Certificados & PKI

Duas units cobrem direções opostas do mesmo formato X.509: sgcCrypto_X509 lê e verifica um certificado que alguém lhe deu, sgcCrypto_X509_Gen gera um certificado ou uma requisição seus. Uma terceira unit, sgcCrypto_ASN1, fornece a infraestrutura de DER/PEM sobre a qual as outras duas se apoiam.

 

Lendo e verificando um certificado

 

sgcCrypto_X509 faz o parsing de um certificado codificado em DER para um record simples, e verifica uma cadeia ou uma lista de revogação em relação a ele. Use este lado da API quando sua aplicação recebe um certificado pela rede e precisa decidir se confia nele.

 


function sgcX509_Parse(const aDER: TBytes; out aCert: TsgcX509Cert): Boolean;
function sgcX509_VerifySignedBy(const aCert, aIssuer: TsgcX509Cert): Boolean;
function sgcX509_VerifyChain(const aChainDER: array of TBytes;
  const aTrustRootsDER: array of TBytes; aNowUnix: Int64): Boolean;
function sgcX509_SubjectOneLine(const aCert: TsgcX509Cert): string;
function sgcX509_IssuerRFC2253(const aCert: TsgcX509Cert): string;
function sgcX509_CRL_Parse(const aDER: TBytes; out aCRL: TsgcX509CRL): Boolean;
function sgcX509_IsRevoked(const aCRL: TsgcX509CRL; const aSerial: TBytes): Boolean;
function sgcX509_CSR_Parse(const aDER: TBytes; out aCSR: TsgcX509CSR): Boolean;
function sgcX509_CSR_Verify(const aCSR: TsgcX509CSR): Boolean;

 

sgcX509_VerifyChain recebe a cadeia e as âncoras de confiança como blobs DER, não como records já parseados, e um timestamp Unix contra o qual testar a janela de validade. O elemento 0 da cadeia é a folha e cada certificado precisa ser assinado pelo seguinte; o topo precisa ser assinado por uma das raízes confiáveis, ou ser igual a uma delas. O caminho é rejeitado quando um certificado está fora de sua janela de validade em aNowUnix, quando um emissor não é uma CA, quando ele viola o pathLenConstraint de basicConstraints (certificados autoemitidos não contam para o limite, RFC 5280 seção 6.1.4), ou quando viola uma extensão nameConstraints em qualquer certificado do caminho, incluindo a âncora de confiança. As escolhas de restrição verificadas são dNSName, iPAddress, rfc822Name, uniformResourceIdentifier e directoryName; as subárvores DNS também se aplicam a um common name da folha que se leia como um nome de host quando a folha não carrega nenhum nome DNS próprio, e um nameConstraints crítico que nomeie qualquer outra escolha rejeita o caminho. As políticas de certificado não são verificadas.

 

TsgcX509Cert expõe os campos que um chamador realmente precisa depois do parsing: Version, SerialNumber, as sequências RDN brutas de Issuer/Subject, NotBeforeUnix/NotAfterUnix, a chave pública (o que quer que PubKeyType indicar, x509pkRSA, x509pkEC, x509pkEd25519, x509pkMLDSA, x509pkSLHDSA ou x509pkMLKEM) e IsCA. Uma chave pós-quântica chega em PQCPublicKey, e o conjunto de parâmetros que seu object identifier nomeia está em qual dos campos MLDSAParams, SLHDSAParams ou MLKEMParams corresponder ao tipo. TsgcX509CSR espelha os mesmos campos para uma requisição PKCS#10.

 

Gerando um certificado ou uma requisição

 

sgcCrypto_X509_Gen constrói um certificado autoassinado, ou uma requisição de assinatura de certificado PKCS#10 para enviar a uma CA, a partir de um par de chaves RSA (veja Criptografia de Chave Pública). As duas recebem o mesmo record de opções.

 


function sgcX509_CreateSelfSigned(const aKey: TsgcRSAPrivateKey; const aOptions: TsgcX509Options): TBytes;
function sgcX509_CreateCSR(const aKey: TsgcRSAPrivateKey; const aOptions: TsgcX509Options): TBytes;
function sgcX509_ToPEM(const aDER: TBytes; const aLabel: string): string;

var
  oKey: TsgcRSAPrivateKey;
  oOptions: TsgcX509Options;
  oDER: TBytes;
begin
  oKey := sgcRSA_GenerateKey(2048);
  oOptions := sgcX509_DefaultOptions;
  oOptions.Subject.CommonName := 'example.com';
  oOptions.NotBefore := Now;
  oOptions.NotAfter := Now + 365;
  SetLength(oOptions.DNSNames, 2);
  oOptions.DNSNames[0] := 'example.com';
  oOptions.DNSNames[1] := 'www.example.com';
  oOptions.KeyUsage := [kuDigitalSignature, kuKeyEncipherment];
  SetLength(oOptions.ExtKeyUsage, 1);
  oOptions.ExtKeyUsage[0] := '1.3.6.1.5.5.7.3.1'; { TLS server authentication }

  oDER := sgcX509_CreateSelfSigned(oKey, oOptions);
  Memo1.Text := sgcX509_ToPEM(oDER, 'CERTIFICATE');
end;

 

Construa a janela de validade a partir de UTC.

A validade do certificado é codificada como UTCTime com um sufixo Z, que significa horário de Greenwich, então preencher NotBefore e NotAfter a partir de um Now local data o certificado incorretamente pelo seu deslocamento em relação a UTC. A leste de Greenwich, um certificado recém-emitido ainda não é válido até que esse deslocamento se esgote; a oeste dele, o certificado expira antes do previsto. sgcX509_UTCNow retorna o valor correto, e sgcX509_DefaultOptions já o usa com uma antecipação de cinco minutos para tolerar o desvio de relógio, a mesma margem que as CAs reais aplicam.

 


function sgcX509_UTCNow: TDateTime;

var
  oOptions: TsgcX509Options;
begin
  oOptions := sgcX509_DefaultOptions;
  oOptions.NotBefore := sgcX509_UTCNow;
  oOptions.NotAfter := sgcX509_UTCNow + 365;
end;

 

Preencha os nomes alternativos do assunto (subject alternative names).

Navegadores e bibliotecas TLS modernos ignoram completamente o common name e olham apenas para a extensão DNSNames; um certificado com um common name perfeito e nenhum nome DNS em SubjectAltName será rejeitado por mais correto que o resto dele esteja.

 

Além de DNSNames, IPAddresses e Emails, TsgcX509Options carrega um array URIs, para nomes alternativos do assunto do tipo uniformResourceIdentifier. Uma identidade de serviço que é uma URI em vez de um nome de host vai aqui, sendo o caso concreto um SPIFFE ID, que uma malha de workloads compara literalmente e que nenhum nome DNS consegue expressar. O valor entra literalmente como conteúdo IA5String, então passe uma URI completa.

 


SetLength(oOptions.URIs, 1);
oOptions.URIs[0] := 'spiffe://example.com/ns/default/sa/payments';

 

Defina IsCA := True e um PathLength não negativo para produzir um certificado de CA em vez de um certificado folha; deixe PathLength em -1 para omitir a restrição em um certificado folha. Um CSR carrega as mesmas extensões requisitadas no seu atributo extensionRequest, que é onde uma CA procura por elas ao emitir o certificado final.

 

Nomes alternativos do assunto em IPv6.

IPAddresses aceita IPv4 na forma com pontos e IPv6 em qualquer uma das formas textuais da RFC 4291: a forma completa de oito grupos, uma forma comprimida com ::, e uma forma com um endereço IPv4 embutido nos últimos 32 bits, em qualquer caixa de letras. Não acrescente colchetes e não acrescente um índice de zona. Uma entrada IPv4 é escrita como 4 octetos e uma entrada IPv6 como 16, que é o que a RFC 5280 exige e o que um cliente TLS compara com o endereço do peer.

 


SetLength(oOptions.IPAddresses, 3);
oOptions.IPAddresses[0] := '192.0.2.10';
oOptions.IPAddresses[1] := '2001:db8::1';
oOptions.IPAddresses[2] := '::ffff:192.0.2.10';

 

Restrições de nome em um certificado de CA.

Um certificado de CA pode limitar o que os certificados abaixo dele têm permissão de nomear, RFC 5280 seção 4.2.1.10. Preencha qualquer um de PermittedDNSNames, ExcludedDNSNames, PermittedIPRanges ou ExcludedIPRanges e a extensão é escrita, marcada como crítica. Uma entrada DNS é uma subárvore, portanto example.com cobre esse nome e todos os nomes abaixo dele. Uma entrada IP é um endereço com um comprimento de prefixo. sgcX509_VerifyChain aplica o que você escrever aqui.

 


oOptions.IsCA := True;
oOptions.PathLength := 0;
oOptions.KeyUsage := [kuKeyCertSign, kuCRLSign];
SetLength(oOptions.PermittedDNSNames, 1);
oOptions.PermittedDNSNames[0] := 'internal.example.com';
SetLength(oOptions.PermittedIPRanges, 2);
oOptions.PermittedIPRanges[0] := '192.0.2.0/24';
oOptions.PermittedIPRanges[1] := '2001:db8::/32';

 

Emitindo a partir de uma CA

 

Um certificado autoassinado não afirma nada que um terceiro possa verificar. sgcX509_CreateSigned emite um certificado a partir de uma chave de CA e do próprio certificado da CA, e sgcX509_CreateSignedFromCSR faz o mesmo partindo de uma requisição PKCS#10.

 


function sgcX509_CreateSigned(const aIssuerKey: TsgcX509SignKey;
  const aIssuerCertDER, aSubjectPublicKeyInfo: TBytes; const aOptions: TsgcX509Options): TBytes;
function sgcX509_CreateSignedFromCSR(const aIssuerKey: TsgcX509SignKey;
  const aIssuerCertDER, aCSRDER: TBytes; const aOptions: TsgcX509Options): TBytes;

 

O nome do emissor é copiado byte a byte do subject do certificado emissor, de modo que a cadeia se encadeia exatamente, e o authorityKeyIdentifier é o subjectKeyIdentifier desse certificado. TsgcX509Options.Issuer nunca é lido por nenhum builder. As duas funções geram um erro antes de assinar qualquer coisa quando o certificado emissor está malformado, não é uma CA ou carrega um key usage sem keyCertSign, quando a chave que você passou não é a chave desse certificado, e quando a chave do subject está malformada ou é de um algoritmo que a unit não conhece.

 

Ao emitir a partir de uma requisição, a assinatura da requisição é verificada primeiro e uma requisição que falha gera um erro. O nome do subject e a chave pública vêm da requisição, portanto aOptions.Subject é ignorado, e as extensões vêm apenas de aOptions. O que a requisição pediu não é copiado: a CA decide o que o certificado afirma.

 

Assinando com uma chave EC

 

sgcX509_CreateSelfSigned e sgcX509_CreateCSR assinam com RSA. O par Ex recebe, em vez disso, uma TsgcX509SignKey, que encapsula qualquer um dos dois tipos de chave, de modo que o mesmo record de opções produz um certificado RSA ou EC dependendo do que você fornecer a ele. Construa o wrapper com sgcX509_RSAKey ou sgcX509_ECKey em vez de preencher o record manualmente; a metade não usada é deixada indefinida de propósito. As duas funções originais permanecem inalteradas e agora simplesmente encapsulam sgcX509_RSAKey internamente.

 

Um certificado P-256 tem aproximadamente um terço do tamanho de um RSA de 2048 bits e verifica muito mais rápido, motivo pelo qual as implantações modernas usam EC por padrão. TsgcX509Options.Hash ainda apenas nomeia o SHA; o algoritmo de assinatura segue o tipo de chave, então rhSHA256 com uma chave EC significa ecdsa-with-SHA256, e com uma chave RSA significa sha256WithRSAEncryption.

 


type
  TsgcX509KeyKind = (xkRSA, xkEC, xkMLDSA, xkSLHDSA);

function sgcX509_RSAKey(const aKey: TsgcRSAPrivateKey): TsgcX509SignKey;
function sgcX509_ECKey(aCurve: TsgcECCurve; const aPrivateKey: TBytes): TsgcX509SignKey;
function sgcX509_CreateSelfSignedEx(const aKey: TsgcX509SignKey; const aOptions: TsgcX509Options): TBytes;
function sgcX509_CreateCSREx(const aKey: TsgcX509SignKey; const aOptions: TsgcX509Options): TBytes;

var
  oPrivateKey, oPublicKey, oDER: TBytes;
  oOptions: TsgcX509Options;
begin
  sgcEC_GenerateKeyPair(eccP256, oPrivateKey, oPublicKey);
  oOptions := sgcX509_DefaultOptions;
  oOptions.Subject.CommonName := 'example.com';
  SetLength(oOptions.DNSNames, 1);
  oOptions.DNSNames[0] := 'example.com';

  oDER := sgcX509_CreateSelfSignedEx(sgcX509_ECKey(eccP256, oPrivateKey), oOptions);
  Memo1.Text := sgcX509_ToPEM(oDER, 'CERTIFICATE');
end;

 

sgcX509_ECKey deriva o ponto público a partir do escalar, então você nunca precisa passá-lo. A curva vem de sgcCrypto_ECCurves e pode ser qualquer uma das sete que ela implementa, embora uma CA pública só aceite P-256 ou P-384.

 

Certificados pós-quânticos

 

Os mesmos builders Ex assinam com uma chave pós-quântica. sgcX509_MLDSAKey encapsula um par de chaves ML-DSA e sgcX509_SLHDSAKey uma chave privada SLH-DSA, e a partir daí nada mais muda: o mesmo record de opções produz um certificado autoassinado ou uma requisição de assinatura de certificado assinada com ML-DSA ou SLH-DSA. Veja Criptografia Pós-Quântica para os esquemas em si e para os contêineres de chave que o certificado carrega.

 


function sgcX509_MLDSAKey(aParams: TsgcMLDSAParams; const aPrivateKey, aPublicKey: TBytes): TsgcX509SignKey;
function sgcX509_SLHDSAKey(aParams: TsgcSLHDSAParams; const aPrivateKey: TBytes): TsgcX509SignKey;

var
  oSeed, oPub, oPriv, oDER: TBytes;
  oOptions: TsgcX509Options;
begin
  sgcMLDSA_GenerateKeyPairAndSeed(mldsa65, oSeed, oPub, oPriv);

  oOptions := sgcX509_DefaultOptions;
  oOptions.Subject.CommonName := 'example.com';
  SetLength(oOptions.DNSNames, 1);
  oOptions.DNSNames[0] := 'example.com';
  { the default options ask for kuKeyEncipherment, which ML-DSA does not allow }
  oOptions.KeyUsage := [kuDigitalSignature];

  oDER := sgcX509_CreateSelfSignedEx(sgcX509_MLDSAKey(mldsa65, oPriv, oPub), oOptions);
  Memo1.Text := sgcX509_ToPEM(oDER, 'CERTIFICATE');
end;

 

Os dois esquemas assinam no modo puro com uma string de contexto vazia, e o algoritmo de assinatura é o próprio object identifier da chave com os parâmetros ausentes, seguindo a RFC 9881 para ML-DSA e a RFC 9909 para SLH-DSA. TsgcX509Options.Hash é ignorado para essas chaves, já que nenhuma das duas faz o hash do corpo separadamente. sgcX509_MLDSAKey verifica os dois comprimentos de chave e faz o par assinar e verificar uma mensagem fixa, de modo que uma chave privada que recebeu a chave pública errada gera um erro ali, em vez de produzir um certificado que nada consegue verificar.

 

O key usage é restrito.

Apenas kuDigitalSignature, kuNonRepudiation, kuKeyCertSign e kuCRLSign são aceitos com uma chave ML-DSA ou SLH-DSA, e a regra é aplicada antes de qualquer coisa ser assinada. sgcX509_DefaultOptions pede kuKeyEncipherment, portanto ela gera um erro com essas chaves até que você altere KeyUsage. Um KeyUsage vazio deixa a extensão de fora, o que as duas RFCs permitem.

 

Certificando uma chave ML-KEM.

Uma chave ML-KEM encapsula, ela nunca assina, portanto pode ser o subject de um certificado, mas nunca pode assinar uma requisição para pedir um. Passe o SubjectPublicKeyInfo dela para sgcX509_CreateSigned depois de provar a posse por outros meios, que é o que a RFC 9935 deixa a cargo da aplicação. kuKeyEncipherment é a única flag de key usage permitida em um certificado desse tipo. Pelo mesmo motivo, sgcX509_CSR_Verify nunca pode passar em uma requisição ML-KEM, e sgcX509_VerifySignedBy nunca verifica nada com uma chave de emissor ML-KEM.

 

Lendo um de volta.

sgcX509_Parse e sgcX509_CSR_Parse tratam um certificado pós-quântico como malformado quando o SubjectPublicKeyInfo dele ou o algoritmo de assinatura ML-DSA ou SLH-DSA carrega parâmetros, e quando o comprimento da chave não corresponde ao conjunto de parâmetros que o object identifier nomeia. Uma chave ML-KEM precisa, além disso, passar na verificação de módulo da FIPS 203. sgcX509_VerifySignedBy verifica uma assinatura ML-DSA ou SLH-DSA no modo puro com um contexto vazio, e retorna False quando o conjunto de parâmetros nomeado pelo algoritmo de assinatura não é o da própria chave do emissor.

 

Infraestrutura de DER, ASN.1 e PEM

 

sgcCrypto_ASN1 lê DER, decodifica PEM e Base64, e faz o parsing de chaves privadas e públicas RSA/EC a partir de DER. sgcCrypto_DER é o escritor, usado internamente por sgcCrypto_X509_Gen e pelas funções de exportação de chave RSA na página Criptografia de Chave Pública; a maioria das aplicações não vai chamá-lo diretamente.

 


function sgcPEM_Decode(const aPEM: string; out aLabel: string): TBytes;
function sgcPEM_Encode(const aDER: TBytes; const aLabel: string): string;
function sgcBase64Encode(const aData: TBytes): string;
function sgcBase64Decode(const aText: string): TBytes;