Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
7 changes: 7 additions & 0 deletions p256/tests/projective.proptest-regressions
Original file line number Diff line number Diff line change
@@ -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)
33 changes: 31 additions & 2 deletions p256/tests/projective.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ use elliptic_curve::{
consts::U32,
group::{GroupEncoding, ff::PrimeField},
ops::{LinearCombination, Reduce, ReduceNonZero},
point::NonIdentity,
point::{AffineCoordinates, NonIdentity},
sec1::{self, ToSec1Point},
};
use p256::{
AffinePoint, FieldBytes, NonZeroScalar, ProjectivePoint, Scalar,
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,
Expand All @@ -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.
Expand All @@ -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(),
Expand Down
2 changes: 2 additions & 0 deletions primeorder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
17 changes: 16 additions & 1 deletion primeorder/src/projective.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -598,6 +598,21 @@ where
}
}

#[cfg(feature = "alloc")]
impl<C> WnafGroup for ProjectivePoint<C>
where
C: PrimeCurveParams,
FieldBytes<C>: 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
//
Expand Down
216 changes: 216 additions & 0 deletions primeorder/src/wnaf.rs
Original file line number Diff line number Diff line change
@@ -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<C>(mut base: ProjectivePoint<C>, window: usize) -> Vec<ProjectivePoint<C>>
where
C: PrimeCurveParams,
elliptic_curve::FieldBytes<C>: 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<i64> {
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<C>(table: &[ProjectivePoint<C>], wnaf: &[i64]) -> ProjectivePoint<C>
where
C: PrimeCurveParams,
elliptic_curve::FieldBytes<C>: Copy,
{
use elliptic_curve::group::Group as _;

let mut result = ProjectivePoint::<C>::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<C>(
&self,
scalar: &elliptic_curve::Scalar<C>,
base: ProjectivePoint<C>,
) -> ProjectivePoint<C>
where
C: PrimeCurveParams,
elliptic_curve::FieldBytes<C>: 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<C>(&self, scalar: &elliptic_curve::Scalar<C>) -> PreparedScalar
where
C: PrimeCurveParams,
elliptic_curve::FieldBytes<C>: 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<i64>,
window: usize,
}

impl PreparedScalar {
/// Multiply this prepared scalar by a base point.
pub fn mul_base<C>(&self, base: ProjectivePoint<C>) -> ProjectivePoint<C>
where
C: PrimeCurveParams,
elliptic_curve::FieldBytes<C>: Copy,
{
let table = wnaf_table(base, self.window);
wnaf_exp(&table, &self.digits)
}
}
Loading