basysKom AnwendungsEntwicklung

IoT Hub Device Provisioning Service, bring your IoT Devices into the Cloud. (Part 4 of 4)
Essential Summary
The Device Provisioning Service is a managed service running in the Azure cloud which supports automatic provisioning of IoT devices for IoT Hub. The following sections will give a short introduction to the most important features of the Device Provisioning Service.

Introduction

While it is possible to fully provision an IoT device during production, there are several problems related to load balancing and selecting an IoT Hub in the geographical region where the device is going be used. It would be helpful if the device would just need to know where to get provisioning information and how to authenticate to the provisioning provider.

The Azure IoT Hub Device Provisioning Service

device provisioning serviceThe Device Provisioning Service is a managed service running in the Azure cloud which supports automatic provisioning of IoT devices for IoT Hub.

The following sections will give a short introduction to the most important features of the Device Provisioning Service.

Identifying a Device Provisioning Service instance

Every Device Provisioning Service instance is assigned a so called ID scope on creation which is unique and does not change during the entire lifetime of the instance. It must be used by the IoT devices when querying the Device Provisioning Service.

Enrollments

The basic requirement for provisioning a device via Device Provisioning Service is an enrollment.

Devices can be enrolled in groups using X.509 certificates or static keys. This means that all devices having a certificate signed by the same CA or a static key derived from the same master key have the right to query provisioning information from the Device Provisioning Service.

Individual enrollments are enrollments for a single device. For devices with a Trusted Platform Module (TPM), an individual enrollment can also use TPM attestation. This allows a device having a TPM with a certain endorsement key to connect to the Device Provisioning Service. Challenge-response authentication is employed to verify that the device really is in possession of the private key belonging to the endorsement key and at the same time, the device obtains a token which is used to retrieve the provisioning data from the Device Provisioning Service as well as for authentication to the IoT Hub.

An individual enrollment with TPM attestation is the best way to use the Device Provisioning Service because there is no static key to be stored on the device and no process for creating and renewing certificates is required. The only additional steps required for provisioning a device during production or the end test is to store the ID scope of the Device Provisioning Service on the device and to read out the public part of the TPM’s endorsement key.

The extracted public key is used in a later step to create an individual enrollment for the device in the Device Provisioning Service. This could be made an automatic process, for example in cooperation with an ERP system.

The default device twin

The values for the tags and desired properties of the initial device twin are individually configurable for every enrollment.

IoT Hub assignment

The Device Provisioning Service can be linked to several IoT Hubs in different geographical regions.

An enrollment can be assigned to one of the linked IoT Hubs or a subset of the linked IoT Hubs based on different policies

  • Static assignment to a certain IoT Hub
  • Lowest latency
  • Evenly weighted distribution
  • Call a custom assignment function on an Azure Function App

The behavior of the Device Provisioning Service on subsequent requests by the same device is also configurable. The device can either always use its first IoT Hub or be assigned to a new IoT Hub according to the assignment policy. Reprovisioning can be configured to migrate the device twin or to initialize the device twin on the new IoT Hub with the default value.

Enabling and disabling enrollments

Enrollment groups and individual enrollments can be temporarily disabled, for example to block misbehaving devices. This does not prevent the device from connecting to the IoT Hub it is currently provisioned to, the IoT Hub registration belonging to the device must also be disabled if a device is to be locked out completely.

Provisioning Information

If the client has successfully established a connection with the Device Provisioning Service, it receives the device id and the URL of the IoT Hub it has been assigned to. This information is used to connect to the IoT Hub where additional configuration data is stored in the device twin.

By combining the Device Provisioning Service with configuration data in the device twin, an IoT device can be bootstrapped just from knowing how to reach the Device Provisioning Service.

Client example

To demonstrate how TPM attestation is done with the Azure IoT C SDK, we will extend our Qt based C++ IoT Hub client example.

The full example code is hosted on github.

We will use a simulated TPM which can be downloaded here for Linux users. Windows users can find a pre-built TPM simulator in the azure-iot-sdk-c repository in the provisioning_client/deps/utpm/tools/tpm_simulator directory.

The Azure IoT SDK must be built with the CMake options use_tpm_simulator and use_prov_client set to ON.

Changes to the IotHubClient class

The IotHubClient class is extended with two Device Provisioning Service specific callbacks, the new member variables mProvisioningHandle and mIdScope and several helper functions for connection establishment.
The information passed to init() is changed to the ID scope of the Device Provisioning Service.

The new method named queryTpmInformation() is used to retrieve and print the values needed for creating an enrollment in the Device Provisioning Service.

The destructor and doWork() are extended to deal with the provisioning client.

#include <iothub.h>
#include <iothub_device_client_ll.h>
#include <iothub_client_options.h>
#include <iothub_message.h>
#include <iothubtransportamqp.h>

#include "azure_prov_client/prov_device_ll_client.h"
#include <azure_prov_client/prov_transport_amqp_client.h>
#include <azure_prov_client/prov_security_factory.h>
#include <azure_prov_client/prov_auth_client.h>

class IotHubClient : public QObject
{
    Q_OBJECT

public:
    IotHubClient(QObject *parent = nullptr);
    ~IotHubClient();

    bool init(const QString &idScope);

    bool sendMessage(int id, const QByteArray &data);
    bool updateDeviceTwin(const QJsonObject &reported);

    bool connected() const;

    bool queryTpmInformation();

signals:
    void connectedChanged(bool connected);
    void deviceTwinUpdated(int statusCode);
    void messageStatusChanged(int id, bool success);
    void desiredObjectChanged(QJsonObject desired);

private:
    // Callbacks for the DPS client
    static void registerDeviceCallback(PROV_DEVICE_RESULT result, const char* iotHubUri,
                                  const char* deviceId, void* context);
    static void registrationStatusCallback(PROV_DEVICE_REG_STATUS registrationStatus, void* context);

    // Callbacks for the IoT Hub client
    static void connectionStatusCallback(IOTHUB_CLIENT_CONNECTION_STATUS result,
                                         IOTHUB_CLIENT_CONNECTION_STATUS_REASON reason,
                                         void* context);
    static void sendConfirmCallback(IOTHUB_CLIENT_CONFIRMATION_RESULT result, void* context);
    static void deviceTwinCallback(DEVICE_TWIN_UPDATE_STATE updateState, const unsigned char* payload,
                                   size_t size, void* context);
    static void reportedStateCallback(int statusCode, void* context);

    // Call the IOT SDK's DoWork functions
    void doWork();

    bool requestProvisioningData();
    void initializeIotHub(const QString &iotHubUri, const QString &deviceId);

    void setConnected(bool connected);

    struct MessageContext {
        MessageContext(IotHubClient *client, int id, IOTHUB_MESSAGE_HANDLE message) {
            this->client = client;
            this->id = id;
            this->message = message;
        }
        IotHubClient *client;
        int id;
        IOTHUB_MESSAGE_HANDLE message;
    };

    QTimer mDoWorkTimer;
    IOTHUB_DEVICE_CLIENT_LL_HANDLE mDeviceHandle = nullptr;
    PROV_DEVICE_LL_HANDLE mProvisioningHandle = nullptr;
    QString mIdScope;
    bool mConnected = false;
};

IotHubClient::~IotHubClient()
{
    mDoWorkTimer.stop();
    if (mDeviceHandle)
        IoTHubDeviceClient_LL_Destroy(mDeviceHandle);
    IoTHub_Deinit();

    if (mProvisioningHandle)
        Prov_Device_LL_Destroy(mProvisioningHandle);
    prov_dev_security_deinit();
}

void IotHubClient::doWork()
{
    if (mDeviceHandle)
        IoTHubDeviceClient_LL_DoWork(mDeviceHandle);
    if (mProvisioningHandle)
        Prov_Device_LL_DoWork(mProvisioningHandle);
} 

The device registration callback

The device registration callback is called with the result of the registration process. On success, the device’s device id and the IoT Hub URI are also passed as parameters.

Our implementation triggers the initialization of the IoT Hub client once the provisioning information has been successfully retrieved. In case of an error, a retry is scheduled.

void IotHubClient::registerDeviceCallback(PROV_DEVICE_RESULT result, const char *iotHubUri,
                                          const char *deviceId, void *context)
{
    qDebug() << "Register device callback with result" << result;

    auto client = static_cast<IotHubClient *>(context);

    if (result == PROV_DEVICE_RESULT_OK) {
        qDebug() << "Provisioning information received";
        qDebug() << "IoT Hub:" << iotHubUri << "Device id:" << deviceId;

        const auto uri = QString::fromUtf8(iotHubUri);
        const auto id = QString::fromUtf8(deviceId);

        QTimer::singleShot(1000, client, [=]() {
            client->initializeIotHub(uri, id);
        });
    } else {
        qDebug() << "Provisioning failed, retry";
        QTimer::singleShot(5000, client, [=]() {
            client->requestProvisioningData();
        });
    }
} 

The registration status callback

The registration status callback is called for the different status changes during the registration process. Our implementation just prints this information.

void IotHubClient::registrationStatusCallback(PROV_DEVICE_REG_STATUS registrationStatus, void *context)
{
    Q_UNUSED(context);
    qDebug() << "Registration status callback with status" << registrationStatus;
} 

Printing provisioning information

A registration id and the public part of the TPM’s endorsement key are required to create a enrollment at the Device Provisioning Service.

The prov_auth_get_endorsement_key() function retrieves the public part of the endorsement key.

The registration id is retrieved by calling prov_auth_get_registration_id(). The value is derived from the public part of the endorsement key.

bool IotHubClient::queryTpmInformation()
{
    PROV_AUTH_HANDLE authHandle = prov_auth_create();
    if (!authHandle) {
        qDebug() << "Provisioning authentication handle creation failed";
        return false;
    }

    QByteArray endorsementKey;
    QString registrationId;
    char *registrationIdBuffer = nullptr;

    BUFFER_HANDLE endorsementKeyBuffer = prov_auth_get_endorsement_key(authHandle);

    if (!endorsementKeyBuffer) {
        qDebug() << "Failed to query endorsement key";
        prov_auth_destroy(authHandle);
        return false;
    } else {
        endorsementKey = QByteArray(reinterpret_cast<const char *>(BUFFER_u_char(endorsementKeyBuffer)),
                        BUFFER_length(endorsementKeyBuffer));
        BUFFER_delete(endorsementKeyBuffer);
    }

    registrationIdBuffer = prov_auth_get_registration_id(authHandle);
    if (!endorsementKeyBuffer) {
        qDebug() << "Failed to query registration id";
        prov_auth_destroy(authHandle);
        return false;
    } else {
        registrationId = QString::fromLatin1(registrationIdBuffer);
        free(registrationIdBuffer);
    }

    prov_auth_destroy(authHandle);

    qDebug() << "Registration id:" << registrationId;
    qDebug() << "Endorsement key:" << endorsementKey.toBase64();

    return true;
} 

The modified init()

After initializing the SDK, the queryTpmInformation() method is called and prints the registration id and the endorsement key to the terminal.

bool IotHubClient::init(const QString &idScope)
{
    if (mProvisioningHandle || mDeviceHandle) {
        qDebug() << "Client is already initialized";
        return false;
    }

    mIdScope = idScope;

    auto result = IoTHub_Init();

    if (result != IOTHUB_CLIENT_OK) {
        qWarning() << "IoTHub_Init failed with result" << result;
        return false;
    }

    result = prov_dev_security_init(SECURE_DEVICE_TYPE_TPM);

    if (result != IOTHUB_CLIENT_OK) {
        qWarning() << "prov_dev_security_init failed with result" << result;
        return false;
    }

    queryTpmInformation();

    const auto success = requestProvisioningData();

    if (success)
        mDoWorkTimer.start();

    return success;
} 

Instead of constructing the IoT Hub client directly, the first step for establishing a connection is now to construct a Device Provisioning Service client to retrieve the necessary provisioning information by calling Prov_Device_LL_Create().

The following call to Prov_Device_LL_Register_Device() initializes the provisioning process. The two callbacks described before are passed as parameters.

bool IotHubClient::requestProvisioningData()
{
    if (mProvisioningHandle) {
        Prov_Device_LL_Destroy(mProvisioningHandle);
        mProvisioningHandle = nullptr;
    }

    mProvisioningHandle = Prov_Device_LL_Create("global.azure-devices-provisioning.net",
                                                mIdScope.toLatin1().constData(),
                                                Prov_Device_AMQP_Protocol);

    if (!mProvisioningHandle) {
        qWarning() << "Failed to create DPS client";
        return false;
    }

    auto result = Prov_Device_LL_Register_Device(mProvisioningHandle, registerDeviceCallback, this,
                                                 registrationStatusCallback, this);

    if (result != PROV_DEVICE_RESULT_OK) {
        qWarning() << "Failed to dispatch register device with result" << result;
        return false;
    }

    return true;
} 

Initializing the IoT Hub client

First, the provisioning client must be destroyed to allow the IoT Hub client to access the TPM.

Instead of using IoTHubDeviceClient_LL_CreateFromConnectionString() to create the client, IoTHubDeviceClient_LL_CreateFromDeviceAuth() must be called now.

The rest of the initialization can be left unchanged.

void IotHubClient::initializeIotHub(const QString &iotHubUri, const QString &deviceId)
{
    // Destroy the provisioning client to unblock TPM access
    if (mProvisioningHandle) {
        Prov_Device_LL_Destroy(mProvisioningHandle);
        mProvisioningHandle = nullptr;

        prov_dev_security_deinit();
    }

    // Destroy previous client
    if (mDeviceHandle) {
        IoTHubDeviceClient_LL_Destroy(mDeviceHandle);
        mDeviceHandle = nullptr;
    }

    mDeviceHandle = IoTHubDeviceClient_LL_CreateFromDeviceAuth(iotHubUri.toUtf8().constData(),
                                                               deviceId.toUtf8().constData(),
                                                               AMQP_Protocol);

    if (!mDeviceHandle) {
        qWarning() << "Failed to create client from connection string";
        return;
    }

    qDebug() << "Client created";

    auto result = IoTHubDeviceClient_LL_SetConnectionStatusCallback(mDeviceHandle,
                                                                    connectionStatusCallback,
                                                                    this);

    if (result != IOTHUB_CLIENT_OK) {
        qWarning() << "Failed to set connection status callback with result" << result;
        return;
    }

    result = IoTHubDeviceClient_LL_SetDeviceTwinCallback(mDeviceHandle, deviceTwinCallback, this);

    if (result != IOTHUB_CLIENT_OK) {
        qWarning() << "Failed to set device twin callback with result" << result;
        return;
    }
} 

Changes to main()

The connection string must be replaced by the ID scope of your Device Provisioning Service instance.
const auto idScope = QLatin1String("0neXXXXXXXX");
auto result = client.init(idScope); 

Building the example

The .pro file must be extended to include additional linker flags for the device provisioning specific libraries.
# Linker flags for device provisioning
LIBS += \
    -lprov_device_ll_client \
    -lprov_auth_client \
    -lprov_amqp_transport \
    -lhsm_security_client \
    -lutpm \
    -luamqp \
    -laziotsharedutil \
    -luuid \
    -lmsr_riot \
    -lssl \
    -lcrypto \
    -lcurl \ 

Testing the example

Start the TPM simulator and the example application.

If the initialization is successful, the registration id and the endorsement key are printed to the terminal. Use these values to create an individual enrollment at your Device Provisioning Service instance.

The example should now retrieve and print the provisioning information and then perform the IoT Hub client initialization.

After the client has been successfully connected, it behaves the same as described in the IoT Hub and Protobuf articles.

Conclusion

The Device Provisioning Service is a good supplement to the IoT Hub for bringing IoT devices into our system and managing the load between different IoT Hub instances.

As demonstrated in the example above, it is easy to use the Azure IoT SDK to communicate with the Device Provisioning Service. The only information that was required to bootstrap out IoT device is the ID scope for our Device Provisioning Service instance.

Picture of Jannis Völker

Jannis Völker

Jannis Völker is a software engineer at basysKom GmbH in Darmstadt. After joining basysKom in 2017, he has been working in connectivity projects for embedded devices, Azure based cloud projects and has made contributions to Qt OPC UA and open62541. He has a background in embedded Linux, Qt and OPC UA and holds a master's degree in computer science from the University of Applied Sciences in Darmstadt.

2 Antworten

  1. I got some queries related security.
    What are the challenges while using individual enrollment of many devices instead of group enrollment?
    How to manage TPM and its expiry?

    • Using individual enrollments for many devices means that you need to write your own application to enroll each device at the DPS.
      DPS uses the endorsement key of the TPM which does not expire.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Weitere Blogartikel

basysKom Newsletter

We collect only the data you enter in this form (no IP address or information that can be derived from it). The collected data is only used in order to send you our regular newsletters, from which you can unsubscribe at any point using the link at the bottom of each newsletter. We will retain this information until you ask us to delete it permanently. For more information about our privacy policy, read Privacy Policy