Document version 1.5 · 2026-08-04 · applies to the service running at pq-check.eu
pq-check's verdict answers one question: does this TLS endpoint negotiate a post-quantum key exchange? The negotiated TLS version and the graded certificate chain are reported alongside the verdict but do not affect it; section 3 explains why. This page documents how the verdict is reached, and what a scanned host sees in its logs.
1. What is tested · 2. How the probe works · 3. Verdicts & grading model · 4. Limitations · 5. Scan footprint · 6. Data handling · 7. Reproducing a result · 8. Contact
pq-check probes the three standardised hybrid ML-KEM key-exchange groups. All three combine a classical elliptic-curve exchange with ML-KEM as standardised in FIPS 203, not the pre-standard Kyber round-3 drafts.
| Group | Codepoint | KEM | Classical half | NIST level |
|---|---|---|---|---|
X25519MLKEM768 | 0x11EC (4588) | ML-KEM-768 | X25519 | 3 |
SecP256r1MLKEM768 | 0x11EB (4587) | ML-KEM-768 | NIST P-256 | 3 |
SecP384r1MLKEM1024 | 0x11ED (4589) | ML-KEM-1024 | NIST P-384 | 5 |
Codepoints are those registered for hybrid ECDHE–ML-KEM in the TLS
supported-groups registry. Superseded draft groups are not probed and do
not earn a pass. The relevant one is X25519Kyber768Draft00
(0x6399), which Chrome and Cloudflare deployed before FIPS 203 was
final; it is wire-incompatible with the standard and is being removed
from client stacks, so a server offering only it has no working PQ path
to current clients.
For each of the three groups, pq-check performs a separate TLS 1.3
handshake whose ClientHello lists that group in
supported_groups and nothing else:
Offering the groups one at a time is what makes per-group support visible: a combined ClientHello would reveal only the server's preferred group.
The pass criterion is a completed handshake. A server that answers with
HelloRetryRequest and then completes counts as supporting the group,
since that is what a real client experiences; one that returns
handshake_failure or illegal_parameter does
not.
If none of the three PQ handshakes succeed, pq-check makes one further
attempt offering only classical groups (X25519, P-256, P-384, P-521) and
allowing TLS 1.2. This separates reachable but classical-only
from unreachable: if it also fails, the target is reported as a
probe failure, never as “not PQ-ready”. A
scan costs three handshakes against a PQ-ready host and four against one
that is not; a host offering X25519MLKEM768 gets one more, the
authentication probe described below.
Post-quantum key exchange exists only in TLS 1.3; the TLS 1.2 key-exchange negotiation has no mechanism for it. If the fallback negotiates TLS 1.2 or lower, the result warns that the TLS version is the blocking issue, since enabling TLS 1.3 has to come before any group configuration.
On a host that offers X25519MLKEM768, pq-check performs one
additional handshake that lists the ML-DSA signature
algorithms (FIPS 204) a normal client does not, then reads
the certificate the server returns. A server can hold a post-quantum
certificate it serves only to clients that offer those algorithms; an
ordinary handshake never sees it, so this probe is the only way to detect
that authentication is already quantum-safe. When a post-quantum
certificate is returned, the result reports it (naming the algorithm, e.g.
ML-DSA-65). The certificate is identified by its algorithm
OID and never verified, consistent with the rest of this
page; a classical certificate here is the expected result today and is not
a finding. This handshake decrypts only the server's certificate message
and sends no application data.
The same handshake doubles as a fallback for PQ-only
servers. A server whose only certificate chain is ML-DSA (such
as mldsa.digicert.com) has no certificate it can use with a
client offering only classical signature algorithms, and refuses every
ordinary handshake — including all of pq-check's standard-library
probes — with a handshake_failure alert. When the
probes fail that way, pq-check attempts the ML-DSA-offering handshake
before declaring a probe failure: if it completes, the host is reported
PQ-ready on X25519MLKEM768 with its post-quantum
certificate, rather than as completing no TLS handshake. A PQ-only
server negotiating a group other than X25519MLKEM768
remains outside this fallback's reach and is still reported as a probe
failure.
Chain verification is disabled, so that an expired, self-signed or hostname-mismatched certificate cannot masquerade as a missing PQ group. pq-check therefore says nothing about whether a certificate is valid or trusted; use a certificate checker for that. No certificate is stored.
Every certificate the server presents is graded, not just the leaf: a leaf is only as sound as what signed it, so an intermediate still carrying a SHA-1 signature is a real finding. The public-key and signature halves are graded separately — a chain can pair a sound key with a legacy signature — and the certificate row reports the worst grade across the chain, naming the offending certificate when it is not the leaf.
| Grade | Meaning | Examples |
|---|---|---|
post-quantum |
Authentication is already quantum-safe. | ML-DSA, SLH-DSA |
classical |
Sound today, breakable by a quantum computer — what essentially every publicly trusted certificate is. | RSA ≥ 2048, ECDSA, Ed25519 with SHA-2 |
legacy |
Broken by attacks that need no quantum computer. This outranks the quantum question: the certificate should be replaced regardless. | SHA-1, MD5, RSA < 2048, DSA |
unknown |
An algorithm this build cannot name, so it is reported as ungraded rather than guessed at — a signal, not a false positive. The standardised PQ signatures are named by their OID (below), so this is reserved for something genuinely newer than the catalog. | an algorithm newer than the catalog |
The grade is reported alongside the verdict and, except as noted under verdicts & grading, does not change it.
An ML-KEM-768 key share is roughly 1.2 KB, which pushes the ClientHello past a single TCP segment. pq-check needs no special handling for this, reading the TLS record layer over an ordinary stream socket.
The target side is where it matters, and it is the main source of false
negatives. Middleboxes, load balancers and older TLS terminators that
assume a ClientHello arrives in one packet may drop or reset such
connections, which pq-check cannot distinguish from the group being
unsupported: both appear as a failed handshake. If a server you know is
configured for X25519MLKEM768 reports as not ready, check
the path for a fragmentation-intolerant middlebox.
pq-check is a single statically linked Go binary, and its probes are
entirely pure Go — no OpenSSL, forked TLS stack, or
third-party cryptographic dependency anywhere in the probe path. Three
pure-Go mechanisms do the work: a real handshake uses the Go standard
library's crypto/tls (Go 1.26), which performs the
hybrid ML-KEM groups natively; a raw-ClientHello prober on plain sockets
enumerates group support without performing any cryptography, so a
codepoint the toolchain cannot itself perform is still probed; and the
post-quantum authentication check (above) is a minimal TLS 1.3 client
built on Go's own crypto/mlkem, crypto/ecdh and
crypto/hkdf. The group list is set explicitly for every
handshake, so results do not depend on toolchain defaults and are
unaffected by GODEBUG=tlsmlkem or
GODEBUG=tlssecpmlkem in the environment.
pq-check assigns no letter grade and no score. A composite score hides which measurement drove it; this tool reports one verdict and the facts behind it.
| Verdict | Condition |
|---|---|
PQ-READY |
At least one of the three hybrid ML-KEM groups completed a handshake. |
NOT PQ-READY |
The host is reachable and completed a handshake, but none of the three groups did. |
PROBE FAILED |
No handshake completed at all — DNS failure, refused connection, timeout, or a target the policy declines to scan. No claim is made about the host's PQ posture. |
The rules behind those three lines:
SecP384r1MLKEM1024 passes exactly as one offering
X25519MLKEM768.legacy chain is still flagged, in red
rather than as a caution. That is not a post-quantum finding: SHA-1 and
RSA-1024 are attackable with today's hardware, and reporting them
quietly alongside a PQ-ready pass would understate them.pq-check treats hybrid as the correct answer for the migration period, not an interim step. Every group it tests combines ECDHE with ML-KEM, so a session stays as strong as the classical exchange if a weakness in ML-KEM emerges, and quantum-safe if the elliptic-curve half falls. Both the German BSI and the French ANSSI direct deployments toward hybrid rather than standalone ML-KEM. Pure ML-KEM groups are not offered by general-purpose TLS servers and browsers today, so pq-check does not test for them; if that changes, this page and the group table change with it.
What pq-check does not check, so that a pass is not read as more than it is:
unrecognized and
graded unknown, never guessed at.Results are provided without warranty of accuracy or completeness; see the imprint.
What a scanned host sees, in full:
X25519MLKEM768 gets one
additional handshake that offers ML-DSA signature algorithms, to check
for a post-quantum certificate. A dual-stack host whose preferred
address answers nothing sees the same pattern repeated once against
its other family's address. No session resumption, no
reconnection.If you operate a host and want it excluded from scanning, write to postmaster@pq-check.eu from a domain-associated address and it will be added to a deny-list.
One row per scan attempt, in a SQLite database on the same server: the timestamp; the requesting client's IP address; the target hostname (lower-cased, with any trailing dot removed) and port; the resolved IP address that was dialled; and the result (PQ-ready flag, groups found, TLS version, the leaf certificate's algorithm names and issuer, the chain's certificate grade, and any error text). Alongside the row, the individual measurements behind the verdict are stored as observations (each algorithm tested, which engine tested it, and the outcome), together with the catalog and grader versions that produced the result, so a past scan can be interpreted against the rules in force when it ran. None of this is data about the person scanning beyond the client IP already listed above.
A scan that never completed a handshake stores the PQ-ready flag as unknown rather than false. An unreachable host is not evidence that it lacks post-quantum support, and this service does not record it as though it were.
Target hostnames and client IP addresses are therefore logged. They are needed to investigate abuse of the service and to answer an operator asking why their host was probed. This data is used for abuse prevention and aggregate operational statistics and for nothing else. Legal basis: Art. 6(1)(f) GDPR, legitimate interest in the secure operation of the service.
No certificates, no public keys, no session keys or key shares, and no
content retrieved from the target, none of which is ever requested. No
cookies are set, no analytics or tracking of any kind runs, and the pages
load no third-party resources (the Content-Security-Policy is
default-src 'none', with everything else same-origin).
Scan records are deleted automatically 90 days after they are written. The purge runs inside the service itself, daily and at every start. Records are not shared with or transferred to any third party, and are not used to build a public database of scanned hosts.
The reverse proxy in front of the service keeps standard HTTP access logs (IP address, timestamp, request line, status) with a 14-day rotation window.
Scan results are shown only to the person who ran the scan. There is no public results feed, no shareable result URL, and no listing of recently scanned hosts.
Every verdict can be checked directly. With OpenSSL 3.5 or newer:
openssl s_client -connect example.com:443 -servername example.com \
-tls1_3 -groups X25519MLKEM768 </dev/null 2>&1 \
| grep -i "Negotiated TLS1.3 group"
A line naming the group means the server supports it, and pq-check should
report the host as PQ-ready. Repeat with SecP256r1MLKEM768
and SecP384r1MLKEM1024 for the other two. A handshake failure
for all three should correspond to a NOT PQ-READY verdict.
If pq-check disagrees with what you measure, please report it with the hostname and the time of the scan. Check first whether the mismatch is genuine: your resolver may have handed you a different endpoint behind the same name (see section 2), or a middlebox on one path may be dropping the large ClientHello.
This document is revised whenever the behaviour it describes changes. Its version and date are at the top.