Skip to content

JSON output

pqdoctor has three JSON shapes:

Command Output Shape
pqdoctor <host> --json One pretty-printed object Report
pqdoctor scan <file> --json One compact object per line (JSON Lines), in completion order Scan record
pqdoctor ssh <host> --json One pretty-printed object SSH report
pqdoctor quic <host> --json One pretty-printed object QuicReport (0.2.0)
pqdoctor dtls <host:port> --json One pretty-printed object DtlsReport (0.2.0)
pqdoctor diff a b --json One pretty-printed object Diff (0.2.0)

Compatibility: fields added in 0.2.0 are left out of the JSON when their check didn’t run, so a 0.1.0 consumer reading a default library report sees exactly the 0.1.0 shape. Cause codes are only ever added, never renamed.

  • Field names are camelCase. Enum values are camelCase (verdicts) or kebab-case (cause codes).
  • TLS numbers (versions, cipher suites, groups, extensions) are plain decimal integers. 772 is TLS 1.3 (0x0304). 4588 is X25519MLKEM768 (0x11EC).
  • Optional fields are null unless noted as omitted when empty.
  • Times are Unix milliseconds.
  • headline and cause are human sentences and can change between versions. Match on verdict and causeCode, which are stable.

pqdoctor <host> --json.

Field Type
host string The name checked, lowercased, without a trailing dot
port number
ip string | null The address that was probed
verdict Verdict
headline string One-line result, e.g. "Not post-quantum"
cause string The explanation sentence
causeCode Cause Stable reason code
group string | null The PQ group negotiated, e.g. "X25519MLKEM768". Only set when post-quantum
prefersPq boolean The server asked a client with only an X25519 share to retry with ML-KEM
warnings string[] E.g. split PQ hellos dropped, or PQ only when offered first
fixes Fix[] In the order to try them. Empty when post-quantum
fingerprint Fingerprint
probes ProbeResult[] All seven probes, in order
verify string An openssl s_client command to confirm by hand
checkedAt number When the check ran
elapsedMs number Time for the main address’s check
auth AuthReport The certificate check. Omitted when it didn’t run
starttls string smtp, imap, pop3; from 0.2.0 also ftp, xmpp, ldap, postgres, mysql, rdp. Omitted when not used
addresses AddressResult[] Every address, when the name has more than one. Omitted otherwise
groups GroupSurvey 0.2.0. Omitted when the survey didn’t run
trust TrustReport 0.2.0. Omitted when it didn’t run
signatures SignatureReport 0.2.0, --slh-dsa. Omitted otherwise
audit AuditReport 0.2.0, --audit. Omitted otherwise
middlebox MiddleboxReport 0.2.0, --middlebox. Omitted otherwise
quic QuicReport 0.2.0, --quic. Omitted otherwise
dtls DtlsReport 0.2.0, --dtls. Omitted otherwise
proxy string 0.2.0: the proxy used, without credentials, e.g. "socks5h://127.0.0.1:1080". Omitted without one

Under STARTTLS, the Cloudflare Tunnel fix is left out of fixes: a tunnel carries HTTP, not mail. With a proxy that resolves names (http://, socks5h://), ip is null.

Value Meaning Exit status
pq Negotiates a post-quantum group 0
pqWithWarning Post-quantum, with a warning (split PQ hellos dropped; not on every address; only a non-X25519 PQ group) 0
notPq TLS 1.3 without post-quantum 1
noTls13 TLS 1.2 only, or no TLS handshake at all 1
broken Something in the path breaks on the PQ hello 1
unreachable Couldn’t connect, or nothing answered 2

pq-preferred, pq, pq-split-dropped, pq-other-group (0.2.0), no-mlkem, pinned-nist, kyber-draft-only, tls12-only, large-hello-dropped, pq-group-rejected, no-tls, no-reply, connect-failed. Each is explained on Causes and fixes.

Field Type
title string E.g. "nginx: link against OpenSSL 3.5+ and list the PQ group"
detail string Why, and what to check
snippet string | null Config lines to paste, newline-separated
Field Type
serverHeader string | null The HTTP Server header, or a mail server’s greeting line (up to 160 characters)
stack string | null The software pqdoctor recognized: nginx, apache, caddy, haproxy, iis, envoy, litespeed, cloudflare, google, akamai, aws, vercel, netlify, github, dotnet, java, node, postfix, exim, dovecot, exchange; from 0.2.0 vsftpd, proftpd, mysql, postgres, windows (RDP)
tlsVersion number | null From the classical TLS 1.3 probe
cipher number | null Cipher suite from the same probe
extensions number[] Extension types in the server’s reply
Field Type
id string tls13Classical, pqOnly, pqPreference, kyberDraft, pqSplit, bigClassicalSplit, nistOnly. See Probes
label string E.g. "Offer only an X25519MLKEM768 key share"
reply Reply
summary string E.g. "TLS 1.3: asked to retry with X25519"
ms number Including a retry, if there was one

Tagged by kind:

kind Other fields Meaning
hello hrr (bool), version, cipher, group (number | null), extensions (number[]) A ServerHello, or a HelloRetryRequest when hrr is true. version comes from supported_versions when present
alert level, description A TLS alert, e.g. description 40 handshake_failure, 70 protocol_version
closed — The server closed the connection
reset — Connection reset
timeout — No reply within 5 s
notTls — The reply wasn’t TLS (or over 64 KiB arrived without a ServerHello)
connectFailed message Couldn’t open the connection
{ "kind": "hello", "hrr": true, "version": 772, "cipher": 4865, "group": 29, "extensions": [43, 51] }

That’s a HelloRetryRequest for TLS 1.3 (772), TLS_AES_128_GCM_SHA256 (4865 = 0x1301), asking for X25519 (29 = 0x001D).

Field Type
ip string
verdict Verdict
causeCode Cause
group string | null
headline string
unreachable boolean True when this address couldn’t be reached from here (e.g. no IPv6 route). That’s not a disagreement

The first reachable address gives the main result. The others get the key-exchange probes only.

Field Type
chain CertInfo[] Leaf first: the chain a client offering classical schemes then ML-DSA gets
chainBytes number Size of the Certificate message
scheme string | null The CertificateVerify scheme, e.g. "ecdsa_secp256r1_sha256"
mldsa string serves, no-cert, ignores-offer, rejects-codepoints, unknown. See ML-DSA readiness. From 0.2.0, a pinned-nist server that refuses the (X25519) certificate probe is unknown, not rejects-codepoints
mldsaOnlyScheme string | null The scheme used when only ML-DSA was offered, e.g. "mldsa44"
headline string E.g. "No ML-DSA certificate"
detail string
fixes Fix[]
probes [string, string][] [label, summary] for the two certificate probes
Field Type
subject string | null Common name
issuer string | null Issuer common name
key string E.g. ec-p256, rsa-2048, ed25519, ml-dsa-44; from 0.2.0 SLH-DSA by OID, e.g. slh-dsa-sha2-128s
signature string What the issuer signed it with, e.g. ecdsa-sha256, ml-dsa-44
bytes number DER size
postQuantum boolean Key or signature is ML-DSA (and from 0.2.0 SLH-DSA or ML-KEM)
notBefore, notAfter string RFC 3339, UTC. Omitted when they don’t parse

pqdoctor scan <file> --json, one per line. --out FILE always appends these records, whatever the output format on screen.

Field Type
host string The name probed. After --www-fallback, this is www.<domain>
listed string The original list entry. Omitted when it’s the same as host
port number
ip string | null
verdict string A Verdict, or error, skipped, resolved
cause string A Cause, or error, dns, cdn-<provider>, resolved
group string | null
prefersPq boolean
stack string | null
server string | null The Server header
headline string
checkedAt number
ms number 0 for error records
protocol string 0.2.0: ssh or quic for those checks (in --history). Omitted for TLS over TCP
{"host":"nginx.org","port":443,"ip":"3.136.61.53","verdict":"noTls13","cause":"tls12-only","group":null,"prefersPq":false,"stack":"nginx","server":"nginx/1.31.3","headline":"Not post-quantum: TLS 1.2 only","checkedAt":1792072811037,"ms":1479}

Scan-only values:

verdict / cause When
error / dns The name didn’t resolve
error / error The check itself failed
skipped / cdn-<provider> The address is in a --skip-ranges network, e.g. cdn-cloudflare
resolved / resolved --resolve-only: resolved and filtered, not contacted

Scans check one address per host (the first IPv4, otherwise the first address) unless --all-addresses is given, and skip the certificate check unless --auth is given.

pqdoctor ssh <host> --json.

Field Type
host, port, ip As in Report
verdict string pq, draftOnly, notPq, unreachable
headline, cause string
causeCode string ssh-mlkem, ssh-sntrup-only, ssh-draft-only, ssh-kex-config, ssh-old-openssh, ssh-no-pq, connect-failed
group string | null The first post-quantum method offered
ident object | null { line, proto, software, comments } from the server’s identification
software string E.g. "OpenSSH 10.0"
openssh [number, number] | null OpenSSH major and minor version
kex string[] Key exchange methods, in the server’s order, marker names removed
pqKex string[] The post-quantum ones
draftKex string[] Draft-only ones
cnsa boolean Offers mlkem1024nistp384-sha384
strictKex boolean Strict key exchange (the Terrapin countermeasure)
hostKey, ciphers, macs string[] Server-to-client lists
pqHostKey string[] 0.2.0: post-quantum host-key algorithms offered (draft names: mldsa, slh-dsa, sphincs, falcon). Omitted when none
warnings string[]
fixes Fix[]
verify string An ssh -vv command
checkedAt, elapsedMs number
Field Type
results object[] One per group: group (name), codepoint (number), accepted (bool), silent (bool: no answer, so unknown), summary
accepted string[] Every post-quantum group the server picked when offered alone
cnsa boolean Accepts ML-KEM-1024 (MLKEM1024 or SecP384r1MLKEM1024)
cnsaNote string The CNSA 2.0 note
Field Type
verdict string trusted, expired, not-yet-valid, name-mismatch, untrusted-root, self-signed, unverifiable
headline, detail string
problems string[] Every problem, most important first. Empty when trusted
anchor string The root’s name, when the chain validated. Omitted otherwise
roots string Which root store, e.g. "Mozilla's root store (webpki-roots, 121 roots)"
Field Type
slhdsa string serves, no-cert, ignores-offer, unknown
slhdsaScheme string E.g. "slhdsa_sha2_128s". Omitted unless it signed
slhdsaProbe string The probe’s summary
composite string "not offered: IANA has no composite signature codepoints yet"
merkleTreeCertificates string Always "not checked: experimental"
Field Type
versions object[] version ("SSL 3.0" … "TLS 1.3"), supported, cipher (the suite picked), summary
suitesUnder string The version the suites were tested under. Omitted when no TLS ≤ 1.2
suites object[] Accepted suites, strongest first: code, name, accepted, keyExchange ("X25519", "P-256", "ffdhe-2048", …)
tested number How many suites were offered
serverPreference, extendedMasterSecret, secureRenegotiation boolean Omitted when not known
findings object[] id, severity (high, medium, low), title, detail, knownIssue. See the table
note string That findings are by configuration only
Field Type
variants object[] id, label, bytes, ok, reply (Reply), summary, ms
failed string[] The ids that got no ServerHello
headline string
Field Type
host, port, ip
verdict Verdict
headline string
causeCode string quic-pq-preferred, quic-pq, quic-no-mlkem, quic-hello-rejected, quic-no-reply, quic-version, quic-skipped
group string | null
prefersPq, retry boolean
versionNegotiation string[] Versions a Version Negotiation packet offered. Omitted when none
probes object[] label, reply (Reply), summary, ms
note string E.g. why it wasn’t checked. Omitted when empty
elapsedMs number
Field Type
verdict Verdict
headline string
version string The highest negotiated, "DTLS 1.3" or "DTLS 1.2". Omitted when none
group string Omitted when unknown
probes object[] label, reply, summary, version, group, cookie (bool), ms
note string

fixed, regressed, changed, added, removed: lists of { target, kind, before, after }, where before / after are { verdict, cause, group, checkedAt } (omitted for added / removed). unchanged: a count.