Skip to main content

arcana/ecc/
curve.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4//! Elliptic curve point operations on short Weierstrass curves y^2 = x^3 + ax + b.
5//!
6//! Uses Jacobian projective coordinates (X, Y, Z) where the affine point is
7//! (X/Z^2, Y/Z^3). The point at infinity is represented by Z = 0.
8//!
9//! Scalar multiplication uses a Montgomery ladder for constant-time execution.
10//!
11//! # Arithmetic layer (expert bricks — read before use)
12//!
13//! This module is part of arcana's public **arithmetic layer** (together with
14//! [`super::field`] and `rsa::bigint`): raw point arithmetic offered for
15//! tooling, research and custom protocols. The supported application surface
16//! is the [`super::curves::Curve`] trait facade. Stability is best-effort
17//! pre-1.0 — the SCA roadmap items `T2-A` (Z-coordinate randomization) and
18//! `T2-B` (scalar blinding) will rework the ladder internals, though the
19//! `(X, Y, Z)` representation is expected to survive.
20//!
21//! ## Constant-time split
22//!
23//! | Function | Timing | Use on secrets? |
24//! |----------|--------|-----------------|
25//! | [`scalar_mul_point`] | CT Montgomery ladder | yes (secret scalars) |
26//! | [`point_add`] | **variable-time** (`H == 0` short-circuits) | **public values only** (verify path) |
27//! | [`point_double`] | straight-line (no branch on point values) | ok — fed secret points inside the CT ladder |
28//! | [`double_scalar_mul`] | **variable-time** | **public values only** (ECDSA verify) |
29//!
30//! ## Validation contract
31//!
32//! The raw point operations do **not** validate their inputs: [`point_add`]
33//! and [`scalar_mul_point`] never check that a point is on the curve, and
34//! feeding an off-curve or small-subgroup point produces undefined results
35//! (invalid-curve attacks). Callers must validate untrusted input first —
36//! call [`is_on_curve`] and reject the point at infinity — or enter through
37//! [`super::compress_pubkey`] / [`super::decompress_pubkey`], which validate
38//! internally (the `Curve` facade always does).
39
40use super::field::*;
41
42/// A point on a short Weierstrass curve in Jacobian projective
43/// coordinates `(X, Y, Z)`.
44///
45/// The corresponding affine point is `(X / Z^2, Y / Z^3)`. The point
46/// at infinity is represented by `Z == 0`. Working in projective
47/// coordinates lets the point doubling and addition formulas avoid
48/// the field inversion that the affine forms would need on every
49/// operation; only [`Self::to_affine`] performs an inversion (once,
50/// at the boundary back to the user-visible representation).
51#[derive(Clone, Copy, Debug)]
52pub struct JacobianPoint<const LIMBS: usize> {
53    /// Projective X coordinate.
54    pub x: FieldElement<LIMBS>,
55    /// Projective Y coordinate.
56    pub y: FieldElement<LIMBS>,
57    /// Projective Z coordinate. `Z == 0` encodes the point at infinity.
58    pub z: FieldElement<LIMBS>,
59}
60
61impl<const LIMBS: usize> JacobianPoint<LIMBS> {
62    /// The point at infinity (identity element).
63    pub fn infinity() -> Self {
64        Self {
65            x: FieldElement::one(),
66            y: FieldElement::one(),
67            z: FieldElement::ZERO,
68        }
69    }
70
71    /// Create a point from affine coordinates (x, y). Z = 1.
72    pub fn from_affine(x: FieldElement<LIMBS>, y: FieldElement<LIMBS>) -> Self {
73        Self {
74            x,
75            y,
76            z: FieldElement::one(),
77        }
78    }
79
80    /// Returns `true` if this point is the point at infinity (Z == 0).
81    pub fn is_infinity(&self) -> bool {
82        self.z.is_zero()
83    }
84
85    /// Convert to affine coordinates (x, y). Returns None for point at infinity.
86    pub fn to_affine(&self, p: &[u64; LIMBS]) -> Option<(FieldElement<LIMBS>, FieldElement<LIMBS>)> {
87        if self.z.is_zero() {
88            return None;
89        }
90        let z_inv = field_inv(&self.z, p);
91        let z_inv2 = field_sqr(&z_inv, p);
92        let z_inv3 = field_mul(&z_inv2, &z_inv, p);
93        let x = field_mul(&self.x, &z_inv2, p);
94        let y = field_mul(&self.y, &z_inv3, p);
95        Some((x, y))
96    }
97}
98
99/// Parameters for a short Weierstrass curve.
100pub struct CurveParams<const LIMBS: usize> {
101    /// Field prime p.
102    pub p: [u64; LIMBS],
103    /// Coefficient a. For the NIST prime curves P-256 / P-384 / P-521
104    /// (secp256r1 / secp384r1 / secp521r1) `a = -3 mod p`. Note that
105    /// secp256k1 uses `a = 0` and the Brainpool curves use a random `a`.
106    pub a: FieldElement<LIMBS>,
107    /// Coefficient b.
108    pub b: FieldElement<LIMBS>,
109    /// X coordinate of the generator point `G`.
110    pub gx: FieldElement<LIMBS>,
111    /// Y coordinate of the generator point `G`.
112    pub gy: FieldElement<LIMBS>,
113    /// Order n.
114    pub n: [u64; LIMBS],
115    /// Bit length of `n` (aka `qlen` in RFC 6979).
116    ///
117    /// For all curves we currently ship, `qlen_bits` is a multiple of 8
118    /// **except for secp521r1** where it is 521 (not a multiple of 8).
119    /// RFC 6979 is careful to distinguish `qlen` from `rlen = 8*ceil(qlen/8)`
120    /// (aka `rlen_bytes = (qlen_bits + 7) / 8`), and for P-521 those two
121    /// values disagree by 7 bits. All RFC 6979 byte-length decisions
122    /// (int2octets, bits2octets, the HMAC-T accumulation loop) must use
123    /// `rlen_bytes`, *not* `LIMBS * 8`.
124    pub qlen_bits: usize,
125    /// **SEC1 §2.3.5 field element octet length** = `ceil(log2(p) / 8)`.
126    ///
127    /// This is the **external** width of a field element for all
128    /// serialization purposes (uncompressed / compressed SEC1 public
129    /// keys, raw r/s in a signature, ECDH shared-secret output). It is
130    /// distinct from the **internal** storage width `LIMBS * 8`, which
131    /// is an implementation detail driven by the 64-bit limb alignment.
132    ///
133    /// | Curve                 | LIMBS*8 (internal) | felem_bytes (external) |
134    /// |-----------------------|--------------------|-------------------------|
135    /// | P-256                 | 32                 | 32                      |
136    /// | P-384                 | 48                 | 48                      |
137    /// | secp256k1             | 32                 | 32                      |
138    /// | brainpoolP256r1       | 32                 | 32                      |
139    /// | brainpoolP384r1       | 48                 | 48                      |
140    /// | brainpoolP512r1       | 64                 | 64                      |
141    /// | **secp521r1 (P-521)** | **72**             | **66**                  |
142    ///
143    /// P-521 is the only curve where the two values disagree (576-bit
144    /// storage vs 521-bit field → 66 bytes externally). The 6 leading
145    /// bytes of `to_bytes_be()` on a P-521 field element are always
146    /// zero and must be stripped at the serialization boundary;
147    /// conversely, parsers must left-pad a 66-byte external value into
148    /// the 72-byte internal buffer before building a `FieldElement<9>`.
149    /// The 6 byte-aligned curves treat this as a no-op because
150    /// `felem_bytes == LIMBS * 8`.
151    pub felem_bytes: usize,
152}
153
154// ============================================================================
155// Curve parameter helpers
156// ============================================================================
157
158/// Decode a hex string into a `FieldElement<LIMBS>`. The hex is interpreted as
159/// big-endian and must contain an even number of hex digits.
160pub fn hex_to_fe<const LIMBS: usize>(hex: &str) -> FieldElement<LIMBS> {
161    let bytes: Vec<u8> = (0..hex.len())
162        .step_by(2)
163        .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
164        .collect();
165    FieldElement::from_bytes_be(&bytes)
166}
167
168/// Decode a hex string into a `[u64; LIMBS]` (used for `p` and `n`).
169/// Big-endian hex; pads with leading zeros if shorter than LIMBS*8 bytes.
170pub fn hex_to_limbs<const LIMBS: usize>(hex: &str) -> [u64; LIMBS] {
171    hex_to_fe::<LIMBS>(hex).limbs
172}
173
174// Backwards-compatible aliases used by P-256/P-384 below.
175fn hex_to_fe4(hex: &str) -> FieldElement<4> {
176    hex_to_fe::<4>(hex)
177}
178fn hex_to_fe6(hex: &str) -> FieldElement<6> {
179    hex_to_fe::<6>(hex)
180}
181
182// ============================================================================
183// P-256 curve parameters
184// ============================================================================
185
186/// NIST P-256 / secp256r1 curve parameters (FIPS 186-4 §D.1.2.3).
187pub fn p256_params() -> CurveParams<4> {
188    CurveParams {
189        p: P256_P,
190        a: hex_to_fe4("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC"),
191        b: hex_to_fe4("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B"),
192        gx: hex_to_fe4("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296"),
193        gy: hex_to_fe4("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5"),
194        n: P256_N,
195        qlen_bits: 256,
196        felem_bytes: 32,
197    }
198}
199
200/// NIST P-384 / secp384r1 curve parameters (FIPS 186-4 §D.1.2.4).
201pub fn p384_params() -> CurveParams<6> {
202    CurveParams {
203        p: P384_P,
204        a: hex_to_fe6(
205            "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC",
206        ),
207        b: hex_to_fe6(
208            "B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF",
209        ),
210        gx: hex_to_fe6(
211            "AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7",
212        ),
213        gy: hex_to_fe6(
214            "3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F",
215        ),
216        n: P384_N,
217        qlen_bits: 384,
218        felem_bytes: 48,
219    }
220}
221
222// ============================================================================
223// secp256k1 (SEC 2 v2.0 §2.4.1)  --  Bitcoin / Ethereum curve
224// ============================================================================
225//
226// y^2 = x^3 + 7  (i.e. a = 0, b = 7)
227// p = 2^256 - 2^32 - 977
228//   = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
229
230/// secp256k1 curve parameters (SEC 2 v2.0 §2.4.1).
231///
232/// `y^2 = x^3 + 7` over `p = 2^256 - 2^32 - 977`. The Bitcoin /
233/// Ethereum signing curve, also widely used in Lightning, Nostr,
234/// and most blockchain ecosystems.
235pub fn secp256k1_params() -> CurveParams<4> {
236    CurveParams {
237        p: hex_to_limbs::<4>("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F"),
238        a: hex_to_fe::<4>("0000000000000000000000000000000000000000000000000000000000000000"),
239        b: hex_to_fe::<4>("0000000000000000000000000000000000000000000000000000000000000007"),
240        gx: hex_to_fe::<4>("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"),
241        gy: hex_to_fe::<4>("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8"),
242        n: hex_to_limbs::<4>("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141"),
243        qlen_bits: 256,
244        felem_bytes: 32,
245    }
246}
247
248// ============================================================================
249// brainpoolP256r1 (RFC 5639 §3.4)
250// ============================================================================
251
252/// brainpoolP256r1 curve parameters (BSI / RFC 5639 §3.4).
253pub fn brainpoolp256r1_params() -> CurveParams<4> {
254    CurveParams {
255        p: hex_to_limbs::<4>("A9FB57DBA1EEA9BC3E660A909D838D726E3BF623D52620282013481D1F6E5377"),
256        a: hex_to_fe::<4>("7D5A0975FC2C3057EEF67530417AFFE7FB8055C126DC5C6CE94A4B44F330B5D9"),
257        b: hex_to_fe::<4>("26DC5C6CE94A4B44F330B5D9BBD77CBF958416295CF7E1CE6BCCDC18FF8C07B6"),
258        gx: hex_to_fe::<4>("8BD2AEB9CB7E57CB2C4B482FFC81B7AFB9DE27E1E3BD23C23A4453BD9ACE3262"),
259        gy: hex_to_fe::<4>("547EF835C3DAC4FD97F8461A14611DC9C27745132DED8E545C1D54C72F046997"),
260        n: hex_to_limbs::<4>("A9FB57DBA1EEA9BC3E660A909D838D718C397AA3B561A6F7901E0E82974856A7"),
261        qlen_bits: 256,
262        felem_bytes: 32,
263    }
264}
265
266// ============================================================================
267// brainpoolP384r1 (RFC 5639 §3.6)
268// ============================================================================
269
270/// brainpoolP384r1 curve parameters (BSI / RFC 5639 §3.6).
271pub fn brainpoolp384r1_params() -> CurveParams<6> {
272    CurveParams {
273        p: hex_to_limbs::<6>(
274            "8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B412B1DA197FB71123ACD3A729901D1A71874700133107EC53",
275        ),
276        a: hex_to_fe::<6>(
277            "7BC382C63D8C150C3C72080ACE05AFA0C2BEA28E4FB22787139165EFBA91F90F8AA5814A503AD4EB04A8C7DD22CE2826",
278        ),
279        b: hex_to_fe::<6>(
280            "04A8C7DD22CE28268B39B55416F0447C2FB77DE107DCD2A62E880EA53EEB62D57CB4390295DBC9943AB78696FA504C11",
281        ),
282        gx: hex_to_fe::<6>(
283            "1D1C64F068CF45FFA2A63A81B7C13F6B8847A3E77EF14FE3DB7FCAFE0CBD10E8E826E03436D646AAEF87B2E247D4AF1E",
284        ),
285        gy: hex_to_fe::<6>(
286            "8ABE1D7520F9C2A45CB1EB8E95CFD55262B70B29FEEC5864E19C054FF99129280E4646217791811142820341263C5315",
287        ),
288        n: hex_to_limbs::<6>(
289            "8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B31F166E6CAC0425A7CF3AB6AF6B7FC3103B883202E9046565",
290        ),
291        qlen_bits: 384,
292        felem_bytes: 48,
293    }
294}
295
296// ============================================================================
297// brainpoolP512r1 (RFC 5639 §3.7)
298// ============================================================================
299
300/// brainpoolP512r1 curve parameters (BSI / RFC 5639 §3.7).
301pub fn brainpoolp512r1_params() -> CurveParams<8> {
302    CurveParams {
303        p: hex_to_limbs::<8>(
304            "AADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA703308717D4D9B009BC66842AECDA12AE6A380E62881FF2F2D82C68528AA6056583A48F3",
305        ),
306        a: hex_to_fe::<8>(
307            "7830A3318B603B89E2327145AC234CC594CBDD8D3DF91610A83441CAEA9863BC2DED5D5AA8253AA10A2EF1C98B9AC8B57F1117A72BF2C7B9E7C1AC4D77FC94CA",
308        ),
309        b: hex_to_fe::<8>(
310            "3DF91610A83441CAEA9863BC2DED5D5AA8253AA10A2EF1C98B9AC8B57F1117A72BF2C7B9E7C1AC4D77FC94CADC083E67984050B75EBAE5DD2809BD638016F723",
311        ),
312        gx: hex_to_fe::<8>(
313            "81AEE4BDD82ED9645A21322E9C4C6A9385ED9F70B5D916C1B43B62EEF4D0098EFF3B1F78E2D0D48D50D1687B93B97D5F7C6D5047406A5E688B352209BCB9F822",
314        ),
315        gy: hex_to_fe::<8>(
316            "7DDE385D566332ECC0EABFA9CF7822FDF209F70024A57B1AA000C55B881F8111B2DCDE494A5F485E5BCA4BD88A2763AED1CA2B2FA8F0540678CD1E0F3AD80892",
317        ),
318        n: hex_to_limbs::<8>(
319            "AADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA70330870553E5C414CA92619418661197FAC10471DB1D381085DDADDB58796829CA90069",
320        ),
321        qlen_bits: 512,
322        felem_bytes: 64,
323    }
324}
325
326// ============================================================================
327// secp521r1 / NIST P-521  (FIPS 186-4 §D.1.2.5, SEC 2 §2.9.1)
328// ============================================================================
329//
330// y^2 = x^3 - 3*x + b
331// p = 2^521 - 1   (a Mersenne prime, so p ≡ 3 (mod 4))
332// qlen = 521 bits (NOT a multiple of 8 -- the only curve we ship with
333// this property; see CurveParams::qlen_bits for the RFC 6979 implications)
334//
335// The field element width LIMBS = 9 uses 576 bits of storage, of which
336// the top 55 bits are always zero for values in [0, p). Likewise scalars
337// mod n have 55 zero leading bits in the 9-limb representation.
338
339/// secp521r1 / NIST P-521 curve parameters (FIPS 186-4 §D.1.2.5).
340///
341/// `y^2 = x^3 - 3*x + b` over the Mersenne prime `p = 2^521 - 1`.
342/// The only curve we ship with `qlen` not a multiple of 8 -- see
343/// [`CurveParams::qlen_bits`] and [`CurveParams::felem_bytes`] for
344/// the implications on RFC 6979 byte lengths and SEC1 octet
345/// encoding respectively.
346pub fn secp521r1_params() -> CurveParams<9> {
347    // All values verified against NIST SP 800-186 / FIPS 186-5 Appendix C.2.3
348    // and cross-checked via Python (on-curve + n*G == infinity).
349    CurveParams {
350        p: hex_to_limbs::<9>(
351            "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
352        ),
353        a: hex_to_fe::<9>(
354            "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC",
355        ),
356        b: hex_to_fe::<9>(
357            "0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00",
358        ),
359        gx: hex_to_fe::<9>(
360            "00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66",
361        ),
362        gy: hex_to_fe::<9>(
363            "011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650",
364        ),
365        n: hex_to_limbs::<9>(
366            "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409",
367        ),
368        qlen_bits: 521,
369        // P-521 is the only curve where felem_bytes (66) < LIMBS*8 (72).
370        // External serialization must strip the 6 leading zero bytes on
371        // output and the parser must left-pad them back in on input.
372        felem_bytes: 66,
373    }
374}
375
376// ============================================================================
377// Point operations
378// ============================================================================
379
380/// Check whether the affine point `(x, y)` lies on the short Weierstrass curve
381/// `y^2 = x^3 + a*x + b` defined by `params`.
382///
383/// **Critical for ECDH**: any externally-supplied public key must be validated
384/// with this function before being multiplied by a secret scalar. Otherwise an
385/// "invalid curve attack" can recover bits of the secret key by feeding crafted
386/// off-curve points whose order in the broken group is small.
387///
388/// Returns `true` iff the affine equation holds modulo `p`.
389pub fn is_on_curve<const LIMBS: usize>(
390    x: &FieldElement<LIMBS>,
391    y: &FieldElement<LIMBS>,
392    params: &CurveParams<LIMBS>,
393) -> bool {
394    let p = &params.p;
395    let y2 = field_sqr(y, p);
396    let x2 = field_sqr(x, p);
397    let x3 = field_mul(&x2, x, p);
398    let ax = field_mul(&params.a, x, p);
399    let rhs = field_add(&field_add(&x3, &ax, p), &params.b, p);
400    y2 == rhs
401}
402
403/// Point doubling in Jacobian coordinates for **any** short Weierstrass curve
404/// y^2 = x^3 + a*x + b. Uses the generic "dbl-2007-bl" formula by Bernstein-Lange
405/// (cost: 2M + 8S + 1*a-mul + 10add). Works for arbitrary `a`, including a=0
406/// (secp256k1) and the random `a` of Brainpool curves.
407///
408/// Reference: <https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-2007-bl>
409///
410/// **Constant-time**: no branches on point values. When the input is the
411/// point at infinity (`Z1 == 0`), the formulas naturally produce `Z3 == 0`
412/// as well (since `Z3 = (Y1+Z1)^2 - YY - ZZ = Y1^2 - Y1^2 - 0 = 0`), so no
413/// explicit special-case branch is needed. The output `(X3, Y3, 0)` is a
414/// non-canonical but valid Jacobian encoding of the point at infinity.
415pub fn point_double<const LIMBS: usize>(
416    pt: &JacobianPoint<LIMBS>,
417    params: &CurveParams<LIMBS>,
418) -> JacobianPoint<LIMBS> {
419    let p = &params.p;
420
421    // XX   = X1^2
422    // YY   = Y1^2
423    // YYYY = YY^2
424    // ZZ   = Z1^2
425    let xx = field_sqr(&pt.x, p);
426    let yy = field_sqr(&pt.y, p);
427    let yyyy = field_sqr(&yy, p);
428    let zz = field_sqr(&pt.z, p);
429
430    // S = 2*((X1+YY)^2 - XX - YYYY)
431    let x_plus_yy = field_add(&pt.x, &yy, p);
432    let x_plus_yy_sq = field_sqr(&x_plus_yy, p);
433    let s_inner = field_sub(&field_sub(&x_plus_yy_sq, &xx, p), &yyyy, p);
434    let s = field_add(&s_inner, &s_inner, p);
435
436    // M = 3*XX + a*ZZ^2
437    let two_xx = field_add(&xx, &xx, p);
438    let three_xx = field_add(&two_xx, &xx, p);
439    let zz_sq = field_sqr(&zz, p);
440    let a_zz_sq = field_mul(&params.a, &zz_sq, p);
441    let m = field_add(&three_xx, &a_zz_sq, p);
442
443    // T = M^2 - 2*S
444    let m_sq = field_sqr(&m, p);
445    let two_s = field_add(&s, &s, p);
446    let t = field_sub(&m_sq, &two_s, p);
447
448    // X3 = T
449    let x3 = t;
450
451    // Y3 = M*(S - T) - 8*YYYY
452    let s_minus_t = field_sub(&s, &t, p);
453    let m_term = field_mul(&m, &s_minus_t, p);
454    let two_yyyy = field_add(&yyyy, &yyyy, p);
455    let four_yyyy = field_add(&two_yyyy, &two_yyyy, p);
456    let eight_yyyy = field_add(&four_yyyy, &four_yyyy, p);
457    let y3 = field_sub(&m_term, &eight_yyyy, p);
458
459    // Z3 = (Y1+Z1)^2 - YY - ZZ
460    //    = 0 iff Z1 = 0 (input is infinity), yielding infinity output.
461    let y_plus_z = field_add(&pt.y, &pt.z, p);
462    let y_plus_z_sq = field_sqr(&y_plus_z, p);
463    let z3 = field_sub(&field_sub(&y_plus_z_sq, &yy, p), &zz, p);
464
465    JacobianPoint { x: x3, y: y3, z: z3 }
466}
467
468/// Jacobian point addition: `P + Q`. Uses the generic "add-2007-bl" formula
469/// (no assumption on Z coordinates).
470///
471/// **Not constant-time**: branches on whether `P == Q` (to delegate to
472/// [`point_double`]) and on whether `P == -Q` (to short-circuit to infinity).
473/// Intended for operations on **public** points only, such as the Shamir
474/// trick in [`double_scalar_mul`] used by ECDSA verify.
475///
476/// For the CT-critical scalar multiplication path (signing, ECDH), use
477/// `point_add_ct` which is uniform over all P, Q but requires the
478/// caller to respect the Montgomery ladder invariant `R1 - R0 == P` so
479/// that `P == Q` can never occur (the only case the CT variant cannot
480/// handle via the generic formulas alone).
481///
482/// Reference: <https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-2007-bl>
483pub fn point_add<const LIMBS: usize>(
484    p_pt: &JacobianPoint<LIMBS>,
485    q_pt: &JacobianPoint<LIMBS>,
486    params: &CurveParams<LIMBS>,
487) -> JacobianPoint<LIMBS> {
488    let p = &params.p;
489
490    // Handle identity cases with CT selection at end.
491    let p_inf = p_pt.z.is_zero();
492    let q_inf = q_pt.z.is_zero();
493
494    // Z1^2, Z1^3
495    let z1z1 = field_sqr(&p_pt.z, p);
496    let z1z1z1 = field_mul(&z1z1, &p_pt.z, p);
497
498    // Z2^2, Z2^3
499    let z2z2 = field_sqr(&q_pt.z, p);
500    let z2z2z2 = field_mul(&z2z2, &q_pt.z, p);
501
502    // U1 = X1 * Z2^2, U2 = X2 * Z1^2
503    let u1 = field_mul(&p_pt.x, &z2z2, p);
504    let u2 = field_mul(&q_pt.x, &z1z1, p);
505
506    // S1 = Y1 * Z2^3, S2 = Y2 * Z1^3
507    let s1 = field_mul(&p_pt.y, &z2z2z2, p);
508    let s2 = field_mul(&q_pt.y, &z1z1z1, p);
509
510    // H = U2 - U1
511    let h = field_sub(&u2, &u1, p);
512    // r = S2 - S1
513    let r = field_sub(&s2, &s1, p);
514
515    // If H == 0 and r == 0, points are equal -> double.
516    let h_zero = h.is_zero();
517    let r_zero = r.is_zero();
518
519    if !p_inf && !q_inf && h_zero && r_zero {
520        return point_double(p_pt, params);
521    }
522    // If H == 0 and r != 0, points are inverses -> infinity.
523    if !p_inf && !q_inf && h_zero && !r_zero {
524        return JacobianPoint::infinity();
525    }
526
527    let h2 = field_sqr(&h, p);
528    let h3 = field_mul(&h2, &h, p);
529    let u1h2 = field_mul(&u1, &h2, p);
530
531    // X3 = r^2 - H^3 - 2*U1*H^2
532    let r2 = field_sqr(&r, p);
533    let u1h2_2 = field_add(&u1h2, &u1h2, p);
534    let x3 = field_sub(&field_sub(&r2, &h3, p), &u1h2_2, p);
535
536    // Y3 = r*(U1*H^2 - X3) - S1*H^3
537    let u1h2_minus_x3 = field_sub(&u1h2, &x3, p);
538    let r_term = field_mul(&r, &u1h2_minus_x3, p);
539    let s1h3 = field_mul(&s1, &h3, p);
540    let y3 = field_sub(&r_term, &s1h3, p);
541
542    // Z3 = Z1 * Z2 * H
543    let z1z2 = field_mul(&p_pt.z, &q_pt.z, p);
544    let z3 = field_mul(&z1z2, &h, p);
545
546    let result = JacobianPoint { x: x3, y: y3, z: z3 };
547
548    // CT select for infinity cases.
549    if p_inf {
550        *q_pt
551    } else if q_inf {
552        *p_pt
553    } else {
554        result
555    }
556}
557
558/// Constant-time Jacobian point addition for the Montgomery-ladder path.
559///
560/// Same `add-2007-bl` formula as [`point_add`], but:
561/// - No early-return on `P == Q` or `P == -Q`. The ladder invariant
562///   `R1 - R0 == P` (with `P != O`) guarantees those inputs never occur
563///   with both points finite. When they *do* occur the formulas give
564///   `Z3 = 0` (= infinity); for `P == -Q` that is the algebraically
565///   correct answer, and for `P == Q` it is wrong (should be `2P`) --
566///   but the ladder guarantees this is unreachable.
567/// - The `p_inf` / `q_inf` selection at the end uses a branchless
568///   [`ct_select_point`] cascade instead of if/else, so the returned
569///   control flow is independent of the point values.
570///
571/// **Callers must ensure `P != Q`** (otherwise a silent wrong result on
572/// that single branch). The Montgomery ladder enforces this by
573/// construction; do not reuse this function outside that context.
574fn point_add_ct<const LIMBS: usize>(
575    p_pt: &JacobianPoint<LIMBS>,
576    q_pt: &JacobianPoint<LIMBS>,
577    params: &CurveParams<LIMBS>,
578) -> JacobianPoint<LIMBS> {
579    let p = &params.p;
580
581    let p_inf = p_pt.z.is_zero();
582    let q_inf = q_pt.z.is_zero();
583
584    let z1z1 = field_sqr(&p_pt.z, p);
585    let z1z1z1 = field_mul(&z1z1, &p_pt.z, p);
586    let z2z2 = field_sqr(&q_pt.z, p);
587    let z2z2z2 = field_mul(&z2z2, &q_pt.z, p);
588
589    let u1 = field_mul(&p_pt.x, &z2z2, p);
590    let u2 = field_mul(&q_pt.x, &z1z1, p);
591    let s1 = field_mul(&p_pt.y, &z2z2z2, p);
592    let s2 = field_mul(&q_pt.y, &z1z1z1, p);
593
594    let h = field_sub(&u2, &u1, p);
595    let r = field_sub(&s2, &s1, p);
596
597    let h2 = field_sqr(&h, p);
598    let h3 = field_mul(&h2, &h, p);
599    let u1h2 = field_mul(&u1, &h2, p);
600
601    let r2 = field_sqr(&r, p);
602    let u1h2_2 = field_add(&u1h2, &u1h2, p);
603    let x3 = field_sub(&field_sub(&r2, &h3, p), &u1h2_2, p);
604
605    let u1h2_minus_x3 = field_sub(&u1h2, &x3, p);
606    let r_term = field_mul(&r, &u1h2_minus_x3, p);
607    let s1h3 = field_mul(&s1, &h3, p);
608    let y3 = field_sub(&r_term, &s1h3, p);
609
610    let z1z2 = field_mul(&p_pt.z, &q_pt.z, p);
611    let z3 = field_mul(&z1z2, &h, p);
612
613    let general = JacobianPoint { x: x3, y: y3, z: z3 };
614
615    // Branchless select cascade:
616    //   q_inf => p_pt, else p_inf => q_pt, else general.
617    // When both are infinity the p_inf override wins, producing q_pt, which
618    // is itself infinity -- correct.
619    let mut out = general;
620    out = ct_select_point(p_pt, &out, q_inf as u64);
621    out = ct_select_point(q_pt, &out, p_inf as u64);
622    out
623}
624
625/// Constant-time conditional select of a Jacobian point.
626///
627/// Returns `a` if `condition != 0`, else `b`. No branch, no data-dependent
628/// memory access. `condition` is expected to be 0 or 1 (the caller enforces
629/// this by deriving it from `bool as u64` or a masked scalar bit).
630fn ct_select_point<const LIMBS: usize>(
631    a: &JacobianPoint<LIMBS>,
632    b: &JacobianPoint<LIMBS>,
633    condition: u64,
634) -> JacobianPoint<LIMBS> {
635    let mask = 0u64.wrapping_sub(condition);
636    let inv = !mask;
637    let mut out = JacobianPoint {
638        x: FieldElement { limbs: [0u64; LIMBS] },
639        y: FieldElement { limbs: [0u64; LIMBS] },
640        z: FieldElement { limbs: [0u64; LIMBS] },
641    };
642    for i in 0..LIMBS {
643        out.x.limbs[i] = (a.x.limbs[i] & mask) | (b.x.limbs[i] & inv);
644        out.y.limbs[i] = (a.y.limbs[i] & mask) | (b.y.limbs[i] & inv);
645        out.z.limbs[i] = (a.z.limbs[i] & mask) | (b.z.limbs[i] & inv);
646    }
647    out
648}
649
650/// Scalar multiplication using the **constant-time Montgomery ladder**.
651/// Computes `k * P` for a scalar `k` and a non-infinity point `P`.
652///
653/// # Constant-time properties
654///
655/// - Fixed iteration count `LIMBS * 64` -- independent of `k`.
656/// - Each iteration performs exactly one `ct_swap`, one `point_add_ct`
657///   and one [`point_double`], in that order. No branch depends on any
658///   scalar bit beyond the `ct_swap` mask.
659/// - [`point_double`] and `point_add_ct` themselves are uniform: they
660///   always compute the generic formulas and then apply branchless
661///   selects for the `Z == 0` (infinity) edge cases that occur during
662///   the leading-zero bits of `k`.
663///
664/// # Ladder invariant
665///
666/// At every step of the scan, `R1 - R0 == P`. This guarantees that
667/// `point_add_ct` is never called with `R0 == R1` (which would require
668/// `P == O`; `P` is assumed non-infinity). The `R0 == -R1` case is
669/// algebraically valid (the formulas give `Z3 = 0` = infinity) and
670/// therefore handled with no special-casing.
671pub fn scalar_mul_point<const LIMBS: usize>(
672    k: &FieldElement<LIMBS>,
673    point: &JacobianPoint<LIMBS>,
674    params: &CurveParams<LIMBS>,
675) -> JacobianPoint<LIMBS> {
676    let mut r0 = JacobianPoint::infinity();
677    let mut r1 = *point;
678
679    let total_bits = LIMBS * 64;
680    for i in (0..total_bits).rev() {
681        let limb_idx = i / 64;
682        let bit_idx = i % 64;
683        let bit = (k.limbs[limb_idx] >> bit_idx) & 1;
684
685        ct_swap(&mut r0, &mut r1, bit);
686        r1 = point_add_ct(&r0, &r1, params);
687        r0 = point_double(&r0, params);
688        ct_swap(&mut r0, &mut r1, bit);
689    }
690
691    r0
692}
693
694/// Constant-time conditional swap of two points.
695fn ct_swap<const LIMBS: usize>(a: &mut JacobianPoint<LIMBS>, b: &mut JacobianPoint<LIMBS>, condition: u64) {
696    let mask = 0u64.wrapping_sub(condition);
697    for i in 0..LIMBS {
698        let t = mask & (a.x.limbs[i] ^ b.x.limbs[i]);
699        a.x.limbs[i] ^= t;
700        b.x.limbs[i] ^= t;
701
702        let t = mask & (a.y.limbs[i] ^ b.y.limbs[i]);
703        a.y.limbs[i] ^= t;
704        b.y.limbs[i] ^= t;
705
706        let t = mask & (a.z.limbs[i] ^ b.z.limbs[i]);
707        a.z.limbs[i] ^= t;
708        b.z.limbs[i] ^= t;
709    }
710}
711
712/// Compute k1*G + k2*Q (used in ECDSA verify).
713/// Not constant-time in the public values (signature verification is public).
714pub fn double_scalar_mul<const LIMBS: usize>(
715    k1: &FieldElement<LIMBS>,
716    g: &JacobianPoint<LIMBS>,
717    k2: &FieldElement<LIMBS>,
718    q: &JacobianPoint<LIMBS>,
719    params: &CurveParams<LIMBS>,
720) -> JacobianPoint<LIMBS> {
721    // Shamir's trick: scan bits of k1 and k2 simultaneously.
722    let total_bits = LIMBS * 64;
723    let mut result = JacobianPoint::infinity();
724
725    // Precompute: G, Q, G+Q
726    let g_plus_q = point_add(g, q, params);
727
728    for i in (0..total_bits).rev() {
729        result = point_double(&result, params);
730
731        let limb_idx = i / 64;
732        let bit_idx = i % 64;
733        let b1 = (k1.limbs[limb_idx] >> bit_idx) & 1;
734        let b2 = (k2.limbs[limb_idx] >> bit_idx) & 1;
735
736        let to_add = match (b1, b2) {
737            (1, 1) => Some(&g_plus_q),
738            (1, 0) => Some(g),
739            (0, 1) => Some(q),
740            _ => None,
741        };
742
743        if let Some(pt) = to_add {
744            result = point_add(&result, pt, params);
745        }
746    }
747
748    result
749}
750
751#[cfg(test)]
752mod tests {
753    use super::*;
754
755    fn hex_to_bytes(hex: &str) -> Vec<u8> {
756        (0..hex.len())
757            .step_by(2)
758            .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
759            .collect()
760    }
761
762    #[test]
763    fn test_p256_generator_on_curve() {
764        let params = p256_params();
765        let p = &params.p;
766
767        // Verify y^2 = x^3 + ax + b for the generator.
768        let x = &params.gx;
769        let y = &params.gy;
770        let y2 = field_sqr(y, p);
771
772        let x2 = field_sqr(x, p);
773        let x3 = field_mul(&x2, x, p);
774        let ax = field_mul(&params.a, x, p);
775        let rhs = field_add(&field_add(&x3, &ax, p), &params.b, p);
776
777        assert_eq!(y2, rhs, "Generator point not on curve");
778    }
779
780    #[test]
781    fn test_p256_scalar_mul_generator() {
782        let params = p256_params();
783        let g = JacobianPoint::from_affine(params.gx, params.gy);
784
785        // 1*G should equal G.
786        let one = FieldElement::<4>::one();
787        let result = scalar_mul_point(&one, &g, &params);
788        let (rx, ry) = result.to_affine(&params.p).unwrap();
789        assert_eq!(rx, params.gx);
790        assert_eq!(ry, params.gy);
791    }
792
793    #[test]
794    fn test_p256_double_generator() {
795        let params = p256_params();
796        let g = JacobianPoint::from_affine(params.gx, params.gy);
797
798        // 2*G via scalar_mul.
799        let two = FieldElement::<4>::from_bytes_be(&[2]);
800        let result = scalar_mul_point(&two, &g, &params);
801        let (rx, _ry) = result.to_affine(&params.p).unwrap();
802
803        // 2*G via point_double.
804        let doubled = point_double(&g, &params);
805        let (dx, _dy) = doubled.to_affine(&params.p).unwrap();
806
807        assert_eq!(rx, dx);
808    }
809
810    #[test]
811    fn test_p256_n_times_g_is_infinity() {
812        let params = p256_params();
813        let g = JacobianPoint::from_affine(params.gx, params.gy);
814
815        // n*G should be the point at infinity.
816        let n = FieldElement::<4> { limbs: params.n };
817        let result = scalar_mul_point(&n, &g, &params);
818        assert!(result.is_infinity(), "n*G should be infinity");
819    }
820
821    /// Generic check: verify y^2 == x^3 + a*x + b for the generator.
822    fn check_generator_on_curve<const LIMBS: usize>(params: &CurveParams<LIMBS>) {
823        assert!(is_on_curve(&params.gx, &params.gy, params), "Generator not on curve");
824    }
825
826    /// Generic check: n*G must be the point at infinity. This is the strongest
827    /// quick sanity check on the (p, a, b, G, n) tuple together.
828    fn check_n_times_g_is_infinity<const LIMBS: usize>(params: &CurveParams<LIMBS>) {
829        let g = JacobianPoint::from_affine(params.gx, params.gy);
830        let n = FieldElement::<LIMBS> { limbs: params.n };
831        let result = scalar_mul_point(&n, &g, params);
832        assert!(result.is_infinity(), "n*G is not infinity");
833    }
834
835    #[test]
836    fn test_p384_generator_on_curve() {
837        check_generator_on_curve(&p384_params());
838    }
839
840    #[test]
841    fn test_p384_n_times_g_is_infinity() {
842        check_n_times_g_is_infinity(&p384_params());
843    }
844
845    #[test]
846    fn test_secp256k1_generator_on_curve() {
847        check_generator_on_curve(&secp256k1_params());
848    }
849
850    #[test]
851    fn test_secp256k1_n_times_g_is_infinity() {
852        check_n_times_g_is_infinity(&secp256k1_params());
853    }
854
855    #[test]
856    fn test_brainpoolp256r1_generator_on_curve() {
857        check_generator_on_curve(&brainpoolp256r1_params());
858    }
859
860    #[test]
861    fn test_brainpoolp256r1_n_times_g_is_infinity() {
862        check_n_times_g_is_infinity(&brainpoolp256r1_params());
863    }
864
865    #[test]
866    fn test_brainpoolp384r1_generator_on_curve() {
867        check_generator_on_curve(&brainpoolp384r1_params());
868    }
869
870    #[test]
871    fn test_brainpoolp384r1_n_times_g_is_infinity() {
872        check_n_times_g_is_infinity(&brainpoolp384r1_params());
873    }
874
875    #[test]
876    fn test_brainpoolp512r1_generator_on_curve() {
877        check_generator_on_curve(&brainpoolp512r1_params());
878    }
879
880    #[test]
881    fn test_brainpoolp512r1_n_times_g_is_infinity() {
882        check_n_times_g_is_infinity(&brainpoolp512r1_params());
883    }
884
885    #[test]
886    fn test_secp521r1_generator_on_curve() {
887        check_generator_on_curve(&secp521r1_params());
888    }
889
890    #[test]
891    fn test_secp521r1_n_times_g_is_infinity() {
892        check_n_times_g_is_infinity(&secp521r1_params());
893    }
894
895    #[test]
896    fn test_p256_known_2g() {
897        // Known value of 2*G for P-256.
898        let params = p256_params();
899        let g = JacobianPoint::from_affine(params.gx, params.gy);
900
901        let two = FieldElement::<4>::from_bytes_be(&[2]);
902        let result = scalar_mul_point(&two, &g, &params);
903        let (rx, ry) = result.to_affine(&params.p).unwrap();
904
905        let expected_x = hex_to_bytes("7CF27B188D034F7E8A52380304B51AC3C08969E277F21B35A60B48FC47669978");
906        let expected_y = hex_to_bytes("07775510DB8ED040293D9AC69F7430DBBA7DADE63CE982299E04B79D227873D1");
907
908        assert_eq!(rx.to_bytes_be(), expected_x);
909        assert_eq!(ry.to_bytes_be(), expected_y);
910    }
911}