From def345ccf605bb903a2cebaf5ed4971ebe63a0ea Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Wed, 8 Apr 2026 16:41:09 -0600 Subject: [PATCH] Preliminary wNAF support RustCrypto/group#12 included a workaround that allows curves with a big endian `PrimeField::Repr` to be used for wNAF, by defining a separate `PrimeField::to_le_repr` method which is always guaranteed to be little endian. This may not be the permanent solution to this problem which gets upstreamed, but it unblocks work for now. This commit adds the relevant impls of `to_le_repr`, along with initial `WnafGroup` impls to `ProjectivePoint` in `k256` and `primeorder` (currently hardcoded to a fixed constant of `4` for now to unblock additional work). This also adds a `ProjectivePoint::wnaf` static method to obtain a wNAF context for that particular curve group, feature-gated on `alloc`. Finally, it adds a smoke test to `p256` which checks it against our scalar multiplication test vectors, however it does not check any other curves yet and probably should. --- Cargo.lock | 31 +++++++++++-------------------- bignp256/Cargo.toml | 4 ++-- bp256/Cargo.toml | 4 ++-- bp384/Cargo.toml | 4 ++-- ed448-goldilocks/Cargo.toml | 2 +- hash2curve/Cargo.toml | 2 +- k256/Cargo.toml | 2 +- k256/src/arithmetic/field.rs | 7 +++++-- k256/src/arithmetic/projective.rs | 11 ++++++++++- k256/src/arithmetic/scalar.rs | 4 ++++ p192/Cargo.toml | 2 +- p224/Cargo.toml | 2 +- p256/Cargo.toml | 2 +- p256/src/arithmetic/scalar.rs | 4 ++++ p256/tests/projective.rs | 18 ++++++++++++++++++ p384/Cargo.toml | 2 +- p521/Cargo.toml | 2 +- p521/src/arithmetic/field.rs | 7 +++++++ primefield/Cargo.toml | 2 +- primefield/src/macros.rs | 5 +++++ primefield/src/monty.rs | 11 +++++++++++ primeorder/Cargo.toml | 2 +- primeorder/src/projective.rs | 27 ++++++++++++++++++++++++++- sm2/Cargo.toml | 4 ++-- 24 files changed, 119 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ced2b26a1..533511165 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -215,7 +215,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cipher", - "cpufeatures 0.3.0", + "cpufeatures", "rand_core 0.10.0", ] @@ -309,15 +309,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861" -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -516,9 +507,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "elliptic-curve" -version = "0.14.0-rc.29" +version = "0.14.0-rc.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e84043d573efd4ac9d2d125817979a379204bf7e328b25a4a30487e8d100e618" +checksum = "7d7a0bfd012613a7bcfe02cbfccf2b846e9ef9e1bccb641c48d461253cfb034d" dependencies = [ "base16ct", "crypto-bigint", @@ -775,7 +766,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures", ] [[package]] @@ -1210,9 +1201,9 @@ dependencies = [ [[package]] name = "rustcrypto-ff" -version = "0.14.0-rc.0" +version = "0.14.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5db129183b2c139d7d87d08be57cba626c715789db17aec65c8866bfd767d1f" +checksum = "fd2a8adb347447693cd2ba0d218c4b66c62da9b0a5672b17b981e4291ec65ff6" dependencies = [ "bitvec", "rand_core 0.10.0", @@ -1237,9 +1228,9 @@ dependencies = [ [[package]] name = "rustcrypto-group" -version = "0.14.0-rc.0" +version = "0.14.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c4b1463f274a3ff6fb2f44da43e576cb9424367bd96f185ead87b52fe00523" +checksum = "369f9b61aa45933c062c9f6b5c3c50ab710687eca83dd3802653b140b43f85ed" dependencies = [ "rand_core 0.10.0", "rustcrypto-ff", @@ -1381,12 +1372,12 @@ dependencies = [ [[package]] name = "sha2" -version = "0.11.0-rc.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] diff --git a/bignp256/Cargo.toml b/bignp256/Cargo.toml index 2b78e90f0..399417324 100644 --- a/bignp256/Cargo.toml +++ b/bignp256/Cargo.toml @@ -18,7 +18,7 @@ edition = "2024" rust-version = "1.85" [dependencies] -elliptic-curve = { version = "0.14.0-rc.29", features = ["sec1"] } +elliptic-curve = { version = "0.14.0-rc.30", features = ["sec1"] } # optional dependencies belt-block = { version = "0.2.0-rc.3", optional = true } @@ -38,7 +38,7 @@ signature = { version = "3.0.0-rc.10", optional = true } [dev-dependencies] criterion = "0.7" -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["dev"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["dev"] } hex-literal = "1" primeorder = { version = "0.14.0-rc.8", features = ["dev"] } proptest = "1" diff --git a/bp256/Cargo.toml b/bp256/Cargo.toml index 5edf4575a..9b949d76b 100644 --- a/bp256/Cargo.toml +++ b/bp256/Cargo.toml @@ -14,7 +14,7 @@ edition = "2024" rust-version = "1.85" [dependencies] -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["sec1"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["sec1"] } # optional dependencies ecdsa = { version = "0.17.0-rc.16", optional = true, default-features = false, features = ["der"] } @@ -24,7 +24,7 @@ sha2 = { version = "0.11", optional = true, default-features = false } [dev-dependencies] criterion = "0.7" -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["dev"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["dev"] } [features] default = ["pkcs8", "std"] diff --git a/bp384/Cargo.toml b/bp384/Cargo.toml index df5de52c3..6fe5840ac 100644 --- a/bp384/Cargo.toml +++ b/bp384/Cargo.toml @@ -14,7 +14,7 @@ edition = "2024" rust-version = "1.85" [dependencies] -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["sec1"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["sec1"] } # optional dependencies ecdsa = { version = "0.17.0-rc.16", optional = true, default-features = false, features = ["der"] } @@ -24,7 +24,7 @@ sha2 = { version = "0.11", optional = true, default-features = false } [dev-dependencies] criterion = "0.7" -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["dev"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["dev"] } [features] default = ["pkcs8", "std"] diff --git a/ed448-goldilocks/Cargo.toml b/ed448-goldilocks/Cargo.toml index b03d5906e..76a9d6bb5 100644 --- a/ed448-goldilocks/Cargo.toml +++ b/ed448-goldilocks/Cargo.toml @@ -16,7 +16,7 @@ This crate also includes signing and verifying of Ed448 signatures. """ [dependencies] -elliptic-curve = { version = "0.14.0-rc.29", features = ["arithmetic", "pkcs8"] } +elliptic-curve = { version = "0.14.0-rc.30", features = ["arithmetic", "pkcs8"] } hash2curve = "0.14.0-rc.11" rand_core = { version = "0.10", default-features = false } sha3 = { version = "0.11", default-features = false } diff --git a/hash2curve/Cargo.toml b/hash2curve/Cargo.toml index 8c8a73237..cb597e0e9 100644 --- a/hash2curve/Cargo.toml +++ b/hash2curve/Cargo.toml @@ -15,7 +15,7 @@ rust-version = "1.85" [dependencies] digest = { version = "0.11" } -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["arithmetic"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["arithmetic"] } [dev-dependencies] hex-literal = "1" diff --git a/k256/Cargo.toml b/k256/Cargo.toml index 8179581ca..56ad8e98a 100644 --- a/k256/Cargo.toml +++ b/k256/Cargo.toml @@ -20,7 +20,7 @@ rust-version = "1.85" [dependencies] cpubits = "0.1" -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["sec1"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["sec1"] } hash2curve = { version = "0.14.0-rc.11", optional = true } # optional dependencies diff --git a/k256/src/arithmetic/field.rs b/k256/src/arithmetic/field.rs index 0f073527a..e11cf5c17 100644 --- a/k256/src/arithmetic/field.rs +++ b/k256/src/arithmetic/field.rs @@ -28,14 +28,13 @@ use core::{ }; use elliptic_curve::{ Generate, - bigint::{Odd, U256, modular::Retrieve}, + bigint::{ArrayEncoding, Odd, U256, modular::Retrieve}, ff::{self, Field, PrimeField}, ops::Invert, rand_core::{TryCryptoRng, TryRng}, subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}, zeroize::DefaultIsZeroes, }; - #[cfg(test)] use num_bigint::{BigUint, ToBigUint}; @@ -319,6 +318,10 @@ impl PrimeField for FieldElement { self.to_bytes() } + fn to_le_repr(&self) -> Self::Repr { + self.0.normalize().to_u256().to_le_byte_array() + } + fn is_odd(&self) -> Choice { self.is_odd() } diff --git a/k256/src/arithmetic/projective.rs b/k256/src/arithmetic/projective.rs index b58a5eb3d..775367b84 100644 --- a/k256/src/arithmetic/projective.rs +++ b/k256/src/arithmetic/projective.rs @@ -24,7 +24,7 @@ use elliptic_curve::{ }; #[cfg(feature = "alloc")] -use alloc::vec::Vec; +use {alloc::vec::Vec, elliptic_curve::group::WnafGroup}; #[rustfmt::skip] const ENDOMORPHISM_BETA: FieldElement = FieldElement::from_bytes_unchecked(&[ @@ -524,6 +524,15 @@ impl PrimeCurve for ProjectivePoint { impl PrimeGroup for ProjectivePoint {} +#[cfg(feature = "alloc")] +impl WnafGroup for ProjectivePoint { + fn recommended_wnaf_for_num_scalars(_num_scalars: usize) -> usize { + // TODO(tarcieri): provide a way for individual curves to configure this + // Returns a number between 2 and 22, inclusive. + 4 // This seems to be a common starting point? + } +} + // // `core::ops` trait impls // diff --git a/k256/src/arithmetic/scalar.rs b/k256/src/arithmetic/scalar.rs index 1c92f81bf..df5ce6c98 100644 --- a/k256/src/arithmetic/scalar.rs +++ b/k256/src/arithmetic/scalar.rs @@ -310,6 +310,10 @@ impl PrimeField for Scalar { self.to_bytes() } + fn to_le_repr(&self) -> Self::Repr { + self.0.to_le_byte_array() + } + fn is_odd(&self) -> Choice { self.0.is_odd().into() } diff --git a/p192/Cargo.toml b/p192/Cargo.toml index 9b0477b47..86292e2f7 100644 --- a/p192/Cargo.toml +++ b/p192/Cargo.toml @@ -17,7 +17,7 @@ edition = "2024" rust-version = "1.85" [dependencies] -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["sec1"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["sec1"] } # optional dependencies ecdsa-core = { version = "0.17.0-rc.16", package = "ecdsa", optional = true, default-features = false, features = ["der"] } diff --git a/p224/Cargo.toml b/p224/Cargo.toml index da8ae519d..49afc9cd3 100644 --- a/p224/Cargo.toml +++ b/p224/Cargo.toml @@ -17,7 +17,7 @@ edition = "2024" rust-version = "1.85" [dependencies] -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["sec1"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["sec1"] } # optional dependencies ecdsa-core = { version = "0.17.0-rc.16", package = "ecdsa", optional = true, default-features = false, features = ["der"] } diff --git a/p256/Cargo.toml b/p256/Cargo.toml index c2001dfb3..69428ed04 100644 --- a/p256/Cargo.toml +++ b/p256/Cargo.toml @@ -18,7 +18,7 @@ edition = "2024" rust-version = "1.85" [dependencies] -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["sec1"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["sec1"] } # optional dependencies ecdsa-core = { version = "0.17.0-rc.16", package = "ecdsa", optional = true, default-features = false, features = ["der"] } diff --git a/p256/src/arithmetic/scalar.rs b/p256/src/arithmetic/scalar.rs index 8a5965104..01dbf7ca5 100644 --- a/p256/src/arithmetic/scalar.rs +++ b/p256/src/arithmetic/scalar.rs @@ -305,6 +305,10 @@ impl PrimeField for Scalar { self.to_bytes() } + fn to_le_repr(&self) -> FieldBytes { + self.0.to_le_byte_array() + } + fn is_odd(&self) -> Choice { self.0.is_odd().into() } diff --git a/p256/tests/projective.rs b/p256/tests/projective.rs index 5be8291eb..24e1cae16 100644 --- a/p256/tests/projective.rs +++ b/p256/tests/projective.rs @@ -18,6 +18,9 @@ use p256::{ use primeorder::test_projective_arithmetic; use proptest::{prelude::any, prop_compose, proptest}; +#[cfg(feature = "alloc")] +use elliptic_curve::point::AffineCoordinates; + test_projective_arithmetic!( AffinePoint, ProjectivePoint, @@ -32,6 +35,21 @@ fn projective_identity_to_bytes() { assert_eq!([0; 33], ProjectivePoint::IDENTITY.to_bytes().as_slice()); } +#[cfg(feature = "alloc")] +#[test] +fn wnaf() { + for (k, coords) in ADD_TEST_VECTORS.iter().enumerate() { + let scalar = Scalar::from(k as u64 + 1); + let mut wnaf = ProjectivePoint::wnaf(); + let mut wnaf_base = wnaf.base(ProjectivePoint::GENERATOR, 1); + let p = wnaf_base.scalar(&scalar).to_affine(); + + let (x, y) = (p.x(), p.y()); + assert_eq!(x.0, coords.0); + assert_eq!(y.0, coords.1); + } +} + prop_compose! { fn non_identity()(bytes in any::<[u8; 32]>()) -> NonIdentity { NonIdentity::mul_by_generator(&NonZeroScalar::reduce_nonzero(&FieldBytes::from(bytes))) diff --git a/p384/Cargo.toml b/p384/Cargo.toml index 7f2ac9efa..039892cb7 100644 --- a/p384/Cargo.toml +++ b/p384/Cargo.toml @@ -18,7 +18,7 @@ edition = "2024" rust-version = "1.85" [dependencies] -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["sec1"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["sec1"] } # optional dependencies ecdsa-core = { version = "0.17.0-rc.16", package = "ecdsa", optional = true, default-features = false, features = ["der"] } diff --git a/p521/Cargo.toml b/p521/Cargo.toml index 374026df3..6318aecd7 100644 --- a/p521/Cargo.toml +++ b/p521/Cargo.toml @@ -18,7 +18,7 @@ rust-version = "1.85" [dependencies] base16ct = "1" -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["sec1"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["sec1"] } # optional dependencies ecdsa-core = { version = "0.17.0-rc.16", package = "ecdsa", optional = true, default-features = false, features = ["der"] } diff --git a/p521/src/arithmetic/field.rs b/p521/src/arithmetic/field.rs index 47f78a176..3db5ae880 100644 --- a/p521/src/arithmetic/field.rs +++ b/p521/src/arithmetic/field.rs @@ -560,6 +560,13 @@ impl PrimeField for FieldElement { self.to_bytes() } + #[inline] + fn to_le_repr(&self) -> FieldBytes { + let mut ret = [0u8; 66]; + fiat_p521_to_bytes(&mut ret, &self.0); // natively little-endian + ret.into() + } + #[inline] fn is_odd(&self) -> Choice { self.is_odd() diff --git a/primefield/Cargo.toml b/primefield/Cargo.toml index 4f1ebee66..91724170a 100644 --- a/primefield/Cargo.toml +++ b/primefield/Cargo.toml @@ -19,7 +19,7 @@ rust-version = "1.85" [dependencies] bigint = { version = "0.7.1", package = "crypto-bigint", default-features = false, features = ["rand_core", "hybrid-array", "subtle"] } common = { package = "crypto-common", version = "0.2", features = ["rand_core"] } -ff = { version = "0.14.0-rc.0", package = "rustcrypto-ff", default-features = false } +ff = { version = "0.14.0-rc.1", package = "rustcrypto-ff", default-features = false } subtle = { version = "2.6", default-features = false, features = ["const-generics"] } rand_core = { version = "0.10", default-features = false } zeroize = { version = "1.7", default-features = false } diff --git a/primefield/src/macros.rs b/primefield/src/macros.rs index 7677a346c..9b895b62d 100644 --- a/primefield/src/macros.rs +++ b/primefield/src/macros.rs @@ -346,6 +346,11 @@ macro_rules! monty_field_element { self.0.to_repr() } + #[inline] + fn to_le_repr(&self) -> Self::Repr { + self.0.to_le_repr() + } + #[inline] fn is_odd(&self) -> $crate::subtle::Choice { self.0.is_odd() diff --git a/primefield/src/monty.rs b/primefield/src/monty.rs index 6d3b0bb7d..d1c91823c 100644 --- a/primefield/src/monty.rs +++ b/primefield/src/monty.rs @@ -475,6 +475,17 @@ where self.to_bytes() } + fn to_le_repr(&self) -> Self::Repr { + match MOD::BYTE_ORDER { + ByteOrder::BigEndian => { + let mut bytes = self.to_bytes(); + bytes.reverse(); + bytes + } + ByteOrder::LittleEndian => self.to_bytes(), + } + } + fn is_odd(&self) -> Choice { self.is_odd() } diff --git a/primeorder/Cargo.toml b/primeorder/Cargo.toml index f9a04ec0f..f07b5f3a3 100644 --- a/primeorder/Cargo.toml +++ b/primeorder/Cargo.toml @@ -18,7 +18,7 @@ edition = "2024" rust-version = "1.85" [dependencies] -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["arithmetic", "sec1"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["arithmetic", "sec1"] } # optional dependencies serdect = { version = "0.4", optional = true, default-features = false } diff --git a/primeorder/src/projective.rs b/primeorder/src/projective.rs index 2e402b983..0fc940c07 100644 --- a/primeorder/src/projective.rs +++ b/primeorder/src/projective.rs @@ -31,7 +31,10 @@ use elliptic_curve::{ }; #[cfg(feature = "alloc")] -use alloc::vec::Vec; +use { + alloc::vec::Vec, + elliptic_curve::group::{Wnaf, WnafGroup}, +}; #[cfg(feature = "serde")] use serdect::serde::{Deserialize, Serialize, de, ser}; @@ -116,6 +119,15 @@ where lincomb(array::from_mut(&mut k), array::from_mut(&mut pc)) } + + /// Obtain a wNAF context for this group. + #[cfg(feature = "alloc")] + pub fn wnaf() -> Wnaf<(), Vec, Vec> + where + FieldBytes: Copy, + { + Wnaf::new() + } } impl ConditionallySelectable for ProjectivePoint @@ -601,6 +613,19 @@ where } } +#[cfg(feature = "alloc")] +impl WnafGroup for ProjectivePoint +where + C: PrimeCurveParams, + FieldBytes: Copy, +{ + fn recommended_wnaf_for_num_scalars(_num_scalars: usize) -> usize { + // TODO(tarcieri): provide a way for individual curves to configure this + // Returns a number between 2 and 22, inclusive. + 4 // This seems to be a common starting point? + } +} + // // `core::ops` trait impls // diff --git a/sm2/Cargo.toml b/sm2/Cargo.toml index 5faa76254..8cc448c34 100644 --- a/sm2/Cargo.toml +++ b/sm2/Cargo.toml @@ -18,7 +18,7 @@ edition = "2024" rust-version = "1.85" [dependencies] -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["sec1"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["sec1"] } fiat-crypto = { version = "0.3", default-features = false } rand_core = { version = "0.10", default-features = false } @@ -33,7 +33,7 @@ sm3 = { version = "0.5", optional = true, default-features = false } [dev-dependencies] criterion = "0.7" -elliptic-curve = { version = "0.14.0-rc.29", default-features = false, features = ["dev"] } +elliptic-curve = { version = "0.14.0-rc.30", default-features = false, features = ["dev"] } hex-literal = "1" proptest = "1"