Nosari20
Device to EMM secure channel using Azure Function

Device to EMM secure channel using Azure Function

Content

Purpose and reflexion

Sometimes we need to perform custom actions on devices and return data to Intune or perform Intune actions from device, here are some examples:

Unfortunately, for the first and the third example, Intune does not offer any solution to report data to Intune and use it in groups. For the second one, a category can be chosen from Company Portal but it is not available for dedicated devices.

For this reason, I had to find a way to achieve my goals, so I designed a simple architecture with minimal requirements.

Technical architecture

My solution uses Azure Functions and certificate authentication, which is described in the following diagram:

Secure channel

With this system, Graph API credentials are never exposed and clients are limited by features exposed through the Azure Function and no one can impersonate devices because device are authenticated with certificate.

Client certificates

In my case, I use Intune and ,unfortunately, it does not offer the ability to create a free private certificate authority, but I discovered that Intune pushes a certificate which contains Azure DeviceID into each device and we can use it to authenticate devices against our Azure Function. If you are using another MDM solution or push your own certificate use you can obviously use them.

Using Intune, only Windows and macOS have a certificate which can be verified easily (issued Microsoft Intune MDM Device CA). On Android and iOS we only have the MS-Organization-Access issued certificate but we cannot retrieve the root CA cert and so we cannot verify client certs.

In my future examples, I will assume that the client certificate used contains the Intune DeviceID in the subject (i.e., Subject = CN=<INTUNE_DEVICEiD>) and it is issued by Microsoft Intune MDM Device CA. Obviously, you can adapt them easily and will also add some examples which do not have the Intune certificate (e.g., Android).

Azure Function code

I created my Azure Function using the procedure provided in MS documentation Quickstart: Create a PowerShell function in Azure using Visual Studio Code

Certificate-based authentication must be set to required to make the process secure, but the function code must include certificate verification as it is not handled automatically. In the following example I will use the certificate provided by Intune (signed by Microsoft Intune MDM Device CA) which contains the Intune device id.

The file function.json looks like the following

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "Request",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "Response"
    }
  ]
} 

The file run.ps1 which is the main code is the following:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
Using namespace System.Net

# Input bindings are passed in via param block.
Param($Request, $TriggerMetadata)

# Function to check certificate
Function Check-Certificate() {
    $ClientCertificateBase64 = $Request.Headers."X-ARR-ClientCert"

    # Check if certificate is provided
    If (-not $ClientCertificateBase64) {
    
        Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
            StatusCode = [HttpStatusCode]::BadRequest
            Body = "{'status':'error','error':'Certificate not provided in request.'}"
        })

    } Else{

        # If provided, check trust

        # Convert certificate to object
        $ClientCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]([System.Convert]::FromBase64String($ClientCertificateBase64))

        # Declare CA and sub-CA
        ## Microsoft Intune Root Certification Authority
        $ca="MIIFaTCCA1GgAwIBAgIQWhvaqZFB9KVNpmVRc71GWjANBgkqhkiG9w0BAQsFADA4 MTYwNAYDVQQDEy1NaWNyb3NvZnQgSW50dW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBB dXRob3JpdHkwHhcNMjEwODEyMDAwMDAwWhcNMjYwODEyMDAwMDAwWjA4MTYwNAYD VQQDEy1NaWNyb3NvZnQgSW50dW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3Jp dHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDQL63MwZDlW02yCipf dAUGn5Q8sSIc6zjdTkdCRh2SZGVFVLCgFr/EULGw3q7xaOS/mTA2I+koZM+95JvE xfsIy+S7I7rLQjYNuJZVXl0s1xj4hfewF1jk8nWjdEwYfZkbZ+/6pYTOtHg/U3tV seT63tg6Vc94dkUpOmN4tlyuie2qbMbc1VmAyyITcoIFtRspdE9qW1NnwgzGJz/Q KaL2H7LtGyNGwXhH4jSwZa8ZNcwKjJ7wdaG2SNxOgrZsCHv272vugNiGEy4yYwGU CyGOAksOaHEobW87Y92s/q4/5beBa979ZCeH4VSpRo2LcQFH9ZM9r05VpFimg8Pu pcOl2g1WcuBj1MBBjT/YPaVgbZUp9wvqqefhW6m/JGTYaPJ4YrnMnrzem6kwGP5K 3xwHWL/5ANTjWDx6+b7dZ5f1AKf2DQFaxslMk4CqTElABI8Te1QzTG/QOmSzewtV cwcWGo4F7yJyBbnYcqvN5XJWURFjecuoTvyxPG3R2ljfTKSyJhrTkP0hw3Zxr3cn pY0ZtK80mKE0A9pFG597Qy3q/9TJNRtDjeQrr7DCPwQ8cvfeyT568J+hUyAeN7PV hOV/OXkWXZND9bbkYKNE4ebfiNTGCeJX9iIGjZfbzz86eDqEVpgHcJstnWN6VWbR qkrK/p6m5fqYNZCMAN+g4OyHZQIDAQABo28wbTAdBgNVHQ4EFgQUK0nOsQclTJ3m CpPzkLkw9+ZEn5IwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQEw FgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI hvcNAQELBQADggIBAGrkO1h+8wKHJHCMf/jRz5DhvigyDOyohx2LlbhL/i/0uSka /u/m9uhi0UXXYX9Y15tpRUjZDqtQAjpnFePNcxe3/RRp5kuqVHaMyOHShjTTJ6YD hKhlZP37qSBuWv+x2RsUrPzVdpFyljJWyJ1yruBJqgJ2sq3lskb8cO94fhcc2StT uO9aB+0YY2ce5OHtOgj39Enx+PRCpweIodhAKdZuTVAX1M4qeBJD87Gg/7b4VfaS aM6Frzrh9VuylCM258Xx1CYGgzTYJCcvCYOHA74nS3XigalsKonbdVHUEac+4D7i P33JSlV1wlxYPPJqayiBam21YtSHmdJKV3pwkFbvlX2+pNioX86E48YaNz4faq3v Cl4xHqMfVfOOG8QLiOnNlHsBKsffD420CKi2SJaKETPJnOLG61265jiT4Yr1mUeW G+tQTquFeFdTTSGfToyXE58IMLhI19hQtf/2HU9aZK/vJsjWPYKCucPCXwQZA2Kk Z/RT8HsSPPet3GyP3gL0nzfacohJ7RKwClE82exXgiGK/UAFqvEL9pwbLJtX4Hx/ +OhT4zQ9CSppKjSBDIRR8gV6G2HY5gWKXq9K+/Dv+m1APPWsR1kTsqy+tAQPRxlK 7bDsXt5GawcCP+FM0vJwd5O+ZPB3x5VLG5OLv48cCRUxf1alMQSfsJz/r2L4"
        ## Microsoft Intune MDM Device CA
        $subca="MIIEfTCCAmWgAwIBAgIQI7X6IR1qWadNFd0MPsJsLjANBgkqhkiG9w0BAQsFADA4 MTYwNAYDVQQDEy1NaWNyb3NvZnQgSW50dW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBB dXRob3JpdHkwHhcNMjEwODEyMDAwMDAwWhcNMjQwODEyMDAwMDAwWjApMScwJQYD VQQDEx5NaWNyb3NvZnQgSW50dW5lIE1ETSBEZXZpY2UgQ0EwggEiMA0GCSqGSIb3 DQEBAQUAA4IBDwAwggEKAoIBAQDR82LMD3kKPMoBlLcYWUdsklJfCVnvSos1pCNq UVWBxIgOIXd8ypG7XY1YUE44GjDqPBShdQWJmNDGIfqfLWo0grnajWQMefXEa+CA pKD/GvozAIHfYETgwi+YMx3EyNWupfXY4nf/fz2T4xldHuu9iZw4Ty2+Rz1Vg22Y EYbCXShyOqsFG7rHANPs9XkjXWpcWhnNiRAkq999YuIC0aKkD9UmaTcsd055cmOt 0NkAehfCJiB1t9pqdTceqKRx4VxySSQG/pWwTOL2A9V/eRrx5hZaMDZk3kVcPU/o HluY1xzWCY9PiPuX54WDlFFm4B7zklRifPw0GvP1YX92s2h1AgMBAAGjgZEwgY4w HQYDVR0OBBYEFGdrgkv3vEOqV0qZz17a0+8EUuuVMB8GA1UdIwQYMBaAFCtJzrEH JUyd5gqT85C5MPfmRJ+SMBAGCSsGAQQBgjcVAQQDAgEAMA4GA1UdDwEB/wQEAwIB hjAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMA0G CSqGSIb3DQEBCwUAA4ICAQDOd1wgRJfrtiE4ApbiQKcOSVAK5my9EgWuAEhOzpFG hCyhMpGwm8ZTz6+qhlVmACH9h8AM9mmtw3X/BMHKZNL6C/j8XlE3DQPbP0zJUiLU nAzgjYtGxTy9VxWf4LszVOO6jBCUs3ztri22gkEYnDguVchE+6xP3uQ5jLLjYCMi czT9UiSByKD+IRgTlapF0bDK8FufNX5s2h9aNR8S39kAGQin1cWQmHmu/173QYBG uS+XyuJl/2X9YiQDuPaUNDWrZ3kcwHQLsVF+z8up2PfYUGN+KB1FkzlxyLkpG5X7 oebsY0mc4W69aRdYw6D0GnttbJZvjFhMAjyaZtwJVmCBogM7b7oV6ZtrHEBrUxEc MPQWbiBPCNl6bE8PsKVVoiKmIgje80Wm5bwWw7XCUIFyI/feyzTRZEOf8MQSmpB9 CfvISw0Y5REKGsvgmu06eII1jA78HH6fXOs+L/4+zVxEdjsTryE3iESLkEQ4Do4U p9iDQr+k/5Tcell2GLjXw6EeClkBG64+97gT5PfIXsssKgrsGwkry67tkWriu90Q on84T3uTgK3AqO8wvqaEIbILHsiHDFjsiNntmr0dPMZMXS+pNVyu4FgaREjNlEGl MUgUWBmoM420smDzVzLD48qBJ7hr8suL5vXQL0VSkxJes2G8Cl45d5EgCkJonvkf ZQ=="


        # Create custom chain object to trust only one authority
        $Chain = [System.Security.Cryptography.X509Certificates.X509Chain]::Create()

        ## Do no check if certficate is revoked
        $Chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck

        ## Use custom trust store
        $Chain.ChainPolicy.TrustMode = [System.Security.Cryptography.X509Certificates.X509ChainTrustMode]::CustomRootTrust

        ## Add 'Microsoft Intune MDM Device CA' and 'Microsoft Intune Root Certification Authority' to custom trust store
        $Chain.ChainPolicy.CustomTrustStore.Add([System.Security.Cryptography.X509Certificates.X509Certificate2]([System.Convert]::FromBase64String($subca)))  | Out-Null
        $Chain.ChainPolicy.CustomTrustStore.Add([System.Security.Cryptography.X509Certificates.X509Certificate2]([System.Convert]::FromBase64String($ca)))  | Out-Null

        ## build chain : return true if valid trust chain
        If (-not $Chain.build($ClientCertificate)) {
    
            Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
                StatusCode = [HttpStatusCode]::BadRequest
                Body = "{'status':'error','error':'Certificate not valid ($($Chain.ChainStatus.StatusInformation)).'}"
            })

        } Else {
            $ClientCertificate
        }
    }

}
# Function to extract DeviceId from certificate
Function Read-DeviceId() {
        
    # Convert certificate to object
    $ClientCertificate = Check-Certificate

    # Extract Common Name
    $AzDeviceId = ($ClientCertificate.Subject -replace "([a-z]*=)" -split ",")[0]

    $AzDeviceId   
    
}

# Function to set note using Graph API
Function Set-Note($IntuneDeviceID,$Note) {
    $Bearer = Get-GraphAPIToken

    # Search for device in Intune
    $Body = @{    
        notes = "$Note"
    } | ConvertTo-Json

    $Url = "https://graph.microsoft.com/beta/deviceManagement/managedDevices('$DeviceId')"
    Try {
        
        $NoteEditRequest = (Invoke-RestMethod -Method 'PATCH' -Headers @{Authorization = "Bearer $($Bearer)"} -Uri $Url -Body $Body  -ContentType 'application/json')
        
    } Catch {
        Write-Host "Error for $Url"
        Write-Host "Body:`n$Body"
        Write-Host "StatusCode: $($_.Exception.Response.StatusCode.value__ )"
        Write-Host "StatusDescription: $($_.Exception.Response.StatusDescription)"
        Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
            StatusCode = [HttpStatusCode]::InternalServerError
            Body = "{'status':'error','error':'Internal server error.'}"
        })
        exit
    }   
    $NoteEditRequest      
}

# Function to authenticate and retrieve Graph API OAuth token
Function Get-GraphAPIToken() {
    
    $ApplicationID = "<APP_ID>"
    $TenantID = "<TENANT_ID>"
    $AccessSecret = "<SECRET>"


    $Body = @{    
        Grant_Type    = "client_credentials"
        Scope         = "https://graph.microsoft.com/.default"
        client_Id     = $ApplicationID
        Client_Secret = $AccessSecret
    } 

    Try {
        $ConnectGraph = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" `
        -Method POST -Body $Body
    } Catch {
        Write-Host "Error for $Url"
        Write-Host "Body:`n$Body"
        Write-Host "StatusCode: $($_.Exception.Response.StatusCode.value__ )"
        Write-Host "StatusDescription: $($_.Exception.Response.StatusDescription)"
    }


    If(-not $ConnectGraph.access_token){
        Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
            StatusCode = [HttpStatusCode]::InternalServerError
            Body = "{'status':'error','error':'Internal server error.'}"
        })
        exit
    }

    return $ConnectGraph.access_token
}


# Handle incomming requests
If($Request.Method  -eq "POST" -and $Request.Query.Action -eq "set") {

    # Retrieve DeviceId
    $DeviceId = Read-DeviceId

    ################ Perform actions on Intune using Graph API  ###########################
    # Add Azure AD satus to note
    $response = Set-Note $DeviceId "$($Request.Body.Note)"
    #######################################################################################

    # Associate values to output bindings by calling 'Push-OutputBinding'.
    Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
        StatusCode = [HttpStatusCode]::OK
        Body = $response
    })
    exit
}


# Unknown method / request
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = [HttpStatusCode]::MethodNotAllowed
    Body = $body
}) 

Clients

Windows

Set AzureAD Status to notes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#Requires -RunAsAdministrator

# Load certficate
$ClientCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My\" | Where-Object {$_.Subject -like "*Microsoft Intune MDM Device CA*"}

# Send request
$Url = "<AZURE-FUNCTION_URL>"

$Body = @{
            # Send Azure AD status to device 'notes' field
    Note = "$(dsregcmd /status | select -First 10)"
} | ConvertTo-Json

Try {
    # Perform request
    $response = (Invoke-RestMethod -Method 'POST' -Uri "$Url?Action=set" -Body $Body -ContentType 'application/json' -Certificate $ClientCertificate)
} Catch {

    "Error for $Url"
    "Body:`n$Body"
    "Body:`n$($_.Exception.Response.Body)"
    "StatusCode: $($_.Exception.Response.StatusCode.value__ )"
    "StatusDescription: $($_.Exception.Response.StatusDescription)"
} 

macOS

Important: at the moment, I have not found a solution to allow a specific app to use a private key using a script or a profile, so a pop-up will appear at first run to allow curl to use the private key. Alternatively, you can push your own certificate and allow all apps to use it.

Update 1/26/2024: Here is a full example in another post

Set MDM profile to notes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/bin/bash

# Search certficate
function findCertAlias { 
    /usr/bin/security find-certificate -a -p > /tmp/certs.pem
    while read line; do
        if [[ "$line" == *"--BEGIN"* ]]; then
            cert=$line
        else
            cert="$cert"$'\n'"$line"
            if [[ "$line" == *"--END"* ]]; then
                echo "$cert" > /tmp/checkcert.pem
                subject=$(openssl x509 -subject -noout -in /tmp/checkcert.pem | cut -d= -f 3)
                issuer=$(openssl x509 -issuer -noout -in /tmp/checkcert.pem | cut -d= -f 3)
                if [[ "$issuer" == "Microsoft Intune MDM Device CA" ]]; then
                    echo $subject
                    break
                fi

            fi
        fi
    done < /tmp/certs.pem
    rm -f /tmp/certs.pem
    rm -f /tmp/checkcert.pem
}

certAlias=$(findCertAlias)


# Send request
Url="<AZURE-FUNCTION_URL>"
response=$(CURL_SSL_BACKEND=secure_transport curl -X POST $Url --cert "$certAlias" -H "Content-Type: application/json" -d "{'Note':'$(profiles status -type enrollment)'}" -sS --write-out '|%{http_code}\n')
RC=$?

if [[ ${RC} == 0 ]]
then

    IFS='|'
    read -a response <<< "$response"

    if [[ ${response[1]} != 200 ]]
    then
        echo "Error for ${Url}"
        echo "HTTP Code: ${response[1]}"
        echo "Body: ${response[0]}"
    fi

else

    echo "Error for ${Url}"
    echo "Exit Code: ${RC}"
    echo "Output: $response"
fi 

Android (snippet)

Note: use your own certificate

Set Bluetooth name to note

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

// Load certificate
val privKey = KeyChain.getPrivateKey(this.requireContext(), test.certAlias)
val pubKey = KeyChain.getCertificateChain(this.requireContext(), test.certAlias)

if (privKey == null) {
    // requestAliasPermission(); Ask user to allow certificate access
    return
}

// Define the certificate alias to be used
val certAlias = "<YOUR_ALIAS>"

// Create custom KeyManager
val keyManager = object:X509KeyManager {

    override fun getCertificateChain(alias: String?): Array<X509Certificate> {
        return pubKey!!
    }

    override fun getPrivateKey(alias: String?): PrivateKey {
        return privKey!!
    }

    override fun chooseClientAlias(keyType: Array<out String>?, issuers: Array<out Principal>?, socket: Socket ): String {
        return certAlias
    }

    override fun getClientAliases(
        keyType: String?,
        issuers: Array<out Principal>?
    ): Array<String> {
        TODO("Not yet implemented")
    }

    override fun getServerAliases(
        keyType: String?,
        issuers: Array<out Principal>?
    ): Array<String> {
        TODO("Not yet implemented")
    }

    override fun chooseServerAlias(
        keyType: String?,
        issuers: Array<out Principal>?,
        socket: Socket?
    ): String {
        TODO("Not yet implemented")
    }

}

// Create custom SSL context to use certificate
var sslContext = SSLContext.getInstance("TLS")
sslContext.init(arrayOf<KeyManager>(km), null, null)
sslFactory = sslContext.socketFactory


// Create client with custom context
val okHttpClient = OkHttpClient.Builder()
        .sslSocketFactory(sslFactory.getSslSocketFactory(), sslFactory.getTrustManager().get())
        .build()

// Create request
val name = Settings.System.getString(requireContext().contentResolver, "bluetooth_name")

val Url = "<AZURE-FUNCTION_URL>"
var formBody = FormBody.Builder()
      .add("note", name) // Request all categories with their ids with the same principle
      .build()

var request = Request.Builder()
    .url(Url)
    .build()


// Send request
var call = okHttpClient.newCall(request)
call.enqueue(object:Callback {
    fun onResponse(call:Call, response:Response){
        Log.d("SET-NOTE",response.body()?.string())
    }
    
    fun onFailure(call: Call, e:IOException) {
        TODO("Not yet implemented")
    }
}) 

Note: macOS scripts will be reuseable (with some changes) for Linux when Intune will include script execution for this OS.