Skip to content

Commit 5afc3ab

Browse files
tob-scott-aclaude
andcommitted
Add self-contained wNAF scalar multiplication in primeorder
The upstream `group::Wnaf` has two bugs that break SEC1/NIST curves: 1. `wnaf_form()` assumes `Scalar::to_repr()` returns little-endian bytes, but `ff::PrimeField` documents repr endianness as implementation-specific. All SEC1/NIST curves use big-endian. 2. `wnaf_form()` drops the final carry when the scalar fills all `bit_len` bits. This is masked on BLS12-381 (255-bit modulus in 256-bit repr) but causes wrong results for p256/k256/p384/p521. Rather than depending on an upstream group crate fix, this adds `primeorder::wnaf::WnafScalarMul` — a self-contained wNAF that handles big-endian repr and the carry correctly. The `WnafGroup` impl on `ProjectivePoint` is filled in (was `todo!()`) but warns that `group::Wnaf` itself remains broken for these curves. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 69e0794 commit 5afc3ab

File tree

4 files changed

+242
-9
lines changed

4 files changed

+242
-9
lines changed

p256/tests/projective.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use primeorder::test_projective_arithmetic;
1919
use proptest::{prelude::*, prop_compose, proptest};
2020

2121
#[cfg(feature = "alloc")]
22-
use elliptic_curve::group::Wnaf;
22+
use primeorder::wnaf::WnafScalarMul;
2323

2424
test_projective_arithmetic!(
2525
AffinePoint,
@@ -32,17 +32,14 @@ test_projective_arithmetic!(
3232
#[cfg(feature = "alloc")]
3333
#[test]
3434
fn wnaf() {
35+
let wnaf = WnafScalarMul::new();
3536
for (k, coords) in ADD_TEST_VECTORS.iter().enumerate() {
3637
let scalar = Scalar::from(k as u64 + 1);
3738
dbg!(&scalar, coords);
3839

39-
let mut wnaf = Wnaf::new();
4040
let p = wnaf
41-
.scalar(&scalar)
42-
.base(ProjectivePoint::GENERATOR)
41+
.mul(&scalar, ProjectivePoint::GENERATOR)
4342
.to_affine();
44-
// let mut wnaf_base = wnaf.base(ProjectivePoint::GENERATOR, 1);
45-
// let p = wnaf_base.scalar(&scalar).to_affine();
4643

4744
let (x, _y) = (p.x(), p.y());
4845
assert_eq!(x.0, coords.0);
@@ -82,7 +79,7 @@ proptest! {
8279
scalar in scalar(),
8380
) {
8481
let result = point * scalar;
85-
let wnaf_result = Wnaf::new().scalar(&scalar).base(point);
82+
let wnaf_result = WnafScalarMul::new().mul(&scalar, point);
8683
prop_assert_eq!(result.to_affine(), wnaf_result.to_affine());
8784
}
8885

primeorder/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ extern crate alloc;
1515
#[cfg(feature = "hash2curve")]
1616
pub mod osswu;
1717
pub mod point_arithmetic;
18+
#[cfg(feature = "alloc")]
19+
pub mod wnaf;
1820

1921
mod affine;
2022
#[cfg(feature = "dev")]

primeorder/src/projective.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -604,8 +604,12 @@ where
604604
C: PrimeCurveParams,
605605
FieldBytes<C>: Copy,
606606
{
607-
fn recommended_wnaf_for_num_scalars(num_scalars: usize) -> usize {
608-
todo!()
607+
fn recommended_wnaf_for_num_scalars(_num_scalars: usize) -> usize {
608+
// NOTE: The upstream `group::Wnaf` produces incorrect results
609+
// for curves whose `Scalar::to_repr()` is big-endian (all
610+
// SEC1/NIST curves). Use `primeorder::wnaf::WnafScalarMul`
611+
// instead.
612+
4
609613
}
610614
}
611615

primeorder/src/wnaf.rs

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
//! Variable-time wNAF (windowed Non-Adjacent Form) scalar multiplication.
2+
//!
3+
//! Provides a correct wNAF implementation for curves whose
4+
//! `Scalar::to_repr()` returns big-endian bytes (SEC1/NIST convention).
5+
//!
6+
//! The upstream `group::Wnaf` assumes little-endian repr and silently
7+
//! produces wrong results for big-endian curves. It also drops the
8+
//! final carry in `wnaf_form` when the scalar fills all `bit_len`
9+
//! bits, which is masked on BLS12-381 (255-bit modulus in 256-bit
10+
//! repr) but causes incorrect results on p256/k256/p384/p521.
11+
12+
use alloc::vec::Vec;
13+
use core::iter;
14+
15+
use elliptic_curve::group::ff::PrimeField;
16+
use elliptic_curve::point::Double;
17+
18+
use crate::{PrimeCurveParams, ProjectivePoint};
19+
20+
/// Compute the wNAF lookup table for `base` with the given window
21+
/// size: entries are `[P, 3P, 5P, ..., (2^w - 1)P]`.
22+
fn wnaf_table<C>(
23+
mut base: ProjectivePoint<C>,
24+
window: usize,
25+
) -> Vec<ProjectivePoint<C>>
26+
where
27+
C: PrimeCurveParams,
28+
elliptic_curve::FieldBytes<C>: Copy,
29+
{
30+
let mut table = Vec::with_capacity(1 << (window - 1));
31+
let dbl = Double::double(&base);
32+
for _ in 0..(1 << (window - 1)) {
33+
table.push(base);
34+
base += &dbl;
35+
}
36+
table
37+
}
38+
39+
/// Convert a big-endian scalar repr to wNAF digit form.
40+
fn wnaf_form(scalar_be: &[u8], window: usize) -> Vec<i64> {
41+
debug_assert!(window >= 2);
42+
debug_assert!(window <= 64);
43+
44+
// Reverse BE repr to LE for the bit-scanning loop.
45+
let mut le = scalar_be.to_vec();
46+
le.reverse();
47+
48+
let bit_len = le.len() * 8;
49+
let mut wnaf = Vec::with_capacity(bit_len + 1);
50+
51+
let width = 1u64 << window;
52+
let window_mask = width - 1;
53+
54+
let mut pos = 0;
55+
let mut carry = 0u64;
56+
57+
while pos < bit_len {
58+
let u64_idx = pos / 64;
59+
let bit_idx = pos % 64;
60+
61+
let cur = read_le_u64(&le, u64_idx);
62+
let next = read_le_u64(&le, u64_idx + 1);
63+
let bit_buf = if bit_idx + window < 64 {
64+
cur >> bit_idx
65+
} else {
66+
(cur >> bit_idx) | (next << (64 - bit_idx))
67+
};
68+
69+
let window_val = carry + (bit_buf & window_mask);
70+
71+
if window_val & 1 == 0 {
72+
wnaf.push(0);
73+
pos += 1;
74+
} else if window_val < width / 2 {
75+
carry = 0;
76+
wnaf.push(window_val as i64);
77+
wnaf.extend(iter::repeat(0).take(window - 1));
78+
pos += window;
79+
} else {
80+
carry = 1;
81+
wnaf.push(
82+
(window_val as i64).wrapping_sub(width as i64),
83+
);
84+
wnaf.extend(iter::repeat(0).take(window - 1));
85+
pos += window;
86+
}
87+
}
88+
89+
// Emit remaining carry — needed when the scalar fills all
90+
// `bit_len` bits and the last digit was negative.
91+
if carry != 0 {
92+
wnaf.push(carry as i64);
93+
}
94+
95+
wnaf
96+
}
97+
98+
/// Read a little-endian `u64` limb from a byte slice, zero-extending
99+
/// past the end.
100+
#[inline]
101+
fn read_le_u64(bytes: &[u8], limb_idx: usize) -> u64 {
102+
let start = limb_idx * 8;
103+
if start >= bytes.len() {
104+
return 0;
105+
}
106+
let end = (start + 8).min(bytes.len());
107+
let mut buf = [0u8; 8];
108+
buf[..end - start].copy_from_slice(&bytes[start..end]);
109+
u64::from_le_bytes(buf)
110+
}
111+
112+
/// Evaluate a wNAF digit sequence against a precomputed table.
113+
fn wnaf_exp<C>(
114+
table: &[ProjectivePoint<C>],
115+
wnaf: &[i64],
116+
) -> ProjectivePoint<C>
117+
where
118+
C: PrimeCurveParams,
119+
elliptic_curve::FieldBytes<C>: Copy,
120+
{
121+
use elliptic_curve::group::Group as _;
122+
123+
let mut result = ProjectivePoint::<C>::identity();
124+
let mut found_one = false;
125+
126+
for &n in wnaf.iter().rev() {
127+
if found_one {
128+
result = Double::double(&result);
129+
}
130+
if n != 0 {
131+
found_one = true;
132+
if n > 0 {
133+
result += &table[(n / 2) as usize];
134+
} else {
135+
result -= &table[((-n) / 2) as usize];
136+
}
137+
}
138+
}
139+
140+
result
141+
}
142+
143+
/// Variable-time wNAF scalar multiplication.
144+
///
145+
/// A self-contained replacement for `group::Wnaf` that correctly
146+
/// handles the big-endian scalar representations used by SEC1/NIST
147+
/// curves.
148+
///
149+
/// # Examples
150+
///
151+
/// ```ignore
152+
/// use primeorder::wnaf::WnafScalarMul;
153+
///
154+
/// // Single multiplication
155+
/// let result = WnafScalarMul::new().mul(&scalar, base);
156+
///
157+
/// // One scalar, many bases (precompute wNAF digits once)
158+
/// let ctx = WnafScalarMul::new().with_scalar(&scalar);
159+
/// let results: Vec<_> = bases.iter().map(|b| ctx.mul_base(*b)).collect();
160+
/// ```
161+
pub struct WnafScalarMul {
162+
window: usize,
163+
}
164+
165+
impl Default for WnafScalarMul {
166+
fn default() -> Self {
167+
Self::new()
168+
}
169+
}
170+
171+
impl WnafScalarMul {
172+
/// Create a new context with the default window size (4).
173+
pub fn new() -> Self {
174+
Self { window: 4 }
175+
}
176+
177+
/// Compute `scalar * base` using wNAF multiplication.
178+
pub fn mul<C>(
179+
&self,
180+
scalar: &elliptic_curve::Scalar<C>,
181+
base: ProjectivePoint<C>,
182+
) -> ProjectivePoint<C>
183+
where
184+
C: PrimeCurveParams,
185+
elliptic_curve::FieldBytes<C>: Copy,
186+
{
187+
let repr = scalar.to_repr();
188+
let digits = wnaf_form(repr.as_ref(), self.window);
189+
let table = wnaf_table(base, self.window);
190+
wnaf_exp(&table, &digits)
191+
}
192+
193+
/// Precompute the wNAF form of a scalar for reuse with many
194+
/// bases.
195+
pub fn with_scalar<C>(
196+
&self,
197+
scalar: &elliptic_curve::Scalar<C>,
198+
) -> PreparedScalar
199+
where
200+
C: PrimeCurveParams,
201+
elliptic_curve::FieldBytes<C>: Copy,
202+
{
203+
let repr = scalar.to_repr();
204+
PreparedScalar {
205+
digits: wnaf_form(repr.as_ref(), self.window),
206+
window: self.window,
207+
}
208+
}
209+
}
210+
211+
/// A scalar whose wNAF digit form has been precomputed.
212+
pub struct PreparedScalar {
213+
digits: Vec<i64>,
214+
window: usize,
215+
}
216+
217+
impl PreparedScalar {
218+
/// Multiply this prepared scalar by a base point.
219+
pub fn mul_base<C>(
220+
&self,
221+
base: ProjectivePoint<C>,
222+
) -> ProjectivePoint<C>
223+
where
224+
C: PrimeCurveParams,
225+
elliptic_curve::FieldBytes<C>: Copy,
226+
{
227+
let table = wnaf_table(base, self.window);
228+
wnaf_exp(&table, &self.digits)
229+
}
230+
}

0 commit comments

Comments
 (0)