From 7d902cb588b0c9af8fce7a4e3311eb6f983293a1 Mon Sep 17 00:00:00 2001 From: Jesse Peterson Date: Wed, 12 Jul 2017 17:09:36 -0400 Subject: [PATCH] Re-implement enrollment and OTA profile customization (#214) --- enroll/endpoint.go | 27 +++++----- enroll/service.go | 107 ++++++++++++++++++++++++++++++--------- enroll/transport_http.go | 23 ++++----- serve.go | 20 +++++--- 4 files changed, 119 insertions(+), 58 deletions(-) diff --git a/enroll/endpoint.go b/enroll/endpoint.go index 760d1ee8..677c40d2 100644 --- a/enroll/endpoint.go +++ b/enroll/endpoint.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/micromdm/micromdm/crypto" + "github.com/micromdm/micromdm/profile" boltdepot "github.com/micromdm/scep/depot/bolt" "github.com/fullsailor/pkcs7" @@ -44,8 +45,8 @@ type otaEnrollmentRequest struct { type mdmEnrollRequest struct{} -type mdmEnrollResponse struct { - Profile +type mobileconfigResponse struct { + profile.Mobileconfig Err error `plist:"error,omitempty"` } @@ -71,12 +72,12 @@ func MakeGetEnrollEndpoint(s Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { switch req := request.(type) { case mdmEnrollRequest: - profile, err := s.Enroll(ctx) - return mdmEnrollResponse{profile, err}, nil + mc, err := s.Enroll(ctx) + return mobileconfigResponse{mc, err}, nil case depEnrollmentRequest: fmt.Printf("got DEP enrollment request from %s\n", req.Serial) - profile, err := s.Enroll(ctx) - return mdmEnrollResponse{profile, err}, nil + mc, err := s.Enroll(ctx) + return mobileconfigResponse{mc, err}, nil default: return nil, errors.New("unknown enrollment type") } @@ -85,8 +86,8 @@ func MakeGetEnrollEndpoint(s Service) endpoint.Endpoint { func MakeOTAEnrollEndpoint(s Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - payload, err := s.OTAEnroll(ctx) - return mdmOTAEnrollResponse{payload, err}, nil + mc, err := s.OTAEnroll(ctx) + return mobileconfigResponse{mc, err}, nil } } @@ -107,8 +108,8 @@ func MakeOTAPhase2Phase3Endpoint(s Service, scepDepot *boltdepot.Depot) endpoint // signing certificate is signed by the Apple Device CA. this means // we don't yet have a SCEP identity and thus are in Phase 2 of the // OTA enrollment - profile, err := s.OTAPhase2(ctx) - return mdmEnrollResponse{profile, err}, nil + mc, err := s.OTAPhase2(ctx) + return mobileconfigResponse{mc, err}, nil } caChain, _, err := scepDepot.CA(nil) @@ -133,10 +134,10 @@ func MakeOTAPhase2Phase3Endpoint(s Service, scepDepot *boltdepot.Depot) endpoint // TODO: the SCEP CA checking ought to be more robust // see: https://github.com/micromdm/scep/issues/32 - profile, err := s.Enroll(ctx) + mc, err := s.Enroll(ctx) // profile, err := s.OTAPhase3(ctx) - return mdmEnrollResponse{profile, err}, nil + return mobileconfigResponse{mc, err}, nil } - return mdmEnrollResponse{Profile{}, errors.New("unauthorized client")}, nil + return mobileconfigResponse{profile.Mobileconfig{}, errors.New("unauthorized client")}, nil } } diff --git a/enroll/service.go b/enroll/service.go index 99f93d30..d4bbb558 100644 --- a/enroll/service.go +++ b/enroll/service.go @@ -1,20 +1,30 @@ package enroll import ( + "bytes" "crypto/x509" - "golang.org/x/net/context" "io/ioutil" "strings" + + "github.com/groob/plist" + "golang.org/x/net/context" + + "github.com/micromdm/micromdm/profile" +) + +const ( + EnrollmentProfileId string = "com.github.micromdm.micromdm.enroll" + OTAProfileId string = "com.github.micromdm.micromdm.ota" ) type Service interface { - Enroll(ctx context.Context) (Profile, error) - OTAEnroll(ctx context.Context) (Payload, error) - OTAPhase2(ctx context.Context) (Profile, error) - OTAPhase3(ctx context.Context) (Profile, error) + Enroll(ctx context.Context) (profile.Mobileconfig, error) + OTAEnroll(ctx context.Context) (profile.Mobileconfig, error) + OTAPhase2(ctx context.Context) (profile.Mobileconfig, error) + OTAPhase3(ctx context.Context) (profile.Mobileconfig, error) } -func NewService(pushTopic, caCertPath, scepURL, scepChallenge, url, tlsCertPath, scepSubject string) (Service, error) { +func NewService(pushTopic, caCertPath, scepURL, scepChallenge, url, tlsCertPath, scepSubject string, profileDB *profile.DB) (Service, error) { var caCert, tlsCert []byte var err error @@ -57,6 +67,7 @@ func NewService(pushTopic, caCertPath, scepURL, scepChallenge, url, tlsCertPath, Topic: pushTopic, CACert: caCert, TLSCert: tlsCert, + ProfileDB: profileDB, }, nil } @@ -68,11 +79,51 @@ type service struct { Topic string // APNS Topic for MDM notifications CACert []byte TLSCert []byte + ProfileDB *profile.DB } -func (svc service) Enroll(ctx context.Context) (Profile, error) { +func profileOrPayloadFromFunc(f interface{}) (interface{}, error) { + fPayload, ok := f.(func() (Payload, error)) + if !ok { + fProfile := f.(func() (Profile, error)) + return fProfile() + } + return fPayload() +} + +func profileOrPayloadToMobileconfig(in interface{}) (profile.Mobileconfig, error) { + if _, ok := in.(Payload); !ok { + _ = in.(Profile) + } + buf := new(bytes.Buffer) + enc := plist.NewEncoder(buf) + enc.Indent(" ") + err := enc.Encode(in) + return buf.Bytes(), err +} + +func (svc service) findOrMakeMobileconfig(id string, f interface{}) (profile.Mobileconfig, error) { + p, err := svc.ProfileDB.ProfileById(id) + if err != nil { + if profile.IsNotFound(err) { + profile, err := profileOrPayloadFromFunc(f) + if err != nil { + return nil, err + } + return profileOrPayloadToMobileconfig(profile) + } + return nil, err + } + return p.Mobileconfig, nil +} + +func (svc service) Enroll(ctx context.Context) (profile.Mobileconfig, error) { + return svc.findOrMakeMobileconfig(EnrollmentProfileId, svc.MakeEnrollmentProfile) +} + +func (svc service) MakeEnrollmentProfile() (Profile, error) { profile := NewProfile() - profile.PayloadIdentifier = "com.github.micromdm.micromdm.mdm" + profile.PayloadIdentifier = EnrollmentProfileId profile.PayloadOrganization = "MicroMDM" profile.PayloadDisplayName = "Enrollment Profile" profile.PayloadDescription = "The server may alter your settings" @@ -81,7 +132,7 @@ func (svc service) Enroll(ctx context.Context) (Profile, error) { mdmPayload := NewPayload("com.apple.mdm") mdmPayload.PayloadDescription = "Enrolls with the MDM server" mdmPayload.PayloadOrganization = "MicroMDM" - mdmPayload.PayloadIdentifier = "com.github.micromdm.mdm" + mdmPayload.PayloadIdentifier = EnrollmentProfileId + ".mdm" mdmPayload.PayloadScope = "System" mdmPayloadContent := MDMPayloadContent{ @@ -99,9 +150,9 @@ func (svc service) Enroll(ctx context.Context) (Profile, error) { if svc.SCEPURL != "" { scepContent := SCEPPayloadContent{ URL: svc.SCEPURL, - Keysize: 1024, + Keysize: 2048, KeyType: "RSA", - KeyUsage: 0, + KeyUsage: int(x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment), Name: "Device Management Identity Certificate", Subject: svc.SCEPSubject, } @@ -113,7 +164,7 @@ func (svc service) Enroll(ctx context.Context) (Profile, error) { scepPayload := NewPayload("com.apple.security.scep") scepPayload.PayloadDescription = "Configures SCEP" scepPayload.PayloadDisplayName = "SCEP" - scepPayload.PayloadIdentifier = "com.github.micromdm.scep" + scepPayload.PayloadIdentifier = EnrollmentProfileId + ".scep" scepPayload.PayloadOrganization = "MicroMDM" scepPayload.PayloadContent = scepContent scepPayload.PayloadScope = "System" @@ -128,7 +179,7 @@ func (svc service) Enroll(ctx context.Context) (Profile, error) { caPayload := NewPayload("com.apple.security.root") caPayload.PayloadDisplayName = "Root certificate for MicroMDM" caPayload.PayloadDescription = "Installs the root CA certificate for MicroMDM" - caPayload.PayloadIdentifier = "com.github.micromdm.ssl.ca" + caPayload.PayloadIdentifier = EnrollmentProfileId + ".cert.ca" caPayload.PayloadContent = svc.CACert payloadContent = append(payloadContent, *caPayload) @@ -139,7 +190,7 @@ func (svc service) Enroll(ctx context.Context) (Profile, error) { tlsPayload := NewPayload("com.apple.security.pem") tlsPayload.PayloadDisplayName = "Self-signed TLS certificate for MicroMDM" tlsPayload.PayloadDescription = "Installs the TLS certificate for MicroMDM" - tlsPayload.PayloadIdentifier = "com.github.micromdm.tls" + tlsPayload.PayloadIdentifier = EnrollmentProfileId + ".cert.selfsigned" tlsPayload.PayloadContent = svc.TLSCert payloadContent = append(payloadContent, *tlsPayload) @@ -151,9 +202,13 @@ func (svc service) Enroll(ctx context.Context) (Profile, error) { } // OTAEnroll returns an Over-the-Air "Profile Service" Payload for enrollment. -func (svc service) OTAEnroll(ctx context.Context) (Payload, error) { +func (svc service) OTAEnroll(ctx context.Context) (profile.Mobileconfig, error) { + return svc.findOrMakeMobileconfig(OTAProfileId, svc.MakeOTAEnrollPayload) +} + +func (svc service) MakeOTAEnrollPayload() (Payload, error) { payload := NewPayload("Profile Service") - payload.PayloadIdentifier = "com.github.micromdm.ota.profile-service" + payload.PayloadIdentifier = OTAProfileId payload.PayloadDisplayName = "MicroMDM Profile Service" payload.PayloadDescription = "Profile Service enrollment" payload.PayloadOrganization = "MicroMDM" @@ -168,20 +223,24 @@ func (svc service) OTAEnroll(ctx context.Context) (Payload, error) { } // OTAPhase2 returns a SCEP Profile for use in phase 2 of Over-the-Air enrollment. -func (svc service) OTAPhase2(ctx context.Context) (Profile, error) { +func (svc service) OTAPhase2(ctx context.Context) (profile.Mobileconfig, error) { + return svc.findOrMakeMobileconfig(OTAProfileId+".phase2", svc.MakeOTAPhase2Profile) +} + +func (svc service) MakeOTAPhase2Profile() (Profile, error) { profile := NewProfile() - profile.PayloadIdentifier = "com.github.micromdm.micromdm.ota-scep" + profile.PayloadIdentifier = OTAProfileId + ".phase2" profile.PayloadOrganization = "MicroMDM" - profile.PayloadDisplayName = "Enrollment Profile" + profile.PayloadDisplayName = "OTA Phase 2" profile.PayloadDescription = "The server may alter your settings" profile.PayloadScope = "System" scepContent := SCEPPayloadContent{ URL: svc.SCEPURL, - Keysize: 1024, + Keysize: 2048, // NOTE: OTA docs recommend 1024 KeyType: "RSA", KeyUsage: int(x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment), - Name: "Device Management Identity Certificate", + Name: "OTA Phase 2 Certificate", Subject: svc.SCEPSubject, } @@ -192,7 +251,7 @@ func (svc service) OTAPhase2(ctx context.Context) (Profile, error) { scepPayload := NewPayload("com.apple.security.scep") scepPayload.PayloadDescription = "Configures SCEP" scepPayload.PayloadDisplayName = "SCEP" - scepPayload.PayloadIdentifier = "com.github.micromdm.scep" + scepPayload.PayloadIdentifier = OTAProfileId + ".phase2.scep" scepPayload.PayloadOrganization = "MicroMDM" scepPayload.PayloadContent = scepContent scepPayload.PayloadScope = "System" @@ -207,6 +266,6 @@ func (svc service) OTAPhase2(ctx context.Context) (Profile, error) { // enrollment process. In our case this would probably be a device-specifc // MDM enrollment payload. // TODO: Not implemented. -func (svc service) OTAPhase3(ctx context.Context) (Profile, error) { - return Profile{}, nil +func (svc service) OTAPhase3(ctx context.Context) (profile.Mobileconfig, error) { + return profile.Mobileconfig{}, nil } diff --git a/enroll/transport_http.go b/enroll/transport_http.go index c2492a63..2b9b3396 100644 --- a/enroll/transport_http.go +++ b/enroll/transport_http.go @@ -26,26 +26,26 @@ func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptran EnrollHandler: httptransport.NewServer( endpoints.GetEnrollEndpoint, decodeMDMEnrollRequest, - encodeResponse, + encodeMobileconfigResponse, opts..., ), OTAEnrollHandler: httptransport.NewServer( endpoints.OTAEnrollEndpoint, - nilRequest, - encodeResponse, + decodeEmptyRequest, + encodeMobileconfigResponse, opts..., ), OTAPhase2Phase3Handler: httptransport.NewServer( endpoints.OTAPhase2Phase3Endpoint, decodeOTAPhase2Phase3Request, - encodeResponse, + encodeMobileconfigResponse, opts..., ), } return h } -func nilRequest(_ context.Context, _ *http.Request) (interface{}, error) { +func decodeEmptyRequest(_ context.Context, _ *http.Request) (interface{}, error) { return nil, nil } @@ -85,16 +85,11 @@ func decodeMDMEnrollRequest(_ context.Context, r *http.Request) (interface{}, er } } -func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { +func encodeMobileconfigResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { w.Header().Set("Content-Type", "application/x-apple-aspen-config") - - enc := plist.NewEncoder(w) - enc.Indent(" ") - if err := enc.Encode(response); err != nil { - return err - } - - return nil + mcResp := response.(mobileconfigResponse) + _, err := w.Write(mcResp.Mobileconfig) + return err } func decodeOTAPhase2Phase3Request(_ context.Context, r *http.Request) (interface{}, error) { diff --git a/serve.go b/serve.go index fb7b81b2..1c47a2e4 100644 --- a/serve.go +++ b/serve.go @@ -142,7 +142,6 @@ func serve(args []string) error { sm.setupBolt() sm.loadPushCerts() sm.setupSCEP(logger) - sm.setupEnrollmentService() sm.setupCheckinService() sm.setupPushService() sm.setupCommandService() @@ -157,12 +156,17 @@ func serve(args []string) error { stdlog.Fatal(err) } - profDB, err := profile.NewDB(sm.db) + sm.profileDB, err = profile.NewDB(sm.db) if err != nil { stdlog.Fatal(err) } - bpDB, err := blueprint.NewDB(sm.db, profDB) + sm.setupEnrollmentService() + if sm.err != nil { + stdlog.Fatalf("enrollment service: %s", sm.err) + } + + bpDB, err := blueprint.NewDB(sm.db, sm.profileDB) if err != nil { stdlog.Fatal(err) } @@ -212,7 +216,7 @@ func serve(args []string) error { dc, err := sm.depClient() if err != nil { - stdlog.Fatalf("creating DEP client %s\n", err) + stdlog.Fatalf("creating DEP client: %s\n", err) } tokenDB := &deptoken.DB{DB: sm.db, Publisher: sm.pubclient} appDB := &appstore.Repo{Path: *flRepoPath} @@ -223,7 +227,7 @@ func serve(args []string) error { Devices: devDB, Tokens: tokenDB, Blueprints: bpDB, - Profiles: profDB, + Profiles: sm.profileDB, Apps: appDB, } listsvc = l @@ -254,7 +258,7 @@ func serve(args []string) error { DEPClient: dc, Blueprints: bpDB, Tokens: tokenDB, - Profiles: profDB, + Profiles: sm.profileDB, Apps: appDB, } applysvc = l @@ -295,7 +299,7 @@ func serve(args []string) error { listAPIHandlers := list.MakeHTTPHandlers(ctx, listEndpoints, connectOpts...) - rmsvc := &remove.RemoveService{Blueprints: bpDB, Profiles: profDB} + rmsvc := &remove.RemoveService{Blueprints: bpDB, Profiles: sm.profileDB} removeAPIHandlers := remove.MakeHTTPHandlers(ctx, remove.MakeEndpoints(rmsvc), connectOpts...) connectHandlers := connect.MakeHTTPHandlers(ctx, connectEndpoints, connectOpts...) @@ -484,6 +488,7 @@ type config struct { APNSPrivateKeyPass string tlsCertPath string scepDepot *boltdepot.Depot + profileDB *profile.DB // TODO: refactor enroll service and remove the need to reference // this on-disk cert. but it might be useful to keep the PEM @@ -661,6 +666,7 @@ func (c *config) setupEnrollmentService() { c.ServerPublicURL, c.tlsCertPath, SCEPCertificateSubject, + c.profileDB, ) }