BACK TO NEWS
defense #nist #fips #ml-kem #ml-dsa #slh-dsa

Inside the NIST PQC suite: ML-KEM (FIPS 203), ML-DSA (FIPS 204), SLH-DSA (FIPS 205)

Four years after the round-3 finalists were named, the standards are signed. Here is what each one does, what parameter set to pick at each security level, and the smallest-possible call into each one from Python and TypeScript.

by · · 5 min read

TL;DR

NIST published three final standards in 2024-2025:

  • FIPS 203 — ML-KEM (Module-Lattice Key Encapsulation, née Kyber)
  • FIPS 204 — ML-DSA (Module-Lattice Digital Signature, née Dilithium)
  • FIPS 205 — SLH-DSA (Stateless Hash-based Digital Signature, née SPHINCS+)

A fourth (FIPS 206 — FN-DSA, née Falcon) is in draft, expected 2026. Every cryptographic library you reach for must by now expose the FIPS names and parameter sets. If yours doesn't, it is out of date.

1. ML-KEM — FIPS 203

What it does: encapsulates a 32-byte shared secret under a recipient's public key. Drop-in replacement for ECDH or RSA-KEM. Used in TLS hybrid KEX, JWE key wrap, MLS group key derivation.

Security reduction: hardness of Module-LWE (Learning With Errors over module lattices). Best known classical attack: ~ 2^l/2 for parameter level l, no quantum speedup beyond Grover.

Parameter sets:

TEXT
1parameter sec. cat pub key ciphertext shared secret
2─────────────────────────────────────────────────────────────
3ML-KEM-512 NIST L1 800 B 768 B 32 B
4ML-KEM-768 NIST L3 1184 B 1088 B 32 B
5ML-KEM-1024 NIST L5 1568 B 1568 B 32 B

Pick ML-KEM-768 by default. L3 is the recommended baseline; L5 only if you're encrypting nation-state archives.

Smallest possible call (Python)

PYTHON
1# pip install pqcrypto
2from pqcrypto.kem import ml_kem_768
3 
4# Recipient generates a keypair, publishes pub.
5pub, priv = ml_kem_768.generate_keypair()
6# Sender encapsulates against pub.
7ciphertext, ss_sender = ml_kem_768.encrypt(pub)
8# Recipient decapsulates the ciphertext.
9ss_recipient = ml_kem_768.decrypt(priv, ciphertext)
10assert ss_sender == ss_recipient # 32 bytes of shared secret

Smallest possible call (TypeScript)

TS
1import { ml_kem768 } from '@noble/post-quantum/ml-kem';
2 
3const kp = ml_kem768.keygen(); // {publicKey, secretKey}
4const { cipherText, sharedSecret: sndSS } = ml_kem768.encapsulate(kp.publicKey);
5const rcvSS = ml_kem768.decapsulate(cipherText, kp.secretKey);
6// sndSS === rcvSS (32 bytes)

2. ML-DSA — FIPS 204

What it does: signs an arbitrary message under a private key, verifies under a public key. Drop-in replacement for Ed25519, ECDSA, RSA-PSS. Used in JWT, code signing, OCSP, CMS.

Security reduction: Module-LWE plus Module-SIS (Short Integer Solutions). Roughly the same hardness assumption as ML-KEM.

Parameter sets:

TEXT
1parameter sec. cat pub key priv key signature
2──────────────────────────────────────────────────────────
3ML-DSA-44 NIST L2 1312 B 2560 B 2420 B
4ML-DSA-65 NIST L3 1952 B 4032 B 3293 B
5ML-DSA-87 NIST L5 2592 B 4896 B 4595 B

Pick ML-DSA-65 for new builds. L3 is the recommended baseline.

Smallest possible call (Python)

PYTHON
1from pqcrypto.sign import ml_dsa_65
2 
3pub, priv = ml_dsa_65.generate_keypair()
4msg = b"the message to sign"
5sig = ml_dsa_65.sign(priv, msg) # 3293 bytes
6ok = ml_dsa_65.verify(pub, msg, sig) # raises on fail

Smallest possible call (TypeScript)

TS
1import { ml_dsa65 } from '@noble/post-quantum/ml-dsa';
2 
3const kp = ml_dsa65.keygen();
4const msg = new TextEncoder().encode('the message to sign');
5const sig = ml_dsa65.sign(msg, kp.secretKey);
6const ok = ml_dsa65.verify(sig, msg, kp.publicKey); // boolean

3. SLH-DSA — FIPS 205

What it does: signs by walking a deep Merkle tree of one-time signatures. Hash-function security only. The most conservative signature primitive in the suite.

Security reduction: collision and preimage resistance of SHA-2 or SHAKE. No number-theoretic assumption, no lattice assumption.

Parameter sets (each comes in s for "small signature" and f for "fast signing"):

TEXT
1parameter sec. cat pub key signature verify
2──────────────────────────────────────────────────────────
3SLH-DSA-128s L1 32 B 7856 B fast
4SLH-DSA-128f L1 32 B 17088 B fast
5SLH-DSA-192s L3 48 B 16224 B fast
6SLH-DSA-192f L3 48 B 35664 B fast
7SLH-DSA-256s L5 64 B 29792 B fast
8SLH-DSA-256f L5 64 B 49856 B fast

s variants: smaller signature, slower signing (minutes per key). f variants: faster signing (seconds), larger signature.

Pick SLH-DSA-128s for code-signing roots. Verification is hash-only and fast on every platform — including pure-PHP without extensions.

Smallest possible call (Python)

PYTHON
1from pqcrypto.sign import sphincs_sha2_128s_simple as slh
2 
3pub, priv = slh.generate_keypair()
4msg = b"firmware image hash"
5sig = slh.sign(priv, msg)
6ok = slh.verify(pub, msg, sig)

4. FN-DSA (Falcon) — FIPS 206 (draft)

What it does: lattice signature with the smallest signatures in the suite.

Status: draft as of mid-2026. Expected final 2026/2027.

Why you wait: Falcon depends on floating-point arithmetic in the reference implementation. Side-channel and porting hazards are real. NIST advises against deployment until FIPS 206 finalises and validated constant-time implementations exist.

When to pick it: bandwidth-constrained workloads where signature size matters more than implementation simplicity (IoT firmware updates, some HSM-internal flows).

5. The decision tree

TEXT
1Need to encapsulate a key (TLS, JWE, MLS, KMS envelope)?
2 └─ ML-KEM-768
3 
4Need to sign a per-message token (JWT, OCSP, request signing)?
5 ├─ Long-lived service? → ML-DSA-65 hybrid with Ed25519
6 └─ Per-request perf? → ML-DSA-65 alone (already fast)
7 
8Need to sign a release artifact (one-shot, verified for years)?
9 └─ SLH-DSA-128s (the "100-year vault" pick)
10 
11Bandwidth-constrained signing?
12 └─ Wait for FIPS 206 (Falcon)
13 
14Pure-PHP verification on shared hosting?
15 └─ SLH-DSA-128s — verification is hash-only

6. Hybrid is still the right move

NIST's own guidance: in this transition window, combine a classical algorithm with a PQ one so a break in either still leaves you safe. The standard pattern:

PYTHON
1# Hybrid TLS shared secret = HKDF(X25519_ss || ML_KEM_ss, salt, info)
2import os
3from cryptography.hazmat.primitives import hashes
4from cryptography.hazmat.primitives.kdf.hkdf import HKDF
5from pqcrypto.kem import ml_kem_768
6 
7# Run both KEMs side by side.
8pq_pub, pq_priv = ml_kem_768.generate_keypair()
9pq_ct, pq_ss = ml_kem_768.encrypt(pq_pub)
10x_ss = os.urandom(32) # in reality: X25519 shared secret
11 
12shared = HKDF(
13 algorithm=hashes.SHA384(), length=32,
14 salt=b'qbit404-hybrid', info=b'tls-1.3-key-derivation'
15).derive(x_ss + pq_ss)

This is the same construction ratified in draft-ietf-tls-hybrid-design. A break in ML-KEM (unlikely but non-zero given the field's young age) does not leak the shared secret because the attacker still has to break X25519 for the same session — which requires Shor, the very thing PQ migration is preventing.

7. The deployment checklist

TEXT
1[ ] CBOM (cryptography BOM) — enumerate every key.
2[ ] Edge TLS: hybrid KEM advertised.
3[ ] Code signing: SLH-DSA root rotation planned.
4[ ] JWT issuers: ML-DSA hybrid emitting.
5[ ] SDK pinning: ML-KEM / ML-DSA aliases, NOT Kyber / Dilithium aliases.
6[ ] CAVP validation: required for FedRAMP / national security workloads.

Library matrix (mid-2026):

Stack KEM Signature Hash sig FIPS-aliased?
OpenSSL ≥ 3.4 ML-KEM ML-DSA SLH-DSA yes
BoringSSL ML-KEM (X25519MLKEM768) (planned) (planned) yes
Noble (JS/TS) ml_kem768 ml_dsa65 slh_dsa_sha2_128s yes
pqcrypto (Python) ml_kem_768 ml_dsa_65 sphincs_sha2_128s partial
liboqs (C / FFI) full matrix full matrix full matrix yes

8. References

  • NIST FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), FIPS 205 (SLH-DSA), FIPS 206 (FN-DSA, draft)
  • NIST SP 800-227 — implementing ML-KEM in TLS 1.3 (2025)
  • BSI / ANSSI joint PQC migration playbook (2024)
  • Hybrid Key Exchange in TLS 1.3, draft-ietf-tls-hybrid-design

The PQC suite is no longer "coming". It is published, parameter-frozen, and supported in OpenSSL and Noble. The remaining work is operational: inventory, rotate, validate. The math is done; the work is yours.

// related ops

RELATED OPS