package service import ( "bytes" "context" "crypto/x509" "encoding/base64" "encoding/json" "encoding/xml" "errors" "fmt" "html" "io" "net/http" "net/url" "regexp" "strconv" "strings" "text/template" "time" "github.com/fleetdm/fleet/v4/server" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/contexts/logging" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/go-kit/log/level" mdm_types "github.com/fleetdm/fleet/v4/server/fleet" mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" "github.com/google/uuid" ) type SoapRequestContainer struct { Data *fleet.SoapRequest Params url.Values Err error } // MDM SOAP request decoder func (req *SoapRequestContainer) DecodeBody(ctx context.Context, r io.Reader, u url.Values, c []*x509.Certificate) error { // Reading the request bytes reqBytes, err := io.ReadAll(r) if err != nil { return ctxerr.Wrap(ctx, err, "reading soap mdm request") } // Set the request parameters req.Params = u // Handle empty body scenario req.Data = &fleet.SoapRequest{Raw: reqBytes} if len(reqBytes) != 0 { // Unmarshal the XML data from the request into the SoapRequest struct err = xml.Unmarshal(reqBytes, &req.Data) if err != nil { return ctxerr.Wrap(ctx, err, "unmarshalling soap mdm request") } } return nil } type SoapResponseContainer struct { Data *fleet.SoapResponse Err error } func (r SoapResponseContainer) error() error { return r.Err } // hijackRender writes the response header and the RAW HTML output func (r SoapResponseContainer) hijackRender(ctx context.Context, w http.ResponseWriter) { xmlRes, err := xml.MarshalIndent(r.Data, "", "\t") if err != nil { logging.WithExtras(ctx, "error with SoapResponseContainer", err) w.WriteHeader(http.StatusBadRequest) return } xmlRes = append(xmlRes, '\n') w.Header().Set("Content-Type", mdm.SoapContentType) w.Header().Set("Content-Length", strconv.Itoa(len(xmlRes))) w.WriteHeader(http.StatusOK) if n, err := w.Write(xmlRes); err != nil { logging.WithExtras(ctx, "err", err, "written", n) } } type SyncMLReqMsgContainer struct { Data *fleet.SyncML Params url.Values Certs []*x509.Certificate Err error } // MDM SOAP request decoder func (req *SyncMLReqMsgContainer) DecodeBody(ctx context.Context, r io.Reader, u url.Values, c []*x509.Certificate) error { // Reading the request bytes reqBytes, err := io.ReadAll(r) if err != nil { return ctxerr.Wrap(ctx, err, "reading SyncML message request") } // Set the request parameters req.Params = u // Set the request certs req.Certs = c // Handle empty body scenario req.Data = &fleet.SyncML{Raw: reqBytes} if len(reqBytes) != 0 { // Unmarshal the XML data from the request into the SoapRequest struct err = xml.Unmarshal(reqBytes, &req.Data) if err != nil { return ctxerr.Wrap(ctx, err, "unmarshalling SyncML message request") } } return nil } type SyncMLResponseMsgContainer struct { Data *fleet.SyncML Err error } func (r SyncMLResponseMsgContainer) error() error { return r.Err } // hijackRender writes the response header and the RAW HTML output func (r SyncMLResponseMsgContainer) hijackRender(ctx context.Context, w http.ResponseWriter) { xmlRes, err := xml.MarshalIndent(r.Data, "", "\t") if err != nil { logging.WithExtras(ctx, "error with SyncMLResponseMsgContainer", err) w.WriteHeader(http.StatusInternalServerError) return } xmlRes = append(xmlRes, '\n') w.Header().Set("Content-Type", mdm.SyncMLContentType) w.Header().Set("Content-Length", strconv.Itoa(len(xmlRes))) w.WriteHeader(http.StatusOK) if n, err := w.Write(xmlRes); err != nil { logging.WithExtras(ctx, "err", err, "written", n) } } type MDMWebContainer struct { Data *string Params url.Values Err error } // MDM SOAP request decoder func (req *MDMWebContainer) DecodeBody(ctx context.Context, r io.Reader, u url.Values, c []*x509.Certificate) error { reqBytes, err := io.ReadAll(r) if err != nil { return ctxerr.Wrap(ctx, err, "reading Webcontainer HTML message request") } // Set the request parameters req.Params = u // Get req data content := string(reqBytes) req.Data = &content return nil } func (req MDMWebContainer) error() error { return req.Err } // hijackRender writes the response header and the RAW HTML output func (req MDMWebContainer) hijackRender(ctx context.Context, w http.ResponseWriter) { resData := []byte(*req.Data + "\n") w.Header().Set("Content-Type", mdm.WebContainerContentType) w.Header().Set("Content-Length", strconv.Itoa(len(resData))) w.WriteHeader(http.StatusOK) if n, err := w.Write(resData); err != nil { logging.WithExtras(ctx, "err", err, "written", n) } } type MDMAuthContainer struct { Data *string Err error } func (r MDMAuthContainer) error() error { return r.Err } // hijackRender writes the response header and the RAW XML output func (r MDMAuthContainer) hijackRender(ctx context.Context, w http.ResponseWriter) { w.Header().Set("Content-Type", "text/html; charset=UTF-8") w.Header().Set("Content-Length", strconv.Itoa(len(*r.Data))) w.WriteHeader(http.StatusOK) if n, err := w.Write([]byte(*r.Data)); err != nil { logging.WithExtras(ctx, "err", err, "written", n) } } // getUtcTime returns the current timestamp plus the specified number of minutes, // formatted as "2006-01-02T15:04:05.000Z". func getUtcTime(minutes int) string { // Get the current time and then add the specified number of minutes now := time.Now() future := now.Add(time.Duration(minutes) * time.Minute) // Format and return the future time as a string return future.UTC().Format("2006-01-02T15:04:05.000Z") } // NewDiscoverResponse creates a new DiscoverResponse struct based on the auth policy, policy url, and enrollment url func NewDiscoverResponse(authPolicy string, policyUrl string, enrollmentUrl string) (mdm_types.DiscoverResponse, error) { if (len(authPolicy) == 0) || (len(policyUrl) == 0) || (len(enrollmentUrl) == 0) { return mdm_types.DiscoverResponse{}, errors.New("invalid parameters") } return mdm_types.DiscoverResponse{ XMLNS: mdm.DiscoverNS, DiscoverResult: mdm_types.DiscoverResult{ AuthPolicy: authPolicy, EnrollmentVersion: mdm.EnrollmentVersionV4, EnrollmentPolicyServiceUrl: policyUrl, EnrollmentServiceUrl: enrollmentUrl, }, }, nil } // NewGetPoliciesResponse creates a new GetPoliciesResponse struct based on the minimal key length, certificate validity period, and renewal period func NewGetPoliciesResponse(minimalKeyLength string, certificateValidityPeriodSeconds string, renewalPeriodSeconds string) (mdm_types.GetPoliciesResponse, error) { if (len(minimalKeyLength) == 0) || (len(certificateValidityPeriodSeconds) == 0) || (len(renewalPeriodSeconds) == 0) { return mdm_types.GetPoliciesResponse{}, errors.New("invalid parameters") } return mdm_types.GetPoliciesResponse{ XMLNS: mdm.PolicyNS, Response: mdm_types.Response{ PolicyFriendlyName: mdm_types.ContentAttr{ Xsi: mdm.DefaultStateXSI, XMLNS: mdm.EnrollXSI, }, NextUpdateHours: mdm_types.ContentAttr{ Xsi: mdm.DefaultStateXSI, XMLNS: mdm.EnrollXSI, }, PoliciesNotChanged: mdm_types.ContentAttr{ Xsi: mdm.DefaultStateXSI, XMLNS: mdm.EnrollXSI, }, Policies: mdm_types.Policies{ Policy: mdm_types.GPPolicy{ PolicyOIDReference: "0", CAs: mdm_types.GenericAttr{ Xsi: mdm.DefaultStateXSI, }, Attributes: mdm_types.Attributes{ CommonName: "FleetDMAttributes", PolicySchema: "3", HashAlgorithmOIDReference: "0", Revision: mdm_types.Revision{ MajorRevision: "101", MinorRevision: "0", }, CertificateValidity: mdm_types.CertificateValidity{ ValidityPeriodSeconds: certificateValidityPeriodSeconds, RenewalPeriodSeconds: renewalPeriodSeconds, }, Permission: mdm_types.Permission{ Enroll: "true", AutoEnroll: "false", }, SupersededPolicies: mdm_types.GenericAttr{ Xsi: mdm.DefaultStateXSI, }, PrivateKeyFlags: mdm_types.GenericAttr{ Xsi: mdm.DefaultStateXSI, }, SubjectNameFlags: mdm_types.GenericAttr{ Xsi: mdm.DefaultStateXSI, }, EnrollmentFlags: mdm_types.GenericAttr{ Xsi: mdm.DefaultStateXSI, }, GeneralFlags: mdm_types.GenericAttr{ Xsi: mdm.DefaultStateXSI, }, RARequirements: mdm_types.GenericAttr{ Xsi: mdm.DefaultStateXSI, }, KeyArchivalAttributes: mdm_types.GenericAttr{ Xsi: mdm.DefaultStateXSI, }, Extensions: mdm_types.GenericAttr{ Xsi: mdm.DefaultStateXSI, }, PrivateKeyAttributes: mdm_types.PrivateKeyAttributes{ MinimalKeyLength: minimalKeyLength, KeySpec: mdm_types.GenericAttr{ Xsi: mdm.DefaultStateXSI, }, KeyUsageProperty: mdm_types.GenericAttr{ Xsi: mdm.DefaultStateXSI, }, Permissions: mdm_types.GenericAttr{ Xsi: mdm.DefaultStateXSI, }, AlgorithmOIDReference: mdm_types.GenericAttr{ Xsi: mdm.DefaultStateXSI, }, CryptoProviders: []mdm_types.ProviderAttr{ {Content: "Microsoft Platform Crypto Provider"}, {Content: "Microsoft Software Key Storage Provider"}, }, }, }, }, }, }, // These are MS-XCEP OIDs defined in section 3.1.4.1.3.16 // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-xcep/161aab9f-d159-4df3-85c9-f732ed2a8445 OIDs: mdm_types.OIDs{ OID: []mdm_types.OID{ { // SHA256WithRSA OID // https://oidref.com/2.16.840.1.101.3.4.2.1 Value: "2.16.840.1.101.3.4.2.1", Group: "4", OIDReferenceID: "0", DefaultName: "szOID_NIST_sha256", }, { // RSA OID // https://oidref.com/1.2.840.113549.1.1.1 Value: "1.2.840.113549.1.1.1", Group: "3", OIDReferenceID: "1", DefaultName: "szOID_RSA_RSA", }, }, }, }, nil } // NewRequestSecurityTokenResponseCollection creates a new RequestSecurityTokenResponseCollection struct based on the provisioned token func NewRequestSecurityTokenResponseCollection(provisionedToken string) (mdm_types.RequestSecurityTokenResponseCollection, error) { if len(provisionedToken) == 0 { return mdm_types.RequestSecurityTokenResponseCollection{}, errors.New("invalid parameters") } enrollSecExtVal := mdm.EnrollSecExt return mdm_types.RequestSecurityTokenResponseCollection{ XMLNS: mdm.EnrollWSTrust, RequestSecurityTokenResponse: mdm_types.RequestSecurityTokenResponse{ TokenType: mdm.EnrollTType, DispositionMessage: mdm_types.SecAttr{ Content: "", XMLNS: mdm.EnrollReq, }, RequestID: mdm_types.SecAttr{ Content: "0", XMLNS: mdm.EnrollReq, }, RequestedSecurityToken: mdm_types.RequestedSecurityToken{ BinarySecurityToken: mdm_types.BinarySecurityToken{ Content: provisionedToken, XMLNS: &enrollSecExtVal, ValueType: mdm.EnrollPDoc, EncodingType: mdm.EnrollEncode, }, }, }, }, nil } // NewSoapFault creates a new SoapFault struct based on the error type, original message type, and error message func NewSoapFault(errorType string, origMessage int, errorMessage error) mdm_types.SoapFault { return mdm_types.SoapFault{ OriginalMessageType: origMessage, Code: mdm_types.Code{ Value: mdm.SoapFaultRecv, Subcode: mdm_types.Subcode{ Value: errorType, }, }, Reason: mdm_types.Reason{ Text: mdm_types.ReasonText{ Content: errorMessage.Error(), Lang: mdm.SoapFaultLocale, }, }, } } // getSTSAuthContent Retuns STS auth content func getSTSAuthContent(data string) errorer { return MDMAuthContainer{ Data: &data, Err: nil, } } // getSoapResponseFault Returns a SoapResponse with a SoapFault on its body func getSoapResponseFault(relatesTo string, soapFault *mdm_types.SoapFault) errorer { if len(relatesTo) == 0 { relatesTo = "invalid_message_id" } response, _ := NewSoapResponse(soapFault, relatesTo) return SoapResponseContainer{ Data: &response, Err: nil, } } // NewSoapResponse creates a new SoapRequest struct based on the message type and the message content func NewSoapResponse(payload interface{}, relatesTo string) (fleet.SoapResponse, error) { // Sanity check if len(relatesTo) == 0 { return fleet.SoapResponse{}, errors.New("relatesTo is invalid") } // Useful constants // Some of these are string urls to be assigned to pointers - they need to have a type and cannot be const literals var ( urlNSS = mdm.EnrollNSS urlNSA = mdm.EnrollNSA urlXSI = mdm.EnrollXSI urlXSD = mdm.EnrollXSD urlXSU = mdm.EnrollXSU urlDiag = mdm.ActionNsDiag urlDiscovery = mdm.ActionNsDiscovery urlPolicy = mdm.ActionNsPolicy urlEnroll = mdm.ActionNsEnroll urlSecExt = mdm.EnrollSecExt MUValue = "1" timestampID = "_0" secWindowStartTimeMin = -5 secWindowEndTimeMin = 5 ) // string pointers - they need to be pointers to not be marshalled into the XML when nil var ( headerXsu *string action string activityID *mdm_types.ActivityId security *mdm_types.WsSecurity ) // Build the response body var body mdm_types.BodyResponse // Set the message specific fields based on the message type switch msg := payload.(type) { case *mdm_types.DiscoverResponse: action = urlDiscovery uuid := uuid.New().String() activityID = &mdm_types.ActivityId{ Content: uuid, CorrelationId: uuid, XMLNS: urlDiag, } body.DiscoverResponse = msg case *mdm_types.GetPoliciesResponse: action = urlPolicy headerXsu = &urlXSU body.Xsi = &urlXSI body.Xsd = &urlXSD body.GetPoliciesResponse = msg case *mdm_types.RequestSecurityTokenResponseCollection: action = urlEnroll headerXsu = &urlXSU security = &mdm_types.WsSecurity{ MustUnderstand: MUValue, XMLNS: urlSecExt, Timestamp: mdm_types.Timestamp{ ID: timestampID, Created: getUtcTime(secWindowStartTimeMin), // minutes ago Expires: getUtcTime(secWindowEndTimeMin), // minutes from now }, } body.RequestSecurityTokenResponseCollection = msg // Setting the target action case *mdm_types.SoapFault: if msg.OriginalMessageType == mdm_types.MDEDiscovery { action = urlDiscovery } else if msg.OriginalMessageType == mdm_types.MDEPolicy { action = urlPolicy } else if msg.OriginalMessageType == mdm_types.MDEEnrollment { action = urlEnroll } else { action = urlDiag } uuid := uuid.New().String() activityID = &mdm_types.ActivityId{ Content: uuid, CorrelationId: uuid, XMLNS: urlDiag, } body.SoapFault = msg default: return fleet.SoapResponse{}, errors.New("mdm response message not supported") } // Return the SoapRequest type with the appropriate fields set return fleet.SoapResponse{ XMLNSS: urlNSS, XMLNSA: urlNSA, XMLNSU: headerXsu, Header: mdm_types.ResponseHeader{ Action: mdm_types.Action{ Content: action, MustUnderstand: MUValue, }, RelatesTo: relatesTo, ActivityId: activityID, Security: security, }, Body: body, }, nil } // NewBinarySecurityTokenPayload returns the BinarySecurityTokenPayload type func NewBinarySecurityTokenPayload(encodedToken string) (fleet.WindowsMDMAccessTokenPayload, error) { if len(encodedToken) == 0 { return fleet.WindowsMDMAccessTokenPayload{}, errors.New("binary security token: token is empty") } rawBytes, err := base64.StdEncoding.DecodeString(encodedToken) if err != nil { return fleet.WindowsMDMAccessTokenPayload{}, fmt.Errorf("binary security token: %v", err) } var tokenPayload fleet.WindowsMDMAccessTokenPayload err = json.Unmarshal(rawBytes, &tokenPayload) if err != nil { return fleet.WindowsMDMAccessTokenPayload{}, fmt.Errorf("binary security token: %v", err) } return tokenPayload, nil } // newParm returns a new ProvisioningDoc Parameter func newParm(name, value, datatype string) mdm_types.Param { return mdm_types.Param{ Name: name, Value: value, Datatype: datatype, } } // newCharacteristic returns a new ProvisioningDoc Characteristic func newCharacteristic(typ string, parms []mdm_types.Param, characteristics []mdm_types.Characteristic) mdm_types.Characteristic { return mdm_types.Characteristic{ Type: typ, Params: parms, Characteristics: characteristics, } } // NewProvisioningDoc returns a new ProvisioningDoc container // NewCertStoreProvisioningData returns a new CertStoreProvisioningData Characteristic // The enrollment client installs the client certificate, as well as the trusted root certificate and intermediate certificates. // The provisioning information in NewCertStoreProvisioningData includes various properties that the device management client uses to communicate with the MDM Server. // identityFingerprint is the fingerprint of the identity certificate // identityCert is the identity certificate bytes // signedClientFingerprint is the fingerprint of the signed client certificate // signedClientCert is the signed client certificate bytes func NewCertStoreProvisioningData(enrollmentType string, identityFingerprint string, identityCert []byte, signedClientFingerprint string, signedClientCert []byte) mdm_types.Characteristic { // Target Cert Store selection based on Enrollment type targetCertStore := "User" if enrollmentType == "Device" { targetCertStore = "System" } root := newCharacteristic("Root", nil, []mdm_types.Characteristic{ newCharacteristic("System", nil, []mdm_types.Characteristic{ newCharacteristic(identityFingerprint, []mdm_types.Param{ newParm("EncodedCertificate", base64.StdEncoding.EncodeToString(identityCert), ""), }, nil), }), }) my := newCharacteristic("My", nil, []mdm_types.Characteristic{ newCharacteristic(targetCertStore, nil, []mdm_types.Characteristic{ newCharacteristic(signedClientFingerprint, []mdm_types.Param{ newParm("EncodedCertificate", base64.StdEncoding.EncodeToString(signedClientCert), ""), }, nil), newCharacteristic("PrivateKeyContainer", nil, nil), }), newCharacteristic("WSTEP", nil, []mdm_types.Characteristic{ newCharacteristic("Renew", []mdm_types.Param{ newParm("ROBOSupport", mdm.WstepROBOSupport, "boolean"), newParm("RenewPeriod", mdm.WstepCertRenewalPeriodInDays, "integer"), newParm("RetryInterval", mdm.WstepRenewRetryInterval, "integer"), }, nil), }), }) certStore := newCharacteristic("CertificateStore", nil, []mdm_types.Characteristic{root, my}) return certStore } // NewApplicationProvisioningData returns a new ApplicationProvisioningData Characteristic // The Application Provisioning configuration is used for bootstrapping a device with an OMA DM account // The paramenters here maps to the W7 application CSP // https://learn.microsoft.com/en-us/windows/client-management/mdm/w7-application-csp func NewApplicationProvisioningData(mdmEndpoint string) mdm_types.Characteristic { provDoc := newCharacteristic("APPLICATION", []mdm_types.Param{ // The PROVIDER-ID parameter specifies the server identifier for a management server used in the current management session newParm("PROVIDER-ID", mdm.DocProvisioningAppProviderID, ""), // The APPID parameter is used to differentiate the types of available application services and protocols. newParm("APPID", "w7", ""), // The NAME parameter is used in the APPLICATION characteristic to specify a user readable application identity. newParm("NAME", mdm.DocProvisioningAppName, ""), // The ADDR parameter is used in the APPADDR param to get or set the address of the OMA DM server. newParm("ADDR", mdmEndpoint, ""), // The ROLE parameter is used in the APPLICATION characteristic to specify the security application chamber that the DM session should run with when communicating with the DM server. // The BACKCOMPATRETRYFREQ parameter is used to specify how many retries the DM client performs when there are Connection Manager-level or WinInet-level errors newParm("CONNRETRYFREQ", mdm.DocProvisioningAppConnRetryFreq, ""), // The INITIALBACKOFFTIME parameter is used to specify the initial wait time in milliseconds when the DM client retries for the first time newParm("INITIALBACKOFFTIME", mdm.DocProvisioningAppInitialBackoffTime, ""), // The MAXBACKOFFTIME parameter is used to specify the maximum number of milliseconds to sleep after package-sending failure newParm("MAXBACKOFFTIME", mdm.DocProvisioningAppMaxBackoffTime, ""), // The DEFAULTENCODING parameter is used to specify whether the DM client should use WBXML or XML for the DM package when communicating with the server. newParm("DEFAULTENCODING", "application/vnd.syncml.dm+xml", ""), // The BACKCOMPATRETRYDISABLED parameter is used to specify whether to retry resending a package with an older protocol version newParm("BACKCOMPATRETRYDISABLED", "", ""), }, []mdm_types.Characteristic{ // CLIENT specifies that the server authenticates itself to the OMA DM Client at the DM protocol level. newCharacteristic("APPAUTH", []mdm_types.Param{ newParm("AAUTHLEVEL", "CLIENT", ""), // DIGEST - Specifies that the SyncML DM 'syncml:auth-md5' authentication type. newParm("AAUTHTYPE", "DIGEST", ""), newParm("AAUTHSECRET", "dummy", ""), newParm("AAUTHDATA", "nonce", ""), }, nil), // APPSRV specifies that the client authenticates itself to the OMA DM Server at the DM protocol level. newCharacteristic("APPAUTH", []mdm_types.Param{ newParm("AAUTHLEVEL", "APPSRV", ""), // DIGEST - Specifies that the SyncML DM 'syncml:auth-md5' authentication type. newParm("AAUTHTYPE", "DIGEST", ""), newParm("AAUTHNAME", "dummy", ""), newParm("AAUTHSECRET", "dummy", ""), newParm("AAUTHDATA", "nonce", ""), }, nil), }) return provDoc } // NewDMClientProvisioningData returns a new DMClient Characteristic // These settings can be used to define different aspects of the DM client behavior // The provisioning information in NewCertStoreProvisioningData includes various properties that the device management client uses to communicate with the MDM Server. // c2DeviceName is the device name used by the IT admin console // listOfMSIAppToInstall contains a list of LocURIs that expected to be provision via EnterpriseDesktopAppManagement CSP func NewDMClientProvisioningData() mdm_types.Characteristic { dmClient := newCharacteristic("DMClient", nil, []mdm_types.Characteristic{ newCharacteristic("Provider", nil, []mdm_types.Characteristic{ newCharacteristic(mdm.DocProvisioningAppProviderID, []mdm_types.Param{}, []mdm_types.Characteristic{ newCharacteristic("Poll", []mdm_types.Param{ newParm("NumberOfFirstRetries", mdm.DmClientCSPNumberOfFirstRetries, mdm.DmClientIntType), newParm("IntervalForFirstSetOfRetries", mdm.DmClientCSPIntervalForFirstSetOfRetries, mdm.DmClientIntType), newParm("NumberOfSecondRetries", mdm.DmClientCSPNumberOfSecondRetries, mdm.DmClientIntType), newParm("IntervalForSecondSetOfRetries", mdm.DmClientCSPIntervalForSecondSetOfRetries, mdm.DmClientIntType), newParm("NumberOfRemainingScheduledRetries", mdm.DmClientCSPNumberOfRemainingScheduledRetries, mdm.DmClientIntType), newParm("IntervalForRemainingScheduledRetries", mdm.DmClientCSPIntervalForRemainingScheduledRetries, mdm.DmClientIntType), newParm("PollOnLogin", mdm.DmClientCSPPollOnLogin, mdm.DmClientBoolType), newParm("AllUsersPollOnFirstLogin", mdm.DmClientCSPPollOnLogin, mdm.DmClientBoolType), }, nil), }), }), }) return dmClient } // NewProvisioningDoc returns a new ProvisioningDoc container func NewProvisioningDoc(certStoreData mdm_types.Characteristic, applicationData mdm_types.Characteristic, dmClientData mdm_types.Characteristic) mdm_types.WapProvisioningDoc { return mdm_types.WapProvisioningDoc{ Version: mdm.DocProvisioningVersion, Characteristics: []mdm_types.Characteristic{ certStoreData, applicationData, dmClientData, }, } } // mdmMicrosoftDiscoveryEndpoint handles the Discovery message and returns a valid DiscoveryResponse message // DiscoverResponse message contains the Uniform Resource Locators (URLs) of service endpoints required for the following enrollment steps func mdmMicrosoftDiscoveryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { req := request.(*SoapRequestContainer).Data // Checking first if Discovery message is valid and returning error if this is not the case if err := req.IsValidDiscoveryMsg(); err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEDiscovery, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil } // Getting the DiscoveryResponse message discoveryResponseMsg, err := svc.GetMDMMicrosoftDiscoveryResponse(ctx, req.Body.Discover.Request.EmailAddress) if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEDiscovery, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil } // Embedding the DiscoveryResponse message inside of a SoapResponse response, err := NewSoapResponse(discoveryResponseMsg, req.GetMessageID()) if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEDiscovery, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil } return SoapResponseContainer{ Data: &response, Err: nil, }, nil } // mdmMicrosoftAuthEndpoint handles the Security Token Service (STS) implementation func mdmMicrosoftAuthEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { params := request.(*SoapRequestContainer).Params // Sanity check on the expected query params if !params.Has(mdm.STSAuthAppRu) || !params.Has(mdm.STSLoginHint) { return getSTSAuthContent(""), errors.New("expected STS params are not present") } appru := params.Get(mdm.STSAuthAppRu) loginHint := params.Get(mdm.STSLoginHint) if (len(appru) == 0) || (len(loginHint) == 0) { return getSTSAuthContent(""), errors.New("expected STS params are empty") } // Getting the STS endpoint HTML content stsAuthContent, err := svc.GetMDMMicrosoftSTSAuthResponse(ctx, appru, loginHint) if err != nil { return getSTSAuthContent(""), errors.New("error generating STS content") } return getSTSAuthContent(stsAuthContent), nil } // mdmMicrosoftPolicyEndpoint handles the GetPolicies message and returns a valid GetPoliciesResponse message // GetPoliciesResponse message contains the certificate policies required for the next enrollment step. For more information about these messages, see [MS-XCEP] sections 3.1.4.1.1.1 and 3.1.4.1.1.2. func mdmMicrosoftPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { req := request.(*SoapRequestContainer).Data // Checking first if GetPolicies message is valid and returning error if this is not the case if err := req.IsValidGetPolicyMsg(); err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEPolicy, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil } // Binary security token should be extracted to ensure this is a valid call hdrSecToken, err := req.GetHeaderBinarySecurityToken() if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEPolicy, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil } // Getting the GetPoliciesResponse message policyResponseMsg, err := svc.GetMDMWindowsPolicyResponse(ctx, hdrSecToken) if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEPolicy, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil } // Embedding the DiscoveryResponse message inside of a SoapResponse response, err := NewSoapResponse(policyResponseMsg, req.GetMessageID()) if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEPolicy, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil } return SoapResponseContainer{ Data: &response, Err: nil, }, nil } // mdmMicrosoftEnrollEndpoint handles the RequestSecurityToken message and returns a valid RequestSecurityTokenResponseCollection message // RequestSecurityTokenResponseCollection message contains the identity and provisioning information for the device management client. func mdmMicrosoftEnrollEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { req := request.(*SoapRequestContainer).Data // Checking first if RequestSecurityToken message is valid and returning error if this is not the case if err := req.IsValidRequestSecurityTokenMsg(); err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil } // Getting the RequestSecurityToken message from the SOAP request reqSecurityTokenMsg, err := req.GetRequestSecurityTokenMessage() if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil } // Binary security token should be extracted to ensure this is a valid call hdrBinarySecToken, err := req.GetHeaderBinarySecurityToken() if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil } // Getting the RequestSecurityTokenResponseCollection message enrollResponseMsg, err := svc.GetMDMWindowsEnrollResponse(ctx, reqSecurityTokenMsg, hdrBinarySecToken) if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil } // Embedding the DiscoveryResponse message inside of a SoapResponse response, err := NewSoapResponse(enrollResponseMsg, req.GetMessageID()) if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil } return SoapResponseContainer{ Data: &response, Err: nil, }, nil } // mdmMicrosoftManagementEndpoint handles the OMA DM management sessions // It receives a SyncML message with protocol commands, it process the commands and responds with a // SyncML message with protocol commands results and more protocol commands for the calling host // Note: This logic needs to be improved with better SyncML message parsing, better message tracking // and better security authentication (done through TLS and in-message hash) func mdmMicrosoftManagementEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { reqSyncML := request.(*SyncMLReqMsgContainer).Data reqCerts := request.(*SyncMLReqMsgContainer).Certs // Checking first if incoming SyncML message is valid and returning error if this is not the case if err := reqSyncML.IsValidMsg(); err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MSMDM, err) return getSoapResponseFault(reqSyncML.SyncHdr.MsgID, soapFault), nil } // Getting the MS-MDM response message resSyncML, err := svc.GetMDMWindowsManagementResponse(ctx, reqSyncML, reqCerts) if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MSMDM, err) return getSoapResponseFault(reqSyncML.SyncHdr.MsgID, soapFault), nil } return SyncMLResponseMsgContainer{ Data: resSyncML, Err: nil, }, nil } // mdmMicrosoftTOSEndpoint handles the TOS content for the incoming MDM enrollment request func mdmMicrosoftTOSEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { params := request.(*MDMWebContainer).Params // Sanity check on the expected query params if !params.Has(mdm.TOCRedirectURI) || !params.Has(mdm.TOCReqID) { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, errors.New("invalid params")) return getSoapResponseFault(mdm.SoapErrorInternalServiceFault, soapFault), nil } redirectURI := params.Get(mdm.TOCRedirectURI) reqID := params.Get(mdm.TOCReqID) // Getting the TOS content message resTOCData, err := svc.GetMDMWindowsTOSContent(ctx, redirectURI, reqID) if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err) return getSoapResponseFault(mdm.SoapErrorInternalServiceFault, soapFault), nil } return MDMWebContainer{ Data: &resTOCData, Err: nil, }, nil } // authBinarySecurityToken checks if the provided token is valid. For programmatic enrollment, it // returns the orbit node key and host uuid. For automatic enrollment, it returns only the UPN (the // host uuid will be an empty string). func (svc *Service) authBinarySecurityToken(ctx context.Context, authToken *fleet.HeaderBinarySecurityToken) (claim string, hostUUID string, err error) { if authToken == nil { return "", "", errors.New("authToken is empty") } err = authToken.IsValidToken() if err != nil { return "", "", errors.New("authToken is not valid") } // Tokens that were generated by enrollment client if authToken.IsDeviceToken() { // Getting the Binary Security Token Payload binSecToken, err := NewBinarySecurityTokenPayload(authToken.Content) if err != nil { return "", "", fmt.Errorf("token creation error %v", err) } // Validating the Binary Security Token Payload err = binSecToken.IsValidToken() if err != nil { return "", "", fmt.Errorf("invalid token data %v", err) } // Validating the Binary Security Token Type used on Programmatic Enrollments if binSecToken.Type == mdm_types.WindowsMDMProgrammaticEnrollmentType { host, err := svc.ds.LoadHostByOrbitNodeKey(ctx, binSecToken.Payload.OrbitNodeKey) if err != nil { return "", "", fmt.Errorf("host data cannot be found %v", err) } // This ensures that only hosts that are eligible for Windows enrollment can be enrolled if !host.IsEligibleForWindowsMDMEnrollment() { return "", "", errors.New("host is not elegible for Windows MDM enrollment") } // No errors, token is authorized return binSecToken.Payload.OrbitNodeKey, host.UUID, nil } // Validating the Binary Security Token Type used on Automatic Enrollments (returned by STS Auth Endpoint) if binSecToken.Type == mdm_types.WindowsMDMAutomaticEnrollmentType { upnToken, err := svc.wstepCertManager.GetSTSAuthTokenUPNClaim(binSecToken.Payload.AuthToken) if err != nil { return "", "", ctxerr.Wrap(ctx, err, "issue retrieving UPN from Auth token") } // No errors, token is authorized return upnToken, "", nil } } // Validating the Binary Security Token Type used on Automatic Enrollments if authToken.IsAzureJWTToken() { // Validate the JWT Auth token by retreving its claims tokenData, err := mdm.GetAzureAuthTokenClaims(authToken.Content) if err != nil { return "", "", fmt.Errorf("binary security token claim failed: %v", err) } // No errors, token is authorized return tokenData.UPN, "", nil } return "", "", errors.New("token is not authorized") } // GetMDMMicrosoftDiscoveryResponse returns a valid DiscoveryResponse message func (svc *Service) GetMDMMicrosoftDiscoveryResponse(ctx context.Context, upnEmail string) (*fleet.DiscoverResponse, error) { // skipauth: This endpoint does not use authentication svc.authz.SkipAuthorization(ctx) // Getting the app config appCfg, err := svc.ds.AppConfig(ctx) if err != nil { return nil, ctxerr.Wrap(ctx, err) } // Getting the DiscoveryResponse message content urlPolicyEndpoint, err := mdm.ResolveWindowsMDMPolicy(appCfg.ServerSettings.ServerURL) if err != nil { return nil, ctxerr.Wrap(ctx, err, "resolve policy endpoint") } urlEnrollEndpoint, err := mdm.ResolveWindowsMDMEnroll(appCfg.ServerSettings.ServerURL) if err != nil { return nil, ctxerr.Wrap(ctx, err, "resolve enroll endpoint") } discoveryMsg, err := NewDiscoverResponse(mdm.AuthOnPremise, urlPolicyEndpoint, urlEnrollEndpoint) if err != nil { return nil, ctxerr.Wrap(ctx, err, "creation of DiscoverResponse message") } return &discoveryMsg, nil } // GetMDMMicrosoftSTSAuthResponse returns a valid Security Token Service (STS) page content func (svc *Service) GetMDMMicrosoftSTSAuthResponse(ctx context.Context, appru string, loginHint string) (string, error) { // skipauth: This endpoint does not use authentication svc.authz.SkipAuthorization(ctx) // Dummy data will be returned as part of the token as user-driven enrollment is not supported yet // In the future, the following calls would have to be made to support user-driven enrollment // encodedBST will carry the token to return // authToken, err := svc.wstepCertManager.NewSTSAuthToken(loginHint) // encodedBST, err := GetEncodedBinarySecurityToken(fleet.WindowsMDMAutomaticEnrollmentType, authToken) encodedBST := "user_driven_enrollment_not_implemented" // STS Auth Endpoint returns HTML content that gets render in a webview container // The webview container expect a POST request to the appru URL with the wresult parameter set to the auth token // The security token in wresult is later passed back in // This string is opaque to the enrollment client; the client does not interpret the string. // The returned HTML content contains a JS script that will perform a POST request to the appru URL automatically // This will set the wresult parameter to the value of auth token tmpl, err := template.New("").Parse(` `) if err != nil { return "", ctxerr.Wrap(ctx, err, "STS content template") } var htmlBuf bytes.Buffer err = tmpl.Execute(&htmlBuf, map[string]string{"ActionURL": appru, "Token": encodedBST}) if err != nil { return "", ctxerr.Wrap(ctx, err, "creation of STS content") } return htmlBuf.String(), nil } // GetMDMWindowsPolicyResponse returns a valid GetPoliciesResponse message func (svc *Service) GetMDMWindowsPolicyResponse(ctx context.Context, authToken *fleet.HeaderBinarySecurityToken) (*fleet.GetPoliciesResponse, error) { if authToken == nil { return nil, fleet.NewInvalidArgumentError("policy response", "authToken is invalid") } // Validate the binary security token _, _, err := svc.authBinarySecurityToken(ctx, authToken) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validate binary security token") } // Token is authorized svc.authz.SkipAuthorization(ctx) // Getting the GetPoliciesResponse message content policyMsg, err := NewGetPoliciesResponse(mdm.PolicyMinKeyLength, mdm.PolicyCertValidityPeriodInSecs, mdm.PolicyCertRenewalPeriodInSecs) if err != nil { return nil, ctxerr.Wrap(ctx, err, "creation of GetPoliciesResponse message") } return &policyMsg, nil } // GetMDMWindowsEnrollResponse returns a valid RequestSecurityTokenResponseCollection message // secTokenMsg is the RequestSecurityToken message // authToken is the base64 encoded binary security token func (svc *Service) GetMDMWindowsEnrollResponse(ctx context.Context, secTokenMsg *fleet.RequestSecurityToken, authToken *fleet.HeaderBinarySecurityToken) (*fleet.RequestSecurityTokenResponseCollection, error) { if authToken == nil { return nil, fleet.NewInvalidArgumentError("enroll response", "authToken is not present") } // Auth the binary security token userID, hostUUID, err := svc.authBinarySecurityToken(ctx, authToken) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validate binary security token") } // Removing the device if already MDM enrolled err = svc.removeWindowsDeviceIfAlreadyMDMEnrolled(ctx, secTokenMsg) if err != nil { return nil, ctxerr.Wrap(ctx, err, "device enroll check") } // Getting the device provisioning information in the form of a WapProvisioningDoc deviceProvisioning, err := svc.getDeviceProvisioningInformation(ctx, secTokenMsg) if err != nil { return nil, ctxerr.Wrap(ctx, err, "device provisioning information") } // Token is authorized svc.authz.SkipAuthorization(ctx) // Getting the RequestSecurityTokenResponseCollection message content secTokenResponseCollectionMsg, err := NewRequestSecurityTokenResponseCollection(deviceProvisioning) if err != nil { return nil, ctxerr.Wrap(ctx, err, "creation of RequestSecurityTokenResponseCollection message") } // RequestSecurityTokenResponseCollection message is ready. The identity // and provisioning information will be sent to the Windows MDM // Enrollment Client // But before doing that, let's save the device information to the list // of MDM enrolled MDM devices // // This method also creates the relevant enrollment activity as it has // access to the device information. err = svc.storeWindowsMDMEnrolledDevice(ctx, userID, hostUUID, secTokenMsg) if err != nil { return nil, ctxerr.Wrap(ctx, err, "enrolled device information cannot be stored") } return &secTokenResponseCollectionMsg, nil } // GetMDMWindowsManagementResponse returns a valid SyncML response message func (svc *Service) GetMDMWindowsManagementResponse(ctx context.Context, reqSyncML *fleet.SyncML, reqCerts []*x509.Certificate) (*fleet.SyncML, error) { if reqSyncML == nil { return nil, fleet.NewInvalidArgumentError("syncml req message", "message is not present") } // Checking if the incoming request is trusted err := svc.isTrustedRequest(ctx, reqSyncML, reqCerts) if err != nil { return nil, ctxerr.Wrap(ctx, err, "management request is not trusted") } // Getting the management response message resSyncMLmsg, err := svc.getManagementResponse(ctx, reqSyncML) if err != nil { return nil, ctxerr.Wrap(ctx, err, "management response message") } // Token is authorized svc.authz.SkipAuthorization(ctx) return resSyncMLmsg, nil } // GetMDMWindowsTOSContent returns valid TOC content func (svc *Service) GetMDMWindowsTOSContent(ctx context.Context, redirectUri string, reqID string) (string, error) { tmpl, err := server.GetTemplate("frontend/templates/windowsTOS.html", "windows-tos") if err != nil { return "", ctxerr.Wrap(ctx, err, "issue generating TOS content") } var htmlBuf bytes.Buffer err = tmpl.Execute(&htmlBuf, map[string]string{"RedirectURL": redirectUri, "ClientData": reqID}) if err != nil { return "", ctxerr.Wrap(ctx, err, "executing TOS template content") } // skipauth: This endpoint does not use authentication svc.authz.SkipAuthorization(ctx) return htmlBuf.String(), nil } // isValidUPN checks if the provided user ID is a valid UPN func isValidUPN(userID string) bool { return upnRegex.MatchString(userID) } // isTrustedRequest checks if the incoming request was sent from MDM enrolled device func (svc *Service) isTrustedRequest(ctx context.Context, reqSyncML *fleet.SyncML, reqCerts []*x509.Certificate) error { if reqSyncML == nil { return fleet.NewInvalidArgumentError("syncml req message", "message is not present") } // Checking if calling request is coming from an already MDM enrolled device deviceID, err := reqSyncML.GetSource() if err != nil || deviceID == "" { return fmt.Errorf("invalid SyncML message %w", err) } enrolledDevice, err := svc.ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, deviceID) if err != nil || enrolledDevice == nil { return errors.New("device was not MDM enrolled") } // Check if TLS certs contains device ID on its common name if len(reqCerts) > 0 { for _, reqCert := range reqCerts { if strings.Contains(reqCert.Subject.CommonName, deviceID) { return nil } } } // TODO: Latest version of the MDM client stack don't populate TLS.PeerCertificates array // This is a temporary workaround to allow the management request to proceed // Transport-level security should be replaced for Application-level security // Transport-level security is defined in the MS-MDM spec in section 1.3.1 // On the other hand, Application-level security is defined here // https://www.openmobilealliance.org/release/DM/V1_2_1-20080617-A/OMA-TS-DM_Security-V1_2_1-20080617-A.pdf // The initial values for Application-level security configuration are defined in the // WAP Profile blob that is sent to the device during the enrollment process. Example below // // // // // // // // // // // // // if len(reqCerts) == 0 { return nil } return errors.New("calling device is not trusted") } // regex to validate UPN var upnRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) // isFleetdPresentOnDevice checks if the device requires Fleetd to be deployed func (svc *Service) isFleetdPresentOnDevice(ctx context.Context, deviceID string) (bool, error) { // checking first if the device was enrolled through programmatic flow enrolledDevice, err := svc.ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, deviceID) if err != nil { return false, ctxerr.Wrap(ctx, err, "get windows enrolled device") } // If user identity is a MS-MDM UPN it means that the device was enrolled through user-driven flow // This means that fleetd might not be installed if isValidUPN(enrolledDevice.MDMEnrollUserID) { return false, nil } // TODO: Add check here to determine if MDM DeviceID is connected with Smbios UUID present on // host table. This new check should look into command results table and extract the value of // ./DevDetail/Ext/Microsoft/SMBIOSSerialNumber for the given DeviceID and use that for hosts // table lookup return true, nil } func (svc *Service) enqueueInstallFleetdCommand(ctx context.Context, deviceID string) error { secrets, err := svc.ds.GetEnrollSecrets(ctx, nil) if err != nil { return ctxerr.Wrap(ctx, err, "getting enroll secrets") } if len(secrets) == 0 { level.Warn(svc.logger).Log("msg", "unable to find a global enroll secret to install fleetd") return nil } appCfg, err := svc.ds.AppConfig(ctx) if err != nil { return ctxerr.Wrap(ctx, err, "getting app config") } fleetURL := appCfg.ServerSettings.ServerURL globalEnrollSecret := secrets[0].Secret addCommandUUID := uuid.NewString() execCommandUUID := uuid.NewString() rawAddCmd := []byte(` ` + addCommandUUID + ` ` + mdm.FleetdWindowsInstallerGUID + ` `) // keeping the same GUID will prevent the MSI to be installed multiple times - it will be // installed only the first time the message is issued. // FleetURL and FleetSecret properties are passed to the Fleet MSI rawExecCmd := []byte(` ` + execCommandUUID + ` ` + mdm.FleetdWindowsInstallerGUID + ` https://download.fleetdm.com/fleetd-base.msi 9F89C57D1B34800480B38BD96186106EB6418A82B137A0D56694BF6FFA4DDF1A /quiet FLEET_URL="` + fleetURL + `" FLEET_SECRET="` + globalEnrollSecret + `" 10 1 5 text/plain xml `) // TODO: add ability to batch-enqueue multiple commands at the same time addFleetdCmd := &fleet.MDMWindowsCommand{ CommandUUID: addCommandUUID, RawCommand: rawAddCmd, TargetLocURI: mdm.FleetdWindowsInstallerGUID, } if err := svc.ds.MDMWindowsInsertCommandForHosts(ctx, []string{deviceID}, addFleetdCmd); err != nil { return ctxerr.Wrap(ctx, err, "insert add command to install fleetd") } execFleetCmd := &fleet.MDMWindowsCommand{ CommandUUID: execCommandUUID, RawCommand: rawExecCmd, TargetLocURI: mdm.FleetdWindowsInstallerGUID, } if err := svc.ds.MDMWindowsInsertCommandForHosts(ctx, []string{deviceID}, execFleetCmd); err != nil { return ctxerr.Wrap(ctx, err, "insert exec command to install fleetd") } return nil } // Alerts Handlers // New session Alert Handler // This handler will return an protocol command to install an MSI on a new session from unenrolled device func (svc *Service) processNewSessionAlert(ctx context.Context, messageID string, deviceID string, cmd mdm_types.ProtoCmdOperation) error { // Checking if fleetd is present on the device fleetdPresent, err := svc.isFleetdPresentOnDevice(ctx, deviceID) if err != nil { return err } if !fleetdPresent { return svc.enqueueInstallFleetdCommand(ctx, deviceID) } return nil } // Generic Alert Handlers // This handler will check for generic alerts. Device unenrollment is handled here func (svc *Service) processGenericAlert(ctx context.Context, messageID string, deviceID string, cmd mdm_types.ProtoCmdOperation) error { // Checking user-initiated unenrollment request if len(cmd.Cmd.Items) > 0 { for _, item := range cmd.Cmd.Items { if item.Meta == nil || item.Meta.Type == nil || item.Meta.Type.Content == nil { continue } // Checking if user-initiated unenrollment request is present if *item.Meta.Type.Content == mdm.AlertUserUnenrollmentRequest { // Deleting the device from the list of enrolled device err := svc.ds.MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx, deviceID) if err != nil { return fmt.Errorf("unenrolling windows device: %w", err) } } } } return nil } // processIncomingAlertsCommands will process the incoming Alerts commands. // These commands don't require an status response. func (svc *Service) processIncomingAlertsCommands(ctx context.Context, messageID string, deviceID string, cmd mdm_types.ProtoCmdOperation) error { if cmd.Cmd.Data == nil { return errors.New("invalid alert command") } // gathering the incoming Alert ID alertID := *cmd.Cmd.Data switch alertID { case mdm.CmdAlertClientInitiatedManagement: return svc.processNewSessionAlert(ctx, messageID, deviceID, cmd) case mdm.CmdAlertServerInitiatedManagement: return svc.processNewSessionAlert(ctx, messageID, deviceID, cmd) case mdm.CmdAlertGeneric: return svc.processGenericAlert(ctx, messageID, deviceID, cmd) } return nil } // processIncomingMDMCmds process the incoming message from the device // It will return the list of operations that need to be sent to the device func (svc *Service) processIncomingMDMCmds(ctx context.Context, deviceID string, reqMsg *fleet.SyncML) ([]*fleet.SyncMLCmd, error) { var responseCmds []*fleet.SyncMLCmd // Get the incoming MessageID reqMessageID, err := reqMsg.GetMessageID() if err != nil { return nil, fmt.Errorf("get incoming msg: %w", err) } // Acknowledge the message header // msgref is always 0 for the header if err = reqMsg.IsValidHeader(); err == nil { ackMsg := NewSyncMLCmdStatus(reqMessageID, "0", mdm.SyncMLHdrName, mdm.CmdStatusOK) responseCmds = append(responseCmds, ackMsg) } if err := svc.ds.MDMWindowsSaveResponse(ctx, deviceID, reqMsg); err != nil { return nil, fmt.Errorf("store incoming msgs: %w", err) } // Iterate over the operations and process them for _, protoCMD := range reqMsg.GetOrderedCmds() { // Alerts, Results and Status don't require a status response switch protoCMD.Verb { case mdm_types.CmdAlert: err := svc.processIncomingAlertsCommands(ctx, reqMessageID, deviceID, protoCMD) if err != nil { return nil, fmt.Errorf("process incoming command: %w", err) } continue case mdm_types.CmdStatus, mdm_types.CmdResults: continue } // CmdStatusOK is returned for the rest of the operations responseCmds = append(responseCmds, NewSyncMLCmdStatus(reqMessageID, protoCMD.Cmd.CmdID, protoCMD.Verb, mdm.CmdStatusOK)) } return responseCmds, nil } // getPendingMDMCmds returns the list of pending MDM commands for the device func (svc *Service) getPendingMDMCmds(ctx context.Context, deviceID string) ([]*mdm_types.SyncMLCmd, error) { pendingCmds, err := svc.ds.MDMWindowsGetPendingCommands(ctx, deviceID) if err != nil { return nil, fmt.Errorf("getting incoming cmds %w", err) } // Converting the pending commands to its target SyncML types var cmds []*mdm_types.SyncMLCmd for _, pendingCmd := range pendingCmds { cmd := new(mdm_types.SyncMLCmd) if err := xml.Unmarshal(pendingCmd.RawCommand, cmd); err != nil { logging.WithErr(ctx, ctxerr.Wrap(ctx, err, "getPendingMDMCmds syncML cmd creation")) continue } cmds = append(cmds, cmd) } return cmds, nil } // createResponseSyncML returns a valid SyncML message func (svc *Service) createResponseSyncML(ctx context.Context, req *fleet.SyncML, responseOps []*mdm_types.SyncMLCmd) (*fleet.SyncML, error) { // Get the DeviceID deviceID, err := req.GetSource() if err != nil || deviceID == "" { return nil, fmt.Errorf("invalid SyncML message %w", err) } // Get SessionID sessionID, err := req.GetSessionID() if err != nil { return nil, fmt.Errorf("session ID processing error %w", err) } // Get MessageID messageID, err := req.GetMessageID() if err != nil { return nil, fmt.Errorf("message ID processing error %w", err) } // Getting the Management endpoint URL appConfig, err := svc.ds.AppConfig(ctx) if err != nil { return nil, fmt.Errorf("appconfig was not available %w", err) } urlManagementEndpoint, err := mdm.ResolveWindowsMDMManagement(appConfig.ServerSettings.ServerURL) if err != nil { return nil, ctxerr.Wrap(ctx, err, "resolve management endpoint") } // Create the SyncML message with the response operations msg, err := createSyncMLMessage(sessionID, messageID, deviceID, urlManagementEndpoint, responseOps) if err != nil { return nil, ctxerr.Wrap(ctx, err, "creation of SyncML message") } return msg, nil } // getManagementResponse returns a valid SyncML response message func (svc *Service) getManagementResponse(ctx context.Context, reqMsg *fleet.SyncML) (*mdm_types.SyncML, error) { if reqMsg == nil { return nil, fleet.NewInvalidArgumentError("syncml req message", "message is not present") } // Get the DeviceID deviceID, err := reqMsg.GetSource() if err != nil || deviceID == "" { return nil, fmt.Errorf("invalid SyncML message %w", err) } // Process the incoming MDM protocol commands and get the response MDM protocol commands resIncomingCmds, err := svc.processIncomingMDMCmds(ctx, deviceID, reqMsg) if err != nil { return nil, fmt.Errorf("message processing error %w", err) } // Process the pending operations and get the MDM response protocol commands resPendingCmds, err := svc.getPendingMDMCmds(ctx, deviceID) if err != nil { return nil, fmt.Errorf("message processing error %w", err) } // Combined cmd responses resCmds := append(resIncomingCmds, resPendingCmds...) // Create the response SyncML message msg, err := svc.createResponseSyncML(ctx, reqMsg, resCmds) if err != nil { return nil, fmt.Errorf("message syncML creation error %w", err) } return msg, nil } // removeWindowsDeviceIfAlreadyMDMEnrolled removes the device if already MDM enrolled // HW DeviceID is used to check the list of enrolled devices func (svc *Service) removeWindowsDeviceIfAlreadyMDMEnrolled(ctx context.Context, secTokenMsg *fleet.RequestSecurityToken) error { // Getting the HW DeviceID from the RequestSecurityToken msg reqHWDeviceID, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemHWDevID) if err != nil { return err } // Device is already enrolled, let's remove it err = svc.ds.MDMWindowsDeleteEnrolledDevice(ctx, reqHWDeviceID) if err != nil { if fleet.IsNotFound(err) { return nil } return err } return nil } // getDeviceProvisioningInformation returns a valid WapProvisioningDoc // This is the provisioning information that will be sent to the Windows MDM Enrollment Client // This information is used to configure the device management client // See section 2.2.9.1 for more details on the XML provision schema used here // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/35e1aca6-1b8a-48ba-bbc0-23af5d46907a func (svc *Service) getDeviceProvisioningInformation(ctx context.Context, secTokenMsg *fleet.RequestSecurityToken) (string, error) { // Getting the HW DeviceID from the RequestSecurityToken msg reqHWDeviceID, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemHWDevID) if err != nil { return "", err } // Getting the EnrollmentType information from the RequestSecurityToken msg reqEnrollType, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemEnrollmentType) if err != nil { return "", err } // Getting the BinarySecurityToken from the RequestSecurityToken msg binSecurityTokenData, err := secTokenMsg.GetBinarySecurityTokenData() if err != nil { return "", err } // Getting the BinarySecurityToken type from the RequestSecurityToken msg binSecurityTokenType, err := secTokenMsg.GetBinarySecurityTokenType() if err != nil { return "", err } // Getting the client CSR request from the device clientCSR, err := mdm.GetClientCSR(binSecurityTokenData, binSecurityTokenType) if err != nil { return "", err } // Getting the signed, DER-encoded certificate bytes and its uppercased, hex-endcoded SHA1 fingerprint rawSignedCertDER, rawSignedCertFingerprint, err := svc.SignMDMMicrosoftClientCSR(ctx, reqHWDeviceID, clientCSR) if err != nil { return "", err } // Preparing client certificate and identity certificate information to be sent to the Windows MDM Enrollment Client certStoreProvisioningData := NewCertStoreProvisioningData( reqEnrollType, svc.wstepCertManager.IdentityFingerprint(), svc.wstepCertManager.IdentityCert().Raw, rawSignedCertFingerprint, rawSignedCertDER) // Preparing the provisioning information that includes the location of the Device Management Service (DMS) appCfg, err := svc.ds.AppConfig(ctx) if err != nil { return "", err } // Getting the MS-MDM management URL to provision the device urlManagementEndpoint, err := mdm.ResolveWindowsMDMManagement(appCfg.ServerSettings.ServerURL) if err != nil { return "", err } // Preparing the Application Provisioning information appConfigProvisioningData := NewApplicationProvisioningData(urlManagementEndpoint) // Preparing the DM Client Provisioning information appDMClientProvisioningData := NewDMClientProvisioningData() // And finally returning the Base64 encoded representation of the Provisioning Doc XML provDoc := NewProvisioningDoc(certStoreProvisioningData, appConfigProvisioningData, appDMClientProvisioningData) encodedProvDoc, err := provDoc.GetEncodedB64Representation() if err != nil { return "", err } return encodedProvDoc, nil } // storeWindowsMDMEnrolledDevice stores the device information to the list of MDM enrolled devices func (svc *Service) storeWindowsMDMEnrolledDevice(ctx context.Context, userID string, hostUUID string, secTokenMsg *fleet.RequestSecurityToken) error { const ( error_tag = "windows MDM enrolled storage: " ) // Getting the DeviceID context information from the RequestSecurityToken msg reqDeviceID, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemDeviceID) if err != nil { return fmt.Errorf("%s %v", error_tag, err) } // Getting the HWDevID context information from the RequestSecurityToken msg reqHWDevID, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemHWDevID) if err != nil { return fmt.Errorf("%s %v", error_tag, err) } // Getting the Enroll DeviceType context information from the RequestSecurityToken msg reqDeviceType, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemDeviceType) if err != nil { return fmt.Errorf("%s %v", error_tag, err) } // Getting the Enroll DeviceName context information from the RequestSecurityToken msg reqDeviceName, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemDeviceName) if err != nil { return fmt.Errorf("%s %v", error_tag, err) } // Getting the Enroll RequestVersion context information from the RequestSecurityToken msg reqEnrollVersion, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemRequestVersion) if err != nil { reqEnrollVersion = "request_version_not_present" } // Getting the RequestVersion context information from the RequestSecurityToken msg reqAppVersion, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemApplicationVersion) if err != nil { return fmt.Errorf("%s %v", error_tag, err) } // Getting the EnrollmentType information from the RequestSecurityToken msg reqEnrollType, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemEnrollmentType) if err != nil { return fmt.Errorf("%s %v", error_tag, err) } // Getting the Windows Enrolled Device Information enrolledDevice := &fleet.MDMWindowsEnrolledDevice{ MDMDeviceID: reqDeviceID, MDMHardwareID: reqHWDevID, MDMDeviceState: mdm.MDMDeviceStateEnrolled, MDMDeviceType: reqDeviceType, MDMDeviceName: reqDeviceName, MDMEnrollType: reqEnrollType, MDMEnrollUserID: userID, // This could be Host UUID or UPN email MDMEnrollProtoVersion: reqEnrollVersion, MDMEnrollClientVersion: reqAppVersion, MDMNotInOOBE: false, HostUUID: hostUUID, } if err := svc.ds.MDMWindowsInsertEnrolledDevice(ctx, enrolledDevice); err != nil { return err } err = svc.ds.NewActivity(ctx, nil, &fleet.ActivityTypeMDMEnrolled{ HostDisplayName: reqDeviceName, MDMPlatform: fleet.MDMPlatformMicrosoft, }) if err != nil { // only logging, the device is enrolled at this point, and we // wouldn't want to fail the request because there was a problem // creating an activity feed item. logging.WithExtras(logging.WithNoUser(ctx), "msg", "failed to generate windows MDM enrolled activity", ) } return nil } // GetContextItem returns the context item from the RequestSecurityToken message func GetContextItem(secTokenMsg *fleet.RequestSecurityToken, contextItem string) (string, error) { reqHWDeviceID, err := secTokenMsg.GetContextItem(contextItem) if err != nil { return "", fmt.Errorf("%s token context information is not present: %v", contextItem, err) } return reqHWDeviceID, nil } // GetAuthorizedSoapFault authorize the request so SoapFault message can be returned func (svc *Service) GetAuthorizedSoapFault(ctx context.Context, eType string, origMsg int, errorMsg error) *fleet.SoapFault { svc.authz.SkipAuthorization(ctx) logging.WithErr(ctx, ctxerr.Wrap(ctx, errorMsg, "soap fault")) soapFault := NewSoapFault(eType, origMsg, errorMsg) return &soapFault } func (svc *Service) SignMDMMicrosoftClientCSR(ctx context.Context, subject string, csr *x509.CertificateRequest) ([]byte, string, error) { if svc.wstepCertManager == nil { return nil, "", errors.New("windows mdm identity keypair was not configured") } cert, fpHex, err := svc.wstepCertManager.SignClientCSR(ctx, subject, csr) if err != nil { return nil, "signing wstep client csr", ctxerr.Wrap(ctx, err) } // TODO: if desired, the signature of this method can be modified to accept a device UUID so // that we can associate the certificate with the host here by calling // svc.wstepCertManager.AssociateCertHash return cert, fpHex, nil } // MS-MDM Commands helpers // createSyncMLMessage takes input data and returns a SyncML struct func createSyncMLMessage(sessionID string, msgID string, deviceID string, source string, protoCommands []*mdm_types.SyncMLCmd) (*mdm_types.SyncML, error) { // Sanity check on input if len(sessionID) == 0 || len(msgID) == 0 || len(deviceID) == 0 || len(source) == 0 { return nil, errors.New("invalid parameters") } if sessionID == "0" { return nil, errors.New("invalid session ID") } if msgID == "0" { return nil, errors.New("invalid msg ID") } if len(protoCommands) == 0 { return nil, errors.New("invalid operations") } // Setting source LocURI var sourceLocURI *mdm_types.LocURI if len(source) > 0 { sourceLocURI = &mdm_types.LocURI{ LocURI: &source, } } // setting up things on the SyncML message var msg mdm_types.SyncML msg.Xmlns = mdm.SyncCmdNamespace msg.SyncHdr = mdm_types.SyncHdr{ VerDTD: mdm.SyncMLSupportedVersion, VerProto: mdm.SyncMLVerProto, SessionID: sessionID, MsgID: msgID, Target: &mdm_types.LocURI{LocURI: &deviceID}, Source: sourceLocURI, } // iterate over operations and append them to the SyncML message for _, protoCmd := range protoCommands { msg.AppendCommand(fleet.MDMRaw, *protoCmd) } // If there was no error, return the SyncML and a nil error return &msg, nil } // newSyncMLCmdWithNoItem creates a new SyncML command func newSyncMLCmdWithNoItem(cmdVerb *string, cmdData *string) *mdm_types.SyncMLCmd { return &mdm_types.SyncMLCmd{ XMLName: xml.Name{Local: *cmdVerb}, Data: cmdData, Items: nil, } } // newSyncMLCmdWithItem creates a new SyncML command func newSyncMLCmdWithItem(cmdVerb *string, cmdData *string, cmdItem *mdm_types.CmdItem) *mdm_types.SyncMLCmd { return &mdm_types.SyncMLCmd{ XMLName: xml.Name{Local: *cmdVerb}, Data: cmdData, Items: []mdm_types.CmdItem{*cmdItem}, } } // newSyncMLItem creates a new SyncML command func newSyncMLItem(cmdSource *string, cmdTarget *string, cmdDataType *string, cmdDataFormat *string, cmdDataValue *string) *mdm_types.CmdItem { var metaFormat *mdm_types.MetaAttr var metaType *mdm_types.MetaAttr var meta *mdm_types.Meta if cmdDataFormat != nil && len(*cmdDataFormat) > 0 { metaFormat = &mdm_types.MetaAttr{ XMLNS: "syncml:metinf", Content: cmdDataFormat, } } if cmdDataType != nil && len(*cmdDataType) > 0 { metaType = &mdm_types.MetaAttr{ XMLNS: "syncml:metinf", Content: cmdDataType, } } if metaFormat != nil || metaType != nil { meta = &mdm_types.Meta{ Format: metaFormat, Type: metaType, } } return &mdm_types.CmdItem{ Meta: meta, Data: cmdDataValue, Target: cmdTarget, Source: cmdSource, } } // NewSyncMLCmd creates a new SyncML command func NewSyncMLCmd(cmdVerb string, cmdSource string, cmdTarget string, cmdDataType string, cmdDataFormat string, cmdDataValue string) *mdm_types.SyncMLCmd { var workCmdVerb *string var workCmdSource *string var workCmdTarget *string var workCmdDataType *string var workCmdDataFormat *string var workCmdDataValue *string if len(cmdVerb) > 0 { workCmdVerb = &cmdVerb } if len(cmdSource) > 0 { workCmdSource = &cmdSource } if len(cmdTarget) > 0 { workCmdTarget = &cmdTarget } if len(cmdDataType) > 0 { workCmdDataType = &cmdDataType } if len(cmdDataFormat) > 0 { workCmdDataFormat = &cmdDataFormat } if len(cmdDataValue) > 0 { workCmdDataValue = &cmdDataValue } item := newSyncMLItem(workCmdSource, workCmdTarget, workCmdDataType, workCmdDataFormat, workCmdDataValue) return newSyncMLCmdWithItem(workCmdVerb, nil, item) } func NewTypedSyncMLCmd(dataType mdm_types.SyncMLDataType, cmdVerb string, cmdTarget string, cmdData string) (*mdm_types.SyncMLCmd, error) { errInvalidParameters := errors.New("invalid parameters") // Checking if command verb is present if cmdVerb == "" { return nil, errInvalidParameters } // Returning command based on input command data type switch dataType { case mdm_types.SFEmpty: if len(cmdData) > 0 { rawCmd := newSyncMLNoItem(cmdVerb, cmdData) return rawCmd, nil } return nil, errInvalidParameters case mdm_types.SFNoFormat: if len(cmdData) > 0 && len(cmdTarget) > 0 { rawCmd := newSyncMLNoFormat(cmdVerb, cmdTarget) return rawCmd, nil } return nil, errInvalidParameters case mdm_types.SFText: if len(cmdData) > 0 && len(cmdTarget) > 0 && len(cmdData) > 0 { rawCmd := newSyncMLCmdText(cmdVerb, cmdTarget, cmdData) return rawCmd, nil } return nil, errInvalidParameters case mdm_types.SFXml: if len(cmdData) > 0 && len(cmdTarget) > 0 && len(cmdData) > 0 { rawCmd := newSyncMLCmdXml(cmdVerb, cmdTarget, cmdData) return rawCmd, nil } return nil, errInvalidParameters case mdm_types.SFInteger: if len(cmdData) > 0 && len(cmdTarget) > 0 && len(cmdData) > 0 { rawCmd := newSyncMLCmdInt(cmdVerb, cmdTarget, cmdData) return rawCmd, nil } return nil, errInvalidParameters case mdm_types.SFBase64: if len(cmdData) > 0 && len(cmdTarget) > 0 && len(cmdData) > 0 { rawCmd := newSyncMLCmdBase64(cmdVerb, cmdTarget, cmdData) return rawCmd, nil } return nil, errInvalidParameters case mdm_types.SFBoolean: if len(cmdData) > 0 && len(cmdTarget) > 0 && len(cmdData) > 0 { rawCmd := newSyncMLCmdBool(cmdVerb, cmdTarget, cmdData) return rawCmd, nil } return nil, errInvalidParameters } return nil, errInvalidParameters } // newSyncMLNoItem creates a new SyncML command with no item // This is used for commands that do not have any items such as Alerts func newSyncMLNoItem(cmdVerb string, cmdData string) *mdm_types.SyncMLCmd { return newSyncMLCmdWithNoItem(&cmdVerb, &cmdData) } // newSyncMLNoFormat creates a new SyncML command with no format // This is used for commands that do not have any data such as Get func newSyncMLNoFormat(cmdVerb string, cmdTarget string) *mdm_types.SyncMLCmd { item := newSyncMLItem(nil, &cmdTarget, nil, nil, nil) return newSyncMLCmdWithItem(&cmdVerb, nil, item) } // newSyncMLCmdText creates a new SyncML command with text data func newSyncMLCmdText(cmdVerb string, cmdTarget string, cmdDataValue string) *mdm_types.SyncMLCmd { cmdType := "text/plain" cmdFormat := "chr" item := newSyncMLItem(nil, &cmdTarget, &cmdType, &cmdFormat, &cmdDataValue) return newSyncMLCmdWithItem(&cmdVerb, nil, item) } // newSyncMLCmdXml creates a new SyncML command with XML data func newSyncMLCmdXml(cmdVerb string, cmdTarget string, cmdDataValue string) *mdm_types.SyncMLCmd { cmdType := "text/plain" cmdFormat := "xml" escapedXML := html.EscapeString(cmdDataValue) item := newSyncMLItem(nil, &cmdTarget, &cmdType, &cmdFormat, &escapedXML) return newSyncMLCmdWithItem(&cmdVerb, nil, item) } // newSyncMLCmdBase64 creates a new SyncML command with Base64 encoded data func newSyncMLCmdBase64(cmdVerb string, cmdTarget string, cmdDataValue string) *mdm_types.SyncMLCmd { cmdFormat := "b64" escapedXML := html.EscapeString(cmdDataValue) item := newSyncMLItem(nil, &cmdTarget, nil, &cmdFormat, &escapedXML) return newSyncMLCmdWithItem(&cmdVerb, nil, item) } // newSyncMLCmdInt creates a new SyncML command with text data func newSyncMLCmdInt(cmdVerb string, cmdTarget string, cmdDataValue string) *mdm_types.SyncMLCmd { cmdType := "text/plain" cmdFormat := "int" item := newSyncMLItem(nil, &cmdTarget, &cmdType, &cmdFormat, &cmdDataValue) return newSyncMLCmdWithItem(&cmdVerb, nil, item) } // newSyncMLCmdBool creates a new SyncML command with text data func newSyncMLCmdBool(cmdVerb string, cmdTarget string, cmdDataValue string) *mdm_types.SyncMLCmd { cmdType := "text/plain" cmdFormat := "bool" item := newSyncMLItem(nil, &cmdTarget, &cmdType, &cmdFormat, &cmdDataValue) return newSyncMLCmdWithItem(&cmdVerb, nil, item) } // NewSyncMLCmdStatus creates a new SyncML command with text data func NewSyncMLCmdStatus(msgRef string, cmdRef string, cmdOrig string, statusCode string) *mdm_types.SyncMLCmd { return &mdm_types.SyncMLCmd{ XMLName: xml.Name{Local: mdm_types.CmdStatus}, MsgRef: &msgRef, CmdRef: &cmdRef, Cmd: &cmdOrig, Data: &statusCode, Items: nil, CmdID: uuid.NewString(), } }