I had some problems with the various PowerShell and bash samples in the Microsoft documentation on how to create a certificate chain for use with the Azure IoT Hub Device Provisioning Service. Why did it have to be so complicated to get started with X.509 based authentication towards DPS?
What if I wrote my own program to create the root certificate, some intermediaries, and could also create device certificates? I set out to do that.
In the end, it turned out to be not that hard. .NET Core 2.0 has some new classes to help with certificate requests, so it isn’t necessary to call into native Windows libraries or use an extra library like BouncyCastle etc.
I’ve published the source to Github here: https://github.com/rwatjen/AzureIoTDPSCertificates.
The main part of the program that creates a new CA certificate is this:
internal static X509Certificate2 CreateAndSignCertificate(string subjectName, X509Certificate2 signingCertificate) { // argument checks omitted using (var ecdsa = ECDsa.Create("ECDsa")) { ecdsa.KeySize = 256; var request = new CertificateRequest( $"CN={subjectName}", ecdsa, HashAlgorithmName.SHA256); // set basic certificate contraints request.CertificateExtensions.Add( new X509BasicConstraintsExtension(false, false, 0, true)); // key usage: Digital Signature and Key Encipherment request.CertificateExtensions.Add( new X509KeyUsageExtension( X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, true)); // set the AuthorityKeyIdentifier. There is no built-in // support, so it needs to be copied from the Subject Key // Identifier of the signing certificate and massaged slightly. // AuthorityKeyIdentifier is "KeyID=" var issuerSubjectKey = signingCertificate.Extensions["Subject Key Identifier"].RawData; var segment = new ArraySegment(issuerSubjectKey, 2, issuerSubjectKey.Length - 2); var authorityKeyIdentifer = new byte[segment.Count + 4]; // these bytes define the "KeyID" part of the AuthorityKeyIdentifer authorityKeyIdentifer[0] = 0x30; authorityKeyIdentifer[1] = 0x16; authorityKeyIdentifer[2] = 0x80; authorityKeyIdentifer[3] = 0x14; segment.CopyTo(authorityKeyIdentifer, 4); request.CertificateExtensions.Add(new X509Extension("2.5.29.35", authorityKeyIdentifer, false)); // DPS samples create certs with the device name as a SAN name // in addition to the subject name var sanBuilder = new SubjectAlternativeNameBuilder(); sanBuilder.AddDnsName(subjectName); var sanExtension = sanBuilder.Build(); request.CertificateExtensions.Add(sanExtension); // Enhanced key usages request.CertificateExtensions.Add( new X509EnhancedKeyUsageExtension( new OidCollection { new Oid("1.3.6.1.5.5.7.3.2"), // TLS Client auth new Oid("1.3.6.1.5.5.7.3.1") // TLS Server auth }, false)); // add this subject key identifier request.CertificateExtensions.Add( new X509SubjectKeyIdentifierExtension(request.PublicKey, false)); // certificate expiry: Valid from Yesterday to Now+365 days // Unless the signing cert's validity is less. It's not possible // to create a cert with longer validity than the signing cert. var notbefore = DateTimeOffset.UtcNow.AddDays(-1); if (notbefore < signingCertificate.NotBefore) { notbefore = new DateTimeOffset(signingCertificate.NotBefore); } var notafter = DateTimeOffset.UtcNow.AddDays(365); if (notafter > signingCertificate.NotAfter) { notafter = new DateTimeOffset(signingCertificate.NotAfter); } // cert serial is the epoch/unix timestamp var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); var unixTime = Convert.ToInt64((DateTime.UtcNow - epoch).TotalSeconds); var serial = BitConverter.GetBytes(unixTime); // create and return the generated and signed using (var cert = request.Create( signingCertificate, notbefore, notafter, serial)) { return cert.CopyWithPrivateKey(ecdsa); } } }
Now, there seems to be a lot of black magic in there, so I’ll try to explain the different parts starting from the top.
The code generates a new Elliptic Curve algorithm implementation with a key size of 256 bits.
Then it creates a certificate request with the certificate subject name, the EC DSA algorithm and a required hashing algorithm.
var request = new CertificateRequest( $"CN={subjectName}", ecdsa, HashAlgorithmName.SHA256);
Afterwards, the request object is used to add a lot of different certificate extensions which define what the resulting certificate can be used for, its expiration date etc.
For a CA certificate, the most important parts are:
// set basic certificate contraints request.CertificateExtensions.Add( new X509BasicConstraintsExtension(true, true, 12, true));
The basic constraints extension’s first parameter defines that this will be a Certificate Authority. The second defines that the chain length will be limited, and the third (12) is how long the chain may be in total. The final parameter defines that this extension is “critical”. When an extension is marked as critical, a system that verifies the certificate must verify the extension and its contents. If it doesn’t understand the extension, or the contents are invalid, the system must reject the certificate.
The next extension added to the certificate request is this:
// key usage: Digital Signature and Key Encipherment request.CertificateExtensions.Add( new X509KeyUsageExtension( X509KeyUsageFlags.KeyCertSign, true));
This means that we want to use this certificate to sign other certificates. That is what a CA does.
The next part is a bit trickier.
if (issuingCa != null) { // set the AuthorityKeyIdentifier. There is no built-in // support, so it needs to be copied from the Subject Key // Identifier of the signing certificate and massaged slightly. // AuthorityKeyIdentifier is "KeyID=" var issuerSubjectKey = issuingCa.Extensions["Subject Key Identifier"].RawData; var segment = new ArraySegment(issuerSubjectKey, 2, issuerSubjectKey.Length - 2); var authorityKeyIdentifier = new byte[segment.Count + 4]; // these bytes define the "KeyID" part of the AuthorityKeyIdentifer authorityKeyIdentifier[0] = 0x30; authorityKeyIdentifier[1] = 0x16; authorityKeyIdentifier[2] = 0x80; authorityKeyIdentifier[3] = 0x14; segment.CopyTo(authorityKeyIdentifier, 4); request.CertificateExtensions.Add(new X509Extension("2.5.29.35", authorityKeyIdentifier, false)); }
If this is for an intermediate CA (the parameter issuingCA was set), then the new certificate’s “Authority Key Identifier” needs to be set to the issuing certificate’s “Subject Key Identifier”. .NET Core 2.0 doesn’t have a built-in extension for AuthorityKeyIdentifier, so it must be added as a generic extension with its OID 2.5.29.35.
It is a bit more complicated than that, because the Authority Key Identifier has a prefix called “KeyID” before the Subject Key Identifier it contains. The X.509 certificate values are ASN.1 encoded. I haven’t found it easy to figure out exactly how to do that in .NET, so I found the byte values that indicate “KeyID” in another certificate and reused them. That is why there are some hardcoded magic numbers in the code above.
The certificates created by the IoT Hub and DPS PowerShell scripts have the “Subject Alternate Name” or “SAN” extension added. It is normally used to create a certificate that has multiple subjects. For a web site SAN certificates make it possible that the same certificate can be used for both “example.com”, “www.example.com”, and “m.example.com” etc.
There is no need for the device certificates to have the SAN extension, but since the ones created by the sample scripts add it, I decided to do the same:
// DPS samples create certs with the device name as a SAN name // in addition to the subject name var sanBuilder = new SubjectAlternativeNameBuilder(); sanBuilder.AddDnsName(subjectName); var sanExtension = sanBuilder.Build(); request.CertificateExtensions.Add(sanExtension);
.NET Core 2.0 has a helper class using the builder pattern to assist with adding the SAN names to the SAN extension, so it is relatively straightforward.
Next, the certificate request needs to have some extra key usages added:
// Enhanced key usages request.CertificateExtensions.Add( new X509EnhancedKeyUsageExtension( new OidCollection { new Oid("1.3.6.1.5.5.7.3.2"), // TLS Client auth new Oid("1.3.6.1.5.5.7.3.1") // TLS Server auth }, false));
I don’t know whether a CA certificate needs these, but since the certificates generated by the samples had them, I decided to add them also.
The next to last information added to the certificate request is the subject key of this certificate. I used the “Subject Key Identifier” of the optional issuing certificate as the “Authority Key Identifier” before. Now I add this certificate’s “Subject Key Identifier”. It’s actually a hash of the certificate’s public key.
// add this subject key identifier request.CertificateExtensions.Add( new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
The final information that is added to the request is the “NotBefore” and “NotAfter” dates and the certificate’s serial number.
// certificate expiry: Valid from Yesterday to Now+365 days // Unless the signing cert's validity is less. It's not possible // to create a cert with longer validity than the signing cert. var notbefore = DateTimeOffset.UtcNow.AddDays(-1); if ((issuingCa != null) && (notbefore < issuingCa.NotBefore)) { notbefore = new DateTimeOffset(issuingCa.NotBefore); } var notafter = DateTimeOffset.UtcNow.AddDays(365); if ((issuingCa != null) && (notafter > issuingCa.NotAfter)) { notafter = new DateTimeOffset(issuingCa.NotAfter); } // cert serial is the epoch/unix timestamp var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); var unixTime = Convert.ToInt64((DateTime.UtcNow - epoch).TotalSeconds); var serial = BitConverter.GetBytes(unixTime);
“NotBefore” is the time where the certificate is valid from. It is possible to create certificates that are not yet valid. In this scenario, the certificate should be valid now. The “NotAfter” date is when the certificate expires. For this example, I set it to one year (365 days) from now.
However, a certificate cannot be valid outside of its issuing CA’s validity period, so there are some checks to verify that, and if necessary shorten the “NotBefore” and “NotAfter” dates to those of the issuing CA’s.
The certificate serial can be used to identify the certificate later. This is required by RFC-5280 to be unique for each certificate a CA issues. It can be used to identify a certificate that has been revoked. Revokation is not something I have taken into consideration in this sample, so the serial I use is the number of seconds since 1-JAN-1970 at 00:00 UTC.
Finally, the certificate request is used to create a new certificate. If it is for the root CA, the certificate is self-signed, ie. signed with its own private key. If the request is for an intermediate CA certificate, it is signed with the issuing CA’s private key:
X509Certificate2 generatedCertificate = null; if (issuingCa != null) { generatedCertificate = request.Create(issuingCa, notbefore, notafter, serial); return generatedCertificate.CopyWithPrivateKey(ecdsa); } else { generatedCertificate = request.CreateSelfSigned( notbefore, notafter); return generatedCertificate; }
There is a slight difference in the generated certificate object, depending on whether it is self-signed or not. If it is self-signed, it contains the private key. If not, it must be copied along with the private key from the EC DSA object.
Summing up
The code above is able to create PFX files and CER files required for Azure IoT Hub Device Provisioning Service. The Github project has details on how to use it, if you want to experiment with DPS also.
Thanks a lot for posting it. It helped our project a lot. Good work. Keep it up.
Also wanted to say thanks super helpful.
Also, a question if you have time, did you ever figure out how to export the entire certificate chain if you do Create() with an issuer cert? When I do an export on that, it only has the final cert…it does not contain the CAs/Signing CA public info that issued it.
After further inspection, it looks like the chain might be in there. Generally I examine certs with KeyStore Explorer and it shows the issuance chain. It did NOT when I used the above approach to create the certs. However, if I attempt to get the issuance chain programmatically in .NET, it does seem to come back with the signing / intermediate CA, so it might just be a KeyStore Explorer problem.
@Travis,
Do you have a code example of how to export the cert chain, and also the “Extended Properties” as certmgr.exe can do?