package mdmtest
import (
"bytes"
"crypto/tls"
"encoding/base64"
"encoding/xml"
"fmt"
"io"
"net/http"
"strconv"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/fleet"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
)
type TestWindowsMDMClient struct {
// DeviceID identifies a MDM enrollment, sent and managed by the device.
DeviceID string
// hardwareID identifies a device.
hardwareID string
// fleetServerURL is the URL of the Fleet server, used to ping the MDM endpoints.
fleetServerURL string
// debug enables debug logging of request/responses.
debug bool
// enrollmentType is used to simulate different Windows enrollment
// types (programatic, automatic.)
enrollmentType fleet.WindowsMDMEnrollmentType
// tokenIdentifier is used for authentication during the programmatic enrollment.
tokenIdentifier string
// lastManagementResp tracks the last response we received from the server.
lastManagementResp *fleet.SyncML
// queuedCommandResponses tracks the commands that will be sent next
// time the device responds to the server.
queuedCommandResponses map[string]fleet.SyncMLCmd
}
// TestWindowsMDMClientOption allows configuring a
// TestWindowsMDMClient.
type TestWindowsMDMClientOption func(*TestWindowsMDMClient)
// TestWindowsMDMClientDebug configures the TestWindowsMDMClient to
// run in debug mode.
func TestWindowsMDMClientDebug() TestWindowsMDMClientOption {
return func(c *TestWindowsMDMClient) {
c.debug = true
}
}
func NewTestMDMClientWindowsProgramatic(serverURL string, orbitNodeKey string, opts ...TestWindowsMDMClientOption) *TestWindowsMDMClient {
c := TestWindowsMDMClient{
fleetServerURL: serverURL,
DeviceID: uuid.NewString(),
enrollmentType: fleet.WindowsMDMProgrammaticEnrollmentType,
tokenIdentifier: orbitNodeKey,
hardwareID: uuid.NewString(),
}
for _, fn := range opts {
fn(&c)
}
return &c
}
func NewTestMDMClientWindowsAutomatic(serverURL string, email string, opts ...TestWindowsMDMClientOption) *TestWindowsMDMClient {
c := TestWindowsMDMClient{
fleetServerURL: serverURL,
DeviceID: uuid.NewString(),
enrollmentType: fleet.WindowsMDMAutomaticEnrollmentType,
tokenIdentifier: email,
hardwareID: uuid.NewString(),
}
for _, fn := range opts {
fn(&c)
}
return &c
}
func (c *TestWindowsMDMClient) StartManagementSession() (map[string]fleet.ProtoCmdOperation, error) {
// Get SessionID
sessionIDInt := 0
if c.lastManagementResp != nil {
sessionID, err := c.lastManagementResp.GetSessionID()
if err != nil {
return nil, fmt.Errorf("session ID processing error %w", err)
}
sessionIDInt, err = strconv.Atoi(sessionID)
if err != nil {
return nil, fmt.Errorf("converting session ID to int: %w", err)
}
}
managementReq := []byte(`
1.2
DM/1.2
` + fmt.Sprint(sessionIDInt+1) + `
1
` + c.fleetServerURL + microsoft_mdm.MDE2ManagementPath + `
2
1201
3
1224
-
com.microsoft/MDM/LoginStatus
user
4
-
` + c.DeviceID + `
-
VMware, Inc.
-
VMware7,1
-
1.3
-
en-US
`)
return c.doManagementReq(managementReq)
}
func (c *TestWindowsMDMClient) doManagementReq(rawXMLReq []byte) (map[string]fleet.ProtoCmdOperation, error) {
if c.debug {
fmt.Println("=============== management request ================")
fmt.Println(string(rawXMLReq))
}
// TODO: this request works because we're allowing devices without
// certificates to communicate with the server. We will need to include the
// certificate we generated during enrollment when we fix that.
managementResp, err := c.request(microsoft_mdm.MDE2ManagementPath, rawXMLReq)
if err != nil {
return nil, err
}
rawXMLResp, err := io.ReadAll(managementResp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}
if c.debug {
fmt.Println("=============== management response ================")
fmt.Println(string(rawXMLResp))
}
var syncML fleet.SyncML
if err := xml.Unmarshal(rawXMLResp, &syncML); err != nil {
return nil, fmt.Errorf("unmarshalling response body: %w", err)
}
c.lastManagementResp = &syncML
cmds := make(map[string]fleet.ProtoCmdOperation)
for _, p := range c.lastManagementResp.GetOrderedCmds() {
cmds[p.Cmd.CmdID.Value] = p
}
// before returning, clean up any lingering responses
c.queuedCommandResponses = make(map[string]fleet.SyncMLCmd)
return cmds, nil
}
func (c *TestWindowsMDMClient) SendResponse() (map[string]fleet.ProtoCmdOperation, error) {
// Get SessionID
sessionID, err := c.lastManagementResp.GetSessionID()
if err != nil {
return nil, fmt.Errorf("session ID processing error %w", err)
}
// Get MessageID
messageID, err := c.lastManagementResp.GetMessageID()
if err != nil {
return nil, fmt.Errorf("message ID processing error %w", err)
}
messageIDInt, err := strconv.Atoi(messageID)
if err != nil {
return nil, fmt.Errorf("converting message ID to int: %w", err)
}
var msg fleet.SyncML
msg.Xmlns = syncml.SyncCmdNamespace
msg.SyncHdr = fleet.SyncHdr{
VerDTD: syncml.SyncMLSupportedVersion,
VerProto: syncml.SyncMLVerProto,
SessionID: sessionID,
MsgID: fmt.Sprint(messageIDInt + 1),
Source: &fleet.LocURI{LocURI: &c.DeviceID},
Target: &fleet.LocURI{
LocURI: ptr.String(c.fleetServerURL + microsoft_mdm.MDE2ManagementPath),
},
}
// iterate over mocked responses and append them to the SyncML message
for _, protoCmd := range c.queuedCommandResponses {
msg.AppendCommand(fleet.MDMRaw, protoCmd)
}
xmlReq, err := xml.MarshalIndent(msg, "", "\t")
if err != nil {
return nil, fmt.Errorf("serializing XML req: %w", err)
}
return c.doManagementReq(xmlReq)
}
// AppendResponse sets a response for a specific command UUID.
func (c *TestWindowsMDMClient) AppendResponse(op fleet.SyncMLCmd) {
c.queuedCommandResponses[op.CmdID.Value] = op
}
func (c *TestWindowsMDMClient) GetCurrentMsgID() (string, error) {
msgID, err := c.lastManagementResp.GetMessageID()
if err != nil {
return "", fmt.Errorf("getting management response msg id: %w", err)
}
return msgID, nil
}
func (c *TestWindowsMDMClient) Enroll() error {
if err := c.Discovery(); err != nil {
return err
}
if err := c.Policy(); err != nil {
return err
}
binarySecToken, tokenValueType, err := c.getToken()
if err != nil {
return fmt.Errorf("getting binary token: %w", err)
}
enrollReq := []byte(`
http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RST/wstep
urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749
http://www.w3.org/2005/08/addressing/anonymous
` + c.fleetServerURL + microsoft_mdm.MDE2EnrollPath + `
` + binarySecToken + `
http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken
http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue
MIIC5jCCAc4CAQAwSjFIMEYGA1UEAww/MEYzQjhFNkMtQTI3MS00NTU2LTlCNzIt
QTI2Q0JEITgwOTBDOEI0ODRBMEUyNEVCNUM1NkU4MDZDQjRFRTVCMIIBIjANBgkq
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoLj7gBWVMPiVsbrB13jW86bB/Rz+bAOj
J9MxMwuwOtbPicESpReZ7QgjNhv5tTubLCHRlIRhcawxPOhpZCZTRolT/3q2xhYT
3WnW8uLiPLTyQpmoI66yfMAUlNfKboeFpgMB6GCM3FColmQBHzWrPulY5zUSwBFs
YwogoSKVH9ekAv5FQZpqW8zj9tTU1t7U1qMwyb03u1+7JGJ0lBBjCDkoMCB0sSVO
Fybg//zsHqdYs876jnh8qH6GG8XUVrCk4PYX/b1Fak9D4DcedCQ/sDlsxB1i4TjY
apbduFo9/wc/OL9KVBk2LWPXvwV0/EWggx4QFZpaabeJy5J0CbdpvQIDAQABoFcw
VQYJKoZIhvcNAQkOMUgwRjATBgNVHSUEDDAKBggrBgEFBQcDAjAvBgorBgEEAYI3
QgEABCE4MDkwQzhCNDg0QTBFMjRFQjVDNTZFODA2Q0I0RUU1QgAwDQYJKoZIhvcN
AQELBQADggEBAFwiNxM90FippSvLgoqMw9TpyoSTD2hftPW+bpGA1OxxBmSwCwI9
oE7/6bMLX9k9iBt6QaQomWp6Gh+Rpuz0uzHp32TLbuV87//awydG8meyU6GMVZ6R
xfIAH4rmdhJ9ccpnugSLMYr3+UKLWSOjeTB2ZKcVx7LTsHzqaDg3ghJDSNx12wSY
LmEKCHDR1FNPcXB6hfs3CfJOnJhcOX+Gg2GrqjAEA2ty2rEJ9LVZo0Q3A7pfEezs
YioVozr1IWYySwWVzMf/SUwKZkKJCAJmSVcixE+4kxPkyPGyauIrN3wWC0zb+mjF
3aJBpJrK45UhKb1LOBHOtV7BsoEkOUNmCdQ=
false
` + c.hardwareID + `
en-US
true
48
DESKTOP-H1T20J1
A2-19-7D-41-B3-9C
` + c.DeviceID + `
Full
CIMClient_Windows
10.0.22598.1
10.0.22598.1
false
5.0
`)
if _, err := c.request(microsoft_mdm.MDE2EnrollPath, enrollReq); err != nil {
return err
}
return nil
}
func (c *TestWindowsMDMClient) Discovery() error {
discoveryReq := []byte(`
http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/Discover
urn:uuid:748132ec-a575-4329-b01b-6171a9cf8478
http://www.w3.org/2005/08/addressing/anonymous
` + c.fleetServerURL + microsoft_mdm.MDE2DiscoveryPath + `
5.0
CIMClient_Windows
6.2.9200.1
48
OnPremise
Federated
`)
// 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 {
return err
}
return nil
}
func (c *TestWindowsMDMClient) Policy() error {
binarySecToken, tokenValueType, err := c.getToken()
if err != nil {
return fmt.Errorf("getting binary token: %w", err)
}
policyReq := []byte(`
http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy/IPolicy/GetPolicies
urn:uuid:72048B64-0F19-448F-8C2E-B4C661860AA0
http://www.w3.org/2005/08/addressing/anonymous
` + c.fleetServerURL + microsoft_mdm.MDE2PolicyPath + `
` + binarySecToken + `
IBM
8217.4131.22.13878
`)
// 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 {
return err
}
return nil
}
func (c *TestWindowsMDMClient) request(path string, reqBody []byte) (*http.Response, error) {
request, err := http.NewRequest("POST", c.fleetServerURL+path, bytes.NewReader(reqBody))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
cc := fleethttp.NewClient(fleethttp.WithTLSClientConfig(&tls.Config{
// Ignoring "G402: TLS InsecureSkipVerify set true", this is only used for automated testing.
InsecureSkipVerify: true, //nolint:gosec
}))
response, err := cc.Do(request)
if err != nil {
return nil, fmt.Errorf("send request: %w", err)
}
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("request error: %d, %s", response.StatusCode, response.Status)
}
return response, nil
}
func (c *TestWindowsMDMClient) getToken() (binarySecToken string, tokenValueType string, err error) {
switch c.enrollmentType {
case fleet.WindowsMDMAutomaticEnrollmentType:
claims := &jwt.MapClaims{
"upn": c.tokenIdentifier,
"tid": "tenant_id",
"unique_name": "foo_bar",
"scp": "mdm_delegation",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte("foo"))
if err != nil {
return "", "", err
}
tokenValueType = syncml.BinarySecurityAzureEnroll
binarySecToken = base64.URLEncoding.EncodeToString([]byte(tokenString))
case fleet.WindowsMDMProgrammaticEnrollmentType:
var err error
tokenValueType = syncml.BinarySecurityDeviceEnroll
binarySecToken, err = fleet.GetEncodedBinarySecurityToken(c.enrollmentType, c.tokenIdentifier)
if err != nil {
return "", "", fmt.Errorf("generating encoded security token: %w", err)
}
}
return binarySecToken, tokenValueType, nil
}