Parse the X.509 certificate family into uniform JSON-serializable records, run structured reports over those records, and emit findings as text or JSON.
Supports the X.509 family — X.509 / CSR / CRL / PKCS#7 / PKCS#12. Built for solo operators and SREs who would otherwise be juggling openssl, grep, and awk against fleets of hosts and stores.
certnom parses AND verifies the X.509 family (chain + OCSP + CRL) — a green result means the chain actually verified, not just that the bytes parsed.
certnom is not published to PyPI — install from source, ideally pinned to a signed release tag:
# Pinned release (recommended)
pip install git+https://github.com/zakrodriguez/certnom.git@v4.2.1
# Or clone and install
git clone https://github.com/zakrodriguez/certnom.git
cd certnom
pip install .Python >= 3.11 required.
There is no PyPI index to trust here, so verifying the signed release before installing is the recommended way to obtain a trustworthy copy — see docs/verify-release.md.
certnom is developed in a private working tree and published as a curated line
of that work. Internal planning and design history is kept out of the published
repository; CHANGELOG.md is the authoritative release timeline, and each
release tag carries a Sigstore-signed, SLSA-attested set of build artifacts
(wheel, sdist, source archive, SBOM). Contributions are handled through a
one-way model — see CONTRIBUTING.md.
# Parse anything — auto-detects the format
certnom server.pem
certnom - < cert.pem
certnom example.com:443
certnom mail.example.com:587 --starttls smtp
# Run a report
certnom report inventory certs/*.pem
certnom report chain example.com:443
certnom report revocation example.com:443 --capture-staple
# Save a baseline, diff it later
certnom baseline save prod-fleet example.com:443 mail.example.com:587
certnom baseline diff prod-fleetcertnom server.pem
certnom - < cert.pem
certnom example.com:443
certnom mail.example.com:587 --starttls smtp # STARTTLS-wrapped protocols
certnom --password env:P12_PW store.p12 # encrypted PKCS#12
certnom certs/*.pem --field header.identifier --field header.valid_to--starttls accepts: smtp, imap, pop3, ftp, ldap, postgres. The tool performs the protocol's plaintext greeting and upgrade exchange before the TLS handshake, then returns the full certificate chain the server presents.
--field <dotted-path> prints just that field (repeatable, tab-separated when multiple). --filter EXPR keeps only records matching the expression (path=value, path~regex, !=, !~; repeatable, AND-ed). Combine with -q/--quiet or -v/--verbose.
Optional .certnom.toml (or [tool.certnom] in pyproject.toml) configures defaults and named source aliases. certnom walks up from CWD to find it; --config <path> overrides discovery.
[certnom]
baseline_path = "./.certnom/baselines"
[certnom.sources.aliases]
prod_mail = "mail.example.com:587 --starttls smtp"
prod_imap = "imap.example.com:993"
prod_pg = "db.example.com:5432 --starttls postgres"
[certnom.inventory]
threshold_warn_days = 30
threshold_critical_days = 7Refer to a configured alias with @name in any command that takes a source — certnom report inventory @prod_mail, certnom baseline save fleet @prod_mail @prod_pg, etc.
All reports produce severity-tagged findings (info / notice / warning / critical). Output is text by default; --format json for structured output. Exit code is 0 when no finding is warning or critical, 1 otherwise — CI-friendly.
--severity and --code narrow which findings appear and contribute to the exit code (both repeatable, OR within each flag, AND across flags):
certnom report inventory certs/*.pem --severity critical
certnom report inventory certs/*.pem --code expiring-7d --code expiredcertnom report inventory certs/*.pem
certnom report inventory mail.example.com:443
certnom report inventory certs/*.pem --threshold-warn-days 60 --threshold-critical-days 14
certnom report inventory certs/*.pem --format json | jq '.attributes.by_issuer_cn'Per-record findings: expired, expiring-7d, expiring-30d, weak-key, sha1-sig, self-signed, wildcard-san, healthy. Fleet aggregates in attributes: total_records, by_type, by_issuer_cn, by_signature_algorithm, by_key_type, expiring_buckets.
certnom report drift old.pem new.pem
certnom report drift baseline.jsonl example.com:443
certnom report drift a.jsonl b.jsonl --by details.subject.CN
certnom diff old.pem new.pem # legacy aliasEach side can be a file, host:port, - for stdin, or a JSONL snapshot. Finding codes: unchanged, rotated-clean, rotated-with-changes, varied-metadata-only, varied-with-changes, replaced, added, removed.
certnom report outliers dc1.example.com:443 dc2.example.com:443 dc3.example.com:443
certnom report outliers *.pem --by header.fingerprint --format jsonFor each key shared across 2+ sources, the modal fingerprint is the majority; sources serving a different fingerprint produce outlier-fingerprint (critical); sources missing a record produce gap-missing-record (warning).
certnom report revocation certs/*.pem
certnom report revocation example.com:443 --capture-staple
certnom report revocation certs/*.pem --crl https://crl.example.com/ca.crl
certnom report revocation certs/*.pem --no-fetch
certnom report revocation example.com:443 --ocsp-strict --no-crlPerforms RFC 6960 AIA-OCSP fetches in addition to the CRL path. Verdicts are reconciled across all 11 (CRL × OCSP) cells: agreement emits a single finding; affirmative-but-contradictory verdicts emit revocation-source-disagreement. UNKNOWN on either side is treated as absence of information, not a disagreement.
Key flags: --capture-staple (prefer the server's stapled OCSP response over a fresh HTTP fetch), --no-ocsp, --no-crl, --no-fetch, --ocsp-strict, --allow-unverified-ocsp, --ocsp-stale-warn-hours N, --ocsp-require-nonce.
certnom report chain server.pem
certnom report chain example.com:443
certnom report chain leaf-and-intermediates.pem --trust-store custom-roots.pem
certnom report chain server.pem --check-host api.example.comCross-references each input chain against a trust store (default: certifi bundle). Findings cover chain validity, incomplete chains, untrusted roots, broken signatures, expired certs, BasicConstraints, KeyUsage, EKU, and hostname matching.
--trust-store <file> (alias --ca-bundle) replaces the certifi default. --no-default-trust uses only the supplied file's anchors.
certnom report compliance certs/*.pem
certnom report compliance certs/*.pem --max-validity-days 90
certnom report compliance mail.example.com:443 --format jsonChecks each X.509 record against the CAB-Forum Baseline Requirements and Mozilla Root Store Policy: validity period ceiling, SAN content, internal names, required extensions (AIA, CDP, policy OID), prohibited algorithms (MD5/SHA-1), prohibited key curves, and Certificate Transparency (SCT count). A cert that passes every check emits compliance-clean.
certnom baseline save prod-fleet @prod_mail @prod_imap @prod_pg
certnom baseline list
certnom baseline show prod-fleet [--records]
certnom baseline diff prod-fleet # exits 1 if drift detected, 0 if clean
certnom baseline rm prod-fleetbaseline diff re-fetches the sources stored in the baseline and runs the drift report against the saved records — no source arguments needed at diff time.
certnom cert2csr cert.pem --key key.pem > req.csr
certnom cert2csr cert.pem --key env:CERTNOM_KEY
certnom cert2csr mail.example.com:443 --key key.pem --leaf-only
# Renew with a brand-new key, verbatim profile guaranteed
certnom cert2csr old-cert.pem --key new-key.pem \
--rotate-key --strict-extensions > renewed.csrBy default cert2csr copies the cert's subject and every extension verbatim into a CSR signed by the supplied key — byte-for-byte stable. --rotate-key allows a new key unrelated to the cert's existing one. --strict-extensions fails loudly (ExtensionCopyError) if any extension can't be copied, instead of warning and continuing.
Every flag below is opt-in: with no new flags the output is byte-identical to a verbatim clone.
Shape the cloned CSR before signing:
# Drop the 7 issuer-only extensions (AKI, AIA, CRL DP, Freshest CRL,
# CT SCT list, CT precert poison, OCSP no-check). SKI is kept (subject-derived).
certnom cert2csr cert.pem --key key.pem --strip-issuer-extensions > req.csr
# Override the CN (auto-promoted into SAN if absent) and edit the SAN set
certnom cert2csr cert.pem --key key.pem \
--cn new.example.com \
--add-san www.example.com --add-san ip:192.0.2.10 \
--remove-san old.example.com > req.csr--strip-issuer-extensions— drop the 7 issuer-only OIDs; each drop is named on stderr.--cn VALUE— override the subject Common Name; other DN components preserved; the new CN is auto-promoted into SAN (CA/B BR §7.1.4.2.2) if absent.--add-san/--remove-san(repeatable) — merge into (never clobber) the cloned SAN set. Bare values auto-detect DNS/IP; explicit prefixesdns:/ip:/email:/uri:. A malformed value (e.g.ip:not-an-ip) exits 2 with no partial CSR.
Control signing and generate a key in one shot:
# Pick the signature hash / padding
certnom cert2csr cert.pem --key key.pem --hash sha384 > req.csr
certnom cert2csr cert.pem --key rsa-key.pem --rsa-pss > req.csr
# Generate a fresh key, write it 0600, and sign the CSR with it (implies --rotate-key)
certnom cert2csr cert.pem --generate-key ec:p256 --key-out new.key > req.csr--hash sha256|sha384|sha512— signature hash (defaultsha256). Rejected for Ed25519/Ed448 (they self-specify their hash). SHA-1/MD5 are not selectable.--rsa-pss— RSASSA-PSS instead of PKCS#1 v1.5 (RSA keys only).--generate-key SPEC— generate a fresh key (rsa:{2048,3072,4096}/ec:{p256,p384,p521}/ed25519/ed448) instead of--key. Implies key rotation, requires--key-out, mutually exclusive with--key, and drops the stale copied SKI.--key-out PATH— write the generated key as PKCS#8 PEM at mode0600, created withO_EXCL(refuses to clobber). The key is never written to stdout or into logs/errors.--force-key-outoverwrites atomically (temp-then-replace).
certnom csr-new --profile profile.toml --key-out new.key > req.csr
# Reuse a profile with per-run tweaks (flags override profile values)
certnom csr-new --profile profile.toml --cn override.example \
--generate-key ec:p384 --key-out new.key > req.csr
# Or sign with an existing key instead of generating one
certnom csr-new --profile profile.toml --key existing.key > req.csrBuilds and signs a CSR from a strict TOML profile with no source certificate, reusing the exact same CN→SAN promotion, SAN-type validation, signing knobs, and key generation as cert2csr. The profile defines [subject], [san], [key], and named [extensions] (keyUsage / extendedKeyUsage / basicConstraints) — named vocabulary only, no raw-OID injection. Unknown profile keys are hard errors (no CSR is emitted); weak keys/curves and incoherent extension combinations are rejected before signing. CLI flags override profile values. A worked sample lives at docs/profile.example.toml:
[subject]
cn = "example.com"
o = "Acme Corp"
c = "US"
[san]
dns = ["example.com", "www.example.com"]
ip = ["192.0.2.1"]
[key]
spec = "ec:p256" # rsa:{2048,3072,4096} | ec:{p256,p384,p521} | ed25519 | ed448
# hash = "sha256" # sha256|sha384|sha512 (not for EdDSA)
# rsa_pss = false # RSA only
[extensions]
key_usage = ["digitalSignature", "keyEncipherment"] # named bits, case-insensitive
eku = ["serverAuth"] # named EKUs, no raw OIDs
# basic_constraints = { ca = false } # omit for leaf defaultimport certnom
from certnom.reports import drift, inventory, outliers
# Parse any format — auto-detect
records = certnom.parse(certnom.from_path("server.pem"))
for r in records:
print(r.header.type, r.header.identifier, r.header.valid_to)
# Parse a live TLS host
src = certnom.from_host("example.com", port=443)
records = certnom.parse(src)
# Capture the server's stapled OCSP response
src = certnom.from_host("example.com", port=443, capture_staple=True)
# Drift report
[a] = certnom.parse(certnom.from_path("old.pem"))
[b] = certnom.parse(certnom.from_path("new.pem"))
report = drift.run(a, b)
print(report.summary)
print(report.has_actionable_findings) # True iff any warning/critical
print(report.to_json(pretty=True))
# Inventory report
records = certnom.parse(certnom.from_path("certs.pem"))
report = inventory.run(records, threshold_warn_days=60)
for f in report.findings:
if f.severity in ("warning", "critical"):
print(f.code, f.summary)
# cert_to_csr
from certnom import cert_to_csr
csr_pem: bytes = cert_to_csr(
cert_pem=b"-----BEGIN CERTIFICATE-----\n...",
key_pem=b"-----BEGIN PRIVATE KEY-----\n...",
require_key_match=True, # default; set False to rotate key
strict_extensions=True, # fail loudly on any non-copyable extension
)Every parsed record is a ParsedRecord with a header (type, identifier, valid_from, valid_to, fingerprint) and a details dict carrying format-specific payload. Every report returns the same Report / ReportFinding shape regardless of format.
from_hostdoes not validate the chain. Certs are returned as-presented so callers can inspect untrusted, expired, or self-signed servers. Validation is the caller's responsibility.- PKCS#12 interactive password prompt is CLI-only. The library raises
PasswordRequiredErrorwhen a password is missing. Non-TTY contexts (CI, piped stdin) need--password env:VAR/file:/path/-to fail loudly rather than hang. - Parse AND verify for X.509. Chain + OCSP + CRL + CABF/Mozilla compliance. A green result means the thing is actually green, not just that the bytes parsed.
MIT — see LICENSE.