Criando uma cadeia de certificados X.509 em C#
Tive alguns problemas com os vários PowerShell e bash samples na documentação da Microsoft sobre como criar uma cadeia de certificados para uso com o Serviço de Provisionamento de Dispositivos Azure IoT Hub. Por que tinha que ser tão complicado para começar com a autenticação baseada em X.509 para DPS?
E se eu escrevesse meu próprio programa para criar o certificado raiz, alguns intermediários, e também pudesse criar certificados de dispositivo? Eu me propus a fazer isso.
No final, acabou não sendo tão difícil. .NET Core 2.0 tem algumas novas classes para ajudar com solicitações de certificado, por isso não é necessário ligar para bibliotecas nativas do Windows ou usar uma biblioteca extra como BouncyCastle etc.
Eu publiquei a fonte para o Github aqui: https://github.com/rwatjen/AzureIoTDPSCertificates.
A parte principal do programa que cria um novo certificado CA é esta:
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);
}
}
}
Agora, parece haver muita magia negra lá, então vou tentar explicar as diferentes partes a partir do início.
O código gera uma nova implementação de algoritmo da Curva Elíptica com um tamanho-chave de 256 bits.
Em seguida, cria uma solicitação de certificado com o nome do assunto do certificado, o algoritmo EC DSA e um algoritmo de hash necessário.
var request = new CertificateRequest(
$"CN={subjectName}",
ecdsa,
HashAlgorithmName.SHA256);
Posteriormente, o objeto de solicitação é usado para adicionar um monte de extensões de certificado diferentes que definem para que o certificado resultante pode ser usado, sua data de validade etc.
Para um certificado CA, as partes mais importantes são:
// set basic certificate contraints
request.CertificateExtensions.Add(
new X509BasicConstraintsExtension(true, true, 12, true));
O primeiro parâmetro da extensão de restrições básicas define que este será uma Autoridade de Certificado. O segundo define que o comprimento da cadeia será limitado, e o terceiro (12) é quanto tempo a cadeia pode ser no total. O parâmetro final define que essa extensão é “crítica”. Quando uma extensão é marcada como crítica, um sistema que verifica o certificado deve verificar a extensão e seu conteúdo. Se ele não entender a extensão, ou o conteúdo for inválido, o sistema deve rejeitar o certificado.
A próxima extensão adicionada à solicitação de certificado é esta:
// key usage: Digital Signature and Key Encipherment
request.CertificateExtensions.Add(
new X509KeyUsageExtension(
X509KeyUsageFlags.KeyCertSign,
true));
Isso significa que queremos usar este certificado para assinar outros certificados. Isso é o que um CA faz.
A próxima parte é um pouco mais complicada.
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));
}
Se isso for para um CA intermediário (o parâmetro emitidoCA foi definido), então o novo certificado “Identificador de Chaves de Autoridade” precisa ser definido para o “Identificador de Chaves de Assunto” do certificado de emissão. O .NET Core 2.0 não tem uma extensão incorporada para AuthorityKeyIdentifier, por isso deve ser adicionado como uma extensão genérica com seu OID 2.5.29.35.
É um pouco mais complicado do que isso, porque o Identificador de Chaves da Autoridade tem um prefixo chamado “KeyID” antes do Identificador de Chave de Assunto que ele contém. Os valores do certificado X.509 são codificados por ASN.1. Eu não achei fácil descobrir exatamente como fazer isso em .NET, então eu encontrei os valores byte que indicam “KeyID” em outro certificado e os reusi. É por isso que há alguns números mágicos codificados no código acima.
Os certificados criados pelos scripts IoT Hub e DPS PowerShell têm a extensão “Nome Alternativo do Assunto” ou “SAN”. Normalmente é usado para criar um certificado que tenha vários assuntos. Para um site, os certificados SAN possibilitam que o mesmo certificado possa ser usado tanto para “example.com”, “www.example.com” e “m.example.com” etc.
Não há necessidade de os certificados do dispositivo terem a extensão de SAN, mas como os criados pelos scripts de amostra o adicionam, decidi fazer o mesmo:
// 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 tem uma classe de ajudante usando o padrão de construtor para ajudar a adicionar os nomes de SAN à extensão SAN, por isso é relativamente simples.
Em seguida, a solicitação de certificado precisa ter alguns usos de chaves extras adicionados:
// 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));
Não sei se um certificado de CA precisa disso, mas como os certificados gerados pelas amostras os tinham, decidi adicioná-los também.
As próximas informações adicionadas à solicitação de certificado é a chave de assunto deste certificado. Usei o “Identificador da Chave de Assunto” do certificado de emissão opcional como “Identificador de Chave de Autoridade” antes. Agora eu adiciono este certificado “Identificador de Chave de Assunto”. Na verdade, é um hash da chave pública do certificado.
// add this subject key identifier
request.CertificateExtensions.Add(
new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
As informações finais adicionadas à solicitação são as datas “NotBefore” e “NotAfter” e o número de série do certificado.
// 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” é a hora de onde o certificado é válido. É possível criar certificados que ainda não são válidos. Nesse cenário, o certificado deve ser válido agora. A data “NotAfter” é quando o certificado expira. Por este exemplo, eu defini-lo para um ano (365 dias) a partir de agora.
No entanto, um certificado não pode ser válido fora do período de validade da CA emitido, por isso existem algumas verificações para verificar isso, e se necessário encurtar as datas “NotBefore” e “NotAfter” para as das CA’s emissoras.
A série de certificados pode ser usada para identificar o certificado posteriormente. Isso é exigido pelo RFC-5280 para ser único para cada certificado que um ca emite. Pode ser usado para identificar um certificado que foi revogado. Revogação não é algo que eu levei em consideração nesta amostra, então a série que eu uso é o número de segundos desde 1-JAN-1970 às 00:00 UTC.
Finalmente, a solicitação de certificado é usada para criar um novo certificado. Se for para a IN raiz, o certificado é auto-assinado, ou seja. assinado com sua própria chave privada. Se a solicitação for para um certificado CA intermediário, ela será assinada com a chave privada da CA emissora:
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;
}
Há uma pequena diferença no objeto de certificado gerado, dependendo se ele é auto-assinado ou não. Se for auto-assinado, contém a chave privada. Caso não, deve ser copiado juntamente com a chave privada do objeto EC DSA.
Resumindo
O código acima é capaz de criar arquivos PFX e arquivos CER necessários para o Serviço de Provisionamento de Dispositivos hub Azure. O projeto Github tem detalhes sobre como usá-lo, se você quiser experimentar com DPS também.
Autor: Rasmus Wätjen