Skip to content

Commit 92dec5a

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 92dec5a

4 files changed

Lines changed: 228 additions & 11 deletions

File tree

p256/tests/projective.rs

Lines changed: 4 additions & 9 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,12 @@ 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();
40-
let p = wnaf
41-
.scalar(&scalar)
42-
.base(ProjectivePoint::GENERATOR)
43-
.to_affine();
44-
// let mut wnaf_base = wnaf.base(ProjectivePoint::GENERATOR, 1);
45-
// let p = wnaf_base.scalar(&scalar).to_affine();
40+
let p = wnaf.mul(&scalar, ProjectivePoint::GENERATOR).to_affine();
4641

4742
let (x, _y) = (p.x(), p.y());
4843
assert_eq!(x.0, coords.0);
@@ -82,7 +77,7 @@ proptest! {
8277
scalar in scalar(),
8378
) {
8479
let result = point * scalar;
85-
let wnaf_result = Wnaf::new().scalar(&scalar).base(point);
80+
let wnaf_result = WnafScalarMul::new().mul(&scalar, point);
8681
prop_assert_eq!(result.to_affine(), wnaf_result.to_affine());
8782
}
8883

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

0 commit comments

Comments
 (0)