From ad22a7a9324d32db9fef645ff45e721111a817e5 Mon Sep 17 00:00:00 2001 From: Jesse Peterson Date: Wed, 19 Apr 2017 09:13:27 -0700 Subject: [PATCH] Move dep-token sub-command to mdmctl (#141) --- cmd/mdmctl/apply.go | 34 +++++ cmd/mdmctl/get.go | 88 +++++++++++ cmd/mdmctl/mdmdctl.go | 3 + core/apply/client.go | 10 ++ core/apply/endpoint.go | 30 ++++ core/apply/service.go | 98 +++++++++++++ core/apply/transport_http.go | 50 +++++-- core/list/client.go | 12 +- core/list/endpoint.go | 36 ++++- core/list/service.go | 152 +++++++++++++++++++ core/list/transport_http.go | 40 ++++- deptoken.go | 274 ----------------------------------- main.go | 3 - serve.go | 60 ++++---- 14 files changed, 560 insertions(+), 330 deletions(-) delete mode 100644 deptoken.go diff --git a/cmd/mdmctl/apply.go b/cmd/mdmctl/apply.go index f8bf389a..b1658642 100644 --- a/cmd/mdmctl/apply.go +++ b/cmd/mdmctl/apply.go @@ -1,8 +1,11 @@ package main import ( + "context" + "errors" "flag" "fmt" + "io/ioutil" "os" "strings" @@ -42,6 +45,8 @@ func (cmd *applyCommand) Run(args []string) error { switch strings.ToLower(args[0]) { case "blueprints": run = cmd.applyBlueprint + case "dep-tokens": + run = cmd.applyDEPTokens default: cmd.Usage() os.Exit(1) @@ -56,6 +61,7 @@ Apply a resource. Valid resource types: * blueprints + * dep-tokens Examples: # Get a list of devices @@ -74,3 +80,31 @@ func (cmd *applyCommand) applyBlueprint(args []string) error { } return nil } + +func (cmd *applyCommand) applyDEPTokens(args []string) error { + flagset := flag.NewFlagSet("dep-tokens", flag.ExitOnError) + var ( + flPublicKeyPath = flagset.String("import-token", "", "filename of p7m encrypted token file (downloaded from DEP portal)") + ) + flagset.Usage = usageFor(flagset, "mdmctl apply dep-tokens [flags]") + if err := flagset.Parse(args); err != nil { + return err + } + if *flPublicKeyPath == "" { + return errors.New("must provide -import-token parameter") + } + if _, err := os.Stat(*flPublicKeyPath); os.IsNotExist(err) { + return err + } + p7mBytes, err := ioutil.ReadFile(*flPublicKeyPath) + if err != nil { + return err + } + ctx := context.Background() + err = cmd.applysvc.ApplyDEPToken(ctx, p7mBytes) + if err != nil { + return err + } + fmt.Println("imported DEP token") + return nil +} diff --git a/cmd/mdmctl/get.go b/cmd/mdmctl/get.go index 3a829fed..b30f5859 100644 --- a/cmd/mdmctl/get.go +++ b/cmd/mdmctl/get.go @@ -2,12 +2,16 @@ package main import ( "context" + "encoding/json" "flag" "fmt" "os" "strings" "text/tabwriter" + "crypto/x509" + "encoding/pem" + "github.com/go-kit/kit/log" "github.com/micromdm/micromdm/core/list" ) @@ -46,6 +50,8 @@ func (cmd *getCommand) Run(args []string) error { switch strings.ToLower(args[0]) { case "devices": run = cmd.getDevices + case "dep-tokens": + run = cmd.getDepTokens default: cmd.Usage() os.Exit(1) @@ -62,6 +68,7 @@ Valid resource types: * devices * blueprints + * dep-tokens Examples: # Get a list of devices @@ -104,3 +111,84 @@ func (cmd *getCommand) getDevices(args []string) error { } return nil } + +func (cmd *getCommand) getDepTokens(args []string) error { + flagset := flag.NewFlagSet("dep-tokens", flag.ExitOnError) + var ( + flFullCK = flagset.Bool("v", false, "display full ConsumerKey in summary list") + flPublicKeyPath = flagset.String("export-public-key", "", "filename of public key to write (to be uploaded to deploy.apple.com)") + flTokenPath = flagset.String("export-token", "", "filename to save decrypted oauth token (JSON)") + ) + flagset.Usage = usageFor(flagset, "mdmctl get dep-tokens [flags]") + if err := flagset.Parse(args); err != nil { + return err + } + + w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + fmt.Fprintf(w, "ConsumerKey\tAccessTokenExpiry\n") + ctx := context.Background() + tokens, certBytes, err := cmd.list.GetDEPTokens(ctx) + if err != nil { + return err + } + var ckTrimmed string + for _, t := range tokens { + if len(t.ConsumerKey) > 40 && !*flFullCK { + ckTrimmed = t.ConsumerKey[0:39] + "…" + } else { + ckTrimmed = t.ConsumerKey + } + fmt.Fprintf(w, "%s\t%s\n", ckTrimmed, t.AccessTokenExpiry.String()) + } + w.Flush() + + if *flPublicKeyPath != "" && certBytes != nil { + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + return err + } + err = WritePEMCertificateFile(cert, *flPublicKeyPath) + if err != nil { + return err + } + fmt.Printf("\nWrote DEP public key to: %s\n", *flPublicKeyPath) + } + + if *flTokenPath != "" && len(tokens) > 0 { + t := tokens[0] + + tokenFile, err := os.Create(*flTokenPath) + if err != nil { + return err + } + defer tokenFile.Close() + + err = json.NewEncoder(tokenFile).Encode(t) + if err != nil { + return err + } + + fmt.Printf("\nWrote DEP token JSON to: %s\n", *flTokenPath) + if len(tokens) > 1 { + fmt.Println("WARNING: more than one DEP token returned; only saved first") + } + } + + return nil +} + +// TODO: move into crypto package and use for all main.savePEMCert() invocations +func WritePEMCertificateFile(cert *x509.Certificate, path string) error { + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + + return pem.Encode( + file, + &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }) +} diff --git a/cmd/mdmctl/mdmdctl.go b/cmd/mdmctl/mdmdctl.go index e16cb16b..e14abfd9 100644 --- a/cmd/mdmctl/mdmdctl.go +++ b/cmd/mdmctl/mdmdctl.go @@ -27,6 +27,8 @@ func main() { cmd := &getCommand{} run = cmd.Run case "apply": + cmd := &applyCommand{} + run = cmd.Run default: usage() os.Exit(1) @@ -43,6 +45,7 @@ func usage() error { Available Commands: get + apply config version diff --git a/core/apply/client.go b/core/apply/client.go index 455960a3..92d2e3e2 100644 --- a/core/apply/client.go +++ b/core/apply/client.go @@ -25,9 +25,19 @@ func NewClient(instance string, logger log.Logger, token string) (Service, error DecodeBlueprintRequest, ).Endpoint() } + var applyDEPTokensEndpoint endpoint.Endpoint + { + applyDEPTokensEndpoint = httptransport.NewClient( + "PUT", + copyURL(u, "/v1/dep-tokens"), + encodeRequestWithToken(token, EncodeHTTPGenericRequest), + DecodeDEPTokensRequest, + ).Endpoint() + } return Endpoints{ ApplyBlueprintEndpoint: applyBlueprintEndpoint, + ApplyDEPTokensEndpoint: applyDEPTokensEndpoint, }, nil } diff --git a/core/apply/endpoint.go b/core/apply/endpoint.go index 0b63ddcc..b813a9d1 100644 --- a/core/apply/endpoint.go +++ b/core/apply/endpoint.go @@ -9,6 +9,7 @@ import ( type Endpoints struct { ApplyBlueprintEndpoint endpoint.Endpoint + ApplyDEPTokensEndpoint endpoint.Endpoint } func (e Endpoints) ApplyBlueprint(ctx context.Context, bp *blueprint.Blueprint) error { @@ -20,6 +21,15 @@ func (e Endpoints) ApplyBlueprint(ctx context.Context, bp *blueprint.Blueprint) return resp.(blueprintResponse).Err } +func (e Endpoints) ApplyDEPToken(ctx context.Context, P7MContent []byte) error { + req := depTokensRequest{P7MContent: P7MContent} + resp, err := e.ApplyDEPTokensEndpoint(ctx, req) + if err != nil { + return err + } + return resp.(depTokensResponse).Err +} + func MakeApplyBlueprintEndpoint(svc Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (response interface{}, err error) { req := request.(blueprintRequest) @@ -30,6 +40,16 @@ func MakeApplyBlueprintEndpoint(svc Service) endpoint.Endpoint { } } +func MakeApplyDEPTokensEndpoint(svc Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + req := request.(depTokensRequest) + err = svc.ApplyDEPToken(ctx, req.P7MContent) + return depTokensResponse{ + Err: err, + }, nil + } +} + type blueprintRequest struct { Blueprint *blueprint.Blueprint `json:"blueprint"` } @@ -37,3 +57,13 @@ type blueprintRequest struct { type blueprintResponse struct { Err error `json:"err,omitempty"` } + +type depTokensRequest struct { + P7MContent []byte `json:"p7m_content"` +} + +type depTokensResponse struct { + Err error `json:"err,omitempty"` +} + +func (r depTokensResponse) error() error { return r.Err } diff --git a/core/apply/service.go b/core/apply/service.go index 7b979e48..ecd60084 100644 --- a/core/apply/service.go +++ b/core/apply/service.go @@ -2,18 +2,116 @@ package apply import ( "context" + "fmt" + "bufio" + "bytes" + "encoding/base64" + "encoding/json" + "github.com/fullsailor/pkcs7" + "io" + "net/textproto" + + "github.com/boltdb/bolt" "github.com/micromdm/micromdm/blueprint" + "github.com/micromdm/micromdm/core/list" ) type Service interface { ApplyBlueprint(ctx context.Context, bp *blueprint.Blueprint) error + ApplyDEPToken(ctx context.Context, P7MContent []byte) error } type ApplyService struct { Blueprints *blueprint.DB + DB *bolt.DB // TODO: replace with reference to DEP token svc/pkg } func (svc *ApplyService) ApplyBlueprint(ctx context.Context, bp *blueprint.Blueprint) error { return svc.Blueprints.Save(bp) } + +// unwrapSMIME removes the S/MIME-like wrapper around raw CMS/PKCS7 data +func unwrapSMIME(smime []byte) ([]byte, error) { + tr := textproto.NewReader(bufio.NewReader(bytes.NewReader(smime))) + if _, err := tr.ReadMIMEHeader(); err != nil { + return nil, err + } + dec := base64.NewDecoder(base64.StdEncoding, tr.DotReader()) + buf := new(bytes.Buffer) + io.Copy(buf, dec) + return buf.Bytes(), nil +} + +// unwrapTokenJSON removes the MIME-like headers and text surrounding the DEP token JSON +func unwrapTokenJSON(wrapped []byte) ([]byte, error) { + tr := textproto.NewReader(bufio.NewReader(bytes.NewReader(wrapped))) + if _, err := tr.ReadMIMEHeader(); err != nil { + return nil, err + } + tokenJSON := new(bytes.Buffer) + for { + line, err := tr.ReadLineBytes() + if err != nil && err == io.EOF { + break + } else if err != nil { + return nil, err + } + line = bytes.Trim(line, "-----BEGIN MESSAGE-----") + line = bytes.Trim(line, "-----END MESSAGE-----") + if _, err := tokenJSON.Write(line); err != nil { + return nil, err + } + } + return tokenJSON.Bytes(), nil +} + +// TODO: move into seperate svc/pkg +const ( + depTokenBucket = "mdm.DEPToken" +) + +func PutDEPToken(db *bolt.DB, consumerKey string, json []byte) error { + err := db.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists([]byte(depTokenBucket)) + if err != nil { + return err + } + return b.Put([]byte(consumerKey), json) + }) + return err +} + +func (svc *ApplyService) ApplyDEPToken(ctx context.Context, P7MContent []byte) error { + unwrapped, err := unwrapSMIME(P7MContent) + if err != nil { + return err + } + key, cert, err := list.GetDEPKeypair(svc.DB) + if err != nil { + return err + } + p7, err := pkcs7.Parse(unwrapped) + if err != nil { + return err + } + decrypted, err := p7.Decrypt(cert, key) + if err != nil { + return err + } + tokenJSON, err := unwrapTokenJSON(decrypted) + if err != nil { + return err + } + var depToken list.DEPToken + err = json.Unmarshal(tokenJSON, &depToken) + if err != nil { + return err + } + err = PutDEPToken(svc.DB, depToken.ConsumerKey, tokenJSON) + if err != nil { + return err + } + fmt.Println("stored DEP token with ck", depToken.ConsumerKey) + return nil +} diff --git a/core/apply/transport_http.go b/core/apply/transport_http.go index 1b44ccff..d199156d 100644 --- a/core/apply/transport_http.go +++ b/core/apply/transport_http.go @@ -12,17 +12,37 @@ import ( "github.com/micromdm/micromdm/blueprint" ) -func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) http.Handler { - var h http.Handler - h = httptransport.NewServer( - endpoints.ApplyBlueprintEndpoint, - decodeBlueprintRequest, - encodeResponse, - opts..., - ) +type HTTPHandlers struct { + BlueprintHandler http.Handler + DEPTokensHandler http.Handler +} + +func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers { + h := HTTPHandlers{ + BlueprintHandler: httptransport.NewServer( + endpoints.ApplyBlueprintEndpoint, + decodeBlueprintRequest, + encodeResponse, + opts..., + ), + DEPTokensHandler: httptransport.NewServer( + endpoints.ApplyDEPTokensEndpoint, + decodeDEPTokensRequest, + encodeResponse, + opts..., + ), + } return h } +func decodeDEPTokensRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req depTokensRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return req, nil +} + func decodeBlueprintRequest(ctx context.Context, r *http.Request) (interface{}, error) { var bp blueprint.Blueprint if err := json.NewDecoder(r.Body).Decode(&bp); err != nil { @@ -62,7 +82,10 @@ func encodeResponse(ctx context.Context, w http.ResponseWriter, response interfa } func EncodeError(ctx context.Context, err error, w http.ResponseWriter) { - http.Error(w, err.Error(), http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + enc.Encode(errorWrapper{Error: err.Error()}) } // EncodeHTTPGenericRequest is a transport/http.EncodeRequestFunc that @@ -84,3 +107,12 @@ func DecodeBlueprintRequest(_ context.Context, r *http.Response) (interface{}, e err := json.NewDecoder(r.Body).Decode(&resp) return resp, err } + +func DecodeDEPTokensRequest(_ context.Context, r *http.Response) (interface{}, error) { + if r.StatusCode != http.StatusOK { + return nil, errorDecoder(r) + } + var resp depTokensResponse + err := json.NewDecoder(r.Body).Decode(&resp) + return resp, err +} diff --git a/core/list/client.go b/core/list/client.go index 30d5b90e..787a636d 100644 --- a/core/list/client.go +++ b/core/list/client.go @@ -25,9 +25,19 @@ func NewClient(instance string, logger log.Logger, token string) (Service, error DecodeDevicesResponse, ).Endpoint() } + var getDEPTokensEndpoint endpoint.Endpoint + { + getDEPTokensEndpoint = httptransport.NewClient( + "GET", + copyURL(u, "/v1/dep-tokens"), + encodeRequestWithToken(token, EncodeHTTPGenericRequest), + DecodeGetDEPTokensResponse, + ).Endpoint() + } return Endpoints{ - ListDevicesEndpoint: listDevicesEndpoint, + ListDevicesEndpoint: listDevicesEndpoint, + GetDEPTokensEndpoint: getDEPTokensEndpoint, }, nil } diff --git a/core/list/endpoint.go b/core/list/endpoint.go index 3177c2bd..e1a3e594 100644 --- a/core/list/endpoint.go +++ b/core/list/endpoint.go @@ -8,7 +8,8 @@ import ( ) type Endpoints struct { - ListDevicesEndpoint endpoint.Endpoint + ListDevicesEndpoint endpoint.Endpoint + GetDEPTokensEndpoint endpoint.Endpoint } func (e Endpoints) ListDevices(ctx context.Context, opts ListDevicesOption) ([]DeviceDTO, error) { @@ -20,6 +21,14 @@ func (e Endpoints) ListDevices(ctx context.Context, opts ListDevicesOption) ([]D return response.(devicesResponse).Devices, response.(devicesResponse).Err } +func (e Endpoints) GetDEPTokens(ctx context.Context) ([]DEPToken, []byte, error) { + resp, err := e.GetDEPTokensEndpoint(ctx, nil) + if err != nil { + return nil, nil, err + } + return resp.(depTokenResponse).DEPTokens, resp.(depTokenResponse).DEPPubKey, nil +} + func MakeListDevicesEndpoint(svc Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (response interface{}, err error) { req := request.(devicesRequest) @@ -31,6 +40,17 @@ func MakeListDevicesEndpoint(svc Service) endpoint.Endpoint { } } +func MakeGetDEPTokensEndpoint(svc Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (response interface{}, err error) { + tokens, pubkey, err := svc.GetDEPTokens(ctx) + return depTokenResponse{ + DEPTokens: tokens, + DEPPubKey: pubkey, + Err: err, + }, nil + } +} + type DeviceDTO struct { SerialNumber string `json:"serial_number"` UDID string `json:"udid"` @@ -43,3 +63,17 @@ type devicesResponse struct { Devices []DeviceDTO `json:"devices"` Err error `json:"err,omitempty"` } + +type DEPToken struct { + ConsumerKey string `json:"consumer_key"` + ConsumerSecret string `json:"consumer_secret"` + AccessToken string `json:"access_token"` + AccessSecret string `json:"access_secret"` + AccessTokenExpiry time.Time `json:"access_token_expiry"` +} + +type depTokenResponse struct { + DEPTokens []DEPToken `json:"dep_tokens"` + DEPPubKey []byte `json:"public_key"` + Err error `json:"err,omitempty"` +} diff --git a/core/list/service.go b/core/list/service.go index 47228761..3e59f75a 100644 --- a/core/list/service.go +++ b/core/list/service.go @@ -1,7 +1,18 @@ package list import ( + "bytes" "context" + "encoding/json" + "time" + + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + + "github.com/boltdb/bolt" "github.com/micromdm/micromdm/device" ) @@ -16,10 +27,12 @@ type ListDevicesOption struct { type Service interface { ListDevices(ctx context.Context, opt ListDevicesOption) ([]DeviceDTO, error) + GetDEPTokens(ctx context.Context) ([]DEPToken, []byte, error) } type ListService struct { Devices *device.DB + DB *bolt.DB // TODO: replace with reference to DEP token svc/pkg } func (svc *ListService) ListDevices(ctx context.Context, opt ListDevicesOption) ([]DeviceDTO, error) { @@ -35,3 +48,142 @@ func (svc *ListService) ListDevices(ctx context.Context, opt ListDevicesOption) } return dto, err } + +// TODO: move into seperate svc/pkg +const ( + depTokenBucket = "mdm.DEPToken" +) + +// TODO: move into seperate svc/pkg +func GetDEPTokens(db *bolt.DB) ([]DEPToken, error) { + var result []DEPToken + err := db.View(func(tx *bolt.Tx) error { + c := tx.Bucket([]byte(depTokenBucket)).Cursor() + + prefix := []byte("CK_") + for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() { + var depToken DEPToken + err := json.Unmarshal(v, &depToken) + if err != nil { + // TODO: log problematic DEP token, or remove altogether? + continue + } + result = append(result, depToken) + } + return nil + }) + return result, err +} + +// TODO: move into seperate svc/pkg +func generateAndStoreDEPKeypair(db *bolt.DB) (key *rsa.PrivateKey, cert *x509.Certificate, err error) { + key, cert, err = SimpleSelfSignedRSAKeypair("micromdm-dep-token", 365) + if err != nil { + return + } + + pkBytes := x509.MarshalPKCS1PrivateKey(key) + certBytes := cert.Raw + + err = db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(depTokenBucket)) + err := b.Put([]byte("key"), pkBytes) + if err != nil { + return err + } + err = b.Put([]byte("certificate"), certBytes) + if err != nil { + return err + } + return nil + }) + + return +} + +// TODO: move into seperate svc/pkg +func GetDEPKeypair(db *bolt.DB) (key *rsa.PrivateKey, cert *x509.Certificate, err error) { + var keyBytes, certBytes []byte + err = db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(depTokenBucket)) + keyBytes = b.Get([]byte("key")) + certBytes = b.Get([]byte("certificate")) + return nil + }) + if err != nil { + return + } + if keyBytes == nil || certBytes == nil { + // if there is no certificate or private key then generate + key, cert, err = generateAndStoreDEPKeypair(db) + } else { + key, err = x509.ParsePKCS1PrivateKey(keyBytes) + if err != nil { + return + } + cert, err = x509.ParseCertificate(certBytes) + if err != nil { + return + } + } + return +} + +func (svc *ListService) GetDEPTokens(ctx context.Context) ([]DEPToken, []byte, error) { + _, cert, err := GetDEPKeypair(svc.DB) + if err != nil { + return nil, nil, err + } + var certBytes []byte + if cert != nil { + certBytes = cert.Raw + } + + tokens, err := GetDEPTokens(svc.DB) + if err != nil { + return nil, certBytes, err + } + + return tokens, certBytes, nil +} + +// TODO: move into crypto package +func RandomCertificateSerialNumber() (*big.Int, error) { + limit := new(big.Int).Lsh(big.NewInt(1), 128) + return rand.Int(rand.Reader, limit) +} + +// TODO: move into crypto package +func SimpleSelfSignedRSAKeypair(cn string, days int) (key *rsa.PrivateKey, cert *x509.Certificate, err error) { + key, err = rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return + } + + serialNumber, err := RandomCertificateSerialNumber() + if err != nil { + return + } + timeNow := time.Now() + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: cn, + }, + NotBefore: timeNow, + NotAfter: timeNow.Add(time.Duration(days) * 24 * time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + if err != nil { + return + } + cert, err = x509.ParseCertificate(certBytes) + if err != nil { + return + } + + return +} diff --git a/core/list/transport_http.go b/core/list/transport_http.go index 046df124..9ffad3a8 100644 --- a/core/list/transport_http.go +++ b/core/list/transport_http.go @@ -11,17 +11,32 @@ import ( httptransport "github.com/go-kit/kit/transport/http" ) -func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) http.Handler { - var h http.Handler - h = httptransport.NewServer( - endpoints.ListDevicesEndpoint, - decodeListDevicesRequest, - encodeResponse, - opts..., - ) +type HTTPHandlers struct { + ListDevicesHandler http.Handler + GetDEPTokensHandler http.Handler +} + +func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers { + h := HTTPHandlers{ + ListDevicesHandler: httptransport.NewServer( + endpoints.ListDevicesEndpoint, + decodeListDevicesRequest, + encodeResponse, + opts..., + ), + GetDEPTokensHandler: httptransport.NewServer( + endpoints.GetDEPTokensEndpoint, + decodeGetDEPTokensRequest, + encodeResponse, + opts...), + } return h } +func decodeGetDEPTokensRequest(ctx context.Context, r *http.Request) (interface{}, error) { + return nil, nil +} + func decodeListDevicesRequest(ctx context.Context, r *http.Request) (interface{}, error) { req := devicesRequest{ Opts: ListDevicesOption{}, @@ -79,3 +94,12 @@ func DecodeDevicesResponse(_ context.Context, r *http.Response) (interface{}, er err := json.NewDecoder(r.Body).Decode(&resp) return resp, err } + +func DecodeGetDEPTokensResponse(_ context.Context, r *http.Response) (interface{}, error) { + if r.StatusCode != http.StatusOK { + return nil, errorDecoder(r) + } + var resp depTokenResponse + err := json.NewDecoder(r.Body).Decode(&resp) + return resp, err +} diff --git a/deptoken.go b/deptoken.go deleted file mode 100644 index 395d76f6..00000000 --- a/deptoken.go +++ /dev/null @@ -1,274 +0,0 @@ -package main - -import ( - "bufio" - "bytes" - "encoding/base64" - "encoding/json" - "flag" - "fmt" - "io" - "io/ioutil" - "net/textproto" - "os" - "path" - "time" - - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "math/big" - - "github.com/boltdb/bolt" - "github.com/fullsailor/pkcs7" - "github.com/micromdm/dep" - "github.com/pkg/errors" -) - -const ( - depTokenRSAKeyFilename = "deptoken.key" - depTokenCertFilename = "deptoken.pem" - depTokenBucket = "mdm.DEPToken" -) - -type DEPTokenJSON struct { - ConsumerKey string `json:"consumer_key"` - ConsumerSecret string `json:"consumer_secret"` - AccessToken string `json:"access_token"` - AccessSecret string `json:"access_secret"` - AccessTokenExpiry time.Time `json:"access_token_expiry"` -} - -func depToken(args []string) error { - flagset := flag.NewFlagSet("dep-token", flag.ExitOnError) - var ( - flPublicKey = flagset.String("export-public-key", "", "filename of public key to write (to be uploaded to deploy.apple.com)") - flImportToken = flagset.String("import-token", "", "filename of p7m encrypted token file (downloaded from DEP portal)") - flExportToken = flagset.String("export-token", "", "filename to save decrypted oauth token JSON") - ) - flagset.Usage = usageFor(flagset, "micromdm dep-token [flags]") - if err := flagset.Parse(args); err != nil { - return err - } - - keyPath := path.Join(configDBPath, depTokenRSAKeyFilename) - var pk *rsa.PrivateKey - if _, err := os.Stat(keyPath); os.IsNotExist(err) { - // key doesn't yet exist, make it - pk, err = rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return err - } - err = savePEMKey(keyPath, pk) - if err != nil { - return err - } - } else { - // key exists, load it - pemKey, err := ioutil.ReadFile(keyPath) - if err != nil { - return err - } - block, _ := pem.Decode(pemKey) - - if block == nil || block.Type != "RSA PRIVATE KEY" { - return errors.New("invalid DEP token private key") - } - - if pk, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil { - return err - } - - // fmt.Println("loaded key", keyPath) - } - - certPath := path.Join(configDBPath, depTokenCertFilename) - var cert []byte - if _, err := os.Stat(certPath); os.IsNotExist(err) { - // cert doesn't yet exist, make it - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - - template := x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - CommonName: "micromdm-dep-token", - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(365 * 24 * time.Hour), - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - } - - cert, err := x509.CreateCertificate(rand.Reader, &template, &template, &pk.PublicKey, pk) - if err != nil { - return err - } - - certOut, err := os.Create(certPath) - pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert}) - certOut.Close() - - // fmt.Println("generated and saved cert", certPath) - } else { - // cert exists, load it - pemCert, err := ioutil.ReadFile(certPath) - if err != nil { - return err - } - block, _ := pem.Decode(pemCert) - - if block == nil || block.Type != "CERTIFICATE" { - return errors.New("invalid DEP token cert") - } - - cert = block.Bytes - - if _, err = x509.ParseCertificate(cert); err != nil { - return err - } - - } - - if *flPublicKey == "" && *flImportToken == "" && *flExportToken == "" { - flagset.Usage() - return nil - } - - if *flPublicKey != "" { - if _, err := os.Stat(certPath); os.IsExist(err) { - return errors.New("public key filename already exists, please choose another") - } - certOut, err := os.Create(*flPublicKey) - if err != nil { - return err - } - defer certOut.Close() - if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil { - return err - } - fmt.Println("wrote", *flPublicKey) - } - - if *flImportToken != "" { - f, err := os.Open(*flImportToken) - if err != nil { - return err - } - defer f.Close() - - tr := textproto.NewReader(bufio.NewReader(f)) - if _, err := tr.ReadMIMEHeader(); err != nil { - return err - } - dec := base64.NewDecoder(base64.StdEncoding, tr.DotReader()) - buf := new(bytes.Buffer) - io.Copy(buf, dec) - p7, err := pkcs7.Parse(buf.Bytes()) - if err != nil { - return err - } - - parsedCert, err := x509.ParseCertificate(cert) - if err != nil { - return err - } - - decrypted, err := p7.Decrypt(parsedCert, pk) - if err != nil { - return err - } - - // the contained decrypted data is also wrapped in a textproto-like - // wrapper. strip it, too. - - tr = textproto.NewReader(bufio.NewReader(bytes.NewReader(decrypted))) - if _, err := tr.ReadMIMEHeader(); err != nil { - return err - } - tokenJSON := new(bytes.Buffer) - for { - line, err := tr.ReadLineBytes() - if err != nil && err == io.EOF { - break - } else if err != nil { - return err - } - line = bytes.Trim(line, "-----BEGIN MESSAGE-----") - line = bytes.Trim(line, "-----END MESSAGE-----") - if _, err := tokenJSON.Write(line); err != nil { - return err - } - } - - var depToken DEPTokenJSON - err = json.Unmarshal(tokenJSON.Bytes(), &depToken) - if err != nil { - return err - } - - // copy over values - depConfig := &dep.Config{} - depConfig.ConsumerKey = depToken.ConsumerKey - depConfig.ConsumerSecret = depToken.ConsumerSecret - depConfig.AccessToken = depToken.AccessToken - depConfig.AccessSecret = depToken.AccessSecret - - sm := &config{} - sm.setupBolt() - if sm.err != nil { - return sm.err - } - - err = sm.db.Update(func(tx *bolt.Tx) error { - b, err := tx.CreateBucketIfNotExists([]byte(depTokenBucket)) - if err != nil { - return err - } - return b.Put([]byte(depConfig.ConsumerKey), tokenJSON.Bytes()) - }) - if err != nil { - return err - } - fmt.Println("saved token", depConfig.ConsumerKey) - } - - if *flExportToken != "" { - sm := &config{} - sm.setupBolt() - if sm.err != nil { - return sm.err - } - - err := sm.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte(depTokenBucket)) - if b == nil { - fmt.Println("no DEP server token found. using depsim") - return nil - } - _, v := b.Cursor().First() - if v == nil { - return errors.New("no dep token found. did you import it?") - } - f, err := os.Create(*flExportToken) - if err != nil { - return errors.Wrap(err, "create file to save DEP token") - } - defer f.Close() - if _, err := f.Write(v); err != nil { - return errors.Wrap(err, "saving DEP token JSON") - } - return nil - }) - if err != nil { - return err - - } - fmt.Println("saved oauth token file") - } - - return nil -} diff --git a/main.go b/main.go index bc1e1b47..a38cf9a0 100644 --- a/main.go +++ b/main.go @@ -28,8 +28,6 @@ func main() { return case "serve": run = serve - case "dep-token": - run = depToken case "get": run = getResource case "apply": @@ -55,7 +53,6 @@ Available Commands: dev get apply - dep-token version Use micromdm -h for additional usage of each command. diff --git a/serve.go b/serve.go index 3a34028a..4dd9c76c 100644 --- a/serve.go +++ b/serve.go @@ -8,7 +8,6 @@ import ( "crypto/x509" "encoding/asn1" "encoding/base64" - "encoding/json" "encoding/pem" "flag" "fmt" @@ -193,7 +192,7 @@ func serve(args []string) error { var listsvc list.Service { - listsvc = &list.ListService{Devices: devDB} + listsvc = &list.ListService{Devices: devDB, DB: sm.db} } var listDevicesEndpoint endpoint.Endpoint { @@ -201,12 +200,13 @@ func serve(args []string) error { } listEndpoints := list.Endpoints{ - ListDevicesEndpoint: listDevicesEndpoint, + ListDevicesEndpoint: listDevicesEndpoint, + GetDEPTokensEndpoint: list.MakeGetDEPTokensEndpoint(listsvc), } var applysvc apply.Service { - applysvc = &apply.ApplyService{Blueprints: bpDB} + applysvc = &apply.ApplyService{Blueprints: bpDB, DB: sm.db} } var applyBlueprintEndpoint endpoint.Endpoint @@ -216,6 +216,7 @@ func serve(args []string) error { applyEndpoints := apply.Endpoints{ ApplyBlueprintEndpoint: applyBlueprintEndpoint, + ApplyDEPTokensEndpoint: apply.MakeApplyDEPTokensEndpoint(applysvc), } applyAPIHandlers := apply.MakeHTTPHandlers(ctx, applyEndpoints, connectOpts...) @@ -242,8 +243,10 @@ func serve(args []string) error { // API commands. Only handled if the user provides an api key. if *flAPIKey != "" { - r.Handle("/v1/devices", apiAuthMiddleware(*flAPIKey, listAPIHandlers)).Methods("GET") - r.Handle("/v1/blueprints", apiAuthMiddleware(*flAPIKey, applyAPIHandlers)).Methods("PUT") + r.Handle("/v1/devices", apiAuthMiddleware(*flAPIKey, listAPIHandlers.ListDevicesHandler)).Methods("GET") + r.Handle("/v1/dep-tokens", apiAuthMiddleware(*flAPIKey, listAPIHandlers.GetDEPTokensHandler)).Methods("GET") + r.Handle("/v1/dep-tokens", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.DEPTokensHandler)).Methods("PUT") + r.Handle("/v1/blueprints", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.BlueprintHandler)).Methods("PUT") } if *flRepoPath != "" { @@ -601,6 +604,22 @@ func (c *config) depClient() (dep.Client, error) { // depsim config depsim := c.depsim var conf *dep.Config + + // try getting the oauth config from bolt + tokens, err := list.GetDEPTokens(c.db) + if err != nil { + return nil, err + } + if len(tokens) >= 1 { + conf = new(dep.Config) + conf.ConsumerSecret = tokens[0].ConsumerSecret + conf.ConsumerKey = tokens[0].ConsumerKey + conf.AccessSecret = tokens[0].AccessSecret + conf.AccessToken = tokens[0].AccessToken + // TODO: handle expiration + } + + // override with depsim keys if specified on CLI if depsim { conf = &dep.Config{ ConsumerKey: "CK_48dd68d198350f51258e885ce9a5c37ab7f98543c4a697323d75682a6c10a32501cb247e3db08105db868f73f2c972bdb6ae77112aea803b9219eb52689d42e6", @@ -610,40 +629,13 @@ func (c *config) depClient() (dep.Client, error) { } } - // try getting the oauth config from bolt - - err := c.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte(depTokenBucket)) - if b == nil { - return nil - } - _, v := b.Cursor().First() - if v == nil { - return nil - } - var token DEPTokenJSON - err := json.Unmarshal(v, &token) - if err != nil { - return err - } - conf = new(dep.Config) - conf.ConsumerSecret = token.ConsumerSecret - conf.ConsumerKey = token.ConsumerKey - conf.AccessSecret = token.AccessSecret - conf.AccessToken = token.AccessToken - // TODO handle expiration. - return nil - }) - if err != nil { - return nil, err - } - if conf == nil { return nil, nil } depServerURL := "https://mdmenrollment.apple.com" if depsim { + // TODO: support supplied depsim URL depServerURL = "http://dep.micromdm.io:9000" } client, err := dep.NewClient(conf, dep.ServerURL(depServerURL))