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

## [Unreleased]

### Added

- **Classical strength for every detected algorithm** ([PRD-010](docs/product/PRD-010-classical-strength.md),
[ENH-001](docs/product/ENH-001-cipher-strength.md)). Strengths are computed with NIST's own methods, and the tests
reproduce NIST's published tables:
- RSA and finite-field DH by the General Number Field Sieve estimate (FIPS 140-3 IG D.B, SP 800-56B Rev. 2
Appendix D);
- elliptic curves at half the order (SP 800-186);
- ciphers and hashes by SP 800-57 Part 1 Tables 2 and 3;
- post-quantum algorithms by their NIST category.

Where it shows:
- **TLS report and PDF:** each cipher suite's word now comes from the server and is as strong as its weakest
part (cipher, key exchange, certificate key, MAC), with its bits and that part named. Groups and certificate
keys show their bits.
- **API:** the evaluation (`eval/9`) carries `suiteStrength` and `keyStrength`; stored scans are re-evaluated
on start.
- **CLI:** `tls scan --details` prints each suite's and group's strength.
- **Inventory:** a *Classical strength* column, and `classicalStrength` on every algorithm and key row of
`/api/v1/inventory`.
- **SSH reports** (page and PDF): a strength for every key exchange, host key (by the presented key's size),
cipher and MAC.
- **CBOM:** `classicalSecurityLevel` on every algorithm with a strength (1.6 and 1.7).

### Changed

- **Suite strength words follow the weakest-part rule**, so some suites read differently:
- a suite authenticated by an RSA-2048 certificate (112 bits) is now *Acceptable*, not *Strong*;
- a suite with a 1024-bit DH prime is *Weak*.

The words are defined in docs/methodology.md and explained in Help.

### Fixed

- **Single-DES suites (56-bit key) are Insecure, not Weak** (#18).
- **A running scan's connection count could go backwards.** Concurrent probes reported counts out of order, and
progress writes could land out of order. The count now only moves forward, and writes are serialised. This was
found by a CI run of the workflow test.

### Security

- Built with Go 1.27.3 (GO-2026-6809). `crypto/x509` now distrusts compromised intermediate certificates, and the
trust check uses it.

## [0.3.0] - 2026-10-17

### Changed
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# The build stage always runs on the builder's native platform and cross-compiles, so a
# multi-arch image needs no emulation.

FROM --platform=$BUILDPLATFORM golang:1.27.2@sha256:5bc7f572bbaa98885a3a1fd9c0aa76b59e3e14e8628bfc316bbfd0c701e4818c AS build
FROM --platform=$BUILDPLATFORM golang:1.27.3@sha256:6928d94d1142af58816fed5a62338d605a266ccd53a0deaab6b1096f19059261 AS build
ARG TARGETOS
ARG TARGETARCH
ARG MODULE=github.com/offsetsecurity/cipherhorizon
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ CycloneDX 1.7 CBOM, with PDF reports for each endpoint and for the whole invento
|---|---|
| **TLS endpoints** | Every protocol from SSLv2 to TLS 1.3, every cipher suite and key-exchange group (hybrids such as `X25519MLKEM768` included), the TLS 1.3 signature schemes (ML-DSA included), the certificate chain against the Mozilla and Chrome root stores or your own CA, OCSP stapling, Certificate Transparency and HSTS. STARTTLS for SMTP, IMAP, POP3, FTP, XMPP, LDAP and PostgreSQL. 55 cited checks and the TLS hygiene score. |
| **SSH servers** | Identification and algorithm lists, negotiation as OpenSSH 10.6 would (the ML-KEM hybrid included), every host key fetched and verified (Ed25519, ECDSA, RSA, ML-DSA and the composite `ssh-mldsa44-ed25519`), 18 cited checks, and the CipherHorizon SSH baseline 2026 with the `sshd_config` lines to meet it. It never logs in. |
| **Classical strength** | Every algorithm, key and cipher suite in bits, computed with NIST's own methods (the GNFS estimate for RSA and DH, half the order for curves, SP 800-57 for ciphers and hashes). A suite is as strong as its weakest part and names it, on the report, the PDF, the CLI, the Inventory, SSH reports and the CBOM (`classicalSecurityLevel`). |
| **Post-quantum readiness** | L0 to L4 for every TLS endpoint and SSH server, each protocol by its own stated rules, with harvest-now-decrypt-later exposure. |
| **Machines** | Two readable inventory scripts (PowerShell, shell) list a machine's cryptographic software and certificates; nothing is installed and no key material is read. |
| **Inventory** | TLS endpoints, SSH servers, machines and imported CBOMs merged into one CycloneDX 1.7 (or 1.6) CBOM: every algorithm with its OID where one exists (the exact certificate signature algorithm, ECDH on each named curve, legacy ciphers such as 3DES and RC4), certificates with their serial and SHA-256 fingerprint. |
Expand Down
1 change: 1 addition & 0 deletions docs/cbom-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ stretched:
| `iana-obsolete` | algorithm | a group IANA marks obsolete, e.g. the pre-standard `X25519Kyber768Draft00` |
| `tls-graph` | document | what the graph's edges mean (above) |
| `evidence` | every component | how it was learned: `observed` (a ServerHello; a certificate and what it states), `accepted` (a probe offering only that group or signature scheme was answered), `derived` (implied by an accepted suite's IANA registry entry: its cipher, key exchange, authentication, MAC or hash), `negotiated` (used by the one handshake a current client completed). Several can apply. Never a probability (AUDIT-TLS-CBOM-001 A-5) |
| `classicalSecurityLevel` (CycloneDX field) | algorithm | the classical strength in bits from NIST's methods (GNFS for RSA and DH, half the order for curves, SP 800-57 tables for ciphers and hashes; docs/methodology.md "Classical strength"). Not set for a broken algorithm (its `classical-status` says why) or one whose size the component does not state (`RSA`, `AES`) |
| `negotiated-in` | algorithm | the version of the handshake that used it (`TLSv1.3`); `negotiated` stays `true` |
| `scan-complete`, `coverage:<stage>` | document | what the scan covered, per stage: `done`, `done; inconclusive: …`, `not tested: …` (switched off), `not applicable: …`, `not reached: …` (an earlier step failed), `not tested (budget)`, `inconclusive: …`. An absent component means "not accepted" only where its stage is `done` (A-6) |
| `mapping` | document | the IANA registries (with retrieval dates) and the catalogue the derived facts were mapped with |
Expand Down
32 changes: 32 additions & 0 deletions docs/methodology.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,38 @@ takes every finding except info, orders it by severity, then by whether it stops
untrusted, name mismatch, missing intermediate, no serverAuth, Must-Staple missing, revoked), then by section order,
and shows the first three distinct rules.

## Classical strength (eval/9, PRD-010)

Every algorithm the product lists has a classical security strength in bits, computed with NIST's own methods.
Where no number applies, it says why instead: a cited break, or no rating. The derivation, with NIST's tables
reproduced to the decimal, is in [ENH-001 §5](product/ENH-001-cipher-strength.md#5-how-the-strength-is-calculated-nists-method-reproduced).
The code is `internal/algo/strength.go` and `strength_of.go`.

| What | Method | Source |
|---|---|---|
| RSA modulus, finite-field DH prime | General Number Field Sieve: E(L) = (1.923·∛(L·ln 2)·∛((ln(L·ln 2))²) − 4.69)/ln 2, rounded to the nearest multiple of eight, at most 256. 1024 → 80, 2048 → 112, 3072 → 128, 4096 → 152, 8192 → 200 | FIPS 140-3 IG D.B; SP 800-56B Rev. 2 Appendix D, Table 4; SP 800-56A Rev. 3, Table 26 |
| Elliptic curves | Half the bit length of the base point's order, taken to the nearest strength SP 800-186 assigns (80, 112, 128, 192, 224, 256): P-256 and Curve25519 → 128, P-384 → 192, Curve448 → 224, P-521 → 256 | SP 800-186 §3 and Table 1 |
| Symmetric ciphers | AES-k → k; 3DES (3TDEA) → 112, disallowed for encryption after 2023; DES, RC4, RC2 → broken. Ciphers NIST does not rate (ChaCha20, Camellia, ARIA, SEED, …) get the bound an exhaustive key search sets, labelled as such | SP 800-57 Part 1 Rev. 5 Table 2; SP 800-131A Rev. 2; RFC 7465 |
| Hashes and MACs | Signatures, where collision resistance is needed: SHA-256 → 128, SHA-384 → 192, SHA-512 → 256; SHA-1 and MD5 → broken. HMAC: SHA-1 → 128, SHA-256 and longer → 256 | SP 800-57 Part 1 Table 3 |
| Post-quantum algorithms | The classical equivalent of their NIST category: ML-KEM-512/768/1024 → 128/192/256, ML-DSA-44/65/87 → 128/192/256, SLH-DSA by its level | FIPS 203, 204, 205 |
| Hybrids | Secure while either part is, so their stronger part: X25519MLKEM768 → 192 | RFC 10024, RFC 10042 |

**A TLS cipher suite is as strong as its weakest part.** Its parts are the bulk cipher; the key exchange (the
weakest group the server accepts for that version, the DH prime it sent, or, for RSA key transport, the certificate
key); the authentication (the weakest end-entity key the server presented that the suite can use); and, for a
non-AEAD suite, the MAC.

| Word | When |
|---|---|
| **Insecure** | a part is broken (NULL, export, RC4, single DES, anonymous), or fewer than 80 bits |
| **Weak** | 80 to 111 bits, or a 64-bit block cipher (Sweet32; RFC 9325 §4.2), or no forward secrecy (RFC 9325 §4.1) |
| **Acceptable** | at least 112 bits, but below 128 or not AEAD (RFC 9325 §4.2 prefers AEAD) |
| **Strong** | at least 128 bits in every part, AEAD, forward secrecy |

An RSA-2048 certificate (112 bits) makes every suite it authenticates *Acceptable*: the honest reading of SP 800-57,
and the owner's decision OD-1. Each suite states its weakest part and the reason for its word. The words are this
product's own; the numbers are NIST's.

## SSH

SSH servers share the readiness scale and the findings model. They are **not scored**: an SSH server is judged against
Expand Down
5 changes: 4 additions & 1 deletion docs/product/ENH-001-cipher-strength.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,10 @@ estimate; a 7680-bit key comfortably meets the 192 category. For the thresholds
The best known classical attack on the elliptic-curve discrete logarithm problem is Pollard's rho, about
√(πn/4) ≈ 2^(f/2) operations for a base point of order n with f bits. **SP 800-186 §3**: *"if an elliptic curve has a
basepoint of order n, then the security strength will be approximately one half of the bit length of n"*. We take
f/2, rounded to a multiple of eight, held to 256.
f/2 to the **nearest strength SP 800-186 Table 1 assigns** (80, 112, 128, 192, 224, 256). This replaces an earlier
"round to a multiple of eight", which matched the prime curves but gave K-233 120 bits where NIST says 112. The
nearest-level rule reproduces every row of Table 1, binary curves included: K-233 116 → 112, K-283 140.5 → 128,
K-409 203.5 → 192, K-571 285 → 256.

| Curve (as TLS or SSH names it) | Order f (bits) | f/2 | Strength | SP 800-186 Table 1 |
|---|---|---|---|---|
Expand Down
67 changes: 67 additions & 0 deletions docs/product/PRD-010-classical-strength.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# PRD-010 · Classical strength for every detected algorithm

Status: baselined 2026-10-18. The owner approved [ENH-001](ENH-001-cipher-strength.md) with **OD-1 as recommended**:
a suite is as strong as its weakest part, so an RSA-2048 certificate makes its suites *Acceptable*. The scope
includes the suite rating, the API, the CLI, the Inventory, SSH and the CBOM `classicalSecurityLevel`, and fixes
issue #18.

Problem, evidence and the NIST method: ENH-001 §1, §2 and §5. Status per requirement: [RTM](../program/RTM.md).
Results: QA-REPORT-010.

## 1. Goals

| # | Goal | Measure |
|---|---|---|
| G-1 | Every place that lists an algorithm says how strong it is classically, from one cited model | No surface lists an algorithm without a strength, or a cited reason it has none |
| G-2 | The strength is NIST's, computed the way NIST computes it | Tests reproduce SP 800-56B Table 4, SP 800-56A Table 26, SP 800-186 Table 1 and SP 800-57 Tables 2 and 3 |
| G-3 | A suite's word explains itself | Each suite names its weakest part and its bits |

**Non-goals:**
- a score;
- re-weighting findings or the TLS hygiene score;
- post-quantum strength, which stays in the readiness level and `nistQuantumSecurityLevel`.

## 2. The rating (ENH-001 §3.1, OD-1)

A suite's parts:
- **cipher:** the bulk cipher;
- **key exchange:**
- ECDHE: the weakest group the server accepts for that version;
- DHE: the prime it sent;
- RSA key transport: the certificate key;
- **authentication:** the weakest end-entity key the server presented whose type the suite can use (RSA or ECDSA
in TLS 1.2; any in TLS 1.3);
- **MAC:** for non-AEAD suites.

Its **bits** are the minimum over the parts. Each part is broken or has a strength (`internal/algo/strength.go`).

| Word | When |
|---|---|
| **Insecure** | any part broken (NULL, export, RC4, DES, anonymous, so no authentication), or fewer than 80 bits |
| **Weak** | 80 to 111 bits, or a 64-bit block cipher (Sweet32), or no forward secrecy (RSA key transport, static DH or ECDH) |
| **Acceptable** | at least 112 bits, but below 128 or not AEAD |
| **Strong** | at least 128 bits in every part, AEAD, forward secrecy |

The reason names the weakest part, for example "RSA-2048 certificate key, 112 bits", or the rule that decided the
word ("64-bit block cipher").

## 3. Requirements

| ID | Requirement | Acceptance | Test | Pri |
|---|---|---|---|---|
| **REQ-STR-1** | **The model.** `algo.StrengthOf` gives every catalogue algorithm, and every variant this program derives (RSA-n, ECDSA-curve, ECDH-curve, DSA-n, the signature variants), its classical strength in bits, or a cited broken status, or "not rated" with a reason. Never a guess. | Every catalogue entry is one of the three; the post-quantum ones carry their NIST category's classical equivalent (FIPS 203, 204, 205); a hybrid is as strong as its stronger part (RFC 10024) | `TestEveryAlgorithmHasACitedStrength` | P0 |
| **REQ-STR-2** | **Suite rating** as §2, in `internal/eval`, with the parts, the bits and the reason. Single DES is **Insecure** (#18). | A table test over the IANA registry; the demo targets' suites rate as expected | `TestSuiteStrengthIsTheWeakestPart`, `TestSingleDESIsInsecure` | P0 |
| **REQ-STR-3** | **API.** The evaluation carries `suiteStrength`: per suite, version, ID, word, bits, the weakest part and the reason. It also carries `keyStrength` for every certificate key and accepted group. `eval.Version` becomes `eval/9`, so stored scans are re-evaluated. | `GET /api/v1/scans/{id}/evaluation` shows them | `TestEvaluationCarriesStrength` | P0 |
| **REQ-STR-4** | **Report and PDF read the evaluation.** The two heuristic copies are deleted. The suite table shows the word with bits and the weakest part; the group and certificate-key tables show bits. | Page, PDF and API agree for every suite of every demo target | `TestPDFAndEvaluationAgreeOnStrength`, `script:testing/e2e/strength-phase.mjs#E2E-STR-1` | P0 |
| **REQ-STR-5** | **CLI.** `tls scan --details` shows each suite's word, bits and weakest part, and each group's bits. `--json` includes the evaluation fields. | | `TestDetailsShowSuiteStrength` | P1 |
| **REQ-STR-6** | **Inventory.** Algorithm and key rows carry `classicalStrength`: `bits`, `status` (rated, disallowed, broken, not-rated) and the cited `basis`. The lists show a *Classical strength* column, and no broken algorithm reads only "not classified". | 3DES shows "112 bits, disallowed" (SP 800-131A Rev. 2); RC4 "broken" (RFC 7465); RSA-2048 112; AES-256-GCM 256 | `TestInventoryRowsCarryClassicalStrength`, `script:testing/e2e/strength-phase.mjs#E2E-STR-2` | P0 |
| **REQ-STR-7** | **SSH.** Each advertised algorithm in the evaluation (key exchange, host key, cipher, MAC) carries its strength; host keys use the size of the key fetched. The report shows it. | `diffie-hellman-group14-sha256` 112; `curve25519-sha256` 128; `3des-cbc` 112 disallowed; `hmac-md5` not rated (MD5 is not an approved hash, so SP 800-57 gives it no strength; RFC 6151 does not call HMAC-MD5 broken); an RSA-1024 host key 80 | `TestSSHAlgorithmsCarryStrength` | P1 |
| **REQ-STR-8** | **CBOM.** `algorithmProperties.classicalSecurityLevel` is set from the model for every algorithm component that has a strength (1.6 and 1.7). A broken one keeps `classical-status` and gets no number. | Validates against both schemas | `TestCBOMCarriesClassicalSecurityLevel` | P0 |
| **REQ-STR-9** | **Documentation.** docs/methodology.md has the method, the table and its sources; Help explains the four words; docs/cbom-model.md the field; the CHANGELOG notes the change in words. | | `report:docs/methodology.md` | P1 |

## 4. Release

A minor release (0.4.0), when the owner asks. Visible changes:
- some suites change word (RSA-2048 suites become *Acceptable*; single DES becomes *Insecure*);
- new API fields;
- the CBOM's `classicalSecurityLevel`.
Loading
Loading