Skip to content

Repository files navigation

certnom

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.

Install

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.

Provenance

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.

Quick start

# 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-fleet

CLI

Bare parse

certnom 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.

Config file

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 = 7

Refer 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.

Reports

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 expired

report inventory — single-collection health check

certnom 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.

report drift — what changed between two collections

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 alias

Each 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.

report outliers — fleet consistency across N sources

certnom report outliers dc1.example.com:443 dc2.example.com:443 dc3.example.com:443
certnom report outliers *.pem --by header.fingerprint --format json

For 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).

report revocation — cross-reference certs against CRLs and OCSP

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-crl

Performs 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.

report chain — validate an X.509 trust path

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.com

Cross-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.

report compliance — CABF / Mozilla standards check

certnom report compliance certs/*.pem
certnom report compliance certs/*.pem --max-validity-days 90
certnom report compliance mail.example.com:443 --format json

Checks 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.

Baselines

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-fleet

baseline 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.

cert2csr — generate a CSR from a cert

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.csr

By 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 prefixes dns: / 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 (default sha256). 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 mode 0600, created with O_EXCL (refuses to clobber). The key is never written to stdout or into logs/errors. --force-key-out overwrites atomically (temp-then-replace).

csr-new — author a CSR from scratch (no source cert)

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.csr

Builds 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 default

Library API

import 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.

Design notes

  • from_host does 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 PasswordRequiredError when 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.

License

MIT — see LICENSE.

About

Parse and verify the X.509 family (X.509/CSR/CRL/PKCS#7/PKCS#12) into uniform records; drift, inventory, chain, revocation, and CABF/Mozilla compliance reports. A green check means it actually verified.

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages