Validators Overview¶
CertMonitor provides a modular validator system to check various aspects of SSL/TLS certificates and connections. Each validator returns a structured, JSON-serializable result (not a bare pass/fail), so you can drive alerts, dashboards, and policy from rich data. Validators can be enabled or disabled per call, and some accept arguments for fine-grained control.
This includes post-quantum readiness: opt-in validators report whether the TLS key exchange and the certificate's keys/signatures use post-quantum algorithms (hybrid or pure ML-KEM / ML-DSA / SLH-DSA), so you can track quantum-safe migration and harvest-now-decrypt-later exposure. See Post-Quantum Readiness below.
Registered vs. enabled¶
Two words worth keeping straight. A validator is registered when CertMonitor knows it exists (every built-in, plus any you add yourself). It's enabled when it actually runs for a given monitor. Enabled is always a subset of registered: only expiration, hostname, and root_certificate run by default, and everything else is registered but waits until you opt in.
Registered: every validator CertMonitor knows about
Enabled by default runs on every validate() call
Opt-in registered and ready, off until you enable it
The default three run out of the box. The opt-in validators are registered and ready, but stay off until you name them in enabled_validators or ENABLED_VALIDATORS. To register and enable your own custom validators, see Custom Validators.
Available validators¶
Enabled by default (expiration, hostname, root_certificate):
- Expiration: Checks if the certificate is expired or expiring soon.
- Hostname: Validates that the certificate matches the expected hostname.
- RootCertificate: Checks if the certificate is issued by a trusted root CA.
Opt-in (enable via enabled_validators=[...] or ENABLED_VALIDATORS):
- SubjectAltNames: Checks that every requested alternate hostname/IP is covered by the SANs, or the primary host when none are requested.
- KeyInfo: Validates the public key type and strength (RSA / EC / post-quantum).
- TLSVersion: Validates the negotiated TLS version.
- WeakCipher: Validates that the negotiated cipher suite is in the allowed list.
- SensitiveDate: Validates that the certificate doesn't expire on built-in or user specified sensitive dates.
- Chain: Inspects the full TLS certificate chain for structural problems (missing intermediates, out-of-order, expired members).
- PqKeyExchange: Reports PQ capability observed under a separate, unauthenticated TLS probe offer.
- PqChain: Reports the post-quantum posture of every certificate in the presented chain.
- PqSignature: Judges the leaf certificate's post-quantum posture (key and signature algorithm).
- Revocation: Checks OCSP responders and CRL distribution points for a revoked certificate, with CRL answers verified by OpenSSL.
What the output looks like¶
Validators return data, not just a boolean. Enable the ones you want and read the structured results:
from certmonitor import CertMonitor
with CertMonitor(
"example.com",
enabled_validators=["expiration", "key_info", "tls_version", "weak_cipher"],
) as monitor:
monitor.get_cert_info()
results = monitor.validate()
These examples show selected fields from illustrative scans. validate() also adds status and code, described in the result contract.
{
"expiration": {
"is_valid": true,
"days_to_expiry": 56,
"expires_on": "2026-08-08T22:14:02+00:00",
"warnings": [],
"lifetime_days": 90,
"lifetime_limit_days": 200
},
"key_info": {
"key_type": "ecPublicKey",
"key_size": 256,
"is_valid": true,
"curve": "secp256r1"
},
"tls_version": {
"is_valid": true,
"protocol_version": "TLSv1.3"
},
"weak_cipher": {
"is_valid": true,
"cipher_suite": "TLS_AES_256_GCM_SHA384"
}
}
When a check fails, the same envelope carries a human-readable reason you can surface directly in an alert:
{
"expiration": {
"is_valid": false,
"days_to_expiry": -4080,
"expires_on": "2015-04-12T23:59:59+00:00",
"warnings": ["Certificate is expired and has been expired for (-4080 days)"],
"reason": "Certificate expired 4080 days ago (expired on 2015-04-12).",
"lifetime_days": 3,
"lifetime_limit_days": 1187
}
}
Because every validator's result is a plain dict keyed by validator name, a monitoring pipeline can do if not results["expiration"]["is_valid"]: alert(results["expiration"]["reason"]) without special-casing.
Post-Quantum Readiness¶
The pq_* validators answer the questions classical TLS tooling can't. pq_key_exchange reads the negotiated TLS 1.3 group directly off the wire (the Python ssl module doesn't expose it) and reports server PQ capability under an unauthenticated probe offer, without establishing protection of the primary session:
with CertMonitor("cloudflare.com", enabled_validators=["pq_key_exchange"]) as monitor:
monitor.get_cert_info()
print(monitor.validate()["pq_key_exchange"])
{
"observation_scope": "server_capability_probe",
"handshake_completed": false,
"authenticated": false,
"endpoint": "cloudflare.com:443",
"observed_at": "2026-09-06T00:26:21.588128+00:00",
"offered_groups": [
4588,
29,
23
],
"kem_id": 4588,
"kem_name": "X25519MLKEM768",
"kem_kind": "hybrid_pq",
"is_pq": true,
"is_valid": true
}
pq_signature and pq_chain report the post-quantum posture of the leaf and the full chain as CAs roll out ML-DSA / SLH-DSA and composite certificates. All three are opt-in while PQC adoption ramps. See their pages for the decision flows and field-by-field output.
The result contract¶
Every validator returns a plain, JSON-serializable dict. All validators
conform to a standard envelope, declared as a TypedDict in
certmonitor.validators.results.ValidationResult so mypy can enforce it
without changing the runtime type:
| Key | Type | Rule |
|---|---|---|
is_valid |
bool |
Always present, strict bool, never None. |
reason |
str |
Present iff is_valid is False. One human-readable sentence stating the primary cause. |
status |
str |
Dispatcher state: pass, warn, fail, error, or unsupported. |
code |
str |
Stable <validator>.<status> code added by the dispatcher. |
warnings |
List[str] |
Optional. Non-fatal findings. |
error |
str |
Optional. Machine-readable error class on operational failures. |
message |
str |
Optional. Human-readable detail accompanying error. |
All other keys are validator-specific data fields: snake_case, documented on the validator's page, with behavior changes called out in the migration guide. The reserved keys above are never reused for data.
Three consequences:
- Operational failures are still results. A validator whose data source
cannot be fetched (connection error, probe failure, chain missing) still
appears in
validate()output withis_valid: Falseand areason; it is never silently omitted, soresults["<name>"]never raisesKeyErrorin a monitoring pipeline. - Schema is static-only. A custom validator declares its full shape by
extending
ValidationResultwith its data fields (seepq_signature.pyfor an example); the value your code receives is still an ordinary dict. is_validis always a strictbool. When a validator cannot determine an answer (e.g.key_infofacing an unrecognized algorithm), it fails closed (is_valid: Falsewith an explanatoryreason) rather than returningNone.