Cryptographie post-quantique

Un ordinateur quantique suffisamment puissant briserait RSA, X25519/X448 et ECDSA/ECDH sur toutes les courbes de la page Cryptographie à clé publique : aucune de ces mathématiques ne résiste à l'algorithme de Shor. Il n'a pas besoin d'exister aujourd'hui pour poser un problème dès aujourd'hui, un adversaire peut enregistrer le trafic chiffré maintenant et le déchiffrer une fois qu'une telle machine existera, une stratégie habituellement appelée « récolter maintenant, déchiffrer plus tard ». Tout ce qui doit rester confidentiel pendant des années, et non des jours, devrait déjà migrer vers un échange de clés post-quantique.

Le NIST a normalisé trois schémas, tous implémentés ici, et tous reposant uniquement sur SHAKE (voir Hachage & authentification des messages), de sorte que leur sécurité repose uniquement sur la solidité du hachage : aucune hypothèse de réseau euclidien ou de théorie des nombres à briser.

Schéma Objectif Norme Unité
ML-KEM Encapsulation de clé, remplace Diffie-Hellman FIPS 203 sgcCrypto_MLKEM
ML-DSA Signatures, basé sur les réseaux euclidiens, le choix par défaut FIPS 204 sgcCrypto_MLDSA
SLH-DSA Signatures, basé uniquement sur le hachage, repli conservateur FIPS 205 sgcCrypto_SLHDSA

ML-KEM et la construction hybride (recommandé)

ML-KEM remplace un échange Diffie-Hellman : l'expéditeur encapsule un secret partagé avec la clé publique du destinataire, le destinataire le décapsule avec la clé privée, et les deux obtiennent les mêmes 32 octets. Il existe en trois ensembles de paramètres, une taille plus grande signifiant une marge de sécurité plus grande et des clés plus grandes.


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 décapsulation FIPS 203 n'échoue jamais.

Un texte chiffré corrompu ou malveillant ne déclenche pas d'erreur, il renvoie silencieusement un secret différent, dérivé d'une valeur de rejet implicite intégrée dans la clé privée. Renvoyer une erreur offrirait à un attaquant un oracle de déchiffrement pour sonder la clé privée ; la norme évite cela par conception. Quoi que le secret dérivé protège, il doit détecter lui-même l'incohérence, par exemple un tag AEAD qui échoue à se vérifier en aval.

Ce qu'il faut réellement déployer aujourd'hui, c'est la construction hybride : X25519 combiné à ML-KEM-768, de sorte que le secret partagé dépende des deux. Elle reste sûre si ML-KEM venait à être un jour brisé par une avancée cryptanalytique, et elle reste sûre si un ordinateur quantique brise X25519 ; un attaquant a besoin que les deux échouent à la fois.


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 et les groupes hybrides TLS 1.3

sgcCrypto_MLKEM_Hybrid contient trois familles hybrides. Les fonctions sgcHybrid_ ci-dessus constituent le format sgcCrypto d'origine et sont conservées par compatibilité. X-Wing est le KEM hybride généraliste normalisé, X25519 combiné à ML-KEM-768, et c'est celui qu'il faut utiliser dans tout nouveau code en dehors de TLS. La troisième famille construit les partages de clé hybrides TLS 1.3, et rien d'autre.

X-Wing (draft-connolly-cfrg-xwing-kem-10) ne conserve qu'un seul secret : la clé privée est une graine de 32 octets, et chaque opération la développe pour obtenir le matériel ML-KEM-768 et X25519 dont elle a besoin. La clé publique fait 1216 octets, le texte chiffré 1120 octets et le secret partagé 32 octets. L'encapsulation exécute le contrôle FIPS 203 de la clé d'encapsulation et lève une exception lorsque la clé publique ne le passe pas.


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;

Les partages de clé hybrides TLS 1.3 de la RFC 10024 sont X25519MLKEM768, SecP256r1MLKEM768 et SecP384r1MLKEM1024, codepoints IANA 0x11EC, 0x11EB et 0x11ED. Ce ne sont pas des KEM utilisables seuls. Le secret partagé est une simple concaténation des deux moitiés, ce qui n'est sûr que parce que TLS l'injecte dans son calendrier de clés avec la transcription du handshake ; servez-vous en donc pour construire un partage de clé TLS, et rien d'autre. Le moteur TLS natif propose X25519MLKEM768 en premier par défaut, de sorte qu'un client ou un serveur qui l'utilise négocie déjà un échange de clés post-quantique sans aucun code supplémentaire.

Les groupes hybrides n'existent qu'en TLS 1.3. Le moteur natif parle aussi TLS 1.2, et une connexion qui bascule vers cette version parce que l'interlocuteur ne peut pas faire TLS 1.3 utilise un groupe classique, X25519, secp256r1 ou secp384r1, sans protection post-quantique. Définissez TLSOptions.Version sur un client ou SSLOptions.Version sur un serveur à tls1_3, et ne listez que des groupes hybrides dans NativeTLS_Options.Groups, quand l'échange de clés doit toujours être post-quantique.

La démo Post-Quantum TLS, Demos\18.Cryptography\08.PostQuantum_TLS, dispose d'un sélecteur de version TLS avec les choix TLS 1.3 et TLS 1.2, TLS 1.3 uniquement et TLS 1.2 uniquement, ainsi que d'un sélecteur de suite de chiffrement, et sa ligne d'état affiche la version, le groupe et la suite de chiffrement négociés. La démo serveur peut signer avec un certificat ML-DSA-44, qui ne sert que les clients TLS 1.3, car TLS 1.2 n'a aucun schéma de signature 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;

Le client garde aPrivateState secret, le transmet tel quel à sgcTLSHybrid_ClientSharedSecret puis l'efface ; sa structure ne fait pas partie de l'API. Les deux fonctions booléennes renvoient False, avec toutes les sorties vides, pour toute entrée invalide du pair : un partage de clé de longueur incorrecte, une clé d'encapsulation ML-KEM qui échoue au contrôle FIPS 203, un point qui n'est pas sur la courbe, ou un résultat X25519 entièrement nul. La RFC 10024 impose d'interrompre la connexion avec une alerte illegal_parameter dans chacun de ces cas.

Tout ce qui repose sur X25519, c'est-à-dire les fonctions sgcHybrid_, X-Wing et le groupe X25519MLKEM768, est retiré d'une compilation avec SGC_CRYPTO_FIPS, voir Mode FIPS. Les deux groupes SecP restent, car P-256, P-384 et ML-KEM sont tous des algorithmes approuvés. Appelez sgcTLSHybrid_Available avant de proposer ou d'accepter un groupe, plutôt que de supposer qu'il est présent.

ML-DSA (recommandé pour les signatures)

La signature tire un vecteur de masquage et tente de construire une signature, en recommençant lorsque la tentative divulguerait des informations sur la clé ; le nombre d'essais attendu est faible mais non fixe, de sorte que la signature prend un temps variable. Cela est inhérent au schéma et ne constitue pas un canal auxiliaire sur la clé. Les signatures sont volumineuses : 2420 octets pour ML-DSA-44 contre 64 pour Ed25519, et des clés publiques de 1312 octets contre 32, prévoyez donc cette différence avant de l'adopter dans un protocole sensible à la taille.


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 (repli conservateur)

SLH-DSA ne repose que sur la fonction de hachage : aucun problème de réseau euclidien à briser, jamais, à aucun moment dans le futur. Cette garantie a un coût. Une signature va de 7856 octets (la plus petite, SLH-DSA-SHAKE-128s) à 49856 octets (la plus grande, SLH-DSA-SHAKE-256f), et signer avec un ensemble de paramètres s (petit) parcourt des centaines de milliers d'opérations de hachage et peut prendre des dizaines de secondes ; les ensembles f (rapide) signent en environ une à sept secondes pour une taille de signature environ deux fois plus grande. La vérification est rapide pour chaque ensemble de paramètres, ce qui convient mieux à un cas d'usage de signature rare et de vérification fréquente, comme le firmware ou la signature de code, qu'à un protocole interactif.


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;

Formats de clé : SubjectPublicKeyInfo, PKCS#8 et PEM

Les chaînes d'octets brutes que consomment les algorithmes FIPS ne constituent pas un format de stockage, et aucune autre implémentation ne les lira. Pour écrire une clé sur disque, la placer dans un certificat ou la transmettre à une autre bibliothèque, exportez-la dans les conteneurs standard : un SubjectPublicKeyInfo X.509 pour une clé publique, un OneAsymmetricKey PKCS#8 pour une clé privée, et une enveloppe PEM (RFC 7468) autour de l'un ou l'autre, étiquetée PUBLIC KEY et PRIVATE KEY. Les trois schémas exposent les mêmes huit fonctions, chacune suivant la RFC qui définit son encodage.

Schéma RFC du conteneur Fonctions DER Fonctions 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'identifiant d'algorithme porte l'identifiant d'objet NIST avec les paramètres absents, de sorte que l'ensemble de paramètres voyage avec la clé et que l'import le renvoie. sgcMLDSA_OID, sgcMLKEM_OID et sgcSLHDSA_OID donnent la chaîne pointée, et les fonctions ParamsFromOID correspondantes font la conversion inverse.

Trois formes de clé privée pour ML-DSA et ML-KEM.

Les fonctions d'export prennent un paramètre aFormat de type TsgcPQCPrivateKeyFormat, déclaré dans sgcCrypto_DER. pqkfSeed, la valeur par défaut, écrit la graine seule, 32 octets pour ML-DSA et les 64 octets d || z pour ML-KEM ; c'est la forme que les deux RFC recommandent et c'est de loin la plus compacte. pqkfExpanded écrit la clé privée FIPS développée. pqkfBoth écrit les deux et lève une exception lorsque la clé développée n'est pas le développement de la graine, ce que les RFC appellent une clé malformée.

Une graine ne peut plus être retrouvée à partir d'une clé développée, demandez-la donc au moment de la génération avec sgcMLDSA_GenerateKeyPairAndSeed ou sgcMLKEM_GenerateKeyPairAndSeed lorsque vous comptez stocker la forme recommandée. SLH-DSA n'a qu'une seule forme de clé privée, SK.seed || SK.prf || PK.seed || PK.root, son export ne prend donc pas de paramètre de format.


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'import valide, il ne fait pas confiance.

Chaque fonction d'import renvoie False au lieu de lever une exception, et elle renvoie False sur un DER malformé, sur des données excédentaires, sur un identifiant d'objet inconnu, sur des paramètres présents alors qu'ils doivent être absents, et sur une clé dont la longueur ne correspond pas à l'ensemble de paramètres. Au-delà de la structure, une clé publique ML-KEM doit passer le contrôle de module FIPS 203, une clé privée ML-KEM développée doit passer le contrôle de hachage et le test de cohérence par paire, et une clé privée ML-DSA développée est reconstruite en sa clé publique, de sorte que les valeurs t0 et tr stockées doivent toutes deux concorder avec elle. Une clé publique incluse qui ne correspond pas à la clé privée est refusée. Lorsqu'une clé privée contient une graine, les trois sorties reviennent renseignées, un appelant n'a donc jamais à la développer séparément.

sgcSLHDSA_ImportPrivateKeyInfo et sgcSLHDSA_ImportPrivateKeyPEM prennent un paramètre supplémentaire aVerify, True par défaut, qui recalcule PK.root à partir de la graine privée et refuse une clé dont la racine stockée diffère. Ce recalcul coûte autant qu'une génération de clé, soit plusieurs secondes pour les ensembles de paramètres s, ne passez donc False que pour des clés que votre propre application a générées ou auxquelles elle fait déjà confiance. L'identifiant d'objet, les longueurs et la structure restent vérifiés lorsqu'il vaut False.

Ce sont ces conteneurs que lit le reste de la bibliothèque. Une clé privée ML-DSA PKCS#8 est ce avec quoi le client JWT signe, un SubjectPublicKeyInfo est ce avec quoi le serveur JWT valide, et les deux sont ce que Certificats & PKI place dans un certificat ou une requête de signature de certificat.

Choisir un ensemble de paramètres

Chaque appel de taille (sgcMLKEM_PublicKeySize, sgcMLDSA_SignatureSize, sgcSLHDSA_SignatureSize et leurs équivalents PrivateKeySize) prend la même énumération de paramètres que l'opération elle-même, de sorte qu'un protocole peut dimensionner ses tampons avant toute opération cryptographique :

Ensemble de paramètres Clé publique Clé privée Texte chiffré / Signature
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

Toutes les tailles sont en octets. Le secret partagé ML-KEM fait toujours 32 octets quel que soit l'ensemble de paramètres (sgcMLKEM_SharedSecretSize).