Skip to content

ML-DSA readiness

Post-quantum key exchange protects today’s traffic from being recorded now and decrypted later. Authentication is the second half: the server’s certificate and its signature on the handshake. The post-quantum signature standard is ML-DSA.

Cloudflare accepts ML-DSA certificates on the hop from Cloudflare to the origin (blog, 2026-07-29). Browsers don’t take ML-DSA certificates from public CAs yet.

TLS 1.3 encrypts the Certificate message, so pqdoctor has to do one real exchange to read it. It completes an X25519 exchange and runs the TLS 1.3 key schedule (AES-128-GCM) just far enough to decrypt the Certificate and CertificateVerify, then hangs up. It never sends Finished.

It makes two such connections:

  1. Classical schemes, then ML-DSA. This is the chain today’s clients get, and it shows whether listing ML-DSA codepoints breaks anything.
  2. Only ML-DSA schemes. Does the server sign with ML-DSA when that’s all the client accepts?

The codepoints are mldsa44 0x0904, mldsa65 0x0905 and mldsa87 0x0906, per draft-ietf-tls-mldsa-06 and the IANA registry.

ml-dsa Headline Means
serves Serves an ML-DSA certificate Offered only ML-DSA, it signs with ML-DSA
no-cert No ML-DSA certificate Offered only ML-DSA, it refuses cleanly
ignores-offer No ML-DSA certificate (and the offer is ignored) Offered only ML-DSA, it still signs with a classical key, so an ML-DSA-only client would fail
rejects-codepoints Breaks on ML-DSA signature algorithms Listing ML-DSA next to classical schemes breaks the handshake. TLS requires unknown schemes to be ignored
unknown ML-DSA readiness unknown Neither probe got far enough

Serving ML-DSA needs a TLS library that has it (OpenSSL 3.5+) and an ML-DSA certificate. Configure it as a second certificate and key next to the current one. The server picks per client, so today’s clients keep the classical certificate.

Terminal window
openssl genpkey -algorithm ML-DSA-44 -out mldsa.key
# issue mldsa.crt from your CA, then in nginx add a second pair:
ssl_certificate mldsa.crt;
ssl_certificate_key mldsa.key;

Public CAs don’t issue ML-DSA certificates yet, so issue it from your own CA or an origin CA. Flarebox makes one on your machine.

An OpenSSL 3.5 server with one ECDSA P-256 certificate was reported “No ML-DSA certificate”. Given an ML-DSA-44 certificate next to the ECDSA one, it served ML-DSA to ML-DSA clients, and a classical-only client still got ecdsa_secp256r1_sha256. The chain grew from 364 to 3,919 bytes.

pqdoctor github.com (authentication)
Authentication No ML-DSA certificate
Leaf: ec-p256 key, signed ecdsa-sha256. Listing ML-DSA schemes doesn't break it, and offered only ML-DSA it refuses cleanly — what any correct TLS stack does, so this doesn't show whether its stack could serve ML-DSA. The chain is 2737 bytes.
ml-dsa: no-cert
leaf github.com ec-p256 signed ecdsa-sha256 1009 B
↑ Sectigo Public Server Authenticat… ec-p256 signed ecdsa-sha384 867 B
↑ Sectigo Public Server Authenticat… ec-p384 signed ecdsa-sha384 842 B

On by default for one host. In a scan, --auth turns it on (two more connections a host). --no-auth skips it.

SLH-DSA, composite signatures, Merkle Tree Certificates

Section titled “SLH-DSA, composite signatures, Merkle Tree Certificates”
  • SLH-DSA (FIPS 205): --slh-dsa adds a probe offering only the 12 SLH-DSA schemes IANA registered (0x0911–0x091C, draft-reddy-tls-slhdsa). signatures.slhdsa is serves, no-cert, ignores-offer or unknown. Certificate keys and signatures are named by NIST CSOR OID, e.g. slh-dsa-sha2-128s (2.16.840.1.101.3.4.3.20), and the pre-hash hash-slh-dsa-* OIDs too.
  • Composite ML-DSA + classical signatures: IANA had registered no TLS codepoints for them on 2026-10-15, so pqdoctor offers none rather than guess. signatures.composite says so.
  • Merkle Tree Certificates are experimental; pqdoctor reports them as not checked: experimental.

The chain itself is checked against Mozilla’s roots by Certificate trust. An ML-DSA or SLH-DSA chain comes back unverifiable there: no public root uses them yet.