PqChain Validator¶
Reports the post-quantum posture of every certificate in the presented chain. During the staged PQ migration the leaf, intermediates, and root rotate independently, so a single yes/no for the whole chain hides the information operators actually need. This validator gives a per-certificate view plus a role-level summary.
A certificate counts as PQ when either its public key algorithm or its signature algorithm is post-quantum (standalone ML-DSA/SLH-DSA or a composite). The signature is the issuing CA's choice rather than the operator's, so both are tracked separately per link.
By default is_valid: true means the leaf certificate's key is
post-quantum (the part the operator controls). Pass
require_full_chain: true via validator args to require that every presented certificate has a PQ key or a PQ signature. This does not require both on each certificate or verify the signatures.
The server may omit the root
This report covers the presented chain, not a path built against a trust store. A missing root produces root_pq: null. A classical root, when present, is a separate migration finding; it does not change the default leaf-key policy.
Opt-in¶
Registered but disabled by default (not in DEFAULT_VALIDATORS):
from certmonitor import CertMonitor
with CertMonitor("example.com", enabled_validators=["pq_chain"]) as m:
print(m.validate()["pq_chain"])
# strict mode:
# m.validate(validator_args={"pq_chain": {"require_full_chain": True}})
Chain retrieval uses the same APIs and fallbacks as the Chain validator. If the server's chain cannot be retrieved or parsed, you get a structured error instead of the report below. Missing issuers are not downloaded.
How it decides¶
Each link is classified independently, then the verdict keys off the leaf
key by default (or the whole chain with require_full_chain).
flowchart TD
A[validate called] --> B{Chain available?}
B -- No --> Z["structured error"]
B -- Yes --> C[For each certificate:<br/>is_pq = key_is_pq OR signature_is_pq]
C --> D[Summarize by role:<br/>leaf_pq / intermediate_pq / root_pq]
D --> E{require_full_chain?}
E -- "false (default)" --> F{Leaf key<br/>post-quantum?}
F -- Yes --> G["is_valid: true"]
F -- No --> H["is_valid: false"]
E -- true --> I{Every certificate<br/>is_pq?}
I -- Yes --> G
I -- No --> H
Example output¶
A post-quantum leaf on a classical chain (one possible migration shape):
These examples show selected fields from illustrative scans. validate() also adds status and code, described in the result contract.
{
"chain_length": 3,
"certs": [
{"position": 0, "role": "leaf", "subject": {"commonName": "example.com"}, "key_algorithm": "ml-dsa-65",
"key_is_pq": true, "signature_algorithm_oid": "1.2.840.113549.1.1.11",
"signature_is_pq": false, "is_pq": true},
{"position": 1, "role": "intermediate", "subject": {"commonName": "Intermediate CA"}, "key_algorithm": "rsaEncryption",
"key_is_pq": false, "signature_is_pq": false, "is_pq": false},
{"position": 2, "role": "root", "subject": {"commonName": "Root CA"}, "key_algorithm": "rsaEncryption",
"key_is_pq": false, "signature_is_pq": false, "is_pq": false}
],
"summary": {"leaf_pq": true, "intermediate_pq": false, "root_pq": false},
"is_valid": true
}
summary values are null for roles with no certificate in the chain
(e.g. a single self-signed cert has no intermediates).
Reference¶
certmonitor.validators.pq_chain.PqChainValidator ¶
Bases: BaseCertValidator
Report the post-quantum posture of every certificate in the chain.
During the staged PQ migration, the leaf, intermediates, and root rotate independently, a post-quantum leaf will routinely chain up to classical intermediates and roots for years. This validator walks the chain the server presented and reports, per certificate, whether the public key and the signature use post-quantum algorithms, plus a role-level summary.
A certificate counts as PQ when either its key algorithm or its signature algorithm is post-quantum (standalone or composite), both are meaningful migration signals, and the signature is the issuing CA's choice rather than the operator's.
By default is_valid is True when the leaf certificate's key
is post-quantum, the part the operator controls. Pass
require_full_chain=True to demand that every certificate in the
chain is PQ.
Note: chains that terminate at public trust anchors will report a classical root for the foreseeable future. This is expected, not a bug, root CAs migrate last.
Opt-in: registered in VALIDATORS but not in
DEFAULT_VALIDATORS. Chain retrieval requires Python 3.10+ (same
constraint as the chain validator); on older interpreters this
validator reports a structured error.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The name of the validator. |
validate ¶
validate(cert: dict[str, Any], host: str, port: int, *, require_full_chain: bool = False) -> PqChainResult
Walk the presented chain and report per-certificate PQ posture.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cert
|
dict[str, Any]
|
The cert data dict built by |
required |
host
|
str
|
The hostname (unused; dispatcher compatibility). |
required |
port
|
int
|
The port (unused; dispatcher compatibility). |
required |
require_full_chain
|
bool
|
When |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
dict |
PqChainResult
|
|
PqChainResult
|
entry is `{position, role, subject, key_algorithm, key_is_pq, |
|
PqChainResult
|
signature_algorithm_oid, signature_is_pq, is_pq}` and the |
|
PqChainResult
|
summary is |
|
PqChainResult
|
( |
Examples:
Example output (post-quantum leaf on a classical chain):
{
"chain_length": 3,
"certs": [
{"position": 0, "role": "leaf", "key_algorithm": "ml-dsa-65", "key_is_pq": true, "is_pq": true},
{"position": 1, "role": "intermediate", "key_algorithm": "rsaEncryption", "key_is_pq": false, "is_pq": false},
{"position": 2, "role": "root", "key_algorithm": "rsaEncryption", "key_is_pq": false, "is_pq": false}
],
"summary": {"leaf_pq": true, "intermediate_pq": false, "root_pq": false},
"is_valid": true
}
subject, signature_algorithm_oid, and
signature_is_pq.)
Source code in certmonitor/validators/pq_chain.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | |