Dos unidades cubren direcciones opuestas del mismo formato X.509: sgcCrypto_X509 lee y verifica un certificado que le entregó otra persona, sgcCrypto_X509_Gen escribe un certificado o una solicitud propios. Una tercera unidad, sgcCrypto_ASN1, proporciona la infraestructura DER/PEM sobre la que se apoyan ambas.
Leer y verificar un certificado
sgcCrypto_X509 analiza un certificado codificado en DER y lo convierte en un registro sencillo, y comprueba una cadena o una lista de revocación frente a él. Use este lado de la API cuando su aplicación recibe un certificado por la red y tiene que decidir si confiar en él.
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 toma la cadena y los anclajes de confianza como blobs DER, no como registros ya analizados, y una marca de tiempo Unix con la que contrastar la ventana de validez. El elemento 0 de la cadena es la hoja y cada certificado debe estar firmado por el siguiente; el superior debe estar firmado por uno de los certificados raíz de confianza, o ser igual a uno de ellos. La ruta se rechaza cuando un certificado está fuera de su ventana de validez en aNowUnix, cuando un emisor no es una CA, cuando incumple el pathLenConstraint de basicConstraints (los certificados autoemitidos no cuentan para el límite, RFC 5280 sección 6.1.4), o cuando incumple una extensión nameConstraints de cualquier certificado de la ruta, incluido el anclaje de confianza. Las opciones de restricción que se comprueban son dNSName, iPAddress, rfc822Name, uniformResourceIdentifier y directoryName; los subárboles DNS se aplican también al nombre común de la hoja cuando este se lee como un nombre de host y la hoja no lleva ningún nombre DNS propio, y una extensión nameConstraints crítica que nombre cualquier otra opción rechaza la ruta. Las políticas de certificado no se comprueban.
TsgcX509Cert expone los campos que realmente necesita quien la llama tras analizar el certificado: Version, SerialNumber, las secuencias RDN en bruto Issuer/Subject, NotBeforeUnix/NotAfterUnix, la clave pública (la que indique PubKeyType, x509pkRSA, x509pkEC, x509pkEd25519, x509pkMLDSA, x509pkSLHDSA o x509pkMLKEM) e IsCA. Una clave poscuántica llega en PQCPublicKey, y el conjunto de parámetros que nombra su identificador de objeto está en el campo que corresponde al tipo, MLDSAParams, SLHDSAParams o MLKEMParams. TsgcX509CSR refleja los mismos campos para una solicitud PKCS#10.
Generar un certificado o una solicitud
sgcCrypto_X509_Gen construye un certificado autofirmado, o una solicitud de firma de certificado PKCS#10 para enviar a una CA, a partir de un par de claves RSA (vea Criptografía de Clave Pública). Ambas funciones toman el mismo registro de opciones.
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;
Construya la ventana de validez a partir de UTC.
La validez del certificado se codifica como UTCTime con un sufijo Z, que significa hora de Greenwich, así que rellenar NotBefore y NotAfter a partir de un Now local deja el certificado mal fechado según su desfase respecto a UTC. Al este de Greenwich, un certificado recién emitido no es entonces válido hasta que transcurre ese desfase; al oeste, el certificado caduca antes de tiempo. sgcX509_UTCNow devuelve el valor correcto, y sgcX509_DefaultOptions ya lo usa con un retroceso de cinco minutos para el desfase de reloj, el mismo margen que aplican las CA reales.
function sgcX509_UTCNow: TDateTime;
var
oOptions: TsgcX509Options;
begin
oOptions := sgcX509_DefaultOptions;
oOptions.NotBefore := sgcX509_UTCNow;
oOptions.NotAfter := sgcX509_UTCNow + 365;
end;
Rellene los nombres alternativos del sujeto.
Los navegadores y bibliotecas TLS modernos ignoran por completo el nombre común y solo miran la extensión DNSNames; un certificado con un nombre común perfecto y ningún nombre DNS en SubjectAltName será rechazado por muy correcto que sea el resto.
Junto a DNSNames, IPAddresses y Emails, TsgcX509Options incluye un array URIs, para nombres alternativos del sujeto de tipo uniformResourceIdentifier. Aquí va una identidad de servicio que es un URI en lugar de un nombre de host, siendo el caso concreto un SPIFFE ID, que una malla de cargas de trabajo compara literalmente y que ningún nombre DNS puede expresar. El valor se incluye tal cual como contenido IA5String, así que pase un URI completo.
SetLength(oOptions.URIs, 1);
oOptions.URIs[0] := 'spiffe://example.com/ns/default/sa/payments';
Establezca IsCA := True y un PathLength no negativo para producir un certificado de CA en lugar de un certificado hoja; deje PathLength en -1 para omitir la restricción en una hoja. Una CSR lleva las mismas extensiones solicitadas en su atributo extensionRequest, que es donde una CA las busca al emitir el certificado final.
Nombres alternativos del sujeto IPv6.
IPAddresses acepta IPv4 en forma decimal con puntos e IPv6 en cualquiera de las formas de texto de RFC 4291: la forma completa de ocho grupos, una forma comprimida con ::, y una forma con una dirección IPv4 incrustada en los últimos 32 bits, en mayúsculas o en minúsculas indistintamente. No añada corchetes ni un índice de zona. Una entrada IPv4 se escribe como 4 octetos y una entrada IPv6 como 16, que es lo que exige RFC 5280 y con lo que un cliente TLS compara la dirección del par.
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';
Restricciones de nombres en un certificado de CA.
Un certificado de CA puede limitar qué nombres pueden llevar los certificados que cuelgan de él, RFC 5280 sección 4.2.1.10. Rellene cualquiera de PermittedDNSNames, ExcludedDNSNames, PermittedIPRanges o ExcludedIPRanges y la extensión se escribe, marcada como crítica. Una entrada DNS es un subárbol, así que example.com cubre ese nombre y todos los que quedan por debajo. Una entrada IP es una dirección con una longitud de prefijo. sgcX509_VerifyChain hace cumplir lo que escriba aquí.
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';
Emitir desde una CA
Un certificado autofirmado no afirma nada que un tercero pueda comprobar. sgcX509_CreateSigned emite un certificado a partir de una clave de CA y del propio certificado de la CA, y sgcX509_CreateSignedFromCSR hace lo mismo partiendo de una solicitud 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;
El nombre del emisor se copia byte a byte del sujeto del certificado emisor, de modo que la cadena enlaza con exactitud, y el authorityKeyIdentifier es el subjectKeyIdentifier de ese certificado. TsgcX509Options.Issuer no lo lee ningún generador. Ambas funciones generan un error antes de firmar nada cuando el certificado emisor está malformado, no es una CA o lleva un uso de clave sin keyCertSign, cuando la clave que ha pasado no es la clave de ese certificado, y cuando la clave del sujeto está malformada o es de un algoritmo que la unidad no conoce.
Al emitir a partir de una solicitud, primero se verifica la firma de la solicitud, y una solicitud que no la supera genera un error. El nombre del sujeto y la clave pública proceden de la solicitud, así que aOptions.Subject se ignora, y las extensiones proceden únicamente de aOptions. Lo que pedía la solicitud no se copia: la CA decide qué afirma el certificado.
Firmar con una clave EC
sgcX509_CreateSelfSigned y sgcX509_CreateCSR firman con RSA. El par Ex toma en su lugar un TsgcX509SignKey, que envuelve cualquiera de los dos tipos de clave, de modo que el mismo registro de opciones produce un certificado RSA o EC según lo que le entregue. Construya el envoltorio con sgcX509_RSAKey o sgcX509_ECKey en lugar de rellenar el registro a mano, la mitad no usada se deja indefinida a propósito. Las dos funciones originales no cambian y ahora simplemente envuelven sgcX509_RSAKey internamente.
Un certificado P-256 tiene aproximadamente un tercio del tamaño de uno RSA de 2048 bits y verifica mucho más rápido, razón por la cual los despliegues modernos usan EC por defecto. TsgcX509Options.Hash sigue nombrando solo el SHA; el algoritmo de firma sigue al tipo de clave, así que rhSHA256 con una clave EC significa ecdsa-with-SHA256, y con una clave 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 el punto público a partir del escalar, así que nunca tiene que pasarlo. La curva procede de sgcCrypto_ECCurves y puede ser cualquiera de las siete que implementa, aunque una CA pública solo aceptará P-256 o P-384.
Certificados poscuánticos
Los mismos generadores Ex firman con una clave poscuántica. sgcX509_MLDSAKey envuelve un par de claves ML-DSA y sgcX509_SLHDSAKey una clave privada SLH-DSA, y a partir de ahí no cambia nada más: el mismo registro de opciones produce un certificado autofirmado o una solicitud de firma de certificado firmada con ML-DSA o SLH-DSA. Vea Criptografía Poscuántica para los esquemas en sí y para los contenedores de clave que lleva el certificado.
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;
Ambos esquemas firman en el modo puro con una cadena de contexto vacía, y el algoritmo de firma es el propio identificador de objeto de la clave con los parámetros ausentes, siguiendo RFC 9881 para ML-DSA y RFC 9909 para SLH-DSA. TsgcX509Options.Hash se ignora para estas claves, ya que ninguna de las dos aplica un hash aparte al cuerpo. sgcX509_MLDSAKey comprueba las longitudes de ambas claves y hace que el par firme y verifique un mensaje fijo, de modo que una clave privada a la que se entrega la clave pública equivocada genera un error ahí, en lugar de producir un certificado que nada puede verificar.
El uso de clave está restringido.
Con una clave ML-DSA o SLH-DSA solo se aceptan kuDigitalSignature, kuNonRepudiation, kuKeyCertSign y kuCRLSign, y la regla se aplica antes de firmar nada. sgcX509_DefaultOptions pide kuKeyEncipherment, así que genera un error con estas claves hasta que cambie KeyUsage. Un KeyUsage vacío deja fuera la extensión, algo que ambos RFC permiten.
Certificar una clave ML-KEM.
Una clave ML-KEM encapsula, nunca firma, así que puede ser el sujeto de un certificado pero nunca puede firmar una solicitud para pedir uno. Pase su SubjectPublicKeyInfo a sgcX509_CreateSigned después de demostrar la posesión por otros medios, que es lo que RFC 9935 deja en manos de la aplicación. kuKeyEncipherment es el único indicador de uso de clave permitido en un certificado así. Por la misma razón, sgcX509_CSR_Verify nunca puede pasar con una solicitud ML-KEM, y sgcX509_VerifySignedBy nunca verifica nada con una clave de emisor ML-KEM.
Volver a leerlo.
sgcX509_Parse y sgcX509_CSR_Parse tratan un certificado poscuántico como malformado cuando su SubjectPublicKeyInfo o su algoritmo de firma ML-DSA o SLH-DSA lleva parámetros, y cuando la longitud de la clave no coincide con el conjunto de parámetros que nombra el identificador de objeto. Una clave ML-KEM debe superar además la comprobación de módulo de FIPS 203. sgcX509_VerifySignedBy verifica una firma ML-DSA o SLH-DSA en el modo puro con un contexto vacío, y devuelve False cuando el conjunto de parámetros que nombra el algoritmo de firma no es el de la propia clave del emisor.
Infraestructura DER, ASN.1 y PEM
sgcCrypto_ASN1 lee DER, decodifica PEM y Base64, y analiza claves privadas y públicas RSA/EC a partir de DER. sgcCrypto_DER es el escritor, usado internamente por sgcCrypto_X509_Gen y por las funciones de exportación de claves RSA de la página Criptografía de Clave Pública; la mayoría de las aplicaciones no la llamarán directamente.
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;