Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ jobs:
done
- name: Fuzz the PE version-resource parser (reads untrusted files from the host)
run: go test ./internal/sense/pefiles -run '^$' -fuzz '^FuzzReadVersion$' -fuzztime 30s
- name: Fuzz the CRL judgement (CRLs come from addresses a target's certificates name)
run: go test ./internal/pki -run '^$' -fuzz '^FuzzJudgeCRL$' -fuzztime 60s
- name: Fuzz what people upload (facts files, systems CSV)
run: |
go test ./internal/machinefacts -run '^$' -fuzz '^FuzzParseFacts$' -fuzztime 60s
Expand Down
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,41 @@ the CBOM/report/eval schema versions, the database) can change in a minor releas

## [Unreleased]

### Security

- **Distrusted CA keys are reported, on any Go version**
([CRYPTO-REVIEW-004](docs/product/CRYPTO-REVIEW-004-emudhra-weak-ca-keys.md)). A new bundled distrust list is
matched by key (SPKI SHA-256) and by certificate fingerprint. Its first entry is the eight eMudhra intermediate CAs
of October 2026, whose P-384 private keys are below 256, with their Sectigo cross-signs. The new rule
`CERT.DISTRUSTED-KEY` (critical) fires on served chains and on certificates found on hosts, in files and in
imports.
- **A chain through a distrusted key is still evaluated.** 0.4.0 rejected these chains only because Go 1.27.3
drops them inside `crypto/x509`, and the report showed just "x509: no valid chains built". The evaluation now
builds signature-valid paths itself. Each store's result keeps the path, names the listed certificate and its
position, and says why the store does not trust it. Every other rule still runs. Evaluation version `eval/13`.

### Added

- **CRL revocation checks, opt-in** (`--revocation` on `tls scan` and `serve`; CRYPTO-REVIEW-004 P1b). Every
certificate on the served path is checked against the CRL it names, signed by its own issuer, up to the root.
New rules: `REV.REVOKED` (critical), `REV.CRL-BAD-SIGNATURE` (high), `REV.CRL-STALE` (medium) and
`REV.UNKNOWN` (low). They read *not checked* when lookups are off.
- **Status, not gate:** a revoked certificate stays on the path, and trust and every other rule are unchanged.
- **Hostile URLs are guarded:** fetches are plain HTTP only, with a 16 MiB size cap and timeouts, and every
connection's real address is checked. Private, loopback and cloud-metadata addresses are refused unless
`--revocation-private` is set.
- **Caching:** CRLs are cached until their nextUpdate.
- **Not OCSP:** OCSP responders are still never contacted.
- **Tested:** fixtures are produced by OpenSSL (`testing/pki/make-crl.sh`), including IDP scope, partitions,
delta and wrong-key CRLs, and a fuzz target runs in CI.
- Settings and Help show whether lookups are on.

### Changed

- Root stores refreshed from CCADB (2026-10-23). Mozilla has removed emSign ECC Root CA - G3, emSign ECC Root CA -
C3 and emSign Root CA - C1. Chrome still lists G3 until Chrome Root Store v43 (about 2026-10-27). CT log list
refreshed: Google's logs are now named like "Google Argon2026h2".

## [0.4.0] - 2026-10-22

### Security
Expand Down
50 changes: 48 additions & 2 deletions docs/methodology.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,51 @@ chain. For internal endpoints, add the private CA to the system's CA bundle (or
result is re-evaluated at once and reads "trusted by your CA bundle". Private chains are exempt from the public
lifetime limit and the CT requirement. The readiness level is unaffected by trust.

### Rules and changes in eval/13 (CRYPTO-REVIEW-004)

| Rule | Severity | Fires when | Source |
|---|---|---|---|
| `CERT.DISTRUSTED-KEY` | critical (high for a CA root programs blocked without a key compromise) | a certificate on the served chain, or a root a path reaches, has a key or fingerprint on the bundled distrust list; also raised on certificates found on hosts, in files and in imports | CA/B Forum BR §4.9.1.2, RFC 5280, and the incident's own sources |

- **A distrusted key is a finding, not a gate.** The chain is still validated and every other rule still runs. Each
store's result keeps the signature-valid path through the listed key, names the listed certificate and its
position, and says why the store does not trust it ("the path goes through "EM OV TLS CA - G3C" whose private key
is public …"). `CERT.UNTRUSTED` and `CERT.PARTIAL-TRUST` fire as usual alongside it.
- **Independent of the Go version.** Go 1.27.3 and later drop paths through a few hardcoded keys inside
`crypto/x509` without saying why. When that happens the evaluation builds the paths itself (names chain, each
signature verifies, served certificates as the only intermediates) and reports the listed key on them. A build
with an older Go gives the same result.
- **The list** is matched by the SHA-256 of the SubjectPublicKeyInfo, which covers every certificate for the key
(cross-signs, reissues), and by certificate SHA-256 for certificates known only by fingerprint (host collectors).
First entry: the eight eMudhra intermediate CAs of October 2026, whose P-384 private keys are below 256.

| Rule | Severity | Fires when | Source |
|---|---|---|---|
| `REV.REVOKED` | critical | a certificate on the served path is listed on its issuer's CRL (signature verified, in scope) | RFC 5280 §5, CA/B Forum BR §4.9, §7.2 |
| `REV.CRL-BAD-SIGNATURE` | high | a CRL a certificate names is under its issuer's name but does not verify under the issuer's key | as above |
| `REV.CRL-STALE` | medium | the CRL that answered is past its nextUpdate at evaluation time | as above |
| `REV.UNKNOWN` | low | no CRL answered for a certificate: unreachable, refused by the address policy, too large, not a CRL, out of scope, a delta, indirect, issuer unknown | as above |

- **CRL lookups are opt-in** (`--revocation` on `tls scan` and `serve`; off by default). They are the only part of
a scan that contacts a host other than the target. With them off, the four checks read *not checked*, never
*passed*.
- **Chain-wide, issuer-signed.** Each certificate on the served path, leaf first, is checked against a CRL it
names, signed by the certificate above it (or, for the top, a bundled root or the system's private anchor).
The walk stops at a self-signed root or a served cross-sign of a known root. A forged leaf's own revocation
data would be signed by the compromised key, which is why the parent's CRL is the one that counts.
- **What a CRL must be to answer:** issued under the certificate's issuer name, signed by the issuer's key, not
dated in the future, a complete CRL (not a delta), not indirect, and in scope by its issuing distribution point
(user-only, CA-only, only-some-reasons and partition URLs are honoured). Otherwise the status is *unknown*.
- **Fetching is guarded.** Plain `http` URLs only, no credentials, at most 4 per certificate, 16 MiB per CRL, 10 s
per fetch, 90 s for the stage, 3 redirects (each re-checked), no environment proxy. Every connection's real
address is checked after DNS: loopback, link-local (cloud metadata), private (RFC 1918, fc00::/7,
100.64.0.0/10), unspecified and multicast are refused unless `--revocation-private` is set (an internal CA's
CRL server). Results are cached per URL until the CRL's nextUpdate (at most a day), failures for 5 minutes.
- **Revocation is a status, not a gate.** A revoked certificate stays on the path; the trust verdict and every
other rule are unchanged. A current CRL that does not list the leaf counts, like a verified staple, as
revocation being known. OCSP responders are never contacted: a lookup would tell the CA which site is checked,
and CRLs are mandatory for every publicly trusted CA.

### Rules and changes in eval/12 (PRD-015)

| Rule | Severity | Fires when | Source |
Expand All @@ -116,8 +161,9 @@ lifetime limit and the CT requirement. The readiness level is unaffected by trus
- **A missing issuer** is called a missing intermediate only when the served path stops at a certificate that names
where its issuer can be downloaded (AIA caIssuers); otherwise it is a root the stores checked do not hold: a
private CA, or a public root since removed.
- **Revocation** is *good* only when a verified stapled OCSP response says so. Otherwise its status is *unknown*: the
scan does not contact the CA's OCSP responder or CRL. The revocation checks still count as run.
- **Revocation** is *good* only when a verified stapled OCSP response says so, or (from eval/13, with CRL lookups
on) a current CRL from the leaf's issuer does not list it. Otherwise its status is *unknown*. The staple checks
still count as run.

### Rules added in eval/3 (PRD-004)

Expand Down
Loading
Loading