Skip to main content

arcana/ecc/
eddsa.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4// Index-based loops in this module deliberately mirror the reference
5// algorithm (parallel-array access `a[i]`/`b[i]`, or a position-indexed
6// packing/reduction state) and keep the constant-time data layout explicit;
7// the `iter().enumerate()` rewrite would obscure it. Documented, single-lint
8// allow per CLAUDE.md §6 (scoped to this module, not a blanket suppression).
9#![allow(clippy::needless_range_loop)]
10
11//! EdDSA digital signatures (RFC 8032).
12//!
13//! # Algorithms
14//!
15//! - **Ed25519** — Edwards curve over `GF(2^255 - 19)` (pure +
16//!   `Ed25519ctx` + `Ed25519ph`).
17//! - **Ed448** — Edwards curve over `GF(2^448 - 2^224 - 1)`, implemented
18//!   in the sibling [`ed448`](super::ed448) module (RFC 8032 §5.2).
19//!
20//! This module implements `Fe25519` field arithmetic, extended-
21//! coordinate point operations on the twisted Edwards form, and
22//! the full sign / verify protocol for Ed25519.
23//!
24//! # Side-channel posture
25//!
26//! Per `arcana/doc/sca/countermeasures/eddsa.rst`:
27//!
28//! | Threat                                    | Status     | Roadmap item                                                            |
29//! |-------------------------------------------|------------|-------------------------------------------------------------------------|
30//! | SPA on scalar mul                         | partial    | `T1-F` — audit pass mirroring Weierstrass commit `76191c1`              |
31//! | DPA on scalar mul                         | vulnerable | `T2-A` (Z-rerandomization, shared with Weierstrass plan)                |
32//! | **Single-fault on RFC 8032 deterministic**| vulnerable | `T1-D` — hedged Ed25519 (CFRG `det-sigs-with-noise`, Romailler 2017)    |
33//! | Template attacks (Samwel et al. 2018)     | vulnerable | `T2-A` + `T2-B` (Z-rerand + scalar blinding)                            |
34//! | DPA on intermediate SHA-512 keyed digest  | partial    | `T2-D` — masked SHA-512, shared with HMAC consumer                      |
35//!
36//! ## Romailler-Pelissier 2017 — single-fault key recovery
37//!
38//! RFC 8032 derives the per-signature nonce `r` deterministically
39//! from `(seed, message)`. Two signatures of the same message
40//! produce the same `r`, which is fine cryptographically but
41//! **fragile under fault** (FDTC 2017,
42//! `romailler2017eddsa_fault`):
43//!
44//! ```text
45//!   Sign M  →  (R, s)         (normal)
46//!   Sign M  →  (R', s')       (one fault during SHA-512 → r' ≠ r)
47//!
48//!   k  = H(R  ‖ A ‖ M),   s  = r  + k * a mod ℓ
49//!   k' = H(R' ‖ A ‖ M),   s' = r' + k' * a mod ℓ
50//!
51//!   →  a = (s - s') / (k - k') mod ℓ
52//! ```
53//!
54//! One well-placed fault recovers the whole secret scalar `a`.
55//! The standard fix is **hedged signing**: derive `r` from
56//! `H(prefix ‖ ρ ‖ M)` with `ρ` 32 bytes of fresh randomness;
57//! deterministic mode (`ρ = 0^32`) stays available for KAT
58//! determinism. Roadmap item `T1-D`.
59//!
60//! # Zeroize-on-Drop
61//!
62//! [`Ed25519SecretKey`] currently does **not** implement `Drop`
63//! with `silentops::ct_zeroize`. Roadmap item `T2-E`.
64
65use crate::Hasher;
66use crate::hash::sha512::Sha512;
67
68// ============================================================
69// Field arithmetic for GF(2^255 - 19)
70// ============================================================
71
72/// Element of GF(2^255 - 19), stored as four 64-bit limbs in little-endian order.
73#[derive(Clone, Copy, Debug)]
74struct Fe25519([u64; 4]);
75
76/// p = 2^255 - 19
77const P: [u64; 4] = [
78    0xFFFF_FFFF_FFFF_FFED,
79    0xFFFF_FFFF_FFFF_FFFF,
80    0xFFFF_FFFF_FFFF_FFFF,
81    0x7FFF_FFFF_FFFF_FFFF,
82];
83
84impl Fe25519 {
85    const ZERO: Fe25519 = Fe25519([0, 0, 0, 0]);
86    const ONE: Fe25519 = Fe25519([1, 0, 0, 0]);
87
88    /// Reduce a 4-limb value mod p.  The input must be < 2*p.
89    fn reduce(mut limbs: [u64; 4]) -> Self {
90        // Try subtracting p; if it underflows, keep the original.
91        let (r0, borrow) = limbs[0].overflowing_sub(P[0]);
92        let (r1, borrow) = sbb(limbs[1], P[1], borrow);
93        let (r2, borrow) = sbb(limbs[2], P[2], borrow);
94        let (r3, borrow) = sbb(limbs[3], P[3], borrow);
95
96        // If borrow, the subtraction underflowed => limbs < p, keep limbs.
97        let mask = if borrow { u64::MAX } else { 0 };
98        limbs[0] = (limbs[0] & mask) | (r0 & !mask);
99        limbs[1] = (limbs[1] & mask) | (r1 & !mask);
100        limbs[2] = (limbs[2] & mask) | (r2 & !mask);
101        limbs[3] = (limbs[3] & mask) | (r3 & !mask);
102
103        Fe25519(limbs)
104    }
105
106    fn add(self, rhs: Self) -> Self {
107        let (r0, carry) = self.0[0].overflowing_add(rhs.0[0]);
108        let (r1, carry) = adc(self.0[1], rhs.0[1], carry);
109        let (r2, carry) = adc(self.0[2], rhs.0[2], carry);
110        let (r3, _carry) = adc(self.0[3], rhs.0[3], carry);
111        Fe25519::reduce([r0, r1, r2, r3])
112    }
113
114    fn sub(self, rhs: Self) -> Self {
115        let (r0, borrow) = self.0[0].overflowing_sub(rhs.0[0]);
116        let (r1, borrow) = sbb(self.0[1], rhs.0[1], borrow);
117        let (r2, borrow) = sbb(self.0[2], rhs.0[2], borrow);
118        let (r3, borrow) = sbb(self.0[3], rhs.0[3], borrow);
119
120        // If borrow, add p.
121        let mask = if borrow { u64::MAX } else { 0 };
122        let (r0, carry) = r0.overflowing_add(P[0] & mask);
123        let (r1, carry) = adc(r1, P[1] & mask, carry);
124        let (r2, carry) = adc(r2, P[2] & mask, carry);
125        let (r3, _) = adc(r3, P[3] & mask, carry);
126
127        Fe25519([r0, r1, r2, r3])
128    }
129
130    fn neg(self) -> Self {
131        Fe25519::ZERO.sub(self)
132    }
133
134    /// Multiply two field elements: schoolbook 256x256 -> 512, then reduce mod p.
135    fn mul(self, rhs: Self) -> Self {
136        let wide = mul256(self.0, rhs.0);
137        fe_reduce_wide(wide)
138    }
139
140    fn square(self) -> Self {
141        self.mul(self)
142    }
143
144    /// Compute self^exp mod p via square-and-multiply.
145    fn pow(self, exp: [u64; 4]) -> Self {
146        let mut result = Fe25519::ONE;
147        let mut base = self;
148        for i in 0..4 {
149            let mut e = exp[i];
150            for _ in 0..64 {
151                if e & 1 == 1 {
152                    result = result.mul(base);
153                }
154                base = base.square();
155                e >>= 1;
156            }
157        }
158        result
159    }
160
161    /// Modular inverse via Fermat's little theorem: a^(p-2) mod p.
162    fn inv(self) -> Self {
163        // p - 2 = 2^255 - 21
164        let pm2: [u64; 4] = [
165            0xFFFF_FFFF_FFFF_FFEB,
166            0xFFFF_FFFF_FFFF_FFFF,
167            0xFFFF_FFFF_FFFF_FFFF,
168            0x7FFF_FFFF_FFFF_FFFF,
169        ];
170        self.pow(pm2)
171    }
172
173    /// Constant-time equality (on fully reduced values).
174    fn equals(self, other: Self) -> bool {
175        let a = Fe25519::reduce(self.0);
176        let b = Fe25519::reduce(other.0);
177        let mut acc = 0u64;
178        for i in 0..4 {
179            acc |= a.0[i] ^ b.0[i];
180        }
181        acc == 0
182    }
183
184    fn is_zero(self) -> bool {
185        self.equals(Fe25519::ZERO)
186    }
187
188    /// Encode to 32 bytes, little-endian.
189    fn to_bytes(self) -> [u8; 32] {
190        let r = Fe25519::reduce(self.0);
191        let mut out = [0u8; 32];
192        out[0..8].copy_from_slice(&r.0[0].to_le_bytes());
193        out[8..16].copy_from_slice(&r.0[1].to_le_bytes());
194        out[16..24].copy_from_slice(&r.0[2].to_le_bytes());
195        out[24..32].copy_from_slice(&r.0[3].to_le_bytes());
196        out
197    }
198
199    /// Decode from 32 bytes, little-endian.  Clears bit 255.
200    fn from_bytes(bytes: &[u8; 32]) -> Self {
201        let mut limbs = [0u64; 4];
202        limbs[0] = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
203        limbs[1] = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
204        limbs[2] = u64::from_le_bytes(bytes[16..24].try_into().unwrap());
205        limbs[3] = u64::from_le_bytes(bytes[24..32].try_into().unwrap());
206        limbs[3] &= 0x7FFF_FFFF_FFFF_FFFF;
207        Fe25519::reduce(limbs)
208    }
209
210    fn from_u64(v: u64) -> Self {
211        Fe25519([v, 0, 0, 0])
212    }
213
214    /// Return 1 if the fully-reduced value is odd, 0 otherwise.
215    fn is_odd(self) -> u8 {
216        let r = Fe25519::reduce(self.0);
217        (r.0[0] & 1) as u8
218    }
219
220    /// Constant-time conditional select: choice=0 -> a, choice=1 -> b.
221    fn ct_select(a: Self, b: Self, choice: u8) -> Self {
222        let mask = (choice as u64).wrapping_neg();
223        Fe25519([
224            a.0[0] ^ (mask & (a.0[0] ^ b.0[0])),
225            a.0[1] ^ (mask & (a.0[1] ^ b.0[1])),
226            a.0[2] ^ (mask & (a.0[2] ^ b.0[2])),
227            a.0[3] ^ (mask & (a.0[3] ^ b.0[3])),
228        ])
229    }
230}
231
232// ---- Low-level helpers ----
233
234#[inline(always)]
235fn adc(a: u64, b: u64, carry_in: bool) -> (u64, bool) {
236    let (s1, c1) = a.overflowing_add(b);
237    let (s2, c2) = s1.overflowing_add(carry_in as u64);
238    (s2, c1 | c2)
239}
240
241#[inline(always)]
242fn sbb(a: u64, b: u64, borrow_in: bool) -> (u64, bool) {
243    let (s1, b1) = a.overflowing_sub(b);
244    let (s2, b2) = s1.overflowing_sub(borrow_in as u64);
245    (s2, b1 | b2)
246}
247
248#[inline(always)]
249fn mul64(a: u64, b: u64) -> u128 {
250    (a as u128) * (b as u128)
251}
252
253/// Schoolbook 256x256 -> 512 bit multiply.
254fn mul256(a: [u64; 4], b: [u64; 4]) -> [u64; 8] {
255    let mut r = [0u64; 8];
256    for i in 0..4 {
257        let mut carry = 0u64;
258        for j in 0..4 {
259            let uv = mul64(a[i], b[j]) + r[i + j] as u128 + carry as u128;
260            r[i + j] = uv as u64;
261            carry = (uv >> 64) as u64;
262        }
263        r[i + 4] = carry;
264    }
265    r
266}
267
268/// Reduce a 512-bit result mod p = 2^255 - 19.
269///
270/// Since 2^255 = 19 (mod p), we split at bit 255 and fold with factor 19.
271fn fe_reduce_wide(w: [u64; 8]) -> Fe25519 {
272    // Extract bit 255 from w[3].
273    let low3_top_bit = (w[3] >> 63) & 1;
274    let low = [w[0], w[1], w[2], w[3] & 0x7FFF_FFFF_FFFF_FFFF];
275
276    // high = bits [255..512) of w, shifted down.
277    let high = [
278        (w[4] << 1) | low3_top_bit,
279        (w[5] << 1) | (w[4] >> 63),
280        (w[6] << 1) | (w[5] >> 63),
281        (w[7] << 1) | (w[6] >> 63),
282        w[7] >> 63,
283    ];
284
285    // w = low + high * 2^255 = low + 19 * high (mod p).
286    let mut acc = [0u64; 5];
287    let mut carry = 0u128;
288    for i in 0..5 {
289        carry += low.get(i).copied().unwrap_or(0) as u128 + 19u128 * high[i] as u128;
290        acc[i] = carry as u64;
291        carry >>= 64;
292    }
293
294    // Split at bit 255 again.
295    let top_bit = (acc[3] >> 63) & 1;
296    acc[3] &= 0x7FFF_FFFF_FFFF_FFFF;
297    let extra = (acc[4] << 1) | top_bit;
298
299    // Add 19 * extra to lower 255 bits.
300    let add = extra as u128 * 19;
301    let (r0, c) = acc[0].overflowing_add(add as u64);
302    let (r1, c) = adc(acc[1], (add >> 64) as u64, c);
303    let (r2, c) = adc(acc[2], 0, c);
304    let (r3, _) = adc(acc[3], 0, c);
305
306    Fe25519::reduce([r0, r1, r2, r3])
307}
308
309// ============================================================
310// Scalar arithmetic mod L (group order)
311// ============================================================
312
313/// L = 2^252 + 27742317777372353535851937790883648493
314const L: [u64; 4] = [
315    0x5812631A5CF5D3ED,
316    0x14DEF9DEA2F79CD6,
317    0x0000000000000000,
318    0x1000000000000000,
319];
320
321/// Reduce a 64-byte (512-bit) little-endian value mod L.
322fn sc_reduce(input: &[u8; 64]) -> [u8; 32] {
323    let mut a = [0u64; 8];
324    for i in 0..8 {
325        a[i] = u64::from_le_bytes(input[i * 8..(i + 1) * 8].try_into().unwrap());
326    }
327
328    let result = bn_mod(&a, 8);
329
330    let mut out = [0u8; 32];
331    for i in 0..4 {
332        out[i * 8..(i + 1) * 8].copy_from_slice(&result[i].to_le_bytes());
333    }
334    out
335}
336
337/// Add two scalars mod L.  Both inputs must already be < L.
338fn sc_add(a: &[u8; 32], b: &[u8; 32]) -> [u8; 32] {
339    let mut al = [0u64; 4];
340    let mut bl = [0u64; 4];
341    for i in 0..4 {
342        al[i] = u64::from_le_bytes(a[i * 8..(i + 1) * 8].try_into().unwrap());
343        bl[i] = u64::from_le_bytes(b[i * 8..(i + 1) * 8].try_into().unwrap());
344    }
345
346    let (r0, carry) = al[0].overflowing_add(bl[0]);
347    let (r1, carry) = adc(al[1], bl[1], carry);
348    let (r2, carry) = adc(al[2], bl[2], carry);
349    let (r3, carry) = adc(al[3], bl[3], carry);
350
351    let mut result = [r0, r1, r2, r3];
352
353    // Subtract L if result >= L (carry set, or no borrow on subtraction).
354    let (s0, borrow) = result[0].overflowing_sub(L[0]);
355    let (s1, borrow) = sbb(result[1], L[1], borrow);
356    let (s2, borrow) = sbb(result[2], L[2], borrow);
357    let (s3, borrow) = sbb(result[3], L[3], borrow);
358
359    let use_sub = carry | !borrow;
360    let mask = if use_sub { 0u64 } else { u64::MAX };
361    result[0] = (result[0] & mask) | (s0 & !mask);
362    result[1] = (result[1] & mask) | (s1 & !mask);
363    result[2] = (result[2] & mask) | (s2 & !mask);
364    result[3] = (result[3] & mask) | (s3 & !mask);
365
366    let mut out = [0u8; 32];
367    for i in 0..4 {
368        out[i * 8..(i + 1) * 8].copy_from_slice(&result[i].to_le_bytes());
369    }
370    out
371}
372
373/// Multiply two 256-bit scalars mod L.
374fn sc_mul(a: &[u8; 32], b: &[u8; 32]) -> [u8; 32] {
375    let mut al = [0u64; 4];
376    let mut bl = [0u64; 4];
377    for i in 0..4 {
378        al[i] = u64::from_le_bytes(a[i * 8..(i + 1) * 8].try_into().unwrap());
379        bl[i] = u64::from_le_bytes(b[i * 8..(i + 1) * 8].try_into().unwrap());
380    }
381
382    let wide = mul256(al, bl);
383    let result = bn_mod(&wide, 8);
384
385    let mut out = [0u8; 32];
386    for i in 0..4 {
387        out[i * 8..(i + 1) * 8].copy_from_slice(&result[i].to_le_bytes());
388    }
389    out
390}
391
392/// Big-number modular reduction: compute a[0..limbs] mod L via bit-by-bit
393/// shift-and-subtract.  Simple but obviously correct.
394fn bn_mod(a: &[u64], limbs: usize) -> [u64; 4] {
395    // Working register: 5 limbs (320 bits) is enough since L is 253 bits and
396    // we only ever hold a value < 2*L during the loop.
397    let mut r = [0u64; 5];
398
399    // Process input bits from MSB down to LSB.
400    for word_idx in (0..limbs).rev() {
401        for bit_idx in (0..64).rev() {
402            // Shift r left by 1.
403            let mut carry = 0u64;
404            for i in 0..5 {
405                let new_carry = r[i] >> 63;
406                r[i] = (r[i] << 1) | carry;
407                carry = new_carry;
408            }
409            // Bring in the next bit from a.
410            r[0] |= (a[word_idx] >> bit_idx) & 1;
411
412            // If r >= L, subtract L.
413            let (s0, b) = r[0].overflowing_sub(L[0]);
414            let (s1, b) = sbb(r[1], L[1], b);
415            let (s2, b) = sbb(r[2], L[2], b);
416            let (s3, b) = sbb(r[3], L[3], b);
417            let (s4, b) = sbb(r[4], 0, b);
418            if !b {
419                r[0] = s0;
420                r[1] = s1;
421                r[2] = s2;
422                r[3] = s3;
423                r[4] = s4;
424            }
425        }
426    }
427
428    [r[0], r[1], r[2], r[3]]
429}
430
431// ============================================================
432// Extended Edwards point (X : Y : Z : T) with T = X*Y/Z
433// ============================================================
434
435/// Point on the Ed25519 curve in extended coordinates.
436#[derive(Clone, Copy, Debug)]
437struct ExtPoint {
438    x: Fe25519,
439    y: Fe25519,
440    z: Fe25519,
441    t: Fe25519,
442}
443
444/// d = -121665/121666 mod p
445/// = 37095705934669439343138083508754565189542113879843219016388785533085940283555
446const D: Fe25519 = Fe25519([
447    0x75EB4DCA135978A3,
448    0x00700A4D4141D8AB,
449    0x8CC740797779E898,
450    0x52036CEE2B6FFE73,
451]);
452
453/// 2*d mod p (kept for reference/tests).
454#[cfg(test)]
455const D2: Fe25519 = Fe25519([
456    0xEBD69B9426B2F159,
457    0x00E0149A8283B156,
458    0x198E80F2EEF3D130,
459    0x2406D9DC56DFFCE7,
460]);
461
462/// Base point x-coordinate.
463/// Bx = 15112221349535807912866137220509078750507884956996801397906370244768422529236
464const B_X: Fe25519 = Fe25519([
465    0xC9562D608F25D51A,
466    0x692CC7609525A7B2,
467    0xC0A4E231FDD6DC5C,
468    0x216936D3CD6E53FE,
469]);
470
471/// Base point y-coordinate.
472/// By = 46316835694926478169428394003475163141307993866256225615783033890098355573289
473const B_Y: Fe25519 = Fe25519([
474    0x6666666666666658,
475    0x6666666666666666,
476    0x6666666666666666,
477    0x6666666666666666,
478]);
479
480impl ExtPoint {
481    const IDENTITY: ExtPoint = ExtPoint {
482        x: Fe25519::ZERO,
483        y: Fe25519::ONE,
484        z: Fe25519::ONE,
485        t: Fe25519::ZERO,
486    };
487
488    fn from_affine(x: Fe25519, y: Fe25519) -> Self {
489        ExtPoint {
490            x,
491            y,
492            z: Fe25519::ONE,
493            t: x.mul(y),
494        }
495    }
496
497    fn basepoint() -> Self {
498        ExtPoint::from_affine(B_X, B_Y)
499    }
500
501    /// Point doubling using the unified doubling formula for twisted Edwards
502    /// curves with a = -1.  Reference: HHCD08 (Hisil et al.), Section 4.
503    fn double(self) -> Self {
504        let a = self.x.square();
505        let b = self.y.square();
506        let c = self.z.square().add(self.z.square()); // 2*Z^2
507        let d = a.neg(); // -X^2  (a = -1 in the twisted Edwards equation)
508        let e = self.x.add(self.y).square().sub(a).sub(b);
509        let g = d.add(b);
510        let f = g.sub(c);
511        let h = d.sub(b);
512
513        ExtPoint {
514            x: e.mul(f),
515            y: g.mul(h),
516            t: e.mul(h),
517            z: f.mul(g),
518        }
519    }
520
521    /// Point addition using the unified addition formula for twisted Edwards
522    /// curves with a = -1.  Reference: HHCD08, Section 3.1.
523    fn add(self, other: Self) -> Self {
524        let a = self.x.mul(other.x);
525        let b = self.y.mul(other.y);
526        let c = self.t.mul(other.t).mul(D);
527        let dd = self.z.mul(other.z);
528
529        let e = (self.x.add(self.y)).mul(other.x.add(other.y)).sub(a).sub(b);
530        let f = dd.sub(c);
531        let g = dd.add(c);
532        let h = b.add(a); // b - (-1)*a = b + a, since curve coeff a = -1
533
534        ExtPoint {
535            x: e.mul(f),
536            y: g.mul(h),
537            t: e.mul(h),
538            z: f.mul(g),
539        }
540    }
541
542    /// Constant-time scalar multiplication (left-to-right double-and-add with
543    /// conditional select on every bit).
544    fn scalar_mul(self, scalar: &[u8; 32]) -> Self {
545        let mut result = ExtPoint::IDENTITY;
546        for byte_idx in (0..32).rev() {
547            for bit_idx in (0..8).rev() {
548                result = result.double();
549                let bit = (scalar[byte_idx] >> bit_idx) & 1;
550                let candidate = result.add(self);
551                result = ExtPoint::ct_select(result, candidate, bit);
552            }
553        }
554        result
555    }
556
557    fn ct_select(a: Self, b: Self, choice: u8) -> Self {
558        ExtPoint {
559            x: Fe25519::ct_select(a.x, b.x, choice),
560            y: Fe25519::ct_select(a.y, b.y, choice),
561            z: Fe25519::ct_select(a.z, b.z, choice),
562            t: Fe25519::ct_select(a.t, b.t, choice),
563        }
564    }
565
566    /// Compress a point to 32 bytes (RFC 8032 encoding):
567    /// encode y in little-endian, put the sign of x into the top bit.
568    fn compress(self) -> [u8; 32] {
569        let z_inv = self.z.inv();
570        let x = self.x.mul(z_inv);
571        let y = self.y.mul(z_inv);
572        let mut bytes = y.to_bytes();
573        bytes[31] |= x.is_odd() << 7;
574        bytes
575    }
576
577    /// Decompress a 32-byte encoded point.
578    fn decompress(bytes: &[u8; 32]) -> Option<Self> {
579        let x_sign = (bytes[31] >> 7) & 1;
580        let mut y_bytes = *bytes;
581        y_bytes[31] &= 0x7F;
582        let y = Fe25519::from_bytes(&y_bytes);
583
584        // x^2 = (y^2 - 1) / (d * y^2 + 1)
585        let y2 = y.square();
586        let u = y2.sub(Fe25519::ONE); // numerator
587        let v = D.mul(y2).add(Fe25519::ONE); // denominator
588
589        // Compute candidate: x = u * v^3 * (u * v^7)^((p-5)/8)
590        let v3 = v.square().mul(v);
591        let v7 = v3.square().mul(v);
592        let uv7 = u.mul(v7);
593
594        // (p-5)/8 = (2^255 - 24) / 8 = 2^252 - 3
595        let exp: [u64; 4] = [
596            0xFFFF_FFFF_FFFF_FFFD,
597            0xFFFF_FFFF_FFFF_FFFF,
598            0xFFFF_FFFF_FFFF_FFFF,
599            0x0FFF_FFFF_FFFF_FFFF,
600        ];
601        let uv7_pow = uv7.pow(exp);
602        let mut x = u.mul(v3).mul(uv7_pow);
603
604        // Verify: v * x^2 should equal u.
605        let check = v.mul(x.square());
606        if check.equals(u) {
607            // ok
608        } else if check.equals(u.neg()) {
609            // Multiply x by sqrt(-1) = 2^((p-1)/4).
610            let sqrt_m1 = compute_sqrt_m1();
611            x = x.mul(sqrt_m1);
612        } else {
613            return None;
614        }
615
616        if x.is_zero() && x_sign == 1 {
617            return None;
618        }
619
620        if x.is_odd() != x_sign {
621            x = x.neg();
622        }
623
624        let t = x.mul(y);
625        Some(ExtPoint {
626            x,
627            y,
628            z: Fe25519::ONE,
629            t,
630        })
631    }
632
633    /// Check projective equality: X1*Z2 == X2*Z1 and Y1*Z2 == Y2*Z1.
634    fn equals(self, other: Self) -> bool {
635        let lhs_x = self.x.mul(other.z);
636        let rhs_x = other.x.mul(self.z);
637        let lhs_y = self.y.mul(other.z);
638        let rhs_y = other.y.mul(self.z);
639        lhs_x.equals(rhs_x) && lhs_y.equals(rhs_y)
640    }
641}
642
643/// Compute sqrt(-1) mod p = 2^((p-1)/4).
644fn compute_sqrt_m1() -> Fe25519 {
645    // (p-1)/4 = (2^255 - 20)/4 = 2^253 - 5
646    let exp: [u64; 4] = [
647        0xFFFF_FFFF_FFFF_FFFB,
648        0xFFFF_FFFF_FFFF_FFFF,
649        0xFFFF_FFFF_FFFF_FFFF,
650        0x1FFF_FFFF_FFFF_FFFF,
651    ];
652    Fe25519::from_u64(2).pow(exp)
653}
654
655// ============================================================
656// Ed25519 public API
657// ============================================================
658
659/// Ed25519 public key (32 bytes, compressed point encoding).
660#[derive(Clone, Debug, PartialEq, Eq)]
661pub struct Ed25519PublicKey(pub [u8; 32]);
662
663/// Ed25519 secret key (the original 32-byte seed).
664#[derive(Clone)]
665pub struct Ed25519SecretKey(pub [u8; 32]);
666
667/// Ed25519 signature (64 bytes: R || S).
668#[derive(Clone, Debug, PartialEq, Eq)]
669pub struct Ed25519Signature(pub [u8; 64]);
670
671/// Generate an Ed25519 key pair from a 32-byte secret seed.
672///
673/// Returns (public_key, secret_key).
674pub fn ed25519_keygen(secret: &[u8; 32]) -> (Ed25519PublicKey, Ed25519SecretKey) {
675    let hash = Sha512::hash(secret);
676
677    let mut a = [0u8; 32];
678    a.copy_from_slice(&hash[..32]);
679    a[0] &= 248;
680    a[31] &= 127;
681    a[31] |= 64;
682
683    let big_a = ExtPoint::basepoint().scalar_mul(&a);
684    let pk_bytes = big_a.compress();
685
686    (Ed25519PublicKey(pk_bytes), Ed25519SecretKey(*secret))
687}
688
689// ----------------------------------------------------------------------------
690// Generic sign / verify core (RFC 8032 §5.1)
691//
692// The three Ed25519 variants (pure, ctx, ph) all share the same algebraic
693// structure -- they differ only in two places:
694//
695//  1. Whether a `dom2(F, C)` prefix is prepended to the SHA-512 inputs.
696//     (None for pure Ed25519, Some for ctx and ph.)
697//  2. Whether the "message" bytes fed to the two SHA-512 calls are the
698//     raw application message (pure / ctx) or a 64-byte SHA-512 digest
699//     of it (ph). The implementation is the same either way: the caller
700//     is responsible for hashing first when in ph mode and just passes
701//     a 64-byte slice as `message_or_hash`.
702//
703// Both helpers are kept private to the module: the public surface is the
704// six wrappers below (sign / verify / ctx_sign / ctx_verify / ph_sign /
705// ph_verify) which give each variant a clear, unambiguous name.
706// ----------------------------------------------------------------------------
707
708/// Update a SHA-512 hasher with the optional `dom2` domain-separation
709/// prefix (RFC 8032 §5.1.6). For pure Ed25519 (`dom2 == None`) this is
710/// a no-op, preserving byte-for-byte the existing test vectors.
711fn update_dom2(hasher: &mut Sha512, dom2: Option<&[u8]>) {
712    if let Some(prefix) = dom2 {
713        hasher.update(prefix);
714    }
715}
716
717/// Generic Ed25519 sign core. Used by all three variants.
718fn ed25519_sign_internal(sk: &Ed25519SecretKey, message_or_hash: &[u8], dom2: Option<&[u8]>) -> Ed25519Signature {
719    let hash = Sha512::hash(&sk.0);
720
721    let mut a = [0u8; 32];
722    a.copy_from_slice(&hash[..32]);
723    a[0] &= 248;
724    a[31] &= 127;
725    a[31] |= 64;
726
727    let prefix = &hash[32..64];
728
729    // Public key A = a * B.
730    let big_a = ExtPoint::basepoint().scalar_mul(&a);
731    let pk_bytes = big_a.compress();
732
733    // r = SHA-512(dom2 || prefix || M) mod L  -- M is `message_or_hash`.
734    let mut hasher = Sha512::new();
735    update_dom2(&mut hasher, dom2);
736    hasher.update(prefix);
737    hasher.update(message_or_hash);
738    let r_hash = hasher.finalize();
739    let mut r_wide = [0u8; 64];
740    r_wide.copy_from_slice(&r_hash);
741    let r_scalar = sc_reduce(&r_wide);
742
743    // R = r * B.
744    let big_r = ExtPoint::basepoint().scalar_mul(&r_scalar);
745    let r_bytes = big_r.compress();
746
747    // S = (r + SHA-512(dom2 || R || A || M) * a) mod L.
748    let mut hasher = Sha512::new();
749    update_dom2(&mut hasher, dom2);
750    hasher.update(&r_bytes);
751    hasher.update(&pk_bytes);
752    hasher.update(message_or_hash);
753    let k_hash = hasher.finalize();
754    let mut k_wide = [0u8; 64];
755    k_wide.copy_from_slice(&k_hash);
756    let k = sc_reduce(&k_wide);
757
758    let ka = sc_mul(&k, &a);
759    let s = sc_add(&r_scalar, &ka);
760
761    let mut sig = [0u8; 64];
762    sig[..32].copy_from_slice(&r_bytes);
763    sig[32..].copy_from_slice(&s);
764
765    Ed25519Signature(sig)
766}
767
768/// Generic Ed25519 verify core. Used by all three variants.
769fn ed25519_verify_internal(
770    pk: &Ed25519PublicKey,
771    message_or_hash: &[u8],
772    sig: &Ed25519Signature,
773    dom2: Option<&[u8]>,
774) -> bool {
775    let r_bytes: [u8; 32] = match sig.0[..32].try_into() {
776        Ok(b) => b,
777        Err(_) => return false,
778    };
779    let big_r = match ExtPoint::decompress(&r_bytes) {
780        Some(p) => p,
781        None => return false,
782    };
783
784    let big_a = match ExtPoint::decompress(&pk.0) {
785        Some(p) => p,
786        None => return false,
787    };
788
789    let s_bytes: [u8; 32] = match sig.0[32..].try_into() {
790        Ok(b) => b,
791        Err(_) => return false,
792    };
793
794    // Check S < L.
795    {
796        let mut sl = [0u64; 4];
797        for i in 0..4 {
798            sl[i] = u64::from_le_bytes(s_bytes[i * 8..(i + 1) * 8].try_into().unwrap());
799        }
800        let (_, borrow) = sl[0].overflowing_sub(L[0]);
801        let (_, borrow) = sbb(sl[1], L[1], borrow);
802        let (_, borrow) = sbb(sl[2], L[2], borrow);
803        let (_, borrow) = sbb(sl[3], L[3], borrow);
804        if !borrow {
805            return false; // S >= L
806        }
807    }
808
809    // k = SHA-512(dom2 || R || A || M) mod L.
810    let mut hasher = Sha512::new();
811    update_dom2(&mut hasher, dom2);
812    hasher.update(&r_bytes);
813    hasher.update(&pk.0);
814    hasher.update(message_or_hash);
815    let k_hash = hasher.finalize();
816    let mut k_wide = [0u8; 64];
817    k_wide.copy_from_slice(&k_hash);
818    let k = sc_reduce(&k_wide);
819
820    // Cofactored verification: [8][S]B == [8]R + [8][k]A.
821    let sb = ExtPoint::basepoint().scalar_mul(&s_bytes);
822    let ka = big_a.scalar_mul(&k);
823    let rhs = big_r.add(ka);
824
825    // Multiply both sides by cofactor 8 (three doublings).
826    let lhs = sb.double().double().double();
827    let rhs = rhs.double().double().double();
828
829    lhs.equals(rhs)
830}
831
832// ----------------------------------------------------------------------------
833// dom2 prefix construction (RFC 8032 §5.1.6 / §5.1.7)
834//
835//   dom2(F, C) = "SigEd25519 no Ed25519 collisions" || octet(F) || octet(len(C)) || C
836//
837// where F = 0 for Ed25519ctx and F = 1 for Ed25519ph. The literal string
838// is exactly 32 bytes; F and len(C) take one byte each; the context C
839// follows verbatim. Maximum context length is 255 bytes.
840// ----------------------------------------------------------------------------
841
842const DOM2_LITERAL: &[u8] = b"SigEd25519 no Ed25519 collisions";
843
844/// Build the `dom2(F, C)` prefix used by Ed25519ctx (F=0) and
845/// Ed25519ph (F=1). Returns `None` if `context.len() > 255` (the
846/// max RFC 8032 allows since `len(C)` must fit in one octet).
847fn build_dom2(f: u8, context: &[u8]) -> Option<Vec<u8>> {
848    if context.len() > 255 {
849        return None;
850    }
851    let mut out = Vec::with_capacity(DOM2_LITERAL.len() + 2 + context.len());
852    out.extend_from_slice(DOM2_LITERAL);
853    out.push(f);
854    out.push(context.len() as u8);
855    out.extend_from_slice(context);
856    Some(out)
857}
858
859// ----------------------------------------------------------------------------
860// Ed25519 (pure) -- public API. RFC 8032 §5.1
861// ----------------------------------------------------------------------------
862
863/// Sign a message with Ed25519 (pure mode, no prehashing).
864///
865/// This is RFC 8032 §5.1 -- the original / canonical Ed25519 mode.
866/// **Identical bytes** to what was produced before this commit's
867/// generic-core refactor; the existing pinned RFC 8032 §7.1 test
868/// vectors still pass byte-exact.
869pub fn ed25519_sign(sk: &Ed25519SecretKey, msg: &[u8]) -> Ed25519Signature {
870    ed25519_sign_internal(sk, msg, None)
871}
872
873/// Verify an Ed25519 signature (pure mode).
874pub fn ed25519_verify(pk: &Ed25519PublicKey, msg: &[u8], sig: &Ed25519Signature) -> bool {
875    ed25519_verify_internal(pk, msg, sig, None)
876}
877
878// ----------------------------------------------------------------------------
879// Ed25519ctx -- RFC 8032 §5.1.6
880//
881// Same as pure Ed25519 except both SHA-512 calls are prefixed by
882// `dom2(0, context)`. The context is a domain-separation tag chosen
883// by the calling protocol (max 255 bytes); a non-empty context is
884// REQUIRED by the RFC. Two ctx signatures with different contexts
885// are guaranteed to be unrelated even if the message bytes are
886// identical -- this is the property protocol designers want when
887// the same key is used for multiple purposes.
888// ----------------------------------------------------------------------------
889
890/// Sign a message with Ed25519ctx (RFC 8032 §5.1.6).
891///
892/// `context` is a non-empty domain-separation tag (max 255 bytes).
893/// Returns `None` if the context is empty (the RFC mandates a
894/// non-empty context for ctx mode) or longer than 255 bytes.
895pub fn ed25519ctx_sign(sk: &Ed25519SecretKey, msg: &[u8], context: &[u8]) -> Option<Ed25519Signature> {
896    if context.is_empty() {
897        return None;
898    }
899    let dom2 = build_dom2(0, context)?;
900    Some(ed25519_sign_internal(sk, msg, Some(&dom2)))
901}
902
903/// Verify an Ed25519ctx signature.
904///
905/// The same `context` used at sign time must be supplied here. A
906/// signature produced under one context will NOT verify under a
907/// different context, even if the (sk, msg) are the same -- that's
908/// the whole point of ctx mode.
909pub fn ed25519ctx_verify(pk: &Ed25519PublicKey, msg: &[u8], context: &[u8], sig: &Ed25519Signature) -> bool {
910    if context.is_empty() || context.len() > 255 {
911        return false;
912    }
913    let dom2 = match build_dom2(0, context) {
914        Some(d) => d,
915        None => return false,
916    };
917    ed25519_verify_internal(pk, msg, sig, Some(&dom2))
918}
919
920// ----------------------------------------------------------------------------
921// Ed25519ph -- RFC 8032 §5.1.7
922//
923// "Pre-hashed" Ed25519: the message is hashed with SHA-512 first, and
924// the resulting 64-byte digest is what gets fed into the two internal
925// SHA-512 calls (with the `dom2(1, context)` prefix). This lets a
926// signer sign very large messages without making two passes over them
927// in the application layer (e.g. file signing, where the application
928// already has a streaming SHA-512 of the file at hand).
929//
930// The context is OPTIONAL in Ed25519ph (zero-length context is allowed).
931// ----------------------------------------------------------------------------
932
933/// Sign a message with Ed25519ph (RFC 8032 §5.1.7, pre-hashed mode).
934///
935/// The message is internally hashed with SHA-512 before being fed
936/// into the Ed25519 algebraic core. `context` may be empty (unlike
937/// ctx mode); maximum context length is still 255 bytes.
938pub fn ed25519ph_sign(sk: &Ed25519SecretKey, msg: &[u8], context: &[u8]) -> Option<Ed25519Signature> {
939    let dom2 = build_dom2(1, context)?;
940    let m_prime = Sha512::hash(msg);
941    Some(ed25519_sign_internal(sk, &m_prime, Some(&dom2)))
942}
943
944/// Verify an Ed25519ph signature.
945pub fn ed25519ph_verify(pk: &Ed25519PublicKey, msg: &[u8], context: &[u8], sig: &Ed25519Signature) -> bool {
946    if context.len() > 255 {
947        return false;
948    }
949    let dom2 = match build_dom2(1, context) {
950        Some(d) => d,
951        None => return false,
952    };
953    let m_prime = Sha512::hash(msg);
954    ed25519_verify_internal(pk, &m_prime, sig, Some(&dom2))
955}
956
957/// Variant of [`ed25519ph_sign`] that takes a precomputed SHA-512
958/// digest of the message instead of the message itself. Useful when
959/// the caller already has the digest from a streaming hash (e.g. a
960/// file scanner that produced the digest as it read the file).
961///
962/// The `prehashed` argument **must** be exactly 64 bytes (the
963/// SHA-512 output length). Returns `None` for any other length.
964pub fn ed25519ph_sign_prehashed(sk: &Ed25519SecretKey, prehashed: &[u8], context: &[u8]) -> Option<Ed25519Signature> {
965    if prehashed.len() != 64 {
966        return None;
967    }
968    let dom2 = build_dom2(1, context)?;
969    Some(ed25519_sign_internal(sk, prehashed, Some(&dom2)))
970}
971
972/// Verify a precomputed-digest Ed25519ph signature. Counterpart to
973/// [`ed25519ph_sign_prehashed`].
974pub fn ed25519ph_verify_prehashed(
975    pk: &Ed25519PublicKey,
976    prehashed: &[u8],
977    context: &[u8],
978    sig: &Ed25519Signature,
979) -> bool {
980    if prehashed.len() != 64 || context.len() > 255 {
981        return false;
982    }
983    let dom2 = match build_dom2(1, context) {
984        Some(d) => d,
985        None => return false,
986    };
987    ed25519_verify_internal(pk, prehashed, sig, Some(&dom2))
988}
989
990// ============================================================
991// Ed448
992// ============================================================
993//
994// Ed448 (RFC 8032 §5.2, Edwards448-Goldilocks over GF(2^448 - 2^224 - 1),
995// SHAKE256, 57-byte keys / 114-byte signatures) is implemented in the
996// sibling module `ed448.rs`.
997
998// ============================================================
999// Tests
1000// ============================================================
1001
1002#[cfg(test)]
1003mod tests {
1004    use super::*;
1005
1006    fn hex_to_bytes(s: &str) -> Vec<u8> {
1007        (0..s.len())
1008            .step_by(2)
1009            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
1010            .collect()
1011    }
1012
1013    #[test]
1014    fn test_fe25519_basic() {
1015        let a = Fe25519::ONE;
1016        let b = Fe25519::ONE;
1017        let c = a.add(b);
1018        assert_eq!(c.0[0], 2);
1019        assert_eq!(c.0[1], 0);
1020
1021        let d = c.sub(a);
1022        assert!(d.equals(Fe25519::ONE));
1023    }
1024
1025    #[test]
1026    fn test_fe25519_mul() {
1027        let a = Fe25519::from_u64(7);
1028        let b = Fe25519::from_u64(13);
1029        let c = a.mul(b);
1030        assert!(c.equals(Fe25519::from_u64(91)));
1031    }
1032
1033    #[test]
1034    fn test_fe25519_inv() {
1035        let a = Fe25519::from_u64(42);
1036        let b = a.inv();
1037        let c = a.mul(b);
1038        assert!(c.equals(Fe25519::ONE));
1039    }
1040
1041    #[test]
1042    fn test_basepoint_on_curve() {
1043        // Verify: -x^2 + y^2 == 1 + d*x^2*y^2
1044        let x2 = B_X.square();
1045        let y2 = B_Y.square();
1046        let lhs = y2.sub(x2);
1047        let rhs = Fe25519::ONE.add(D.mul(x2).mul(y2));
1048        assert!(lhs.equals(rhs), "Base point not on curve!");
1049    }
1050
1051    #[test]
1052    fn test_d_constant() {
1053        // d = -121665 * inv(121666) mod p
1054        let n = Fe25519::from_u64(121665).neg();
1055        let d_inv = Fe25519::from_u64(121666).inv();
1056        let d_computed = n.mul(d_inv);
1057        assert!(d_computed.equals(D), "D constant is wrong");
1058    }
1059
1060    #[test]
1061    fn test_d2_constant() {
1062        let d2_computed = D.add(D);
1063        assert!(d2_computed.equals(D2), "D2 constant is wrong");
1064    }
1065
1066    #[test]
1067    fn test_basepoint_compress_decompress() {
1068        let bp = ExtPoint::basepoint();
1069        let compressed = bp.compress();
1070        let decompressed = ExtPoint::decompress(&compressed).expect("decompress failed");
1071        assert!(bp.equals(decompressed));
1072    }
1073
1074    fn naive_scalar_mul(p: ExtPoint, scalar: &[u8; 32]) -> ExtPoint {
1075        // Compute by iterating MSB to LSB, doubling and conditionally
1076        // adding -- but using a simple non-CT branch (for testing only).
1077        let mut acc = ExtPoint::IDENTITY;
1078        let mut started = false;
1079        for byte_idx in (0..32).rev() {
1080            for bit_idx in (0..8).rev() {
1081                if started {
1082                    acc = acc.double();
1083                }
1084                let bit = (scalar[byte_idx] >> bit_idx) & 1;
1085                if bit == 1 {
1086                    if !started {
1087                        acc = p;
1088                        started = true;
1089                    } else {
1090                        acc = acc.add(p);
1091                    }
1092                }
1093            }
1094        }
1095        acc
1096    }
1097
1098    #[test]
1099    fn test_scalar_mul_vs_naive() {
1100        let bp = ExtPoint::basepoint();
1101        let mut s = [0u8; 32];
1102        // Some pseudo-random scalar with many bits set.
1103        for i in 0..32 {
1104            s[i] = (i as u8) ^ 0xa5;
1105        }
1106        s[31] &= 0x7f;
1107        let r1 = bp.scalar_mul(&s);
1108        let r2 = naive_scalar_mul(bp, &s);
1109        assert!(r1.equals(r2));
1110    }
1111
1112    #[test]
1113    fn test_sc_reduce_basic() {
1114        // Test 1: input = 1
1115        let mut x = [0u8; 64];
1116        x[0] = 1;
1117        let r = sc_reduce(&x);
1118        assert_eq!(r[0], 1);
1119        for i in 1..32 {
1120            assert_eq!(r[i], 0);
1121        }
1122
1123        // Test 2: input = L (encoded LE in 64 bytes)
1124        let mut x = [0u8; 64];
1125        x[0..8].copy_from_slice(&L[0].to_le_bytes());
1126        x[8..16].copy_from_slice(&L[1].to_le_bytes());
1127        x[16..24].copy_from_slice(&L[2].to_le_bytes());
1128        x[24..32].copy_from_slice(&L[3].to_le_bytes());
1129        let r = sc_reduce(&x);
1130        for i in 0..32 {
1131            assert_eq!(r[i], 0, "L mod L should be 0, byte {}", i);
1132        }
1133
1134        // Test 3: input = L+1 → result = 1
1135        let mut x = [0u8; 64];
1136        x[0..8].copy_from_slice(&(L[0] + 1).to_le_bytes());
1137        x[8..16].copy_from_slice(&L[1].to_le_bytes());
1138        x[16..24].copy_from_slice(&L[2].to_le_bytes());
1139        x[24..32].copy_from_slice(&L[3].to_le_bytes());
1140        let r = sc_reduce(&x);
1141        assert_eq!(r[0], 1);
1142    }
1143
1144    #[test]
1145    fn test_fe_pow_fermat() {
1146        // a^(p-1) == 1 mod p for a != 0.
1147        let a = Fe25519::from_u64(7);
1148        let pm1: [u64; 4] = [
1149            0xFFFF_FFFF_FFFF_FFEC,
1150            0xFFFF_FFFF_FFFF_FFFF,
1151            0xFFFF_FFFF_FFFF_FFFF,
1152            0x7FFF_FFFF_FFFF_FFFF,
1153        ];
1154        let r = a.pow(pm1);
1155        assert!(r.equals(Fe25519::ONE));
1156    }
1157
1158    #[test]
1159    fn test_scalar_mul_two() {
1160        let bp = ExtPoint::basepoint();
1161        let mut two = [0u8; 32];
1162        two[0] = 2;
1163        let r = bp.scalar_mul(&two);
1164        assert!(r.equals(bp.double()));
1165    }
1166
1167    #[test]
1168    fn test_scalar_mul_three() {
1169        let bp = ExtPoint::basepoint();
1170        let mut three = [0u8; 32];
1171        three[0] = 3;
1172        let r = bp.scalar_mul(&three);
1173        assert!(r.equals(bp.double().add(bp)));
1174    }
1175
1176    #[test]
1177    fn test_scalar_mul_identity() {
1178        let bp = ExtPoint::basepoint();
1179        let mut one = [0u8; 32];
1180        one[0] = 1;
1181        let result = bp.scalar_mul(&one);
1182        assert!(result.equals(bp));
1183    }
1184
1185    #[test]
1186    fn test_point_add_double() {
1187        let bp = ExtPoint::basepoint();
1188        let bp2_add = bp.add(bp);
1189        let bp2_dbl = bp.double();
1190        assert!(bp2_add.equals(bp2_dbl));
1191    }
1192
1193    /// RFC 8032, Section 7.1 - Test Vector 1 (empty message).
1194    #[test]
1195    fn test_ed25519_vector1() {
1196        let sk_hex = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60";
1197        let pk_hex = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";
1198        let sig_hex = "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155\
1199                        5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b";
1200
1201        let sk_bytes = hex_to_bytes(sk_hex);
1202        let pk_bytes = hex_to_bytes(pk_hex);
1203        let sig_bytes = hex_to_bytes(sig_hex);
1204
1205        let mut sk = [0u8; 32];
1206        sk.copy_from_slice(&sk_bytes);
1207
1208        let (pk, secret) = ed25519_keygen(&sk);
1209        assert_eq!(&pk.0[..], &pk_bytes[..], "Public key mismatch");
1210
1211        let signature = ed25519_sign(&secret, b"");
1212        assert_eq!(&signature.0[..], &sig_bytes[..], "Signature mismatch");
1213
1214        assert!(ed25519_verify(&pk, b"", &signature), "Verification failed");
1215    }
1216
1217    /// RFC 8032, Section 7.1 - Test Vector 2 (message = 0x72).
1218    #[test]
1219    fn test_ed25519_vector2() {
1220        let sk_hex = "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb";
1221        let pk_hex = "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c";
1222        let sig_hex = "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da\
1223                        085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00";
1224
1225        let sk_bytes = hex_to_bytes(sk_hex);
1226        let pk_bytes = hex_to_bytes(pk_hex);
1227        let sig_bytes = hex_to_bytes(sig_hex);
1228
1229        let mut sk = [0u8; 32];
1230        sk.copy_from_slice(&sk_bytes);
1231
1232        let (pk, secret) = ed25519_keygen(&sk);
1233        assert_eq!(&pk.0[..], &pk_bytes[..], "Public key mismatch");
1234
1235        let msg = [0x72u8];
1236        let signature = ed25519_sign(&secret, &msg);
1237        assert_eq!(&signature.0[..], &sig_bytes[..], "Signature mismatch");
1238
1239        assert!(ed25519_verify(&pk, &msg, &signature), "Verification failed");
1240    }
1241
1242    // ----- RFC 8032 §7.2 -- Ed25519ctx test vector ("foo" context) ---------
1243    //
1244    //  SECRET KEY: 0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6
1245    //  PUBLIC KEY: dfc9425e4f968f7f0c29f0259cf5f9aed6851c2bb4ad8bfb860cfee0ab248292
1246    //  MESSAGE   : f726936d19c800494e3fdaff20b276a8
1247    //  CONTEXT   : 666f6f                  (= "foo")
1248    //  SIGNATURE : 55a4cc2f70a54e04288c5f4cd1e45a7bb520b36292911876cada7323198dd87a
1249    //              8b36950b95130022907a7fb7c4e9b2d5f6cca685a587b4b21f4b888e4e7edb0d
1250    #[test]
1251    fn rfc8032_section_7_2_ed25519ctx_vector() {
1252        let sk_bytes = hex_to_bytes("0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6");
1253        let pk_bytes = hex_to_bytes("dfc9425e4f968f7f0c29f0259cf5f9aed6851c2bb4ad8bfb860cfee0ab248292");
1254        let msg = hex_to_bytes("f726936d19c800494e3fdaff20b276a8");
1255        let context = hex_to_bytes("666f6f"); // "foo"
1256        let sig_bytes = hex_to_bytes(
1257            "55a4cc2f70a54e04288c5f4cd1e45a7bb520b36292911876cada7323198dd87a\
1258             8b36950b95130022907a7fb7c4e9b2d5f6cca685a587b4b21f4b888e4e7edb0d",
1259        );
1260
1261        let mut sk_arr = [0u8; 32];
1262        sk_arr.copy_from_slice(&sk_bytes);
1263        let (pk, secret) = ed25519_keygen(&sk_arr);
1264        assert_eq!(pk.0.as_slice(), pk_bytes.as_slice(), "pk mismatch");
1265
1266        // Sign and check byte-exact against the pinned signature.
1267        let signature = ed25519ctx_sign(&secret, &msg, &context).expect("ctx_sign");
1268        assert_eq!(
1269            signature.0.as_slice(),
1270            sig_bytes.as_slice(),
1271            "Ed25519ctx signature mismatch"
1272        );
1273
1274        // Verify must accept with the same context.
1275        assert!(ed25519ctx_verify(&pk, &msg, &context, &signature));
1276    }
1277
1278    // ----- RFC 8032 §7.3 -- Ed25519ph test vector ("abc", no context) ------
1279    //
1280    //  SECRET KEY: 833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42
1281    //  PUBLIC KEY: ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf
1282    //  MESSAGE   : 616263                   (= "abc")
1283    //  CONTEXT   : (empty)
1284    //  SIGNATURE : 98a70222f0b8121aa9d30f813d683f809e462b469c7ff87639499bb94e6dae41
1285    //              31f85042463c2a355a2003d062adf5aaa10b8c61e636062aaad11c2a26083406
1286    #[test]
1287    fn rfc8032_section_7_3_ed25519ph_vector() {
1288        let sk_bytes = hex_to_bytes("833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42");
1289        let pk_bytes = hex_to_bytes("ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf");
1290        let msg = hex_to_bytes("616263"); // "abc"
1291        let sig_bytes = hex_to_bytes(
1292            "98a70222f0b8121aa9d30f813d683f809e462b469c7ff87639499bb94e6dae41\
1293             31f85042463c2a355a2003d062adf5aaa10b8c61e636062aaad11c2a26083406",
1294        );
1295
1296        let mut sk_arr = [0u8; 32];
1297        sk_arr.copy_from_slice(&sk_bytes);
1298        let (pk, secret) = ed25519_keygen(&sk_arr);
1299        assert_eq!(pk.0.as_slice(), pk_bytes.as_slice(), "pk mismatch");
1300
1301        // Empty context is allowed in ph mode.
1302        let signature = ed25519ph_sign(&secret, &msg, b"").expect("ph_sign");
1303        assert_eq!(
1304            signature.0.as_slice(),
1305            sig_bytes.as_slice(),
1306            "Ed25519ph signature mismatch"
1307        );
1308
1309        assert!(ed25519ph_verify(&pk, &msg, b"", &signature));
1310    }
1311
1312    /// Pre-hashed entry point: feeding the SHA-512 of `"abc"` to
1313    /// `ed25519ph_sign_prehashed` must produce the same byte-exact
1314    /// signature as `ed25519ph_sign(b"abc", _)`. Same for verify.
1315    #[test]
1316    fn ed25519ph_prehashed_matches_message_form() {
1317        let sk_bytes = hex_to_bytes("833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42");
1318        let mut sk_arr = [0u8; 32];
1319        sk_arr.copy_from_slice(&sk_bytes);
1320        let (pk, secret) = ed25519_keygen(&sk_arr);
1321
1322        let from_msg = ed25519ph_sign(&secret, b"abc", b"").expect("ph_sign");
1323
1324        // Pre-hash with SHA-512 ourselves and feed the digest in.
1325        let digest = Sha512::hash(b"abc");
1326        let from_digest = ed25519ph_sign_prehashed(&secret, &digest, b"").expect("ph_sign_prehashed");
1327
1328        assert_eq!(from_msg.0, from_digest.0);
1329
1330        // Both forms verify equivalently.
1331        assert!(ed25519ph_verify(&pk, b"abc", b"", &from_msg));
1332        assert!(ed25519ph_verify_prehashed(&pk, &digest, b"", &from_msg));
1333    }
1334
1335    // ----- Cross-mode rejection tests --------------------------------------
1336    //
1337    // The dom2 prefix is what makes pure / ctx / ph mutually unforgeable.
1338    // A signature produced under one mode must NOT verify under another --
1339    // and ctx signatures with different `context` values must be mutually
1340    // unforgeable too. These tests catch any regression in the prefix
1341    // handling that would silently let one mode's signature pass another
1342    // mode's verifier.
1343
1344    fn make_test_keys() -> (Ed25519PublicKey, Ed25519SecretKey) {
1345        let sk_bytes = hex_to_bytes("0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6");
1346        let mut sk = [0u8; 32];
1347        sk.copy_from_slice(&sk_bytes);
1348        ed25519_keygen(&sk)
1349    }
1350
1351    #[test]
1352    fn ed25519ctx_requires_nonempty_context() {
1353        let (_pk, sk) = make_test_keys();
1354        // ctx_sign with empty context must return None per RFC 8032 §5.1.
1355        assert!(ed25519ctx_sign(&sk, b"any message", b"").is_none());
1356    }
1357
1358    #[test]
1359    fn ed25519ctx_signature_does_not_verify_as_pure() {
1360        let (pk, sk) = make_test_keys();
1361        let msg = b"shared message";
1362        let sig = ed25519ctx_sign(&sk, msg, b"appA").expect("ctx_sign");
1363        // The same (pk, msg, sig) must be REJECTED by pure verify
1364        // because pure verify doesn't apply the dom2 prefix.
1365        assert!(!ed25519_verify(&pk, msg, &sig));
1366    }
1367
1368    #[test]
1369    fn pure_signature_does_not_verify_as_ctx() {
1370        let (pk, sk) = make_test_keys();
1371        let msg = b"shared message";
1372        let sig = ed25519_sign(&sk, msg);
1373        // Pure signature presented to ctx verify must be rejected
1374        // (ctx verify will hash with the dom2 prefix).
1375        assert!(!ed25519ctx_verify(&pk, msg, b"appA", &sig));
1376    }
1377
1378    #[test]
1379    fn ed25519ctx_different_contexts_dont_cross() {
1380        let (pk, sk) = make_test_keys();
1381        let msg = b"shared message";
1382        let sig_a = ed25519ctx_sign(&sk, msg, b"appA").expect("ctx_sign A");
1383        let sig_b = ed25519ctx_sign(&sk, msg, b"appB").expect("ctx_sign B");
1384
1385        // Each verifies under its own context.
1386        assert!(ed25519ctx_verify(&pk, msg, b"appA", &sig_a));
1387        assert!(ed25519ctx_verify(&pk, msg, b"appB", &sig_b));
1388
1389        // Cross-context must fail.
1390        assert!(!ed25519ctx_verify(&pk, msg, b"appB", &sig_a));
1391        assert!(!ed25519ctx_verify(&pk, msg, b"appA", &sig_b));
1392
1393        // And the two signatures must actually be different (otherwise
1394        // the previous assertion would just be a fluke).
1395        assert_ne!(sig_a.0, sig_b.0);
1396    }
1397
1398    #[test]
1399    fn ed25519ph_signature_does_not_verify_as_pure() {
1400        let (pk, sk) = make_test_keys();
1401        let msg = b"shared message";
1402        let sig = ed25519ph_sign(&sk, msg, b"").expect("ph_sign");
1403        // ph verify pre-hashes the message; pure verify doesn't.
1404        // The same signature presented to pure verify must reject.
1405        assert!(!ed25519_verify(&pk, msg, &sig));
1406    }
1407
1408    #[test]
1409    fn ed25519ph_signature_does_not_verify_as_ctx() {
1410        let (pk, sk) = make_test_keys();
1411        let msg = b"shared message";
1412        let sig = ed25519ph_sign(&sk, msg, b"context").expect("ph_sign");
1413        // ph and ctx use different F bytes (1 vs 0) in the dom2 prefix,
1414        // so even with the same context they don't cross.
1415        assert!(!ed25519ctx_verify(&pk, msg, b"context", &sig));
1416    }
1417
1418    #[test]
1419    fn ed25519ctx_max_context_length() {
1420        let (pk, sk) = make_test_keys();
1421        let msg = b"x";
1422        // 255-byte context is allowed.
1423        let ctx_max = vec![0xa5u8; 255];
1424        let sig = ed25519ctx_sign(&sk, msg, &ctx_max).expect("ctx max len");
1425        assert!(ed25519ctx_verify(&pk, msg, &ctx_max, &sig));
1426
1427        // 256-byte context is NOT allowed (the length octet would
1428        // overflow).
1429        let ctx_too_long = vec![0xa5u8; 256];
1430        assert!(ed25519ctx_sign(&sk, msg, &ctx_too_long).is_none());
1431        assert!(!ed25519ctx_verify(&pk, msg, &ctx_too_long, &sig));
1432    }
1433}