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:

Sizes measured with OpenSSL 3.5 bytes
ML-KEM-512
Public key: 800 bytes
Ciphertext or signature: 768 bytes
ML-KEM-768
Public key: 1,184 bytes
Ciphertext or signature: 1,088 bytes
ML-KEM-1024
Public key: 1,568 bytes
Ciphertext or signature: 1,568 bytes
ML-DSA-44
Public key: 1,312 bytes
Ciphertext or signature: 2,420 bytes
ML-DSA-65
Public key: 1,952 bytes
Ciphertext or signature: 3,309 bytes
ML-DSA-87
Public key: 2,592 bytes
Ciphertext or signature: 4,627 bytes
SLH-DSA-SHA2-128s
Public key: 32 bytes
Ciphertext or signature: 7,856 bytes
SLH-DSA-SHA2-128f
Public key: 32 bytes
Ciphertext or signature: 17,088 bytes
SLH-DSA-SHAKE-256s
Public key: 64 bytes
Ciphertext or signature: 29,792 bytes
SLH-DSA-SHAKE-256f
Public key: 64 bytes
Ciphertext or signature: 49,856 bytes
Show the data table
AlgorithmPublic keyCiphertext or signature
ML-KEM-512800 bytes768 bytes
ML-KEM-7681,184 bytes1,088 bytes
ML-KEM-10241,568 bytes1,568 bytes
ML-DSA-441,312 bytes2,420 bytes
ML-DSA-651,952 bytes3,309 bytes
ML-DSA-872,592 bytes4,627 bytes
SLH-DSA-SHA2-128s32 bytes7,856 bytes
SLH-DSA-SHA2-128f32 bytes17,088 bytes
SLH-DSA-SHAKE-256s64 bytes29,792 bytes
SLH-DSA-SHAKE-256f64 bytes49,856 bytes

Public key inside its SPKI structure. All ten values match FIPS 203, 204 and 205 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:

Size of the TLS 1.3 handshake messages bytes
ClientHello (default groups against X25519 only)
Classical: 281 bytes
Post-quantum: 1,515 bytes
Client key share
Classical: 32 bytes
Post-quantum: 1,216 bytes
Server key share
Classical: 32 bytes
Post-quantum: 1,120 bytes
Certificate in DER (P-256 against ML-DSA-65)
Classical: 382 bytes
Post-quantum: 5,506 bytes
CertificateVerify signature
Classical: 71 bytes
Post-quantum: 3,309 bytes
Show the data table
MessageClassicalPost-quantum
ClientHello (default groups against X25519 only)281 bytes1,515 bytes
Client key share32 bytes1,216 bytes
Server key share32 bytes1,120 bytes
Certificate in DER (P-256 against ML-DSA-65)382 bytes5,506 bytes
CertificateVerify signature71 bytes3,309 bytes

Measured with openssl s_client -trace.

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:

  1. check that your TLS and SSH servers run OpenSSL 3.5 or OpenSSH 10 or later, which negotiate the hybrid by default
  2. verify the negotiated group with openssl s_client -brief and the SSH key exchange with ssh -v
  3. 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

  1. FIPS 203, Module-Lattice-Based Key-Encapsulation Mechanism Standard
  2. FIPS 204, Module-Lattice-Based Digital Signature Standard
  3. FIPS 205, Stateless Hash-Based Digital Signature Standard
  4. NIST IR 8547 ipd, Transition to Post-Quantum Cryptography Standards
  5. NIST CSRC Post-Quantum Cryptography project
  6. The NIST PQC Project, MPTS 2026
  7. OpenSSL 3.5 NEWS
  8. OpenSSL release strategy
  9. OpenSSH release notes
  10. NIST SP 800-57 Part 1 Rev. 5