Provisioning X.509 Devices for Azure IoT Hub using .NET Core

This article shows how Azure device provisioning service can be used to setup an Azure IoT Hub and provision devices using X.509 certificates in an enrollment group. The certificates are created using the Nuget package CertificateManager. By using this package, the X.509 certificates can be created in .NET Core and created on the fly as needed.

Code: https://github.com/damienbod/AzureIoTHubDps

History

2023-03-27 Updated to .NET 7, fix some DPS group provisioning issues.

Setting up the Project

The demo project is a .NET Core console project, and the Microsoft.Azure.Devices Nuget packages are added as well as the CertificateManager package. The Azure packages can be used for Azure DPS and also Azure IoT Hub. The Azure IoT C# SDK has lots of examples, and the code in this demo was built based on these.

  • CertificateManager
  • Microsoft.Azure.Devices
  • Microsoft.Azure.Devices.Provisioning.Service
  • Microsoft.Azure.Devices.Client
  • Microsoft.Azure.Devices.Provisioning.Client
  • Microsoft.Azure.Devices.Provisioning.Transport.Amqp
  • Microsoft.Azure.Devices.Provisioning.Transport.Http
  • Microsoft.Azure.Devices.Provisioning.Transport.Mqtt

Creating the X.509 certificates

A X.509 chain is created from a self signed root certificate. This certificate is then used to create the intermediate certificates which will be used to create the Azure device provisioning service enrollment groups. The certificates are exported as pfx files and also the public key in the pem format. The pem file will be used to setup the Azure IoT Hub and the Azure DPS.

using CertificateManager;
using CertificateManager.Models;
using Microsoft.Extensions.DependencyInjection;

var serviceProvider = new ServiceCollection()
    .AddCertificateManager()
    .BuildServiceProvider();

string password = "1234";
var cc = serviceProvider.GetService<CreateCertificatesClientServerAuth>();
var iec = serviceProvider.GetService<ImportExportCertificate>();
if (cc == null) throw new ArgumentNullException(nameof(cc));
if (iec == null) throw new ArgumentNullException(nameof(iec));

var dpsCa = cc.NewRootCertificate(
    new DistinguishedName { CommonName = "dpsCa", Country = "CH" },
    new ValidityPeriod { ValidFrom = DateTime.UtcNow, ValidTo = DateTime.UtcNow.AddYears(10) },
    3, "dpsCa");
dpsCa.FriendlyName = "developement root certificate";

var dpsIntermediate1 = cc.NewIntermediateChainedCertificate(
    new DistinguishedName { CommonName = "dpsIntermediate1", Country = "CH" },
    new ValidityPeriod { ValidFrom = DateTime.UtcNow, ValidTo = DateTime.UtcNow.AddYears(10) },
    2, "dpsIntermediate1", dpsCa);
dpsIntermediate1.FriendlyName = "dpsIntermediate1 certificate";

var dpsIntermediate2 = cc.NewIntermediateChainedCertificate(
    new DistinguishedName { CommonName = "dpsIntermediate2", Country = "CH" },
    new ValidityPeriod { ValidFrom = DateTime.UtcNow, ValidTo = DateTime.UtcNow.AddYears(10) },
    2, "dpsIntermediate2", dpsCa);
dpsIntermediate2.FriendlyName = "dpsIntermediate2 certificate";

// EXPORTS PFX

var rootCertInPfxBtyes = iec.ExportRootPfx(password, dpsCa);
File.WriteAllBytes("dpsCa.pfx", rootCertInPfxBtyes);

var dpsIntermediate1Btyes = iec.ExportChainedCertificatePfx(password, dpsIntermediate1, dpsCa);
File.WriteAllBytes("dpsIntermediate1.pfx", dpsIntermediate1Btyes);

var dpsIntermediate2Btyes = iec.ExportChainedCertificatePfx(password, dpsIntermediate2, dpsCa);
File.WriteAllBytes("dpsIntermediate2.pfx", dpsIntermediate2Btyes);

Console.WriteLine("Certificates exported to pfx and cer files");

// EXPORTS PEM

var dpsCaPEM = iec.PemExportPublicKeyCertificate(dpsCa);
File.WriteAllText("dpsCa.pem", dpsCaPEM);

var dpsIntermediate1PEM = iec.PemExportPublicKeyCertificate(dpsIntermediate1);
File.WriteAllText("dpsIntermediate1.pem", dpsIntermediate1PEM);

var dpsIntermediate2PEM = iec.PemExportPublicKeyCertificate(dpsIntermediate2);
File.WriteAllText("dpsIntermediate2.pem", dpsIntermediate2PEM);

Create an Azure Device Provisioning service

Create a new Azure Device Provisioning service. You can use the Azure UI for this, or automate this using arm templates, or Azure cli.

Then create an Azure IoT Hub and connect this to the Azure DPS. See the Azure docs for this. Add the root certificate to the Certificates in the Azure IoT Hub and also the Azure DPS. The pem file with the certificate public key is used to do this. This also needs to be verified. Again, see the Azure documentation for this. The demo code provides a console application which creates the verify certificate for the Azure process.

Create an Enrollment group

An enrollment group can be created using the intermediate certificate created above. The devices will be registered to this group. This is useful, if you need to manage the devices per group, for example per customer, country, etc.

private static async Task CreateEnrollmentGroup(
	string enrollmentGroup, X509Certificate2 groupCertificate)
{
	if (_sp == null) throw new ArgumentNullException(nameof(_sp));

	var cc = _sp.GetService<CreateCertificatesClientServerAuth>();
	var dpsEnrollmentGroup = _sp.GetService<DpsEnrollmentGroup>();
	var iec = _sp.GetService<ImportExportCertificate>();

	if (cc == null) throw new ArgumentNullException(nameof(cc));
	if (dpsEnrollmentGroup == null) 
		throw new ArgumentNullException(nameof(dpsEnrollmentGroup));
	if (iec == null) throw new ArgumentNullException(nameof(iec));

	await dpsEnrollmentGroup.CreateDpsEnrollmentGroupAsync(
		enrollmentGroup, new X509Certificate2(groupCertificate));
}

The method CreateDpsEnrollmentGroupAsync can be used to create a new enrollment group.

The enrollment group can be viewed in the Azure portal UI.

It would also be possible to create the certificate on the fly, and use this to create the Azure DPS enrollment group. Then this process could be automated. You would need to save the certificate somewhere for later use, otherwise you cannot add device registrations to the group.

Enroll a device

The device can be created be creating a X.509 certificate using the device ID (which must be lowercase). The device is registered to the enrollment group, and this then creates a new Azure IoT Hub device.

private static async Task<X509Certificate2> CreateGroupEnrollmentDeviceAsync(
	string commonNameDeviceId, 
	X509Certificate2 dpsGroupCertificate, 
	string password)
{
	if (_sp == null) throw new ArgumentNullException(nameof(_sp));

	var cc = _sp.GetService<CreateCertificatesClientServerAuth>();
	var dpsRegisterDevice = _sp.GetService<DpsRegisterDevice>();
	var iec = _sp.GetService<ImportExportCertificate>();

	if (cc == null) throw new ArgumentNullException(nameof(cc));
	if (dpsRegisterDevice == null) 
		throw new ArgumentNullException(nameof(dpsRegisterDevice));
	if (iec == null) throw new ArgumentNullException(nameof(iec));

	commonNameDeviceId = commonNameDeviceId.ToLower();

	var device = cc.NewDeviceChainedCertificate(
	  new DistinguishedName { CommonName = $"{commonNameDeviceId}" },
	  new ValidityPeriod { ValidFrom = DateTime.UtcNow, 
		ValidTo = DateTime.UtcNow.AddYears(10) },
	  $"{commonNameDeviceId}", dpsGroupCertificate);
	device.FriendlyName = $"IoT device {commonNameDeviceId}";
	
	var deviceInPfxBytes = iec.ExportChainedCertificatePfx(
		password, device, dpsGroupCertificate);
	var deviceCert = new X509Certificate2(deviceInPfxBytes, password);

	await dpsRegisterDevice.RegisterDeviceAsync(deviceCert, dpsGroupCertificate);

	// Save File to use in IoC device
	File.WriteAllBytes($"{commonNameDeviceId}.pfx", deviceInPfxBytes);
	var devicePEM = iec.PemExportPublicKeyCertificate(device);
	File.WriteAllText($"{commonNameDeviceId}.pem", devicePEM);

	return device;
}

The RegisterDeviceAsync method registers the device with the certificate using the device certificate and the enrollment certificate.

using Microsoft.Azure.Devices.Provisioning.Client;
using Microsoft.Azure.Devices.Provisioning.Client.Transport;
using Microsoft.Azure.Devices.Shared;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Security.Cryptography.X509Certificates;

namespace DpsManagement;

public class DpsRegisterDevice
{
    private IConfiguration Configuration { get; set; }
    private readonly ILogger<DpsRegisterDevice> _logger;

    public DpsRegisterDevice(IConfiguration config, ILoggerFactory loggerFactory)
    {
        Configuration = config;
        _logger = loggerFactory.CreateLogger<DpsRegisterDevice>();
    }

    /// <summary>
    /// transport exception if the Common Name "CN=" value within the device x.509 certificate does not match the Group Enrollment name within DPS.
    /// https://github.com/Azure/azure-iot-sdk-c/blob/main/tools/CACertificates/CACertificateOverview.md
    /// </summary>
    public async Task<DeviceRegistrationResult> RegisterDeviceAsync(
        X509Certificate2 deviceCertificate,
        X509Certificate2 enrollmentCertificate)
    {
        var scopeId = Configuration["ScopeId"];

        using (var security = new SecurityProviderX509Certificate(deviceCertificate, new X509Certificate2Collection(enrollmentCertificate)))

        // To optimize for size, reference only the protocols used by your application.
        using (var transport = new ProvisioningTransportHandlerAmqp(TransportFallbackType.TcpOnly))
        //using (var transport = new ProvisioningTransportHandlerHttp())
        //using (var transport = new ProvisioningTransportHandlerMqtt(TransportFallbackType.TcpOnly))
        // using (var transport = new ProvisioningTransportHandlerMqtt(TransportFallbackType.WebSocketOnly))
        {
            var client = ProvisioningDeviceClient.Create("global.azure-devices-provisioning.net", scopeId, security, transport);

            var result = await client.RegisterAsync();
            _logger.LogInformation("DPS client created: {result}", result);
            return result;
        }
    }
}

The registered devices can be viewed in the enrollment group.

Disable a device in the Azure IoT Hub

If an Azure IoT Hub device needs to be disabled, the RegistryManager can be used to update the device on the hub. The device status is set to DeviceStatus.Disabled. You could also delete the device which would also prevent data from being sent.

public async Task DisableDeviceAsync(string deviceId)
{
	var device = await _registryManager.GetDeviceAsync(deviceId);
	device.Status = DeviceStatus.Disabled;
	device = await _registryManager.UpdateDeviceAsync(device);
	_logger.LogInformation("iot hub device disabled  {device}", device);
}

Disable an enrollment group

The enrollment can be disabled, which prevents new devices from being registered. The ProvisioningServiceClient is used for this. This will not prevent data from being sent on the Azure IoT Hub devices already registered.

public async Task DisableEnrollmentGroupAsync(string enrollmentGroupId)
{
	var groupEnrollment = await 
		_provisioningServiceClient.GetEnrollmentGroupAsync(enrollmentGroupId);

	if (groupEnrollment != null && groupEnrollment.ProvisioningStatus != null 
		&& groupEnrollment.ProvisioningStatus.Value != ProvisioningStatus.Disabled)
	{
		groupEnrollment.ProvisioningStatus = ProvisioningStatus.Disabled;
		var update = await _provisioningServiceClient
			.CreateOrUpdateEnrollmentGroupAsync(groupEnrollment);
		_logger.LogInformation("DisableEnrollmentGroupAsync update.ProvisioningStatus: ",
			update.ProvisioningStatus);
	}
}

It is very easy to setup a certificate chain, and add Azure IoT devices using the Device Provisioning service. Now that this is setup, the next step would be to define how the devices can be updated and send data from the cloud, and to the cloud. Then the data from the devices can be consumed and routed to the next Azure service.

Links

https://docs.microsoft.com/en-us/azure/iot-hub/

https://docs.microsoft.com/en-us/azure/iot-dps/

https://github.com/damienbod/AspNetCoreCertificates

https://github.com/Azure/azure-iot-sdk-csharp

3 comments

  1. […] Provisioning X.509 Devices for Azure IoT Hub using .NET Core – Damien Bowden […]

  2. i am facing an issue
    Microsoft.Azure.Devices.Provisioning.Client.ProvisioningTransportException: ‘{“errorCode”:401002,”trackingId”:”fea203a3-788c-48b1-89f8-606da1fbaafd”,”message”:”Signing certificate info did not match chain elements.”,”timestampUtc”:”2020-09-23T15:47:54.3825606Z”}’

    1. Fixed this in the latest version, greetings Damien

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.