Deux unités couvrent les directions opposées du même format X.509 : sgcCrypto_X509 lit et vérifie un certificat que quelqu'un d'autre vous a donné, sgcCrypto_X509_Gen écrit un certificat ou une requête qui vous appartient. Une troisième unité, sgcCrypto_ASN1, fournit l'infrastructure DER/PEM sur laquelle reposent les deux autres.
Lire et vérifier un certificat
sgcCrypto_X509 analyse un certificat encodé en DER dans un enregistrement simple, et vérifie une chaîne ou une liste de révocation par rapport à celui-ci. Utilisez ce côté de l'API lorsque votre application reçoit un certificat depuis le réseau et doit décider si elle lui fait confiance.
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 prend la chaîne et les ancres de confiance sous forme de blocs DER, et non d'enregistrements analysés, ainsi qu'un horodatage Unix pour éprouver la fenêtre de validité. L'élément 0 de la chaîne est la feuille et chaque certificat doit être signé par le suivant ; le sommet doit être signé par l'une des racines de confiance, ou être égal à l'une d'elles. Le chemin est rejeté lorsqu'un certificat est en dehors de sa fenêtre de validité à aNowUnix, lorsqu'un émetteur n'est pas une AC, lorsqu'il enfreint le pathLenConstraint de basicConstraints (les certificats auto-émis ne comptent pas dans la limite, RFC 5280 section 6.1.4), ou lorsqu'il enfreint une extension nameConstraints portée par n'importe quel certificat du chemin, ancre de confiance incluse. Les choix de contrainte vérifiés sont dNSName, iPAddress, rfc822Name, uniformResourceIdentifier et directoryName ; les sous-arbres DNS s'appliquent aussi au nom commun d'une feuille qui se lit comme un nom d'hôte lorsque la feuille ne porte aucun nom DNS propre, et un nameConstraints critique nommant tout autre choix rejette le chemin. Les politiques de certificat ne sont pas vérifiées.
TsgcX509Cert expose les champs dont un appelant a réellement besoin après l'analyse : Version, SerialNumber, les séquences RDN brutes Issuer/Subject, NotBeforeUnix/NotAfterUnix, la clé publique (selon ce qu'indique PubKeyType, x509pkRSA, x509pkEC, x509pkEd25519, x509pkMLDSA, x509pkSLHDSA ou x509pkMLKEM) et IsCA. Une clé post-quantique arrive dans PQCPublicKey, et l'ensemble de paramètres que nomme son identifiant d'objet se trouve dans celui de MLDSAParams, SLHDSAParams ou MLKEMParams qui correspond au type. TsgcX509CSR reflète les mêmes champs pour une requête PKCS#10.
Générer un certificat ou une requête
sgcCrypto_X509_Gen construit un certificat auto-signé, ou une requête de signature de certificat PKCS#10 à envoyer à une AC, à partir d'une paire de clés RSA (voir Cryptographie à clé publique). Les deux prennent le même enregistrement d'options.
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;
Construisez la fenêtre de validité à partir de l'UTC.
La validité d'un certificat est encodée en UTCTime avec un suffixe Z, signifiant l'heure de Greenwich, de sorte que remplir NotBefore et NotAfter à partir d'un Now local date le certificat de façon erronée selon votre décalage par rapport à l'UTC. À l'est de Greenwich, un certificat fraîchement émis n'est alors pas encore valide tant que ce décalage ne s'est pas écoulé ; à l'ouest, le certificat expire trop tôt. sgcX509_UTCNow renvoie la valeur correcte, et sgcX509_DefaultOptions l'utilise déjà avec une antidate de cinq minutes pour absorber le décalage d'horloge, la même marge qu'appliquent les AC réelles.
function sgcX509_UTCNow: TDateTime;
var
oOptions: TsgcX509Options;
begin
oOptions := sgcX509_DefaultOptions;
oOptions.NotBefore := sgcX509_UTCNow;
oOptions.NotAfter := sgcX509_UTCNow + 365;
end;
Renseignez les noms alternatifs du sujet.
Les navigateurs et bibliothèques TLS modernes ignorent entièrement le nom commun et ne regardent que l'extension DNSNames ; un certificat avec un nom commun parfait et aucun nom DNS dans SubjectAltName sera rejeté aussi correct que soit le reste.
Aux côtés de DNSNames, IPAddresses et Emails, TsgcX509Options porte un tableau URIs, pour les noms alternatifs du sujet de type uniformResourceIdentifier. Une identité de service qui est une URI plutôt qu'un nom d'hôte se place ici, le cas concret étant un identifiant SPIFFE, qu'un maillage applicatif (workload mesh) fait correspondre littéralement et qu'aucun nom DNS ne peut exprimer. La valeur est insérée telle quelle, sous forme de contenu IA5String, passez donc une URI complète.
SetLength(oOptions.URIs, 1);
oOptions.URIs[0] := 'spiffe://example.com/ns/default/sa/payments';
Définissez IsCA := True et un PathLength non négatif pour produire un certificat d'AC plutôt qu'un certificat feuille ; laissez PathLength à -1 pour omettre la contrainte sur une feuille. Une CSR porte les mêmes extensions demandées dans son attribut extensionRequest, c'est là qu'une AC les recherche lors de l'émission du certificat final.
Noms alternatifs du sujet en IPv6.
IPAddresses accepte l'IPv4 en notation pointée et l'IPv6 dans n'importe laquelle des formes textuelles de la RFC 4291 : la forme complète à huit groupes, une forme compressée avec ::, et une forme avec une adresse IPv4 incorporée dans les 32 derniers bits, quelle que soit la casse. N'ajoutez pas de crochets et n'ajoutez pas d'index de zone. Une entrée IPv4 est écrite sur 4 octets et une entrée IPv6 sur 16, ce qu'exige la RFC 5280 et ce à quoi un client TLS compare l'adresse du pair.
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';
Contraintes de noms sur un certificat d'AC.
Un certificat d'AC peut limiter ce que les certificats situés en dessous de lui ont le droit de nommer, RFC 5280 section 4.2.1.10. Renseignez l'un quelconque de PermittedDNSNames, ExcludedDNSNames, PermittedIPRanges ou ExcludedIPRanges et l'extension est écrite, marquée critique. Une entrée DNS est un sous-arbre, example.com couvre donc ce nom et tous les noms situés en dessous. Une entrée IP est une adresse avec une longueur de préfixe. sgcX509_VerifyChain fait respecter ce que vous écrivez ici.
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';
Émettre depuis une AC
Un certificat auto-signé n'affirme rien qu'un tiers puisse vérifier. sgcX509_CreateSigned émet un certificat à partir d'une clé d'AC et du certificat de cette AC, et sgcX509_CreateSignedFromCSR fait de même en partant d'une requête 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;
Le nom de l'émetteur est copié octet pour octet depuis le sujet du certificat émetteur, de sorte que la chaîne s'enchaîne exactement, et l'authorityKeyIdentifier est le subjectKeyIdentifier de ce certificat. TsgcX509Options.Issuer n'est jamais lu par aucun constructeur. Les deux fonctions lèvent une exception avant de signer quoi que ce soit lorsque le certificat émetteur est malformé, n'est pas une AC ou porte un usage de clé sans keyCertSign, lorsque la clé que vous avez passée n'est pas la clé de ce certificat, et lorsque la clé du sujet est malformée ou relève d'un algorithme que l'unité ne connaît pas.
Lors de l'émission à partir d'une requête, la signature de la requête est vérifiée d'abord et une requête qui échoue lève une exception. Le nom du sujet et la clé publique proviennent de la requête, aOptions.Subject est donc ignoré, et les extensions proviennent de aOptions seul. Ce que la requête a demandé n'est pas recopié : c'est l'AC qui décide de ce que le certificat affirme.
Signer avec une clé EC
sgcX509_CreateSelfSigned et sgcX509_CreateCSR signent avec RSA. La paire Ex prend à la place un TsgcX509SignKey, qui enveloppe l'un ou l'autre type de clé, si bien que le même enregistrement d'options produit un certificat RSA ou un certificat EC selon ce que vous lui fournissez. Construisez l'enveloppe avec sgcX509_RSAKey ou sgcX509_ECKey plutôt qu'en remplissant l'enregistrement à la main, la moitié inutilisée est volontairement laissée non définie. Les deux fonctions d'origine sont inchangées et enveloppent désormais simplement sgcX509_RSAKey en interne.
Un certificat P-256 fait environ un tiers de la taille d'un certificat RSA de 2048 bits et se vérifie bien plus vite, ce qui explique pourquoi les déploiements modernes optent par défaut pour l'EC. TsgcX509Options.Hash ne fait toujours que nommer le SHA ; l'algorithme de signature suit le type de clé, si bien que rhSHA256 avec une clé EC signifie ecdsa-with-SHA256, et avec une clé RSA il signifie 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 dérive le point public à partir du scalaire, vous n'avez donc jamais à le fournir. La courbe provient de sgcCrypto_ECCurves et peut être n'importe laquelle des sept qu'elle implémente, bien qu'une AC publique n'accepte que P-256 ou P-384.
Certificats post-quantiques
Les mêmes constructeurs Ex signent avec une clé post-quantique. sgcX509_MLDSAKey enveloppe une paire de clés ML-DSA et sgcX509_SLHDSAKey une clé privée SLH-DSA, et à partir de là rien d'autre ne change : le même enregistrement d'options produit un certificat auto-signé ou une requête de signature de certificat signés avec ML-DSA ou SLH-DSA. Voir Cryptographie post-quantique pour les schémas eux-mêmes et pour les conteneurs de clé que porte le certificat.
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;
Les deux schémas signent en mode pur avec une chaîne de contexte vide, et l'algorithme de signature est l'identifiant d'objet propre à la clé avec les paramètres absents, conformément à la RFC 9881 pour ML-DSA et à la RFC 9909 pour SLH-DSA. TsgcX509Options.Hash est ignoré pour ces clés, puisque aucun des deux ne hache le corps séparément. sgcX509_MLDSAKey vérifie les deux longueurs de clé et fait signer puis vérifier un message fixe par la paire, de sorte qu'une clé privée à laquelle on a fourni la mauvaise clé publique lève une exception à cet endroit plutôt que de produire un certificat que rien ne peut vérifier.
L'usage de clé est restreint.
Seuls kuDigitalSignature, kuNonRepudiation, kuKeyCertSign et kuCRLSign sont acceptés avec une clé ML-DSA ou SLH-DSA, et la règle est appliquée avant que quoi que ce soit ne soit signé. sgcX509_DefaultOptions demande kuKeyEncipherment, il lève donc une exception avec ces clés tant que vous ne modifiez pas KeyUsage. Un KeyUsage vide laisse l'extension de côté, ce que les deux RFC autorisent.
Certifier une clé ML-KEM.
Une clé ML-KEM encapsule, elle ne signe jamais, elle peut donc être le sujet d'un certificat mais ne peut jamais signer une requête pour en demander un. Passez son SubjectPublicKeyInfo à sgcX509_CreateSigned après avoir prouvé la possession par d'autres moyens, ce que la RFC 9935 laisse à l'application. kuKeyEncipherment est le seul indicateur d'usage de clé autorisé sur un tel certificat. Pour la même raison, sgcX509_CSR_Verify ne peut jamais réussir sur une requête ML-KEM, et sgcX509_VerifySignedBy ne vérifie jamais rien avec une clé d'émetteur ML-KEM.
Relire un certificat.
sgcX509_Parse et sgcX509_CSR_Parse traitent un certificat post-quantique comme malformé lorsque son SubjectPublicKeyInfo ou son algorithme de signature ML-DSA ou SLH-DSA porte des paramètres, et lorsque la longueur de la clé ne correspond pas à l'ensemble de paramètres que nomme l'identifiant d'objet. Une clé ML-KEM doit en outre passer le contrôle de module FIPS 203. sgcX509_VerifySignedBy vérifie une signature ML-DSA ou SLH-DSA en mode pur avec un contexte vide, et renvoie False lorsque l'ensemble de paramètres nommé par l'algorithme de signature n'est pas celui de la clé de l'émetteur.
Infrastructure DER, ASN.1 et PEM
sgcCrypto_ASN1 lit le DER, décode le PEM et le Base64, et analyse les clés privées et publiques RSA/EC à partir du DER. sgcCrypto_DER est l'écrivain, utilisé en interne par sgcCrypto_X509_Gen et par les fonctions d'export de clé RSA de la page Cryptographie à clé publique ; la plupart des applications ne l'appelleront pas directement.
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;