ECDSA / ECDH — countermeasures

Spec:

FIPS 186-5 [NationalIoSaTechnology23], RFC 6979 [Por13], NIST SP 800-56A [NationalIoSaTechnology18]

Crate path:

arcana::ecc::curves (Curve trait + 7 unit structs), arcana::ecc::ecdsa (LIMBS-generic internals), arcana::ecc::curve (Jacobian point ops + scalar_mul_point), arcana::ecc::field (multi-precision modular arithmetic).

Cargo feature:

none — ECDSA / ECDH are unconditionally compiled.

ECDSA / ECDH on the seven short-Weierstrass curves (P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1) share a single CT scalar-multiplication path, ecc::curve::scalar_mul_point, which has been hardened (commit 76191c1, 2026-04-21) and is the primary defensive surface for the family.

This chapter is the most extensive of the per-algorithm chapters because the published attack literature on ECDSA is the richest of any classical primitive (LadderLeak, Minerva, OpenSSL CVE-2024-13176 all landed in 2020-2024).

Coverage matrix

ECDSA/ECDH countermeasure / threat matrix

Threat

Status

Countermeasure(s)

SPA / SEMA on scalar mul

implemented

CT Montgomery ladder scalar_mul_point with ct_swap between two accumulator points; point_add_ct is branch-free on point coords.

Software / cache-timing on scalar mul

implemented

Same ladder. Release-asm verification: scalar_mul_point = 1 branch (loop), point_double = 0, point_add_ct = 0.

LadderLeak ([ANT+20])

implemented

The ladder does not branch on bit-length of the scalar and uses ct_swap masked by the bit value.

Minerva ([JSSS20])

partial — audit pending

The ECDSA nonce sampling and bits2int / rfc6979_k paths must be re-audited specifically for bit-length leakage on k. Item T1-B.

DPA / CPA on field operations

vulnerable

Plan T2-A: Z-coordinate randomization ([BJ02], [Cor99a]).

Template attacks on ladder traces

vulnerable

Plan T2-A (Z-rerand destroys profile alignment) + T2-B (scalar blinding).

Single-fault on RFC 6979 deterministic ECDSA

vulnerable

Plan T1-D: optional hedged mode mirroring [MTR24].

Invalid-curve attack on peer pubkey (ECDH)

implemented

parse_and_validate_pubkey enforces y^2 = x^3 + ax + b before any scalar mul.

Small-subgroup attack on ECDH output

implemented

ecdh_internal rejects the point at infinity output.

SPA / cache-timing — CT Montgomery ladder (implemented)

State as of 2026-04-21

The scalar multiplication path was hardened in commit 76191c1 (2026-04-21). The structure of the loop body is now:

for i in (0..total_bits).rev() {
    let bit = (k.limbs[i / 64] >> (i % 64)) & 1;
    ct_swap(&mut r0, &mut r1, bit);
    r1 = point_add_ct(&r0, &r1, params);
    r0 = point_double(&r0, params);
    ct_swap(&mut r0, &mut r1, bit);
}

Three source-level secret-dependent branches were removed:

  1. point_double’s if pt.z.is_zero() { infinity() } else { ... } — replaced by the observation that the dbl-2007-bl formula naturally produces Z3 = 0 when Z1 = 0 (because Z3 = (Y1+Z1)^2 - YY - ZZ = Y1^2 - Y1^2 - 0).

  2. point_add_ct’s if h_zero && r_zero / h_zero && !r_zero short-circuits (point doubling on equal points; infinity on inverse points). Replaced by the observation that the Montgomery ladder invariant R1 - R0 = P (with P O) guarantees R1 R0 so H = 0 only happens for R1 = -R0, and that case naturally produces Z3 = 0 via the general formulas.

  3. point_add_ct’s if p_inf / q_inf selects — replaced by a ct_select_point cascade.

For the non-CT path (double_scalar_mul used by ECDSA verify on public values), the original point_add is kept unchanged; it has 6 branches (P=Q, P=-Q, p_inf, q_inf, fall-throughs) which is acceptable since verify operates on attacker-known inputs.

The ecc::field::field_add / field_sub / reduce_wide mask selects are wrapped in core::hint::black_box to keep LLVM from recovering branches over the secret-derived need_sub flag.

Verification artefact (release-asm branch counts on x86_64 with all 4 curve widths inlined):

Function

Pre 76191c1

Post 76191c1

scalar_mul_point

1 (loop)

1 (loop)

point_double

8

0

point_add_ct

n/a (non-existent)

0 (inlined)

point_add (verify)

12

6 (public values)

double_scalar_mul

2

2 (public values)

Minerva / LadderLeak — bit-length nonce leakage (T1-B)

Principle of the attack

Minerva ([JSSS20]) exploits a timing leak that reveals the bit-length of the ECDSA nonce k (e.g. through a variable-iteration modular inverse, or through bits2int truncating leading zero bits). LadderLeak ([ANT+20]) shows that less than one bit of nonce-bias per signature is enough for full key recovery via a lattice attack on the Hidden Number Problem.

Live in 2024:

  • CVE-2024-23342 — python-ecdsa Minerva-class via the modular inverse implementation.

  • CVE-2024-13176 — OpenSSL P-521: ~300 ns leak when the top word of k^{-1} mod n happens to be zero (P-521 nonce is 521 bits → 9-limb storage, top 55 bits zero by construction; the inverse can also have a zero high word with non-trivial probability).

Audit checklist for arcana

The CT scalar multiplication is the obvious audit point but not the only one. The Minerva / LadderLeak class also bites:

  1. ecc::ecdsa::rfc6979_k — the deterministic nonce derivation. bits2int must extract the correct bit window without an early exit on leading zeros. Sub-byte right-shift for P-521 is already wired (shr_limbs); audit confirms it is fixed-cost.

  2. ecc::ecdsa::sample_random_scalar — rejection sampling on k to ensure 1 k < n. The rejection loop iteration count is publicly leakable (it is statistical, independent of the secret key) — but the interior of each iteration must not leak bits of the candidate. Currently uses fill_bytes_masked + scalar_is_valid, both branchless; re-confirm.

  3. ecc::field::scalar_inv (= field_inv mod n) — the modular inverse via Fermat’s little theorem. Variable-time extended GCD has been the historical Minerva target; arcana’s choice of Fermat (a^(n-2) mod n) sidesteps the GCD structure entirely, but the underlying field_pow must be square-always + CT-select. Currently is; re-confirm with black_box shielding (item T1-E overlap).

  4. ecc::ecdsa::reduce_mod_n — the up-to-2 conditional subtractions inside the signing flow. Already CT-masked but benefits from the black_box already applied to ecc::field::field_add; the reduce_mod_n function (in ecdsa.rs) is a separate copy of the same pattern and must receive the same treatment.

Item T1-B is the audit + black_box patch on the four points above. Estimated effort: 1 day audit + 1 day fix.

DPA / CPA — Z-coordinate randomization (T2-A)

Principle of the countermeasure

A Jacobian point (X, Y, Z) represents the same affine point as (λ²X, λ³Y, λZ) for any λ 0 mod p. Drawing a fresh random λ at the start of each scalar multiplication randomizes the per-iteration intermediate values, so DPA correlation across multiple signatures becomes uncorrelated:

  • The s_1, s_2, …, s_t traces of the per-iteration intermediates depend on λ, which is fresh per trace.

  • Hamming-weight prediction across traces no longer aligns; the CPA peak averages out.

Reference: [BJ02] (CHES 2002), [Cor99a] (CHES 1999).

Implementation route in arcana

In ecc::curve::scalar_mul_point, before the loop:

// λ ← random non-zero in F_p, e.g. via a per-call SCA-RNG seed
let lambda = sample_random_field_element_nonzero(rng, p);
let lambda_sq = field_sqr(&lambda, p);
let lambda_cu = field_mul(&lambda_sq, &lambda, p);
let p_rerand = JacobianPoint {
    x: field_mul(&point.x, &lambda_sq, p),
    y: field_mul(&point.y, &lambda_cu, p),
    z: field_mul(&point.z, &lambda, p),
};
// ... ladder uses p_rerand instead of point ...

Cost: 3 field multiplications + 2 squarings before the ladder, i.e. < 1 % overhead. Requires a CryptoRng callback at the scalar_mul_point boundary — the API change is the bigger work item than the math.

Note: the ECDH path (ecdh_internal) and the ECDSA random-nonce signing path (sign_random_internal) already have an RNG plumbed through; the RFC 6979 deterministic signing path (sign_rfc6979_internal) does not. To avoid breaking the RFC 6979 KAT determinism, Z-rerand on that path needs an internal SCA-RNG seeded from the per-signature data (sk.bytes digest "z-rerand"), à la [CGerardL+24] for ML-DSA.

Scalar blinding (T2-B)

In addition to Z-rerand, the textbook DPA defence on ECDSA is scalar blinding:

\[k' \;=\; k + r \cdot n, \qquad r \stackrel{\$}{\leftarrow} \{0, 1\}^{64}\]

so that k' \cdot G = k \cdot G (since n \cdot G = O) but the ladder iterates over the bit-pattern of k', not k. The attacker observing per-iteration leakage now sees a different sequence each call. Reference: [Cor99a].

64 random bits is the typical width quoted in CHES papers; it implies ~64 extra iterations of the ladder per call (~25 % cost hit on a 256-bit curve). For a level-1 evaluation threat, 32 bits may suffice, but 64 is the safer default.

Implementation route: take k, compute k' = k + r·n in the scalar field, run the ladder on k' over ceil(log2(n)) + 64 bits. T2-B lands together with T2-A since both share the RNG plumbing of the previous section.

DFA on RFC 6979 deterministic signatures (T1-D)

Principle of the attack

RFC 6979 derives k deterministically from (sk, hash(msg)) via HMAC-DRBG. Two signatures of the same message reuse the same k — which is fine cryptographically, but fragile under fault. [RP17] demonstrates the attack on Ed25519 (same principle applies to deterministic ECDSA): sign the same message twice; fault one of the two signatures so it uses a slightly different k (e.g. by glitching the HMAC state); the two resulting signatures share enough algebraic structure that k and sk fall out of a small linear system. [CLLW20] generalises to a lattice attack across many faulted signatures.

Hedged signatures

The CFRG draft [MTR24] standardises the countermeasure: keep RFC 6979 deterministic derivation but mix in 32 bytes of fresh randomness:

\[k = \mathrm{HMAC\_DRBG}(sk \;\|\; H(msg) \;\|\; r), \qquad r \stackrel{\$}{\leftarrow} \{0, 1\}^{256}\]

Properties:

  • If r is fresh entropy, hedged is as strong as fully randomized signing against fault.

  • If r is zero, the scheme degrades to standard RFC 6979 — preserving determinism for ACVP / KAT purposes when needed.

  • If r is bad (predictable), the scheme is no worse than RFC 6979 — there is no nonce-reuse oracle.

Implementation route in arcana

  • New Curve::sign_hedged<H>(sk, digest, rng) API alongside the existing sign_rfc6979 and sign_random.

  • The internal nonce derivation feeds r into rfc6979_k as an extra HMAC-DRBG personalization byte string.

  • Tests: KAT regression for sign_rfc6979 (still deterministic with r = 0^{32}); randomized round-trip for sign_hedged; fault-injection unit test that asserts two sign_hedged of the same message produce different r-component signatures.

Out-of-scope: the EdDSA side has its own chapter (EdDSA / Ed25519 — countermeasures); the hedged-signing implementation will share the rng plumbing with this ECDSA path.

Invalid-curve and small-subgroup defences (implemented)

ECDH is the canonical target of invalid-curve attacks: an attacker sends a peer pubkey on a different curve whose order has small prime factors; the shared secret leaks sk mod p_i for each small p_i. ecc::ecdsa::parse_and_validate_pubkey enforces the on-curve check y^2 = x^3 + ax + b mod p on every externally- provided pk; the same entry point is used by ECDSA verify, ECDH derive, and SEC1 compress / decompress, so the validation cannot drift between code paths.

The output of ecdh_internal is rejected if it lands on the point at infinity (small-subgroup defence-in-depth on cofactor-1 curves).

These two defences are already in place, validated by the Wycheproof corpus (arcana/tests/wycheproof.rs exercises the full set of off-curve and infinity-encoding negative vectors).

Reading list

Beyond the cited items above:

  • [BJPW14] — synthesis of SCA on ECC in smartcards.

  • [GJ20] — modern formulas for Joye double-add ladder (alternative to Montgomery, no special case at bit 0; performance-comparable).

  • [PGM+17] — DRAM-based cross-CPU attacks on ECDSA in mbedTLS (relevant for shared hosts; outside the embedded evaluation scope).

  • [MSEH20] — TPM-Fail: ECDSA + RSA timing on certified TPMs.

  • [BB03] — historical baseline for remote timing.

Code path summary

Path

Today (2026-04-21)

Target (post T1-B + T1-D + T2-A + T2-B)

ecc::curve::scalar_mul_point

CT Montgomery ladder (76191c1)

  • Z-coordinate randomization, scalar blinding

ecc::ecdsa::rfc6979_k / sample_random_scalar

CT structure, black_box audit pending

Audited, black_box shielding throughout

ecc::field::scalar_inv

Fermat ladder via field_pow (CT)

Audited; same black_box shielding

Curve::sign_hedged (new)

n/a

RFC 6979 + 32-byte additional randomness

ecdh_internal / parse_and_validate_pubkey

On-curve check, infinity reject

Unchanged