Improve Microsoft endpoint validation (#38180)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **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
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Improved SOAP message validation on Windows MDM endpoints
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
</s:Envelope>
|
||||
`)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user