Skip to content
Merged
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,22 @@ the CBOM/report/eval schema versions, the database) can change in a minor releas

### Fixed

- **Reports on real servers say the right thing** ([PRD-015](docs/product/PRD-015-accuracy-on-real-scans.md),
[CRYPTO-REVIEW-003](docs/product/CRYPTO-REVIEW-003-real-scans.md); eval/12, stored scans are re-evaluated on start):
- a server whose only suites Go's TLS client will not offer (3DES, RC4, NULL, small DH) had its no-SNI fallback
certificate reported as its own; the served chain is now read from the scanner's raw TLS 1.2 handshakes, which
also finds an RSA certificate a server keeps for RSA-only clients. Scan such servers again;
- a leaf signed straight by an unknown root is no longer called a missing intermediate;
- a chain under Superfish, eDellRoot or DSDTestProvider is *The certificate chains to a root whose private key is
public* (critical);
- revocation reads *Status unknown*, not *Good*, unless a stapled OCSP response vouches for the certificate;
- an untrusted root is described as a private CA or a public root since removed, not only as a private one;
- a composite DH modulus is critical, and a DH prime that is not a safe prime is low;
- at equal bits, the weakest classical strength picks a disallowed algorithm (3DES) over an allowed one.
- **Fix first** shows the three things to fix first, each with one chip saying when, and folds the rest under one
line that counts them by severity.
- **Every TLS version on Configuration starts folded**, its head saying whether it should be on and how its suites
rate; version heads, suite rows and Fix first rows line up.
- **SSH evaluation (eval/11; stored scans are re-evaluated on start):**
- a truncated MAC is as strong as its tag: hmac-sha1-96 is 96 bits, not 128 (NIST SP 800-107 Rev. 1);
- the next steps name OpenSSH releases only to a server that identifies as OpenSSH;
Expand Down
17 changes: 17 additions & 0 deletions docs/methodology.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,23 @@ 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/12 (PRD-015)

| Rule | Severity | Fires when | Source |
|---|---|---|---|
| `KX.DH-COMPOSITE` | critical | the DHE modulus fails a primality test (Miller-Rabin with 20 bases plus Baillie-PSW); `KX.DH-SMALL` is then not raised | RFC 7919, NIST SP 800-56A Rev. 3 |
| `KX.DH-NOT-SAFE-PRIME` | low | the DHE prime is not a safe prime ((p-1)/2 not prime) and is not a published group | RFC 7919, CVE-2016-0701 |
| `CERT.COMPROMISED-ROOT` | critical | the chain is issued under Superfish, eDellRoot or DSDTestProvider, roots installed with their private keys; `CERT.UNTRUSTED` is then not raised | CVE-2015-2077, RFC 5280 |

- **The certificate a server serves is always read.** When Go's TLS client cannot complete a handshake (3DES, RC4,
NULL or small-DH suites only; an RSA certificate a server keeps for RSA-only clients), the chain is read from the
plaintext Certificate message of the scanner's own TLS 1.2-and-earlier handshakes (source `raw-tlsv1.2`).
- **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.

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

| Rule | Severity | Fires when | Source |
Expand Down
82 changes: 82 additions & 0 deletions docs/product/CRYPTO-REVIEW-003-real-scans.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# CRYPTO-REVIEW-003 · The owner's real scans, finding by finding (PRD-015)

Date: 2026-10-22. Reviewer role: cryptographer (PRD-015 REQ-ACC-10). Data: a copy of the owner's database
(`dist/cipherhorizon.db`, 55 TLS endpoints, one SSH server), re-evaluated with eval/12, and eight hosts scanned
again with this build (3des, rc4, dh-composite, dh-small-subgroup, superfish, revoked, null at badssl.com, and
www.google.com). badssl.com publishes what each host demonstrates, so each report is checked against a known
answer. The owner's own database was not changed.

## Verdict

**Signed off** after the fixes below. Every badssl.com host now reports the defect it was built to show, under the
right name and severity; no host reports a defect it does not have, with the two limits stated at the end.

## Host by host

| Host | It demonstrates | Before | Now |
|---|---|---|---|
| expired | an expired leaf | CERT.EXPIRED | same: correct |
| wrong.host | a name mismatch | CERT.NAME-MISMATCH | same |
| self-signed | a self-signed leaf | CERT.UNTRUSTED ("self-signed") | same |
| untrusted-root | an unknown root | CERT.UNTRUSTED | same, with "a root the stores checked do not hold" |
| revoked | a revoked certificate | Revocation **Good**, nothing said | Revocation **unknown**: no staple, the CA is not contacted (REQ-ACC-5) |
| incomplete-chain | a missing intermediate | CERT.CHAIN-INCOMPLETE | same: the leaf names its issuer's URL (AIA), so the intermediate is truly missing |
| sha384, sha512, extended-validation, 1000-sans, 10000-sans | (now) an expired leaf under a retired root | EXPIRED, and UNTRUSTED saying "a private or unknown root: add it to your CA bundle" | EXPIRED, and UNTRUSTED saying "either a private CA or a public root since removed" (REQ-ACC-6). DigiCert Global Root CA is absent from both bundled store snapshots. |
| superfish, edellroot, dsdtestprovider | a root whose private key is public | EXPIRED, **CHAIN-INCOMPLETE** (wrong) and UNTRUSTED | **CERT.COMPROMISED-ROOT** (critical) and EXPIRED (REQ-ACC-3, 4) |
| preact-cli, webpack-dev-server | a dev-server certificate with a published key | CHAIN-INCOMPLETE (wrong), UNTRUSTED | UNTRUSTED with "a root the stores checked do not hold" (the published key itself is not detectable: see limits) |
| 3des, rc4, rc4-md5, null, dh480, dh512 | weak suites | correct suite findings, but the **certificate was the no-SNI fallback** | on a new scan, the served `*.badssl.com` certificate, read from the raw TLS 1.2 flight (REQ-ACC-1); checked on 3des, rc4 and null, scanned again |
| dh480, dh512, dh1024 | a small DH prime | KX.DH-SMALL (high, high, medium) | same |
| dh2048 | a 2048-bit group (good) | nothing | nothing |
| dh-composite | a composite modulus | KX.DH-SMALL "a small prime, 2047 bits" (wrong word) | **KX.DH-COMPOSITE** (critical), no "small prime" (REQ-ACC-2) |
| dh-small-subgroup | a prime that is not a safe prime | nothing | **KX.DH-NOT-SAFE-PRIME** (low) |
| static-rsa | RSA key transport only | NO-FS, plus NULL and RC4 | same: the host's `kRSA` cipher string does accept the NULL and RC4 RSA suites |
| null | NULL and anonymous suites | CIPHER.NULL, CIPHER.ANON (critical) | same, now with the served certificate |
| mozilla-modern, -intermediate, -old | Mozilla's three profiles | as expected (old: DH-SMALL, 3DES; modern: HSTS short only) | same |
| no-sct | no CT proof | CT.NO-SCTS (with EXPIRED: the certificate has since expired) | same |
| client, client-cert-missing | a client-certificate request | FEAT.CLIENT-CERT-REQUESTED (info) | same |
| hsts, preloaded-hsts | HSTS | HTTP.HSTS-SHORT where max-age < 1 year | same |
| tls-v1-0, tls-v1-1, tls-v1-2 | one version each | PROTO.NO-TLS12 for 1.0 and 1.1 | same |
| www.google.com | (a large site) | RSA suites with "?" key exchange and signature | the RSA-2048 certificate is now read; TLS 1.3 suites rate *Acceptable* (112-bit authentication for a client offering only RSA signatures), consistent with rating every suite by the weakest key it can be authenticated with |

## Numbers and claims checked

- **Primality.** `math/big.ProbablyPrime(20)`: Miller-Rabin with 20 random bases plus Baillie-PSW, which has no
known counterexample. A safe prime is p with (p-1)/2 prime; RFC 7919's groups are safe primes, and RFC 5114's
are not but publish their subgroup order, so a prime matching a published group is not flagged.
- **A composite modulus.** The discrete logarithm modulo a composite reduces to logarithms modulo its factors
(Pohlig-Hellman over the factorisation), so a 2047-bit composite gives far less than 2047-bit security.
Critical, and Logjam-labelled: an attacker able to factor or who chose the modulus can read the sessions.
- **Not a safe prime.** Small subgroups exist; a server that reuses its exponent and does not check the peer's
value leaks it piece by piece (CVE-2016-0701, Lim-Lee). Low: modern servers use fresh exponents.
- **Compromised roots.** Superfish (CVE-2015-2077), eDellRoot and DSDTestProvider (Dell, 2015) were installed with
their private keys. Matched by the issuer's common name, which is enough here: the chain is untrusted by every
store either way, and the finding names *why* that matters (any machine that installed the root trusts
certificates anyone can make).
- **The missing-issuer guess.** Public CAs put an AIA caIssuers URL in every leaf they issue from an intermediate
(the CA/Browser Forum Baseline Requirements' certificate profiles); a leaf with none was most likely signed by a root directly. On the owner's data
this separates incomplete-chain (has AIA) from the five rogue roots (none) without error.
- **Revocation.** Without a verified stapled response saying *good*, revocation is unknown to a scanner that does
not contact the CA. Let's Encrypt no longer runs OCSP at all (2025), so a CRL URL alone is now common.

## Defects found and fixed in this branch

1. The no-SNI fallback certificate stood in for the served one on servers Go's client cannot reach (scanner).
2. Google's RSA certificate was never read (scanner): every RSA suite had unknown parts.
3. Rogue-root leaves were "missing an intermediate" (evaluation).
4. Compromised roots were not named (evaluation).
5. Revocation read "good" with no evidence (evaluation, a new section status `unknown`).
6. The untrusted-root advice was wrong for public roots since removed (wording).
7. A composite DH modulus passed as "a small prime"; a non-safe prime was not seen (scanner and evaluation).
8. The weakest-strength figure chose RSA-2048 over 3DES at equal bits; at equal bits a disallowed algorithm is
now the weaker (screen).

## Limits, stated on the page or in Help

- The CA's OCSP responder and CRL are not contacted (PRD-015, out of scope); revocation says *unknown*.
- A leaf whose private key was published with a tool (preact-cli, webpack-dev-server) is not recognised as such;
it reads *untrusted root*, which is true.
- Scans made before this version keep the certificate they recorded; scanning again reads the served one.

## Sign-off

Signed off for PRD-015 REQ-ACC-10, cryptography.
2 changes: 1 addition & 1 deletion docs/product/PRD-013-visual-rebuild.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ Results: QA-REPORT-012.
| **REQ-VIS-10** | **SSH report** algorithm tables use the bits bar and the post-quantum glyph. | | `script:testing/e2e/visual-phase.mjs#E2E-VIS-6` | P2 |
| **REQ-VIS-11** | **Fit and access** (§4): no sideways scroll at 1440, 1100, 860 and 375 px; every control reachable by Tab; dark mode; contrast AA. | | `script:testing/e2e/visual-phase.mjs#E2E-VIS-7` | P0 |
| **REQ-VIS-12** | **Sign-off.** A cryptographer checks every number and claim the new screens show, against its source (CRYPTO-REVIEW-001); the PM signs off the walk (QA-REPORT-012). | | `report:docs/product/CRYPTO-REVIEW-001-visual-rebuild.md` | P0 |
| **REQ-VIS-13** | **Versions fold** (owner request, 2026-10-21): each TLS version that is on is open only when it needs a look (an old version on, a suite below Strong, or the only version); folded, its head says how its suites rate; one button shows or hides every version and says which it will do. | A reviewer sees first what there is to do | `script:testing/e2e/visual-phase.mjs#E2E-VIS-FOLD` | P1 |
| **REQ-VIS-13** | **Versions fold** (owner request, 2026-10-21): every TLS version that is on starts folded (the owner's second request, 2026-10-22; the only version opens by itself); folded, its head says whether it should be on and how its suites rate; one button shows or hides every version and says which it will do. | A reviewer sees first what there is to do | `script:testing/e2e/visual-phase.mjs#E2E-VIS-FOLD` | P1 |
54 changes: 54 additions & 0 deletions docs/product/PRD-015-accuracy-on-real-scans.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# PRD-015 · Accuracy on real scans

Status: draft 2026-10-22, from the owner's review of their own database (55 scans, mostly the badssl.com test
hosts, plus google.com, qualys.com and their own sites): "a lot of things don't make sense", "the alignment on many
of them is so bad", "things to fix looks broken", "the menus of TLS should be collapsed rather than expanded".
Reviewed as a cryptographer ([CRYPTO-REVIEW-003](CRYPTO-REVIEW-003-real-scans.md)), as QA and as the PM
([QA-REPORT-014](../program/QA-REPORT-014.md)).

## Why

badssl.com is built so that each host shows one defect; it is the fairest test of whether the report says the
right thing. On the owner's scans it did not, in six ways a cryptographer would catch at once:

1. **The wrong certificate.** On every server whose only suites Go's TLS client will not offer (3des, rc4, rc4-md5,
null, dh480, dh512 at badssl.com), the only certificate the report held was the server's *no-SNI fallback*. The
certificate section described a certificate the host never serves. Google's RSA certificate was never seen
either, so every RSA suite showed "?" for its key exchange and signature.
2. **The wrong diagnosis.** A leaf signed straight by an unknown root (superfish, edellroot, dsdtestprovider,
preact-cli, webpack-dev-server) was reported as a *missing intermediate*, with the advice to serve one.
3. **A known-compromised root not named.** Superfish, eDellRoot and DSDTestProvider are roots whose private keys
were published; the report said only "untrusted".
4. **"Good" for revocation that was never checked.** revoked.badssl.com read Revocation: Good. The scan never
contacts the CA, so the status is unknown.
5. **Advice that is wrong.** A chain ending at DigiCert Global Root CA (sha384, sha512, extended-validation), a
public root absent from both bundled store snapshots (Mozilla retires website trust for roots past 15 years
under its root-age policy), was "a private or unknown root: add it to your CA bundle".
6. **DH parameters judged by size alone.** dh-composite (a composite modulus) and dh-small-subgroup (a prime that
is not a safe prime) passed or were called "a small prime".

And on the screens: Fix first listed every finding with two labels each (severity and urgency), rows and version
heads did not line up, and every TLS version opened by default.

## Requirements

| ID | Requirement | Acceptance | Test | Pri |
|---|---|---|---|---|
| **REQ-ACC-1** | **The served certificate is always read.** When crypto/tls cannot complete a handshake (3DES, RC4, NULL, small DH, an RSA certificate a server keeps for RSA-only clients), the chain is read from the raw TLS 1.2-and-earlier flights the suite enumeration receives, where the Certificate message is plaintext. The no-SNI comparison runs after. | A server with only 3DES and RC4 suites has its SNI chain, sourced `raw-tlsv1.2` | `TestCertificateIsReadFromARawFlight` | P0 |
| **REQ-ACC-2** | **DH parameters are tested, not just measured.** The modulus is tested for primality and, when prime, for being a safe prime. A composite modulus is a critical finding (and not also "a small prime"); a prime that is not a safe prime and not a published group is a low one. | ffdhe2048 is a safe prime; 2^127-1 is prime, not safe; a product of two primes is composite | `TestDHPrimality`, `TestEveryRuleFiresAndIsCited` | P0 |
| **REQ-ACC-3** | **The missing issuer is named for what it most likely is.** "Missing intermediate" only when the served path stops at a certificate that names where its issuer can be downloaded (AIA caIssuers, as public CAs' leaves do); otherwise "a root the stores checked do not hold". | A private root's leaf served alone is untrusted, not incomplete | `TestT1TrustMatrix` | P0 |
| **REQ-ACC-4** | **Known-compromised roots are named.** A chain issued under Superfish, eDellRoot or DSDTestProvider is CERT.COMPROMISED-ROOT (critical, clients fail), in place of "untrusted". | | `TestCompromisedRootIsItsOwnFinding` | P0 |
| **REQ-ACC-5** | **Revocation is "unknown" unless a staple vouches for it.** With no stapled, verified, good OCSP response, the revocation area's status is *unknown* (a new section status), with the reason; its checks still count as run. The page says "not contacted: revocation status unknown". | | `TestRevocationIsUnknownWithoutAStaple` | P0 |
| **REQ-ACC-6** | **An untrusted root is described truthfully**: "a root the stores checked do not hold: either a private CA, or a public root since removed from them". | | `TestT1TrustMatrix` | P1 |
| **REQ-ACC-7** | **Fix first is a list of priorities**: the first three rows, each with one chip saying when to act (coloured by severity, the severity in its tooltip); the rest folded under one line that counts them by severity; numbering runs on. | | `script:testing/e2e/visual-phase.mjs#E2E-FIX-1` | P0 |
| **REQ-ACC-8** | **Every TLS version starts folded**, its head saying whether it should be on and how its suites rate (the only version opens by itself). | | `script:testing/e2e/visual-phase.mjs#E2E-VIS-FOLD` | P0 |
| **REQ-ACC-9** | **Things on one line are on one line**: Fix first chips beside their titles, titles in one column; each strength chip centred on its suite row; version heads on one grid; no empty rank column when the client picks the order; the weakest strength prefers a disallowed algorithm at equal bits. Measured in the browser. | | `script:testing/e2e/visual-phase.mjs#E2E-VIS-ALIGN` | P0 |
| **REQ-ACC-10** | **Sign-off**: the cryptographer checks every finding the owner's scans produce against what each badssl.com host demonstrates (CRYPTO-REVIEW-003); QA and the PM walk the screens on the owner's data (QA-REPORT-014). The evaluation is eval/12, so stored scans are re-evaluated on start. | | `report:docs/product/CRYPTO-REVIEW-003-real-scans.md`, `report:docs/program/QA-REPORT-014.md` | P0 |

## Out of scope

- Contacting the CA's OCSP responder or CRL. The URLs come from the certificate, which the target chose; fetching
them is a request to an address an attacker picks, and stays a separate, opt-in decision.
- Detecting a published private key for a leaf (the preact-cli and webpack-dev-server certificates): it needs a
list of key fingerprints this product does not carry. Those hosts read "untrusted root", which is true.
- Reports scanned before this version keep the certificate they recorded: scan them again to read the served one.
Loading
Loading