Repository navigation
Release v0.6.0
π¦ CertMonitor v0.6.0 β Proven Revocation
Release Date: September 8, 2026
Repository: bradh11/certmonitor
π Overview
Every revocation answer now has to prove itself the same way. OCSP responses, CRLs, responder certificates, and the issuer's own signature over the certificate are verified in-house, with RSASSA-PSS, ECDSA over P-521, Ed25519, and Ed448 joining RSA PKCS#1 v1.5 and ECDSA over P-256 and P-384, and OpenSSL is out of the CRL path. An issuer that merely shares the right name no longer counts, a CRL that is out of scope, carries a critical extension CertMonitor cannot process, or comes from a key not allowed to sign CRLs is refused rather than read, and the caches hold signed evidence that is re-checked against the certificate in hand on every reuse. Around it: exact RSA key sizes and Edwards keys in key_info, proxy secrets kept out of error output, unknown validator arguments reported instead of ignored, failed collections surfaced as such, and one deadline for the whole STARTTLS preamble. Still zero runtime dependencies. Read the Upgrading section before adopting.
β¬οΈ Upgrading from 0.5.x
Most users need no changes: the revocation validator is opt-in, and every other validator keeps its result shape. Check the following if you consume revocation results or call into certmonitor.revocation directly.
- CRL error codes changed. The OpenSSL-era
verify_codefield is gone, and so are theCRLVerificationFailedandSnapshotMismatchcodes. A CRL answer now carriesverificationandverification_errorlike an OCSP answer, and fails with one ofCRLInvalidSignature,CRLIssuerMismatch,CRLNotYetValid,CRLStale,CRLScopeUnsupported,CRLUnsupportedCriticalExtension,CRLSignerNotAuthorized, orMissingIssuer. Update any matching on the old names. - The CRL method needs the issuer certificate, from the served chain or the certificate's
caIssuerspointer, as OCSP always did. A server that sends only its leaf and offers nocaIssuersURL now reportsMissingIssuerfor CRL checks. - Stricter by design. Results that passed under 0.5.x can now be
warnorerror: an issuer that only name-matches the leaf caps every answer atunsupported; an OCSP response withoutnextUpdateis refused once older thanmax_age_hours(default 24); CRLs that are partitioned, delta, from a key withoutcRLSign, or carrying unknown critical extensions are refused and the next source consulted; a cached OCSP answer is re-checked against the current certificate's issuer binding.accept_unverifiedstill takes an unverifiable answer at its word, and never a failed one. - Removed from
certmonitor.revocation:RevocationEvidence(crl_check=...)andserial_bytes().fetch_crl()still fetches and parses but no longer caches;remember_crl()does, andRevocationEvidencecalls it after the signature verifies. validator_argsare checked. A key naming no known validator is anerrorresult (UnknownValidator), andcertmonitor check --argfor an unknown validator exits 2. Arguments for a validator that is not enabled produce awarnsaying they were not applied.- Failed collections are explicit.
certmonitor check --jsonandscan_hosts()add top-levelerrorandmessagewhen no certificate was collected,CertMonitor.collection_errorexposes the same, andcompare_snapshots()reports such snapshots as failed scans instead of a replaced certificate. key_inforeports exact RSA sizes, so a 2041-bit key is 2041 bits and fails the 2048 floor it previously passed; Ed25519 and Ed448 keys now pass instead of failing closed asunknown.
β¨ Added
revocation:max_age_hoursargument.key_info: Ed25519 and Ed448 keys are recognized (algorithm: "Ed25519"or"Ed448") and judged strong, instead ofunknownand failing closed.CertMonitor.collection_error:Noneafter a successful collection, else{"error", "message"}.key_infoandrevocation: RSA public keys encoded withid-RSASSA-PSS(RFC 4055) are recognized asrsassaPss, judged by the RSA size floor, and may only verify RSASSA-PSS signatures whose parameters satisfy the key's restrictions. A PKCS#1 v1.5 signature under such a key is a hard failure thataccept_unverifieddoes not rescue.revocation: OCSP responses, CRLs, responder certificates, and issuer bindings signed with RSASSA-PSS, ECDSA over P-521, Ed25519, or Ed448 are now verified in-house, alongside RSA PKCS#1 v1.5 and ECDSA over P-256 and P-384. Scheme dispatch and the padding checks live incertmonitor.signatures;certinfogainsrsa_pss_encoded_message,rsa_pss_parameters,eddsa_verify, andparse_spki, and reportssignature_algorithm_paramsandkey_bits.
π Changed
revocation: CRL answers reportMissingIssuerwhen no chain orcaIssuerscertificate signed the leaf (the CRL method now needs the issuer certificate, as OCSP always did),CRLIssuerMismatchwhen the CRL names another issuer, andCRLNotYetValidorCRLStaleoutside its validity window. The OpenSSL-eraverify_codefield and theCRLVerificationFailedandSnapshotMismatchCRL error codes are gone, as areRevocationEvidence(crl_check=...)andcertmonitor.revocation.serial_bytes().revocation:fetch_crl()no longer caches what it fetched. A CRL enters the cache through the newremember_crl()once its signature has verified, whichRevocationEvidencedoes for you; call it yourself only if you verify CRLs throughfetch_crl()directly.starttls.negotiate()takes a keyword-onlytimeoutfor the whole preamble, defaulting to the socket's own timeout, and restores the socket's timeout afterwards.open_stream()passes itstimeoutthrough.
π οΈ Fixed
revocation: the issuer certificate must verify the leaf's signature before it is trusted; a certificate that merely shares the issuer's name (from the chain or thecaIssuerspointer) is rejected, and when the leaf's algorithm cannot be checked every answer built on it is capped atunsupported.revocation: OCSP answers must match the whole CertID (hash algorithm, issuer name hash, issuer key hash, serial), and the cache is keyed the same way.revocation: OCSP responses withoutnextUpdateare refused oncethisUpdateis older thanmax_age_hours(default 24), any response older than ten days is refused, and cache lifetime is anchored tothisUpdateso a re-fetched historical response gets no new lease.revocation: answers whose signature failed verification are no longer cached.revocation: a CRL carrying a critical extension CertMonitor cannot process, on the list or on any entry, is refused withCRLUnsupportedCriticalExtension(RFC 5280 sections 5.2 and 5.3), and a CRL from an issuer whosekeyUsageis present withoutcRLSignis refused withCRLSignerNotAuthorized(section 6.3.3); previously both were verified passes.certinfo.crl_info()reportsunsupported_critical_extensionsandcertinfo.certificate_signature_parts()reportskey_usage.revocation: a CRL is cached only once its signature has verified, so a corrupted or forged CRL is fetched again next time instead of being served from the cache until itsnextUpdate. CRL cache expiry follows the CRL acceptance rule (itsnextUpdate, at most a day), not the OCSP ten-day ceiling, so an old CRL that is still inside its validity window is reused rather than downloaded on every check.revocation: the OCSP cache keeps the responder's evidence, not the first caller's verdict. Every reuse is capped by the current certificate's issuer binding, so a cache hit no longer reportssignature_verified: truefor a certificate whose issuer is only name-matched, and is held to the same freshness rule as a fresh response, so nothing is served oncethisUpdateis ten days old; cache expiry is capped there too, and at thenotAfterof a delegated responder certificate that signed the answer.revocation: the CRL is verified in-house against the bound issuer and the collected certificate's serial is looked up directly; the extra OpenSSL handshake, which could attribute another certificate's revocation to the snapshot, is gone. CRL answers now carryverificationandverification_errorlike OCSP answers, and a failed CRL signature reportsCRLInvalidSignature.revocation: a delta CRL, or a CRL whose issuing distribution point limits it to some reasons, to CA or attribute certificates, or to another issuer, or whose issuing distribution point names a location other than the one it was fetched from (RFC 5280 section 6.3.3), is refused withCRLScopeUnsupportedinstead of being read as a complete list; the next distribution point or OCSP is consulted. A CRL withoutnextUpdateis refused oncethisUpdateis older than ten days.certinfo.crl_info()reportsdelta_crl_indicatorandissuing_distribution_point, which now also carriesdistribution_point_urisandnames_other_locations.key_info: an Ed25519 or Ed448 key whose encoding is not 32 or 57 bytes is reported asunknownand fails closed.key_info: RSA modulus sizes are reported exactly (a 2041-bit key is 2041 bits, not 2048), so undersized keys no longer pass the 2048-bit floor. A modulus whose DER INTEGER carries a surplus leading zero byte is reported asalgorithm: "unknown"and fails closed.- Signature verification refuses RSA keys with a modulus over 16384 bits or a public exponent over 64 bits (OpenSSL's limits) as
unsupportedinstead of computing with them. - Signature verification refuses an RSASSA-PSS parameter integer carrying a second leading zero octet, which is not minimal DER, and an Ed25519 or Ed448 public key of small order: under the cofactorless verification equation, a small-order key needs no private key at all, so refusing it outright (RFC 8032 section 8.8 discusses the cofactor) removes a universal-forgery shape.
- Certificate parsing rejects a
pathLenConstraintinteger carrying a second leading zero octet, which is not minimal DER. pq_key_exchange: a ServerHello split across TLS records is reassembled instead of reported asServerHello split across records.- Proxy URL parse errors (
has no host,has an invalid port) redact the password;scan_hosts()labels an invalid endpoint dict by itshost, never by the whole dict, so proxy credentials no longer reach CLI or fleet output. validate()no longer ignoresvalidator_argskeys that name no validator that runs: an unknown name is an error (UnknownValidator), and a real validator that is not enabled gets awarnresult saying its arguments were not applied.certmonitor checkrejects an--argfor an unknown validator as a usage error (exit 2) and warns on stderr about one for a validator-vdoes not enable.certmonitor check --jsonandscan_hosts()add top-levelerrorandmessagewhen no certificate was collected, andcompare_snapshots()treats such snapshots (and older ones whose results are all errors beside an empty certificate) as failed scans instead of reporting the certificate as replaced with aNoneissuer.- STARTTLS multi-line replies are capped at 64 lines, and so are the untagged IMAP lines that may precede the
STARTTLSreply. - STARTTLS negotiation runs against one deadline of
timeoutseconds for the whole preamble, every read and write included, instead of a per-read socket timeout that restarted whenever a byte arrived. - CI:
cargo auditnow actually runs, in the security job, on every PR and weekly; the old step was gated on a matrix value that never matched. - Security policy points at the right repository, lists 0.5.x as supported, and describes the in-house parser;
pytest.inideclares its marker correctly.
π 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.
π License
This project is licensed under the MIT License. See the LICENSE file for details.
Full Changelog: v0.5.2...v0.6.0