As you may have read, I made a previous blog post, which described how to create a Certificate Authority root certificate and a chain of intermediates using a Microsoft provided PowerShell sample.
In that post, I also showed how to create a Device Enrollment Group within the Device Provisioning Service (DPS). That’s all very nice, but the purpose of IoT Hub and the DPS is to connect devices. So let’s go ahead and do that.
First of all, the device needs its own certificate, which is trusted by the root CA certificate configured in the DPS device enrollment group.
Create a device certificate manually
The PowerShell guide from earlier also creates a new device certificate that is signed with the root CA certificate.
This is the “New-CACertsDevice” command. Give it the device name as parameter, and it will create four files named after the device name.
The .pem files are plain text (well, not so plain but you can see it in a text editor).
The most interesting one is the “testdevice-all.pem” file:
The file contains the testdevice private key and certificate and also the root CA certificate (without the private key).
There are a number of “interesting” parts in it. E.g. the attribute called “1.3.6.1.4.1.311.17.3.71” contains the full DNS name of the computer where the certificate was generated.
The long number string “1.3.6.1.4.1.311.17.3.71” is called a OID. OIDs are organized in a hierarchy with different meanings. For example, the first part (“1.3.6.1.4.1.311”) is assigned to Microsoft.
Using OpenSSL (which you should have in your path, if you followed the instructions from the link above), it is possible to get more information out of the certificate file:
The certificate “Issuer” is “Azure IoT CA TestOnly Root CA” – this is the root CA’s name. The “CN” in the screenshot above means “Common Name”.
The certificate’s subject (the name it was issued for) is “testdevice”. The certificate contains information about what it can be used for. The most important part is “TLS Web Client Authentication” – this is what we need to authenticate towards the DPS.
Now, after the long text about chain of trust, intermediate certificate authorities etc. it would be boring to have a device certificate that was simply signed by the root CA.
So run “New-CACertsDevice device “CN=Azure IoT CA TestOnly Intermediate 3 CA”.
This creates a new certificate called for a device called “device”.
Now the “device-all.pem” is much longer, since it contains the certificate of each of the intermediates all the way up to the root CA:
❯ type .\device-all.pem Bag Attributes localKeyID: 01 00 00 00 friendlyName: te-3b7ab179-1752-4e35-bb92-70072e4738e1 Microsoft CSP Name: Microsoft Software Key Storage Provider Key Attributes X509v3 Key Usage: 90 -----BEGIN PRIVATE KEY----- (cut for brevity) -----END PRIVATE KEY----- Bag Attributes 1.3.6.1.4.1.311.17.3.121: 00 localKeyID: 01 00 00 00 1.3.6.1.4.1.311.17.3.71: (cut for privacy) subject=/CN=device issuer=/CN=Azure IoT CA TestOnly Intermediate 3 CA -----BEGIN CERTIFICATE----- (cut for brevity) -----END CERTIFICATE----- Bag Attributes: subject=/CN=Azure IoT CA TestOnly Root CA issuer=/CN=Azure IoT CA TestOnly Root CA -----BEGIN CERTIFICATE----- (cut for brevity) -----END CERTIFICATE----- Bag Attributes 1.3.6.1.4.1.311.17.3.121: 00 1.3.6.1.4.1.311.17.3.71: (cut for privacy) subject=/CN=Azure IoT CA TestOnly Intermediate 1 CA issuer=/CN=Azure IoT CA TestOnly Root CA -----BEGIN CERTIFICATE----- (cut for brevity) -----END CERTIFICATE----- Bag Attributes 1.3.6.1.4.1.311.17.3.121: 00 1.3.6.1.4.1.311.17.3.71: (cut for privacy) subject=/CN=Azure IoT CA TestOnly Intermediate 2 CA issuer=/CN=Azure IoT CA TestOnly Intermediate 1 CA -----BEGIN CERTIFICATE----- (cut for brevity) -----END CERTIFICATE----- Bag Attributes 1.3.6.1.4.1.311.17.3.121: 00 1.3.6.1.4.1.311.17.3.71: (cut for privacy) subject=/CN=Azure IoT CA TestOnly Intermediate 3 CA issuer=/CN=Azure IoT CA TestOnly Intermediate 2 CA -----BEGIN CERTIFICATE----- (cut for brevity) -----END CERTIFICATE-----
That is a lot of information in this file. It contains the device certificate along with its private key and then each of the intermediate and root certificates.
Take a look at the subject and issuer properties starting at the bottom.
The very last one is the “Intermediate 3 CA” certificate authority. It was issued by the “Intermediate 2” CA. Intermediate 2 was issued by 1, 1 was issued by the root CA. The root CA’s subject and issuer are identical. That is because the root CA trusts itself. This is the start of the chain of trust.
The very first certificate in the file also has a private key. That is the device’s certificate.
The PowerShell command also created a file called “device.pfx”. The PFX file format is a binary format (also called PKCS12) which has pretty much the same information as the “device-all.pem” file.
The PFX file is well understood by Windows and .NET, so that is what the DPS client code will use.
C# client to provision a device using the Device Provisioning Service
In my use case, I had a Windows-based client, which was to act as a device in the IoT Hub. The client did not have unique information – users download an installer and install it, just like any other Windows program. So it needed a way to provision itself with unique information and a certificate that it created on its own. That will be described in a future blog post.
First of all, how does a C# client contact the DPS and provision itself?
The following .NET Core 2.0 console application loads a certificate chain from a PFX file and provisions the device in DPS. It has been adapted from https://github.com/Azure/azure-iot-sdk-csharp/blob/master/provisioning/device/samples/ProvisioningDeviceClientX509/Program.cs.
The app requires the following NuGet packages:
- Microsoft.Azure.Devices.Client
- Microsoft.Azure.Devices.Provisioning.Client
- Microsoft.Azure.Devices.Provisioning.Transport.Http
The code required for a device to provision itself in the IoT Hub Group Enrollment is as follows. Note that you need to insert your own “Scope ID” from the Device Provisioning Service properties in the Azure Portal.
using Microsoft.Azure.Devices.Client; using Microsoft.Azure.Devices.Provisioning.Client; using Microsoft.Azure.Devices.Provisioning.Client.Transport; using Microsoft.Azure.Devices.Shared; using System; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; namespace Device { class Program { // these come from the Device Provisioning Service properties in Azure. // Look them up in the Portal. static string s_ProvisioningEndpoint = "global.azure-devices-provisioning.net"; static string s_IdScope = "<your own scope ID>"; static void Main(string[] args) { var p = new Program(); p.Provision().Wait(); } public async Task Provision() { Console.WriteLine("Provisioning device"); var pfxContents = LoadCertificateFromPfx("device.pfx", "123"); if (pfxContents.certificate == null) { Console.WriteLine("Can't continue without certificate"); return; } var transport = new ProvisioningTransportHandlerHttp(); var securityProvider = new SecurityProviderX509Certificate( pfxContents.certificate, pfxContents.collection); var provClient = ProvisioningDeviceClient.Create( s_ProvisioningEndpoint, s_IdScope, securityProvider, transport); Console.WriteLine( $"RegistrationID = {securityProvider.GetRegistrationID()}"); Console.Write("ProvisioningClient RegisterAsync . . . "); var result = await provClient.RegisterAsync(); Console.WriteLine($"{result.Status}"); Console.WriteLine("ProvisioningClient AssignedHub: " + $"{result.AssignedHub}; DeviceID: {result.DeviceId}"); // the device has been provisioned using DPS, communicate with IoT Hub var auth = new DeviceAuthenticationWithX509Certificate( result.DeviceId, pfxContents.certificate); using (var iotClient = DeviceClient.Create(result.AssignedHub, auth)) { Console.WriteLine("DeviceClient OpenAsync."); await iotClient.OpenAsync(); Console.WriteLine("DeviceClient SendEventAsync."); await iotClient.SendEventAsync( new Message(Encoding.UTF8.GetBytes("TestMessage"))); Console.WriteLine("DeviceClient CloseAsync."); await iotClient.CloseAsync(); } } private (X509Certificate2 certificate, X509Certificate2Collection collection) LoadCertificateFromPfx(string certificateFileName, string password) { var certificateCollection = new X509Certificate2Collection(); certificateCollection.Import( certificateFileName, password, X509KeyStorageFlags.Exportable|X509KeyStorageFlags.UserKeySet); X509Certificate2 certificate = null; var outcollection = new X509Certificate2Collection(); foreach (X509Certificate2 element in certificateCollection) { Console.WriteLine($"Found certificate: {element?.Thumbprint} " + $"{element?.Subject}; PrivateKey: {element?.HasPrivateKey}"); if (certificate == null && element.HasPrivateKey) { certificate = element; } else { outcollection.Add(element); } } if (certificate == null) { Console.WriteLine($"ERROR: {certificateFileName} did not " + $"contain any certificate with a private key."); return (null, null); } else { Console.WriteLine($"Using certificate {certificate.Thumbprint} " + $"{certificate.Subject}"); return (certificate, outcollection); } } } }
The interesting parts in the code above are the LoadCertificateFromPfx method which loads a PFX file and returns a tuple of (X509Certificate2, X509Certificate2Collection), and the lines
var securityProvider = new SecurityProviderX509Certificate( pfxContents.certificate, pfxContents.collection); var provClient = ProvisioningDeviceClient.Create( s_ProvisioningEndpoint, s_IdScope, securityProvider, transport); var result = await provClient.RegisterAsync();
The SecurityProviderX509Certificate class is a wrapper for the certificate and certificate chain. By passing in the certificate chain, the SDK will install all the certificates in the chain in the user’s Windows certificate store. This seems to be necessary for the authentication to work. I don’t know why the entire chain must be installed, I just know it doesn’t work without it.
The ProvisioningDeviceClient type is the actual web service client that communicates with the DPS. It can be created with different transports. In this example is uses HTTPS, but it also supports AMQP and MQTT which are message transfer protocols. You can read Clemens Vaster’s educational comparison here: http://vasters.com/blog/From-MQTT-to-AMQP-and-back/.
Now, this example uses a hardcoded PFX password “123”. Obviously, production code should not do that.
Summing up
At this point, we have a certificate chain where the root CA certificate is configured in Azure IoT Hub DPS, and a console app which can use a device certificate to authenticate against DPS and be provisioned in the IoT Hub.
As mentioned earlier, I wanted to have the client create its own certificate and provision itself. My next post in this series will cover creating a new certificate and signing it with another.