I had some problems<\/a> 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?<\/p>\n 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.<\/p>\n 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\u2019t necessary to call into native Windows libraries or use an extra library like BouncyCastle<\/a> etc.<\/p>\n I\u2019ve published the source to Github here: https:\/\/github.com\/rwatjen\/AzureIoTDPSCertificates<\/a>.<\/p>\n The main part of the program that creates a new CA certificate is this:<\/p>\n <\/p>\n Now, there seems to be a lot of black magic in there, so I\u2019ll try to explain the different parts starting from the top.<\/p>\n The code generates a new Elliptic Curve algorithm implementation with a key size of 256 bits.<\/p>\n Then it creates a certificate request with the certificate subject name, the EC DSA algorithm and a required hashing algorithm.<\/p>\n 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.<\/p>\n For a CA certificate, the most important parts are:<\/p>\n The basic constraints extension\u2019s 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 \u201ccritical\u201d. When an extension is marked as critical, a system that verifies the certificate must verify the extension and its contents. If it doesn\u2019t understand the extension, or the contents are invalid, the system must reject the certificate.<\/p>\n The next extension added to the certificate request is this:<\/p>\n This means that we want to use this certificate to sign other certificates. That is what a CA does.<\/p>\n The next part is a bit trickier.<\/p>\n If this is for an intermediate CA (the parameter issuingCA was set), then the new certificate\u2019s \u201cAuthority Key Identifier\u201d needs to be set to the issuing certificate\u2019s \u201cSubject Key Identifier\u201d. .NET Core 2.0 doesn\u2019t have a built-in extension for AuthorityKeyIdentifier, so it must be added as a generic extension with its OID 2.5.29.35.<\/p>\n It is a bit more complicated than that, because the Authority Key Identifier has a prefix called \u201cKeyID\u201d before the Subject Key Identifier it contains. The X.509 certificate values are ASN.1 encoded. I haven\u2019t found it easy to figure out exactly how to do that in .NET, so I found the byte values that indicate \u201cKeyID\u201d in another certificate and reused them. That is why there are some hardcoded magic numbers in the code above.<\/p>\n The certificates created by the IoT Hub and DPS PowerShell scripts have the \u201cSubject Alternate Name\u201d or \u201cSAN\u201d 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 \u201cexample.com\u201d, \u201cwww.example.com\u201d, and \u201cm.example.com\u201d etc.<\/p>\n 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:<\/p>\n .NET Core 2.0 has a helper class using the builder pattern<\/a> to assist with adding the SAN names to the SAN extension, so it is relatively straightforward.<\/p>\n Next, the certificate request needs to have some extra key usages added:<\/p>\n I don\u2019t know whether a CA certificate needs these, but since the certificates generated by the samples had them, I decided to add them also.<\/p>\n The next to last information added to the certificate request is the subject key of this<\/i> certificate. I used the \u201cSubject Key Identifier\u201d of the optional issuing certificate as the \u201cAuthority Key Identifier\u201d before. Now I add this certificate\u2019s \u201cSubject Key Identifier\u201d. It\u2019s actually a hash of the certificate\u2019s public key.<\/p>\n The final information that is added to the request is the \u201cNotBefore\u201d and \u201cNotAfter\u201d dates and the certificate\u2019s serial number.<\/p>\n \u201cNotBefore\u201d 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 \u201cNotAfter\u201d date is when the certificate expires. For this example, I set it to one year (365 days) from now.<\/p>\n However, a certificate cannot be valid outside of its issuing CA\u2019s validity period, so there are some checks to verify that, and if necessary shorten the \u201cNotBefore\u201d and \u201cNotAfter\u201d dates to those of the issuing CA\u2019s.<\/p>\n The certificate serial can be used to identify the certificate later. This is required by RFC-5280<\/a> 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.<\/p>\n 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\u2019s private key:<\/p>\n 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.<\/p>\n The code above is able to create PFX files and CER files required for Azure IoT Hub Device Provisioning Service. The Github project<\/a> has details on how to use it, if you want to experiment with DPS also.<\/p>\ninternal static X509Certificate2 CreateAndSignCertificate(string subjectName, X509Certificate2 signingCertificate)\r\n{\r\n \/\/ argument checks omitted\r\n\r\n using (var ecdsa = ECDsa.Create(\"ECDsa\"))\r\n {\r\n ecdsa.KeySize = 256;\r\n var request = new CertificateRequest(\r\n $\"CN={subjectName}\",\r\n ecdsa,\r\n HashAlgorithmName.SHA256);\r\n\r\n \/\/ set basic certificate contraints\r\n request.CertificateExtensions.Add(\r\n new X509BasicConstraintsExtension(false, false, 0, true));\r\n\r\n \/\/ key usage: Digital Signature and Key Encipherment\r\n request.CertificateExtensions.Add(\r\n new X509KeyUsageExtension(\r\n X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment,\r\n true));\r\n\r\n \/\/ set the AuthorityKeyIdentifier. There is no built-in \r\n \/\/ support, so it needs to be copied from the Subject Key \r\n \/\/ Identifier of the signing certificate and massaged slightly.\r\n \/\/ AuthorityKeyIdentifier is \"KeyID=\"\r\n var issuerSubjectKey = signingCertificate.Extensions[\"Subject Key Identifier\"].RawData;\r\n var segment = new ArraySegment(issuerSubjectKey, 2, issuerSubjectKey.Length - 2);\r\n var authorityKeyIdentifer = new byte[segment.Count + 4];\r\n \/\/ these bytes define the \"KeyID\" part of the AuthorityKeyIdentifer\r\n authorityKeyIdentifer[0] = 0x30;\r\n authorityKeyIdentifer[1] = 0x16;\r\n authorityKeyIdentifer[2] = 0x80;\r\n authorityKeyIdentifer[3] = 0x14;\r\n segment.CopyTo(authorityKeyIdentifer, 4);\r\n request.CertificateExtensions.Add(new X509Extension(\"2.5.29.35\", authorityKeyIdentifer, false));\r\n\r\n \/\/ DPS samples create certs with the device name as a SAN name \r\n \/\/ in addition to the subject name\r\n var sanBuilder = new SubjectAlternativeNameBuilder();\r\n sanBuilder.AddDnsName(subjectName);\r\n var sanExtension = sanBuilder.Build();\r\n request.CertificateExtensions.Add(sanExtension);\r\n\r\n \/\/ Enhanced key usages\r\n request.CertificateExtensions.Add(\r\n new X509EnhancedKeyUsageExtension(\r\n new OidCollection {\r\n new Oid(\"1.3.6.1.5.5.7.3.2\"), \/\/ TLS Client auth\r\n new Oid(\"1.3.6.1.5.5.7.3.1\") \/\/ TLS Server auth\r\n },\r\n false));\r\n\r\n \/\/ add this subject key identifier\r\n request.CertificateExtensions.Add(\r\n new X509SubjectKeyIdentifierExtension(request.PublicKey, false));\r\n\r\n \/\/ certificate expiry: Valid from Yesterday to Now+365 days\r\n \/\/ Unless the signing cert's validity is less. It's not possible\r\n \/\/ to create a cert with longer validity than the signing cert.\r\n var notbefore = DateTimeOffset.UtcNow.AddDays(-1);\r\n if (notbefore < signingCertificate.NotBefore) { notbefore = new DateTimeOffset(signingCertificate.NotBefore); } var notafter = DateTimeOffset.UtcNow.AddDays(365); if (notafter > signingCertificate.NotAfter)\r\n {\r\n notafter = new DateTimeOffset(signingCertificate.NotAfter);\r\n }\r\n\r\n \/\/ cert serial is the epoch\/unix timestamp\r\n var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);\r\n var unixTime = Convert.ToInt64((DateTime.UtcNow - epoch).TotalSeconds);\r\n var serial = BitConverter.GetBytes(unixTime);\r\n\r\n \/\/ create and return the generated and signed\r\n using (var cert = request.Create(\r\n signingCertificate,\r\n notbefore,\r\n notafter,\r\n serial))\r\n {\r\n return cert.CopyWithPrivateKey(ecdsa);\r\n }\r\n }\r\n}\r\n<\/pre>\n
\r\nvar request = new CertificateRequest(\r\n $\"CN={subjectName}\",\r\n ecdsa,\r\n HashAlgorithmName.SHA256);\r\n<\/pre>\n
\/\/ set basic certificate contraints\r\nrequest.CertificateExtensions.Add(\r\n new X509BasicConstraintsExtension(true, true, 12, true));\r\n<\/pre>\n
\/\/ key usage: Digital Signature and Key Encipherment\r\nrequest.CertificateExtensions.Add(\r\n new X509KeyUsageExtension(\r\n X509KeyUsageFlags.KeyCertSign,\r\n true));\r\n<\/pre>\n
if (issuingCa != null)\r\n{\r\n \/\/ set the AuthorityKeyIdentifier. There is no built-in \r\n \/\/ support, so it needs to be copied from the Subject Key \r\n \/\/ Identifier of the signing certificate and massaged slightly.\r\n \/\/ AuthorityKeyIdentifier is \"KeyID=\"\r\n var issuerSubjectKey = issuingCa.Extensions[\"Subject Key Identifier\"].RawData;\r\n var segment = new ArraySegment(issuerSubjectKey, 2, issuerSubjectKey.Length - 2);\r\n var authorityKeyIdentifier = new byte[segment.Count + 4];\r\n \/\/ these bytes define the \"KeyID\" part of the AuthorityKeyIdentifer\r\n authorityKeyIdentifier[0] = 0x30;\r\n authorityKeyIdentifier[1] = 0x16;\r\n authorityKeyIdentifier[2] = 0x80;\r\n authorityKeyIdentifier[3] = 0x14;\r\n segment.CopyTo(authorityKeyIdentifier, 4);\r\n request.CertificateExtensions.Add(new X509Extension(\"2.5.29.35\", authorityKeyIdentifier, false));\r\n}\r\n<\/pre>\n
\/\/ DPS samples create certs with the device name as a SAN name \r\n\/\/ in addition to the subject name\r\nvar sanBuilder = new SubjectAlternativeNameBuilder();\r\nsanBuilder.AddDnsName(subjectName);\r\nvar sanExtension = sanBuilder.Build();\r\nrequest.CertificateExtensions.Add(sanExtension);\r\n<\/pre>\n
\/\/ Enhanced key usages\r\nrequest.CertificateExtensions.Add(\r\n new X509EnhancedKeyUsageExtension(\r\n new OidCollection {\r\n new Oid(\"1.3.6.1.5.5.7.3.2\"), \/\/ TLS Client auth\r\n new Oid(\"1.3.6.1.5.5.7.3.1\") \/\/ TLS Server auth\r\n },\r\n false));\r\n<\/pre>\n
\/\/ add this subject key identifier\r\nrequest.CertificateExtensions.Add(\r\n new X509SubjectKeyIdentifierExtension(request.PublicKey, false));\r\n<\/pre>\n
\/\/ certificate expiry: Valid from Yesterday to Now+365 days\r\n\/\/ Unless the signing cert's validity is less. It's not possible\r\n\/\/ to create a cert with longer validity than the signing cert.\r\nvar notbefore = DateTimeOffset.UtcNow.AddDays(-1);\r\nif ((issuingCa != null) && (notbefore < issuingCa.NotBefore)) { notbefore = new DateTimeOffset(issuingCa.NotBefore); } var notafter = DateTimeOffset.UtcNow.AddDays(365); if ((issuingCa != null) && (notafter > issuingCa.NotAfter))\r\n{\r\n notafter = new DateTimeOffset(issuingCa.NotAfter);\r\n}\r\n\r\n\/\/ cert serial is the epoch\/unix timestamp\r\nvar epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);\r\nvar unixTime = Convert.ToInt64((DateTime.UtcNow - epoch).TotalSeconds);\r\nvar serial = BitConverter.GetBytes(unixTime);\r\n<\/pre>\n
X509Certificate2 generatedCertificate = null;\r\nif (issuingCa != null)\r\n{\r\n generatedCertificate = request.Create(issuingCa, notbefore, notafter, serial);\r\n return generatedCertificate.CopyWithPrivateKey(ecdsa);\r\n}\r\nelse\r\n{\r\n generatedCertificate = request.CreateSelfSigned(\r\n notbefore, notafter);\r\n return generatedCertificate;\r\n}\r\n<\/pre>\n
Summing up<\/h2>\n
Share this:<\/h3>