Post-Quantum Crypto in 2026: Migrate to ML-KEM and ML-DSA Before It’s an Emergency

NIST published FIPS 203 and FIPS 204 in August 2024. That was almost two years ago. If you’re reading this and still running pure RSA-2048 or ECDH on anything that stores data for more than five years — you have a problem that compound interest is already worsening.

This isn’t FUD. The threat model here is concrete and doesn’t require a cryptographically relevant quantum computer to exist today. Adversaries with long-term archive programs — nation-state actors being the obvious ones — are capturing encrypted traffic right now and will decrypt it the moment hardware catches up. That’s the "harvest now, decrypt later" (HNDL) attack, and it’s been operational doctrine for years.

So let’s talk about what the standards actually say, what the libraries look like in 2026, where the real migration pain is, and how to prioritize without burning your whole engineering quarter on it.


What Got Standardized and Why It Matters

NIST’s post-quantum cryptography project ran from 2016 to 2024. Three algorithms made it to full standards:

FIPS 203 — ML-KEM (Module-Lattice Key Encapsulation Mechanism), formerly CRYSTALS-Kyber. This is your RSA/ECDH replacement for key exchange and key transport. It does not sign — it encapsulates a shared secret.

FIPS 204 — ML-DSA (Module-Lattice Digital Signature Algorithm), formerly CRYSTALS-Dilithium. This replaces RSA-PSS/ECDSA for signatures. Code signing, TLS certificates, JWTs with asymmetric signing — this is your target.

FIPS 205 — SLH-DSA (Stateless Hash-Based Digital Signature), formerly SPHINCS+. A conservative fallback for signatures. Hash-based, no lattice assumptions. Slower and produces larger signatures but the security assumptions are ancient and battle-tested. Good for root CAs and firmware signing where performance is secondary.

There is no standardized post-quantum symmetric replacement needed yet — AES-256 and SHA-384+ are considered quantum-resistant under Grover’s algorithm (it halves the effective security bits, so AES-256 becomes 128-bit quantum-secure, which is still fine).

The Math in 30 Seconds

Classical key exchange (RSA, ECDH) is broken by Shor’s algorithm running on a large enough quantum computer. Shor factors integers and computes discrete logarithms in polynomial time. ML-KEM is based on the hardness of the Module Learning With Errors (MLWE) problem — no known quantum algorithm solves this efficiently. Same family of problems underlies ML-DSA.


Key Sizes and Performance Reality Check

Before you copy-paste configs, understand what you’re trading.

Algorithm Public Key Ciphertext / Signature Security Level
RSA-2048 256 bytes 256 bytes classical 112-bit
X25519 32 bytes 32 bytes classical 128-bit
ML-KEM-768 1,184 bytes 1,088 bytes NIST Level 3
ML-KEM-1024 1,568 bytes 1,568 bytes NIST Level 5
ECDSA P-256 64 bytes ~72 bytes classical 128-bit
ML-DSA-65 1,952 bytes 3,293 bytes NIST Level 3
ML-DSA-87 2,592 bytes 4,595 bytes NIST Level 5

The sizes jump hard. That 3.3KB ML-DSA-65 signature isn’t free when you’re embedding signatures in JWTs, TLS handshakes, or firmware images. Plan for it.

Performance is better than the size growth suggests. ML-KEM keygen and encapsulation are fast — faster than RSA, competitive with X25519 on modern CPUs. ML-DSA signing is slower than ECDSA but acceptable for most workloads.


The Migration Priority Framework

Don’t try to migrate everything simultaneously. Triage by threat exposure:

Critical — migrate now:

  • Long-lived encrypted data (encrypted backups, database encryption keys, archived communications). These are the primary HNDL targets. If your backup encryption key is RSA-2048 and someone captured your encrypted blob two years ago, they’re waiting on hardware, not on you.
  • Internode key exchange for any infrastructure that moves sensitive data between services.
  • Certificate authorities — your root CA signing with ECDSA-256 is fine for now but start your hybrid CA infrastructure today. CA cert lifetimes are long.

High — migrate within 6 months:

  • TLS configuration for public-facing services. Chrome has supported X25519MLKEM768 (a hybrid of X25519 and ML-KEM-768) since version 131. Cloudflare has offered it since 2023. Your reverse proxy should negotiate it.
  • SSH host and user keys for privileged access.
  • Code signing for software you distribute to customers.

Medium — plan but don’t panic:

  • Internal API authentication tokens. JWTs signed with ES256 don’t have the HNDL problem — a signature proves integrity at the time of issuance, and a captured signed token is useless after expiry. Migrate here, but it’s not an emergency.
  • Short-lived session-based systems. If your session key lives 15 minutes, HNDL isn’t the primary threat vector.

Low — watch but hold:

  • HMAC-based MACs. HMAC-SHA256 with a 256-bit key is already quantum-resistant.
  • Symmetric encryption of ephemeral data.

Hybrid Mode: The Right Default in 2026

Pure ML-KEM/ML-DSA in production is still uncomfortable for one reason: these algorithms are young. Not untrustworthy — the math has been public and scrutinized for eight years — but not RSA-level battle-hardened. A critical implementation flaw found in a widely-deployed library could be catastrophic.

The right answer is hybrid mode: combine classical and post-quantum algorithms. A hybrid key exchange fails secure — an attacker has to break both X25519 and ML-KEM-768 to recover the session key. In practice this means:

  • TLS: Use X25519MLKEM768 named group (already in OpenSSL 3.3+ and BoringSSL)
  • SSH: Use sntrup761x25519-sha512 or wait for mlkem768x25519 which is landing in OpenSSH
  • Application-layer KEM: Concatenate the shared secrets from X25519 and ML-KEM, then run both through HKDF

Hybrid costs you roughly the sum of both algorithms’ overhead. Acceptable.


Practical Implementation

OpenSSL 3.x + OQS Provider

The Open Quantum Safe project maintains liboqs and oqs-provider, an OpenSSL provider that adds PQC algorithms. As of 2026, this is the most accessible path for OpenSSL-based systems.

# Build and install liboqs
git clone --depth 1 https://github.com/open-quantum-safe/liboqs
cd liboqs && mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr/local -DBUILD_SHARED_LIBS=ON ..
make -j$(nproc) && sudo make install

# Build oqs-provider
cd ../../
git clone --depth 1 https://github.com/open-quantum-safe/oqs-provider
cd oqs-provider && mkdir build && cd build
cmake -DOPENSSL_ROOT_DIR=/usr/local/ssl ..
make -j$(nproc) && sudo make install

Configure OpenSSL to load the provider in /etc/ssl/openssl.cnf:

[openssl_init]
providers = provider_sect

[provider_sect]
default = default_sect
oqsprovider = oqs_sect

[default_sect]
activate = 1

[oqs_sect]
module = /usr/local/lib/ossl-modules/oqsprovider.so
activate = 1

Generate an ML-DSA-65 key and self-signed cert:

# ML-DSA-65 is the level-3 variant — good balance of size vs security
openssl genpkey -algorithm mldsa65 -out server_pq.key
openssl req -new -x509 -key server_pq.key -out server_pq.crt \
  -days 365 -subj "/CN=example.com"

# Verify the algorithm
openssl x509 -in server_pq.crt -text -noout | grep 'Public Key Algorithm'
# Should show: Public Key Algorithm: id-ML-DSA-65

Nginx with PQC TLS

For a hybrid TLS setup that supports both classical clients and PQC-capable clients:

server {
    listen 443 ssl;
    server_name example.com;

    # Serve hybrid cert chain — your CA needs to support this
    # In practice, use a dual-cert setup or wait for wider CA support
    ssl_certificate     /etc/ssl/certs/server_pq.crt;
    ssl_certificate_key /etc/ssl/private/server_pq.key;

    # Prioritize hybrid PQC groups; fall back to classical
    # X25519MLKEM768 is the IETF draft hybrid, widely supported
    ssl_ecdh_curve X25519MLKEM768:X25519:P-256;

    ssl_protocols TLSv1.3;
    ssl_prefer_server_ciphers off;
}

Gotcha: nginx must be compiled against your OQS-enabled OpenSSL build. A system nginx linked against system OpenSSL won’t expose PQC groups. Use a Docker build or recompile.

Docker Compose: PQC-enabled Nginx

# docker-compose.yml
services:
  nginx-pqc:
    build:
      context: ./nginx-pqc
      dockerfile: Dockerfile
    ports:
      - "443:443"
    volumes:
      - ./certs:/etc/ssl/certs:ro
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

  # Test client — verify PQC negotiation actually happened
  test-client:
    image: openquantumsafe/curl
    command: >
      curl -v --curves X25519MLKEM768
      https://nginx-pqc/healthz
    depends_on:
      - nginx-pqc
# nginx-pqc/Dockerfile
FROM openquantumsafe/nginx:latest
# The OQS project maintains pre-built nginx images with liboqs baked in
# Use these for testing; for production, build your own from source
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 443

Go: ML-KEM in Application Code

Go’s golang.org/x/crypto package added mlkem in late 2024. It’s clean and idiomatic:

package main

import (
    "crypto/ecdh"
    "crypto/rand"
    "crypto/sha256"
    "fmt"

    "golang.org/x/crypto/mlkem"
)

// HybridKEM combines X25519 and ML-KEM-768 shared secrets.
// Breaking this requires breaking both simultaneously.
func HybridEncapsulate(peerX25519 *ecdh.PublicKey, peerMLKEM *mlkem.EncapsulationKey768) (
    ciphertext []byte, sharedSecret []byte, err error,
) {
    // Classical leg
    ephX25519, err := ecdh.X25519().GenerateKey(rand.Reader)
    if err != nil {
        return nil, nil, err
    }
    classicalSecret, err := ephX25519.ECDH(peerX25519)
    if err != nil {
        return nil, nil, err
    }

    // PQC leg
    mlkemCT, mlkemSecret, err := peerMLKEM.Encapsulate()
    if err != nil {
        return nil, nil, err
    }

    // Combine: H(classical || pqc || ephX25519_pub || mlkemCT)
    h := sha256.New()
    h.Write(classicalSecret)
    h.Write(mlkemSecret)
    h.Write(ephX25519.PublicKey().Bytes())
    h.Write(mlkemCT)
    combined := h.Sum(nil)

    ct := append(ephX25519.PublicKey().Bytes(), mlkemCT...)
    return ct, combined, nil
}

func main() {
    // Recipient generates long-term ML-KEM keypair
    dk, err := mlkem.GenerateKey768(rand.Reader)
    if err != nil {
        panic(err)
    }

    ek := dk.EncapsulationKey()
    fmt.Printf("ML-KEM-768 public key: %d bytes\n", len(ek.Bytes()))
    // → ML-KEM-768 public key: 1184 bytes
}

Gotcha: mlkem.GenerateKey768 uses the decapsulation key struct, not a separate private key type. The API is slightly different from what you’d expect coming from RSA/ECDH. Read the godoc before writing wrappers.

Checking What Your TLS Stack Actually Negotiates

Don’t assume — verify:

# Check which named group was negotiated
openssl s_client -connect example.com:443 \
  -groups X25519MLKEM768:X25519:P-256 2>&1 | grep -E "Server Temp Key|groups"

# With curl (if compiled with OQS OpenSSL)
curl -v --curves X25519MLKEM768 https://example.com 2>&1 | grep "SSL connection"

# Wireshark filter to spot hybrid key exchange in captures
# Look for ClientHello extension type 0x0033 (supported_groups)
# and group ID 0x11EC (X25519MLKEM768)

PKI and Certificate Migration — the Slow Part

This is where the real complexity lives. Your TLS leaf certs can be swapped in a day. Your CA chain is another story.

Let’s browsers accept self-signed or privately-signed ML-DSA certs today? Yes, technically. Will they trust them? Only if the root is in the browser trust store. The major trust stores (Mozilla, Google, Apple, Microsoft) are working through adding PQC-capable roots from commercial CAs, but the process is slow.

The practical 2026 state:

  • Cloudflare, DigiCert, and Let’s Encrypt are in various stages of PQC certificate support. Let’s Encrypt has signaled ML-DSA support is coming but as of mid-2026 classical certificates are still the only issuable option from their ACME endpoint.
  • Internal PKI: if you run your own CA (with CFSSL, Step-CA, or Vault PKI), you can migrate to ML-DSA today. Step-CA added ML-DSA support in v0.26.

For step-ca:

# Initialize a new CA with ML-DSA-65
step ca init \
  --name "Internal PQC CA" \
  --dns "ca.internal" \
  --address ":9000" \
  --provisioner "[email protected]" \
  --key-type ML-DSA \
  --curve ML-DSA-65

# Issue a leaf cert
step ca certificate "api.internal" api.crt api.key \
  --ca-url https://ca.internal:9000 \
  --root /etc/step-ca/certs/root_ca.crt \
  --kty ML-DSA \
  --curve ML-DSA-65

Gotcha: If you switch your internal CA to pure ML-DSA, any client that doesn’t support it breaks immediately. For zero-downtime migration, run a parallel ML-DSA CA and use dual-cert issuance where the server presents both an ECDSA and an ML-DSA chain.


SSH Key Migration

OpenSSH 9.0 shipped sntrup761x25519-sha512 for key exchange (a hybrid of NTRU and X25519). This is not ML-KEM but is quantum-resistant and already widely deployed. mlkem768x25519 is in the OpenSSH development tree.

For SSH host keys and user keys, the situation is different from key exchange — these use signatures, not KEMs. OpenSSH doesn’t yet have stable ML-DSA user key support. The path:

  1. Enable [email protected] for key exchange now (it’s often already the default in OpenSSH 9.x)
  2. Keep ECDSA or Ed25519 for host/user authentication keys for now
  3. Watch for ssh-mldsa65 key type landing in OpenSSH — expected in 2026
# /etc/ssh/sshd_config
# Prioritize the PQ hybrid KEx, fall back to X25519
KexAlgorithms [email protected],curve25519-sha256

# Verify what's being used after connection
ssh -v user@host 2>&1 | grep "kex:"

Gotchas and Production Pitfalls

Larger handshakes break MTU assumptions. ML-KEM ciphertexts push TLS ClientHello messages past 1500 bytes. If you have network gear doing aggressive packet filtering or if PMTU discovery is broken somewhere in your path, you’ll see mysterious TLS failures on specific network segments. Test end-to-end, not just localhost.

Certificate pinning breaks. Any mobile app or client that pins a certificate fingerprint will break when you rotate to ML-DSA certs. You need to rotate pins before rotating certs. This is standard cert rotation hygiene but the ML-DSA key size change makes fingerprint comparison obvious — something to catch in QA.

HSM support is patchy. Hardware security modules from most vendors don’t support ML-KEM/ML-DSA as of 2026. Thales and Utimaco are working on it; AWS KMS and GCP Cloud HSM have experimental PQC endpoints. Don’t architect a system that requires PQC keys to live in an HSM right now unless you’ve confirmed support with your specific vendor and firmware version.

Algorithm confusion in hybrid systems. When you combine classical and PQC outputs, the concatenation and hashing must be deterministic and agreed upon by both sides. Rolling your own hybrid KDF is a great way to introduce subtle bugs that only fail under specific conditions. Use existing implementations: HPKE (RFC 9180) with PQC KEM, or X25519MLKEM768 as specified in the IETF draft — don’t reinvent the combining step.

liboqs is not FIPS validated. The OQS project is excellent for testing and non-FIPS environments. If you’re in a regulated environment that requires FIPS 140-3 validation for your PQC implementation, check the NIST Cryptographic Module Validation Program (CMVP) — a small number of vendors have validated PQC modules, but the list is short.


Timeline and Realistic Expectations

NIST has stated that classical RSA and ECC should be phased out by 2030 for sensitive use cases. NSA’s CNSA 2.0 suite says quantum-resistant algorithms should be the only option by 2033 for national security systems.

Your realistic target: hybrid mode everywhere by end of 2026, pure PQC where classical clients have been phased out by 2028-2029.

The one exception to "don’t rush" is anything that encrypts data with a lifetime past 2030. Encrypt that data with ML-KEM-768 today, even if the tooling is slightly awkward. The HNDL window is open right now.


Library Ecosystem Status in 2026

Language / Library ML-KEM ML-DSA Status
Go x/crypto/mlkem ML-KEM-768, ML-KEM-1024 Not yet Stable
OpenSSL 3.3+ + oqs-provider Yes Yes Stable (provider)
BoringSSL X25519MLKEM768 Partial Google-internal
Rustls + pqcrypto crate Experimental Experimental Not production
BouncyCastle (Java) Yes (1.78+) Yes (1.78+) Stable
libsodium No No Not planned
OpenSSH KEx only (NTRU) Not yet In progress

The Go story is the most coherent for application developers. The OpenSSL/OQS path is best for infrastructure components. Java/BouncyCastle is ahead of most other ecosystems.


The Actual Call to Action

Stop treating this as a future problem. The threat is active, the standards are final, the libraries are usable, and the performance cost is acceptable on any hardware built in the last three years.

Start here, this week:

  1. Audit what data you encrypt today and its expected confidentiality lifetime.
  2. Enable X25519MLKEM768 in your nginx/Caddy/HAProxy TLS config — this is a one-line change.
  3. For any new service or API you’re building, use mlkem768x25519 hybrid KEM for session keys from day one.
  4. Schedule your internal CA migration to ML-DSA for this quarter.

The cryptographic community spent eight years designing and validating these algorithms. The hard work is done. Your migration plan doesn’t need to be perfect — it needs to start.

👁 Views: 205,564 · Unique visitors: 57,721