Skip to content

Commit

Permalink
Token PKI staging (#60)
Browse files Browse the repository at this point in the history
  • Loading branch information
jessepeterson authored Jun 13, 2024
1 parent 7990d5e commit 4a137c4
Show file tree
Hide file tree
Showing 13 changed files with 229 additions and 49 deletions.
6 changes: 5 additions & 1 deletion docs/operations-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ The `/v1/tokens/{name}` endpoints deal with the raw DEP OAuth tokens in JSON for

For the PUT operation you can supply a "force" URL parameter which will override the matching consumer key check.

The PUT endpoint is discouraged; instead you should perform the full PKI exchange with the "tokenpki" endpoints. If you import only the "raw" OAuth tokens then NanoDEP will not have access to the correct private key for the associated DEP name. This private key is used for some modern DEP operations and won't be possible.

#### Assigner

* Endpoint: `GET, PUT /v1/assigner/{name}`
Expand Down Expand Up @@ -205,7 +207,7 @@ For the DEP "MDM server" in the environment variable $DEP_NAME (see above) this
**The first argument is required** and specifies the path to the token file downloaded from the Apple portal.
This script has one optional argument:
- If you spply a "1" as the second argument it will override ("force" mode) the consumer key check to be able to save a differing consumer key.
- If you supply a "1" as the second argument it will override ("force" mode) the consumer key check to be able to save a differing consumer key.
##### Example usage
Expand Down Expand Up @@ -540,6 +542,8 @@ In "decrypt and decode tokens" mode (that is, by specifying the path to the down
**Note: `deptokens` is not required to use NanoDEP: `depserver` contains this functionality built-in using the tools/scripts (or via the API) directly. See above documentation.**
**Note: `deptokens` is discouraged for use with NanoDEP's `depserver`. The private key and certificate for the PKI exchange is not preserved when only uploading OAuth tokens. Some modern DEP functionality will not be possible. See the note above regarding the Tokens API.**
### Switches
Command line switches for the `deptokens` tool.
Expand Down
29 changes: 25 additions & 4 deletions http/api/tokenpki.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,27 @@ import (
"github.com/micromdm/nanodep/tokenpki"
)

type TokenPKIRetriever interface {
RetrieveTokenPKI(ctx context.Context, name string) (pemCert []byte, pemKey []byte, err error)
type TokenPKIStagingRetriever interface {
RetrieveStagingTokenPKI(ctx context.Context, name string) (pemCert []byte, pemKey []byte, err error)
}

type TokenPKICurrentRetriever interface {
RetrieveCurrentTokenPKI(ctx context.Context, name string) (pemCert []byte, pemKey []byte, err error)
}

type TokenPKIUpstager interface {
UpstageTokenPKI(ctx context.Context, name string) error
}

type TokenPKIStorer interface {
StoreTokenPKI(ctx context.Context, name string, pemCert []byte, pemKey []byte) error
}

type DecryptTokenPKIStorage interface {
TokenPKIStagingRetriever
TokenPKIUpstager
}

// PEMRSAPrivateKey returns key as a PEM block.
func PEMRSAPrivateKey(key *rsa.PrivateKey) []byte {
block := &pem.Block{
Expand Down Expand Up @@ -98,7 +111,7 @@ func GetCertTokenPKIHandler(store TokenPKIStorer, logger log.Logger) http.Handle
// Note the whole URL path is used as the DEP name. This necessitates
// stripping the URL prefix before using this handler. Also note we expose Go
// errors to the output as this is meant for "API" users.
func DecryptTokenPKIHandler(store TokenPKIRetriever, tokenStore AuthTokensStore, logger log.Logger) http.HandlerFunc {
func DecryptTokenPKIHandler(store DecryptTokenPKIStorage, tokenStore AuthTokensStore, logger log.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
logger := ctxlog.Logger(r.Context(), logger)
if r.URL.Path == "" {
Expand All @@ -115,7 +128,7 @@ func DecryptTokenPKIHandler(store TokenPKIRetriever, tokenStore AuthTokensStore,
return
}
defer r.Body.Close()
certBytes, keyBytes, err := store.RetrieveTokenPKI(r.Context(), r.URL.Path)
certBytes, keyBytes, err := store.RetrieveStagingTokenPKI(r.Context(), r.URL.Path)
if err != nil {
logger.Info("msg", "retrieving token keypair", "err", err)
jsonError(w, err)
Expand Down Expand Up @@ -146,6 +159,14 @@ func DecryptTokenPKIHandler(store TokenPKIRetriever, tokenStore AuthTokensStore,
jsonError(w, err)
return
}
// decryption and unmarshal of tokens successful, now "upgrade"
// our staging token PKI to the real thing.
err = store.UpstageTokenPKI(r.Context(), r.URL.Path)
if err != nil {
logger.Info("msg", "upstaging token PKI", "err", err)
jsonError(w, err)
return
}
storeTokens(r.Context(), logger, r.URL.Path, tokens, tokenStore, w, force)
}
}
62 changes: 55 additions & 7 deletions storage/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
Expand Down Expand Up @@ -159,26 +160,73 @@ func (s *FileStorage) StoreCursor(_ context.Context, name, cursor string) error

// StoreTokenPKI stores the PEM bytes in pemCert and pemKey to disk for name DEP name.
func (s *FileStorage) StoreTokenPKI(_ context.Context, name string, pemCert []byte, pemKey []byte) error {
if err := os.WriteFile(s.tokenpkiFilename(name, "cert"), pemCert, 0664); err != nil {
if err := os.WriteFile(s.tokenpkiFilename(name, "staging.cert"), pemCert, 0664); err != nil {
return err
}
if err := os.WriteFile(s.tokenpkiFilename(name, "key"), pemKey, 0664); err != nil {
if err := os.WriteFile(s.tokenpkiFilename(name, "staging.key"), pemKey, 0664); err != nil {
return err
}
return nil
}

// RetrieveTokenPKI reads and returns the PEM bytes for the DEP token exchange
// certificate and private key from disk using name DEP name.
func (s *FileStorage) RetrieveTokenPKI(_ context.Context, name string) ([]byte, []byte, error) {
certBytes, err := os.ReadFile(s.tokenpkiFilename(name, "cert"))
// copyFile non-atomically copies file at path src to file at path dst.
func copyFile(dst, src string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()

dstFile, err := os.Create(dst)
if err != nil {
return err
}
defer dstFile.Close()

_, err = io.Copy(dstFile, srcFile)
return err
}

// UpstageTokenPKI copies the staging PKI certificate and key to the current PKI certificate and key.
// Warning: this operation is not atomic.
func (s *FileStorage) UpstageTokenPKI(ctx context.Context, name string) error {
err := copyFile(
s.tokenpkiFilename(name, "cert"),
s.tokenpkiFilename(name, "staging.cert"),
)
if err != nil {
return err
}
return copyFile(
s.tokenpkiFilename(name, "key"),
s.tokenpkiFilename(name, "staging.key"),
)
}

// RetrieveStagingTokenPKI reads and returns the PEM bytes for the staged
// DEP token exchange certificate and private key from disk using name DEP name.
func (s *FileStorage) RetrieveStagingTokenPKI(ctx context.Context, name string) ([]byte, []byte, error) {
return s.retrieveTokenPKIExtn(name, "staging.")
}

// RetrieveCurrentTokenPKI reads and returns the PEM bytes for the previously-
// upstaged DEP token exchange certificate and private key from disk using
// name DEP name.
func (s *FileStorage) RetrieveCurrentTokenPKI(_ context.Context, name string) ([]byte, []byte, error) {
return s.retrieveTokenPKIExtn(name, "")
}

// retrieveTokenPKIExtn reads and returns the PEM bytes for the DEP token exchange
// certificate and private key from disk using name DEP name and extn type.
func (s *FileStorage) retrieveTokenPKIExtn(name, extn string) ([]byte, []byte, error) {
certBytes, err := os.ReadFile(s.tokenpkiFilename(name, extn+"cert"))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil, fmt.Errorf("%v: %w", err, storage.ErrNotFound)
}
return nil, nil, err
}
keyBytes, err := os.ReadFile(s.tokenpkiFilename(name, "key"))
keyBytes, err := os.ReadFile(s.tokenpkiFilename(name, extn+"key"))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil, fmt.Errorf("%v: %w", err, storage.ErrNotFound)
Expand Down
42 changes: 34 additions & 8 deletions storage/mysql/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,28 +238,54 @@ ON DUPLICATE KEY UPDATE
return err
}

// StoreTokenPKI stores the PEM bytes in pemCert and pemKey for name DEP name.
// StoreTokenPKI stores the staging PEM bytes in pemCert and pemKey for name DEP name.
func (s *MySQLStorage) StoreTokenPKI(ctx context.Context, name string, pemCert []byte, pemKey []byte) error {
_, err := s.db.ExecContext(
ctx, `
INSERT INTO dep_names
(name, tokenpki_cert_pem, tokenpki_key_pem)
(name, tokenpki_staging_cert_pem, tokenpki_staging_key_pem)
VALUES
(?, ?, ?) as new
ON DUPLICATE KEY UPDATE
tokenpki_cert_pem = new.tokenpki_cert_pem,
tokenpki_key_pem = new.tokenpki_key_pem;`,
tokenpki_staging_cert_pem = new.tokenpki_staging_cert_pem,
tokenpki_staging_key_pem = new.tokenpki_staging_key_pem;`,
name,
pemCert,
pemKey,
)
return err
}

// RetrieveTokenPKI reads the PEM bytes for the DEP token exchange certificate
// and private key using name DEP name.
func (s *MySQLStorage) RetrieveTokenPKI(ctx context.Context, name string) (pemCert []byte, pemKey []byte, err error) {
keypair, err := s.q.GetKeypair(ctx, name)
// UpstageTokenPKI copies the staging PKI certificate and private key to the
// current PKI certificate and private key.
func (s *MySQLStorage) UpstageTokenPKI(ctx context.Context, name string) error {
err := s.q.UpstageKeypair(ctx, name)
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("%v: %w", err, storage.ErrNotFound)
}
return err
}

// RetrieveStagingTokenPKI returns the PEM bytes for the staged DEP
// token exchange certificate and private key using name DEP name.
func (s *MySQLStorage) RetrieveStagingTokenPKI(ctx context.Context, name string) ([]byte, []byte, error) {
keypair, err := s.q.GetStagingKeypair(ctx, name)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil, fmt.Errorf("%v: %w", err, storage.ErrNotFound)
}
return nil, nil, err
}
if keypair.TokenpkiStagingCertPem == nil { // tokenpki_staging_cert_pem and tokenpki_staging_key_pem are set together
return nil, nil, fmt.Errorf("empty certificate: %w", storage.ErrNotFound)
}
return keypair.TokenpkiStagingCertPem, keypair.TokenpkiStagingKeyPem, nil
}

// RetrieveCurrentTokenPKI returns the PEM bytes for the previously-upstaged DEP
// token exchange certificate and private key using name DEP name.
func (s *MySQLStorage) RetrieveCurrentTokenPKI(ctx context.Context, name string) (pemCert []byte, pemKey []byte, err error) {
keypair, err := s.q.GetCurrentKeypair(ctx, name)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil, fmt.Errorf("%v: %w", err, storage.ErrNotFound)
Expand Down
20 changes: 19 additions & 1 deletion storage/mysql/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ SELECT config_base_url FROM dep_names WHERE name = ?;
-- name: GetSyncerCursor :one
SELECT syncer_cursor FROM dep_names WHERE name = ?;

-- name: GetKeypair :one
-- name: GetCurrentKeypair :one
SELECT
tokenpki_cert_pem,
tokenpki_key_pem
Expand All @@ -13,6 +13,24 @@ FROM
WHERE
name = ?;

-- name: GetStagingKeypair :one
SELECT
tokenpki_staging_cert_pem,
tokenpki_staging_key_pem
FROM
dep_names
WHERE
name = ?;

-- name: UpstageKeypair :exec
UPDATE
dep_names
SET
tokenpki_cert_pem = tokenpki_staging_cert_pem,
tokenpki_key_pem = tokenpki_staging_key_pem
WHERE
name = ?;

-- name: GetAuthTokens :one
SELECT
consumer_key,
Expand Down
1 change: 1 addition & 0 deletions storage/mysql/schema.00001.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE dep_names ADD COLUMN tokenpki_staging_cert_pem TEXT NULL, ADD COLUMN tokenpki_staging_key_pem TEXT NULL;
6 changes: 4 additions & 2 deletions storage/mysql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ CREATE TABLE dep_names (
config_base_url VARCHAR(255) NULL,

-- Token PKI
tokenpki_cert_pem TEXT NULL,
tokenpki_key_pem TEXT NULL,
tokenpki_cert_pem TEXT NULL,
tokenpki_key_pem TEXT NULL,
tokenpki_staging_cert_pem TEXT NULL,
tokenpki_staging_key_pem TEXT NULL,

-- Syncer
-- From Apple docs: "The string can be up to 1000 characters".
Expand Down
8 changes: 8 additions & 0 deletions storage/mysql/sqlc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ sql:
go_type:
type: "byte"
slice: true
- column: "dep_names.tokenpki_staging_cert_pem"
go_type:
type: "byte"
slice: true
- column: "dep_names.tokenpki_staging_key_pem"
go_type:
type: "byte"
slice: true
- column: "dep_names.access_token_expiry"
go_type:
type: "sql.NullString"
Expand Down
2 changes: 1 addition & 1 deletion storage/mysql/sqlc/db.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 17 additions & 15 deletions storage/mysql/sqlc/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4a137c4

Please sign in to comment.