Crittografia post-quantistica

Un computer quantistico sufficientemente grande romperebbe RSA, X25519/X448 ed ECDSA/ECDH su ogni curva della pagina Crittografia a chiave pubblica: nessuna di quella matematica resiste all'algoritmo di Shor. Non è necessario che esista già oggi perché sia un problema già oggi, un avversario può registrare adesso il traffico cifrato e decifrarlo non appena una tale macchina esisterà, una strategia solitamente chiamata "harvest now, decrypt later". Qualsiasi cosa debba rimanere riservata per anni, non per giorni, dovrebbe già passare a uno scambio di chiavi post-quantistico.

NIST ha standardizzato tre schemi, tutti implementati qui, e tutti costruiti unicamente su SHAKE (vedere Hashing e autenticazione dei messaggi), quindi la loro sicurezza dipende solo dalla solidità dell'hash: nessuna assunzione di teoria dei reticoli o dei numeri da violare.

Schema Scopo Standard Unit
ML-KEM Key encapsulation, sostituisce Diffie-Hellman FIPS 203 sgcCrypto_MLKEM
ML-DSA Firme, basato su reticoli, la scelta predefinita FIPS 204 sgcCrypto_MLDSA
SLH-DSA Firme, basato solo su hash, alternativa conservativa FIPS 205 sgcCrypto_SLHDSA

ML-KEM e la costruzione ibrida (consigliata)

ML-KEM sostituisce uno scambio Diffie-Hellman: il mittente incapsula un segreto condiviso verso la chiave pubblica del destinatario, il destinatario lo decapsula con la chiave privata, ed entrambi ottengono gli stessi 32 byte. È disponibile in tre parameter set, dove più grande significa un margine di sicurezza maggiore e chiavi più grandi.


procedure sgcMLKEM_GenerateKeyPair(aParams: TsgcMLKEMParams; out aPublicKey, aPrivateKey: TBytes);
procedure sgcMLKEM_Encapsulate(aParams: TsgcMLKEMParams; const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcMLKEM_Decapsulate(aParams: TsgcMLKEMParams; const aPrivateKey, aCiphertext: TBytes): TBytes;

var
  oPub, oPriv, oCiphertext, oSecretA, oSecretB: TBytes;
begin
  sgcMLKEM_GenerateKeyPair(mlkem768, oPub, oPriv);
  sgcMLKEM_Encapsulate(mlkem768, oPub, oCiphertext, oSecretA);   { sender }
  oSecretB := sgcMLKEM_Decapsulate(mlkem768, oPriv, oCiphertext); { recipient }
  { oSecretA = oSecretB }
end;

La decapsulazione FIPS 203 non fallisce mai.

Un ciphertext corrotto o malevolo non genera un errore, restituisce silenziosamente un segreto diverso, derivato da un valore di rifiuto implicito incorporato nella chiave privata. Restituire un errore al suo posto consegnerebbe a un attaccante un oracolo di decifratura con cui sondare la chiave privata; lo standard evita questo per progetto. Qualunque cosa il segreto derivato protegga deve rilevare da sola la discrepanza, ad esempio un tag AEAD che non riesce a verificarsi a valle.

Ciò che conviene effettivamente adottare oggi è la costruzione ibrida: X25519 combinato con ML-KEM-768, in modo che il segreto condiviso dipenda da entrambi. Resta sicuro se ML-KEM viene mai violato da un progresso crittanalitico, e resta sicuro se un computer quantistico viola X25519; un attaccante ha bisogno che falliscano entrambi contemporaneamente.


procedure sgcHybrid_GenerateKeyPair(out aPublicKey, aPrivateKey: TBytes);
procedure sgcHybrid_Encapsulate(const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcHybrid_Decapsulate(const aPrivateKey, aCiphertext: TBytes; out aSharedSecret: TBytes): Boolean;

X-Wing e i gruppi ibridi di TLS 1.3

sgcCrypto_MLKEM_Hybrid contiene tre famiglie ibride. Le funzioni sgcHybrid_ viste sopra sono il formato sgcCrypto originale e sono mantenute per compatibilità. X-Wing è il KEM ibrido standardizzato di uso generale, X25519 combinato con ML-KEM-768, ed è quello da utilizzare in codice nuovo al di fuori di TLS. La terza famiglia costruisce le key share ibride di TLS 1.3 e nient'altro.

X-Wing (draft-connolly-cfrg-xwing-kem-10) memorizza un solo segreto: la chiave privata è un seed di 32 byte, e ogni operazione lo espande nel materiale ML-KEM-768 e X25519 di cui ha bisogno. La chiave pubblica è di 1216 byte, il ciphertext di 1120 byte e il segreto condiviso di 32 byte. L'incapsulamento esegue il controllo della chiave di incapsulamento di FIPS 203 e genera un'eccezione quando la chiave pubblica non lo supera.


procedure sgcXWing_GenerateKeyPair(out aPublicKey, aPrivateKey: TBytes);
procedure sgcXWing_Encapsulate(const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcXWing_Decapsulate(const aPrivateKey, aCiphertext: TBytes; out aSharedSecret: TBytes): Boolean;

var
  oPub, oPriv, oCiphertext, oSecretA, oSecretB: TBytes;
begin
  sgcXWing_GenerateKeyPair(oPub, oPriv);
  sgcXWing_Encapsulate(oPub, oCiphertext, oSecretA);      { sender }
  if sgcXWing_Decapsulate(oPriv, oCiphertext, oSecretB) then
    { oSecretA = oSecretB };
end;

Le key share ibride di TLS 1.3 della RFC 10024 sono X25519MLKEM768, SecP256r1MLKEM768 e SecP384r1MLKEM1024, codepoint IANA 0x11EC, 0x11EB e 0x11ED. Non sono KEM utilizzabili per conto proprio. Il segreto condiviso è una semplice concatenazione delle due metà, il che è sicuro solo perché TLS lo immette nel proprio key schedule insieme al transcript dell'handshake, quindi vanno utilizzate per costruire una key share TLS e per nient'altro. Il motore TLS nativo offre X25519MLKEM768 per primo per impostazione predefinita, quindi un client o un server che lo utilizza negozia già uno scambio di chiavi post-quantistico senza codice aggiuntivo.

I gruppi ibridi esistono solo in TLS 1.3. Il motore nativo parla anche TLS 1.2, e una connessione che ripiega su di esso perché il peer non supporta TLS 1.3 utilizza un gruppo classico, X25519, secp256r1 o secp384r1, senza protezione post-quantistica. Impostare TLSOptions.Version su un client o SSLOptions.Version su un server su tls1_3, ed elencare solo gruppi ibridi in NativeTLS_Options.Groups, quando lo scambio di chiavi deve essere sempre post-quantistico.

Il demo Post-Quantum TLS, Demos\18.Cryptography\08.PostQuantum_TLS, dispone di un selettore di versione TLS con le opzioni TLS 1.3 e TLS 1.2, solo TLS 1.3 e solo TLS 1.2, e di un selettore di suite di cifratura, e la sua riga di stato mostra la versione, il gruppo e la suite di cifratura negoziati. Il demo server può firmare con un certificato ML-DSA-44, che serve solo i client TLS 1.3, perché TLS 1.2 non ha alcuno schema di firma ML-DSA.


type
  TsgcTLSHybridGroup = (thgX25519MLKEM768, thgSecP256r1MLKEM768, thgSecP384r1MLKEM1024);

function sgcTLSHybrid_GroupID(aGroup: TsgcTLSHybridGroup): Word;
function sgcTLSHybrid_GroupName(aGroup: TsgcTLSHybridGroup): string;
function sgcTLSHybrid_Available(aGroup: TsgcTLSHybridGroup): Boolean;

procedure sgcTLSHybrid_ClientKeyShare(aGroup: TsgcTLSHybridGroup; out aKeyShare, aPrivateState: TBytes);
function sgcTLSHybrid_ServerKeyShare(aGroup: TsgcTLSHybridGroup; const aClientKeyShare: TBytes;
  out aKeyShare, aSharedSecret: TBytes): Boolean;
function sgcTLSHybrid_ClientSharedSecret(aGroup: TsgcTLSHybridGroup;
  const aPrivateState, aServerKeyShare: TBytes; out aSharedSecret: TBytes): Boolean;

Il client mantiene segreto aPrivateState, lo passa invariato a sgcTLSHybrid_ClientSharedSecret e poi lo cancella; il suo layout non fa parte dell'API. Le due funzioni Boolean restituiscono False, con tutti gli output vuoti, per qualsiasi input non valido del peer: una key share di lunghezza errata, una chiave di incapsulamento ML-KEM che non supera il controllo di FIPS 203, un punto che non si trova sulla curva o un risultato X25519 tutto a zero. La RFC 10024 richiede che in ciascuno di questi casi la connessione venga interrotta con un alert illegal_parameter.

Tutto ciò che è costruito su X25519, quindi le funzioni sgcHybrid_, X-Wing e il gruppo X25519MLKEM768, viene escluso dalla compilazione in una build SGC_CRYPTO_FIPS, vedere Modalità FIPS. I due gruppi SecP rimangono, perché P-256, P-384 e ML-KEM sono tutti algoritmi approvati. Chiamare sgcTLSHybrid_Available prima di offrire o accettare un gruppo, anziché dare per scontato che sia presente.

ML-DSA (consigliato per le firme)

La firma estrae un vettore di mascheramento e tenta di costruire una firma, riavviando quando il tentativo rivelerebbe informazioni sulla chiave; il numero atteso di tentativi è piccolo ma non fisso, quindi la firma richiede una quantità di tempo variabile. Ciò è intrinseco allo schema e non è un side channel sulla chiave. Le firme sono grandi: 2420 byte per ML-DSA-44 contro 64 per Ed25519, e le chiavi pubbliche 1312 byte contro 32, quindi occorre tenere conto della differenza prima di adottarlo in un protocollo sensibile alla dimensione.


procedure sgcMLDSA_GenerateKeyPair(aParams: TsgcMLDSAParams; out aPublicKey, aPrivateKey: TBytes);
function sgcMLDSA_Sign(aParams: TsgcMLDSAParams; const aPrivateKey, aMessage, aContext: TBytes): TBytes;
function sgcMLDSA_Verify(aParams: TsgcMLDSAParams; const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;

var
  oPub, oPriv, oSignature, oEmpty: TBytes;
begin
  sgcMLDSA_GenerateKeyPair(mldsa44, oPub, oPriv);
  oSignature := sgcMLDSA_Sign(mldsa44, oPriv, oMessage, oEmpty);
  if sgcMLDSA_Verify(mldsa44, oPub, oMessage, oSignature, oEmpty) then
    { valid };
end;

SLH-DSA (alternativa conservativa)

SLH-DSA si basa unicamente sulla funzione hash: nessun problema reticolare da violare, mai, in nessun momento futuro. Questa garanzia è costosa. Una firma va da 7856 byte (la più piccola, SLH-DSA-SHAKE-128s) a 49856 byte (la più grande, SLH-DSA-SHAKE-256f), e firmare con un parameter set s (small) percorre centinaia di migliaia di operazioni hash e può richiedere decine di secondi; i set f (fast) firmano in circa uno-sette secondi a circa il doppio della dimensione della firma. La verifica è rapida per ogni parameter set, il che si adatta a un caso d'uso firma-raramente-verifica-spesso come il firmware o il code signing meglio di un protocollo interattivo.


procedure sgcSLHDSA_GenerateKeyPair(aParams: TsgcSLHDSAParams; out aPublicKey, aPrivateKey: TBytes);
function sgcSLHDSA_Sign(aParams: TsgcSLHDSAParams; const aPrivateKey, aMessage, aContext: TBytes;
  aDeterministic: Boolean = True): TBytes;
function sgcSLHDSA_Verify(aParams: TsgcSLHDSAParams; const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;

Formati delle chiavi: SubjectPublicKeyInfo, PKCS#8 e PEM

Le stringhe di byte grezze che gli algoritmi FIPS consumano non sono un formato di memorizzazione, e nessun'altra implementazione le leggerà. Per scrivere una chiave su disco, inserirla in un certificato o consegnarla a un'altra libreria, esportarla nei container standard: un SubjectPublicKeyInfo X.509 per una chiave pubblica, un OneAsymmetricKey PKCS#8 per una chiave privata e l'armatura PEM (RFC 7468) attorno a entrambe, etichettata PUBLIC KEY e PRIVATE KEY. I tre schemi espongono le stesse otto funzioni, ciascuna conforme alla RFC che ne definisce la codifica.

Schema RFC del container Funzioni DER Funzioni PEM
ML-DSA RFC 9881 sgcMLDSA_ExportSubjectPublicKeyInfo, sgcMLDSA_ImportSubjectPublicKeyInfo, sgcMLDSA_ExportPrivateKeyInfo, sgcMLDSA_ImportPrivateKeyInfo sgcMLDSA_ExportPublicKeyPEM, sgcMLDSA_ImportPublicKeyPEM, sgcMLDSA_ExportPrivateKeyPEM, sgcMLDSA_ImportPrivateKeyPEM
ML-KEM RFC 9935 sgcMLKEM_ExportSubjectPublicKeyInfo, sgcMLKEM_ImportSubjectPublicKeyInfo, sgcMLKEM_ExportPrivateKeyInfo, sgcMLKEM_ImportPrivateKeyInfo sgcMLKEM_ExportPublicKeyPEM, sgcMLKEM_ImportPublicKeyPEM, sgcMLKEM_ExportPrivateKeyPEM, sgcMLKEM_ImportPrivateKeyPEM
SLH-DSA RFC 9909 sgcSLHDSA_ExportSubjectPublicKeyInfo, sgcSLHDSA_ImportSubjectPublicKeyInfo, sgcSLHDSA_ExportPrivateKeyInfo, sgcSLHDSA_ImportPrivateKeyInfo sgcSLHDSA_ExportPublicKeyPEM, sgcSLHDSA_ImportPublicKeyPEM, sgcSLHDSA_ExportPrivateKeyPEM, sgcSLHDSA_ImportPrivateKeyPEM

L'identificatore di algoritmo porta l'object identifier NIST con i parametri assenti, quindi il parameter set viaggia insieme alla chiave e l'importazione lo restituisce. sgcMLDSA_OID, sgcMLKEM_OID e sgcSLHDSA_OID forniscono la stringa in notazione puntata, e le corrispondenti funzioni ParamsFromOID eseguono la conversione inversa.

Tre forme di chiave privata per ML-DSA e ML-KEM.

Le funzioni di esportazione accettano un parametro aFormat di tipo TsgcPQCPrivateKeyFormat, dichiarato in sgcCrypto_DER. pqkfSeed, il valore predefinito, scrive il solo seed, 32 byte per ML-DSA e i 64 byte d || z per ML-KEM; è la forma consigliata da entrambe le RFC ed è di gran lunga la più piccola. pqkfExpanded scrive la chiave privata FIPS espansa. pqkfBoth scrive entrambe e genera un'eccezione quando la chiave espansa non è l’espansione del seed, che è ciò che le RFC chiamano chiave malformata.

Un seed non può essere recuperato in seguito da una chiave espansa, quindi va richiesto al momento della generazione con sgcMLDSA_GenerateKeyPairAndSeed o sgcMLKEM_GenerateKeyPairAndSeed quando si intende memorizzare la forma consigliata. SLH-DSA ha un'unica forma di chiave privata, SK.seed || SK.prf || PK.seed || PK.root, quindi la sua esportazione non accetta alcun parametro di formato.


function sgcMLDSA_ExportPrivateKeyPEM(aParams: TsgcMLDSAParams; const aSeed, aPrivateKey: TBytes;
  aFormat: TsgcPQCPrivateKeyFormat = pqkfSeed): string;
function sgcMLDSA_ImportPrivateKeyPEM(const aPEM: string; out aParams: TsgcMLDSAParams;
  out aSeed, aPrivateKey, aPublicKey: TBytes): Boolean;

var
  vParams: TsgcMLDSAParams;
  oSeed, oPub, oPriv: TBytes;
  vPublicPEM, vPrivatePEM: string;
begin
  sgcMLDSA_GenerateKeyPairAndSeed(mldsa65, oSeed, oPub, oPriv);

  vPublicPEM := sgcMLDSA_ExportPublicKeyPEM(mldsa65, oPub);
  vPrivatePEM := sgcMLDSA_ExportPrivateKeyPEM(mldsa65, oSeed, oPriv, pqkfSeed);

  { reading it back: the parameter set comes from the key itself }
  if sgcMLDSA_ImportPrivateKeyPEM(vPrivatePEM, vParams, oSeed, oPriv, oPub) then
    { vParams = mldsa65, and oPriv and oPub are the expansion of oSeed };
end;

L’importazione valida, non si fida.

Ogni funzione di importazione restituisce False anziché generare un'eccezione, e restituisce False su DER malformato, su dati residui, su un object identifier sconosciuto, su parametri presenti quando devono essere assenti e su una chiave la cui lunghezza non corrisponde al parameter set. Oltre alla struttura, una chiave pubblica ML-KEM deve superare il controllo del modulo di FIPS 203, una chiave privata ML-KEM espansa deve superare il controllo dell'hash e il test di coerenza della coppia, e una chiave privata ML-DSA espansa viene ricostruita nella propria chiave pubblica, in modo che i valori t0 e tr memorizzati debbano entrambi concordare con essa. Una chiave pubblica incorporata che non corrisponde alla chiave privata viene rifiutata. Quando una chiave privata contiene un seed, tutti e tre gli output vengono restituiti compilati, così un chiamante non deve mai espanderlo separatamente.

sgcSLHDSA_ImportPrivateKeyInfo e sgcSLHDSA_ImportPrivateKeyPEM accettano un parametro aggiuntivo aVerify, True per impostazione predefinita, che ricalcola PK.root a partire dal seed privato e rifiuta una chiave la cui root memorizzata è diversa. Quella ricostruzione costa quanto una generazione di chiavi, cioè alcuni secondi per i parameter set s, quindi passare False solo per chiavi generate dalla propria applicazione o di cui già ci si fida. L'object identifier, le lunghezze e la struttura vengono comunque verificati quando è False.

Questi sono i container che il resto della libreria legge. Una chiave privata ML-DSA PKCS#8 è ciò con cui firma il client JWT, un SubjectPublicKeyInfo è ciò con cui valida il server JWT, ed entrambi sono ciò che Certificati e PKI inserisce in un certificato o in una richiesta di firma del certificato.

Scegliere un parameter set

Ogni funzione di dimensione (sgcMLKEM_PublicKeySize, sgcMLDSA_SignatureSize, sgcSLHDSA_SignatureSize e le rispettive controparti PrivateKeySize) accetta lo stesso enum di parametri dell'operazione stessa, in modo che un protocollo possa dimensionare i propri buffer prima di eseguire qualsiasi operazione crittografica:

Parameter set Chiave pubblica Chiave privata Ciphertext / Firma
ML-KEM-5128001632768
ML-KEM-768118424001088
ML-KEM-1024156831681568
ML-DSA-44131225602420
ML-DSA-65195240323309
ML-DSA-87259248964627
SLH-DSA-SHAKE-128s32647856
SLH-DSA-SHAKE-128f326417088
SLH-DSA-SHAKE-192s489616224
SLH-DSA-SHAKE-192f489635664
SLH-DSA-SHAKE-256s6412829792
SLH-DSA-SHAKE-256f6412849856

Tutte le dimensioni sono in byte. Il segreto condiviso di ML-KEM è sempre di 32 byte indipendentemente dal parameter set (sgcMLKEM_SharedSecretSize).