diff --git a/Cargo.toml b/Cargo.toml index 879ffdb4d..f654ee68a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,5 @@ ed448-goldilocks = { path = "ed448-goldilocks" } hash2curve = { path = "hash2curve" } primefield = { path = "primefield" } primeorder = { path = "primeorder" } + +#rustcrypto-group = { git = "https://github.com/RustCrypto/group", branch = "dense-wnaf" } diff --git a/p256/tests/projective.proptest-regressions b/p256/tests/projective.proptest-regressions new file mode 100644 index 000000000..b332dce68 --- /dev/null +++ b/p256/tests/projective.proptest-regressions @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc e19ee42c127b7289fbe7e42df47abf141eb644afcbd13ac141e39b9960362174 # shrinks to point = ProjectivePoint { x: FieldElement(0x823CD15F6DD3C71933565064513A6B2BD183E554C6A08622F713EBBBFACE98BE), y: FieldElement(0x55DF5D5850F47BAD82149139979369FE498A9022A412B5E0BEDD2CFC21C3ED91), z: FieldElement(0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5) }, scalar = Scalar(0x0000000000000000000000000000000000000000000000000000000000000001) diff --git a/p256/tests/projective.rs b/p256/tests/projective.rs index 5be8291eb..b2226fa3e 100644 --- a/p256/tests/projective.rs +++ b/p256/tests/projective.rs @@ -8,7 +8,7 @@ use elliptic_curve::{ consts::U32, group::{GroupEncoding, ff::PrimeField}, ops::{LinearCombination, Reduce, ReduceNonZero}, - point::NonIdentity, + point::{AffineCoordinates, NonIdentity}, sec1::{self, ToSec1Point}, }; use p256::{ @@ -16,7 +16,10 @@ use p256::{ test_vectors::group::{ADD_TEST_VECTORS, MUL_TEST_VECTORS}, }; use primeorder::test_projective_arithmetic; -use proptest::{prelude::any, prop_compose, proptest}; +use proptest::{prelude::*, prop_compose, proptest}; + +#[cfg(feature = "alloc")] +use primeorder::wnaf::WnafScalarMul; test_projective_arithmetic!( AffinePoint, @@ -26,6 +29,21 @@ test_projective_arithmetic!( MUL_TEST_VECTORS ); +#[cfg(feature = "alloc")] +#[test] +fn wnaf() { + let wnaf = WnafScalarMul::new(); + for (k, coords) in ADD_TEST_VECTORS.iter().enumerate() { + let scalar = Scalar::from(k as u64 + 1); + dbg!(&scalar, coords); + + let p = wnaf.mul(&scalar, ProjectivePoint::GENERATOR).to_affine(); + + let (x, _y) = (p.x(), p.y()); + assert_eq!(x.0, coords.0); + } +} + #[test] fn projective_identity_to_bytes() { // This is technically an invalid SEC1 encoding, but is preferable to panicking. @@ -52,6 +70,17 @@ prop_compose! { // TODO: move to `primeorder::test_projective_arithmetic`. proptest! { + #[cfg(feature = "alloc")] + #[test] + fn wnaf_proptest( + point in projective(), + scalar in scalar(), + ) { + let result = point * scalar; + let wnaf_result = WnafScalarMul::new().mul(&scalar, point); + prop_assert_eq!(result.to_affine(), wnaf_result.to_affine()); + } + #[test] fn batch_normalize( a in non_identity(), diff --git a/primeorder/src/lib.rs b/primeorder/src/lib.rs index ee296cfc3..b4e51912a 100644 --- a/primeorder/src/lib.rs +++ b/primeorder/src/lib.rs @@ -15,6 +15,8 @@ extern crate alloc; #[cfg(feature = "hash2curve")] pub mod osswu; pub mod point_arithmetic; +#[cfg(feature = "alloc")] +pub mod wnaf; mod affine; #[cfg(feature = "dev")] diff --git a/primeorder/src/projective.rs b/primeorder/src/projective.rs index b94eb6d3b..a1c95466a 100644 --- a/primeorder/src/projective.rs +++ b/primeorder/src/projective.rs @@ -31,7 +31,7 @@ use elliptic_curve::{ }; #[cfg(feature = "alloc")] -use alloc::vec::Vec; +use {alloc::vec::Vec, elliptic_curve::group::WnafGroup}; #[cfg(feature = "serde")] use serdect::serde::{Deserialize, Serialize, de, ser}; @@ -598,6 +598,21 @@ where } } +#[cfg(feature = "alloc")] +impl WnafGroup for ProjectivePoint +where + C: PrimeCurveParams, + FieldBytes: Copy, +{ + fn recommended_wnaf_for_num_scalars(_num_scalars: usize) -> usize { + // NOTE: The upstream `group::Wnaf` produces incorrect results + // for curves whose `Scalar::to_repr()` is big-endian (all + // SEC1/NIST curves). Use `primeorder::wnaf::WnafScalarMul` + // instead. + 4 + } +} + // // `core::ops` trait impls // diff --git a/primeorder/src/wnaf.rs b/primeorder/src/wnaf.rs new file mode 100644 index 000000000..5b4a195df --- /dev/null +++ b/primeorder/src/wnaf.rs @@ -0,0 +1,216 @@ +//! Variable-time wNAF (windowed Non-Adjacent Form) scalar multiplication. +//! +//! Provides a correct wNAF implementation for curves whose +//! `Scalar::to_repr()` returns big-endian bytes (SEC1/NIST convention). +//! +//! The upstream `group::Wnaf` assumes little-endian repr and silently +//! produces wrong results for big-endian curves. It also drops the +//! final carry in `wnaf_form` when the scalar fills all `bit_len` +//! bits, which is masked on BLS12-381 (255-bit modulus in 256-bit +//! repr) but causes incorrect results on p256/k256/p384/p521. + +use alloc::vec::Vec; +use core::iter; + +use elliptic_curve::group::ff::PrimeField; +use elliptic_curve::point::Double; + +use crate::{PrimeCurveParams, ProjectivePoint}; + +/// Compute the wNAF lookup table for `base` with the given window +/// size: entries are `[P, 3P, 5P, ..., (2^w - 1)P]`. +fn wnaf_table(mut base: ProjectivePoint, window: usize) -> Vec> +where + C: PrimeCurveParams, + elliptic_curve::FieldBytes: Copy, +{ + let mut table = Vec::with_capacity(1 << (window - 1)); + let dbl = Double::double(&base); + for _ in 0..(1 << (window - 1)) { + table.push(base); + base += &dbl; + } + table +} + +/// Convert a big-endian scalar repr to wNAF digit form. +fn wnaf_form(scalar_be: &[u8], window: usize) -> Vec { + debug_assert!(window >= 2); + debug_assert!(window <= 64); + + // Reverse BE repr to LE for the bit-scanning loop. + let mut le = scalar_be.to_vec(); + le.reverse(); + + let bit_len = le.len() * 8; + let mut wnaf = Vec::with_capacity(bit_len + 1); + + let width = 1u64 << window; + let window_mask = width - 1; + + let mut pos = 0; + let mut carry = 0u64; + + while pos < bit_len { + let u64_idx = pos / 64; + let bit_idx = pos % 64; + + let cur = read_le_u64(&le, u64_idx); + let next = read_le_u64(&le, u64_idx + 1); + let bit_buf = if bit_idx + window < 64 { + cur >> bit_idx + } else { + (cur >> bit_idx) | (next << (64 - bit_idx)) + }; + + let window_val = carry + (bit_buf & window_mask); + + if window_val & 1 == 0 { + wnaf.push(0); + pos += 1; + } else if window_val < width / 2 { + carry = 0; + wnaf.push(window_val as i64); + wnaf.extend(iter::repeat_n(0, window - 1)); + pos += window; + } else { + carry = 1; + wnaf.push((window_val as i64).wrapping_sub(width as i64)); + wnaf.extend(iter::repeat_n(0, window - 1)); + pos += window; + } + } + + // Emit remaining carry — needed when the scalar fills all + // `bit_len` bits and the last digit was negative. + if carry != 0 { + wnaf.push(carry as i64); + } + + wnaf +} + +/// Read a little-endian `u64` limb from a byte slice, zero-extending +/// past the end. +#[inline] +fn read_le_u64(bytes: &[u8], limb_idx: usize) -> u64 { + let start = limb_idx * 8; + if start >= bytes.len() { + return 0; + } + let end = (start + 8).min(bytes.len()); + let mut buf = [0u8; 8]; + buf[..end - start].copy_from_slice(&bytes[start..end]); + u64::from_le_bytes(buf) +} + +/// Evaluate a wNAF digit sequence against a precomputed table. +fn wnaf_exp(table: &[ProjectivePoint], wnaf: &[i64]) -> ProjectivePoint +where + C: PrimeCurveParams, + elliptic_curve::FieldBytes: Copy, +{ + use elliptic_curve::group::Group as _; + + let mut result = ProjectivePoint::::identity(); + let mut found_one = false; + + for &n in wnaf.iter().rev() { + if found_one { + result = Double::double(&result); + } + if n != 0 { + found_one = true; + if n > 0 { + result += &table[(n / 2) as usize]; + } else { + result -= &table[((-n) / 2) as usize]; + } + } + } + + result +} + +/// Variable-time wNAF scalar multiplication. +/// +/// A self-contained replacement for `group::Wnaf` that correctly +/// handles the big-endian scalar representations used by SEC1/NIST +/// curves. +/// +/// # Examples +/// +/// ```ignore +/// use primeorder::wnaf::WnafScalarMul; +/// +/// // Single multiplication +/// let result = WnafScalarMul::new().mul(&scalar, base); +/// +/// // One scalar, many bases (precompute wNAF digits once) +/// let ctx = WnafScalarMul::new().with_scalar(&scalar); +/// let results: Vec<_> = bases.iter().map(|b| ctx.mul_base(*b)).collect(); +/// ``` +pub struct WnafScalarMul { + window: usize, +} + +impl Default for WnafScalarMul { + fn default() -> Self { + Self::new() + } +} + +impl WnafScalarMul { + /// Create a new context with the default window size (4). + pub fn new() -> Self { + Self { window: 4 } + } + + /// Compute `scalar * base` using wNAF multiplication. + pub fn mul( + &self, + scalar: &elliptic_curve::Scalar, + base: ProjectivePoint, + ) -> ProjectivePoint + where + C: PrimeCurveParams, + elliptic_curve::FieldBytes: Copy, + { + let repr = scalar.to_repr(); + let digits = wnaf_form(repr.as_ref(), self.window); + let table = wnaf_table(base, self.window); + wnaf_exp(&table, &digits) + } + + /// Precompute the wNAF form of a scalar for reuse with many + /// bases. + pub fn with_scalar(&self, scalar: &elliptic_curve::Scalar) -> PreparedScalar + where + C: PrimeCurveParams, + elliptic_curve::FieldBytes: Copy, + { + let repr = scalar.to_repr(); + PreparedScalar { + digits: wnaf_form(repr.as_ref(), self.window), + window: self.window, + } + } +} + +/// A scalar whose wNAF digit form has been precomputed. +pub struct PreparedScalar { + digits: Vec, + window: usize, +} + +impl PreparedScalar { + /// Multiply this prepared scalar by a base point. + pub fn mul_base(&self, base: ProjectivePoint) -> ProjectivePoint + where + C: PrimeCurveParams, + elliptic_curve::FieldBytes: Copy, + { + let table = wnaf_table(base, self.window); + wnaf_exp(&table, &self.digits) + } +}