'Group enrollment in Azure Device Provisoning Service (DPS) not working through REST API: unauthorized 401002

I am trying to support DPS for my ESP32 firmware through HTTPS REST API using SAS.

My device registration ID is: xx-xx-8c4b14149ff4

I took the group enrollment from DPS primary key to generate the symmetric key from the registration ID.

I created the related SAS from that and forged the request, but the server returns "Unauthorized" with error code being 401002.

Here is my request (a curl version is provided for handiness ):

curl -L -i -X PUT \
-H 'Content-Type: application/json' \
-H 'Content-Encoding: utf-8' \
-H 'Authorization: SharedAccessSignature sr=0neXXXXXX22%2Fregistrations%xx-xx-8c4b14149ff4&sig=XXXXXXXXXXXXXXXXXXX%3D&skn=registration&se=1651482003' \
-d '{"registrationId": "xx-xx-8c4b14149ff4"}' \
https://global.azure-devices-provisioning.net/0neXXXXXX22/registrations/xx-xx-8c4b14149ff4/register?api-version=2021-06-01

Note that I replaced secret information with "xx".

The response body is as follows:

{
    "errorCode": 401002,
    "trackingId": "9fecada7-4e51-455e-9392-68522654a64a",
    "message": "Unauthorized",
    "timestampUtc": "2022-05-02T08:04:03.5761437Z"
}

Is there anything I must tweak to use the HTTPS REST API from the portal?

What should I look at beside the information themselves (which I already double-checked)?

References:



Solution 1:[1]

Actually my signature code had a small bug. I fixed it and now it works.

Here is the (working) code for signing:

#include <mbedtls/md.h>         // mbed tls lib used to sign SHA-256

#include <base64.hpp>           // Densaugeo Base64 version 1.2.0 or 1.2.1


/// Returns the SHA-256 signature of [dataToSign] with the key [enrollmentPrimaryKey]
/// params[in]: dataToSign The data to sign (for our purpose, it is the registration ID (or the device ID if it is different)
/// params[in]: enrollmentPrimaryKey The group enrollment primary key.
/// returns The SHA-256 base-64 signature to present to DPS.
/// Note: I use mbed to SHA-256 sign.
String Sha256Sign(String dataToSign, String enrollmentPrimaryKey){
  /// Length of the dataToSign string
  const unsigned dataToSignLength = dataToSign.length();
  /// Buffer to hold the dataToSign as a char[] buffer from String.
  char dataToSignChar[dataToSignLength + 1];
  /// String to c-style string (char[])
  dataToSign.toCharArray(dataToSignChar, dataToSignLength + 1);

  /// The binary decoded key (from the base 64 definition)
  unsigned char decodedPSK[32];

  /// Encrypted binary signature
  unsigned char encryptedSignature[32];

  /// Base 64 encoded signature
  unsigned char encodedSignature[100];
  
  Serial.printf("Sha256Sign(): Registration Id to sign is: (%d bytes) %s\n", dataToSignLength, dataToSignChar);
  Serial.printf("Sha256Sign(): DPS group enrollment primary key is: (%d bytes) %s\n", enrollmentPrimaryKey.length(), enrollmentPrimaryKey.c_str());


  // Need to base64 decode the Preshared key and the length
  const unsigned base64DecodedDeviceLength = decode_base64((unsigned char*)enrollmentPrimaryKey.c_str(), decodedPSK);
  Serial.printf("Sha256Sign(): Decoded primary key is: (%d bytes) ", base64DecodedDeviceLength);

  for(int i= 0; i<base64DecodedDeviceLength; i++) {
    Serial.printf("%02x ", (int)decodedPSK[i]);
  }
  Serial.println();
  
  // Use mbed to sign
  mbedtls_md_type_t mdType = MBEDTLS_MD_SHA256;
  mbedtls_md_context_t hmacKeyContext;    

  mbedtls_md_init(&hmacKeyContext);
  mbedtls_md_setup(&hmacKeyContext, mbedtls_md_info_from_type(mdType), 1);
  mbedtls_md_hmac_starts(&hmacKeyContext, (const unsigned char *) decodedPSK, base64DecodedDeviceLength);
  mbedtls_md_hmac_update(&hmacKeyContext, (const unsigned char *) dataToSignChar, dataToSignLength);
  mbedtls_md_hmac_finish(&hmacKeyContext, encryptedSignature);
  mbedtls_md_free(&hmacKeyContext);
  
  Serial.print("Sha256Sign(): Computed hash is: ");

  for(int i= 0; i<sizeof(encryptedSignature); i++) {
    Serial.printf("%02x ", (int)encryptedSignature[i]);
  }
  Serial.println();
  // base64 decode the HMAC to a char
  encode_base64(encryptedSignature, sizeof(encryptedSignature), encodedSignature);

  Serial.printf("Sha256Sign(): Computed hash as base64: %s\n", encodedSignature);

  // creating the real SAS Token
  return String((char*)encodedSignature);
}

See it there: How to generate a symmetric key in C or C++ the same way this script does?

Solution 2:[2]

Can you provide a code sample of how you are generating the individual key? Did you try the samples at https://docs.microsoft.com/en-us/azure/iot-dps/how-to-legacy-device-symm-key?tabs=linux%22%20%5Cl%20%22derive-a-device-key#derive-a-device-key ?

Here's an example using C#

        private string GenerateDeviceSas(string enrollmentGroupName, string dpsIdScope, string registrationId, string key)
        {
            if(String.IsNullOrEmpty(dpsIdScope) || String.IsNullOrEmpty(registrationId) || String.IsNullOrEmpty(key))
            {
                Console.WriteLine("Error: Missing required values in settings.json");
                return null;
            }

            if (String.IsNullOrEmpty(enrollmentGroupName))
            {
                // Generate Device API SAS key for individual enrollment
                return GenerateSasToken($"{dpsIdScope}/registrations/{registrationId}", key, "registration");
            }
            else
            {

                // Generate derived Device API SAS key for group enrollment
                // See https://docs.microsoft.com/en-us/azure/iot-dps/how-to-legacy-device-symm-key?tabs=linux#create-a-symmetric-key-enrollment-group

                HMACSHA256 hmacsha256 = new HMACSHA256();
                hmacsha256.Key = Convert.FromBase64String(key);
                var sig = hmacsha256.ComputeHash(ASCIIEncoding.ASCII.GetBytes(registrationId));
                var derivedkey = Convert.ToBase64String(sig);

                return GenerateSasToken($"{dpsIdScope}/registrations/{registrationId}", derivedkey, "registration");

            }
        }

        private static string GenerateSasToken(string resourceUri, string key, string policyName, int expiryInSeconds = 3600)
        {
            TimeSpan fromEpochStart = DateTime.UtcNow - new DateTime(1970, 1, 1);
            string expiry = Convert.ToString((int)fromEpochStart.TotalSeconds + expiryInSeconds);

            string stringToSign = WebUtility.UrlEncode(resourceUri) + "\n" + expiry;

            HMACSHA256 hmac = new HMACSHA256(Convert.FromBase64String(key));
            string signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));

            string token = String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}", WebUtility.UrlEncode(resourceUri), WebUtility.UrlEncode(signature), expiry);

            if (!String.IsNullOrEmpty(policyName))
            {
                token += "&skn=" + policyName;
            }

            return token;
        }

Solution 3:[3]

Consider the following and note how I calculate a deviceKey from the DPS key and then generate the SAS Token from the device key. Some people overlook that. :)

from base64 import b64encode, b64decode
from hashlib import sha256
from time import time
from urllib.parse import urlencode, quote_plus
from hmac import HMAC
import requests

def generate_sas_token(uri, key, policy_name, expiry=3600):
    ttl = time() + expiry
    sign_key = "%s\n%d" % ((quote_plus(uri)), int(ttl))
    sign_key = sign_key.encode('utf-8')
    signature = b64encode(HMAC(b64decode(key), sign_key, sha256).digest())

    rawtoken = {
        'sr' :  uri,
        'sig': signature,
        'se' : str(int(ttl))
    }
    
    if policy_name:
        rawtoken['skn'] = policy_name

    return 'SharedAccessSignature ' + urlencode(rawtoken)

device_id = "3c71bfa95f7c"
scope_id = "0neYOURSCOPEHERE39"
dpskey = 'CsBYOURSASCODEHEREXWyfUMl/OA=='

deviceKey = b64encode(HMAC(b64decode(dpskey), device_id.encode('utf-8'), sha256).digest())
uri = scope_id + '/registrations/' + device_id
policy= 'registration'
url = "https://global.azure-devices-provisioning.net/" + scope_id + "/registrations/" + device_id + "/register?api-version=2021-06-01"
headers = {'Authorization': generate_sas_token(uri=uri, key=deviceKey, policy_name=policy), 'User-Agent': 'MicroPython', 'content-type': 'application/json', 'Content-Encoding': 'utf-8'}
data = '{"registrationId" : "' + device_id + '"}'

print(generate_sas_token(uri=uri, key=deviceKey, policy_name=policy))

print(requests.request("PUT", url=url, data=data, headers=headers).json())

The response shows "assigning", which is success. Hope this helps.

C:/Python38-64/python.exe h:/test/sastoken.py
SharedAccessSignature sr=0ne00223A39%2Fregistrations%2F3c71bfa95f7c&sig=UH5KKeREMOVED0vcs%3D&se=1651880438&skn=registration
{'operationId': '4.4cb788ae24922c84.6856a3d0-857f-4c79-a90c-360fdf3355f8', 'status': 'assigning'}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Stéphane de Luca
Solution 2 KevinH
Solution 3 Kevin Saye