Post-quantum cryptography has moved from conference talks into default settings. Chrome and Firefox send a post-quantum key share in their TLS handshakes by default, OpenSSH 10.0 uses one by default, and OpenSSL 3.5 shipped the algorithms as first-class key types. This post is about what actually changed and what did not: which primitives a quantum computer breaks, which it merely dents, what the three NIST standards contain, and what the hybrid handshake looks like on the wire. Every command that prints output below was run on my machine.
What a quantum computer breaks
Shor's 1994 algorithm factors integers and computes discrete logarithms in polynomial time. The two problems look different but share the structure that matters: both reduce to finding the period of a function on an abelian group, and the quantum Fourier transform finds periods efficiently. For RSA, pick a random
is a non-trivial factor. For an elliptic-curve key
How far away is such a machine? Gidney's 2025 estimate puts RSA-2048 within reach of fewer than a million noisy physical qubits running for under a week, down from the 20 million qubits of the 2019 estimate under the same error-correction assumptions. Today's devices have on the order of a thousand physical qubits with worse error rates, and nobody has factored a cryptographically interesting number. But one attack works today: harvest now, decrypt later. Traffic recorded in 2026 stays recorded, and a key exchange that a future machine can reverse exposes everything encrypted under it. That is why key exchange migrates first and signatures later: a forged signature only matters at the moment of verification, while confidentiality has to hold for the lifetime of the data.
What it only dents
Grover's 1996 algorithm searches an unstructured space of
| Primitive | Classical security | Best known quantum attack | Verdict |
|---|---|---|---|
| RSA-2048, DH-2048 | about 112 bits | Shor, polynomial time | broken once a large machine exists |
| ECDH / ECDSA P-256, X25519, Ed25519 | about 128 bits | Shor, polynomial time | broken, with fewer qubits than RSA-2048 |
| AES-128 | 128 bits | Grover, | acceptable; AES-256 for long-term margin |
| AES-256, ChaCha20 | 256 bits | Grover, | safe |
| SHA-256 (preimage / collision) | 256 / 128 bits | Grover | safe |
| HMAC, KMAC, HKDF | key length | Grover on the key | safe |
The three standards
NIST finalised its first three post-quantum standards on 13 August 2024, after an eight-year public competition:
- FIPS 203, ML-KEM (formerly CRYSTALS-Kyber). A key-encapsulation mechanism built on Module Learning With Errors, with parameter sets ML-KEM-512, -768 and -1024 at NIST security categories 1, 3 and 5 (as hard as AES-128, -192 and -256 key search). This is the one in your browser.
- FIPS 204, ML-DSA (formerly CRYSTALS-Dilithium). A lattice signature scheme from the same family; ML-DSA-44, -65 and -87 target categories 2, 3 and 5. The general-purpose replacement for ECDSA and RSA signatures.
- FIPS 205, SLH-DSA (formerly SPHINCS+). A stateless hash-based signature scheme whose security rests on nothing but the hash function. Tiny keys, large signatures and slow signing make it the conservative choice for roots of trust and firmware signing rather than for TLS.
Two more are coming: FN-DSA (Falcon), a lattice scheme with smaller signatures but a floating-point implementation that is easy to get wrong, as draft FIPS 206, and HQC, a code-based KEM selected in March 2025 as a backup should the lattice assumption fail. NIST's draft transition plan, IR 8547, proposes deprecating the quantum-vulnerable RSA and elliptic-curve algorithms at the 112-bit security strength (RSA-2048, for example) after 2030 and disallowing all of them after 2035.
Why lattices: the one idea behind ML-KEM and ML-DSA
Learning With Errors asks you to recover a secret vector
ML-KEM uses the module variant: the entries of
ML-DSA works in a ring of the same form, with
Everything got bigger
The objects are large. The sizes below are the ones published in the three FIPS documents; the classical rows use the usual encodings (uncompressed points for P-256, raw modulus for RSA).
| Scheme | NIST category | Public key (bytes) | Ciphertext or signature (bytes) | Secret key (bytes) |
|---|---|---|---|---|
| X25519 | classical | 32 | 32 | 32 |
| ECDH P-256 | classical | 65 | 65 | 32 |
| RSA-2048 (encryption) | classical | 256 | 256 | about 1.2 KB |
| ML-KEM-512 | 1 | 800 | 768 | 1632 |
| ML-KEM-768 | 3 | 1184 | 1088 | 2400 |
| ML-KEM-1024 | 5 | 1568 | 1568 | 3168 |
| Ed25519 | classical | 32 | 64 | 32 |
| ECDSA P-256 | classical | 65 | 64 | 32 |
| RSA-2048 (signature) | classical | 256 | 256 | about 1.2 KB |
| ML-DSA-44 | 2 | 1312 | 2420 | 2560 |
| ML-DSA-65 | 3 | 1952 | 3309 | 4032 |
| ML-DSA-87 | 5 | 2592 | 4627 | 4896 |
| SLH-DSA-SHA2-128s | 1 | 32 | 7856 | 64 |
| SLH-DSA-SHA2-128f | 1 | 32 | 17088 | 64 |
Key exchange gets away with this: one ML-KEM-768 exchange adds about 2.3 KB to a handshake, less than a single certificate. Signatures do not. A handshake carries several signatures and public keys (the handshake signature, the chain, Certificate Transparency SCTs), and replacing all of them with ML-DSA-44 adds on the order of 15 KB per connection, which is the main reason the web PKI still signs classically while key exchange has already moved.
A KEM is not Diffie–Hellman
Diffie–Hellman is symmetric: both sides publish a value and both combine. A KEM has three operations and a fixed direction. KeyGen produces an encapsulation key ek and a decapsulation key dk; Encaps(ek) produces a ciphertext and a fresh shared secret; Decaps(dk, ct) recovers the same secret. TLS 1.3 happens to fit this shape: the client's key_share carries ek, and the server's key_share carries ct, in the same two flights the protocol always had.
The group that browsers negotiate is X25519MLKEM768 (code point 0x11EC). The client share is the ML-KEM-768 encapsulation key followed by an X25519 public key,
Why keep the X25519 half? Because the lattice assumption is young: two later-round NIST candidates, SIKE and Rainbow, fell to classical attacks in 2022, SIKE in about an hour on a laptop. A hybrid means a flaw in ML-KEM leaves you with exactly the X25519 security you had before, while a quantum computer leaves you with ML-KEM. The cost is about 2.3 KB per handshake and a ClientHello that no longer fits in one packet, which flushed out a generation of buggy middleboxes. Chrome and Firefox turned the hybrid on by default in late 2024, Go's crypto/tls followed in 1.24, OpenSSL 3.5 puts X25519MLKEM768 first in its default group list, and OpenSSH 10.0 made mlkem768x25519-sha256 its default key exchange.
Trying it with OpenSSL 3.5
OpenSSL 3.5 added ML-KEM, ML-DSA and SLH-DSA to the default and FIPS providers and taught pkeyutl to encapsulate and decapsulate. The transcripts below come from a 3.6 build on macOS; the commands have not changed since 3.5.
-
$ openssl list -kem-algorithms | grep -i kem SecP384r1MLKEM1024 @ default { 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 $ openssl genpkey -algorithm ML-KEM-768 -out mlkem768.pem $ openssl pkey -in mlkem768.pem -pubout -out mlkem768_pub.pem $ openssl pkeyutl -encap -inkey mlkem768_pub.pem -pubin -out ct.bin -secret ss_enc.bin $ openssl pkeyutl -decap -inkey mlkem768.pem -in ct.bin -secret ss_dec.bin $ wc -c ct.bin ss_enc.bin ss_dec.bin 1088 ct.bin 32 ss_enc.bin 32 ss_dec.bin $ cmp ss_enc.bin ss_dec.bin && echo "shared secrets match" shared secrets matchThe ciphertext is exactly the 1088 bytes FIPS 203 specifies for ML-KEM-768, and both sides hold the same 32-byte secret.
openssl pkey -textshows that the private key file stores the 64-byte seed from which the whole expanded key is re-derived. -
$ openssl genpkey -algorithm ML-DSA-65 -out mldsa65.pem $ openssl pkey -in mldsa65.pem -pubout -out mldsa65_pub.pem $ printf 'hello, post-quantum world\n' > msg.txt $ openssl pkeyutl -sign -inkey mldsa65.pem -in msg.txt -out msg.sig $ wc -c msg.sig 3309 msg.sig $ openssl pkeyutl -verify -pubin -inkey mldsa65_pub.pem -in msg.txt -sigfile msg.sig Signature Verified SuccessfullyThere is no
-digestoption: ML-DSA signs the message itself ("pure" mode), and since 3.5pkeyutlno longer needs-rawinfor key types that have no prehash variant. SwapML-DSA-65forSLH-DSA-SHA2-128sand the signature grows to 7856 bytes. -
$ openssl s_client -connect cloudflare.com:443 -groups X25519MLKEM768 </dev/null 2>/dev/null \ | grep -E "Negotiated|Cipher is|^Protocol" Negotiated TLS1.3 group: X25519MLKEM768 New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 Protocol: TLSv1.3Without
-groupsthe 3.5 default list already offersX25519MLKEM768first, so this is what a stock client negotiates against a server that supports it. Against a server that does not, the same command prints a classical group instead, and pinning-groups X25519MLKEM768alone makes it fail, which is a quick way to audit your own endpoints.
What to do about it
- Inventory first. Find where RSA and elliptic curves are used and which data must stay confidential past 2035; recorded session traffic is the urgent case.
- Key exchange now. For TLS and SSH this is usually a library upgrade rather than a redesign. Verify with
openssl s_clientas above orssh -Q kex. - Symmetric keys are fine. Prefer AES-256 where it costs nothing; otherwise leave them alone.
- Signatures on a schedule. Long-lived roots of trust, secure boot and firmware are the place to start, with SLH-DSA or the stateful hash-based schemes of SP 800-208; the web PKI is still working out how to carry the extra kilobytes.
- Design for agility. Code that assumes a public key is 32 bytes, a signature fits in 72, or a ClientHello fits in one packet breaks on contact with these algorithms.
WARNING
Use a maintained library (OpenSSL 3.5+, BoringSSL, Go's standard library, liboqs for experiments) rather than reference code from a paper: the 2023 KyberSlash timing bugs were in reference implementations, and decapsulation must be constant-time for implicit rejection to mean anything. And deploy the hybrid, not bare ML-KEM; the point of the construction is that you never have to guess which half fails first.
Further reading
- NIST, FIPS 203, Module-Lattice-Based Key-Encapsulation Mechanism Standard, FIPS 204, Module-Lattice-Based Digital Signature Standard and FIPS 205, Stateless Hash-Based Digital Signature Standard, the source of every size in this post.
- NIST, IR 8547 (draft), Transition to Post-Quantum Cryptography Standards, the 2030 and 2035 timeline, and the PQC project FAQ on why Grover matters less than it looks.
- IETF, draft-ietf-tls-ecdhe-mlkem, which defines X25519MLKEM768, and draft-ietf-tls-hybrid-design, the general framework.
- OpenSSL, 3.5 final release announcement and the openssl-pkeyutl(1) manual for the
-encapand-decapoptions. - Craig Gidney, How to factor 2048 bit RSA integers with less than a million noisy qubits, 2025.
- Peter Shor, Polynomial-Time Algorithms for Prime Factorization and Discrete Logarithms on a Quantum Computer, and Lov Grover, A fast quantum mechanical algorithm for database search.
- Cloudflare, The state of the post-quantum Internet, and OpenSSH, Post-quantum cryptography in OpenSSH.