Skip to content

Accuracy

Three checks, each against something independent of pqdoctor’s own probes.

Check Result
60 real sites vs OpenSSL 3.5.5 59 / 59 where OpenSSL gave a definite answer
100 hosts from the study vs OpenSSL 3.5.5 91 / 93 where OpenSSL gave a definite answer; both differences explained below
The fix lab: real servers with known faults 30 / 30

Ground truth is OpenSSL 3.5.5 (bundled with Node 24) connecting with only X25519MLKEM768 offered and TLS 1.3 required. A completed handshake proves the site supports hybrid post-quantum key exchange. Both tools ran back to back from the same machine on 2026-10-13.

Agreement: 59 / 59 on every site where OpenSSL gave a definite answer.

  • The one OpenSSL couldn’t decide (Netflix reset the PQ-only connection) was checked again with a normal group list. It got TLS 1.3 with plain X25519, so pqdoctor’s “Not PQ” is right there too.
  • The three “TLS 1.2 only” diagnoses (nginx.org, www.mit.edu, www.baidu.com) were confirmed separately. OpenSSL requiring TLS 1.3 got a protocol_version alert from all three.
  • Re-run later through the pqdoctor CLI (4 hosts at a time, at most 2 new hosts a second): again 59 / 59, with Netflix again undecided by OpenSSL. Cause codes: 23 pq-preferred, 22 pq, 12 no-mlkem, 3 tls12-only.

The sites included cloudflare.com, google.com, github.com, microsoft.com, apple.com, wikipedia.org, www.debian.org, kernel.org, www.gov.uk, www.irs.gov, stripe.com and slack.com.

The first run fired 7 parallel probes per site. www.debian.org rate-limited the burst, every probe timed out, and pqdoctor reported “No TLS handshake at all”. That was confidently wrong. Probes now run one at a time with a short gap, a failed probe is retried once, and when only timeouts or resets come back, the verdict is “Unreachable — possibly rate-limited”, not a diagnosis.

A seeded random sample of 100 answering hosts from the study, half of them non-PQ where possible, was checked with the same OpenSSL 3.5.5 PQ-only test.

Agreement: 91 / 93 where OpenSSL gave a definite answer. OpenSSL couldn’t decide 7: each reset the connection (ECONNRESET).

The two differences:

  • receptivity.io: OpenSSL said no, pqdoctor said pq-preferred. Re-checked on 2026-10-14: the server’s ServerHello picks X25519MLKEM768, and OpenSSL’s handshake fails later with an unrecognized_name alert. That’s an SNI or certificate problem after the key exchange, which the ground-truth script counts as “no” because it treats every alert as a failure. The key exchange is post-quantum; the ground-truth script is too coarse here.
  • kaufland.de: OpenSSL said no, pqdoctor said pq. Re-checked on 2026-10-14: the server itself flips. A direct OpenSSL PQ-only connection succeeded once. Minutes later, 8 of 8 OpenSSL attempts and 4 of 4 pqdoctor runs both said not PQ. At any one moment the two tools agree; the deployment doesn’t.

Real TLS servers (Go 1.25.4 and OpenSSL 3.5.5) were set up with known problems, checked with pqdoctor, then given the fix pqdoctor suggests and checked again.

The group strings are taken word for word from pqdoctor’s own nginx, Apache and Node snippets. They go through the same OpenSSL call (SSL_CTX_set1_groups_list) that nginx’s ssl_ecdh_curve and Apache’s Groups use.

30 / 30 checks passed: 11 key-exchange diagnoses, 11 fixes, 3 ML-DSA checks, and 5 STARTTLS and every-address checks.

Case Expected pqdoctor said Fix applied After
OpenSSL 3.5 defaults PQ PQ nothing to fix PQ
Groups pinned to X25519:P-256 Not PQ Not PQ ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1 PQ
Groups pinned to P-256:P-384 Not PQ Not PQ Groups X25519MLKEM768:X25519:prime256v1 PQ
Node with ecdhCurve X25519 Not PQ Not PQ ecdhCurve: 'X25519MLKEM768:X25519:P-256' PQ
TLS 1.2 only No TLS 1.3 No TLS 1.3 ssl_protocols TLSv1.2 TLSv1.3 + groups PQ
Go 1.25 defaults PQ PQ nothing to fix PQ
Go without ML-KEM (GODEBUG=tlsmlkem=0) Not PQ Not PQ Go 1.24+ defaults (CurvePreferences = nil) PQ
Go with CurvePreferences X25519, P-256 Not PQ Not PQ remove CurvePreferences PQ
Behind a box dropping split hellos PQ + warning PQ + warning remove the box PQ
Behind a box dropping hellos over 1,200 bytes Breaks on PQ Breaks on PQ remove the box PQ
Behind a box resetting on the PQ codepoint Breaks on PQ Breaks on PQ remove the box PQ

ML-DSA: an ECDSA-only server was reported “No ML-DSA certificate”. With an ML-DSA-44 certificate added next to it, it served ML-DSA to ML-DSA clients, and classical clients still got ECDSA. See ML-DSA readiness.

STARTTLS and every address: SMTP, IMAP and POP3 with pinned groups were each fixed by the Postfix or Dovecot list. A server without STARTTLS gave an error, not a diagnosis. localhost answering classical on 127.0.0.1 and PQ on ::1 was reported per address. See Mail.

  • The nginx, Apache, HAProxy, Postfix, Dovecot and Exim binaries. Only their group strings are validated, through OpenSSL 3.5. The servers need Linux to run.
  • Public IPv6. The lab machine has no IPv6 route, so IPv6 is proven on ::1 only.
  • Kyber-draft-only servers. No maintained TLS stack still ships X25519Kyber768Draft00 alone. Covered by unit tests only.
  • The stack guess from the Server header. The lab uses IPs, so no header is read.