Post-quantum and TLS

ML-DSA certificates: what is ready, the secure way

Your key exchange is post-quantum, and the next question from the audit is "and the certificates?". Pull up a chair: the honest answer has a working half and a waiting half, and it pays to know which is which.

The short answer

ML-DSA (FIPS 204) certificates work today in a private PKI: OpenSSL 3.5 can issue an ML-DSA-87 CA and server certificate and complete a TLS 1.3 handshake with ML-KEM-1024. No public CA issues ML-DSA certificates and browsers do not accept them, so public sites stay on ECDSA P-384 while Chrome and Let's Encrypt build Merkle Tree Certificates.

Updated Houssam Hammoudi, CTOTested with OpenSSL 3.5.8, curl 8.14.1 (alpine:3.22)

On this page
  1. What goes wrong
  2. What the docs say
  3. The secure configuration
  4. Prove it
  5. Mistakes people make
  6. Checklist

What goes wrong

A TLS certificate proves who you are talking to at the moment you connect. A quantum computer that appears in ten years cannot go back and forge a signature on a handshake that already happened. That is why key exchange changes first and certificates second. Recorded traffic is protected by the key exchange, not by the certificate.

The mistakes come from mixing the two. Some teams wait for post-quantum certificates before they turn on ML-KEM, and leave recorded traffic exposed for years. Others buy an "ML-DSA certificate" product and expect browsers to accept it. Neither works.

There is a real job for ML-DSA today: private PKI. Internal services, machine-to-machine APIs and long-lived device identities can use ML-DSA certificates now, if every client in that PKI runs a library that understands them. The cost is size: an ML-DSA-87 certificate is about 7.5 KB, sixteen times an ECDSA P-384 one.

What the docs say

This document specifies the conventions for using FIPS 204, the Module-Lattice-Based Digital Signature Algorithm (ML-DSA) in Internet X.509 certificates and CRLs.

Source: RFC 9881, Algorithm Identifiers for ML-DSA

Chrome has no immediate plan to add traditional X.509 certificates containing post-quantum cryptography to the Chrome Root Store.

Source: Google, Cultivating a robust and efficient quantum-safe HTTPS

We are targeting late 2026 for a staging environment that issues MTCs, and 2027 for a production-ready environment.

Source: Let's Encrypt, A Post-Quantum Future for Let's Encrypt

The standards exist (FIPS 204, RFC 9881). What does not exist yet is a public trust path: no root program accepts ML-DSA certificates, and the web is moving to a different format, Merkle Tree Certificates, for that job.

The secure configuration

A private PKI with ML-DSA-87 (CNSA 2.0 names ML-DSA-87 for all classification levels). Keep the root key offline; this example shows the commands, not the custody.

bash
# Root CA: ML-DSA-87, self-signed, CA only.
openssl genpkey -algorithm ML-DSA-87 -out ca.key
openssl req -x509 -new -key ca.key -out ca.pem -days 3650 \
  -subj "/CN=Example Internal PQ Root" \
  -addext basicConstraints=critical,CA:TRUE \
  -addext keyUsage=critical,keyCertSign,cRLSign

# Server key and certificate: ML-DSA-87, TLS server auth only.
openssl genpkey -algorithm ML-DSA-87 -out srv.key
openssl req -new -key srv.key -out srv.csr -subj "/CN=pq.example.internal"
cat > srv.ext <<'EOF'
subjectAltName=DNS:pq.example.internal
keyUsage=critical,digitalSignature
extendedKeyUsage=serverAuth
EOF
openssl x509 -req -in srv.csr -CA ca.pem -CAkey ca.key -days 30 \
  -extfile srv.ext -out srv.pem

Serve it with any server built on OpenSSL 3.5 or newer. With nginx:

conf
server {
    listen 443 ssl;
    server_name pq.example.internal;
    ssl_certificate     /etc/nginx/tls/srv.pem;   # ML-DSA-87
    ssl_certificate_key /etc/nginx/tls/srv.key;
    ssl_protocols TLSv1.3;
    ssl_ecdh_curve MLKEM1024;                     # post-quantum key exchange too
}

For public hostnames, keep ECDSA P-384 (see the cert-manager page) and ML-KEM key exchange.

Prove it

The test in secure-tests/ml-dsa-certificates/run.sh issues the chain above and runs a TLS 1.3 server with it, all inside one alpine:3.22 container.

bash
openssl x509 -in srv.pem -noout -text | grep -E "Signature Algorithm|Public Key Algorithm" | sed "s/^ *//" | sort -u
text
Public Key Algorithm: ML-DSA-87
Signature Algorithm: ML-DSA-87

The size cost, against a P-384 certificate for the same name:

text
ca.pem    7514 bytes DER
srv.pem   7547 bytes DER
ec.pem     467 bytes DER

Chain validation and a full post-quantum handshake, ML-KEM-1024 key exchange and ML-DSA-87 authentication:

bash
openssl verify -CAfile ca.pem srv.pem
openssl s_client -connect 127.0.0.1:8443 -servername pq.example.internal -groups MLKEM1024 \
  -CAfile ca.pem -verify_return_error </dev/null 2>&1 \
  | grep -E "^Negotiated TLS1.3 group|Peer signature type|Verification:|^New, TLS"
text
srv.pem: OK
Peer signature type: mldsa87
Negotiated TLS1.3 group: MLKEM1024
Verification: OK
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384

curl built on OpenSSL 3.5 works too:

text
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / MLKEM1024 / id-ml-dsa-87
*  SSL certificate verify ok.

Mistakes people make

Waiting for certificates before fixing key exchange

Recorded traffic depends on the key exchange. Turn on ML-KEM now; the certificate can follow later without re-exposing anything already sent.

Deploying ML-DSA where one client cannot read it

Every client in the PKI must parse ML-DSA: browsers, Java, Go, older OpenSSL, mobile SDKs, hardware appliances. One old client means a failed handshake. Inventory the clients first and start with a service whose clients you control.

Forgetting the size

An ML-DSA-87 server certificate is about 7.5 KB, and the handshake signature that proves the key adds about 4.6 KB more. Each intermediate adds another 7.5 KB. Constrained links and UDP-based protocols feel it. Keep chains short where your custody allows it.

Buying a "post-quantum certificate" for a public site

No public root program accepts ML-DSA certificates today, so a browser will not trust one. Check the claim against the root program of the browsers you serve before you pay.

Mixing up draft names and final names

Code written for the Dilithium drafts does not interoperate with ML-DSA (FIPS 204). Use libraries that implement the final standard and RFC 9881 identifiers, such as OpenSSL 3.5.

Checklist

  • Turn on ML-KEM key exchange first; do not wait for certificates.
  • Keep public hostnames on ECDSA P-384.
  • Pick one internal service whose clients you control for an ML-DSA pilot.
  • Confirm every client library in that path supports ML-DSA (FIPS 204).
  • Issue from an ML-DSA-87 root kept offline.
  • Measure handshake size and latency on the real network path.
  • Track the Chrome and Let's Encrypt Merkle Tree Certificate timelines.

The certificates are the second act. The key exchange is on stage now, and the audience is already recording.

H2-CPQE

Learn it on a live range

Post-quantum TLS, in Edge and Post-Quantum Networking: a real host in your browser, and every objective checked on the machine.

Start free

The Dome

Want it run for you?

The Dome puts post-quantum TLS, a WAF that blocks, signed DNS and a zero-trust mesh in front of your application. Tell us what you run.

See the Dome