Repository navigation
Release v0.5.0
π¦ CertMonitor v0.5.0 β Verified Trust, Revocation, STARTTLS, and a CLI
Release Date: September 6, 2026
Repository: bradh11/certmonitor
π Overview
CertMonitor v0.5.0 makes every verdict earn its name. Trust is established by a real verified TLS handshake instead of issuer-name heuristics, hostname identity follows the Subject Alternative Names the way browsers do, the chain validator fails on structural problems by default, and every result carries a stable status and code. The release also adds a standard-library certmonitor command, offline validation of PEM and DER files, a bounded fleet scanner with full endpoint configuration, SHA-256 fingerprints, and a lifetime policy that tracks the CA/Browser Forum schedule automatically. Mail, directory, and database ports are discovered and checked through their STARTTLS preambles without naming the protocol, HTTP CONNECT and SOCKS5 proxies carry every connection, compare_snapshots() and certmonitor diff explain what changed between two observations, and the opt-in revocation validator asks OCSP responders and CRLs with both answers verified, OCSP by in-house RSA and ECDSA checks in the Rust extension and CRLs through OpenSSL. Python 3.8 and 3.9 are dropped; 3.10 through 3.15 are supported, and wheels now ship for Linux x64/ARM64, macOS Intel/ARM64, and Windows x64. Still zero runtime dependencies. Read the Upgrading section before adopting.
β¬οΈ Upgrading from 0.4.0
This release corrects cases where validation could report a misleading pass, and it changes the SAN validator to check only requested alternate names. It is not a behavior-identical replacement for 0.4.0; review these differences before upgrading.
What stays compatible:
CertMonitor(host, port, enabled_validators)keeps its positional arguments.
New connection options are keyword-only.- Defaults remain
expiration,hostname, androot_certificate. - Calling
get_cert_info()beforevalidate()still works. Validation can now
collect certificate data automatically. - Results remain dictionaries with boolean
is_valid. Validator registration
and the legacy alternate-name list argument remain supported; the latter
retains its existing deprecation warning. close()resets connection state and retains the last snapshot for inspection
after context exit. Onlyrefresh()discards it; reconnecting to read
cipher details afterclose()keeps the collected certificate.- Trust results retain
issuerandwarnings; wildcard hostname matches retain
the pattern inmatched_name. Collection preserves email and URI SAN entries. - Custom validator mappings are copied before the dispatcher adds metadata,
so shared and read-only mappings are supported.
Intentional changes:
| Area | Change | Migration |
|---|---|---|
| Hostname | SAN matching determines validity; CN is informational. | Certificates need matching DNS/IP SANs. Inspect common_name_matches separately. |
| SAN representation | Singleton values are lists. | Read subjectAltName["DNS"] as a list, even for one name. |
| Alternate names | With alternate_names, only the requested alternates decide is_valid and every one must match; contains_host is still reported. Without them the validator checks the primary host, as before. |
Pass alternate_names for the names you require; hostname remains the primary identity check. |
| Root trust | A separate verified handshake replaces metadata heuristics. Direct metadata-only validator calls return TrustNotVerified. Hosts that only negotiate with legacy protocol or cipher settings are verified with matching settings and carry a warning. |
Use CertMonitor.validate(). Configure cafile or capath for private PKI and allow the extra handshake. |
| Rotation | A different leaf on the verification connection returns SnapshotMismatch. |
Refresh and retry; use connection_host to target individual backend nodes. |
| Expiration | notBefore and sub-day urgency are checked. The total lifetime is compared with the public TLS limit in force on the issue date (825, 398, 200, 100, or 47 days) and warns rather than fails. | Pass a number as max_lifetime_days for a private PKI policy, or None to disable it. Read lifetime_days and lifetime_limit_days from the result. |
| Chain policy | Non-CA issuers and weak signatures fail by default. | Read structural validity separately from verified trust. Explicit reject_weak_signatures=False preserves warning-only weak-signature policy. |
| PQ probe | Results describe unauthenticated capability observations; TLS alerts are errors. The probe honors connection_host and server_hostname. |
Do not infer protection of application traffic from capability results. |
| Result metadata | status, code, and observation fields are added. Argument mistakes in validator_args report status: error with InvalidValidatorArgs or UnknownValidatorArgs. |
Permit additive keys in result schemas. Use status/error fields rather than parsing human-readable messages. |
| Timeouts | timeout bounds each network operation, including each connection attempt during collection (at most two). |
Budget up to twice timeout for hosts that silently drop handshakes. |
Collection remains permissive. Revocation checking is opt-in through the revocation
validator; root_certificate itself still reports revocation_status: not_checked.
β¨ Added
- Release CI builds Linux x64/ARM64 (glibc 2.28+), macOS Intel/ARM64, and Windows x64 wheels plus a source distribution, and publishes to PyPI only after every distribution has been built.
- Project URLs and additional keywords in the package metadata. The documentation site gains a Read the Docs canonical URL, a light/dark toggle, and copy buttons on code blocks.
- Keyword-only
CertMonitoroptions:connection_hostandserver_hostnameto connect to one address or SNI name whilehoststays the identity being checked;timeout;cafileandcapathfor a private CA; andclient_cert/client_keyfor mutual TLS. refresh()collects a new snapshot, andsnapshot_atrecords when a certificate was collected.- Every
validate()result carriesstatus(pass,warn,fail,error,unsupported) and a stable<validator>.<status>code. Mistakes invalidator_argsreportstatus: errorwithInvalidValidatorArgsorUnknownValidatorArgs. - The
expirationresult includeslifetime_days. - Every collected certificate reports its SHA-256 fingerprint:
CertMonitor.fingerprint_sha256,get_cert_info()["fingerprint_sha256"], and thefingerprint_sha256field inscan_hosts()results andcertmonitor check --json. - A Monitoring Integrations guide with a Prometheus textfile exporter, a GitHub Actions job, a cron recipe, and a health-check one-liner, all standard library only.
- A
certmonitorcommand:checkvalidates hosts and certificate files with human-readable or--jsonoutput and CI-friendly exit codes,infoprints a parsed certificate or its PEM, andvalidatorslists validators and their arguments. Standard library only (#82). CertMonitor.from_file()andCertMonitor.from_bytes()run the certificate checks on a PEM or DER file (or in-memory data) without a connection. Connection-only checks reportstatus: unsupported, andcert_data["source"]records where a certificate came from (#78).scan_hosts()scans many hosts with a bounded worker pool and yields one result per host, including hosts whose scan raised. Entries may be(host, port)pairs or dicts carrying per-endpoint connection settings (connection_host,server_hostname,timeout, CA store, client certificate);validator_args,cafile,capath,client_cert, andclient_keyapply to every host.- Python 3.14 support, now part of the CI test matrix.
- Python 3.15 pre-release support: CI exercises 3.15 betas via
allow-prereleasesso the package is ready ahead of the final release. - STARTTLS support for SMTP, IMAP, POP3, FTP, PostgreSQL, and LDAP. When a port does not answer a TLS handshake, CertMonitor discovers the service from its greeting or from the PostgreSQL and LDAP StartTLS requests, without looking at the port number, and runs the right preamble before the TLS handshake for collection, the verified trust handshake, and the native post-quantum probe.
starttls="smtp"(and the other names) onCertMonitor,scan_hosts()endpoints, andcertmonitor check --starttlsis the override that skips discovery (#88). compare_snapshots()andcertmonitor diffexplain what changed between two observations (replacement, validity, SANs, issuer, subject, key, validator status) with a severity that separates routine renewals from changes that need attention.scan_hosts()results andcheck --jsonnow carry the parsedcertificateandpublic_key_infoso runs can be compared (#89).- Proxy support:
proxy="http://[user:pass@]host:port"(HTTP CONNECT with basic authentication) or"socks5://[user:pass@]host:port"(SOCKS5 with username/password) onCertMonitor,scan_hosts(), andcertmonitor check --proxyroutes protocol detection, collection, the verified trust handshake, and STARTTLS preambles through the tunnel. Results record the route with the password removed. The native post-quantum probe tunnels through the proxy as well (#88, closes #95). - Opt-in
revocationvalidator: checks the certificate's OCSP responder and CRL distribution points. CRL answers are verified by OpenSSL through one extra handshake with the CRL loaded. OCSP responses are parsed and their signatures verified in-house (RSA PKCS#1 v1.5 and ECDSA over P-256 and P-384, in the Rust extension with no crates), accepting the issuing CA or a delegated responder certificate it issued for OCSP signing; an answer that cannot be verified is a warning with the reason unlessaccept_unverified=True. Fetches go through the monitor's timeout and proxy and are cached untilnextUpdate(#90, #102).
π Changed
- The
root_certificateverdict is bound to the collected leaf and reused by latervalidate()calls on the same snapshot;refresh()collects and verifies again. Verifying contexts are built once per monitor (#69). - Reviewed the tutorials and reference documentation for current behavior, preserved API docstrings, and improved navigation, code examples, and light/dark reading styles.
- Breaking:
root_certificateestablishes trust with a separate verified TLS handshake against the system or configured CA store instead of issuer metadata heuristics. The verified leaf must match the collected snapshot (SnapshotMismatchotherwise). Hosts that only negotiate with legacy protocol or cipher settings are verified with matching settings and carry a warning. Revocation is reported asnot_checked. hostnameaccepts anexpected_identityargument (viavalidator_args) to check a different name than the one the monitor connected with.- Breaking:
hostnamevalidates identity with DNS and IP Address SANs only. The Common Name is reported incommon_nameandcommon_name_matchesbut never decidesis_valid.matched_nameis the SAN entry that matched. Singleton SAN values are normalized to lists. subject_alt_namesfails if any requestedalternate_namesentry (list or tuple) is missing, and falls back to checking the primary host when none are requested.contains_hostis always reported.expirationchecksnotBeforeand sub-day urgency, accepts fractional thresholds, and warns when the total lifetime exceedsmax_lifetime_days. The default"public"applies the CA/Browser Forum limit in force on the certificate's issue date (825, 398, 200, 100, or 47 days); a number sets a private policy andNonedisables the check. The limit used is reported aslifetime_limit_days.- Breaking:
chainfails on non-CA issuers and weak signatures by default;reject_weak_signatures=Falserestores warning-only handling. Structural validity is reported separately from cryptographic trust. pq_key_exchangeresults carry observation evidence (endpoint,observed_at,offered_groups,handshake_completed,authenticated). TLS alerts are reported as inconclusive errors, the native probe bounds its write timeout, and it honorsconnection_hostandserver_hostnameseparately.validate()collects the certificate automatically.close()resets connection state but retains the last snapshot; onlyrefresh()discards it.timeoutbounds each network operation, including each connection attempt made while collecting.- Breaking: Dropped support for Python 3.8 and 3.9 (both end-of-life). The new minimum is Python 3.10 (
requires-python = ">=3.10,<3.16"), with socket chain APIs and private fallbacks used when available. - Modernized the codebase to Python 3.10+ idioms: built-in generics (
list/dict),X | Noneunions, and a full ruffpyupgrade(UP) sweep, withtarget-version = "py310"enforcing it going forward. No runtime behavior change. - Raised the Rust extension's abi3 floor to
abi3-py310to match the new Python minimum. - CI: the test matrix is now Python 3.10 through 3.15, and the GitHub Actions were refreshed (checkout v6, setup-python v6, setup-uv v8.2.0, codecov v5 with token upload, action-gh-release v2).
make testnow runs the Rust unit tests (cargo test) as part of the local CI-equivalent suite; previously they ran only in CI.- Added a
make typecheck-tycommand to run astral'stytype checker on demand (a 0.0.x preview, advisory only). mypy remains the enforced gate; ty is kept out ofmake testand is commented out in CI with a note to revisit once it stabilizes. certmonitor validatorsseparates each validator with a blank line.
π οΈ Fixed
- The TLS collector negotiates with a single version-ranged context instead of retrying deprecated per-protocol constants under
warnings.catch_warnings(), which is not thread-safe under concurrent scans. Legacy TLS 1.0 and 1.1 servers remain reachable where the local build allows them, and collecting a certificate now costs one connection instead of up to five (#67). - The README logo now uses an absolute CDN URL so it renders on the PyPI project page (relative paths only resolve on GitHub).
π Documentation
Comprehensive documentation is available at certmonitor.readthedocs.io.
π Python Compatibility
Tested with Python 3.10 through 3.15 with 99% code coverage across all supported versions. Wire-level code has libFuzzer targets, and the signature verifier is checked against 2,320 Wycheproof vectors and an OpenSSL differential test on every run.
π License
This project is licensed under the MIT License. See the LICENSE file for details.
Full Changelog: v0.4.0...v0.5.0