Skip to content

Distrusted CA keys and opt-in CRL revocation (CRYPTO-REVIEW-004 P0 + P1b) - #28

Open
khalid-offset wants to merge 2 commits into
mainfrom
fix/distrusted-keys
Open

khalid-offset wants to merge 2 commits into
mainfrom
fix/distrusted-keys

Conversation

@khalid-offset

Copy link
Copy Markdown
Contributor

Response to the eMudhra CA key compromise (Oct 2026). Eight eMudhra P-384 intermediate CAs had private keys below 256. Full analysis and plan: docs/product/CRYPTO-REVIEW-004-emudhra-weak-ca-keys.md.

The rule throughout: CipherHorizon evaluates, it doesn't gate. A distrusted key or a revoked certificate is reported as a finding on a fully evaluated chain. Every other check still runs.

P0: distrusted keys, on any Go version

  • Distrust list. internal/refdata/distrust.json is matched by key hash (SPKI SHA-256) and by certificate SHA-256. Its first entry is the 8 eMudhra keys and their 16 certificates (emSign originals and Sectigo cross-signs). These were checked against the published certificates, and a test re-derives each key from its private value.
  • Own path builder. pki.Paths builds chains whose signatures check out, with no policy filtering. Go 1.27.3+ silently drops chains through these keys, and older Go versions accept them. We now report the chain, the listed certificate and the reason either way, instead of "x509: no valid chains built".
  • New rule CERT.DISTRUSTED-KEY (critical). It fires on served chains and on certificates found on hosts, in files and in imports. Evaluation version: eval/13.
  • Root lists refreshed from CCADB (2026-10-23). Mozilla has removed emSign ECC Root CA - G3, ECC C3 and C1. Chrome removes G3 in v43 (about 27 Oct): refresh again then. The CT log list is refreshed too.

P1b: opt-in CRL revocation, up the served chain

  • Off by default. --revocation turns it on for tls scan and serve. When off, the new checks read "not checked", never "passed".
  • Chain-wide. Each certificate on the served path is checked against the CRL it names, signed by its own issuer. The walk stops at a self-signed root or a served copy of a known root.
  • Pure judgement. pki.JudgeCRL checks the issuer name and signature, the dates, delta and indirect CRLs, and issuing-distribution-point scope (user-only, CA-only, partial reasons, partitions).
  • Guarded fetcher (internal/revocation).
    • Plain http URLs only, no credentials in the URL.
    • 16 MiB cap, timeouts, at most 3 redirects (each re-checked), no environment proxy.
    • Every connection's real address is checked: loopback, link-local, private and cloud-metadata addresses are refused unless --revocation-private is set.
    • Results are cached until each CRL's nextUpdate.
  • New rules: REV.REVOKED (critical), REV.CRL-BAD-SIGNATURE (high), REV.CRL-STALE (medium), REV.UNKNOWN (low).
  • Revocation is a status, not a gate. Trust and every other rule are unchanged.
  • OCSP responders are still never contacted.
  • UI: a CRL tile in Configuration, plus Settings and Help text. Fixed three texts that contradicted findings: the CERT.PARTIAL-TRUST and OCSP.REVOKED pass texts, and the score's "not measured" note.

Testing

  • Unit tests: pki (OpenSSL-made CRL fixtures from testing/pki/make-crl.sh), revocation (address policy, cache, caps, redirects), tlsprobe (chain walk with a fake fetcher), and eval (every new rule, plus "not checked" when lookups are off).
  • Build guard: a chain through a CA with a P-384 private key of 4 yields CERT.DISTRUSTED-KEY and keeps the full path, on both Go 1.27.0 and 1.27.3.
  • Fuzzing: a new FuzzJudgeCRL target is added to the CI fuzz job.
  • go test ./... and golangci-lint both pass.
  • Real servers (--revocation):
    • eMudhra test host: REV.REVOKED (G3B, cACompromise, on emSign Root TLS CA - G3's CRL) and CERT.DISTRUSTED-KEY.
    • revoked.badssl.com: leaf revoked (keyCompromise).
    • www.google.com: not revoked; the revocation section reads "good".
  • Screens: the endpoint report, Configuration (CRL tile), Weaknesses and Settings were checked in the browser.

Not in this PR

P1 (weak-key tests: tiny EC private keys, ROCA, batch GCD), P2 (which hosts in the estate are still exposed), P3 (scheduled CCADB/OneCRL refresh). No release cut.

eMudhra's eight P-384 intermediate CAs (Oct 2026) have private keys below
256. v0.4.0 rejected chains through them only because Go 1.27.3 drops them
inside crypto/x509, and the report said just "no valid chains built".

- refdata: hand-curated distrust list (distrust.json), matched by SPKI and
  certificate SHA-256; first incident is eMudhra (8 keys, 16 certificates,
  verified against the published certificates; a test re-derives each key
  from its private value).
- pki: Paths builds signature-valid paths without policy filtering;
  Validate takes the list, keeps the path through a listed key and names
  it (StoreResult.Listed) instead of losing it.
- eval: CERT.DISTRUSTED-KEY (critical) on served chains and on host, file
  and imported certificates; a finding, not a gate: every other rule still
  runs. eval/13.
- Root stores and CT log list refreshed from CCADB/Google (2026-10-23):
  Mozilla removed emSign ECC Root CA - G3, ECC C3 and Root CA - C1.
…-004 P1b)

With --revocation (tls scan, serve), the revocation stage fetches the CRL
each certificate on the served path names and checks it against its own
issuer, up to the root (stopping at a served copy of a known anchor).
Revocation is a status beside trust, never a gate.

- pki.JudgeCRL (pure): issuer name, issuer signature, dates, delta,
  indirect and IDP scope (user/CA-only, partial reasons, partitions).
- internal/revocation: guarded fetcher. http only, no credentials, 16 MiB,
  timeouts, re-checked redirects, no env proxy; loopback, link-local,
  private and metadata addresses refused unless --revocation-private;
  cache until nextUpdate.
- Report.CRLLookups and CertResult.CRL; rules REV.REVOKED,
  REV.CRL-BAD-SIGNATURE, REV.CRL-STALE, REV.UNKNOWN (not-checked when
  lookups are off). A current CRL for the leaf makes revocation known.
- OpenSSL-made fixtures (testing/pki/make-crl.sh), FuzzJudgeCRL in CI.
- UI: CRL tile and table, Settings and Help; fixed contradictory pass
  texts (CERT.PARTIAL-TRUST, OCSP.REVOKED) and the hygiene note.
- OCSP responders are still never contacted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant