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

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.

<PackageReference Include="CertificateManager" Version="1.0.4" />
<PackageReference Include="Microsoft.Azure.Devices" Version="1.19.0" />
<PackageReference Include="Microsoft.Azure.Devices.Provisioning.Service" Version="1.6.0" />
<PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.23.1" />
<PackageReference Include="Microsoft.Azure.Devices.Provisioning.Client" Version="1.5.0" />
<PackageReference Include="Microsoft.Azure.Devices.Provisioning.Transport.Amqp" Version="1.2.0" />

<PackageReference Include="Microsoft.Azure.Devices.Provisioning.Transport.Http" Version="1.2.0" />
<PackageReference Include="Microsoft.Azure.Devices.Provisioning.Transport.Mqtt" Version="1.2.0" />

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.


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

string password = "1234";
var cc = serviceProvider.GetService<CreateCertificatesClientServerAuth>();
var iec = serviceProvider.GetService<ImportExportCertificate>();

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.

public async Task CreateDpsEnrollmentGroupAsync(
	string enrollmentGroupId,
	X509Certificate2 pemCertificate)
{
	_logger.LogInformation("Starting CreateDpsEnrollmentGroupAsync...");
	_logger.LogInformation("Creating a new enrollmentGroup...");
   
	Attestation attestation = X509Attestation.CreateFromRootCertificates(pemCertificate);
	EnrollmentGroup enrollmentGroup = new EnrollmentGroup(enrollmentGroupId, attestation)
	{
		ProvisioningStatus = ProvisioningStatus.Enabled,
		ReprovisionPolicy = new ReprovisionPolicy
		{
			MigrateDeviceData = false,
			UpdateHubAssignment = true
		},
		Capabilities = new DeviceCapabilities
		{
			IotEdge = false
		},
		InitialTwinState = new TwinState(
			new TwinCollection("{ \"updatedby\":\"" + "damien" + "\", \"timeZone\":\"" + TimeZoneInfo.Local.DisplayName + "\" }"),
			new TwinCollection("{ }")
		)
	};
	_logger.LogInformation($"{enrollmentGroup}");
	_logger.LogInformation($"Adding new enrollmentGroup...");

	EnrollmentGroup enrollmentGroupResult = await _provisioningServiceClient
		.CreateOrUpdateEnrollmentGroupAsync(enrollmentGroup)
		.ConfigureAwait(false);

	_logger.LogInformation($"EnrollmentGroup created with success.");
	_logger.LogInformation($"{enrollmentGroupResult}");
}

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

/// -- DPS Create Enrollment Group
var dpsEnrollmentGroup = sp.GetService<DpsEnrollmentGroup>();

var dpsEnrollmentCertificate = 
  new X509Certificate2($"{pathToCerts}dpsIntermediate1.pem");

await dpsEnrollmentGroup
   .CreateDpsEnrollmentGroupAsync(
          "dpsIntermediate1", 
          dpsEnrollmentCertificate
   );

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.

private static async Task<X509Certificate2> CreateEnrollmentGroup(
	string enrollmentGroup, X509Certificate2 parentCert)
{
	var cc = _sp.GetService<CreateCertificatesClientServerAuth>();
	var dpsEnrollmentGroup = _sp.GetService<DpsEnrollmentGroup>();
	var iec = _sp.GetService<ImportExportCertificate>();

	var enrollmentGroupCert = cc.NewIntermediateChainedCertificate(
	   new DistinguishedName { CommonName = enrollmentGroup },
	   new ValidityPeriod { ValidFrom = DateTime.UtcNow, 
               ValidTo = DateTime.UtcNow.AddYears(10) },
	   2, enrollmentGroup, parentCert);
	enrollmentGroupCert.FriendlyName = $"{enrollmentGroup} certificate";

	var enrollmentGroupPEM = 
           iec.PemExportPublicKeyCertificate(enrollmentGroupCert);
	var cert = iec.PemImportCertificate(enrollmentGroupPEM);

	await dpsEnrollmentGroup.CreateDpsEnrollmentGroupAsync(
		enrollmentGroup, new X509Certificate2(cert));
	return enrollmentGroupCert;
}

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> CreateDeviceAsync(
	string deviceId, X509Certificate2 parentCertificate, string password)
{
	deviceId = deviceId.ToLower();
	var cc = _sp.GetService<CreateCertificatesClientServerAuth>();
	var iec = _sp.GetService<ImportExportCertificate>();
	var dpsRegisterDevice = _sp.GetService<DpsRegisterDevice>();

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

	await dpsRegisterDevice.RegisterDeviceAsync(
		deviceCert, 
		parentCertificate);

	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.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

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>();
        }

        public async Task<DeviceRegistrationResult> RegisterDeviceAsync(
            X509Certificate2 deviceCertificate,
            X509Certificate2 enrollmentCertificate)
        {
            var scopeId = Configuration["ScopeId"];
            //// The cert from the enrollment group is required for group registrations
            //X509Certificate2 enrollmentCertificate = new X509Certificate2("dpsIntermediate1.pfx", "1234");

            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}");
                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}");
}

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.ProvisioningStatus.Value != ProvisioningStatus.Disabled)
	{
		groupEnrollment.ProvisioningStatus = ProvisioningStatus.Disabled;
		var update = await 
			_provisioningServiceClient
				.CreateOrUpdateEnrollmentGroupAsync(groupEnrollment);
	}
}

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

One comment

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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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

%d bloggers like this: