Certificati e PKI

Due unit coprono le direzioni opposte dello stesso formato X.509: sgcCrypto_X509 legge e verifica un certificato ricevuto da qualcun altro, sgcCrypto_X509_Gen scrive un proprio certificato o una propria richiesta. Una terza unit, sgcCrypto_ASN1, fornisce l'infrastruttura DER/PEM su cui entrambe si appoggiano.

Lettura e verifica di un certificato

sgcCrypto_X509 analizza un certificato codificato in DER in un record semplice, e verifica una catena o una lista di revoca rispetto ad esso. Utilizzare questo lato dell'API quando l'applicazione riceve un certificato dalla rete e deve decidere se fidarsi di esso.


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 accetta la catena e i trust anchor come blob DER, non come record già analizzati, e un timestamp Unix rispetto al quale verificare la finestra di validità. L'elemento 0 della catena è il leaf e ogni certificato deve essere firmato dal successivo; il vertice deve essere firmato da una delle radici attendibili, oppure esserne uguale. Il percorso viene rifiutato quando un certificato è fuori dalla propria finestra di validità in aNowUnix, quando un issuer non è una CA, quando viola il pathLenConstraint di basicConstraints (i certificati self-issued non vengono conteggiati ai fini del limite, RFC 5280 sezione 6.1.4), oppure quando viola un'estensione nameConstraints su un qualsiasi certificato del percorso, incluso il trust anchor. Le scelte di vincolo verificate sono dNSName, iPAddress, rfc822Name, uniformResourceIdentifier e directoryName; i subtree DNS si applicano anche a un common name del leaf che si legge come un nome host quando il leaf non porta alcun nome DNS proprio, e un nameConstraints critico che nomini una qualsiasi altra scelta rifiuta il percorso. Le certificate policy non vengono verificate.

TsgcX509Cert espone i campi di cui un chiamante ha effettivamente bisogno dopo il parsing: Version, SerialNumber, le sequenze RDN grezze di Issuer/Subject, NotBeforeUnix/NotAfterUnix, la chiave pubblica (a seconda di ciò che indica PubKeyType: x509pkRSA, x509pkEC, x509pkEd25519, x509pkMLDSA, x509pkSLHDSA o x509pkMLKEM) e IsCA. Una chiave post-quantistica arriva in PQCPublicKey, e il parameter set nominato dal suo object identifier si trova in quello fra MLDSAParams, SLHDSAParams o MLKEMParams che corrisponde al tipo. TsgcX509CSR rispecchia gli stessi campi per una richiesta PKCS#10.

Generazione di un certificato o di una richiesta

sgcCrypto_X509_Gen costruisce un certificato self-signed, oppure una richiesta di firma del certificato PKCS#10 da inviare a una CA, a partire da una coppia di chiavi RSA (vedere Crittografia a chiave pubblica). Entrambe accettano lo stesso record di opzioni.


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;

Costruire la finestra di validità a partire da UTC.

La validità del certificato è codificata come UTCTime con un suffisso Z, che indica l'ora di Greenwich, quindi compilare NotBefore e NotAfter a partire da un Now locale data il certificato in modo errato del proprio scostamento da UTC. A est di Greenwich un certificato appena emesso non è quindi ancora valido finché quello scostamento non trascorre, a ovest di esso il certificato scade in anticipo. sgcX509_UTCNow restituisce il valore corretto, e sgcX509_DefaultOptions lo utilizza già con un anticipo di cinque minuti per lo sfasamento dell'orologio, lo stesso margine applicato dalle CA reali.


function sgcX509_UTCNow: TDateTime;

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

Compilare i subject alternative name.

I browser e le librerie TLS moderni ignorano completamente il common name e guardano solo l'estensione DNSNames; un certificato con un common name perfetto e nessun nome DNS in SubjectAltName verrà rifiutato per quanto corretto sia il resto.

Accanto a DNSNames, IPAddresses ed Emails, TsgcX509Options porta un array URIs, per subject alternative name di tipo uniformResourceIdentifier. Qui va un'identità di servizio che è un URI anziché un nome host, il caso concreto essendo uno SPIFFE ID, che una mesh di workload confronta letteralmente e che nessun nome DNS può esprimere. Il valore viene inserito testualmente come contenuto IA5String, quindi passare un URI completo.


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

Impostare IsCA := True e un PathLength non negativo per produrre un certificato CA anziché un certificato leaf; lasciare PathLength a -1 per omettere il vincolo su un leaf. Una CSR porta le stesse estensioni richieste nel proprio attributo extensionRequest, che è dove una CA le cerca al momento dell'emissione del certificato finale.

Subject alternative name IPv6.

IPAddresses accetta IPv4 in forma puntata e IPv6 in una qualsiasi delle forme testuali della RFC 4291: la forma completa a otto gruppi, una forma compressa con :: e una forma con un indirizzo IPv4 incorporato negli ultimi 32 bit, in qualsiasi combinazione di maiuscole e minuscole. Non aggiungere parentesi quadre e non aggiungere uno zone index. Una voce IPv4 viene scritta come 4 ottetti e una voce IPv6 come 16, che è ciò che richiede la RFC 5280 e ciò con cui un client TLS confronta l'indirizzo del 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';

Name constraints su un certificato CA.

Un certificato CA può limitare ciò che i certificati sottostanti hanno il diritto di nominare, RFC 5280 sezione 4.2.1.10. Compilare uno qualsiasi fra PermittedDNSNames, ExcludedDNSNames, PermittedIPRanges o ExcludedIPRanges e l'estensione viene scritta, contrassegnata come critica. Una voce DNS è un subtree, quindi example.com copre quel nome e ogni nome sottostante. Una voce IP è un indirizzo con una lunghezza di prefisso. sgcX509_VerifyChain applica ciò che si scrive qui.


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

Emissione da una CA

Un certificato self-signed non asserisce nulla che un terzo possa verificare. sgcX509_CreateSigned emette un certificato a partire da una chiave CA e dal certificato della CA stessa, e sgcX509_CreateSignedFromCSR fa lo stesso partendo da una richiesta 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;

Il nome dell'issuer viene copiato byte per byte dal subject del certificato dell'issuer, così la catena si collega esattamente, e l'authorityKeyIdentifier è il subjectKeyIdentifier di quel certificato. TsgcX509Options.Issuer non viene mai letto da alcun builder. Entrambe le funzioni generano un'eccezione prima di firmare qualsiasi cosa quando il certificato dell'issuer è malformato, non è una CA o porta un key usage privo di keyCertSign, quando la chiave passata non è la chiave di quel certificato e quando la chiave del subject è malformata o di un algoritmo che la unit non conosce.

Quando si emette a partire da una richiesta, la firma sulla richiesta viene verificata per prima e una richiesta che non la supera genera un'eccezione. Il subject name e la chiave pubblica provengono dalla richiesta, quindi aOptions.Subject viene ignorato, e le estensioni provengono unicamente da aOptions. Ciò che la richiesta ha chiesto non viene copiato: è la CA a decidere che cosa il certificato asserisce.

Firmare con una chiave EC

sgcX509_CreateSelfSigned e sgcX509_CreateCSR firmano con RSA. La coppia Ex accetta invece una TsgcX509SignKey, che avvolge l'uno o l'altro tipo di chiave, così lo stesso record di opzioni produce un certificato RSA o un certificato EC a seconda di cosa le si passa. Costruire il wrapper con sgcX509_RSAKey o sgcX509_ECKey anziché compilare il record a mano, la metà non utilizzata viene lasciata indefinita di proposito. Le due funzioni originali sono invariate e ora si limitano ad avvolgere internamente sgcX509_RSAKey.

Un certificato P-256 è all'incirca un terzo della dimensione di uno RSA a 2048 bit e verifica molto più velocemente, motivo per cui i deployment moderni scelgono EC per default. TsgcX509Options.Hash continua a indicare solo lo SHA; l'algoritmo di firma segue il tipo di chiave, quindi rhSHA256 con una chiave EC significa ecdsa-with-SHA256, e con una chiave 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 il punto pubblico dallo scalare, quindi non è mai necessario passarlo. La curva proviene da sgcCrypto_ECCurves e può essere una qualsiasi delle sette che implementa, sebbene una CA pubblica accetterà solo P-256 o P-384.

Certificati post-quantistici

Gli stessi builder Ex firmano con una chiave post-quantistica. sgcX509_MLDSAKey avvolge una coppia di chiavi ML-DSA e sgcX509_SLHDSAKey una chiave privata SLH-DSA, e da lì non cambia nient'altro: lo stesso record di opzioni produce un certificato self-signed o una richiesta di firma del certificato firmata con ML-DSA o SLH-DSA. Vedere Crittografia post-quantistica per gli schemi stessi e per i container di chiave che il certificato trasporta.


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;

Entrambi gli schemi firmano in modalità pura con una stringa di contesto vuota, e l'algoritmo di firma è l'object identifier della chiave stessa con i parametri assenti, secondo la RFC 9881 per ML-DSA e la RFC 9909 per SLH-DSA. TsgcX509Options.Hash viene ignorato per queste chiavi, poiché nessuna delle due calcola separatamente l'hash del corpo. sgcX509_MLDSAKey verifica entrambe le lunghezze di chiave e fa firmare e verificare alla coppia un messaggio fisso, così una chiave privata a cui è stata abbinata la chiave pubblica sbagliata genera un'eccezione lì invece di produrre un certificato che nulla può verificare.

Il key usage è limitato.

Con una chiave ML-DSA o SLH-DSA sono accettati solo kuDigitalSignature, kuNonRepudiation, kuKeyCertSign e kuCRLSign, e la regola viene applicata prima che qualsiasi cosa venga firmata. sgcX509_DefaultOptions chiede kuKeyEncipherment, quindi genera un'eccezione con queste chiavi finché non si cambia KeyUsage. Un KeyUsage vuoto lascia fuori l'estensione, cosa che entrambe le RFC consentono.

Certificare una chiave ML-KEM.

Una chiave ML-KEM incapsula, non firma mai, quindi può essere il subject di un certificato ma non può mai firmare una richiesta per ottenerne uno. Passare il suo SubjectPublicKeyInfo a sgcX509_CreateSigned dopo avere dimostrato il possesso per altre vie, cosa che la RFC 9935 lascia all'applicazione. kuKeyEncipherment è l'unico flag di key usage consentito su un certificato del genere. Per lo stesso motivo sgcX509_CSR_Verify non può mai avere successo su una richiesta ML-KEM, e sgcX509_VerifySignedBy non verifica mai nulla con una chiave di issuer ML-KEM.

Rileggerne uno.

sgcX509_Parse e sgcX509_CSR_Parse trattano un certificato post-quantistico come malformato quando il suo SubjectPublicKeyInfo o il suo algoritmo di firma ML-DSA o SLH-DSA porta dei parametri, e quando la lunghezza della chiave non corrisponde al parameter set nominato dall'object identifier. Una chiave ML-KEM deve inoltre superare il controllo del modulo di FIPS 203. sgcX509_VerifySignedBy verifica una firma ML-DSA o SLH-DSA in modalità pura con un contesto vuoto, e restituisce False quando il parameter set nominato dall'algoritmo di firma non è quello della chiave dell'issuer.

Infrastruttura DER, ASN.1 e PEM

sgcCrypto_ASN1 legge DER, decodifica PEM e Base64, ed esegue il parsing di chiavi private e pubbliche RSA/EC a partire da DER. sgcCrypto_DER è il writer, utilizzato internamente da sgcCrypto_X509_Gen e dalle funzioni di esportazione delle chiavi RSA nella pagina Crittografia a chiave pubblica; la maggior parte delle applicazioni non lo chiamerà direttamente.


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;