diff --git a/changes/issue-12600-windows-installer b/changes/issue-12600-windows-installer new file mode 100644 index 0000000000..a5cb01d398 --- /dev/null +++ b/changes/issue-12600-windows-installer @@ -0,0 +1 @@ +* Add MSI installer deployement support through MS-MDM diff --git a/changes/issue-12604-azure-tos-endpoint b/changes/issue-12604-azure-tos-endpoint new file mode 100644 index 0000000000..11f6f7a41f --- /dev/null +++ b/changes/issue-12604-azure-tos-endpoint @@ -0,0 +1 @@ +* Adding support for MDM TOS endpoint diff --git a/server/fleet/microsoft_mdm.go b/server/fleet/microsoft_mdm.go index 2ad91adc0c..5cbdd841ee 100644 --- a/server/fleet/microsoft_mdm.go +++ b/server/fleet/microsoft_mdm.go @@ -223,11 +223,6 @@ func (req *SoapRequest) IsValidRequestSecurityTokenMsg() error { return errors.New("invalid requestsecuritytoken message: AdditionalContext.ContextItems missing") } - reqVersion, err := req.Body.RequestSecurityToken.GetContextItem(mdm.ReqSecTokenContextItemRequestVersion) - if err != nil || (reqVersion != mdm.EnrollmentVersionV5 && reqVersion != mdm.EnrollmentVersionV4) { - return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", mdm.ReqSecTokenContextItemRequestVersion, reqVersion, err) - } - reqEnrollType, err := req.Body.RequestSecurityToken.GetContextItem(mdm.ReqSecTokenContextItemEnrollmentType) if err != nil || reqEnrollType != mdm.ReqSecTokenEnrollType { return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", mdm.ReqSecTokenContextItemEnrollmentType, reqEnrollType, err) diff --git a/server/fleet/service.go b/server/fleet/service.go index e540799eaa..98cb462ba5 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -780,4 +780,7 @@ type Service interface { // GetMDMWindowsManagementResponse returns a valid SyncML response message GetMDMWindowsManagementResponse(ctx context.Context, reqSyncML *SyncMLMessage) (*string, error) + + // GetMDMWindowsTOSContent returns TOS content + GetMDMWindowsTOSContent(ctx context.Context, redirectUri string, reqID string) (string, error) } diff --git a/server/mdm/microsoft/microsoft_mdm.go b/server/mdm/microsoft/microsoft_mdm.go index fd3fcbbeb7..2889ff8072 100644 --- a/server/mdm/microsoft/microsoft_mdm.go +++ b/server/mdm/microsoft/microsoft_mdm.go @@ -42,6 +42,9 @@ const ( // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/5b02c625-ced2-4a01-a8e1-da0ae84f5bb7 MDE2ManagementPath = MDMPath + "/management" + // MDE2TOSPath is the HTTP endpoint path that delivers Terms of Service Content + MDE2TOSPath = MDMPath + "/tos" + // These are the entry points for the Microsoft Device Enrollment (MS-MDE) and Microsoft Device Enrollment v2 (MS-MDE2) protocols. // These are required to be implemented by the MDM server to support user-driven enrollments MSEnrollEntryPoint = "/EnrollmentServer/Discovery.svc" @@ -138,6 +141,9 @@ const ( // HTTP Content Type for SyncML MDM responses SyncMLContentType = "application/vnd.syncml.dm+xml" + // HTTP Content Type for Webcontainer responses + WebContainerContentType = "text/html; charset=UTF-8" + // Minimal Key Length for SHA1WithRSA encryption PolicyMinKeyLength = "2048" @@ -232,6 +238,12 @@ const ( // Login related query param expected by STS Auth endpoint STSLoginHint = "login_hint" + // redirect_uri query param expected by TOS endpoint + TOCRedirectURI = "redirect_uri" + + // client-request-id query param expected by TOS endpoint + TOCReqID = "client-request-id" + // Alert Command IDs DeviceUnenrollmentID = "1226" HostInitMessageID = "1201" diff --git a/server/service/handler.go b/server/service/handler.go index 267419a0e9..d5d81b0724 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -614,6 +614,9 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // It should be authenticated through TLS headers once proper implementation is in place neWindowsMDM.POST(microsoft_mdm.MDE2ManagementPath, mdmMicrosoftManagementEndpoint, SyncMLReqMsgContainer{}) + // This endpoint is unauthenticated and is used by to retrieve the MDM enrollment Terms of Use + neWindowsMDM.GET(microsoft_mdm.MDE2TOSPath, mdmMicrosoftTOSEndpoint, MDMWebContainer{}) + ne.POST("/api/fleet/orbit/enroll", enrollOrbitEndpoint, EnrollOrbitRequest{}) // For some reason osquery does not provide a node key with the block data. diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 77aea9c173..a4b836798a 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -6181,6 +6181,22 @@ func (s *integrationMDMTestSuite) TestInvalidGetAuthRequest() { require.Contains(t, resContent, "forbidden") } +func (s *integrationMDMTestSuite) TestValidGetTOC() { + t := s.T() + + resp := s.DoRaw("GET", microsoft_mdm.MDE2TOSPath+"?api-version=1.0&redirect_uri=ms-appx-web%3a%2f%2fMicrosoft.AAD.BrokerPlugin&client-request-id=f2cf3127-1e80-4d73-965d-42a3b84bdb40", nil, http.StatusOK) + + resBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.WebContainerContentType) + + resTOCcontent := string(resBytes) + require.Contains(t, resTOCcontent, "Microsoft.AAD.BrokerPlugin") + require.Contains(t, resTOCcontent, "IsAccepted=true") + require.Contains(t, resTOCcontent, "OpaqueBlob=") +} + func (s *integrationMDMTestSuite) TestValidSyncMLRequestNoAuth() { t := s.T() diff --git a/server/service/microsoft_mdm.go b/server/service/microsoft_mdm.go index 674754ecf7..e0ac9f2ebd 100644 --- a/server/service/microsoft_mdm.go +++ b/server/service/microsoft_mdm.go @@ -10,12 +10,12 @@ import ( "errors" "fmt" "html" - "html/template" "io" "net/http" "net/url" "strconv" "strings" + "text/template" "time" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" @@ -134,6 +134,43 @@ func (r SyncMLResponseMsgContainer) hijackRender(ctx context.Context, w http.Res } } +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) 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 @@ -163,7 +200,7 @@ func getUtcTime(minutes int) string { } // NewDiscoverResponse creates a new DiscoverResponse struct based on the auth policy, policy url, and enrollment url -func NewDiscoverResponse(authPolicy string, policyUrl string, enrollmentUrl string, authUrl *string) (mdm_types.DiscoverResponse, error) { +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") } @@ -175,7 +212,6 @@ func NewDiscoverResponse(authPolicy string, policyUrl string, enrollmentUrl stri EnrollmentVersion: mdm.EnrollmentVersionV4, EnrollmentPolicyServiceUrl: policyUrl, EnrollmentServiceUrl: enrollmentUrl, - AuthServiceUrl: authUrl, }, }, nil } @@ -843,6 +879,32 @@ func mdmMicrosoftManagementEndpoint(ctx context.Context, request interface{}, sv }, 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 func (svc *Service) authBinarySecurityToken(ctx context.Context, authToken *fleet.HeaderBinarySecurityToken) (string, error) { if authToken == nil { @@ -936,18 +998,7 @@ func (svc *Service) GetMDMMicrosoftDiscoveryResponse(ctx context.Context, upnEma return nil, ctxerr.Wrap(ctx, err, "resolve enroll endpoint") } - // Only adding STS Auth endpoint if the UPN email is provided - var urlSTSAuthEndpoint *string - if len(upnEmail) > 0 { - workUrlSTSAuthEndpoint, err := mdm.ResolveWindowsMDMAuth(appCfg.ServerSettings.ServerURL) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "resolve enroll endpoint") - } - - urlSTSAuthEndpoint = &workUrlSTSAuthEndpoint - } - - discoveryMsg, err := NewDiscoverResponse(mdm.AuthOnPremise, urlPolicyEndpoint, urlEnrollEndpoint, urlSTSAuthEndpoint) + discoveryMsg, err := NewDiscoverResponse(mdm.AuthOnPremise, urlPolicyEndpoint, urlEnrollEndpoint) if err != nil { return nil, ctxerr.Wrap(ctx, err, "creation of DiscoverResponse message") } @@ -979,12 +1030,12 @@ func (svc *Service) GetMDMMicrosoftSTSAuthResponse(ctx context.Context, appru st // Dinamically create a form element to submit the request var form = document.createElement('form'); form.method = 'POST'; - form.action = "` + appru + `" + form.action = "{{.ActionURL}}" var inputToken = document.createElement('input'); inputToken.type = 'hidden'; inputToken.name = 'wresult'; - inputToken.value = '` + encodedBST + `'; + inputToken.value = '{{.Token}}'; form.appendChild(inputToken); // Submit the form @@ -1001,7 +1052,7 @@ func (svc *Service) GetMDMMicrosoftSTSAuthResponse(ctx context.Context, appru st } var htmlBuf bytes.Buffer - err = tmpl.Execute(&htmlBuf, map[string][]byte{"ActionURL": []byte(appru), "Token": []byte(encodedBST)}) + err = tmpl.Execute(&htmlBuf, map[string]string{"ActionURL": appru, "Token": encodedBST}) if err != nil { return "", ctxerr.Wrap(ctx, err, "creation of STS content") } @@ -1112,6 +1163,52 @@ func (svc *Service) GetMDMWindowsManagementResponse(ctx context.Context, reqSync return resSyncMLmsg, nil } +// GetMDMWindowsTOSContent returns valid TOC content +func (svc *Service) GetMDMWindowsTOSContent(ctx context.Context, redirectUri string, reqID string) (string, error) { + tmpl, err := template.New("").Parse(` + +
+ + + +
+