From 825f2979a2dc28c6cc57bb62aff16737978bd90e Mon Sep 17 00:00:00 2001 From: Jesse Peterson Date: Mon, 21 Oct 2024 09:56:55 -0700 Subject: [PATCH] migrate certauth tests to e2e (#150) --- service/certauth/certauth_test.go | 171 ------------------------------ test/e2e/certauth.go | 118 +++++++++++++++++++++ test/e2e/e2e.go | 3 + test/enrollment/enrollment.go | 88 +++++++++------ 4 files changed, 175 insertions(+), 205 deletions(-) delete mode 100644 service/certauth/certauth_test.go create mode 100644 test/e2e/certauth.go diff --git a/service/certauth/certauth_test.go b/service/certauth/certauth_test.go deleted file mode 100644 index dd0edb4..0000000 --- a/service/certauth/certauth_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package certauth - -import ( - "errors" - "io/ioutil" - "os" - "testing" - - "github.com/micromdm/nanomdm/mdm" - "github.com/micromdm/nanomdm/storage/file" - "github.com/micromdm/nanomdm/test" -) - -func loadAuthMsg() (*mdm.Authenticate, error) { - b, err := ioutil.ReadFile("../../mdm/testdata/Authenticate.2.plist") - if err != nil { - return nil, err - } - r, err := mdm.DecodeCheckin(b) - if err != nil { - return nil, err - } - a, ok := r.(*mdm.Authenticate) - if !ok { - return nil, errors.New("not an Authenticate message") - } - return a, nil -} - -func loadTokenMsg() (*mdm.TokenUpdate, error) { - b, err := ioutil.ReadFile("../../mdm/testdata/TokenUpdate.2.plist") - if err != nil { - return nil, err - } - r, err := mdm.DecodeCheckin(b) - if err != nil { - return nil, err - } - a, ok := r.(*mdm.TokenUpdate) - if !ok { - return nil, errors.New("not a TokenUpdate message") - } - return a, nil -} - -func TestNilCertAuth(t *testing.T) { - auth, err := loadAuthMsg() - if err != nil { - t.Fatal(err) - } - certAuth := New(nil, nil) - if certAuth == nil { - t.Fatal("New returned nil") - } - err = certAuth.Authenticate(&mdm.Request{}, auth) - if err == nil { - t.Fatal("expected error, nil returned") - } - if !errors.Is(err, ErrMissingCert) { - t.Fatalf("wrong error: %v", err) - } -} - -func TestCertAuth(t *testing.T) { - _, crt, err := test.SimpleSelfSignedRSAKeypair("TESTDEVICE", 1) - if err != nil { - t.Fatal(err) - } - storage, err := file.New("test-db") - if err != nil { - t.Fatal(err) - } - certAuth := New(&test.NopService{}, storage) - if certAuth == nil { - t.Fatal("New returned nil") - } - token, err := loadTokenMsg() - if err != nil { - t.Fatal(err) - } - // a non-Auth message without first Auth'ing the cert should - // generate an ErrNoCertAssoc. - err = certAuth.TokenUpdate(&mdm.Request{Certificate: crt}, token) - if err == nil { - t.Fatal("expected err; nil returned") - } - if !errors.Is(err, ErrNoCertAssoc) { - t.Fatalf("wrong error: %v", err) - } - // send another one to make sure we're not accidentally allowing - // retroactive - err = certAuth.TokenUpdate(&mdm.Request{Certificate: crt}, token) - if err == nil { - t.Fatal("expected err; nil returned") - } - if !errors.Is(err, ErrNoCertAssoc) { - t.Fatalf("wrong error: %v", err) - } - authMsg, err := loadAuthMsg() - if err != nil { - t.Fatal(err) - } - // let's actually associate our cert... - err = certAuth.Authenticate(&mdm.Request{Certificate: crt}, authMsg) - if err != nil { - t.Fatal(err) - } - // ... and try again. - err = certAuth.TokenUpdate(&mdm.Request{Certificate: crt}, token) - if err != nil { - t.Fatal(err) - } - _, crt2, err := test.SimpleSelfSignedRSAKeypair("TESTDEVICE", 2) - if err != nil { - t.Fatal(err) - } - // lets try and spoof our UDID using another certificate (bad!) - err = certAuth.TokenUpdate(&mdm.Request{Certificate: crt2}, token) - if err == nil { - t.Fatal("expected err; nil returned") - } - if !errors.Is(err, ErrNoCertAssoc) { - t.Fatalf("wrong error: %v", err) - } - os.RemoveAll("test-db") -} - -func TestCertAuthRetro(t *testing.T) { - _, crt, err := test.SimpleSelfSignedRSAKeypair("TESTDEVICE", 1) - if err != nil { - t.Fatal(err) - } - storage, err := file.New("test-db") - if err != nil { - t.Fatal(err) - } - certAuth := New(&test.NopService{}, storage, WithAllowRetroactive()) - if certAuth == nil { - t.Fatal("New returned nil") - } - token, err := loadTokenMsg() - if err != nil { - t.Fatal(err) - } - // usually a non-Auth message without first Auth'ing the cert would - // generate an ErrNoCertAssoc. instead this should allow us to - // register a cert. - err = certAuth.TokenUpdate(&mdm.Request{Certificate: crt}, token) - if err != nil { - t.Fatal(err) - } - // send another one to make sure we're still associated - err = certAuth.TokenUpdate(&mdm.Request{Certificate: crt}, token) - if err != nil { - t.Fatal(err) - } - _, crt2, err := test.SimpleSelfSignedRSAKeypair("TESTDEVICE", 2) - if err != nil { - t.Fatal(err) - } - // lets try and spoof our UDID using another certificate (bad!) to - // make sure we were properly setting retroactive association - err = certAuth.TokenUpdate(&mdm.Request{Certificate: crt2}, token) - if err == nil { - t.Fatal("expected err; nil returned") - } - if !errors.Is(err, ErrNoCertReuse) { - t.Fatalf("wrong error: %v", err) - } - os.RemoveAll("test-db") -} diff --git a/test/e2e/certauth.go b/test/e2e/certauth.go new file mode 100644 index 0000000..0b25efa --- /dev/null +++ b/test/e2e/certauth.go @@ -0,0 +1,118 @@ +package e2e + +import ( + "context" + "errors" + "testing" + + "github.com/groob/plist" + "github.com/micromdm/nanomdm/mdm" + "github.com/micromdm/nanomdm/service/certauth" + "github.com/micromdm/nanomdm/storage" + "github.com/micromdm/nanomdm/test" + "github.com/micromdm/nanomdm/test/enrollment" +) + +func certAuth(t *testing.T, ctx context.Context, store storage.CertAuthStore) { + d, auth, tok, err := setupEnrollment() + if err != nil { + t.Fatal(err) + } + + // init service + svc := certauth.New(&test.NopService{}, store) + + // send a non-Authenticate message (without an initial Authenticate message) + err = svc.TokenUpdate(d.NewMDMRequest(ctx), tok) + expectErr(t, err, certauth.ErrNoCertAssoc) + + // send another one to make sure we're not accidentally allowing retroactive mode + err = svc.TokenUpdate(d.NewMDMRequest(ctx), tok) + expectErr(t, err, certauth.ErrNoCertAssoc) + + // sent an authenticate message. this should associate our cert hash. + err = svc.Authenticate(d.NewMDMRequest(ctx), auth) + expectErr(t, err, nil) + + // now send an a message that should be authenticated + err = svc.TokenUpdate(d.NewMDMRequest(ctx), tok) + expectErr(t, err, nil) + + // lets swap out the device identity. i.e. attempt to spoof the device with another cert. + err = enrollment.ReplaceIdentityRandom(d) + if err != nil { + t.Fatal(err) + } + + // try the spoofed request + err = svc.TokenUpdate(d.NewMDMRequest(ctx), tok) + expectErr(t, err, certauth.ErrNoCertAssoc) +} + +func certAuthRetro(t *testing.T, ctx context.Context, store storage.CertAuthStore) { + d, _, tok, err := setupEnrollment() + if err != nil { + t.Fatal(err) + } + + // init service with retroactive + svc := certauth.New(&test.NopService{}, store, certauth.WithAllowRetroactive()) + + // without retroactive a non-Authenticate message would generate an ErrNoCertAssoc. + // however with retro on it should allow the association. + err = svc.TokenUpdate(d.NewMDMRequest(ctx), tok) + expectErr(t, err, nil) + + // send another one to make sure the reto association is still good. + err = svc.TokenUpdate(d.NewMDMRequest(ctx), tok) + expectErr(t, err, nil) + + // lets swap out the device identity. i.e. attempt to spoof the device with another cert. + err = enrollment.ReplaceIdentityRandom(d) + if err != nil { + t.Fatal(err) + } + + // try the spoofed request post-association + err = svc.TokenUpdate(d.NewMDMRequest(ctx), tok) + expectErr(t, err, certauth.ErrNoCertReuse) +} + +func expectErr(t *testing.T, have, want error) { + if !errors.Is(have, want) { + t.Helper() + t.Errorf("have: %v; want: %v", have, want) + } +} + +func setupEnrollment() (*enrollment.Enrollment, *mdm.Authenticate, *mdm.TokenUpdate, error) { + // create our test device + d, err := enrollment.NewRandomDeviceEnrollment(nil, "com.apple.test-topic", "/", "") + if err != nil { + return d, nil, nil, err + } + + // gen the Authenticate msg and turn into NanoMDM msg + r, err := d.GenAuthenticate() + if err != nil { + return d, nil, nil, err + } + auth := new(mdm.Authenticate) + err = plist.NewDecoder(r).Decode(auth) + if err != nil { + return d, auth, nil, err + } + + // gen the TokenUpdate msg and turn into NanoMDM msg + r, err = d.GenTokenUpdate() + if err != nil { + return d, auth, nil, err + } + tok := new(mdm.TokenUpdate) + err = plist.NewDecoder(r).Decode(tok) + if err != nil { + return d, auth, tok, err + } + + return d, auth, tok, err +} diff --git a/test/e2e/e2e.go b/test/e2e/e2e.go index acbb8ba..3e1b873 100644 --- a/test/e2e/e2e.go +++ b/test/e2e/e2e.go @@ -78,6 +78,9 @@ func TestE2E(t *testing.T, ctx context.Context, store storage.AllStorage) { t.Fatal(err) } + t.Run("certauth", func(t *testing.T) { certAuth(t, ctx, store) }) + t.Run("certauth-retro", func(t *testing.T) { certAuthRetro(t, ctx, store) }) + // regression test for retrieving push info of missing devices. t.Run("invalid-pushinfo", func(t *testing.T) { _, err := store.RetrievePushInfo(ctx, []string{"INVALID"}) diff --git a/test/enrollment/enrollment.go b/test/enrollment/enrollment.go index 841ef7c..618a913 100644 --- a/test/enrollment/enrollment.go +++ b/test/enrollment/enrollment.go @@ -106,6 +106,13 @@ func NewFromCheckins(doer protocol.Doer, serverURL, checkInURL, authenticatePath return e, err } +// ReplaceIdentityRandom changes the certificate private key to a random certificate and key. +func ReplaceIdentityRandom(e *Enrollment) error { + var err error + e.key, e.cert, err = test.SimpleSelfSignedRSAKeypair("TESTDEVICE", 2) + return err +} + // NewRandomDeviceEnrollment creates a new randomly identified MDM enrollment. func NewRandomDeviceEnrollment(doer protocol.Doer, topic, serverURL, checkInURL string) (*Enrollment, error) { udid := randString(32) @@ -138,8 +145,8 @@ func (c *Enrollment) GetIdentity(context.Context) (*x509.Certificate, crypto.Pri return c.cert, c.key, nil } -// genAuthenticate creates an XML Plist Authenticate check-in message. -func (e *Enrollment) genAuthenticate() (io.Reader, error) { +// GenAuthenticate creates an XML Plist Authenticate check-in message. +func (e *Enrollment) GenAuthenticate() (io.Reader, error) { a := &mdm.Authenticate{ Enrollment: e.enrollment, MessageType: mdm.MessageType{MessageType: "Authenticate"}, @@ -149,8 +156,8 @@ func (e *Enrollment) genAuthenticate() (io.Reader, error) { return test.PlistReader(a) } -// genTokenUpdate creates an XML Plist TokenUpdate check-in message. -func (e *Enrollment) genTokenUpdate() (io.Reader, error) { +// GenTokenUpdate creates an XML Plist TokenUpdate check-in message. +func (e *Enrollment) GenTokenUpdate() (io.Reader, error) { t := &mdm.TokenUpdate{ Enrollment: e.enrollment, MessageType: mdm.MessageType{MessageType: "TokenUpdate"}, @@ -160,14 +167,36 @@ func (e *Enrollment) genTokenUpdate() (io.Reader, error) { return test.PlistReader(t) } -// DoTokenUpdate sends a TokenUpdate to the MDM server. -func (e *Enrollment) DoTokenUpdate(ctx context.Context) error { +// doAuthenticate sends an Authenticate check-in message to the MDM server. +func (e *Enrollment) doAuthenticate(ctx context.Context) error { + e.enrolled = false + + // generate Authenticate check-in message + auth, err := e.GenAuthenticate() + if err != nil { + return err + } + + // send it to the MDM server + authResp, err := e.transport.DoCheckIn(ctx, auth) + if err != nil { + return err + } + defer authResp.Body.Close() + + // check for any errors + return HTTPErrors(authResp) +} + +// DoAuthenticate sends an Authenticate check-in message to the MDM server. +func (e *Enrollment) DoAuthenticate(ctx context.Context) error { e.enrollM.Lock() defer e.enrollM.Unlock() - return e.doTokenUpdate(ctx) + return e.doAuthenticate(ctx) } -// doTokenUpdate sends a TokenUpdate to the MDM server. +// doTokenUpdate sends a TokenUpdate check-in message to the MDM server. +// A new random push token is generated for the device. func (e *Enrollment) doTokenUpdate(ctx context.Context) error { // generate new random push token. // the token comes from Apple's APNs service. so we'll simulate this @@ -175,7 +204,7 @@ func (e *Enrollment) doTokenUpdate(ctx context.Context) error { e.push.Token = []byte(randString(32)) // generate TokenUpdate check-in message - msg, err := e.genTokenUpdate() + msg, err := e.GenTokenUpdate() if err != nil { return err } @@ -191,6 +220,14 @@ func (e *Enrollment) doTokenUpdate(ctx context.Context) error { return HTTPErrors(resp) } +// DoTokenUpdate sends a TokenUpdate check-in message to the MDM server. +// A new random push token is generated for the device. +func (e *Enrollment) DoTokenUpdate(ctx context.Context) error { + e.enrollM.Lock() + defer e.enrollM.Unlock() + return e.doTokenUpdate(ctx) +} + // DoEnroll enrolls (or re-enrolls) this enrollment into MDM. // Authenticate and TokenUpdate check-in messages are sent via the // transport to the MDM server. @@ -198,32 +235,14 @@ func (e *Enrollment) DoEnroll(ctx context.Context) error { e.enrollM.Lock() defer e.enrollM.Unlock() - if e.enrolled { - e.enrolled = false - } - - // generate Authenticate check-in message - auth, err := e.genAuthenticate() - if err != nil { - return err - } - - // send it to the MDM server - authResp, err := e.transport.DoCheckIn(ctx, auth) + err := e.doAuthenticate(ctx) if err != nil { - return err + return fmt.Errorf("authenticate check-in: %w", err) } - // check for any errors - if err = HTTPErrors(authResp); err != nil { - authResp.Body.Close() - return fmt.Errorf("enrollment authenticate check-in: %w", err) - } - authResp.Body.Close() - err = e.doTokenUpdate(ctx) if err != nil { - return err + return fmt.Errorf("tokenupdate check-in: %w", err) } e.enrolled = true @@ -250,8 +269,9 @@ func (e *Enrollment) EnrollID() *mdm.EnrollID { func (e *Enrollment) NewMDMRequest(ctx context.Context) *mdm.Request { return &mdm.Request{ - Context: ctx, - EnrollID: e.EnrollID(), + Context: ctx, + EnrollID: e.EnrollID(), + Certificate: e.cert, } } @@ -269,12 +289,12 @@ func (e *Enrollment) DoReportAndFetch(ctx context.Context, report io.Reader) (*h // genSetBootstrapToken creates an XML Plist SetBootstrapToken check-in message. func (e *Enrollment) genSetBootstrapToken(token []byte) (io.Reader, error) { + b64Token := base64.StdEncoding.EncodeToString(token) msg := &mdm.SetBootstrapToken{ Enrollment: e.enrollment, MessageType: mdm.MessageType{MessageType: "SetBootstrapToken"}, - BootstrapToken: mdm.BootstrapToken{BootstrapToken: make([]byte, base64.StdEncoding.EncodedLen(len(token)))}, + BootstrapToken: mdm.BootstrapToken{BootstrapToken: []byte(b64Token)}, } - base64.StdEncoding.Encode(msg.BootstrapToken.BootstrapToken, token) return test.PlistReader(msg) }