From 3edec93328b29f3b48ea80501a89ae35370ee5b0 Mon Sep 17 00:00:00 2001 From: Jesse Peterson Date: Thu, 13 Jun 2024 14:37:28 -0700 Subject: [PATCH] GetToken check-in message framing (#105) --- cmd/nanomdm/main.go | 5 +- docs/enroll.mobileconfig | 1 + mdm/checkin.go | 40 +++++++++++++ mdm/checkin_test.go | 33 +++++++++++ service/certauth/helpers_test.go | 4 ++ service/certauth/service.go | 7 +++ service/dump/dump.go | 11 ++++ service/microwebhook/service.go | 14 +++++ service/multi/multi.go | 10 ++++ service/nanomdm/service.go | 25 ++++++++ service/nanomdm/token.go | 71 +++++++++++++++++++++++ service/nanomdm/token_test.go | 97 ++++++++++++++++++++++++++++++++ service/request.go | 16 ++++++ service/service.go | 7 +++ 14 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 service/nanomdm/token.go create mode 100644 service/nanomdm/token_test.go diff --git a/cmd/nanomdm/main.go b/cmd/nanomdm/main.go index 187e907..49657ac 100644 --- a/cmd/nanomdm/main.go +++ b/cmd/nanomdm/main.go @@ -112,10 +112,13 @@ func main() { stdlog.Fatal(err) } + tokenMux := nanomdm.NewTokenMux() + // create 'core' MDM service nanoOpts := []nanomdm.Option{ - nanomdm.WithLogger(logger.With("service", "nanomdm")), nanomdm.WithUserAuthenticate(nanomdm.NewUAService(mdmStorage, *flUAZLChal)), + nanomdm.WithGetToken(tokenMux), + nanomdm.WithLogger(logger.With("service", "nanomdm")), } if *flDMURLPfx != "" { var warningText string diff --git a/docs/enroll.mobileconfig b/docs/enroll.mobileconfig index 530a681..599c028 100644 --- a/docs/enroll.mobileconfig +++ b/docs/enroll.mobileconfig @@ -46,6 +46,7 @@ com.apple.mdm.per-user-connections com.apple.mdm.bootstraptoken + com.apple.mdm.token ServerURL https://mdm.example.org/mdm diff --git a/mdm/checkin.go b/mdm/checkin.go index db9a38a..27387b6 100644 --- a/mdm/checkin.go +++ b/mdm/checkin.go @@ -103,6 +103,44 @@ type DeclarativeManagement struct { Raw []byte `plist:"-"` // Original XML plist } +// TokenParameters is a representation of a "GetTokenRequest.TokenParameters" structure. +// See https://developer.apple.com/documentation/devicemanagement/gettokenrequest/tokenparameters +type TokenParameters struct { + PhoneUDID string + SecurityToken string + WatchUDID string +} + +// GetTokenResponse is a representation of a "GetTokenResponse" structure. +// See https://developer.apple.com/documentation/devicemanagement/gettokenresponse +type GetTokenResponse struct { + TokenData []byte +} + +// GetToken is a representation of a "GetToken" check-in message type. +// See https://developer.apple.com/documentation/devicemanagement/get_token +type GetToken struct { + Enrollment + MessageType + TokenServiceType string + TokenParameters *TokenParameters `plist:",omitempty"` + Raw []byte `plist:"-"` // Original XML plist +} + +// Validate validates a GetToken check-in message. +func (m *GetToken) Validate() error { + if m == nil { + return errors.New("nil GetToken") + } + if m.TokenServiceType == "" { + return errors.New("empty GetToken TokenServiceType") + } + if m.TokenServiceType == "com.apple.watch.pairing" && m.TokenParameters == nil { + return fmt.Errorf("nil TokenParameters for GetToken: %s", m.TokenServiceType) + } + return nil +} + // newCheckinMessageForType returns a pointer to a check-in struct for MessageType t func newCheckinMessageForType(t string, raw []byte) interface{} { switch t { @@ -120,6 +158,8 @@ func newCheckinMessageForType(t string, raw []byte) interface{} { return &UserAuthenticate{Raw: raw} case "DeclarativeManagement": return &DeclarativeManagement{Raw: raw} + case "GetToken": + return &GetToken{Raw: raw} default: return nil } diff --git a/mdm/checkin_test.go b/mdm/checkin_test.go index 6701a86..aaf4aca 100644 --- a/mdm/checkin_test.go +++ b/mdm/checkin_test.go @@ -109,3 +109,36 @@ func TestTokenUpdate(t *testing.T) { }) } } + +func TestGetTokenMAID(t *testing.T) { + test := ` + + + + MessageType + GetToken + UDID + test + TokenServiceType + com.apple.maid + + +` + m, err := DecodeCheckin([]byte(test)) + if err != nil { + t.Fatal(err) + } + msg, ok := m.(*GetToken) + if !ok { + t.Fatal("incorrect decoded check-in message type") + } + if err := msg.Validate(); err != nil { + t.Fatal(err) + } + if msg, want, have := "invalid UDID", "test", msg.UDID; have != want { + t.Errorf("%s: %q, want: %q", msg, have, want) + } + if msg, want, have := "invalid TokenServiceType", "com.apple.maid", msg.TokenServiceType; have != want { + t.Errorf("%s: %q, want: %q", msg, have, want) + } +} diff --git a/service/certauth/helpers_test.go b/service/certauth/helpers_test.go index cb4b4f9..27f38a9 100644 --- a/service/certauth/helpers_test.go +++ b/service/certauth/helpers_test.go @@ -81,6 +81,10 @@ func (s *NopService) DeclarativeManagement(r *mdm.Request, m *mdm.DeclarativeMan return nil, nil } +func (s *NopService) GetToken(r *mdm.Request, m *mdm.GetToken) (*mdm.GetTokenResponse, error) { + return nil, nil +} + func (s *NopService) CommandAndReportResults(r *mdm.Request, results *mdm.CommandResults) (*mdm.Command, error) { return nil, nil } diff --git a/service/certauth/service.go b/service/certauth/service.go index fd0af1a..65670ea 100644 --- a/service/certauth/service.go +++ b/service/certauth/service.go @@ -53,6 +53,13 @@ func (s *CertAuth) DeclarativeManagement(r *mdm.Request, m *mdm.DeclarativeManag return s.next.DeclarativeManagement(r, m) } +func (s *CertAuth) GetToken(r *mdm.Request, m *mdm.GetToken) (*mdm.GetTokenResponse, error) { + if err := s.validateOrAssociateForExistingEnrollment(r, &m.Enrollment); err != nil { + return nil, err + } + return s.next.GetToken(r, m) +} + func (s *CertAuth) CommandAndReportResults(r *mdm.Request, results *mdm.CommandResults) (*mdm.Command, error) { if err := s.validateOrAssociateForExistingEnrollment(r, &results.Enrollment); err != nil { return nil, err diff --git a/service/dump/dump.go b/service/dump/dump.go index 9708ba0..8a70924 100644 --- a/service/dump/dump.go +++ b/service/dump/dump.go @@ -2,6 +2,7 @@ package dump import ( + "encoding/base64" "fmt" "os" @@ -70,6 +71,16 @@ func (svc *Dumper) GetBootstrapToken(r *mdm.Request, m *mdm.GetBootstrapToken) ( return bsToken, err } +func (svc *Dumper) GetToken(r *mdm.Request, m *mdm.GetToken) (*mdm.GetTokenResponse, error) { + svc.file.Write(m.Raw) + token, err := svc.next.GetToken(r, m) + if token != nil && len(token.TokenData) > 0 { + b64 := base64.StdEncoding.EncodeToString(token.TokenData) + svc.file.WriteString("GetToken TokenData: " + b64 + "\n") + } + return token, err +} + func (svc *Dumper) CommandAndReportResults(r *mdm.Request, results *mdm.CommandResults) (*mdm.Command, error) { svc.file.Write(results.Raw) cmd, err := svc.next.CommandAndReportResults(r, results) diff --git a/service/microwebhook/service.go b/service/microwebhook/service.go index 7ca33d3..e1ba749 100644 --- a/service/microwebhook/service.go +++ b/service/microwebhook/service.go @@ -143,3 +143,17 @@ func (w *MicroWebhook) DeclarativeManagement(r *mdm.Request, m *mdm.DeclarativeM } return nil, postWebhookEvent(r.Context, w.client, w.url, ev) } + +func (w *MicroWebhook) GetToken(r *mdm.Request, m *mdm.GetToken) (*mdm.GetTokenResponse, error) { + ev := &Event{ + Topic: "mdm.GetToken", + CreatedAt: time.Now(), + CheckinEvent: &CheckinEvent{ + UDID: m.UDID, + EnrollmentID: m.EnrollmentID, + RawPayload: m.Raw, + Params: r.Params, + }, + } + return nil, postWebhookEvent(r.Context, w.client, w.url, ev) +} diff --git a/service/multi/multi.go b/service/multi/multi.go index 4331b91..ec7c692 100644 --- a/service/multi/multi.go +++ b/service/multi/multi.go @@ -122,6 +122,16 @@ func (ms *MultiService) DeclarativeManagement(r *mdm.Request, m *mdm.Declarative return retBytes, err } +func (ms *MultiService) GetToken(r *mdm.Request, m *mdm.GetToken) (*mdm.GetTokenResponse, error) { + resp, err := ms.svcs[0].GetToken(r, m) + rc := ms.RequestWithContext(r) + ms.runOthers(r.Context, func(svc service.CheckinAndCommandService) error { + _, err := svc.GetToken(rc, m) + return err + }) + return resp, err +} + func (ms *MultiService) CommandAndReportResults(r *mdm.Request, results *mdm.CommandResults) (*mdm.Command, error) { cmd, err := ms.svcs[0].CommandAndReportResults(r, results) rc := ms.RequestWithContext(r) diff --git a/service/nanomdm/service.go b/service/nanomdm/service.go index 66dc383..989a688 100644 --- a/service/nanomdm/service.go +++ b/service/nanomdm/service.go @@ -24,6 +24,9 @@ type Service struct { // UserAuthenticate processor ua service.UserAuthenticate + + // GetToken handler + gt service.GetToken } // normalize generates enrollment IDs that are used by other @@ -74,6 +77,13 @@ func WithUserAuthenticate(ua service.UserAuthenticate) Option { } } +// WithGetToken configures a GetToken check-in message handler. +func WithGetToken(gt service.GetToken) Option { + return func(s *Service) { + s.gt = gt + } +} + // New returns a new NanoMDM main service. func New(store storage.ServiceStore, opts ...Option) *Service { nanomdm := &Service{ @@ -189,6 +199,21 @@ func (s *Service) DeclarativeManagement(r *mdm.Request, message *mdm.Declarative return s.dm.DeclarativeManagement(r, message) } +// GetToken implements the GetToken Check-in message interface. +func (s *Service) GetToken(r *mdm.Request, message *mdm.GetToken) (*mdm.GetTokenResponse, error) { + if err := s.setupRequest(r, &message.Enrollment); err != nil { + return nil, err + } + ctxlog.Logger(r.Context, s.logger).Info( + "msg", "GetToken", + "token_service_type", message.TokenServiceType, + ) + if s.gt == nil { + return nil, errors.New("no GetToken handler") + } + return s.gt.GetToken(r, message) +} + // CommandAndReportResults command report and next-command request implementation. func (s *Service) CommandAndReportResults(r *mdm.Request, results *mdm.CommandResults) (*mdm.Command, error) { if err := s.setupRequest(r, &results.Enrollment); err != nil { diff --git a/service/nanomdm/token.go b/service/nanomdm/token.go new file mode 100644 index 0000000..64c0522 --- /dev/null +++ b/service/nanomdm/token.go @@ -0,0 +1,71 @@ +package nanomdm + +import ( + "fmt" + "sync" + + "github.com/micromdm/nanomdm/mdm" + "github.com/micromdm/nanomdm/service" +) + +// StaticToken holds static token bytes. +type StaticToken struct { + token []byte +} + +// NewStaticToken creates a new static token handler. +func NewStaticToken(token []byte) *StaticToken { + return &StaticToken{token: token} +} + +// GetToken always responds with the static token bytes. +func (t *StaticToken) GetToken(_ *mdm.Request, _ *mdm.GetToken) (*mdm.GetTokenResponse, error) { + return &mdm.GetTokenResponse{TokenData: t.token}, nil +} + +// TokenMux is a middleware multiplexer for GetToken check-in messages. +// A TokenServiceType string is associated with a GetToken handler and +// then dispatched appropriately. +type TokenMux struct { + typesMu sync.RWMutex + types map[string]service.GetToken +} + +// NewTokenMux creates a new TokenMux. +func NewTokenMux() *TokenMux { return &TokenMux{} } + +// Handle registers a GetToken handler for the given service type. +// See https://developer.apple.com/documentation/devicemanagement/gettokenrequest +func (mux *TokenMux) Handle(serviceType string, handler service.GetToken) { + if serviceType == "" { + panic("tokenmux: invalid service type") + } + if handler == nil { + panic("tokenmux: invalid handler") + } + mux.typesMu.Lock() + defer mux.typesMu.Unlock() + if mux.types == nil { + mux.types = make(map[string]service.GetToken) + } else if _, exists := mux.types[serviceType]; exists { + panic("tokenmux: multiple registrations for " + serviceType) + } + mux.types[serviceType] = handler +} + +// GetToken is the middleware that dispatches a GetToken handler based on service type. +func (mux *TokenMux) GetToken(r *mdm.Request, t *mdm.GetToken) (*mdm.GetTokenResponse, error) { + if t == nil { + return nil, fmt.Errorf("nil MDM GetToken") + } + var next service.GetToken + mux.typesMu.RLock() + if mux.types != nil { + next = mux.types[t.TokenServiceType] + } + mux.typesMu.RUnlock() + if next == nil { + return nil, fmt.Errorf("no handler for TokenServiceType: %v", t.TokenServiceType) + } + return next.GetToken(r, t) +} diff --git a/service/nanomdm/token_test.go b/service/nanomdm/token_test.go new file mode 100644 index 0000000..c211f2a --- /dev/null +++ b/service/nanomdm/token_test.go @@ -0,0 +1,97 @@ +package nanomdm + +import ( + "bytes" + "context" + "testing" + + "github.com/groob/plist" + "github.com/micromdm/nanomdm/mdm" + "github.com/micromdm/nanomdm/service" +) + +func newTokenMDMReq() *mdm.Request { + return &mdm.Request{Context: context.Background()} +} + +const tokenTestCheckin = ` + + + + MessageType + GetToken + UDID + test + TokenServiceType + com.apple.maid + + +` + +func TestTokenFull(t *testing.T) { + tokenTestData := []byte("hello") + + // create muxer + m := NewTokenMux() + + // associate a new static token handler with a type + m.Handle("com.apple.maid", NewStaticToken(tokenTestData)) + + // create a new NanoMDM service with our token muxer + s := New(nil, WithGetToken(m)) + + // process GetToken check-in message + respBytes, err := service.CheckinRequest(s, newTokenMDMReq(), []byte(tokenTestCheckin)) + if err != nil { + t.Fatal(err) + } + + // unmarshal response bytes + resp := new(mdm.GetTokenResponse) + err = plist.Unmarshal(respBytes, resp) + if err != nil { + t.Fatal(err) + } + + // check that our token data matches + if want, have := string(tokenTestData), string(resp.TokenData); have != want { + t.Errorf("have %q; want %q", have, want) + } +} + +func newGetToken(serviceType string, id string) *mdm.GetToken { + return &mdm.GetToken{ + TokenServiceType: serviceType, + Enrollment: mdm.Enrollment{UDID: id}, + } +} + +func TestToken(t *testing.T) { + tokenTestData := []byte("hello") + + // create muxer + m := NewTokenMux() + + // associate a new static token handler with a type + m.Handle("com.apple.maid", NewStaticToken(tokenTestData)) + + // create a new NanoMDM service with our token muxer + s := New(nil, WithGetToken(m)) + + // dispatch a GetToken check-in message + resp, err := s.GetToken(newTokenMDMReq(), newGetToken("com.apple.maid", "AAAA-1111")) + if err != nil { + t.Fatal(err) + } + + // check that our token data our matches (from the static handler) + if !bytes.Equal(tokenTestData, resp.TokenData) { + t.Error("input and output not equal") + } + + // supply an invalid service type (not handled) and expect an error + _, err = s.GetToken(newTokenMDMReq(), newGetToken("com.apple.does-not-exist", "AAAA-1111")) + if err == nil { + t.Fatal("should be an error") + } +} diff --git a/service/request.go b/service/request.go index 6139ebc..92e9af7 100644 --- a/service/request.go +++ b/service/request.go @@ -78,6 +78,22 @@ func CheckinRequest(svc Checkin, r *mdm.Request, bodyBytes []byte) ([]byte, erro if err != nil { err = fmt.Errorf("declarativemanagement service: %w", err) } + case *mdm.GetToken: + if err := m.Validate(); err != nil { + return nil, fmt.Errorf("gettoken validate: %w", err) + } + resp, err := svc.GetToken(r, m) + if err != nil { + return nil, fmt.Errorf("gettoken service: %w", err) + } + if resp == nil { + return nil, errors.New("gettoken service: no response") + } + respBytes, err = plist.Marshal(resp) + if err != nil { + return nil, fmt.Errorf("gettoken marshal: %w", err) + } + return respBytes, nil default: return nil, errors.New("unhandled check-in request type") } diff --git a/service/service.go b/service/service.go index 41f7225..ee57787 100644 --- a/service/service.go +++ b/service/service.go @@ -16,6 +16,12 @@ type UserAuthenticate interface { UserAuthenticate(*mdm.Request, *mdm.UserAuthenticate) ([]byte, error) } +// GetToken is the interface for handling a GetToken check-in message. +// See https://developer.apple.com/documentation/devicemanagement/get_token +type GetToken interface { + GetToken(*mdm.Request, *mdm.GetToken) (*mdm.GetTokenResponse, error) +} + // Checkin represents the various check-in requests. // See https://developer.apple.com/documentation/devicemanagement/check-in type Checkin interface { @@ -26,6 +32,7 @@ type Checkin interface { GetBootstrapToken(*mdm.Request, *mdm.GetBootstrapToken) (*mdm.BootstrapToken, error) UserAuthenticate DeclarativeManagement + GetToken } // CommandAndReportResults represents the command report and next-command request.