X25519 / X448 — countermeasures
- Spec:
RFC 7748 [LHT16]
- Crate path:
arcana::ecc::x25519(Curve25519 Diffie-Hellman),arcana::ecc::x448(Curve448 Diffie-Hellman)- Cargo feature:
none — both compiled unconditionally.
X25519 and X448 are the two ECDH primitives on Montgomery curves in arcana. They are CT by construction (the X coordinate is the only state, no Y, no special cases for the neutral element) which is why they are popular in modern protocol designs (TLS 1.3, Noise, Signal, WireGuard).
That said, any concrete Montgomery-ladder implementation can still leak SCA information through the field operations, the RNG-derived blinding, and side effects of the SHAKE-style clamping [WPB21].
Coverage matrix
Threat |
Status |
Countermeasure(s) |
|---|---|---|
Software / cache-timing on Montgomery ladder |
partial — audit pending |
Item |
SPA on Cortex-M0 / RISC-V |
vulnerable |
Same audit |
DPA on field operations |
vulnerable |
Plan |
Template attacks on the per-iteration |
vulnerable |
Plan |
Invalid-curve attack (peer pubkey on twist) |
implemented |
X25519 and X448 are by design twist-secure (RFC 7748 §6.1), so invalid-curve attacks reduce to a small subgroup attack which is mitigated by the “all-zero shared secret” contributory check (when applicable). |
Small-subgroup contributory check |
partial |
The X25519 / X448 functions return a shared secret of all-
zero when the peer pubkey is in the small-order subgroup;
callers should reject. Audit |
Background — Montgomery ladder for X25519
X25519 (RFC 7748) computes X(k · P) from k and X(P)
using a constant-time Montgomery ladder over (X : Z)
projective coordinates:
X1 := X(P)
X2, Z2 := 1, 0 ; representing the neutral element
X3, Z3 := X1, 1
for t in [254..0]:
k_t := bit t of k
cswap(k_t, X2, X3)
cswap(k_t, Z2, Z3)
(X2, Z2, X3, Z3) := double_and_add(X1, X2, Z2, X3, Z3)
cswap(k_t, X2, X3)
cswap(k_t, Z2, Z3)
return X2 / Z2
The structure is essentially identical to the Weierstrass-side
scalar_mul_point and benefits from the same hardening
techniques.
Audit gaps (T1-G)
The arcana X25519 / X448 implementations were ported from
RFC 7748 reference code with the standard idiom “constant-time
swap implemented as mask = -bit; t = mask & (a ^ b); a ^= t;
b ^= t”. The same LLVM regression observed on Weierstrass
ecc::field (mask-pattern → branch recovery) applies here, so
the audit checklist mirrors the Weierstrass one:
cswapmust compile branchless underopt-level=2. Applycore::hint::black_boxon the mask if the release asm shows a recovered branch.double_and_addbody must not branch on field-element limbs. The inner field operations (ecc::fieldfor Curve25519 / Curve448 primes) are shared with the Weierstrass code and already received theblack_boxtreatment in commit 76191c1; confirm the X25519 path uses the samefield_add/field_sub/reduce_wideand not a separate copy.Final inversion
Z2^{-1} mod puses Fermat (Z2^(p-2) mod p), which goes through the CTfield_pow; re-confirm.Clamping of the scalar
k(clear bits 0, 1, 2, 255; set bit 254 for X25519; analogous for X448) is bitwise; no branch risk by construction.
Estimated effort: 1 day audit + 0.5 day fix.
Z-randomization on (X : Z) (T2-A)
The Montgomery projective (X : Z) representation admits the
same λ-rescaling as Jacobian Weierstrass:
So at the ladder start, draw λ from the SCA-RNG and replace
(X1, 1) (the input point) by (λX1, λ) — both X3, Z3
follow because the loop derives them from X2, Z2.
This is the exact countermeasure that broke :cite:`weissbart2021_curve25519_ml_sca`’s template attack on unprotected Curve25519 implementations; once Z-rerandomization is in, the per-iteration intermediates randomize across signatures and the profiled attack does not align.
Cost: 2 field multiplications. Negligible.
Implementation hook: today the X25519 / X448 entry points
(x25519_derive_public, x25519_ecdh) are pure functions
without an RNG argument. Adding Z-rerand requires either:
changing the API to take a
CryptoRngcallback (breaking), orderiving an internal SCA-RNG seed from
H(sk_bytes ‖ peer_pk_bytes ‖ "x25519-z-rerand")and using a SHAKE-derived stream, à la ECDSA-deterministicT2-A.
The latter preserves the API, the determinism for KAT, and the zero-RNG-failure-mode property that makes X25519 attractive in the first place. Recommendation: go with the SHAKE-derived approach.
Scalar blinding (T2-B)
Scalar blinding k' = k + r · ℓ works the same as for Edwards;
ℓ = 2^252 + 27742... for X25519 (the order of the prime-order
subgroup). 64 random bits is the standard, costing ~25 % per-call
overhead on a 254-bit scalar.
For X25519 this is layered on top of the existing clamping;
clamp first, then blind, then ladder. The blinding does not break
the clamping properties (k' mod 8 = k mod 8, etc.) since
ℓ is congruent to a known value mod 8.
Reading list
[WPB21] — ML-based template SCA on Curve25519 Cortex-M0; the canonical “even with random delays, you leak” baseline.
[Ber06] — the original Curve25519 paper, which already gives the CT-by-construction argument.
[DHH+15] — high-speed Curve25519 on ARM Cortex-M0; the reference for embedded performance numbers.
[A+24] — recent unified hardware design including Z-randomization and CT timing.
Code path summary
Path |
Today (2026-04-21) |
Target (post T1-G + T2-A + T2-B) |
|---|---|---|
|
CT structure (RFC 7748 idiom), audit pending |
Audited CT, Z-rerand, scalar blinding |
|
Pure function, no RNG |
Same API; internal SCA-RNG seeded from sk |
|
Pure function, no RNG |
Same API; internal SCA-RNG seeded from sk + peer_pk |
|
Same as X25519 mutatis mutandis |
Same plan as X25519 |
Field arithmetic (CURVE25519_P, CURVE448_P) |
Reuses |
Unchanged |