Skip to content

feat(keys): detect near-zero EC private scalars (eMudhra P-384 compromise) - #35

Merged
nmatt0 merged 1 commit into
masterfrom
feat/ec-small-scalar-keyweak
Oct 21, 2026
Merged

nmatt0 merged 1 commit into
masterfrom
feat/ec-small-scalar-keyweak

Conversation

@nmatt0

@nmatt0 nmatt0 commented Oct 21, 2026

Copy link
Copy Markdown
Owner

What

The --keys pass could not see a whole class of broken key: EC keys whose
private scalar is tiny enough to recover by brute force. PublicKey discarded
the EC point during parsing, and every weakness check was gated on
algo == "RSA", so an EC key walked straight through.

The October 2026 eMudhra CA compromise made this concrete: eight
publicly-trusted P-384 intermediate CAs were issued with private scalars in the
range 4 to 237 (3 to 8 bits), trivially recoverable by searching d*G == Q.
mithril would have reported nothing on them.

How

  • New self-contained EC module (src/ecc.{hpp,cpp}): short-Weierstrass curves
    (P-256, P-384, P-521, secp256k1), Jacobian point arithmetic (no per-step
    modular inverse), and Barrett reduction so the bounded small-scalar search is
    fast without modifying the shared BigUint.
  • parse_spki and parse_ssh_blob now retain the uncompressed EC point.
  • keys.cpp walks d = 1..limit for d*G == Q under a per-file step budget
    and emits ec-small-scalar (high confidence, validated tier) carrying the
    recovered scalar and curve name.
  • The eight real eMudhra intermediate certificates are vendored into the key
    corpus (public certificates only, no private keys; SHA-256 verified against
    the upstream incident bug) and fingerprinted.

Testing

  • Unit KATs: curve constants, a real recovered point (d=4), plus off-curve and
    below-bound negative cases. 1438 unit checks, 0 failures.
  • Python suite: synthetic d*G across all four curves, all eight real eMudhra
    P-384 scalars recovered end to end, and a healthy-key false-positive guard.
  • Validated across a large firmware test set: no false positives, and the new
    check adds negligible time to a scan.
  • ASan/UBSan clean; fuzz run clean.

Notes

Recovery is bounded by a small scalar limit and a per-file step budget, so the
search stays cheap and cannot hang on a malformed oversized point. Only public
key material is stored; nothing recovers or emits a private key beyond the
already-public scalar value itself.

…mise)

The --keys pass was RSA-only: PublicKey discarded EC points, and every
weakness check was gated on algo=="RSA". An EC key whose private scalar is
tiny (recoverable by d*G==Q search) slipped through. The October 2026
eMudhra CA compromise shipped eight publicly-trusted P-384 intermediates
whose private scalars were 4..237, so this class is not hypothetical.

Add a self-contained EC module (src/ecc.{hpp,cpp}): short-Weierstrass curves
(P-256/384/521, secp256k1), Jacobian point ops with no per-step inverse, and
Barrett reduction so a bounded small-scalar search is fast without touching
the shared BigUint. parse_spki and parse_ssh_blob now keep the EC point, and
keys.cpp walks d=1..limit for d*G==Q under a per-file step budget, emitting
ec-small-scalar (high, validated) with the recovered d and curve.

Vendor the eight real eMudhra intermediates (public certs only, SHA-256
verified against Mozilla bug 2083449) into the key corpus and fingerprint
them. Unit KATs (incl. the real d=4 point, off-curve and below-bound
negatives) and the Python suite (synthetic d*G on four curves, all eight real
eMudhra recoveries, and a healthy-key false-positive guard) cover it.
@nmatt0
nmatt0 merged commit 05c2923 into master Oct 21, 2026
4 checks passed
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