Resposta curta. O ML-DSA é o esquema de assinatura baseado em reticulados padronizado no FIPS 204 (agosto de 2024), antes chamado CRYSTALS-Dilithium. Ele tem três conjuntos de parâmetros: ML-DSA-44, ML-DSA-65 e ML-DSA-87. As assinaturas deles têm 2.420, 3.309 e 4.627 bytes, contra 64 no Ed25519 ou no ECDSA P-256. Por padrão, a assinatura é hedged (aleatorizada); um modo determinístico muda apenas o que o signatário produz. Uma string de contexto opcional de até 255 bytes e uma variante separada com pré-hash (HashML-DSA) mudam o que o verificador precisa fazer, então os dois lados precisam concordar sobre essas opções. O OpenSSL 3.5, o Go 1.27, o Java 24+, o .NET 10, o BouncyCastle e o @noble/post-quantum implementam o esquema. É nas chaves privadas que eles mais divergem. Guarde a semente de 32 bytes.
Por que o ML-DSA, e por que agora
Assinaturas ECDSA, EdDSA e RSA podem ser falsificadas por qualquer pessoa que tenha um computador quântico grande o bastante. Ao contrário do tráfego criptografado, uma assinatura não pode ser coletada agora e quebrada depois de um jeito que prejudique o passado. Uma assinatura falsificada importa no momento em que alguém confia nela. Por isso a migração urgente foi a da troca de chaves, e por isso as assinaturas têm mais tempo. Mas esse “mais tempo” tem limites:
- Raízes de confiança de longa duração, como chaves de assinatura de firmware, raízes de CA e chaves de assinatura de código e de documentos, precisam continuar confiáveis por dez ou vinte anos depois de criadas.
- O rascunho do plano de transição do NIST, o IR 8547, descontinuaria as assinaturas clássicas de 112 bits após 2030 e deixaria de permitir todas as assinaturas vulneráveis à computação quântica após 2035. Os prazos e como se planejar para eles estão em Migração pós-quântica em 2026.
O ML-DSA é a resposta de uso geral do NIST. Os esquemas irmãos atendem a nichos. O SLH-DSA, do FIPS 205, é um esquema baseado em hash, com premissas conservadoras, chaves pequenas e assinaturas muito grandes ou lentas. Os esquemas baseados em hash com estado LMS e XMSS (SP 800-208) são usados em firmware. O FN-DSA, baseado no Falcon, está previsto como FIPS 206. Até outubro de 2026, o NIST não tinha publicado um rascunho dele em csrc.nist.gov.
Os três conjuntos de parâmetros
| ML-DSA-44 | ML-DSA-65 | ML-DSA-87 | |
|---|---|---|---|
| Categoria de segurança do NIST | 2 | 3 | 5 |
| Chave pública | 1.312 B | 1.952 B | 2.592 B |
| Assinatura | 2.420 B | 3.309 B | 4.627 B |
| Chave privada expandida | 2.560 B | 4.032 B | 4.896 B |
| Semente da chave privada | 32 B | 32 B | 32 B |
| Chave pública em SPKI (DER) | 1.334 B | 1.974 B | 2.614 B |
| OID (RFC 9881) | 2.16.840.1.101.3.4.3.17 | …3.18 | …3.19 |
Os tamanhos brutos vêm da tabela 2 do FIPS 204. Os tamanhos em SPKI são os que o OpenSSL 3.5.7 gravou para chaves recém-geradas. Para comparação, uma chave pública Ed25519 tem 32 bytes, e a assinatura dela, 64. Uma chave pública RSA-2048 tem 294 bytes em SPKI, e a assinatura dela, 256.
Qual conjunto escolher:
- O ML-DSA-65 é o padrão habitual. A JEP 497 o torna o padrão do Java quando nenhum conjunto de parâmetros é informado, e a maioria das recomendações trata a categoria 3 como o meio-termo confortável.
- O ML-DSA-87 é o que a CNSA 2.0 da NSA exige para os sistemas de segurança nacional dos EUA (“parâmetros de nível V” para todos os níveis de classificação).
- O ML-DSA-44 é o menor. Ele serve onde cada byte conta e os dados têm vida curta. A documentação do Go chega a dizer que “a maioria das aplicações deveria usar MLDSA44”.
Na prática, os bytes importam mais do que a velocidade. Na VM Linux arm64 usada neste artigo, uma execução de 2 segundos do openssl speed em um único núcleo (OpenSSL 3.5.7) deu estas taxas:
| Algoritmo | Assinaturas/s | Verificações/s |
|---|---|---|
| ML-DSA-65 | 1.488 | 8.019 |
| RSA-2048 | 1.825 | 74.119 |
| ECDSA P-256 | 65.465 | 24.446 |
O ML-DSA-65 assina mais ou menos na velocidade do RSA-2048 e verifica uma ordem de grandeza mais devagar que o RSA, o que é rápido o bastante para a maioria dos serviços. A mudança maior é o tamanho. Uma cadeia de certificados, um JWT ou uma entrada de log assinada cresce em alguns kilobytes. No nosso teste, um certificado folha ML-DSA-65 assinado por uma CA ML-DSA-65 ficou com 5.514 bytes em DER.
Hedged, determinística, contexto: o que muda uma assinatura
Três opções do FIPS 204 decidem quais bytes saem de Sign e o que precisa ser informado a Verify.
Hedged ou determinística. Internamente, ao assinar, o ML-DSA deriva um segredo próprio de cada assinatura a partir da chave privada, da mensagem e de um valor de 32 bytes, rnd. Por padrão (modo “hedged”), rnd é aleatoriedade nova. Na variante determinística, são 32 bytes zerados. O FIPS 204 §3.4 explica os prós e contras: a parte pré-calculada protege contra um gerador de números aleatórios defeituoso, e a parte nova dificulta ataques de canal lateral e de injeção de falhas. E acrescenta que a variante determinística “não deveria ser usada em plataformas em que ataques de canal lateral são uma preocupação”. A verificação é idêntica nos dois casos, então a escolha só diz respeito ao signatário. O OpenSSL mostra a diferença diretamente:
$ 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ão escreva testes que comparem uma assinatura nova com uma armazenada, a menos que você assine de forma determinística. As assinaturas hedged mudam a cada chamada, e isso é intencional.
String de contexto. Tanto Sign quanto Verify recebem uma string de contexto ctx de no máximo 255 bytes, vazia por padrão. O ML-DSA assina 0x00 ‖ len(ctx) ‖ ctx ‖ M, e não apenas M, então uma assinatura feita sob um contexto não passa na verificação sob outro. O contexto é uma ferramenta barata de separação de domínio: dois protocolos que compartilham uma chave não podem ser induzidos a aceitar as assinaturas um do outro se cada um usar o próprio contexto. Ele também quebra a verificação de formas confusas quando um lado o define e o outro não:
$ 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
Os protocolos fixam o contexto, então raramente é você quem o escolhe. O ML-DSA no X.509 (RFC 9881) e no JOSE e no COSE (RFC 9964, maio de 2026) usa o contexto vazio nos dois casos. A RFC 9964 diz que “o parâmetro ctx DEVE ser a string vazia”.
HashML-DSA. O FIPS 204 §5.4 também define uma variante com pré-hash. Primeiro se calcula o hash da mensagem com SHA-512, SHA-256, SHAKE128 ou outra função aprovada, e a assinatura cobre o digest junto com o OID da função de hash e um byte de separação de domínio diferente (1 em vez de 0). É um esquema de assinatura à parte, com OIDs próprios. Uma assinatura HashML-DSA não é uma assinatura ML-DSA válida sobre a mesma mensagem. O FIPS 204 diz que “a versão ‘pura’ do ML-DSA é a preferida”, e a RFC 9881 não permite o HashML-DSA em certificados. A RFC 9964 também não o define para JOSE nem para COSE. Use-o apenas quando um protocolo o exigir.
Para mensagens muito grandes, existe um terceiro caminho, o μ externo (external μ). O signatário calcula o representante da mensagem μ, de 64 bytes, fora do módulo de assinatura e passa só esse valor ao módulo. A RFC 9881 descreve esse caminho. O Go 1.27 (crypto.MLDSAMu) e o .NET 10 (SignMu) o expõem. O resultado são assinaturas ML-DSA puras comuns, então os verificadores não precisam de nada especial.
Formatos de chave privada: guarde a semente
Um par de chaves ML-DSA é gerado a partir de uma semente ξ de 32 bytes. A chave privada “expandida”, de 2.560 a 4.896 bytes, é calculada a partir dessa semente, assim como a chave pública. A RFC 9881 define três codificações PKCS#8:
seed: só os 32 bytes,[0] IMPLICIT OCTET STRING. É a forma recomendada. Em PKCS#8, ocupa 54 bytes.expandedKey: só a chave expandida.both: uma sequência com a semente e a chave expandida.
A RFC recomenda que as implementações “mantenham e exportem a semente”. A expansão não pode ser revertida, então uma chave armazenada sem a semente nunca pode voltar à forma compacta. As bibliotecas adotaram padrões diferentes, e isso quebra importações. O OpenSSL 3.5 grava both por padrão:
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...
O Go 1.27 recusa essa forma e diz o que fazer. Ele aceita apenas a semente:
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
Nos nossos testes, o Java 25 (JEP 497), o .NET 10 e o Go exportaram chaves PKCS#8 de 54 bytes só com a semente. Escolha a forma com semente já na origem:
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
A mesma escolha vale para HSMs e produtos de KMS. Antes de gerar uma chave de longa duração, confirme que é possível exportar a semente ou fazer backup dela. Uma chave só expandida prende você a implementações que aceitam chaves expandidas. O @noble/post-quantum é um exemplo: o secretKey dele é a codificação expandida (4.032 bytes no ML-DSA-65), e o keygen(seed) dele recebe a semente de 32 bytes. Guarde você mesmo a semente.
Para conferir o que uma chave ou um certificado contém de fato, o inspetor de certificados pós-quânticos decodifica chaves e certificados ML-DSA e SLH-DSA, e o explorador de ASN.1 mostra qual das três formas de chave privada você tem.
Código que funciona hoje
Todos os trechos de código abaixo foram executados em 20 de outubro de 2026 no contêiner indicado no parágrafo que antecede cada um. Cada um assina "hello" com ML-DSA-65 e verifica o resultado. Quando a biblioteca oferece suporte, uma string de contexto é definida. Para testar o esquema sem instalar nada, o laboratório de assinaturas pós-quânticas assina e verifica ML-DSA, SLH-DSA e Falcon no navegador.
OpenSSL 3.5 (node:24.22.0-trixie, OpenSSL 3.5.7). O ML-DSA precisa de -rawin, porque, sem essa opção, o pkeyutl esperaria um digest pré-calculado:
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). O novo pacote crypto/mldsa. O crypto/x509 analisa e serializa as chaves, e o crypto/tls aceita certificados ML-DSA no TLS 1.3. A documentação do pacote avisa que ele não está disponível no FIPS 140-3 Go Cryptographic Module v1.0.0.
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 gera a variante determinística. sk.Bytes() retorna a semente de 32 bytes.
Java, JDK 24 e posteriores (eclipse-temurin:25.0.4.1_1-jdk-noble). A JEP 497 adicionou o ML-DSA aos provedores padrão. Strings de contexto e HashML-DSA ficam explicitamente fora dos objetivos dessa JEP, então o JDK assina apenas com o contexto vazio.
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 (mesma imagem do JDK). Ele oferece suporte ao contexto por meio de ContextParameterSpec e ao HashML-DSA com um nome de algoritmo próprio. Nosso teste confirma que uma assinatura HashML-DSA não passa na verificação do ML-DSA puro:
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). O System.Security.Cryptography.MLDsa usa a biblioteca criptográfica da plataforma: no Linux, o OpenSSL 3.5 ou mais recente; no Windows, o CNG com suporte a PQC. O mesmo programa informou MLDsa.IsSupported: False na imagem do SDK com Ubuntu 24.04 (10.0.402-noble, OpenSSL 3.0.13) e True no Alpine 3.24 (OpenSSL 3.5.9). Confira IsSupported e sua imagem base antes de colocar em produção. No SDK 10.0.402, a geração de chaves, a assinatura e a verificação compilam sem problemas, mas os métodos de exportação de chaves ainda fazem o build falhar com error SYSLIB5006: ... is for evaluation purposes only, a menos que você suprima o diagnóstico.
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 e TypeScript. O suporte nativo está chegando. O Node.js 24.22 cria chaves com crypto.generateKeyPairSync("ml-dsa-65") e assina com crypto.sign(null, data, privateKey) por meio do OpenSSL 3.5 que vem embutido nele, e o WebCrypto dele aceita { name: "ML-DSA-65" } com um ExperimentalWarning. O Chromium 156 (o build do Playwright 1.64.1) gerou uma chave e uma assinatura de 3.309 bytes por meio de crypto.subtle em uma página HTTPS. Confira os outros motores de navegador antes de depender disso. Para código que precisa rodar em qualquer lugar, o @noble/post-quantum 0.7.0 é uma implementação em JS puro que funciona no Node.js e em qualquer navegador (executado em 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
Certificados ML-DSA e TLS
O OpenSSL 3.5 já consegue emitir certificados ML-DSA e usá-los no TLS 1.3. Este teste cria uma raiz e um certificado folha ML-DSA-65 e se conecta com o s_client. Um cliente que oferece apenas algoritmos de assinatura clássicos é rejeitado:
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
Os identificadores (code points) do TLS (mldsa44 0x0904, mldsa65 0x0905, mldsa87 0x0906) vêm do draft-ietf-tls-mldsa. Em outubro de 2026, ele estava na fila do RFC Editor. O Go 1.27 oferece suporte aos mesmos três esquemas.
Na web pública, isso ainda não é utilizável. Os navegadores não aceitam certificados ML-DSA de CAs públicas. O Firefox os mantém atrás da preferência security.tls.enable_mldsa, que vem como false por padrão. O Chrome declarou em fevereiro de 2026 que não tem planos imediatos de adicionar certificados X.509 com chaves pós-quânticas ao root store dele. Em vez disso, está trabalhando nos Merkle Tree Certificates, que evitam enviar várias assinaturas de alguns kilobytes cada em todo handshake. Hoje, os certificados ML-DSA são realistas em PKI privada: service meshes, identidade de dispositivos, mTLS interno, assinatura de código e de documentos em que você controla as duas pontas.
Erros comuns
- Esperar uma assinatura estável. A assinatura hedged é o padrão em todas as bibliotecas acima (conferimos no OpenSSL, no Java e no noble assinando duas vezes), então a mesma entrada gera bytes diferentes a cada vez. Compare resultados de verificação, não bytes de assinatura.
- Confundir o ML-DSA puro com o HashML-DSA. São algoritmos diferentes, com OIDs diferentes. Um verificador configurado para um rejeita o outro, e o FIPS 204 recomenda usar cada par de chaves para apenas um deles.
- Definir um contexto em apenas um dos lados. A falha parece exatamente uma assinatura inválida. Registre no log o contexto que seu protocolo usa ao lado do nome do algoritmo.
- Descartar a semente. Exportar só a chave expandida funciona até você precisar carregá-la no Go ou em outra implementação que só aceita a semente. Converta com
-provparam ml-dsa.output_formats=seed-onlyenquanto ainda é possível. - Supor que o runtime oferece suporte. O .NET e outras camadas sobre a biblioteca do sistema herdam a versão dela. Uma aplicação que funciona no notebook de um desenvolvedor com OpenSSL 3.5 pode falhar em um contêiner criado sobre uma imagem base mais antiga.
- Esquecer os limites de tamanho. Cabeçalhos de JWT, cookies, registros DNS, extensões X.509, colunas de banco de dados, códigos QR, buffers de HSM: tudo o que foi dimensionado para uma assinatura ECDSA de 64 bytes precisa comportar 3.309 bytes ou mais.
Fontes
- FIPS 204, Module-Lattice-Based Digital Signature Standard, o padrão de assinatura digital baseado em reticulados de módulo (agosto de 2024): conjuntos de parâmetros (tabela 1), tamanhos (tabela 2), assinatura hedged e determinística (§3.4), contexto e HashML-DSA (§5.2–5.4).
- RFC 9881: identificadores de algoritmo do ML-DSA para X.509, formatos de chave privada, μ externo.
- RFC 9964: ML-DSA para JOSE e COSE (
ML-DSA-44/65/87, tipo de chaveAKP). - draft-ietf-tls-mldsa: ML-DSA no TLS 1.3.
- NIST IR 8547 (rascunho público inicial): cronograma da transição.
- Documentação das bibliotecas: NEWS do OpenSSL 3.5, notas de versão do Go 1.27, JEP 497, Novidades nas bibliotecas do .NET 10.
A saída dos comandos foi produzida em 20 de outubro de 2026 em 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 (com bcprov-jdk18on 1.86), mcr.microsoft.com/dotnet/sdk:10.0.402-alpine3.24, mcr.microsoft.com/dotnet/sdk:10.0.402-noble e mcr.microsoft.com/playwright:v1.64.1-noble (Chromium 156).