En bref. ML-DSA est le schéma de signature fondé sur les réseaux euclidiens normalisé dans FIPS 204 (août 2024), anciennement CRYSTALS-Dilithium. Il comporte trois jeux de paramètres : ML-DSA-44, ML-DSA-65 et ML-DSA-87. Leurs signatures font 2420, 3309 et 4627 octets, contre 64 pour Ed25519 ou ECDSA P-256. Par défaut, la signature est hedged (avec aléa) ; un mode déterministe ne modifie que ce que produit le signataire. Une chaîne de contexte facultative de 255 octets au plus et une variante distincte avec pré-hachage (HashML-DSA) modifient ce que doit faire le vérificateur : les deux parties doivent donc s’accorder à leur sujet. OpenSSL 3.5, Go 1.27, Java 24+, .NET 10, BouncyCastle et @noble/post-quantum l’implémentent tous. C’est sur les clés privées qu’ils divergent le plus. Conservez la graine de 32 octets.
Pourquoi ML-DSA, et pourquoi maintenant
Les signatures ECDSA, EdDSA et RSA peuvent être falsifiées par quiconque dispose d’un ordinateur quantique suffisamment puissant. Contrairement au trafic chiffré, une signature ne peut pas être collectée aujourd’hui puis cassée plus tard d’une manière qui porte atteinte au passé. Une signature falsifiée pose problème au moment où quelqu’un s’y fie. C’est pourquoi la migration urgente était celle de l’échange de clés, et pourquoi les signatures disposent de plus de temps. Mais ce « plus de temps » a ses limites :
- Les racines de confiance à longue durée de vie, comme les clés de signature de micrologiciels, les AC racines et les clés de signature de code ou de documents, doivent rester dignes de confiance pendant dix ou vingt ans après leur création.
- Le projet de plan de transition du NIST, IR 8547, déconseillerait les signatures classiques de 112 bits après 2030 et interdirait toutes les signatures vulnérables au quantique après 2035. Les échéances et la façon de s’y préparer sont traitées dans Migration post-quantique en 2026.
ML-DSA est la réponse généraliste du NIST. Les autres schémas de la famille occupent des niches. FIPS 205 SLH-DSA est un schéma fondé sur le hachage, aux hypothèses prudentes, avec de petites clés et des signatures très volumineuses ou lentes à produire. Les schémas à état fondés sur le hachage LMS et XMSS (SP 800-208) servent pour les micrologiciels. FN-DSA, fondé sur Falcon, est prévu sous le nom de FIPS 206. En octobre 2026, le NIST n’en avait encore publié aucun projet sur csrc.nist.gov.
Les trois jeux de paramètres
| ML-DSA-44 | ML-DSA-65 | ML-DSA-87 | |
|---|---|---|---|
| Catégorie de sécurité NIST | 2 | 3 | 5 |
| Clé publique | 1312 octets | 1952 octets | 2592 octets |
| Signature | 2420 octets | 3309 octets | 4627 octets |
| Clé privée étendue | 2560 octets | 4032 octets | 4896 octets |
| Graine de la clé privée | 32 octets | 32 octets | 32 octets |
| Clé publique en SPKI (DER) | 1334 octets | 1974 octets | 2614 octets |
| OID (RFC 9881) | 2.16.840.1.101.3.4.3.17 | …3.18 | …3.19 |
Les tailles brutes proviennent du tableau 2 de FIPS 204. Les tailles SPKI sont celles qu’OpenSSL 3.5.7 a produites pour des clés fraîchement générées. À titre de comparaison, une clé publique Ed25519 fait 32 octets et sa signature 64. Une clé publique RSA-2048 fait 294 octets en SPKI, et sa signature 256.
Quel jeu choisir :
- ML-DSA-65 est le choix par défaut habituel. La JEP 497 en fait le jeu par défaut de Java lorsqu’aucun jeu de paramètres n’est indiqué, et la plupart des recommandations voient dans la catégorie 3 un juste milieu confortable.
- ML-DSA-87 est ce qu’exige la CNSA 2.0 de la NSA pour les systèmes de sécurité nationale des États-Unis (« paramètres de niveau V » pour tous les niveaux de classification).
- ML-DSA-44 est le plus petit. Il convient là où chaque octet compte et où les données ont une courte durée de vie. La documentation de Go va jusqu’à dire : « La plupart des applications devraient utiliser MLDSA44. »
En pratique, les octets comptent davantage que la vitesse. Sur la VM Linux arm64 utilisée pour cet article, une exécution de openssl speed de 2 secondes sur un seul cœur (OpenSSL 3.5.7) a donné ces débits :
| Algorithme | Signatures/s | Vérifications/s |
|---|---|---|
| ML-DSA-65 | 1488 | 8019 |
| RSA-2048 | 1825 | 74 119 |
| ECDSA P-256 | 65 465 | 24 446 |
ML-DSA-65 signe à peu près à la vitesse de RSA-2048 et vérifie un ordre de grandeur plus lentement que RSA, ce qui reste assez rapide pour la plupart des services. Le changement le plus important tient à la taille. Une chaîne de certificats, un JWT ou une entrée de journal signée grossit de plusieurs kilo-octets. Lors de notre test, un certificat feuille ML-DSA-65 signé par une AC ML-DSA-65 pesait 5514 octets en DER.
Hedged, déterministe, contexte : ce qui modifie une signature
Trois options de FIPS 204 déterminent quels octets sortent de Sign et ce qu’il faut indiquer à Verify.
Hedged ou déterministe. À l’intérieur de ML-DSA, l’opération de signature dérive un secret propre à chaque signature à partir de la clé privée, du message et d’une valeur de 32 octets, rnd. Par défaut (mode « hedged »), rnd est un aléa frais. Dans la variante déterministe, ce sont 32 octets nuls. Le §3.4 de FIPS 204 explique le compromis. La partie précalculée protège contre un générateur de nombres aléatoires défaillant, et la partie fraîche complique les attaques par canal auxiliaire et par injection de fautes. Le texte ajoute que la variante déterministe « ne devrait pas être utilisée sur des plateformes où les attaques par canal auxiliaire sont un sujet de préoccupation ». La vérification est identique dans les deux cas : le choix ne concerne donc que le signataire. OpenSSL montre directement la différence :
$ openssl pkeyutl -sign -rawin -inkey k65.pem -in msg.txt | sha256sum
f5c30f1347b231f64f7adf48a4dfeb77d1c259cfc568420ad1f2238335e28616 -
$ openssl pkeyutl -sign -rawin -inkey k65.pem -in msg.txt | sha256sum
c0eada83bdaa8e278defdd974cbd849fac583f7519a075ee53ec2a3d93adc6ba -
$ openssl pkeyutl -sign -rawin -inkey k65.pem -in msg.txt -pkeyopt deterministic:1 | sha256sum
c179822bec24d34a38c16257b41624cfb3a9ed40b7d7c79a5b5f2bdf0b87d4bf -
$ openssl pkeyutl -sign -rawin -inkey k65.pem -in msg.txt -pkeyopt deterministic:1 | sha256sum
c179822bec24d34a38c16257b41624cfb3a9ed40b7d7c79a5b5f2bdf0b87d4bf -
N’écrivez pas de tests qui comparent une signature fraîche à une signature enregistrée, sauf si vous signez de façon déterministe. Les signatures hedged diffèrent à chaque appel, par conception.
Chaîne de contexte. Sign et Verify prennent toutes deux une chaîne de contexte ctx de 255 octets au plus, vide par défaut. ML-DSA signe 0x00 ‖ len(ctx) ‖ ctx ‖ M, et non M seul : une signature produite sous un contexte ne se vérifie donc pas sous un autre. Le contexte est un outil peu coûteux de séparation de domaine. Deux protocoles qui partagent une clé ne peuvent pas être amenés à accepter les signatures l’un de l’autre si chacun utilise son propre contexte. Il fait aussi échouer la vérification de façon déroutante quand une partie le définit et que l’autre ne le fait pas :
$ openssl pkeyutl -sign -rawin -inkey k65.pem -in msg.txt -pkeyopt context-string:invoice-v1 -out sigctx.bin
$ openssl pkeyutl -verify -rawin -pubin -inkey p65.pem -in msg.txt -sigfile sigctx.bin -pkeyopt context-string:invoice-v1
Signature Verified Successfully
$ openssl pkeyutl -verify -rawin -pubin -inkey p65.pem -in msg.txt -sigfile sigctx.bin
...:error:030000EA:digital envelope routines:EVP_DigestVerify:provider signature failure:../crypto/evp/m_sigver.c:781:ML-DSA-65 digest_verify:
Signature Verification Failure
Les protocoles imposent le contexte : vous le choisissez donc rarement vous-même. ML-DSA dans X.509 (RFC 9881) et dans JOSE et COSE (RFC 9964, mai 2026) utilisent tous deux le contexte vide. La RFC 9964 dit : « Le paramètre ctx DOIT être la chaîne vide ».
HashML-DSA. Le §5.4 de FIPS 204 définit aussi une variante avec pré-hachage. Le message est d’abord haché avec SHA-512, SHA-256, SHAKE128 ou une autre fonction approuvée, et la signature couvre le condensat, ainsi que l’OID de la fonction de hachage et un octet de séparation de domaine différent (1 au lieu de 0). C’est un schéma de signature distinct, avec ses propres OID. Une signature HashML-DSA n’est pas une signature ML-DSA valide du même message. FIPS 204 indique que « la version “pure” de ML-DSA est à privilégier », et la RFC 9881 n’autorise pas HashML-DSA dans les certificats. La RFC 9964 ne le définit pas non plus pour JOSE ou COSE. Ne l’utilisez que lorsqu’un protocole l’exige.
Pour les très gros messages, il existe une troisième voie, le μ externe. Le signataire calcule le représentant du message μ, de 64 octets, en dehors du module de signature et ne transmet que cette valeur. La RFC 9881 le décrit. Go 1.27 (crypto.MLDSAMu) et .NET 10 (SignMu) l’exposent. Cette voie produit des signatures ML-DSA pures ordinaires : les vérificateurs n’ont besoin de rien de particulier.
Formats de clé privée : conservez la graine
Une paire de clés ML-DSA est générée à partir d’une graine ξ de 32 octets. La clé privée « étendue », de 2560 à 4896 octets, est calculée à partir de cette graine, tout comme la clé publique. La RFC 9881 définit trois encodages PKCS#8 :
seed: uniquement les 32 octets,[0] IMPLICIT OCTET STRING. C’est la forme recommandée. Elle fait 54 octets en PKCS#8.expandedKey: uniquement la clé étendue.both: une séquence contenant la graine et la clé étendue.
La RFC recommande que les implémentations « conservent et exportent la graine ». L’expansion est irréversible : une clé stockée sans sa graine ne pourra jamais être reconvertie vers la forme compacte. Les bibliothèques ont adopté des choix par défaut différents, ce qui casse les importations. OpenSSL 3.5 écrit both par défaut :
0:d=0 hl=4 l=4094 cons: SEQUENCE
4:d=1 hl=2 l= 1 prim: INTEGER :00
7:d=1 hl=2 l= 11 cons: SEQUENCE
9:d=2 hl=2 l= 9 prim: OBJECT :ML-DSA-65
20:d=1 hl=4 l=4074 prim: OCTET STRING [HEX DUMP]:30820FE60420A58A79B197C3...
Go 1.27 refuse cette forme et vous indique quoi faire. Il n’accepte que la graine :
both.pem (4098 bytes): x509: ML-DSA private keys with both seed and expanded key are not supported, use e.g. "openssl pkey -provparam ml-dsa.output_formats=seed-only" to convert to a seed-only key
seed-only.pem (54 bytes): *mldsa.PrivateKey
expanded-only.pem (4060 bytes): x509: semi-expanded ML-DSA private keys without seed are not supported
Lors de nos tests, Java 25 (JEP 497), .NET 10 et Go ont tous exporté des clés PKCS#8 de 54 octets réduites à la graine. Choisissez la forme graine dès la source :
openssl genpkey -algorithm ML-DSA-65 -provparam ml-dsa.output_formats=seed-only -out key.pem
openssl pkey -in both.pem -provparam ml-dsa.output_formats=seed-only -out key.pem
Le même choix vaut pour les HSM et les produits KMS. Avant de générer une clé à longue durée de vie, vérifiez que vous pouvez exporter ou sauvegarder la graine. Une clé uniquement étendue vous lie aux implémentations qui acceptent les clés étendues. @noble/post-quantum en est l’illustration : son secretKey est l’encodage étendu (4032 octets pour ML-DSA-65), et son keygen(seed) prend la graine de 32 octets. Conservez la graine vous-même.
Pour vérifier ce que contient réellement une clé ou un certificat, l’inspecteur de certificats post-quantiques décode les clés et les certificats ML-DSA et SLH-DSA, et l’explorateur ASN.1 montre laquelle des trois formes de clé privée vous avez.
Du code qui fonctionne aujourd’hui
Chaque extrait de code ci-dessous a été exécuté le 20 octobre 2026 dans le conteneur indiqué dans le paragraphe qui le précède. Chacun signe "hello" avec ML-DSA-65 et vérifie le résultat. Lorsque la bibliothèque le permet, une chaîne de contexte est définie. Pour essayer le schéma sans rien installer, le laboratoire de signatures post-quantiques produit et vérifie des signatures ML-DSA, SLH-DSA et Falcon dans le navigateur.
OpenSSL 3.5 (node:24.22.0-trixie, OpenSSL 3.5.7). ML-DSA nécessite -rawin, car sinon pkeyutl attendrait un condensat précalculé :
openssl genpkey -algorithm ML-DSA-65 -provparam ml-dsa.output_formats=seed-only -out key.pem
openssl pkey -in key.pem -pubout -out pub.pem
openssl pkeyutl -sign -rawin -inkey key.pem -in msg.txt -out sig.bin
openssl pkeyutl -verify -rawin -pubin -inkey pub.pem -in msg.txt -sigfile sig.bin
# Signature Verified Successfully
Go 1.27 (golang:1.27.3-trixie). Le nouveau paquet crypto/mldsa. crypto/x509 analyse et sérialise les clés, et crypto/tls accepte les certificats ML-DSA dans TLS 1.3. La documentation du paquet avertit qu’il est indisponible sous la version v1.0.0 du module cryptographique FIPS 140-3 de Go.
sk, err := mldsa.GenerateKey(mldsa.MLDSA65())
if err != nil {
log.Fatal(err)
}
opts := &mldsa.Options{Context: "invoice-v1"}
sig, err := sk.Sign(nil, []byte("hello"), opts) // 3309 bytes; the io.Reader is ignored
if err != nil {
log.Fatal(err)
}
fmt.Println(mldsa.Verify(sk.PublicKey(), []byte("hello"), sig, opts)) // <nil>
fmt.Println(mldsa.Verify(sk.PublicKey(), []byte("hello"), sig, nil)) // mldsa: invalid signature
sk.SignDeterministic fournit la variante déterministe. sk.Bytes() renvoie la graine de 32 octets.
Java, JDK 24 et versions ultérieures (eclipse-temurin:25.0.4.1_1-jdk-noble). La JEP 497 a ajouté ML-DSA aux fournisseurs standard. Les chaînes de contexte et HashML-DSA font explicitement partie des non-objectifs de cette JEP : le JDK ne signe donc qu’avec le contexte vide.
KeyPair kp = KeyPairGenerator.getInstance("ML-DSA-65").generateKeyPair();
Signature signer = Signature.getInstance("ML-DSA");
signer.initSign(kp.getPrivate());
signer.update("hello".getBytes(StandardCharsets.UTF_8));
byte[] sig = signer.sign(); // 3309 bytes
// kp.getPublic().getEncoded().length == 1974, kp.getPrivate().getEncoded().length == 54
BouncyCastle 1.86 (même image JDK). Il prend en charge le contexte via ContextParameterSpec, et HashML-DSA sous son propre nom d’algorithme. Notre test confirme qu’une signature HashML-DSA ne passe pas la vérification ML-DSA pure :
Signature signer = Signature.getInstance("ML-DSA", "BC");
signer.initSign(kp.getPrivate());
signer.setParameter(new ContextParameterSpec("invoice-v1".getBytes(StandardCharsets.UTF_8)));
signer.update(msg);
byte[] sig = signer.sign(); // verifies only with the same context
Signature pre = Signature.getInstance("HASH-ML-DSA", "BC"); // SHA-512 pre-hash
pre.initSign(kp.getPrivate());
pre.update(msg);
byte[] preSig = pre.sign(); // "ML-DSA".verify(preSig) == false
.NET 10 (mcr.microsoft.com/dotnet/sdk:10.0.402-alpine3.24). System.Security.Cryptography.MLDsa utilise la bibliothèque cryptographique de la plateforme, c’est-à-dire OpenSSL 3.5 ou plus récent sous Linux, et CNG avec prise en charge de la PQC sous Windows. Le même programme a affiché MLDsa.IsSupported: False dans l’image du SDK Ubuntu 24.04 (10.0.402-noble, OpenSSL 3.0.13) et True sous Alpine 3.24 (OpenSSL 3.5.9). Vérifiez IsSupported et votre image de base avant la mise en production. Dans le SDK 10.0.402, la génération de clés, la signature et la vérification se compilent sans problème, mais les méthodes d’exportation de clés font encore échouer le build avec error SYSLIB5006: ... is for evaluation purposes only, à moins de désactiver ce diagnostic.
using MLDsa key = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);
byte[] data = Encoding.UTF8.GetBytes("hello");
byte[] context = Encoding.UTF8.GetBytes("invoice-v1");
byte[] signature = key.SignData(data, context); // 3309 bytes
Console.WriteLine(key.VerifyData(data, signature, context)); // True
Console.WriteLine(key.VerifyData(data, signature)); // False
#pragma warning disable SYSLIB5006 // the key export methods are still [Experimental] in .NET 10
byte[] pkcs8 = key.ExportPkcs8PrivateKey(); // 54 bytes: the seed form
JavaScript et TypeScript. La prise en charge native arrive. Node.js 24.22 crée des clés avec crypto.generateKeyPairSync("ml-dsa-65") et signe avec crypto.sign(null, data, privateKey) grâce à l’OpenSSL 3.5 qu’il embarque, et son WebCrypto accepte { name: "ML-DSA-65" } en émettant un ExperimentalWarning. Chromium 156 (le build fourni avec Playwright 1.64.1) a généré une clé et une signature de 3309 octets via crypto.subtle sur une page HTTPS. Vérifiez les autres moteurs de navigateur avant d’en dépendre. Pour du code qui doit fonctionner partout, @noble/post-quantum 0.7.0 est une implémentation en JavaScript pur qui fonctionne dans Node.js et dans n’importe quel navigateur (exécutée dans node:24.22.0-trixie) :
import { ml_dsa65 } from "@noble/post-quantum/ml-dsa.js";
import { randomBytes } from "node:crypto";
const { secretKey, publicKey } = ml_dsa65.keygen(randomBytes(32)); // keep the 32-byte seed, not secretKey
const msg = new TextEncoder().encode("hello");
const context = new TextEncoder().encode("invoice-v1");
const sig = ml_dsa65.sign(msg, secretKey, { context });
ml_dsa65.verify(sig, msg, publicKey, { context }); // true
ml_dsa65.verify(sig, msg, publicKey); // false
ml_dsa65.sign(msg, secretKey, { extraEntropy: false }); // deterministic variant
Certificats ML-DSA et TLS
OpenSSL 3.5 peut dès aujourd’hui émettre des certificats ML-DSA et les utiliser dans TLS 1.3. Ce test crée une AC racine et un certificat feuille ML-DSA-65, puis se connecte avec s_client. Un client qui ne propose que des algorithmes de signature classiques est rejeté :
leaf certificate: 5514 bytes DER
Protocol version: TLSv1.3
Signature type: mldsa65
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
--- a classic-only client
error:0A000410:SSL routines:ssl3_read_bytes:ssl/tls alert handshake failure
Les identifiants TLS de ces schémas (code points mldsa44 0x0904, mldsa65 0x0905, mldsa87 0x0906) proviennent de draft-ietf-tls-mldsa. En octobre 2026, ce document se trouvait dans la file d’attente du RFC Editor. Go 1.27 prend en charge les trois mêmes schémas.
Pour le Web public, ce n’est pas encore utilisable. Les navigateurs n’acceptent pas les certificats ML-DSA émis par des AC publiques. Firefox les garde derrière la préférence security.tls.enable_mldsa, qui vaut false par défaut. Chrome a déclaré en février 2026 n’avoir aucun projet immédiat d’ajouter à son magasin de certificats racines des certificats X.509 à clés post-quantiques. Il travaille plutôt sur les Merkle Tree Certificates, qui évitent d’envoyer plusieurs signatures de l’ordre du kilo-octet à chaque handshake. Les certificats ML-DSA sont réalistes dès aujourd’hui dans une PKI privée : maillages de services (service mesh), identité des appareils, mTLS interne, signature de code et de documents, partout où vous contrôlez les deux extrémités.
Erreurs fréquentes
- S’attendre à une signature stable. La signature hedged est le mode par défaut de toutes les bibliothèques ci-dessus (nous l’avons vérifié pour OpenSSL, Java et noble en signant deux fois) : la même entrée donne donc des octets différents à chaque fois. Comparez les résultats de la vérification, pas les octets des signatures.
- Confondre ML-DSA pur et HashML-DSA. Ce sont des algorithmes différents, avec des OID différents. Un vérificateur configuré pour l’un rejette l’autre, et FIPS 204 recommande de n’utiliser une paire de clés que pour l’un des deux.
- Définir un contexte d’un seul côté. L’échec ressemble en tout point à celui d’une signature invalide. Journalisez le contexte qu’utilise votre protocole à côté du nom de l’algorithme.
- Jeter la graine. N’exporter que la clé étendue fonctionne jusqu’au jour où vous devez la charger dans Go ou dans une autre implémentation qui n’accepte que la graine. Convertissez avec
-provparam ml-dsa.output_formats=seed-onlytant que c’est encore possible. - Supposer que l’environnement d’exécution le prend en charge. .NET et les autres surcouches de la bibliothèque système héritent de sa version. Une application qui fonctionne sur le portable d’un développeur avec OpenSSL 3.5 peut échouer dans un conteneur construit sur une image de base plus ancienne.
- Oublier les limites de taille. En-têtes JWT, cookies, enregistrements DNS, extensions X.509, colonnes de base de données, codes QR, tampons des HSM : tout ce qui est dimensionné pour une signature ECDSA de 64 octets doit pouvoir contenir 3309 octets ou plus.
Sources
- FIPS 204, Module-Lattice-Based Digital Signature Standard, la norme de signature numérique fondée sur les réseaux modulaires (août 2024) : jeux de paramètres (tableau 1), tailles (tableau 2), signature hedged et déterministe (§3.4), contexte et HashML-DSA (§5.2–5.4).
- RFC 9881 : identifiants d’algorithmes ML-DSA pour X.509, formats de clé privée, μ externe.
- RFC 9964 : ML-DSA pour JOSE et COSE (
ML-DSA-44/65/87, type de cléAKP). - draft-ietf-tls-mldsa : ML-DSA dans TLS 1.3.
- NIST IR 8547 (projet public initial) : calendrier de transition.
- Documentation des bibliothèques : NEWS d’OpenSSL 3.5, notes de version de Go 1.27, JEP 497, Nouveautés des bibliothèques de .NET 10.
Les sorties de commandes ont été produites le 20 octobre 2026 dans node:24.22.0-trixie (OpenSSL 3.5.7, Node.js 24.22.0), golang:1.27.3-trixie, eclipse-temurin:25.0.4.1_1-jdk-noble (avec bcprov-jdk18on 1.86), mcr.microsoft.com/dotnet/sdk:10.0.402-alpine3.24, mcr.microsoft.com/dotnet/sdk:10.0.402-noble et mcr.microsoft.com/playwright:v1.64.1-noble (Chromium 156).