Implementing NIST Post-Quantum Standards: FIPS 204, 205, and 206
TL;DR: NIST published FIPS 204, 205, and 206 in August 2024, completing the first wave of post-quantum cryptography standardization. ML-DSA (FIPS 204) handles signatures; SLH-DSA (FIPS 205) provides hash-based signature alternatives; ML-KEM (FIPS 206) handles key encapsulation. Each has distinct performance and size profiles suited to different applications. This guide covers when to use each standard, the key parameters, hybrid transition strategies, and working Python examples using the liboqs library.
Why NIST Published Three Standards Instead of One
NIST selected three distinct algorithms because different applications have genuinely different requirements, and because hedging against algorithm-specific cryptanalytic breaks is responsible standards policy. ML-DSA, SLH-DSA, and ML-KEM use different mathematical foundations: lattice problems, hash-function collision resistance, and lattice problems again, respectively. A future discovery that breaks the Module Learning With Errors (MLWE) problem underlying ML-DSA and ML-KEM would not break SLH-DSA, which relies only on hash function properties.
The three standards address three different cryptographic primitives. FIPS 204 (ML-DSA) is a digital signature scheme: it allows a private key holder to produce a signature that anyone with the corresponding public key can verify, without knowledge of the private key. This is what Bitcoin and Ethereum use (via ECDSA) for transaction authorization, and it is what any blockchain network needs for consensus and transaction validation.
FIPS 205 (SLH-DSA) is also a digital signature scheme but based on hash functions rather than lattice problems. It provides a conservative security argument: if you trust SHA-256 or SHAKE-256 (and the security community has analyzed these functions for thirty-plus years without finding fundamental breaks), you can trust SLH-DSA. The cost is significantly larger signature sizes.
FIPS 206 (ML-KEM) is a key encapsulation mechanism (KEM), not a signature scheme. It is used to establish a shared secret between two parties: one party generates a key pair and shares the public key, the other encapsulates a random secret and sends the ciphertext, and the first party decapsulates to recover the same secret. Both parties then use that shared secret as a symmetric encryption key. This is the post-quantum replacement for Diffie-Hellman and ECDH key exchange.
ML-DSA (FIPS 204): Lattice-Based Digital Signatures
ML-DSA is the primary post-quantum signature standard and the correct starting point for any application that needs digital signatures. It is based on the Module Learning With Errors (MLWE) problem, a variant of the Learning With Errors (LWE) problem that has been studied extensively since Regev's 2005 paper. ML-DSA inherits the performance efficiency of lattice-based cryptography: it is significantly faster to verify than ECDSA and competitive on signing speed at comparable security levels.
ML-DSA provides three parameter sets reflecting different trade-offs between signature size, key size, and security level. ML-DSA-44 targets NIST security level 2 (roughly equivalent to AES-128 against quantum adversaries), with 1312-byte public keys, 2528-byte private keys, and 2420-byte signatures. ML-DSA-65 targets level 3 (roughly AES-192), with 1952-byte public keys, 4032-byte private keys, and 3309-byte signatures. ML-DSA-87 targets level 5 (roughly AES-256), with 2592-byte public keys, 4896-byte private keys, and 4627-byte signatures.
For blockchain and financial applications where assets must remain secure for twenty or more years, ML-DSA-65 is the minimum recommended parameter set, and ML-DSA-87 is appropriate for high-value or long-lived key material. The performance cost of moving from ML-DSA-65 to ML-DSA-87 is modest: roughly 30% reduction in signing throughput and 15% reduction in verification throughput, both still faster than ECDSA verification on a per-operation basis.
A distinguishing property of ML-DSA is deterministic signing: given the same private key and message, ML-DSA always produces the same signature. This contrasts with ECDSA, where each signature requires a fresh random nonce. ECDSA's nonce requirement has historically been a significant source of implementation vulnerabilities; reusing a nonce or using a biased nonce generator leaks the private key. ML-DSA eliminates this attack surface entirely.
Python implementation using liboqs:
import oqs
import hashlib
def sign_transaction(tx_bytes: bytes, private_key_seed: bytes) -> tuple:
"""Sign a transaction payload with ML-DSA-87."""
with oqs.Signature("ML-DSA-87") as sig:
# In practice, derive key pair from seed using deterministic method
public_key = sig.generate_keypair()
tx_hash = hashlib.sha3_256(tx_bytes).digest()
signature = sig.sign(tx_hash)
return public_key, signature
def verify_transaction(tx_bytes: bytes, signature: bytes, public_key: bytes) -> bool:
"""Verify an ML-DSA-87 transaction signature."""
with oqs.Signature("ML-DSA-87") as verifier:
tx_hash = hashlib.sha3_256(tx_bytes).digest()
return verifier.verify(tx_hash, signature, public_key)
SLH-DSA (FIPS 205): Hash-Based Signatures for Maximum Conservatism
SLH-DSA is the correct choice when signature generation is infrequent and signature size is not a binding constraint, but the security argument must be as conservative as possible. Its security reduces to the collision resistance and preimage resistance of an underlying hash function (SHA-256, SHA-512, SHAKE-128, or SHAKE-256 depending on the selected variant). Unlike lattice-based schemes, SLH-DSA makes no assumptions about the hardness of algebraic problems; if your hash function is secure, your signatures are secure.
SLH-DSA has twelve parameter sets organized along three dimensions: hash function family (SHA-2 or SHAKE), security level (128, 192, or 256 bits), and optimization target (small signatures or fast signing). The small-signature variants ("s" suffix) produce the smallest signatures but are slower; the fast variants ("f" suffix) sign faster but produce larger signatures. SLH-DSA-SHA2-128s, the smallest and most commonly cited variant, produces 7856-byte signatures. SLH-DSA-SHA2-256f, the largest fast variant, produces 49856-byte signatures.
For blockchain applications, SLH-DSA's large signature sizes make it impractical for transaction signing in high-volume systems. Consider SLH-DSA for: protocol upgrade authorization keys that sign at most a few times per year; long-lived certificate authority roots where signature size does not affect latency; code signing for critical infrastructure where conservative security assumptions matter most; and firmware signing for hardware wallets or HSMs where each key may need to sign for a decade.
Python example with SLH-DSA:
import oqs
# SLH-DSA-SHA2-128s: smallest signature, slowest signing
with oqs.Signature("SPHINCS+-SHA2-128s-simple") as signer:
public_key = signer.generate_keypair()
critical_payload = b"Protocol upgrade authorization: version 2.1"
signature = signer.sign(critical_payload)
print(f"Signature size: {len(signature)} bytes") # 7856
# SLH-DSA-SHAKE-256f: larger signature, faster signing
with oqs.Signature("SPHINCS+-SHAKE-256f-simple") as signer:
public_key = signer.generate_keypair()
signature = signer.sign(critical_payload)
print(f"Signature size: {len(signature)} bytes") # 49856
ML-KEM (FIPS 206): Key Encapsulation for Secure Channels
ML-KEM is the post-quantum replacement for Diffie-Hellman and ECDH key exchange. It establishes a shared secret between two parties without requiring the secret to traverse the network. ML-KEM is not a signature scheme and does not provide authentication on its own; it must be combined with ML-DSA or another signature scheme to authenticate the key exchange and prevent man-in-the-middle attacks.
ML-KEM provides three parameter sets. ML-KEM-512 targets NIST security level 1 (approximately AES-128), with 800-byte public keys and 768-byte ciphertexts. ML-KEM-768 targets level 3 (approximately AES-192), with 1184-byte public keys and 1088-byte ciphertexts. ML-KEM-1024 targets level 5 (approximately AES-256), with 1568-byte public keys and 1568-byte ciphertexts. All three parameter sets produce 32-byte shared secrets, which are then typically used to derive symmetric keys using a KDF.
For blockchain peer-to-peer networking, ML-KEM-768 is appropriate for most inter-node communication. For validator-to-validator communication of high-value consensus messages, ML-KEM-1024 provides additional margin. Google has already deployed ML-KEM-768 in the Chrome browser for TLS connections to Google services, providing real-world evidence of its practical deployment characteristics.
Python key encapsulation example:
import oqs
from hashlib import sha3_256
def establish_shared_secret():
"""Demonstrate ML-KEM-768 key encapsulation flow."""
# Recipient side: generate key pair (long-lived or ephemeral)
with oqs.KeyEncapsulation("ML-KEM-768") as recipient:
public_key = recipient.generate_keypair()
# public_key is sent to sender (e.g. in a hello message)
# Sender side: encapsulate a shared secret
with oqs.KeyEncapsulation("ML-KEM-768") as sender:
ciphertext, shared_secret_a = sender.encap_secret(public_key)
# ciphertext is sent back to recipient
# Recipient side: decapsulate to recover shared secret
with oqs.KeyEncapsulation("ML-KEM-768") as recipient_dec:
shared_secret_b = recipient_dec.decap_secret(ciphertext)
# shared_secret_a == shared_secret_b
# Both parties now derive session keys from this shared secret
session_key = sha3_256(shared_secret_b).digest()
return session_key
Comparison Table: All Three NIST PQC Standards
| Property | ML-DSA (FIPS 204) | SLH-DSA (FIPS 205) | ML-KEM (FIPS 206) |
|---|---|---|---|
| Primitive type | Digital signature | Digital signature | Key encapsulation |
| Mathematical basis | Module LWE lattice problem | Hash function collision resistance | Module LWE lattice problem |
| Public key size (level 3) | 1952 bytes | 48 bytes | 1184 bytes |
| Signature / ciphertext size (level 3) | 3309 bytes | 17088 bytes (SHA2-192s) | 1088 bytes ciphertext |
| Sign / encap speed | Fast (~4100 ops/s) | Slow (~50 ops/s for "s" params) | Very fast (~25,000 ops/s) |
| Verify / decap speed | Very fast (~19,000 ops/s) | Fast (~2000 ops/s) | Very fast (~25,000 ops/s) |
| Stateful | No | No | No |
| Best use case | Transaction signing, TLS certificates, general signatures | Root CAs, code signing, protocol upgrade keys | TLS key exchange, VPN, P2P channel setup |
Hybrid Transition Strategies
Deploying post-quantum cryptography in production requires a hybrid strategy during the transition period: classical and post-quantum algorithms run in parallel, with the system secure against both classical and quantum adversaries simultaneously. The hybrid approach is explicitly recommended by NIST, NSA, and major standards bodies for the transition period between now and full PQC deployment.
For digital signatures in a hybrid scheme, the standard pattern combines ECDSA (or EdDSA) with ML-DSA. A signed message carries two independent signatures: one from the classical key and one from the post-quantum key. Verification requires both to pass. The classical component protects against adversaries who may have found unknown weaknesses in ML-DSA; the ML-DSA component protects against quantum adversaries who can break ECDSA with Shor's algorithm. The scheme is secure unless BOTH components are simultaneously broken.
For key exchange in a hybrid scheme, the standard pattern combines X25519 (or ECDH) with ML-KEM. The two shared secrets are concatenated and hashed together to produce the final session key. Breaking the hybrid scheme requires simultaneously breaking both the classical and post-quantum components. TLS 1.3 with X25519 and ML-KEM-768 was deployed by Google in Chrome in 2023 and represents the leading real-world example of this pattern at scale.
The transition timeline requires explicit planning. Define a trigger condition for dropping the classical component: this might be a specific date, a regulatory requirement, or a monitored metric about quantum computing progress. Build the trigger into your protocol as a network-level parameter change rather than requiring a coordinated software update across all nodes. Systems that can evolve their cryptographic parameters without flag-day upgrades are significantly more manageable during the migration period. QuanChain's Quantum Oracle architecture implements exactly this kind of monitored, protocol-level parameter evolution.
Implementation Checklist for Production Deployment
Before deploying any of the three NIST PQC standards in production, verify the following: you are using a validated implementation (liboqs, BoringSSL with PQC extensions, or an HSM that certifies FIPS 204/205/206 compliance); all buffer sizes in your storage layer, wire protocol, and API surface have been updated to accommodate the larger key and signature sizes; your key generation uses a validated DRBG (Deterministic Random Bit Generator) that is itself quantum-resistant (AES-256-CTR-DRBG rather than a classical hash-based DRBG); your hybrid scheme requires BOTH signatures to be valid, not either; and you have load-tested signing and verification throughput under your expected peak transaction volume, since the size increases can stress mempool, gossip network, and block propagation in ways that only appear under production load.
Frequently Asked Questions
Related Guides
Cryptography · 14 min read
Post-Quantum Cryptography: A Complete Developer Guide for 2026
Research · 8 min read· Blog
CRYSTALS-Dilithium Explained: The NIST Signature Standard Replacing ECDSA
Research · 10 min read· Blog
Post-Quantum Cryptography Explained: Lattice, Hash, and Code-Based Systems
Research · 9 min read· Blog
NIST Post-Quantum Cryptography Standards 2024: What Changed and What It Means