Certificaten & PKI

Twee units behandelen tegengestelde richtingen van hetzelfde X.509-formaat: sgcCrypto_X509 leest en verifieert een certificaat dat iemand anders u heeft gegeven, sgcCrypto_X509_Gen schrijft een certificaat of aanvraag van uzelf. Een derde unit, sgcCrypto_ASN1, levert de DER/PEM-basis waarop beide steunen.

Een certificaat lezen en verifiëren

sgcCrypto_X509 parseert een DER-gecodeerd certificaat naar een gewoon record, en controleert er een keten of een intrekkingslijst tegen. Gebruik deze kant van de API wanneer uw toepassing een certificaat over de lijn ontvangt en moet beslissen of het vertrouwd wordt.


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 neemt de keten en de vertrouwensankers als DER-blobs, niet als geparseerde records, en een Unix-tijdstempel om het geldigheidsvenster tegen af te toetsen. Element 0 van de keten is het leaf-certificaat en elk certificaat moet door het volgende zijn ondertekend; het bovenste moet zijn ondertekend door, of gelijk zijn aan, een van de vertrouwde roots. Het pad wordt geweigerd wanneer een certificaat op aNowUnix buiten zijn geldigheidsvenster valt, wanneer een uitgever geen CA is, wanneer het basicConstraints pathLenConstraint schendt (zelf uitgegeven certificaten tellen niet mee voor de limiet, RFC 5280 sectie 6.1.4), of wanneer het een nameConstraints-extensie schendt op enig certificaat in het pad, het vertrouwensanker inbegrepen. De gecontroleerde beperkingskeuzes zijn dNSName, iPAddress, rfc822Name, uniformResourceIdentifier en directoryName; de DNS-subbomen gelden ook voor een common name van het leaf-certificaat die als hostnaam leesbaar is wanneer dat certificaat geen eigen DNS-naam draagt, en een kritieke nameConstraints die een andere keuze noemt, weigert het pad. Certificaatbeleid wordt niet gecontroleerd.

TsgcX509Cert stelt de velden bloot die een aanroeper na het parseren daadwerkelijk nodig heeft: Version, SerialNumber, de raw Issuer/Subject RDN-reeksen, NotBeforeUnix/NotAfterUnix, de publieke sleutel (wat PubKeyType ook aangeeft, x509pkRSA, x509pkEC, x509pkEd25519, x509pkMLDSA, x509pkSLHDSA of x509pkMLKEM) en IsCA. Een post-quantum sleutel komt binnen in PQCPublicKey, en de parameterset die de objectidentificatie ervan noemt staat in MLDSAParams, SLHDSAParams of MLKEMParams, welke van die drie ook bij het type past. TsgcX509CSR spiegelt dezelfde velden voor een PKCS#10-aanvraag.

Een certificaat of een aanvraag genereren

sgcCrypto_X509_Gen bouwt een zelfondertekend certificaat, of een PKCS#10-certificaataanvraag om naar een CA te sturen, vanuit een RSA-sleutelpaar (zie Public key-cryptografie). Beide gebruiken hetzelfde options-record.


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;

Bouw het geldigheidsvenster op vanuit UTC.

Certificaatgeldigheid wordt gecodeerd als UTCTime met een Z-achtervoegsel, wat Greenwichtijd betekent, dus het vullen van NotBefore en NotAfter vanuit een lokale Now dateert het certificaat verkeerd met uw verschil ten opzichte van UTC. Ten oosten van Greenwich is een net uitgegeven certificaat dan nog niet geldig totdat dat verschil is verstreken, ten westen ervan verloopt het certificaat te vroeg. sgcX509_UTCNow geeft de juiste waarde terug, en sgcX509_DefaultOptions gebruikt deze al met een terugdatering van vijf minuten voor klokafwijking, dezelfde marge die echte CA's toepassen.


function sgcX509_UTCNow: TDateTime;

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

Vul de subject alternative names in.

Moderne browsers en TLS-bibliotheken negeren de common name volledig en kijken alleen naar de DNSNames-extensie; een certificaat met een perfecte common name en geen DNS-naam in SubjectAltName wordt afgewezen, hoe correct de rest ervan ook is.

Naast DNSNames, IPAddresses en Emails draagt TsgcX509Options een URIs-array, voor subject alternative names van het type uniformResourceIdentifier. Een service-identiteit die een URI is in plaats van een hostnaam komt hier terecht, het concrete geval is een SPIFFE ID, die een workload mesh letterlijk matcht en die geen enkele DNS-naam kan uitdrukken. De waarde gaat er letterlijk in als IA5String-inhoud, geef dus een volledige URI mee.


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

Stel IsCA := True in en een niet-negatieve PathLength om een CA-certificaat te produceren in plaats van een leaf-certificaat; laat PathLength op -1 staan om de beperking op een leaf weg te laten. Een CSR draagt dezelfde gevraagde extensies in het extensionRequest-attribuut, waar een CA ze zoekt bij het uitgeven van het uiteindelijke certificaat.

IPv6 subject alternative names.

IPAddresses neemt IPv4 in puntnotatie en IPv6 in elk van de tekstvormen van RFC 4291: de volledige vorm met acht groepen, een vorm die met :: is ingekort, en een vorm met een ingebed IPv4-adres in de laatste 32 bits, met hoofdletters of kleine letters. Voeg geen vierkante haken toe en geen zone-index. Een IPv4-item wordt als 4 octetten geschreven en een IPv6-item als 16, wat RFC 5280 vereist en waarmee een TLS-client het adres van de tegenpartij vergelijkt.


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';

Naamsbeperkingen op een CA-certificaat.

Een CA-certificaat kan beperken welke namen de certificaten eronder mogen dragen, RFC 5280 sectie 4.2.1.10. Vul een van PermittedDNSNames, ExcludedDNSNames, PermittedIPRanges of ExcludedIPRanges en de extensie wordt geschreven, gemarkeerd als kritiek. Een DNS-item is een subboom, dus example.com dekt die naam en elke naam eronder. Een IP-item is een adres met een prefixlengte. sgcX509_VerifyChain handhaaft wat u hier schrijft.


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';

Uitgeven vanaf een CA

Een zelfondertekend certificaat beweert niets dat een derde partij kan controleren. sgcX509_CreateSigned geeft een certificaat uit vanaf een CA-sleutel en het eigen certificaat van die CA, en sgcX509_CreateSignedFromCSR doet hetzelfde, uitgaande van een PKCS#10-aanvraag.


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;

De naam van de uitgever wordt byte voor byte gekopieerd uit het subject van het uitgeverscertificaat, zodat de keten exact aansluit, en de authorityKeyIdentifier is de subjectKeyIdentifier van dat certificaat. TsgcX509Options.Issuer wordt door geen enkele builder gelezen. Beide functies geven een fout voordat er iets wordt ondertekend wanneer het uitgeverscertificaat misvormd is, geen CA is of een key usage zonder keyCertSign draagt, wanneer de sleutel die u meegaf niet de sleutel van dat certificaat is, en wanneer de subjectsleutel misvormd is of van een algoritme dat de unit niet kent.

Bij het uitgeven vanaf een aanvraag wordt de handtekening op de aanvraag eerst geverifieerd, en een aanvraag die daar niet doorheen komt geeft een fout. De subjectnaam en de publieke sleutel komen uit de aanvraag, dus aOptions.Subject wordt genegeerd, en de extensies komen uitsluitend uit aOptions. Wat de aanvraag vroeg wordt niet overgenomen: de CA beslist wat het certificaat beweert.

Ondertekenen met een EC-sleutel

sgcX509_CreateSelfSigned en sgcX509_CreateCSR ondertekenen met RSA. Het Ex-paar neemt in plaats daarvan een TsgcX509SignKey, die beide soorten sleutels omwikkelt, zodat hetzelfde options-record een RSA- of een EC-certificaat oplevert, afhankelijk van wat u eraan meegeeft. Bouw de wrapper met sgcX509_RSAKey of sgcX509_ECKey in plaats van het record met de hand te vullen, de ongebruikte helft wordt met opzet ongedefinieerd gelaten. De twee oorspronkelijke functies zijn ongewijzigd en wikkelen nu intern gewoon sgcX509_RSAKey om.

Een P-256-certificaat is ruwweg een derde van de grootte van een 2048 bit RSA-certificaat en verifieert veel sneller, wat de reden is waarom moderne implementaties standaard EC gebruiken. TsgcX509Options.Hash noemt nog steeds alleen de SHA; het handtekeningalgoritme volgt het soort sleutel, dus rhSHA256 met een EC-sleutel betekent ecdsa-with-SHA256, en met een RSA-sleutel betekent het 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 leidt het publieke punt af van de scalar, dus u hoeft het nooit mee te geven. De curve komt uit sgcCrypto_ECCurves en kan elk van de zeven zijn die deze implementeert, hoewel een publieke CA alleen P-256 of P-384 zal accepteren.

Post-quantum certificaten

Dezelfde Ex-builders ondertekenen met een post-quantum sleutel. sgcX509_MLDSAKey wikkelt een ML-DSA-sleutelpaar om en sgcX509_SLHDSAKey een SLH-DSA-privésleutel, en verder verandert er niets: hetzelfde options-record levert een zelfondertekend certificaat of een certificaataanvraag op, ondertekend met ML-DSA of SLH-DSA. Zie Post-quantumcryptografie voor de schema's zelf en voor de sleutelcontainers die het certificaat draagt.


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;

Beide schema's ondertekenen in de pure modus met een lege contexttekenreeks, en het handtekeningalgoritme is de eigen objectidentificatie van de sleutel met afwezige parameters, volgens RFC 9881 voor ML-DSA en RFC 9909 voor SLH-DSA. TsgcX509Options.Hash wordt voor deze sleutels genegeerd, omdat geen van beide de body apart hasht. sgcX509_MLDSAKey controleert beide sleutellengtes en laat het paar een vast bericht ondertekenen en verifiëren, zodat een privésleutel die de verkeerde publieke sleutel meekrijgt daar een fout geeft in plaats van een certificaat te produceren dat niets kan verifiëren.

Key usage is beperkt.

Alleen kuDigitalSignature, kuNonRepudiation, kuKeyCertSign en kuCRLSign worden geaccepteerd met een ML-DSA- of SLH-DSA-sleutel, en die regel wordt afgedwongen voordat er iets wordt ondertekend. sgcX509_DefaultOptions vraagt om kuKeyEncipherment, dus dat geeft een fout met deze sleutels totdat u KeyUsage wijzigt. Een lege KeyUsage laat de extensie weg, wat beide RFC's toestaan.

Een ML-KEM-sleutel certificeren.

Een ML-KEM-sleutel kapselt in, hij ondertekent nooit, dus hij kan het subject van een certificaat zijn maar kan nooit een aanvraag ondertekenen om er een te vragen. Geef de SubjectPublicKeyInfo ervan door aan sgcX509_CreateSigned nadat u het bezit op een andere manier hebt aangetoond, wat RFC 9935 aan de toepassing overlaat. kuKeyEncipherment is de enige key usage-vlag die op zo'n certificaat is toegestaan. Om dezelfde reden kan sgcX509_CSR_Verify nooit slagen op een ML-KEM-aanvraag, en verifieert sgcX509_VerifySignedBy nooit iets met een ML-KEM-uitgeverssleutel.

Er een teruglezen.

sgcX509_Parse en sgcX509_CSR_Parse beschouwen een post-quantum certificaat als misvormd wanneer de SubjectPublicKeyInfo ervan of het ML-DSA- of SLH-DSA-handtekeningalgoritme parameters draagt, en wanneer de sleutellengte niet past bij de parameterset die de objectidentificatie noemt. Een ML-KEM-sleutel moet bovendien de FIPS 203-moduluscontrole doorstaan. sgcX509_VerifySignedBy verifieert een ML-DSA- of SLH-DSA-handtekening in de pure modus met een lege context, en geeft False terug wanneer de parameterset die het handtekeningalgoritme noemt niet die van de uitgeverssleutel zelf is.

DER-, ASN.1- en PEM-infrastructuur

sgcCrypto_ASN1 leest DER, decodeert PEM en Base64, en parseert RSA/EC-privé- en publieke sleutels uit DER. sgcCrypto_DER is de schrijver, intern gebruikt door sgcCrypto_X509_Gen en door de RSA-sleutelexportfuncties op de pagina Public key-cryptografie; de meeste toepassingen zullen deze niet rechtstreeks aanroepen.


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;