From e225ef57912c8f4ac8977e24b5ebe1d9fd875257 Mon Sep 17 00:00:00 2001 From: Jordan Montgomery Date: Mon, 12 Jan 2026 15:28:45 -0500 Subject: [PATCH] Improve Microsoft endpoint validation (#38180) **Related issue:** Resolves #13698 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually --- changes/13698-windows-mdm | 1 + go.mod | 1 + go.sum | 2 + pkg/mdm/mdmtest/windows.go | 60 ++++++++- server/mdm/microsoft/wstep.go | 69 ++++++++-- server/service/integration_logger_test.go | 9 +- .../service/integration_mdm_lifecycle_test.go | 29 ++++- server/service/integration_mdm_test.go | 119 +++++++++++++++--- server/service/microsoft_mdm.go | 2 +- 9 files changed, 254 insertions(+), 38 deletions(-) create mode 100644 changes/13698-windows-mdm diff --git a/changes/13698-windows-mdm b/changes/13698-windows-mdm new file mode 100644 index 0000000000..d971853a52 --- /dev/null +++ b/changes/13698-windows-mdm @@ -0,0 +1 @@ +* Improved SOAP message validation on Windows MDM endpoints diff --git a/go.mod b/go.mod index b300932c19..1ee14ea096 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/Masterminds/semver v1.5.0 github.com/Masterminds/semver/v3 v3.3.1 + github.com/MicahParks/jwkset v0.11.0 github.com/RobotsAndPencils/buford v0.14.0 github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f github.com/WatchBeam/clock v0.0.0-20170901150240-b08e6b4da7ea diff --git a/go.sum b/go.sum index f191756f44..6c71ac1182 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,8 @@ github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7r github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOhoyQ= +github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= diff --git a/pkg/mdm/mdmtest/windows.go b/pkg/mdm/mdmtest/windows.go index 817c5c5418..6bcdf8a43f 100644 --- a/pkg/mdm/mdmtest/windows.go +++ b/pkg/mdm/mdmtest/windows.go @@ -2,13 +2,16 @@ package mdmtest import ( "bytes" + "crypto/rsa" "crypto/tls" "encoding/base64" "encoding/xml" + "errors" "fmt" "io" "net/http" "strconv" + "strings" "github.com/fleetdm/fleet/v4/pkg/fleethttp" "github.com/fleetdm/fleet/v4/server/fleet" @@ -42,6 +45,10 @@ type TestWindowsMDMClient struct { // queuedCommandResponses tracks the commands that will be sent next // time the device responds to the server. queuedCommandResponses map[string]fleet.SyncMLCmd + // jwtSigningKey is the key used to sign JWTs + jwtSigningKey *rsa.PrivateKey + // jwtSigningKeyID is the ID to report in the header for the signing key + jwtSigningKeyID string } // This is a test-only enrollment type to force erroneous behavior. @@ -65,6 +72,13 @@ func TestWindowsMDMClientNotInOOBE() TestWindowsMDMClientOption { } } +func TestWindowsMDMClientWithSigningKey(signingKey *rsa.PrivateKey, signingKeyID string) TestWindowsMDMClientOption { + return func(c *TestWindowsMDMClient) { + c.jwtSigningKey = signingKey + c.jwtSigningKeyID = signingKeyID + } +} + func NewTestMDMClientWindowsProgramatic(serverURL string, orbitNodeKey string, opts ...TestWindowsMDMClientOption) *TestWindowsMDMClient { return newTestMDMClient(serverURL, fleet.WindowsMDMProgrammaticEnrollmentType, orbitNodeKey, opts...) } @@ -374,10 +388,21 @@ YioVozr1IWYySwWVzMf/SUwKZkKJCAJmSVcixE+4kxPkyPGyauIrN3wWC0zb+mjF `) - if _, err := c.request(microsoft_mdm.MDE2EnrollPath, enrollReq); err != nil { + resp, err := c.request(microsoft_mdm.MDE2EnrollPath, enrollReq) + if err != nil { return err } + // Check for SOAP fault + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + if strings.Contains(string(body), "s:fault") { + return fmt.Errorf("enroll request returned SOAP fault: %s", string(body)) + } + return nil } @@ -416,10 +441,21 @@ func (c *TestWindowsMDMClient) Discovery() error { // TODO: parse the response and store the policy and enroll endpoints instead // of hardcoding them to truly test that the server is behaving as expected. - if _, err := c.request(microsoft_mdm.MDE2DiscoveryPath, discoveryReq); err != nil { + resp, err := c.request(microsoft_mdm.MDE2DiscoveryPath, discoveryReq) + if err != nil { return err } + // Check for SOAP fault + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + if strings.Contains(string(body), "s:fault") { + return fmt.Errorf("discovery request returned SOAP fault: %s", string(body)) + } + return nil } @@ -467,9 +503,19 @@ func (c *TestWindowsMDMClient) Policy() error { // TODO: store the policy requirements to generate a certificate and generate // one on the fly using them instead of using hardcoded values. - if _, err := c.request(microsoft_mdm.MDE2PolicyPath, policyReq); err != nil { + resp, err := c.request(microsoft_mdm.MDE2PolicyPath, policyReq) + if err != nil { return err } + // Check for SOAP fault + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + if strings.Contains(string(body), "s:fault") { + return fmt.Errorf("policy request returned SOAP fault: %s", string(body)) + } return nil } @@ -503,9 +549,13 @@ func (c *TestWindowsMDMClient) getToken() (binarySecToken string, tokenValueType "unique_name": "foo_bar", "scp": "mdm_delegation", } + if c.jwtSigningKey == nil || c.jwtSigningKeyID == "" { + return "", "", errors.New("jwt signing key is not set") + } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte("foo")) + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + token.Header["kid"] = c.jwtSigningKeyID + tokenString, err := token.SignedString(c.jwtSigningKey) if err != nil { return "", "", err } diff --git a/server/mdm/microsoft/wstep.go b/server/mdm/microsoft/wstep.go index beb0c01ac2..9c486cbb6e 100644 --- a/server/mdm/microsoft/wstep.go +++ b/server/mdm/microsoft/wstep.go @@ -13,11 +13,14 @@ import ( "errors" "fmt" "math/big" + "os" "strconv" "strings" "time" + "github.com/MicahParks/jwkset" "github.com/fleetdm/fleet/v4/server" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/cryptoutil" "github.com/golang-jwt/jwt/v4" @@ -215,7 +218,7 @@ func (m *manager) GetSTSAuthTokenUPNClaim(tokenStr string) (string, error) { } // Since we used the private key to sign the tokens, we use the public counterpart to verify the signature - token, err := jwt.ParseWithClaims(tokenStr, &STSClaims{}, func(token *jwt.Token) (interface{}, error) { + token, err := jwt.ParseWithClaims(tokenStr, &STSClaims{}, func(token *jwt.Token) (any, error) { return m.identityCert.PublicKey, nil }) if err != nil { @@ -235,27 +238,71 @@ func (m *manager) GetSTSAuthTokenUPNClaim(tokenStr string) (string, error) { // GetAzureAuthTokenClaims validates the given Azure AD token and returns // UPN, TenantID, UniqueName, DeviceID -func GetAzureAuthTokenClaims(tokenStr string) (AzureData, error) { +func GetAzureAuthTokenClaims(ctx context.Context, tokenStr string) (AzureData, error) { if len(tokenStr) == 0 { - return AzureData{}, errors.New("invalid STS token") + return AzureData{}, ctxerr.New(ctx, "invalid STS token") } // Decode base64 token tokenBytes, err := base64.StdEncoding.DecodeString(tokenStr) if err != nil { - return AzureData{}, errors.New("invalid Azure JWT token") + return AzureData{}, ctxerr.Wrap(ctx, err, "invalid Azure JWT token") } // Validate token format (header.payload.signature) parts := bytes.Split(tokenBytes, []byte(".")) if len(parts) != 3 { - return AzureData{}, errors.New("invalid Azure JWT format") + return AzureData{}, ctxerr.New(ctx, "invalid Azure JWT format") } // Parse JWT token - token, _, err := new(jwt.Parser).ParseUnverified(string(tokenBytes), jwt.MapClaims{}) + jwksURI := "https://login.microsoftonline.com/common/discovery/v2.0/keys" + var token *jwt.Token + FLEET_DEV_AZURE_JWT_JWKS_URI := os.Getenv("FLEET_DEV_AZURE_JWT_JWKS_URI") + if FLEET_DEV_AZURE_JWT_JWKS_URI != "" { + jwksURI = FLEET_DEV_AZURE_JWT_JWKS_URI + } + + keys, err := jwkset.NewDefaultHTTPClient([]string{jwksURI}) if err != nil { - return AzureData{}, errors.New("parse error Azure JWT content") + return AzureData{}, ctxerr.Wrap(ctx, err, "failed to retrieve Azure JWT signing keys") + } + token, err = jwt.Parse(string(tokenBytes), func(token *jwt.Token) (any, error) { + tokenAlg, ok := token.Header["alg"] + if !ok { + return nil, errors.New("Azure JWT missing alg header") + } + tokenAlgStr, ok := tokenAlg.(string) + if !ok { + return nil, errors.New("invalid alg header in Azure JWT") + } + + kid, ok := token.Header["kid"] + if !ok { + return nil, errors.New("Azure JWT missing kid header") + } + kidStr, ok := kid.(string) + if !ok { + return nil, errors.New("invalid kid header in Azure JWT") + } + + key, err := keys.KeyRead(ctx, kidStr) + if err != nil { + if errors.Is(err, jwkset.ErrKeyNotFound) { + return nil, fmt.Errorf("Azure JWT signed by unknown key: %w", err) + } + return nil, fmt.Errorf("failed to retrieve Azure JWT signing key: %w", err) + } + + // Alg is optional in the JWK but if present must match the token + keyAlg := key.Marshal().ALG.String() + if keyAlg != "" && keyAlg != tokenAlgStr { + return nil, fmt.Errorf("Azure JWT signing key algorithm mismatch: expected %s from key, got %s", keyAlg, tokenAlgStr) + } + return key.Key(), nil + }) + if err != nil { + return AzureData{}, ctxerr.Wrap(ctx, err, "parse error Azure JWT content") } // Parse JWT token @@ -264,25 +311,25 @@ func GetAzureAuthTokenClaims(tokenStr string) (AzureData, error) { // Get UPN claim upnClaim, ok := claims["upn"].(string) if !ok || len(upnClaim) == 0 { - return AzureData{}, errors.New("invalid UPN claim") + return AzureData{}, ctxerr.New(ctx, "invalid UPN claim") } // Get TenantID claim tenantIDClaim, ok := claims["tid"].(string) if !ok || len(tenantIDClaim) == 0 { - return AzureData{}, errors.New("invalid TenantID claim") + return AzureData{}, ctxerr.New(ctx, "invalid TenantID claim") } // Get UniqueName claim uniqueNameClaim, ok := claims["unique_name"].(string) if !ok { - return AzureData{}, errors.New("invalid UniqueName claim") + return AzureData{}, ctxerr.New(ctx, "invalid UniqueName claim") } // Get SCP claim azureSCPClaim, ok := claims["scp"].(string) if !ok || azureSCPClaim != "mdm_delegation" { - return AzureData{}, errors.New("invalid SCP claim") + return AzureData{}, ctxerr.New(ctx, "invalid SCP claim") } return AzureData{ diff --git a/server/service/integration_logger_test.go b/server/service/integration_logger_test.go index a08ce9d8cb..cbd0be32fb 100644 --- a/server/service/integration_logger_test.go +++ b/server/service/integration_logger_test.go @@ -371,7 +371,7 @@ func (s *integrationLoggerTestSuite) TestWindowsMDMEnrollEmptyBinarySecurityToke host := createOrbitEnrolledHost(t, "windows", "", s.ds) mdmDevice := mdmtest.NewTestMDMClientWindowsEmptyBinarySecurityToken(s.server.URL, *host.OrbitNodeKey) err = mdmDevice.Enroll() - require.NoError(t, err) + require.Error(t, err) t.Log(s.buf.String()) @@ -394,12 +394,11 @@ func (s *integrationLoggerTestSuite) TestWindowsMDMEnrollEmptyBinarySecurityToke require.Equal(t, "info", m["level"]) require.Equal(t, "binarySecurityToken is empty", m["soap_fault"]) case microsoft_mdm.MDE2EnrollPath: - require.Equal(t, "info", m["level"]) - require.Equal(t, "binarySecurityToken is empty", m["soap_fault"]) - foundEnroll = true + foundEnroll = false } } require.True(t, foundDiscovery) require.True(t, foundPolicy) - require.True(t, foundEnroll) + // Will not enroll due to soap fault on prior request + require.False(t, foundEnroll) } diff --git a/server/service/integration_mdm_lifecycle_test.go b/server/service/integration_mdm_lifecycle_test.go index 04762df1d2..a9c024adc7 100644 --- a/server/service/integration_mdm_lifecycle_test.go +++ b/server/service/integration_mdm_lifecycle_test.go @@ -327,6 +327,16 @@ func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsWindows() { // the ack of the message should be the only returned command require.Len(t, cmds, 1) + // Simulate the host having fleetd installed and reporting back in as un-enrolled + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(context.Background(), ` + UPDATE host_mdm + SET enrolled = 0, server_url = '' + WHERE host_id = ? + `, host.ID) + return err + }) + // re-enroll require.NoError(t, device.Enroll()) }, @@ -359,13 +369,28 @@ func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsWindows() { &orbitScriptResp, ) + // Simulate the host having fleetd installed after being wiped and reporting back in as un-enrolled + mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(context.Background(), ` + UPDATE host_mdm + SET enrolled = 0, server_url = '' + WHERE host_id = ? + `, host.ID) + return err + }) + require.NoError(t, device.Enroll()) }, }, { "host turns on MDM features out of the blue", func(t *testing.T, host *fleet.Host, device *mdmtest.TestWindowsMDMClient) { - require.NoError(t, device.Enroll()) + if strings.Contains(t.Name(), "automatic") { + require.NoError(t, device.Enroll()) + } else { + // A programatically-enrolled host that randomly turns on MDM after already enabled will get a SOAP fault + require.Error(t, device.Enroll()) + } }, }, { @@ -438,7 +463,7 @@ func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsWindows() { host := createOrbitEnrolledHost(t, "windows", "windows_automatic", s.ds) azureMail := "foo.bar.baz@example.com" - device := mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, azureMail) + device := mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, azureMail, mdmtest.TestWindowsMDMClientWithSigningKey(s.jwtSigningKey, defaultFakeJWTKeyID)) device.HardwareID = host.UUID device.DeviceID = host.UUID require.NoError(t, device.Enroll()) diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index ca6c5a0e1c..29f07baa24 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -35,12 +35,14 @@ import ( "testing" "time" + "github.com/MicahParks/jwkset" "github.com/davecgh/go-spew/spew" "github.com/fleetdm/fleet/v4/server/mdm/android" android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock" android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service" "github.com/fleetdm/fleet/v4/server/mdm/android/service/androidmgmt" "github.com/fleetdm/fleet/v4/server/mdm/android/tests" + "github.com/golang-jwt/jwt/v4" "google.golang.org/api/androidmanagement/v1" eeservice "github.com/fleetdm/fleet/v4/ee/server/service" @@ -131,6 +133,7 @@ type integrationMDMTestSuite struct { androidAPIClient *android_mock.Client androidSvc android.Service proxyCallbackURL string + jwtSigningKey *rsa.PrivateKey } // appleVPPConfigSrvConf is used to configure the mock server that mocks Apple's VPP endpoints. @@ -158,6 +161,8 @@ var defaultVPPAssetList = []vpp.Asset{ }, } +const defaultFakeJWTKeyID = "fleetdefaultkeyid" + func (s *integrationMDMTestSuite) SetupSuite() { s.withDS.SetupSuite("integrationMDMTestSuite") @@ -734,6 +739,37 @@ func (s *integrationMDMTestSuite) SetupSuite() { s.T().Cleanup(s.appleVPPConfigSrv.Close) s.T().Cleanup(s.appleVPPProxySrv.Close) s.T().Cleanup(s.appleGDMFSrv.Close) + + // Setup fake JWKs server for Azure JWT verification + metadata := jwkset.JWKMetadataOptions{ + KID: defaultFakeJWTKeyID, + } + options := jwkset.JWKOptions{ + Metadata: metadata, + } + + // Create the JWK from the key and options. + jwk, err := jwkset.NewJWKFromKey(testKey, options) + require.NoError(s.T(), err) + + jwkSet := jwkset.NewMemoryStorage() + + err = jwkSet.KeyWrite(ctx, jwk) + require.NoError(s.T(), err) + + jwksServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/jwks.json" { + response, err := jwkSet.JSONPublic(r.Context()) + require.NoError(s.T(), err) + + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(response) + require.NoError(s.T(), err) + } + })) + s.T().Cleanup(jwksServer.Close) + s.jwtSigningKey = testKey + s.T().Setenv("FLEET_DEV_AZURE_JWT_JWKS_URI", jwksServer.URL+"/jwks.json") } func (s *integrationMDMTestSuite) TearDownSuite() { @@ -1491,25 +1527,25 @@ func enrollWindowsHostInMDMViaOrbit(t *testing.T, host *fleet.Host, ds fleet.Dat } // Simulates a host being orbit enrolled first then an MDM enrollment coming via the settings app -func createWindowsHostThenEnrollMDMViaSettingsApp(ds fleet.Datastore, fleetServerURL, email string, t *testing.T) (*fleet.Host, *mdmtest.TestWindowsMDMClient) { - host := createOrbitEnrolledHost(t, "windows", uuid.NewString(), ds) - mdmDevice := enrollWindowsMDMViaSettingsApp(t, ds, fleetServerURL, email) +func (s *integrationMDMTestSuite) createWindowsHostThenEnrollMDMViaSettingsApp(fleetServerURL, email string) (*fleet.Host, *mdmtest.TestWindowsMDMClient) { + host := createOrbitEnrolledHost(s.T(), "windows", uuid.NewString(), s.ds) + mdmDevice := s.enrollWindowsMDMViaSettingsApp(fleetServerURL, email) return host, mdmDevice } // Note that this method only creates the MDM Enrollment but it will still need to be linked to the host record either // via DS methods or by simualting a refetch. -func enrollWindowsMDMViaSettingsApp(t *testing.T, ds fleet.Datastore, fleetServerURL, email string) *mdmtest.TestWindowsMDMClient { - mdmDevice := mdmtest.NewTestMDMClientWindowsAutomatic(fleetServerURL, email, mdmtest.TestWindowsMDMClientNotInOOBE()) +func (s *integrationMDMTestSuite) enrollWindowsMDMViaSettingsApp(fleetServerURL, email string) *mdmtest.TestWindowsMDMClient { + mdmDevice := mdmtest.NewTestMDMClientWindowsAutomatic(fleetServerURL, email, mdmtest.TestWindowsMDMClientNotInOOBE(), mdmtest.TestWindowsMDMClientWithSigningKey(s.jwtSigningKey, defaultFakeJWTKeyID)) err := mdmDevice.Enroll() - require.NoError(t, err) + require.NoError(s.T(), err) return mdmDevice } -func enrollWindowsHostInMDMViaAutopilot(t *testing.T, ds fleet.Datastore, fleetServerURL, email string) *mdmtest.TestWindowsMDMClient { - mdmDevice := mdmtest.NewTestMDMClientWindowsAutomatic(fleetServerURL, email) +func (s *integrationMDMTestSuite) enrollWindowsHostInMDMViaAutopilot(fleetServerURL, email string) *mdmtest.TestWindowsMDMClient { + mdmDevice := mdmtest.NewTestMDMClientWindowsAutomatic(fleetServerURL, email, mdmtest.TestWindowsMDMClientWithSigningKey(s.jwtSigningKey, defaultFakeJWTKeyID)) err := mdmDevice.Enroll() - require.NoError(t, err) + require.NoError(s.T(), err) return mdmDevice } @@ -7663,7 +7699,20 @@ func (s *integrationMDMTestSuite) TestValidGetPoliciesRequestWithAzureToken() { t := s.T() // Preparing the GetPolicies Request message with Azure JWT token - azureADTok := "ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKU1V6STFOaUlzSW5nMWRDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUlzSW10cFpDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUo5LmV5SmhkV1FpT2lKb2RIUndjem92TDIxaGNtTnZjMnhoWW5NdWIzSm5MeUlzSW1semN5STZJbWgwZEhCek9pOHZjM1J6TG5kcGJtUnZkM011Ym1WMEwyWmhaVFZqTkdZekxXWXpNVGd0TkRRNE15MWlZelptTFRjMU9UVTFaalJoTUdFM01pOGlMQ0pwWVhRaU9qRTJPRGt4TnpBNE5UZ3NJbTVpWmlJNk1UWTRPVEUzTURnMU9Dd2laWGh3SWpveE5qZzVNVGMxTmpZeExDSmhZM0lpT2lJeElpd2lZV2x2SWpvaVFWUlJRWGt2T0ZSQlFVRkJOV2gwUTNFMGRERjNjbHBwUTIxQmVEQlpWaTloZGpGTVMwRkRPRXM1Vm10SGVtNUdXVGxzTUZoYWVrZHVha2N6VVRaMWVIUldNR3QxT1hCeFJXdFRZeUlzSW1GdGNpSTZXeUp3ZDJRaUxDSnljMkVpWFN3aVlYQndhV1FpT2lJeU9XUTVaV1E1T0MxaE5EWTVMVFExTXpZdFlXUmxNaTFtT1RneFltTXhaRFl3TldVaUxDSmhjSEJwWkdGamNpSTZJakFpTENKa1pYWnBZMlZwWkNJNkltRXhNMlkzWVdVd0xURXpPR0V0TkdKaU1pMDVNalF5TFRka09USXlaVGRqTkdGak15SXNJbWx3WVdSa2NpSTZJakU0Tmk0eE1pNHhPRGN1TWpZaUxDSnVZVzFsSWpvaVZHVnpkRTFoY21OdmMweGhZbk1pTENKdmFXUWlPaUpsTTJNMU5XVmtZeTFqTXpRNExUUTBNVFl0T0dZd05TMHlOVFJtWmpNd05qVmpOV1VpTENKd2QyUmZkWEpzSWpvaWFIUjBjSE02THk5d2IzSjBZV3d1YldsamNtOXpiMlowYjI1c2FXNWxMbU52YlM5RGFHRnVaMlZRWVhOemQyOXlaQzVoYzNCNElpd2ljbWdpT2lJd0xrRldTVUU0T0ZSc0xXaHFlbWN3VXpoaU0xZFdXREJ2UzJOdFZGRXpTbHB1ZUUxa1QzQTNUbVZVVm5OV2FYVkhOa0ZRYnk0aUxDSnpZM0FpT2lKdFpHMWZaR1ZzWldkaGRHbHZiaUlzSW5OMVlpSTZJa1pTUTJ4RldURk9ObXR2ZEdWblMzcFplV0pFTjJkdFdGbGxhVTVIUkZrd05FSjJOV3R6ZDJGeGJVRWlMQ0owYVdRaU9pSm1ZV1UxWXpSbU15MW1NekU0TFRRME9ETXRZbU0yWmkwM05UazFOV1kwWVRCaE56SWlMQ0oxYm1seGRXVmZibUZ0WlNJNkluUmxjM1JBYldGeVkyOXpiR0ZpY3k1dmNtY2lMQ0oxY0c0aU9pSjBaWE4wUUcxaGNtTnZjMnhoWW5NdWIzSm5JaXdpZFhScElqb2lNVGg2WkVWSU5UZFRSWFZyYWpseGJqRm9aMlJCUVNJc0luWmxjaUk2SWpFdU1DSjkuVG1FUlRsZktBdWo5bTVvQUc2UTBRblV4VEFEaTNFamtlNHZ3VXo3UTdqUUFVZVZGZzl1U0pzUXNjU2hFTXVxUmQzN1R2VlpQanljdEVoRFgwLVpQcEVVYUlSempuRVEyTWxvc21SZURYZzhrYkhNZVliWi1jb0ZucDEyQkVpQnpJWFBGZnBpaU1GRnNZZ0hSSF9tSWxwYlBlRzJuQ2p0LTZSOHgzYVA5QS1tM0J3eV91dnV0WDFNVEVZRmFsekhGa04wNWkzbjZRcjhURnlJQ1ZUYW5OanlkMjBBZFRMbHJpTVk0RVBmZzRaLThVVTctZkcteElycWVPUmVWTnYwOUFHV192MDd6UkVaNmgxVk9tNl9nelRGcElVVURuZFdabnFLTHlySDlkdkF3WnFFSG1HUmlTNElNWnRFdDJNTkVZSnhDWHhlSi1VbWZJdV9tUVhKMW9R" + // Preparing the SecurityToken Request message with Azure JWT token + claims := &jwt.MapClaims{ + "upn": "fleetie@example.com", + "tid": "tenant_id", + "unique_name": "foo_bar", + "scp": "mdm_delegation", + } + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + token.Header["kid"] = defaultFakeJWTKeyID + tokenString, err := token.SignedString(s.jwtSigningKey) + require.NoError(t, err) + + azureADTok := base64.URLEncoding.EncodeToString([]byte(tokenString)) requestBytes, err := s.newGetPoliciesMsg(false, azureADTok) require.NoError(t, err) @@ -7826,7 +7875,19 @@ func (s *integrationMDMTestSuite) TestValidRequestSecurityTokenRequestWithAzureT t := s.T() // Preparing the SecurityToken Request message with Azure JWT token - azureADTok := "ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKU1V6STFOaUlzSW5nMWRDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUlzSW10cFpDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUo5LmV5SmhkV1FpT2lKb2RIUndjem92TDIxaGNtTnZjMnhoWW5NdWIzSm5MeUlzSW1semN5STZJbWgwZEhCek9pOHZjM1J6TG5kcGJtUnZkM011Ym1WMEwyWmhaVFZqTkdZekxXWXpNVGd0TkRRNE15MWlZelptTFRjMU9UVTFaalJoTUdFM01pOGlMQ0pwWVhRaU9qRTJPRGt4TnpBNE5UZ3NJbTVpWmlJNk1UWTRPVEUzTURnMU9Dd2laWGh3SWpveE5qZzVNVGMxTmpZeExDSmhZM0lpT2lJeElpd2lZV2x2SWpvaVFWUlJRWGt2T0ZSQlFVRkJOV2gwUTNFMGRERjNjbHBwUTIxQmVEQlpWaTloZGpGTVMwRkRPRXM1Vm10SGVtNUdXVGxzTUZoYWVrZHVha2N6VVRaMWVIUldNR3QxT1hCeFJXdFRZeUlzSW1GdGNpSTZXeUp3ZDJRaUxDSnljMkVpWFN3aVlYQndhV1FpT2lJeU9XUTVaV1E1T0MxaE5EWTVMVFExTXpZdFlXUmxNaTFtT1RneFltTXhaRFl3TldVaUxDSmhjSEJwWkdGamNpSTZJakFpTENKa1pYWnBZMlZwWkNJNkltRXhNMlkzWVdVd0xURXpPR0V0TkdKaU1pMDVNalF5TFRka09USXlaVGRqTkdGak15SXNJbWx3WVdSa2NpSTZJakU0Tmk0eE1pNHhPRGN1TWpZaUxDSnVZVzFsSWpvaVZHVnpkRTFoY21OdmMweGhZbk1pTENKdmFXUWlPaUpsTTJNMU5XVmtZeTFqTXpRNExUUTBNVFl0T0dZd05TMHlOVFJtWmpNd05qVmpOV1VpTENKd2QyUmZkWEpzSWpvaWFIUjBjSE02THk5d2IzSjBZV3d1YldsamNtOXpiMlowYjI1c2FXNWxMbU52YlM5RGFHRnVaMlZRWVhOemQyOXlaQzVoYzNCNElpd2ljbWdpT2lJd0xrRldTVUU0T0ZSc0xXaHFlbWN3VXpoaU0xZFdXREJ2UzJOdFZGRXpTbHB1ZUUxa1QzQTNUbVZVVm5OV2FYVkhOa0ZRYnk0aUxDSnpZM0FpT2lKdFpHMWZaR1ZzWldkaGRHbHZiaUlzSW5OMVlpSTZJa1pTUTJ4RldURk9ObXR2ZEdWblMzcFplV0pFTjJkdFdGbGxhVTVIUkZrd05FSjJOV3R6ZDJGeGJVRWlMQ0owYVdRaU9pSm1ZV1UxWXpSbU15MW1NekU0TFRRME9ETXRZbU0yWmkwM05UazFOV1kwWVRCaE56SWlMQ0oxYm1seGRXVmZibUZ0WlNJNkluUmxjM1JBYldGeVkyOXpiR0ZpY3k1dmNtY2lMQ0oxY0c0aU9pSjBaWE4wUUcxaGNtTnZjMnhoWW5NdWIzSm5JaXdpZFhScElqb2lNVGg2WkVWSU5UZFRSWFZyYWpseGJqRm9aMlJCUVNJc0luWmxjaUk2SWpFdU1DSjkuVG1FUlRsZktBdWo5bTVvQUc2UTBRblV4VEFEaTNFamtlNHZ3VXo3UTdqUUFVZVZGZzl1U0pzUXNjU2hFTXVxUmQzN1R2VlpQanljdEVoRFgwLVpQcEVVYUlSempuRVEyTWxvc21SZURYZzhrYkhNZVliWi1jb0ZucDEyQkVpQnpJWFBGZnBpaU1GRnNZZ0hSSF9tSWxwYlBlRzJuQ2p0LTZSOHgzYVA5QS1tM0J3eV91dnV0WDFNVEVZRmFsekhGa04wNWkzbjZRcjhURnlJQ1ZUYW5OanlkMjBBZFRMbHJpTVk0RVBmZzRaLThVVTctZkcteElycWVPUmVWTnYwOUFHV192MDd6UkVaNmgxVk9tNl9nelRGcElVVURuZFdabnFLTHlySDlkdkF3WnFFSG1HUmlTNElNWnRFdDJNTkVZSnhDWHhlSi1VbWZJdV9tUVhKMW9R" + claims := &jwt.MapClaims{ + "upn": "fleetie@example.com", + "tid": "tenant_id", + "unique_name": "foo_bar", + "scp": "mdm_delegation", + } + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + token.Header["kid"] = defaultFakeJWTKeyID + tokenString, err := token.SignedString(s.jwtSigningKey) + require.NoError(t, err) + + azureADTok := base64.URLEncoding.EncodeToString([]byte(tokenString)) requestBytes, err := s.newSecurityTokenMsg(azureADTok, false, false) require.NoError(t, err) @@ -8396,7 +8457,7 @@ func (s *integrationMDMTestSuite) TestWindowsAutomaticEnrollmentCommands() { require.NoError(t, err) azureMail := "foo.bar.baz@example.com" - d := mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, azureMail) + d := mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, azureMail, mdmtest.TestWindowsMDMClientWithSigningKey(s.jwtSigningKey, defaultFakeJWTKeyID)) require.NoError(t, d.Enroll()) checkinAndAck := func(expectFleetdCmds bool) { @@ -8487,6 +8548,36 @@ func (s *integrationMDMTestSuite) TestWindowsAutomaticEnrollmentCommands() { checkinAndAck(false) } +func (s *integrationMDMTestSuite) TestWindowsAzureInitiatedBadKeys() { + t := s.T() + ctx := context.Background() + + // define a global enroll secret + err := s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}}) + require.NoError(t, err) + + // We could generate the key differently, this just ensures it's generated the same as the "happy path" tests + _, badKey, err := apple_mdm.NewSCEPCACertKey() + require.NoError(t, err) + + autopilotUserMail := "swan@example.com" + + // Bad key + mdmDevice := mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, autopilotUserMail, mdmtest.TestWindowsMDMClientWithSigningKey(badKey, defaultFakeJWTKeyID)) + err = mdmDevice.Enroll() + require.Error(s.T(), err) + + // Good key but wrong ID + mdmDevice = mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, autopilotUserMail, mdmtest.TestWindowsMDMClientWithSigningKey(s.jwtSigningKey, "bad-key-id")) + err = mdmDevice.Enroll() + require.Error(s.T(), err) + + // Happy path to ensure the setup is correct + mdmDevice = mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, autopilotUserMail, mdmtest.TestWindowsMDMClientWithSigningKey(s.jwtSigningKey, defaultFakeJWTKeyID)) + err = mdmDevice.Enroll() + require.NoError(s.T(), err) +} + func (s *integrationMDMTestSuite) TestWindowsAzureInitiatedEnrollmentAndMapping() { t := s.T() ctx := context.Background() @@ -8503,11 +8594,11 @@ func (s *integrationMDMTestSuite) TestWindowsAzureInitiatedEnrollmentAndMapping( // Enroll another host to ensure the wires don't get crossed somehow autopilotUserMail := "swan@example.com" - autopilotDevice := enrollWindowsHostInMDMViaAutopilot(t, s.ds, s.server.URL, autopilotUserMail) + autopilotDevice := s.enrollWindowsHostInMDMViaAutopilot(s.server.URL, autopilotUserMail) require.NoError(t, autopilotDevice.Enroll()) settingsAppUserMail := "fleetie@example.com" - settingsAppHost, settingsAppDevice := createWindowsHostThenEnrollMDMViaSettingsApp(s.ds, s.server.URL, settingsAppUserMail, t) + settingsAppHost, settingsAppDevice := s.createWindowsHostThenEnrollMDMViaSettingsApp(s.server.URL, settingsAppUserMail) require.NoError(t, settingsAppDevice.Enroll()) // Transfer the host to the team. Ensure it doesn't wind up in "No team" at the end diff --git a/server/service/microsoft_mdm.go b/server/service/microsoft_mdm.go index 7e42ee8f30..2c1204b970 100644 --- a/server/service/microsoft_mdm.go +++ b/server/service/microsoft_mdm.go @@ -1021,7 +1021,7 @@ func (svc *Service) authBinarySecurityToken(ctx context.Context, authToken *flee if authToken.IsAzureJWTToken() { // Validate the JWT Auth token by retreving its claims - tokenData, err := microsoft_mdm.GetAzureAuthTokenClaims(authToken.Content) + tokenData, err := microsoft_mdm.GetAzureAuthTokenClaims(ctx, authToken.Content) if err != nil { return "", "", fmt.Errorf("binary security token claim failed: %v", err) }