FIPS 203, 204 and 205 explained: ML-KEM, ML-DSA and SLH-DSA tested with OpenSSL
Table of contents
- Key takeaways
- What FIPS 203, 204 and 205 are and what each one replaces
- What security categories 1, 2, 3 and 5 mean
- ML-KEM (FIPS 203): parameter sets and sizes
- ML-DSA (FIPS 204): parameter sets and sizes
- SLH-DSA (FIPS 205): large signatures, minimal assumptions
- What is still coming: FN-DSA (FIPS 206), HQC and more signatures
- When RSA and ECC stop being acceptable under NIST IR 8547
- Hands-on: ML-KEM, ML-DSA and SLH-DSA with OpenSSL 3.5 in Docker
- Set up the container and list the algorithms
- Encapsulate and decapsulate a secret with ML-KEM
- Sign and verify with ML-DSA and SLH-DSA
- Measured sizes against the specification
- The hybrid X25519MLKEM768 group in a TLS 1.3 handshake
- mlkem768x25519-sha256 and ML-DSA signatures in OpenSSH
- FAQ
- Are Kyber and ML-KEM the same thing?
- Which parameter sets should I start with?
- Do I need to migrate now if no quantum computer exists?
- Conclusion
- Sources
FIPS 203 (ML-KEM) defines how to agree on a secret key, and FIPS 204 (ML-DSA) and FIPS 205 (SLH-DSA) how to sign, all three resistant to a quantum computer and final since August 2024. With OpenSSL 3.5.9 I measured their sizes: an ML-KEM-768 public key takes 1,184 bytes and an SLH-DSA-128s signature 7,856.
FIPS 203, FIPS 204 and FIPS 205 are the three post-quantum cryptography standards NIST approved on 13 August 2024. FIPS 203 defines ML-KEM, a mechanism for agreeing on a secret key; FIPS 204 and FIPS 205 define ML-DSA and SLH-DSA, two digital signature schemes. This guide covers what each one is for, what its parameter sets mean, how large its keys and signatures are, and the timeline NIST proposes for retiring RSA and elliptic curves. Then I test all of it with OpenSSL 3.5.9 in Docker: keys, signatures, encapsulation, a hybrid TLS 1.3 handshake and OpenSSH. There is also a Spanish version of this guide.
Key takeaways
- FIPS 203 (ML-KEM, formerly CRYSTALS-Kyber) replaces key exchange with RSA or ECDH; FIPS 204 (ML-DSA, formerly Dilithium) and FIPS 205 (SLH-DSA, formerly SPHINCS+) replace RSA, ECDSA and EdDSA signatures.
- Each standard has parameter sets tied to NIST security categories: category 1 matches breaking AES-128, category 3 AES-192 and category 5 AES-256.
- Sizes grow a lot: an ML-KEM-768 public key takes 1,184 bytes against 32 for X25519, and an SLH-DSA signature ranges from 7,856 to 49,856 bytes.
- The NIST IR 8547 draft proposes deprecating 112-bit RSA and ECC after 2030 and disallowing all RSA and ECC after 2035.
- OpenSSL 3.5 ships all three algorithms and offers the hybrid X25519MLKEM768 group by default in TLS 1.3; in my test the ClientHello grew from 281 to 1,515 bytes.
- As of 30 September 2026, FN-DSA (FIPS 206) and HQC still have no published draft on NIST’s PQC project page.
What FIPS 203, 204 and 205 are and what each one replaces
The three standards cover the two public-key operations a large quantum computer would break: agreeing on a secret and signing. Shor’s algorithm factors integers and solves discrete logarithms in polynomial time, which takes down RSA, Diffie-Hellman and elliptic curves. The new algorithms rest on problems with no known efficient quantum attack. The history of the competition is in the article on the NIST PQC standards.
The distinction that confuses people most is KEM versus signature. A key encapsulation mechanism (KEM) lets two parties share a 32-byte secret. One side publishes its encapsulation key; the other generates the secret and sends it inside a ciphertext that only the decapsulation key opens. A digital signature does something else: it proves who issued a message and that nobody altered it.
| Standard | Algorithm | Based on | Purpose | Replaces |
|---|---|---|---|---|
| FIPS 203 | ML-KEM | CRYSTALS-Kyber | Key encapsulation | RSA-KEM, DH, ECDH |
| FIPS 204 | ML-DSA | CRYSTALS-Dilithium | Digital signature | RSA, ECDSA, EdDSA |
| FIPS 205 | SLH-DSA | SPHINCS+ | Digital signature (hash) | RSA, ECDSA, EdDSA |
| FIPS 206 (draft pending) | FN-DSA | Falcon | Digital signature | RSA, ECDSA, EdDSA |
ML-KEM and ML-DSA rest on module lattices (the module learning with errors problem). SLH-DSA depends only on the security of a hash function, which is why NIST keeps it as the fallback if lattices are ever broken.
What security categories 1, 2, 3 and 5 mean
NIST does not describe post-quantum security as a number of bits but as five categories, each defined by a reference attack. The NIST IR 8547 draft summarises them like this:
| Category | Reference attack | Example |
|---|---|---|
| 1 | Key search on a block cipher with a 128-bit key | AES-128 |
| 2 | Collision search on a 256-bit hash function | SHA-256 |
| 3 | Key search on a block cipher with a 192-bit key | AES-192 |
| 4 | Collision search on a 384-bit hash function | SHA3-384 |
| 5 | Key search on a block cipher with a 256-bit key | AES-256 |
A category 3 parameter set claims that breaking it costs at least as much as brute-forcing an AES-192 key, under any realistic model of computation. The suffix in each name follows this logic: ML-KEM-512 is category 1, ML-KEM-768 category 3 and ML-KEM-1024 category 5.
ML-KEM (FIPS 203): parameter sets and sizes
ML-KEM has three parameter sets, and all three produce the same 32-byte shared secret. What changes is the size of the key and of the ciphertext that crosses the network, per Table 3 of FIPS 203:
| Parameter set | Category | Encapsulation key | Decapsulation key | Ciphertext |
|---|---|---|---|---|
| ML-KEM-512 | 1 | 800 bytes | 1,632 bytes | 768 bytes |
| ML-KEM-768 | 3 | 1,184 bytes | 2,400 bytes | 1,088 bytes |
| ML-KEM-1024 | 5 | 1,568 bytes | 3,168 bytes | 1,568 bytes |
ML-KEM-768 is the one TLS and OpenSSH use in their hybrid groups. FIPS 203 gives a general rule for choosing: "When initially establishing cryptographic protections for data, the strongest possible parameter set should be used". The same paragraph adds that some parameter sets can hurt performance, so the real choice weighs security against bandwidth.
One detail worth knowing before you code against ML-KEM is implicit rejection. If the ciphertext arrives altered, decapsulation does not return an error: it returns a different secret, derived from the ciphertext and a random value z stored in the private key. The failure shows up later, when the two parties cannot decrypt anything with mismatched keys. I check this in the hands-on section.
ML-DSA (FIPS 204): parameter sets and sizes
ML-DSA is the family’s general-purpose signature: keys and signatures of a few kilobytes and an implementation NIST describes as simple. Each set’s name gives the dimensions of the internal matrix, not the security bits, which is why ML-DSA-44 is category 2 rather than category 1:
| Parameter set | Category | Private key | Public key | Signature |
|---|---|---|---|---|
| ML-DSA-44 | 2 | 2,560 bytes | 1,312 bytes | 2,420 bytes |
| ML-DSA-65 | 3 | 4,032 bytes | 1,952 bytes | 3,309 bytes |
| ML-DSA-87 | 5 | 4,896 bytes | 2,592 bytes | 4,627 bytes |
For comparison, in the same container an Ed25519 signature took 64 bytes and its public key 32. An X.509 certificate chain with ML-DSA multiplies that weight on every handshake. FIPS 204 also allows storing just a 32-byte seed as the private key and regenerating the rest when signing.
SLH-DSA (FIPS 205): large signatures, minimal assumptions
SLH-DSA trades signature size for confidence in its assumptions: its security depends only on SHA-2 or SHAKE. Its public keys are tiny (32, 48 or 64 bytes) and its signatures huge. FIPS 205 defines 12 parameter sets, combining three levels, two hash families (SHA2 and SHAKE) and two variants: s for small signatures and f for fast signing.
| Parameter set | Category | Public key | Signature |
|---|---|---|---|
| SLH-DSA-*-128s | 1 | 32 bytes | 7,856 bytes |
| SLH-DSA-*-128f | 1 | 32 bytes | 17,088 bytes |
| SLH-DSA-*-192s | 3 | 48 bytes | 16,224 bytes |
| SLH-DSA-*-192f | 3 | 48 bytes | 35,664 bytes |
| SLH-DSA-*-256s | 5 | 64 bytes | 29,792 bytes |
| SLH-DSA-*-256f | 5 | 64 bytes | 49,856 bytes |
The asterisk stands for SHA2 or SHAKE, with identical sizes. Each key supports up to 264 signatures; a FIPS 205 footnote works out that at 10 billion signatures per second it would take over 58 years to exhaust one. SLH-DSA fits firmware signing, roots of trust or long-lived documents, where you sign rarely and verify for decades. In April 2026 NIST released draft SP 800-230 with extra SLH-DSA parameter sets for uses that sign a limited number of messages.
What is still coming: FN-DSA (FIPS 206), HQC and more signatures
The family is not closed, and as of 30 September 2026 three pieces are on the way:
- FN-DSA (FIPS 206): the Falcon-based signature, with shorter signatures and fast verification but a trickier implementation. NIST’s presentation at the MPTS 2026 workshop lists it as "Under development (hopefully soon)", and the PQC project’s news page does not list a published draft yet
- HQC: a second KEM based on error-correcting codes rather than lattices, selected on 11 March 2025 (NIST IR 8545) as a backup for ML-KEM. Its draft standard is marked "underway" in the same presentation
- Additional signatures: on 14 May 2026, NIST IR 8610 moved nine candidates to the third round of the additional-signatures process, looking for schemes that do not rely on structured lattices
None of this changes what you can deploy today. ML-KEM, ML-DSA and SLH-DSA are final standards, FIPS 140 validation testing for them is under way, and they already ship in OpenSSL and OpenSSH.
When RSA and ECC stop being acceptable under NIST IR 8547
NIST IR 8547, published as a draft in November 2024, proposes the timeline for retiring vulnerable algorithms. It is still a draft (the CSRC page lists it as an Initial Public Draft), so the dates can move, but it is the only official NIST timeline on the table:
| Algorithms | Security | Proposal |
|---|---|---|
| RSA, ECDSA, EdDSA (signatures) | 112 bits | Deprecated after 2030, disallowed after 2035 |
| RSA, ECDSA, EdDSA (signatures) | 128 bits or more | Disallowed after 2035 |
| DH, ECDH, MQV, RSA (key establishment) | 112 bits | Deprecated after 2030, disallowed after 2035 |
| DH, ECDH, MQV, RSA (key establishment) | 128 bits or more | Disallowed after 2035 |
Per Table 2 of SP 800-57, 2048-bit RSA and curves of 224 to 255 bits give 112 bits of security; 3072-bit RSA and P-256 give 128. The 2035 target comes from the US National Security Memorandum NSM-10. Approved symmetric algorithms with at least 128 bits, such as AES-128 or SHA-256, remain approved. How to prioritise the migration is covered in the article on what to do with the final NIST standards.
Hands-on: ML-KEM, ML-DSA and SLH-DSA with OpenSSL 3.5 in Docker
OpenSSL 3.5 was the first release with all three algorithms, and it is the long-term support release until 8 April 2030. I ran everything on 30 September 2026 in an Alpine 3.23.6 container on arm64 Linux, with OpenSSL 3.5.9 (released 29 September) and OpenSSH 10.2p1.
Set up the container and list the algorithms
Start an Alpine container, install OpenSSL and OpenSSH and check the version. Then list the available KEMs:
docker run --rm -it mirror.gcr.io/library/alpine:3.23 sh
apk add --no-cache openssl openssh
openssl version
openssl list -kem-algorithms | grep -i mlkem
The output confirms the version and shows the three pure ML-KEM sets and four hybrid groups:
OpenSSL 3.5.9 29 Sep 2026 (Library: OpenSSL 3.5.9 29 Sep 2026)
{ 2.16.840.1.101.3.4.4.1, id-alg-ml-kem-512, ML-KEM-512, MLKEM512 } @ default
{ 2.16.840.1.101.3.4.4.2, id-alg-ml-kem-768, ML-KEM-768, MLKEM768 } @ default
{ 2.16.840.1.101.3.4.4.3, id-alg-ml-kem-1024, ML-KEM-1024, MLKEM1024 } @ default
X25519MLKEM768 @ default
X448MLKEM1024 @ default
SecP256r1MLKEM768 @ default
SecP384r1MLKEM1024 @ default
openssl list -signature-algorithms lists the three ML-DSA and twelve SLH-DSA sets the same way, all in the default provider.
Encapsulate and decapsulate a secret with ML-KEM
A KEM flow is four steps: generate the key, publish the public half, encapsulate with it and decapsulate with the private key. With openssl pkeyutl it looks like this:
openssl genpkey -algorithm ML-KEM-768 -out kem.key
openssl pkey -in kem.key -pubout -out kem.pub
openssl pkeyutl -encap -pubin -inkey kem.pub \
-out kem.ct -secret sender.ss
openssl pkeyutl -decap -inkey kem.key -in kem.ct -secret receiver.ss
cmp sender.ss receiver.ss && echo "same secret"
wc -c kem.ct sender.ss
The sender gets sender.ss and a 1,088-byte ciphertext; the receiver recovers the same 32-byte secret. Then I flipped a single byte of the ciphertext and decapsulated again: openssl exited with code 0 and returned a different 32-byte secret (1666f3ae… against 5863fcc4…). That is FIPS 203’s implicit rejection at work, and the reason your protocol must confirm the key before trusting it.
Sign and verify with ML-DSA and SLH-DSA
Post-quantum signatures take -rawin, because the algorithm processes the whole message rather than a digest computed separately:
openssl genpkey -algorithm ML-DSA-65 -out sign.key
openssl pkey -in sign.key -pubout -out sign.pub
openssl pkeyutl -sign -rawin -inkey sign.key \
-in msg.txt -out msg.sig
openssl pkeyutl -verify -rawin -pubin -inkey sign.pub \
-in msg.txt -sigfile msg.sig
wc -c msg.sig
Verification prints Signature Verified Successfully and the signature is 3,309 bytes. With an altered message, openssl exits with code 1 and the error Signature Verification Failure. For SLH-DSA, change the algorithm name, for example to SLH-DSA-SHA2-128s.
Measured sizes against the specification
I repeated the flow with the three ML-KEM sets, the three ML-DSA sets and four SLH-DSA sets, measuring the public key inside its SPKI structure, the ciphertext and the signature. Every value matches the FIPS 203, 204 and 205 tables byte for byte:
The PKCS#8 private key weighs more than the table says, and there is a reason. By default OpenSSL 3.5 stores the seed and the expanded key together (the seed-priv format in its documentation). An ML-KEM-768 key took 2,498 bytes in DER: the 64-byte (d, z) seed, the 2,400-byte decapsulation key and the ASN.1 header. For SLH-DSA-128s the whole private key is 84 bytes.
The hybrid X25519MLKEM768 group in a TLS 1.3 handshake
OpenSSL 3.5 offers two key shares by default in TLS 1.3, X25519MLKEM768 and X25519, so an up-to-date client and server negotiate the hybrid with no configuration. The group combines ML-KEM-768 with X25519: the secret holds as long as either one resists. To see it, I started a server with a self-signed ML-DSA-65 certificate and connected with s_client:
openssl req -x509 -newkey ML-DSA-65 -keyout srv.key -out srv.crt \
-days 30 -nodes -subj "/CN=pqc.lab"
openssl s_server -accept 4433 -cert srv.crt -key srv.key -www -quiet &
echo | openssl s_client -connect 127.0.0.1:4433 -brief
These are the relevant lines of the -brief summary, with both post-quantum parts of the handshake, the key agreement and the server’s signature:
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Signature type: mldsa65
Negotiated TLS1.3 group: X25519MLKEM768
With -trace instead of -brief you see each message’s size. These are the ones I measured, against a server with a P-256 certificate to isolate the group’s effect, and against the ML-DSA-65 one to see the certificate’s:
The client key share is 1,184 bytes of ML-KEM key plus 32 of X25519, and the server’s is 1,088 bytes of ciphertext plus 32. With the default groups the client also sends a separate X25519 key share in case the server does not know the hybrid. If you force -groups MLKEM768 or SecP256r1MLKEM768 on the client, an OpenSSL 3.5 server with its default configuration aborts with alert handshake failure (alert 40), because neither is in its default list. Real-world adoption of these groups is covered in the article on post-quantum cryptography in TLS.
mlkem768x25519-sha256 and ML-DSA signatures in OpenSSH
OpenSSH uses the same KEM under another name, mlkem768x25519-sha256, and since OpenSSH 10.0 (9 April 2025) it is the default key exchange. With OpenSSH 10.2p1 in the container, a connection to localhost confirms it:
ssh-keygen -A
/usr/sbin/sshd
ssh -v -o BatchMode=yes localhost true 2>&1 | grep "kex: algorithm"
The debug line shows kex: algorithm: mlkem768x25519-sha256. Signatures are another matter: the host key stayed ssh-ed25519.
OpenSSH 10.3 added experimental support for a composite ML-DSA-44 plus Ed25519 signature, off by default. I tried it on Alpine edge with OpenSSH 10.5p1: ssh-keygen -t mldsa44-ed25519 produced a 1,895-byte public key (99 for Ed25519), and with it as both host and user key the session authenticated with kex: algorithm: mlkem768x25519-sha256 and host key algorithm: ssh-mldsa44-ed25519@openssh.com. Step-by-step server configuration is in the guide to migrating SSH to post-quantum cryptography.
FAQ
Are Kyber and ML-KEM the same thing?
Not exactly. ML-KEM is the standardised version of CRYSTALS-Kyber, and Appendix C of FIPS 203 lists the changes that make it produce different outputs from Kyber for the same inputs. That is why TLS moved from the X25519Kyber768 group to X25519MLKEM768. The same holds for Dilithium versus ML-DSA and SPHINCS+ versus SLH-DSA.
Which parameter sets should I start with?
ML-KEM-768 for key agreement, because it is the one TLS and OpenSSH hybrid groups use. For signatures, ML-DSA-65 covers category 3 with 3,309-byte signatures; SLH-DSA fits where you sign rarely and want to depend on a hash alone. FIPS 203 recommends the strongest parameter set your system can take.
Do I need to migrate now if no quantum computer exists?
For key agreement, yes. An attacker can store encrypted traffic today and decrypt it once the machine exists, so the hybrid KEM protects data you are already sending. Signatures only need replacing before the attack becomes possible, and the IR 8547 draft sets 2035 as the limit.
Conclusion
FIPS 203 solves key agreement with ML-KEM, and FIPS 204 and FIPS 205 solve signing with ML-DSA and SLH-DSA. All three have been final since August 2024 and already work in everyday software. In my test with OpenSSL 3.5.9 every size matched the NIST documents. The real cost is bandwidth: a 1,515-byte ClientHello and a 5,506-byte ML-DSA-65 certificate.
The plan that follows has three steps:
- check that your TLS and SSH servers run OpenSSL 3.5 or OpenSSH 10 or later, which negotiate the hybrid by default
- verify the negotiated group with
openssl s_client -briefand the SSH key exchange withssh -v - inventory where you sign with RSA or ECDSA, because that migration is the slow one and the NIST draft closes it in 2035
The state of adoption is covered in the article on what is actually happening with the post-quantum migration.
Sources: [1] FIPS 203, Module-Lattice-Based Key-Encapsulation Mechanism Standard[1], [2] FIPS 204, Module-Lattice-Based Digital Signature Standard[2], [3] FIPS 205, Stateless Hash-Based Digital Signature Standard[3], [4] NIST IR 8547 ipd, Transition to Post-Quantum Cryptography Standards[4], [5] NIST CSRC Post-Quantum Cryptography project[5], [6] The NIST PQC Project, MPTS 2026[6], [7] OpenSSL 3.5 NEWS[7], [8] OpenSSL release strategy[8], [9] OpenSSH release notes[9], [10] NIST SP 800-57 Part 1 Rev. 5[10].
Sources
- FIPS 203, Module-Lattice-Based Key-Encapsulation Mechanism Standard
- FIPS 204, Module-Lattice-Based Digital Signature Standard
- FIPS 205, Stateless Hash-Based Digital Signature Standard
- NIST IR 8547 ipd, Transition to Post-Quantum Cryptography Standards
- NIST CSRC Post-Quantum Cryptography project
- The NIST PQC Project, MPTS 2026
- OpenSSL 3.5 NEWS
- OpenSSL release strategy
- OpenSSH release notes
- NIST SP 800-57 Part 1 Rev. 5