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
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ add_executable(mithril
src/certid.cpp
src/pubkey.cpp
src/bigint.cpp
src/ecc.cpp
src/keyweak.cpp
src/keycorpus.cpp
src/keys.cpp
Expand Down Expand Up @@ -102,6 +103,7 @@ add_executable(mithril_unit
src/certid.cpp
src/pubkey.cpp
src/bigint.cpp
src/ecc.cpp
src/keyweak.cpp
src/keycorpus.cpp
src/keys.cpp
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**IoT static scanner for secrets, SBOM, CVEs and more.**

mithril analyzes the *contents* of firmware and IoT software, whether a single file or an unpacked rootfs. It looks for embedded secrets and keys, a software bill of materials (SBOM), known CVEs affecting those components, open-source licenses, boot-security posture (U-Boot env, device tree, FIT/AVB/UEFI verified boot), and weak or leaked public keys (keys whose private half is public, ROCA, undersized RSA). It reports each finding with its evidence and a confidence, and it speaks clean JSON so scripts and LLM agents can drive it as easily as people can.
mithril analyzes the *contents* of firmware and IoT software, whether a single file or an unpacked rootfs. It looks for embedded secrets and keys, a software bill of materials (SBOM), known CVEs affecting those components, open-source licenses, boot-security posture (U-Boot env, device tree, FIT/AVB/UEFI verified boot), and weak or leaked public keys (keys whose private half is public, ROCA, undersized RSA, near-zero EC scalars). It reports each finding with its evidence and a confidence, and it speaks clean JSON so scripts and LLM agents can drive it as easily as people can.

A scan makes no network calls. Secrets, SBOM, and license analysis are fully offline, and the CVE pass reads a local vulnerability mirror you refresh out of band.

Expand All @@ -20,7 +20,7 @@ It has no build-time dependency on moria. Point it at any file or directory.
- **Firmware-aware SBOM.** Beyond package databases (dpkg/opkg/apk/rpm), it recovers components from ELF version banners, versioned libc filenames, and the kernel banner, so it finds openssl/busybox/uClibc even in a stripped image with no package manager. Emitted as CycloneDX and SPDX.
- **CVEs it owns end to end.** A local OSV + NVD/CPE mirror plus a curated, version- and kconfig-gated kernel-CVE checklist (high-signal, not exhaustive: an empty kernel result means none of the curated set is in range, not that the kernel is clean). Every match records how sure it is (exact version vs affected range vs CPE), stays release-precise where `/etc/os-release` allows, and is annotated with CISA KEV and EPSS.
- **Honest by construction.** Secrets ride a deterministic offline ladder: `pattern` (shape), then `structural` (a parsed JWT or PEM key), then `validated` (a recomputed checksum, such as GitHub tokens and crypt hashes). mithril asserts well-formed, never "live," and an empty result is a real answer rather than a tool failure.
- **Weak and leaked keys, proven offline.** For every public key it recovers (certs, SSH host/authorized keys, embedded keys) it checks whether the private half is obtainable: a fingerprint match against a compiled-in corpus of hardcoded/default keys whose private half is public (rapid7 ssh-badkeys, the Vagrant insecure key), the ROCA fingerprint (CVE-2017-15361), undersized/broken RSA, and factoring recoveries that hand you a factor outright (Fermat close primes, batch-GCD shared primes across the image, Wiener small-`d`) via a small in-tree bignum. A hit means impersonation, login, or forgery, not just a weak cipher suite.
- **Weak and leaked keys, proven offline.** For every public key it recovers (certs, SSH host/authorized keys, embedded keys) it checks whether the private half is obtainable: a fingerprint match against a compiled-in corpus of hardcoded/default keys whose private half is public (rapid7 ssh-badkeys, the Vagrant insecure key, the compromised eMudhra intermediate CAs), the ROCA fingerprint (CVE-2017-15361), undersized/broken RSA, factoring recoveries that hand you a factor outright (Fermat close primes, batch-GCD shared primes across the image, Wiener small-`d`), and the EC analog: a near-zero EC private scalar recovered by a bounded `d*G` search (the October 2026 eMudhra P-384 compromise) — all via a small in-tree bignum. A hit means impersonation, login, or forgery, not just a weak cipher suite.
- **Fully offline and reproducible.** No network at scan time; the CVE mirror is refreshed by an explicit, separate step. Air-gappable.
- **Safe on hostile input.** Untrusted bytes go through one bounds-checked reader. Parsers degrade to a clean error rather than crash, and the CVE index is memory-mapped so a scan never parses the whole thing.

Expand Down Expand Up @@ -50,7 +50,7 @@ mithril --licenses <dir> # licenses only
mithril --license-paths <dir> # licenses, with each license's file locations listed
mithril --boot <dir> # boot-security intel: U-Boot env, device tree, FIT/AVB, UEFI Secure Boot
mithril --boot bios.bin # UEFI Secure Boot posture from a raw AMI/EDK2 BIOS (docs/boot-security.md)
mithril --keys <dir> # public-key weakness: leaked/default keys, ROCA, weak RSA, factoring
mithril --keys <dir> # public-key weakness: leaked/default keys, ROCA, weak RSA, factoring, EC small-scalar
mithril --rules my.json <dir> # add user-defined rules (docs/user-rules.md)
mithril --fetch-db # FIRST RUN: download the CVE mirror (a networked command)
mithril --fetch-db --with-kernel-feed # also fetch the optional full kernel.org CVE feed
Expand Down
5 changes: 5 additions & 0 deletions THIRD_PARTY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ Public keys whose private half is public, fingerprinted into `src/keycorpus_corp
- **HashiCorp Vagrant insecure key** — `data/keycorpus/vagrant/vagrant.pub`. The default "insecure" key
Vagrant ships as an authorized key; its private half is published in the vagrant repo.
License: MIT (HashiCorp). Source: <https://github.com/hashicorp/vagrant> (`keys/`).
- **eMudhra compromised intermediate CAs (October 2026)** — `data/keycorpus/emudhra/*.crt`. The eight
eMudhra P-384 intermediate CA certificates whose private keys were generated with a broken RNG (tiny
scalars 4–237) and are trivially recoverable. These are the real, public, Certificate-Transparency-logged
certificates; their SHA-256 fingerprints were verified against the Mozilla incident bug before vendoring.
Public certificates only (no private keys). Source: Mozilla bug 2083449 / CT logs.

## Boot-key corpus — `data/bootkeys/`

Expand Down
Binary file added data/keycorpus/emudhra/EMDVTLSCAG3B.crt
Binary file not shown.
Binary file added data/keycorpus/emudhra/EMDVTLSCAG3C.crt
Binary file not shown.
Binary file added data/keycorpus/emudhra/EMEVTLSCAG3B.crt
Binary file not shown.
Binary file added data/keycorpus/emudhra/EMEVTLSCAG3C.crt
Binary file not shown.
Binary file added data/keycorpus/emudhra/EMOVCSCAG3B.crt
Binary file not shown.
Binary file added data/keycorpus/emudhra/EMOVCSCAG3C.crt
Binary file not shown.
Binary file added data/keycorpus/emudhra/EMOVTLSCAG3B.crt
Binary file not shown.
Binary file added data/keycorpus/emudhra/EMOVTLSCAG3C.crt
Binary file not shown.
60 changes: 53 additions & 7 deletions docs/key-weakness.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ Two failure classes, following the standard taxonomy (see the research survey th
Detection is a fingerprint lookup against a curated corpus.
- **Generation** — the private key is computable from the public key because of bad randomness or
structured primes. ROCA (CVE-2017-15361), structural weaknesses (undersized modulus, broken exponent),
and the factoring recoveries that actually produce a factor: Fermat close primes, batch-GCD shared
primes, and Wiener small-`d`. The factoring recoveries use a small in-tree bignum (`bigint.{hpp,cpp}`),
the same zero-dep choice as SHA-256/CRC32/DEFLATE.
the factoring recoveries that actually produce a factor (Fermat close primes, batch-GCD shared primes,
Wiener small-`d`), and the EC analog: a near-zero EC private scalar recovered by a bounded `d*G` search
(the October 2026 eMudhra intermediate-CA compromise). The factoring recoveries and the EC arithmetic use
a small in-tree bignum (`bigint.{hpp,cpp}`), the same zero-dep choice as SHA-256/CRC32/DEFLATE.

## What it recovers keys from

Expand All @@ -28,10 +29,12 @@ Two failure classes, following the standard taxonomy (see the research survey th
- **Private keys** — the public half (modulus `n`, exponent `e`) is pulled from PEM `PRIVATE KEY` blocks
(PKCS#1 and PKCS#8 RSA), so an embedded private key is checked for weakness too.

For an RSA key it keeps the modulus `n` (big-endian, leading zeros stripped) and the exponent `e`; every
generation check keys on `n`. Non-RSA keys are still recorded so a disclosure (corpus) match still fires.
All DER parsing is a bounded ASN.1 TLV walk (shared shape with `certid.cpp`/`derkey.cpp`); base64 and the
SSH wire decode are bounds-checked. Keys with no issue are **not reported** (the pass stays high-signal).
For an RSA key it keeps the modulus `n` (big-endian, leading zeros stripped) and the exponent `e`; the RSA
generation checks key on `n`. For an EC key on a recognized curve it keeps the affine public point
(`ec_x`/`ec_y`) so the EC small-scalar check can run. Keys of other types are still recorded so a
disclosure (corpus) match still fires. All DER parsing is a bounded ASN.1 TLV walk (shared shape with
`certid.cpp`/`derkey.cpp`); base64 and the SSH wire decode are bounds-checked. Keys with no issue are
**not reported** (the pass stays high-signal).

## The three checks

Expand All @@ -55,6 +58,10 @@ The corpus is generated by `tools/gen_keycorpus.py` from the vendored **public**
- `rapid7/ssh-badkeys` `host/` + `authorized/` `.pub` files (hardcoded appliance keys; F5 BIG-IP, ExaGrid,
Ceragon, Barracuda, Cisco, Comtrend, ..., many with a CVE).
- The HashiCorp Vagrant insecure key (private key published in the vagrant repo).
- The 8 eMudhra intermediate CA certificates with compromised keys (October 2026), keyed by `fp_der`
(SHA-256[0:32] of the cert DER). The vendored `.crt` files are the real public CT-logged certificates;
their SHA-256 fingerprints were verified against Mozilla bug 2083449. This names the specific certs when
a trust store bundles them; the general EC small-scalar check (below) recovers the key in any wrapper.

Regenerate after changing the vendored sources; `tools/gen_keycorpus.py --check` (wired into `run.sh`)
fails if `src/keycorpus_corpus.inc` is stale. See `THIRD_PARTY.md` for attribution.
Expand Down Expand Up @@ -110,6 +117,31 @@ sub, mul, bit-by-bit divmod, Euclid GCD, digit-by-digit isqrt, perfect-square te
The per-key factoring checks (Fermat, Wiener) are guarded to `512 ≤ bits ≤ 16384` so an oversized modulus
cannot make a scan superlinearly slow.

### 5. EC small private scalar (`ec-small-scalar`, high, `validated`)

`src/ecc.cpp`. The EC analog of the factoring recoveries: the one real-world EC generation failure is a
**catastrophically small private scalar** — the key is generated as `Q = d*G` for a tiny integer `d`, so
`d` is recovered by a bounded search (`d = 1, 2, 3, ...`, comparing `d*G` to the public point `Q`). This is
the **October 2026 eMudhra** intermediate-CA compromise: eight P-384 CA certs whose private scalars were
`4, 56, 66, 115, 155, 156, 230, 237` (3–8 bits instead of ~384). The vendored eMudhra certs are the test
anchor; the search recovers each published scalar exactly.

- **Curves:** NIST P-256 / P-384 / P-521 and secp256k1 (short-Weierstrass, `a = p-3` for the NIST curves,
`a = 0` for secp256k1). Domain parameters are compiled in and unit-tested (`G` on curve, known `d*G`).
- **Arithmetic:** the in-tree `BigUint`, with **Barrett reduction** (precompute `mu = floor(2^(2k)/p)` once
per key) so each modular multiply is two multiplications, not the bit-by-bit divmod — the small-scalar
walk would be minutes per key otherwise. The walk accumulates `d*G` in Jacobian coordinates (no modular
inverse in the hot loop) and compares to `Q` by cross-multiplication.
- **Bound:** `kEcSmallScalarLimit = 2^12` per key (~17× the eMudhra max of 237). A legitimate scalar needs
~2^128 for any security, so a hit within this bound is proof of a broken key, never a lucky guess. A
healthy EC key walks the full range (a miss) cheaply (~20 ms on P-384, Release). A per-file step budget
(`kEcFileStepBudget`) gives a normal trust store full reach while bounding a pathological many-EC-cert
file; every key keeps at least a `kEcMinScan=512` floor (still > 2× the eMudhra max). Only a well-formed
point (coordinates no larger than the field) is analyzed, which also bounds the reducer's input.
- **Reach / future work:** this reaches only the "broken RNG produced a near-zero scalar" floor. A
baby-step/giant-step search (`O(sqrt(limit))`) would extend the reach cheaply; a fast per-curve reduction
(Solinas) would make a much larger bound affordable. Both deferred.

## Output

Human mode prints a severity-sorted, deduped list (like the boot pass): `high`/`med`/`info` tag, the
Expand Down Expand Up @@ -138,6 +170,20 @@ issue produce no finding.
`fermat-factorable`, a small-`d`/large-`e` key → `wiener-weak` (and asserts the large `e` is not
misreported as `rsa-broken-exponent`), and a shared-prime pair across two files → `shared-prime` on both.

## Testing (EC small-scalar)

- `tests/unit/unit_tests.cpp::test_ecc` — curve lookup (incl. the OpenSSH/alias names), `G` on curve,
known-answer `d*G` recovery on all four curves (the P-384 `d=4` vector is the real eMudhra
`EM OV TLS CA - G3C` point), a below-the-scalar bound that must miss, a tampered off-curve point, and an
end-to-end DER-SPKI → `scan_keys` → `ec-small-scalar`.
- `tests/test_keys.py` — synthetic `Q = d*G` SPKIs for `d ∈ {1,5,255}` on each curve (recovered `d`
asserted), healthy openssl EC keys silent (false-positive guard), and the **8 real eMudhra
intermediates** each flagged `ec-small-scalar` with the exact published scalar and named by the corpus.
- Validated at scale: a raw `--keys` sweep over the full local firmware corpus (2868 samples) produced zero
`ec-small-scalar` findings and zero crashes (false-positive / robustness check); the EC path is also in
the `scan_keys` fuzz target. Note the raw-image sweep only sees certs in uncompressed regions; the
intended flow unpacks the rootfs first (`moria -e` → mithril), where each cert is its own file.

## Remaining (deferred): Debian OpenSSL blocklist

The one Phase-2 item still deferred is the **Debian OpenSSL weak-key** set (CVE-2008-0166): a fingerprint
Expand Down
4 changes: 3 additions & 1 deletion fuzz/fuzz_secrets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
// Build (from the mithril dir):
// clang++ -std=c++20 -O1 -g -fsanitize=fuzzer,address -I src \
// src/ahocorasick.cpp src/validators.cpp src/rules_builtin.cpp src/engine.cpp \
// src/sbom.cpp fuzz/fuzz_secrets.cpp -o fuzz/fuzz_secrets
// src/sbom.cpp src/pubkey.cpp src/certid.cpp src/bigint.cpp src/ecc.cpp \
// src/keyweak.cpp src/keycorpus.cpp src/keys.cpp src/sha256.cpp \
// fuzz/fuzz_secrets.cpp -o fuzz/fuzz_secrets
// Run: ./fuzz/fuzz_secrets -max_total_time=60
#include <cstddef>
#include <cstdint>
Expand Down
7 changes: 5 additions & 2 deletions mithril.1
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,11 @@ Identify open-source licenses.
Boot-security intel: U-Boot environment, device tree, and FIT/AVB/UEFI verified-boot posture.
.TP
.B \-\-keys
Public-key weakness: keys whose private half is public (hardcoded/default keys),
the ROCA fingerprint (CVE-2017-15361), and undersized or broken RSA keys.
Public-key weakness: keys whose private half is public (hardcoded/default keys,
the compromised eMudhra intermediate CAs), the ROCA fingerprint (CVE-2017-15361),
undersized or broken RSA keys, RSA factoring recoveries (Fermat/batch-GCD/Wiener),
and near-zero EC private scalars recovered by a bounded search (the Oct 2026
eMudhra P-384 compromise).
.TP
.BR \-A ", " \-\-all
Run every pass. This is the default when no pass is named.
Expand Down
Loading
Loading