Measurement · August 1, 2026
Hybrid certificates, weighed
Three shapes of post-quantum certificate, eleven client stacks, 88 measured cells. The design that works on every stack works because no deployed verifier checks its post-quantum half.
Someone scanned 32,011 domains this June and found that 49.3% of them already negotiate hybrid post-quantum key exchange. The same scan found hybrid post-quantum certificates at 0%. Not low single digits. Zero.
That gap is the whole story. Key exchange migrated quickly because it is private: two endpoints agree on an algorithm, and nobody else has to know. Certificates are the opposite kind of problem. A certificate has to be read and understood by every client that will ever meet it, most of which you do not control and cannot upgrade. The industry’s answer is the hybrid certificate: carry classical and post-quantum material together, so old software keeps working while new software gets the post-quantum guarantee.
Vendors started shipping that answer this summer. Nobody published measurements. So I built the certificates and ran them at eleven client stacks in a home lab over two evenings: eight chain shapes, real TLS 1.3 handshakes rather than offline parsing alone, 88 cells, every one backed by captured output.
Hybrid certificates do work. The uncomfortable part is why. The shape that works on every stack works because no deployed client checks the half you are paying for.
Three shapes
Pure post-quantum drops the classical algorithm entirely: an ML-DSA key signed by an ML-DSA issuer. Honest, and it fails loudly on anything that does not support it.
Catalyst keeps an ordinary classical certificate and hangs the post-quantum material off it in three non-critical X.509 extensions (subjectAltPublicKeyInfo, altSignatureAlgorithm, altSignatureValue, from ITU-T X.509 section 9.8). Non-critical has a precise meaning in RFC 5280: a verifier that does not recognize the extension is required to ignore it. That requirement is the compatibility mechanism, and it is advertised as the feature.
Composite puts both algorithms under a single identifier: one key, one signature, both halves inside, structurally bound. You cannot ignore half of a composite signature, because there is no half to ignore.
What eleven stacks actually did
Two 2026 papers stop just short of this measurement. Lee et al. tested certificate-path verification with a single live handshake demo and name rustls as future work. Delgado Jiménez measured placement and cost thoroughly on one implementation stack and writes that repeating the analysis across other TLS libraries would strengthen confidence in the results. This is that gap, filled.
Counts below are clients that passed, out of clients where the test applies. The fleet: OpenSSL 3.0.13 and 3.5.5, GnuTLS 3.8.3, Go 1.26.0, Java 21, .NET 8, Python 3.13, Node 22, rustls 0.23.43, Bouncy Castle 1.82, and Windows schannel on 25H2 build 26200. There is no parsing column because it would read 11 of 11 the whole way down, which is the first finding rather than a detail.
| Chain shape | Validates offline | Completes TLS 1.3 handshake |
|---|---|---|
| ECDSA P-256 control | 10 of 10 | 10 of 10 |
| Pure ML-DSA 44/65/87 | 4 of 10 | 3 of 10 |
| Mixed (classical root, ML-DSA below) | 4 of 10 | 3 of 10 |
| SLH-DSA root, ML-DSA below | 3 of 10 | 3 of 10 |
| Catalyst hybrid | 10 of 10 | 10 of 10 |
| Composite | 2 of 10 | not testable |
Four things in that table are worth more than the rest.
Parsing never fails. All 88 cells parse, composite included. Post-quantum certificates break validation, not parsers. Any inventory tool that reports “the certificate parses fine” will green-light a chain that nothing in your fleet can actually use.
The runtime decides, not the operating system. Node 22 (bundled OpenSSL 3.5.7) and Python 3.13 (linked against 3.5.6) validate and handshake every ML-DSA chain on the same Ubuntu host where the system OpenSSL 3.0.13 and GnuTLS fail all of them. Same machine, same certificates, opposite answers. Your post-quantum readiness is a property of each application’s crypto library, so an OS-level inventory will tell you almost nothing.
Catalyst passes everything. Every client, every test that applies: parse, offline validation, completed TLS 1.3 handshake. The industry’s core compatibility claim is now measured rather than asserted.
Composite’s two passes are not equal. Bouncy Castle minted the certificate and verifies it properly, which I confirmed with a negative control: flip one bit in the signature and it rejects with a real cryptographic error. Go’s pass is an artifact. Go does not validate signatures on a self-signed certificate already sitting in the root pool, so it reports verify OK on a certificate whose algorithms it simultaneously reports as unknown. The real composite finding is that this is not an absence of support, it is that the competing OID families do not cross-verify. Bouncy Castle 1.82 mints draft-07 identifiers while the IETF draft has moved to -19.
The measurement that should change your mind
Catalyst works everywhere because non-critical means ignored, and ignored means unchecked. That sounds like a compatibility story until you ask what happens to a certificate whose post-quantum half is wrong.
Lee et al. asked. Nine standard verifiers, including OpenSSL, Go, GnuTLS, NSS, Java, and Windows CryptoAPI, accept a Catalyst certificate whose ML-DSA alternative signature is forged or replaced with garbage. Bouncy Castle accepts it on the default JCA path and rejects it only through an opt-in API. wolfSSL, the one stack that checks an alternative signature when present, cannot be told to require one, so deleting the extensions is enough.
Put that next to my results and the honest conclusion is sharper than either half alone. The one hybrid design that works on every stack is the one whose post-quantum half no deployed verifier checks. An attacker who eventually breaks the classical half does not have to defeat the ML-DSA signature. They delete it, and every verifier in that list keeps saying yes. Catalyst’s compatibility and its inability to protect you are not two properties in tension. They are one property, described twice.
Size is not the deciding factor
The three shapes land within 145 bytes of each other: a pure ML-DSA-65 leaf at 5,591 bytes, composite at 5,628, catalyst at 5,736. Lee et al. measured five strategies clustering inside a 4.4% spread, for the same reason, and their single-certificate numbers sit within fifteen bytes of mine across four algorithms (ML-DSA-44 at 3,981 against my 3,992, ML-DSA-65 at 5,510 against 5,521, ML-DSA-87 at 7,468 against 7,479, SLH-DSA-128s at 8,129 against 8,144). Two labs, different tooling, two weeks apart, same answer. Treat that as cross-validation rather than as either lab’s discovery.
So the certificate format is not where the bytes go. The chain is. What a TLS server transmits, intermediate plus leaf: 900 bytes for the ECDSA control, 11,156 for ML-DSA-65, 15,072 for ML-DSA-87, and 15,703 for the SLH-DSA-root chain, which is the most expensive thing I measured.
That last one is a trap worth naming, because “SLH-DSA for roots” is common advice and the reasoning behind it is sound: roots are long-lived, rarely rotated, and conservative hash-based security is a good fit. The root certificate itself is also never transmitted during a handshake, which makes the choice look free. It is not. The root’s 7,856-byte signature sits on the intermediate certificate, and the intermediate is sent. Delgado Jiménez measured the same effect across hierarchy depths: 39,962 bytes read at depth 2 falling to 28,947 at depth 3, against a 16,008-byte all-ML-DSA baseline.
The same study is emphatic about where SLH-DSA must never go. In the leaf, a handshake takes 1,414 ms under hybrid key exchange, roughly 1,682 times the ML-DSA-leaf baseline, and holding the same throughput would need about 2,500 times the server CPU. Root, penalized but plausible. Leaf, a self-inflicted denial of service.
Windows gives both answers at once
The clearest illustration of all of this sits on one machine. Windows CNG, the certificate stack, offline-validates ML-DSA-44, 65, and 87 chains: X509Chain.Build returns true with only an UntrustedRoot flag, which is expected for a privately-rooted lab chain. Windows schannel, the TLS stack, cannot connect to any of them. AuthenticateAsClient throws SSPI 0x80090326, SEC_E_ILLEGAL_MESSAGE.
I isolated the mechanism rather than guessing at it. schannel’s ClientHello advertises twelve signature schemes, all classical, with no ML-DSA among them. A server holding only an ML-DSA credential therefore has nothing it can sign with, and aborts with handshake_failure(40) immediately after the ClientHello. No ServerHello, no Certificate, no CertificateVerify: the failure happens at negotiation, before any certificate crosses the wire, and schannel reports that alert as a malformed message. The result replicates against two independent server implementations, OpenSSL 3.5.5 and Bouncy Castle’s bctls 1.82, and the decoded wire trace shows the whole sequence in plaintext.
None of that contradicts Microsoft. Their documented scope puts schannel’s post-quantum work at ML-KEM key exchange while ML-DSA lives in CNG and AD CS, which is exactly the split I measured. This is a demonstration of a documented boundary, not a discovery. The operational consequence is what matters: on a single box, certificate validation reports that ML-DSA is fine while TLS refuses to connect at all. An inventory built on certificate validation will approve a rollout that dies in the handshake.
And on that same stack, catalyst is the only post-quantum-bearing chain that completes a real TLS 1.3 handshake, because schannel sees an ordinary ECDSA certificate and ignores everything else. The strongest operational argument for catalyst certificates and the reason not to trust them are the same measurement.
The errors are worse than the failures
Every failure above has one root cause: the client does not support the algorithm. Almost none of them say so.
Go reports certificate signed by unknown authority, which sends an operator to audit trust stores that are working correctly. GnuTLS says certificate is NOT trusted. OpenSSL at least gestures at the key with an X509_PUBKEY_get0 decode error. Java 21 produces the most dangerous one in the fleet: CertPathValidatorException: signature check failed, which is the exact wording it uses for a tampered certificate. An unsupported algorithm looks like an attack in your logs.
Only rustls writes an error worth reading. It names the rejected algorithm by OID and then enumerates every algorithm it does support, which turns a debugging session into a glance. That should be the reference every other stack is measured against.
Worst of all is a success message. OpenSSL 3.0’s s_client prints Verify return code: 0 (ok) after a handshake that died at alert 40 with no certificate ever exchanged. Any health check grepping for that line passes on a completely dead connection.
What I would actually do
Test handshakes, not parsers. Every certificate in this study parsed on every stack, so a parse-based inventory has a 100% false-pass rate on exactly the chains you care about.
Inventory runtimes rather than operating systems, because the bundled crypto in each application decides the outcome and two services on one host will disagree.
If you deploy catalyst certificates, deploy them as a transition device and write down that the post-quantum half is decorative until verifiers enforce it. It buys compatibility today, which is a real thing to want. It does not buy post-quantum authentication, and it should not appear in a risk register as a control.
If you want the binding to actually hold, composite is the only shape measured to enforce it, and you have to pick the OID family your verifiers already speak. Anything minted today is a bet on an identifier rather than on a stable format.
Keep SLH-DSA at the root, never at the leaf, and count the intermediate bytes before you decide the root is free.
Provenance
The matrix is Verified: measured on one machine on 2026-07-31 and 2026-08-01, WSL2 Ubuntu for the Linux stacks and Windows 11 25H2 build 26200 for schannel, with every cell backed by a script and its captured output. Client versions are listed above. Everything is now published: scripts, raw evidence, and the transport measurements are at github.com/DannyBCarrJr/pqc-cert-matrix, MIT licensed. Rerun any cell yourself and tell me if it disagrees.
That repository also carries a PRIOR-ART.md recording, per finding, what published work already covers. It is worth reading before quoting anything here as new, because it has demoted several of this project’s claims, including two of the transport results measured after this article went up.
Claims about forged and stripped alternative signatures, and the 4.4% size spread, are Reported from Lee et al., read on 2026-07-31. Placement, latency, and byte-count figures are Reported from Delgado Jiménez, read on 2026-07-31 with the specific numbers re-checked against the paper on 2026-08-01. The adoption statistics are Reported from Dubey and Varshney.
Two corrections worth stating, since both were mine. My working notes first claimed composite “validates nowhere”, which was wrong: it validates correctly inside its own OID family, and the real finding is the cross-family gap. I also misidentified the Windows error constant as SEC_E_MESSAGE_ALTERED and described schannel as rejecting an ML-DSA CertificateVerify message that, as the wire trace later showed, no server ever sent. Both are corrected above and neither claim was published before now.
Limitations, stated rather than buried: this is one machine, so nothing here speaks to performance at scale. Apple’s stacks, NSS and Firefox, embedded stacks including mbedTLS and wolfSSL, and hardware key storage are all untested. The schannel result reflects what the default SslStream profile requested on build 26200; a different schannel consumer or policy configuration could advertise different signature schemes, and that would change the row.
If I have something wrong here, email the studio and I will correct it in place with a note saying what changed.
Sources
- Beyond Size: Do Hybrid PQC Certificates Actually Enforce the Classical-PQC Binding? A Cost-and-Security Study, Minwoo Lee, Minjoo Sim, Siwoo Eum, Subeen Cho, Yulim Hyoung and Hwajeong Seo, Hansung University; IACR ePrint 2026/1416, 2026-07-16
- Signature Placement in Post-Quantum TLS Certificate Hierarchies: An Experimental Study of ML-DSA and SLH-DSA in TLS 1.3 Authentication, José Luis Delgado Jiménez, arXiv:2604.06100v3, revised 2026-05-19
- Measurement Study of Post-Quantum Readiness of Internet: 2026, Vanishka Mohan Dubey and Gaurav Varshney, arXiv:2606.16473, 2026-06-15
- Microsoft, KB5101650, for schannel’s documented post-quantum scope (ML-KEM key exchange, TLS 1.3, off by default) against ML-DSA in CNG and AD CS