Skip to main content

arcana/ecc/
ed448.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3// Index-based loops in this module deliberately mirror the reference
4// algorithm (parallel-array access `a[i]`/`b[i]`, or a position-indexed
5// packing/reduction state) and keep the constant-time data layout explicit;
6// the `iter().enumerate()` rewrite would obscure it. Documented, single-lint
7// allow per CLAUDE.md §6 (scoped to this module, not a blanket suppression).
8#![allow(clippy::needless_range_loop)]
9
10//! Ed448 (RFC 8032 §5.2) — EdDSA over edwards448 ("Goldilocks").
11//!
12//! Untwisted Edwards curve `x^2 + y^2 = 1 + d·x^2·y^2` with `d = -39081`
13//! over `GF(2^448 - 2^224 - 1)`, cofactor 4, hash SHAKE256. Keys are 57
14//! octets, signatures 114 octets.
15//!
16//! This module reuses the generic constant-time field arithmetic of
17//! [`super::field`] (`FieldElement<7>` over [`super::field::CURVE448_P`],
18//! the same prime X448 uses). Curve constants are taken verbatim from the
19//! RFC 8032 reference code and were cross-checked by running the RFC's own
20//! `[L+1]·B == B` self-test (see the commit message / `tests` below).
21//!
22//! # Side-channel posture
23//!
24//! - The secret scalar multiplication [`Ed448Point::scalar_mul`] is a
25//!   fixed-length MSB→LSB double-and-add: every bit performs one `double`
26//!   plus one `add` and a branchless [`Ed448Point::ct_select`], so the
27//!   sequence of group operations is independent of the scalar. The
28//!   underlying field ops are the CT primitives of [`super::field`]
29//!   (mask-select shielded with `core::hint::black_box`).
30//! - Scalar reduction modulo `L` ([`reduce_mod_l`]) is a fixed-iteration
31//!   bit-serial long division (no secret-dependent branch or bound).
32//! - `decode` operates on public data (public keys, the signature's `R`),
33//!   so its `is_some`/sign branches are not secret-dependent.
34//!
35//! [CROSS-CHECK RECOMMENDED] `ct_select` is an inline mask-select mirroring
36//! the existing `Fe25519::ct_select` in [`super::eddsa`]; a future pass may
37//! route both through `silentops::ct` once it exposes a `[u64; N]` select.
38
39use super::field::{
40    CURVE448_P, FieldElement, field_add, field_inv, field_mul, field_neg, field_sqrt_p3mod4, field_sub,
41};
42
43mod consts;
44use crate::Xof;
45use crate::hash::sha3::Shake256;
46use consts::{ED448_BX_BE, ED448_BY_BE, ED448_L};
47
48/// Field element over `p = 2^448 - 2^224 - 1` (7 × u64 limbs, little-endian).
49type Fe = FieldElement<7>;
50
51const P: [u64; 7] = CURVE448_P;
52
53// --- field helpers (thin wrappers binding the per-call prime P) --------------
54
55#[inline]
56fn fe_zero() -> Fe {
57    Fe::ZERO
58}
59#[inline]
60fn fe_one() -> Fe {
61    Fe::one()
62}
63#[inline]
64fn fe_u64(v: u64) -> Fe {
65    let mut f = Fe::ZERO;
66    f.limbs[0] = v;
67    f
68}
69#[inline]
70fn fe_add(a: &Fe, b: &Fe) -> Fe {
71    field_add(a, b, &P)
72}
73#[inline]
74fn fe_sub(a: &Fe, b: &Fe) -> Fe {
75    field_sub(a, b, &P)
76}
77#[inline]
78fn fe_mul(a: &Fe, b: &Fe) -> Fe {
79    field_mul(a, b, &P)
80}
81#[inline]
82fn fe_sqr(a: &Fe) -> Fe {
83    field_mul(a, a, &P)
84}
85#[inline]
86fn fe_neg(a: &Fe) -> Fe {
87    field_neg(a, &P)
88}
89#[inline]
90fn fe_inv(a: &Fe) -> Fe {
91    field_inv(a, &P)
92}
93
94/// The curve constant `d = -39081 mod p`.
95#[inline]
96fn fe_d() -> Fe {
97    fe_neg(&fe_u64(39081))
98}
99
100/// Constant-time field select: returns `b` if `choice == 1`, else `a`.
101/// `choice` must be 0 or 1.
102#[inline]
103fn fe_ct_select(a: &Fe, b: &Fe, choice: u8) -> Fe {
104    let mask = core::hint::black_box(0u64.wrapping_sub(choice as u64));
105    let inv = !mask;
106    let mut out = Fe::ZERO;
107    for i in 0..7 {
108        out.limbs[i] = (b.limbs[i] & mask) | (a.limbs[i] & inv);
109    }
110    out
111}
112
113/// Returns 1 if the (fully reduced) field element is odd, else 0.
114#[inline]
115fn fe_is_odd(a: &Fe) -> u8 {
116    (a.limbs[0] & 1) as u8
117}
118
119/// Returns `true` if `v < p` (canonical-range check for a decoded `y`).
120fn fe_is_canonical(v: &Fe) -> bool {
121    // v < p  iff  (v - p) borrows.
122    let mut borrow: u64 = 0;
123    for i in 0..7 {
124        let diff = (v.limbs[i] as u128)
125            .wrapping_sub(P[i] as u128)
126            .wrapping_sub(borrow as u128);
127        borrow = ((diff >> 64) as u64) & 1;
128    }
129    borrow == 1
130}
131
132// --- Ed448 point (projective coordinates X:Y:Z, x = X/Z, y = Y/Z) ------------
133
134/// A point on edwards448 in projective coordinates `(X : Y : Z)`.
135///
136/// Affine `(x, y) = (X/Z, Y/Z)`. The neutral element is `(0 : 1 : 1)`.
137/// Addition and doubling use the unified projective formulas from the
138/// RFC 8032 reference code (originally from the EFD), valid for the
139/// untwisted Edwards curve `a = 1, d = -39081`.
140#[derive(Clone)]
141pub struct Ed448Point {
142    x: Fe,
143    y: Fe,
144    z: Fe,
145}
146
147impl Ed448Point {
148    /// The neutral element `(0 : 1 : 1)`.
149    pub fn identity() -> Self {
150        Ed448Point {
151            x: fe_zero(),
152            y: fe_one(),
153            z: fe_one(),
154        }
155    }
156
157    /// The standard base point `B` (RFC 8032 §5.2).
158    pub fn base() -> Self {
159        Ed448Point {
160            x: Fe::from_bytes_be(&ED448_BX_BE),
161            y: Fe::from_bytes_be(&ED448_BY_BE),
162            z: fe_one(),
163        }
164    }
165
166    /// Point addition (unified projective formula, RFC 8032 ref code).
167    pub fn add(&self, q: &Ed448Point) -> Ed448Point {
168        let xcp = fe_mul(&self.x, &q.x);
169        let ycp = fe_mul(&self.y, &q.y);
170        let zcp = fe_mul(&self.z, &q.z);
171        let b = fe_sqr(&zcp);
172        let e = fe_mul(&fe_d(), &fe_mul(&xcp, &ycp));
173        let f = fe_sub(&b, &e);
174        let g = fe_add(&b, &e);
175        // (x1+y1)*(x2+y2) - xcp - ycp
176        let sum = fe_mul(&fe_add(&self.x, &self.y), &fe_add(&q.x, &q.y));
177        let inner = fe_sub(&fe_sub(&sum, &xcp), &ycp);
178        let x3 = fe_mul(&fe_mul(&zcp, &f), &inner);
179        let y3 = fe_mul(&fe_mul(&zcp, &g), &fe_sub(&ycp, &xcp));
180        let z3 = fe_mul(&f, &g);
181        Ed448Point { x: x3, y: y3, z: z3 }
182    }
183
184    /// Point doubling (projective formula, RFC 8032 ref code).
185    pub fn double(&self) -> Ed448Point {
186        let x1s = fe_sqr(&self.x);
187        let y1s = fe_sqr(&self.y);
188        let z1s = fe_sqr(&self.z);
189        let xys = fe_add(&self.x, &self.y);
190        let f = fe_add(&x1s, &y1s);
191        let j = fe_sub(&f, &fe_add(&z1s, &z1s));
192        let x3 = fe_mul(&fe_sub(&fe_sub(&fe_sqr(&xys), &x1s), &y1s), &j);
193        let y3 = fe_mul(&f, &fe_sub(&x1s, &y1s));
194        let z3 = fe_mul(&f, &j);
195        Ed448Point { x: x3, y: y3, z: z3 }
196    }
197
198    /// Branchless point select: returns `b` if `choice == 1`, else `a`.
199    pub fn ct_select(a: &Ed448Point, b: &Ed448Point, choice: u8) -> Ed448Point {
200        Ed448Point {
201            x: fe_ct_select(&a.x, &b.x, choice),
202            y: fe_ct_select(&a.y, &b.y, choice),
203            z: fe_ct_select(&a.z, &b.z, choice),
204        }
205    }
206
207    /// Scalar multiplication `[k]·self`, constant-time in `k`.
208    ///
209    /// `scalar_le` is the little-endian byte encoding of `k`; every bit
210    /// (MSB→LSB) performs one `double` and one masked `add`, so the
211    /// operation sequence is independent of the scalar value.
212    pub fn scalar_mul(&self, scalar_le: &[u8]) -> Ed448Point {
213        let mut r = Ed448Point::identity();
214        let nbits = scalar_le.len() * 8;
215        for i in (0..nbits).rev() {
216            r = r.double();
217            let bit = (scalar_le[i / 8] >> (i % 8)) & 1;
218            let r_plus = r.add(self);
219            r = Ed448Point::ct_select(&r, &r_plus, bit);
220        }
221        r
222    }
223
224    /// Projective equality: `self == q` iff `X1·Z2 == X2·Z1 && Y1·Z2 == Y2·Z1`.
225    pub fn equals(&self, q: &Ed448Point) -> bool {
226        fe_mul(&self.x, &q.z) == fe_mul(&q.x, &self.z) && fe_mul(&self.y, &q.z) == fe_mul(&q.y, &self.z)
227    }
228
229    /// `true` if this is the neutral element `(0 : 1 : 1)` (projectively).
230    pub fn is_identity(&self) -> bool {
231        self.x.is_zero() && self.y == self.z
232    }
233
234    /// Encode to the 57-octet RFC 8032 representation: little-endian `y`
235    /// (448 bits) with the sign bit of `x` in bit 455 (MSB of octet 56).
236    pub fn encode(&self) -> [u8; 57] {
237        let zinv = fe_inv(&self.z);
238        let xp = fe_mul(&self.x, &zinv);
239        let yp = fe_mul(&self.y, &zinv);
240        let yb = yp.to_bytes_le(); // 56 bytes
241        let mut out = [0u8; 57];
242        out[..56].copy_from_slice(&yb);
243        out[56] = fe_is_odd(&xp) << 7;
244        out
245    }
246
247    /// Decode a 57-octet RFC 8032 point encoding. Returns `None` if the
248    /// encoding is non-canonical or `x` cannot be recovered.
249    ///
250    /// Operates on public data (public key / signature `R`).
251    pub fn decode(s: &[u8; 57]) -> Option<Ed448Point> {
252        let sign = (s[56] >> 7) & 1;
253        // bits 448..454 of the encoding must be zero (only bit 455 is the sign).
254        if s[56] & 0x7f != 0 {
255            return None;
256        }
257        let y = Fe::from_bytes_le(&s[..56]);
258        if !fe_is_canonical(&y) {
259            return None;
260        }
261        // x^2 = (y^2 - 1) / (d·y^2 - 1)
262        let y2 = fe_sqr(&y);
263        let num = fe_sub(&y2, &fe_one());
264        let den = fe_sub(&fe_mul(&fe_d(), &y2), &fe_one());
265        let x2 = fe_mul(&num, &fe_inv(&den));
266        // p ≡ 3 (mod 4): candidate root via a^((p+1)/4); verify it squares back.
267        let mut x = field_sqrt_p3mod4(&x2, &P);
268        if fe_sqr(&x) != x2 {
269            return None; // not a quadratic residue → invalid point
270        }
271        // x == 0 with sign bit set is invalid.
272        if x.is_zero() && sign == 1 {
273            return None;
274        }
275        // Flip x to match the requested sign bit.
276        if fe_is_odd(&x) != sign {
277            x = fe_neg(&x);
278        }
279        Some(Ed448Point { x, y, z: fe_one() })
280    }
281}
282
283// --- scalar reduction modulo L ----------------------------------------------
284
285/// The group order `L` as little-endian bytes (56 octets; `L < 2^446`).
286/// Test-only helper: `scalar_mul` takes the raw little-endian scalar, and
287/// the sign/verify paths use `reduce_mod_l` + `ED448_L` directly.
288#[cfg(test)]
289fn l_bytes_le() -> [u8; 56] {
290    let mut out = [0u8; 56];
291    for (i, limb) in ED448_L.iter().enumerate() {
292        out[i * 8..i * 8 + 8].copy_from_slice(&limb.to_le_bytes());
293    }
294    out
295}
296
297/// Reduce an arbitrary little-endian integer modulo `L`, returning the
298/// 57-octet little-endian residue (the high octet is always 0; sized to
299/// match the signature's `S` field). Fixed-iteration bit-serial long
300/// division — constant-time in the number of input bits.
301pub fn reduce_mod_l(input_le: &[u8]) -> [u8; 57] {
302    let l = ED448_L; // [u64; 7], little-endian
303    let mut rem = [0u64; 7];
304
305    let total_bits = input_le.len() * 8;
306    for i in (0..total_bits).rev() {
307        // rem <<= 1
308        let mut carry = 0u64;
309        for limb in rem.iter_mut() {
310            let new_carry = *limb >> 63;
311            *limb = (*limb << 1) | carry;
312            carry = new_carry;
313        }
314        // bring down bit i
315        rem[0] |= ((input_le[i / 8] >> (i % 8)) & 1) as u64;
316
317        // if rem >= L, subtract L (branchless trial-subtract)
318        let mut borrow: u64 = 0;
319        let mut trial = [0u64; 7];
320        for j in 0..7 {
321            let diff = (rem[j] as u128).wrapping_sub(l[j] as u128).wrapping_sub(borrow as u128);
322            trial[j] = diff as u64;
323            borrow = ((diff >> 64) as u64) & 1;
324        }
325        // need_sub = (carry from the shift) | !borrow  (rem >= L)
326        let need_sub = carry | (1u64.wrapping_sub(borrow));
327        let mask = core::hint::black_box(0u64.wrapping_sub(need_sub));
328        let inv = !mask;
329        for j in 0..7 {
330            rem[j] = (trial[j] & mask) | (rem[j] & inv);
331        }
332    }
333
334    let mut out = [0u8; 57];
335    for (i, limb) in rem.iter().enumerate() {
336        out[i * 8..i * 8 + 8].copy_from_slice(&limb.to_le_bytes());
337    }
338    out
339}
340
341// --- hashing helpers (SHAKE256) ---------------------------------------------
342
343/// `SHAKE256(data, 114)` — Ed448 secret-key expansion (no dom4 prefix).
344fn shake256_114(data: &[u8]) -> [u8; 114] {
345    let mut x = Shake256::new();
346    x.update(data);
347    let mut out = [0u8; 114];
348    x.squeeze(&mut out);
349    out
350}
351
352/// `SHAKE256(data, 64)` — the Ed448ph pre-hash `PH(M)` (RFC 8032 §5.2).
353fn shake256_64(data: &[u8]) -> [u8; 64] {
354    let mut x = Shake256::new();
355    x.update(data);
356    let mut out = [0u8; 64];
357    x.squeeze(&mut out);
358    out
359}
360
361/// The Ed448 hash `H = SHAKE256(dom4(hflag, ctx) || parts.., 114)`, where
362/// `dom4(x, y) = "SigEd448" || octet(x) || octet(len(y)) || y` (RFC 8032).
363fn hash_dom4(hflag: u8, ctx: &[u8], parts: &[&[u8]]) -> [u8; 114] {
364    let mut x = Shake256::new();
365    x.update(b"SigEd448");
366    x.update(&[hflag, ctx.len() as u8]);
367    x.update(ctx);
368    for p in parts {
369        x.update(p);
370    }
371    let mut out = [0u8; 114];
372    x.squeeze(&mut out);
373    out
374}
375
376/// Clamp the 57-octet secret scalar per RFC 8032 §5.2.5: clear the two
377/// least-significant bits, set bit 447, clear the whole final octet.
378fn clamp(mut a: [u8; 57]) -> [u8; 57] {
379    a[0] &= 0xFC;
380    a[55] |= 0x80;
381    a[56] = 0;
382    a
383}
384
385/// `true` iff the 57-octet little-endian scalar `s` satisfies `s < L`.
386fn scalar_lt_l(s: &[u8; 57]) -> bool {
387    if s[56] != 0 {
388        return false; // s >= 2^448 > L
389    }
390    let v = Fe::from_bytes_le(&s[..56]);
391    let mut borrow: u64 = 0;
392    for i in 0..7 {
393        let diff = (v.limbs[i] as u128)
394            .wrapping_sub(ED448_L[i] as u128)
395            .wrapping_sub(borrow as u128);
396        borrow = ((diff >> 64) as u64) & 1;
397    }
398    borrow == 1
399}
400
401// --- keys, signatures, and the EdDSA scheme ---------------------------------
402
403/// An Ed448 public key: the 57-octet encoding of `[a]B`.
404#[derive(Clone, Copy, Debug, PartialEq, Eq)]
405pub struct Ed448PublicKey(pub [u8; 57]);
406
407/// An Ed448 secret key (the 57-octet seed).
408#[derive(Clone, Copy)]
409pub struct Ed448SecretKey(pub [u8; 57]);
410
411/// An Ed448 signature: `R || S`, 114 octets.
412#[derive(Clone, Copy, Debug, PartialEq, Eq)]
413pub struct Ed448Signature(pub [u8; 114]);
414
415/// Derive the public key `[a]B` from a 57-octet secret seed (RFC 8032 §5.2).
416pub fn ed448_keygen(secret: &[u8; 57]) -> (Ed448PublicKey, Ed448SecretKey) {
417    let khash = shake256_114(secret);
418    let mut a = [0u8; 57];
419    a.copy_from_slice(&khash[..57]);
420    let a = clamp(a);
421    let pk = Ed448Point::base().scalar_mul(&a).encode();
422    (Ed448PublicKey(pk), Ed448SecretKey(*secret))
423}
424
425/// Core Ed448 signing (RFC 8032 §5.2.6). `hflag` is 0 for Ed448, 1 for
426/// Ed448ph; `ctx` is the context string (caller guarantees `len ≤ 255`).
427///
428/// Deterministic by construction (no per-signature randomness). The secret
429/// scalar `a` and the prefix derive from `SHAKE256(sk)`; all operations on
430/// them are constant-time (see the module-level side-channel posture).
431fn ed448_sign_internal(sk: &Ed448SecretKey, msg: &[u8], ctx: &[u8], hflag: u8) -> [u8; 114] {
432    let khash = shake256_114(&sk.0);
433    let mut a = [0u8; 57];
434    a.copy_from_slice(&khash[..57]);
435    let a = clamp(a);
436    let prefix = &khash[57..]; // 57-octet signing prefix
437
438    let pk = Ed448Point::base().scalar_mul(&a).encode();
439
440    // r = H(dom4 || prefix || M) mod L ; R = [r]B
441    let r = reduce_mod_l(&hash_dom4(hflag, ctx, &[prefix, msg]));
442    let rr = Ed448Point::base().scalar_mul(&r).encode();
443
444    // h = H(dom4 || R || A || M) mod L
445    let h = reduce_mod_l(&hash_dom4(hflag, ctx, &[&rr, &pk, msg]));
446
447    // S = (r + h*a) mod L. Reduce a mod L first: the clamped a (~2^447)
448    // can exceed L, which would break field_mul's input < L^2 assumption.
449    let a_red = reduce_mod_l(&a);
450    let r_fe = Fe::from_bytes_le(&r[..56]);
451    let h_fe = Fe::from_bytes_le(&h[..56]);
452    let a_fe = Fe::from_bytes_le(&a_red[..56]);
453    let s_fe = field_add(&r_fe, &field_mul(&h_fe, &a_fe, &ED448_L), &ED448_L);
454    let s_bytes = s_fe.to_bytes_le(); // 56 octets; S's high octet stays 0
455
456    let mut sig = [0u8; 114];
457    sig[..57].copy_from_slice(&rr);
458    sig[57..113].copy_from_slice(&s_bytes);
459    sig
460}
461
462/// Core Ed448 verification (RFC 8032 §5.2.7), cofactored: checks
463/// `[4]([S]B) == [4](R + [h]A)`. Operates entirely on public data.
464fn ed448_verify_internal(pk: &Ed448PublicKey, msg: &[u8], sig: &[u8; 114], ctx: &[u8], hflag: u8) -> bool {
465    let mut rr = [0u8; 57];
466    rr.copy_from_slice(&sig[..57]);
467    let mut s = [0u8; 57];
468    s.copy_from_slice(&sig[57..]);
469
470    if !scalar_lt_l(&s) {
471        return false;
472    }
473    let r_point = match Ed448Point::decode(&rr) {
474        Some(p) => p,
475        None => return false,
476    };
477    let a_point = match Ed448Point::decode(&pk.0) {
478        Some(p) => p,
479        None => return false,
480    };
481
482    let h = reduce_mod_l(&hash_dom4(hflag, ctx, &[&rr, &pk.0, msg]));
483
484    let lhs = Ed448Point::base().scalar_mul(&s).double().double();
485    let rhs = r_point.add(&a_point.scalar_mul(&h)).double().double();
486    lhs.equals(&rhs)
487}
488
489// --- public API (mirrors the Ed25519 surface) -------------------------------
490
491/// Sign `msg` with Ed448 (pure, empty context).
492pub fn ed448_sign(sk: &Ed448SecretKey, msg: &[u8]) -> Ed448Signature {
493    Ed448Signature(ed448_sign_internal(sk, msg, b"", 0))
494}
495
496/// Verify an Ed448 (pure, empty context) signature.
497pub fn ed448_verify(pk: &Ed448PublicKey, msg: &[u8], sig: &Ed448Signature) -> bool {
498    ed448_verify_internal(pk, msg, &sig.0, b"", 0)
499}
500
501/// Sign `msg` with Ed448 and an explicit context string (`len ≤ 255`).
502pub fn ed448ctx_sign(sk: &Ed448SecretKey, msg: &[u8], context: &[u8]) -> Option<Ed448Signature> {
503    if context.len() > 255 {
504        return None;
505    }
506    Some(Ed448Signature(ed448_sign_internal(sk, msg, context, 0)))
507}
508
509/// Verify an Ed448 signature made with an explicit context string.
510pub fn ed448ctx_verify(pk: &Ed448PublicKey, msg: &[u8], context: &[u8], sig: &Ed448Signature) -> bool {
511    if context.len() > 255 {
512        return false;
513    }
514    ed448_verify_internal(pk, msg, &sig.0, context, 0)
515}
516
517/// Sign `msg` with Ed448ph: signs `SHAKE256(msg, 64)` with `hflag = 1`.
518pub fn ed448ph_sign(sk: &Ed448SecretKey, msg: &[u8], context: &[u8]) -> Option<Ed448Signature> {
519    if context.len() > 255 {
520        return None;
521    }
522    let ph = shake256_64(msg);
523    Some(Ed448Signature(ed448_sign_internal(sk, &ph, context, 1)))
524}
525
526/// Verify an Ed448ph signature over `msg` (re-hashes `msg` with SHAKE256).
527pub fn ed448ph_verify(pk: &Ed448PublicKey, msg: &[u8], context: &[u8], sig: &Ed448Signature) -> bool {
528    if context.len() > 255 {
529        return false;
530    }
531    let ph = shake256_64(msg);
532    ed448_verify_internal(pk, &ph, &sig.0, context, 1)
533}
534
535/// Sign an already-computed 64-octet pre-hash with Ed448ph.
536pub fn ed448ph_sign_prehashed(sk: &Ed448SecretKey, prehashed: &[u8], context: &[u8]) -> Option<Ed448Signature> {
537    if prehashed.len() != 64 || context.len() > 255 {
538        return None;
539    }
540    Some(Ed448Signature(ed448_sign_internal(sk, prehashed, context, 1)))
541}
542
543/// Verify an Ed448ph signature against an already-computed 64-octet pre-hash.
544pub fn ed448ph_verify_prehashed(pk: &Ed448PublicKey, prehashed: &[u8], context: &[u8], sig: &Ed448Signature) -> bool {
545    if prehashed.len() != 64 || context.len() > 255 {
546        return false;
547    }
548    ed448_verify_internal(pk, prehashed, &sig.0, context, 1)
549}
550
551#[cfg(test)]
552mod tests {
553    use super::*;
554
555    fn hex_to_vec(h: &str) -> Vec<u8> {
556        (0..h.len())
557            .step_by(2)
558            .map(|i| u8::from_str_radix(&h[i..i + 2], 16).unwrap())
559            .collect()
560    }
561    fn hex57(h: &str) -> [u8; 57] {
562        let mut a = [0u8; 57];
563        a.copy_from_slice(&hex_to_vec(h));
564        a
565    }
566    fn hex114(h: &str) -> [u8; 114] {
567        let mut a = [0u8; 114];
568        a.copy_from_slice(&hex_to_vec(h));
569        a
570    }
571
572    /// RFC 8032 §7.4 "Blank" vector: Ed448 pure, empty message + context.
573    /// Validates keygen, deterministic sign (byte-exact), and verify.
574    #[test]
575    fn rfc8032_blank_vector() {
576        let sk = hex57(
577            "6c82a562cb808d10d632be89c8513ebf6c929f34ddfa8c9f63c9960ef6e348a3\
578             528c8a3fcc2f044e39a3fc5b94492f8f032e7549a20098f95b",
579        );
580        let pk_expected = hex57(
581            "5fd7449b59b461fd2ce787ec616ad46a1da1342485a70e1f8a0ea75d80e96778\
582             edf124769b46c7061bd6783df1e50f6cd1fa1abeafe8256180",
583        );
584        let sig_expected = hex114(
585            "533a37f6bbe457251f023c0d88f976ae2dfb504a843e34d2074fd823d41a591f\
586             2b233f034f628281f2fd7a22ddd47d7828c59bd0a21bfd3980ff0d2028d4b18a\
587             9df63e006c5d1c2d345b925d8dc00b4104852db99ac5c7cdda8530a113a0f4db\
588             b61149f05a7363268c71d95808ff2e652600",
589        );
590        let (pk, sk2) = ed448_keygen(&sk);
591        assert_eq!(pk.0, pk_expected, "keygen public key mismatch");
592        let sig = ed448_sign(&sk2, b"");
593        assert_eq!(sig.0, sig_expected, "signature mismatch (not RFC-conformant)");
594        assert!(ed448_verify(&pk, b"", &sig), "verify rejected a valid signature");
595        let mut bad = sig;
596        bad.0[0] ^= 1;
597        assert!(!ed448_verify(&pk, b"", &bad), "verify accepted a tampered signature");
598    }
599
600    /// RFC 8032 §7.4 "1 octet (with context)" vector: Ed448 + context "foo".
601    #[test]
602    fn rfc8032_ctx_vector() {
603        let sk = hex57(
604            "c4eab05d357007c632f3dbb48489924d552b08fe0c353a0d4a1f00acda2c463a\
605             fbea67c5e8d2877c5e3bc397a659949ef8021e954e0a12274e",
606        );
607        let pk_exp = hex57(
608            "43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c6798c086\
609             6aea01eb00742802b8438ea4cb82169c235160627b4c3a9480",
610        );
611        let sig_exp = hex114(
612            "d4f8f6131770dd46f40867d6fd5d5055de43541f8c5e35abbcd001b32a89f7d2\
613             151f7647f11d8ca2ae279fb842d607217fce6e042f6815ea000c85741de5c8da\
614             1144a6a1aba7f96de42505d7a7298524fda538fccbbb754f578c1cad10d54d0d\
615             5428407e85dcbc98a49155c13764e66c3c00",
616        );
617        let msg = [0x03u8];
618        let ctx = [0x66u8, 0x6f, 0x6f]; // "foo"
619        let (pk, sk2) = ed448_keygen(&sk);
620        assert_eq!(pk.0, pk_exp);
621        let sig = ed448ctx_sign(&sk2, &msg, &ctx).expect("ctx <= 255");
622        assert_eq!(sig.0, sig_exp, "ctx signature not RFC-conformant");
623        assert!(ed448ctx_verify(&pk, &msg, &ctx, &sig));
624        // wrong context must fail
625        assert!(!ed448ctx_verify(&pk, &msg, b"bar", &sig));
626    }
627
628    /// RFC 8032 §7.5 "TEST abc" vector: Ed448ph (pre-hash), empty context.
629    #[test]
630    fn rfc8032_ph_vector() {
631        let sk = hex57(
632            "833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42\
633             ef7822e0d5104127dc05d6dbefde69e3ab2cec7c867c6e2c49",
634        );
635        let pk_exp = hex57(
636            "259b71c19f83ef77a7abd26524cbdb3161b590a48f7d17de3ee0ba9c52beb743\
637             c09428a131d6b1b57303d90d8132c276d5ed3d5d01c0f53880",
638        );
639        let sig_exp = hex114(
640            "822f6901f7480f3d5f562c592994d9693602875614483256505600bbc281ae38\
641             1f54d6bce2ea911574932f52a4e6cadd78769375ec3ffd1b801a0d9b3f4030cd\
642             433964b6457ea39476511214f97469b57dd32dbc560a9a94d00bff07620464a3\
643             ad203df7dc7ce360c3cd3696d9d9fab90f00",
644        );
645        let msg = [0x61u8, 0x62, 0x63]; // "abc"
646        let (pk, sk2) = ed448_keygen(&sk);
647        assert_eq!(pk.0, pk_exp);
648        let sig = ed448ph_sign(&sk2, &msg, b"").expect("ctx <= 255");
649        assert_eq!(sig.0, sig_exp, "ph signature not RFC-conformant");
650        assert!(ed448ph_verify(&pk, &msg, b"", &sig));
651        // the prehashed entry point must agree with the all-in-one path
652        let ph = shake256_64(&msg);
653        let sig2 = ed448ph_sign_prehashed(&sk2, &ph, b"").expect("64-byte prehash");
654        assert_eq!(sig2.0, sig.0, "prehashed path disagrees with ed448ph_sign");
655    }
656
657    #[test]
658    fn base_point_on_curve() {
659        // x^2 + y^2 == 1 + d·x^2·y^2  (affine, z = 1)
660        let b = Ed448Point::base();
661        let x2 = fe_sqr(&b.x);
662        let y2 = fe_sqr(&b.y);
663        let lhs = fe_add(&x2, &y2);
664        let rhs = fe_add(&fe_one(), &fe_mul(&fe_d(), &fe_mul(&x2, &y2)));
665        assert_eq!(lhs, rhs, "base point not on edwards448");
666    }
667
668    #[test]
669    fn order_of_base_point() {
670        // [L]·B must be the neutral element.
671        let lb = Ed448Point::base().scalar_mul(&l_bytes_le());
672        assert!(lb.is_identity(), "[L]·B != identity (wrong base point or order)");
673    }
674
675    #[test]
676    fn encode_decode_roundtrip_base() {
677        let b = Ed448Point::base();
678        let enc = b.encode();
679        let dec = Ed448Point::decode(&enc).expect("base point must decode");
680        assert!(b.equals(&dec), "decode(encode(B)) != B");
681        // re-encode must be byte-identical (canonical).
682        assert_eq!(dec.encode(), enc);
683    }
684
685    #[test]
686    fn scalar_mul_distributive_small() {
687        // [3]B == B + B + B
688        let b = Ed448Point::base();
689        let three = {
690            let mut s = [0u8; 56];
691            s[0] = 3;
692            s
693        };
694        let lhs = b.scalar_mul(&three);
695        let rhs = b.add(&b).add(&b);
696        assert!(lhs.equals(&rhs), "[3]B != B+B+B");
697    }
698
699    #[test]
700    fn reduce_mod_l_known() {
701        // L reduces to 0; (L-1) reduces to itself; (L+1) reduces to 1.
702        let l = l_bytes_le();
703        let red_l = reduce_mod_l(&l);
704        assert!(red_l.iter().all(|&b| b == 0), "L mod L != 0");
705
706        let mut lp1 = l;
707        // L's low byte is 0xf3; +1 won't carry past it.
708        lp1[0] = lp1[0].wrapping_add(1);
709        let red = reduce_mod_l(&lp1);
710        assert_eq!(red[0], 1);
711        assert!(red[1..].iter().all(|&b| b == 0), "(L+1) mod L != 1");
712    }
713}