From 37a4fdb8b86e00d8944004efb0cec473ffb762d9 Mon Sep 17 00:00:00 2001 From: Shridhar Panigrahi Date: Wed, 11 Mar 2026 04:32:52 +0530 Subject: [PATCH 1/5] Fix hardcoded scalar size in Straus NAF computation The `add_scalar` method in `NafMatrix` hardcodes `x_u64[0..4]` when reading scalar bytes, which only works for 32-byte scalars. This panics for curves with different scalar sizes like P-384 or P-521. Use the actual scalar byte length instead. Signed-off-by: Shridhar Panigrahi --- generic-ec/src/multiscalar/straus.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generic-ec/src/multiscalar/straus.rs b/generic-ec/src/multiscalar/straus.rs index 569322f..381340d 100644 --- a/generic-ec/src/multiscalar/straus.rs +++ b/generic-ec/src/multiscalar/straus.rs @@ -186,7 +186,7 @@ impl NafMatrix { fn add_scalar(&mut self, scalar: &Scalar) { let scalar_bytes = scalar.to_le_bytes(); let mut x_u64 = vec![0u64; scalar_bytes.len() / 8 + 1]; - read_le_u64_into(&scalar_bytes, &mut x_u64[0..4]); + read_le_u64_into(&scalar_bytes, &mut x_u64[0..scalar_bytes.len() / 8]); let offset = self.matrix.len(); debug_assert!( From 40356da7b6e13ee38ce31ec70057881cb42a1fe5 Mon Sep 17 00:00:00 2001 From: Shridhar Panigrahi Date: Thu, 12 Mar 2026 15:17:00 +0530 Subject: [PATCH 2/5] Remove deprecated `doc_auto_cfg` feature The `doc_auto_cfg` feature was merged into `doc_cfg` in Rust 1.92.0 (see rust-lang/rust#138907), causing nightly rustdoc builds to fail with E0557. Remove it from the feature list. Signed-off-by: Shridhar Panigrahi --- generic-ec-curves/src/lib.rs | 2 +- generic-ec/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/generic-ec-curves/src/lib.rs b/generic-ec-curves/src/lib.rs index e8beb40..a141b06 100644 --- a/generic-ec-curves/src/lib.rs +++ b/generic-ec-curves/src/lib.rs @@ -6,7 +6,7 @@ //! [`generic-ec` crate]: https://docs.rs/generic-ec #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))] -#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #![forbid(missing_docs)] #![no_std] diff --git a/generic-ec/src/lib.rs b/generic-ec/src/lib.rs index 4a9b7e4..6700d14 100644 --- a/generic-ec/src/lib.rs +++ b/generic-ec/src/lib.rs @@ -184,7 +184,7 @@ #![cfg_attr(not(test), forbid(unused_crate_dependencies))] #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))] #![no_std] -#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #[cfg(feature = "std")] extern crate std; From e0cd78c0f3b59ae2b22fc5fec12ee22dd24df8be Mon Sep 17 00:00:00 2001 From: Shridhar Panigrahi Date: Fri, 13 Mar 2026 00:03:58 +0530 Subject: [PATCH 3/5] Add secp384r1 (NIST P-384) curve support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the first curve with a larger scalar size (48 bytes instead of 32) to be added to the library. It exercises the full scalar pipeline with a non-256-bit field and validates that no hardcoded scalar-size assumptions remain. What's included: - p384 crate dependency and `secp384r1` feature flag wired through generic-ec-curves and generic-ec - CurveName, FromUniformBytes (L=64 per RFC 9380), BytesModOrder, Reduce<48>, and NoInvalidPoints implementations - New scalar_from_{be,le}_bytes_mod_order_reducing_48 utility functions for Horner-method reduction using 48-byte chunks - All existing tests instantiated for Secp384r1: scalar ops, point ops, serde, multiscalar (Straus), coordinates, dlog_eq ZKPs, Reduce<48> - Benchmarks updated for the new curve All 180 tests pass. Zero breakage — the dynamic scalar-size handling (including the Straus NAF fix from #57) generalizes cleanly to 384-bit scalars. Relates to #58 Signed-off-by: Shridhar Panigrahi --- Cargo.lock | 11 ++ generic-ec-curves/Cargo.toml | 2 + generic-ec-curves/benches/curves.rs | 3 + generic-ec-curves/src/lib.rs | 3 + .../src/rust_crypto/curve_name.rs | 5 + generic-ec-curves/src/rust_crypto/mod.rs | 19 ++- generic-ec-curves/src/rust_crypto/scalar.rs | 38 +++++- generic-ec-curves/src/utils.rs | 108 ++++++++++++++++++ generic-ec/Cargo.toml | 3 +- generic-ec/src/lib.rs | 7 ++ generic-ec/src/multiscalar/straus.rs | 2 + tests/benches/multiscalar.rs | 1 + tests/benches/random.rs | 1 + tests/tests/curves.rs | 11 +- tests/tests/dlog_eq.rs | 4 + tests/tests/multiscalar.rs | 4 +- tests/tests/serde.rs | 3 + 17 files changed, 219 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ed3298..be9cc17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -737,6 +737,7 @@ dependencies = [ "group", "k256", "p256", + "p384", "rand", "rand_core", "rand_dev", @@ -1086,6 +1087,16 @@ dependencies = [ "primeorder", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "elliptic-curve", + "primeorder", +] + [[package]] name = "papergrid" version = "0.11.0" diff --git a/generic-ec-curves/Cargo.toml b/generic-ec-curves/Cargo.toml index b3b1c1a..f6f1160 100644 --- a/generic-ec-curves/Cargo.toml +++ b/generic-ec-curves/Cargo.toml @@ -18,6 +18,7 @@ zeroize = { workspace = true, features = ["zeroize_derive"] } elliptic-curve = { version = "0.13", default-features = false, features = ["sec1", "hash2curve"], optional = true } k256 = { version = "0.13", optional = true, default-features = false, features = ["hash2curve"] } p256 = { version = "0.13", optional = true, default-features = false, features = ["hash2curve"] } +p384 = { version = "0.13", optional = true, default-features = false, features = ["hash2curve"] } sha2 = { workspace = true, optional = true } stark-curve = { version = "0.1", default-features = false, optional = true } @@ -39,6 +40,7 @@ default = [] rust-crypto = ["elliptic-curve"] secp256k1 = ["rust-crypto", "k256", "sha2"] secp256r1 = ["rust-crypto", "p256", "sha2"] +secp384r1 = ["rust-crypto", "p384", "sha2"] stark = ["rust-crypto", "stark-curve", "sha2"] ed25519 = ["dep:curve25519", "dep:group"] diff --git a/generic-ec-curves/benches/curves.rs b/generic-ec-curves/benches/curves.rs index 2deb569..9647d26 100644 --- a/generic-ec-curves/benches/curves.rs +++ b/generic-ec-curves/benches/curves.rs @@ -14,6 +14,9 @@ fn bench_curves(c: &mut criterion::Criterion) { bench_curve::(c, &mut rng, "secp256r1"); bench_bytes_reduction::(c, &mut rng, "secp256r1"); + bench_curve::(c, &mut rng, "secp384r1"); + bench_bytes_reduction::(c, &mut rng, "secp384r1"); + bench_curve::(c, &mut rng, "stark"); bench_bytes_reduction::(c, &mut rng, "stark"); diff --git a/generic-ec-curves/src/lib.rs b/generic-ec-curves/src/lib.rs index a141b06..9b64a91 100644 --- a/generic-ec-curves/src/lib.rs +++ b/generic-ec-curves/src/lib.rs @@ -27,6 +27,9 @@ pub use rust_crypto::Secp256k1; #[cfg(feature = "secp256r1")] pub use rust_crypto::Secp256r1; +#[cfg(feature = "secp384r1")] +pub use rust_crypto::Secp384r1; + #[cfg(feature = "stark")] pub use rust_crypto::Stark; diff --git a/generic-ec-curves/src/rust_crypto/curve_name.rs b/generic-ec-curves/src/rust_crypto/curve_name.rs index 83bb2f8..4ce673a 100644 --- a/generic-ec-curves/src/rust_crypto/curve_name.rs +++ b/generic-ec-curves/src/rust_crypto/curve_name.rs @@ -14,6 +14,11 @@ impl CurveName for k256::Secp256k1 { const CURVE_NAME: &'static str = "secp256k1"; } +#[cfg(feature = "secp384r1")] +impl CurveName for p384::NistP384 { + const CURVE_NAME: &'static str = "secp384r1"; +} + #[cfg(feature = "stark")] impl CurveName for stark_curve::StarkCurve { const CURVE_NAME: &'static str = "stark"; diff --git a/generic-ec-curves/src/rust_crypto/mod.rs b/generic-ec-curves/src/rust_crypto/mod.rs index f371d98..c462f01 100644 --- a/generic-ec-curves/src/rust_crypto/mod.rs +++ b/generic-ec-curves/src/rust_crypto/mod.rs @@ -20,7 +20,7 @@ use generic_ec_core::{ use subtle::{ConditionallySelectable, ConstantTimeEq}; use zeroize::{DefaultIsZeroes, Zeroize}; -#[cfg(any(feature = "secp256k1", feature = "secp256r1", feature = "stark"))] +#[cfg(any(feature = "secp256k1", feature = "secp256r1", feature = "secp384r1", feature = "stark"))] use sha2::Sha256; pub use self::{curve_name::CurveName, point::RustCryptoPoint, scalar::RustCryptoScalar}; @@ -67,6 +67,12 @@ pub type Secp256k1 = RustCryptoCurve>; #[cfg(feature = "secp256r1")] pub type Secp256r1 = RustCryptoCurve>; +/// secp384r1 curve (NIST P-384) +/// +/// Based on [p384] crate +#[cfg(feature = "secp384r1")] +pub type Secp384r1 = RustCryptoCurve>; + /// Stark curve /// /// Based on [stark_curve] crate @@ -175,6 +181,13 @@ unsafe impl NoInvalidPoints for Secp256r1 {} /// Safe because: /// - RustCrypto curves are always on curve: /// generic-ec-curves/src/rust_crypto/point.rs:60 +/// - p384 is prime order and so is always torsion-free: +/// +#[cfg(feature = "secp384r1")] +unsafe impl NoInvalidPoints for Secp384r1 {} +/// Safe because: +/// - RustCrypto curves are always on curve: +/// generic-ec-curves/src/rust_crypto/point.rs:60 /// - stark is prime order and so is always torsion-free: /// #[cfg(feature = "stark")] @@ -187,7 +200,7 @@ mod tests { Curve, }; - use super::{Secp256k1, Secp256r1, Stark}; + use super::{Secp256k1, Secp256r1, Secp384r1, Stark}; /// Asserts that `E` implements `Curve` fn _impls_curve() {} @@ -196,10 +209,12 @@ mod tests { fn _curves_impl_trait() { _impls_curve::(); _impls_curve::(); + _impls_curve::(); _impls_curve::(); _exposes_affine_coords::(); _exposes_affine_coords::(); + _exposes_affine_coords::(); _exposes_affine_coords::(); } } diff --git a/generic-ec-curves/src/rust_crypto/scalar.rs b/generic-ec-curves/src/rust_crypto/scalar.rs index 2d2ffdd..64b1d21 100644 --- a/generic-ec-curves/src/rust_crypto/scalar.rs +++ b/generic-ec-curves/src/rust_crypto/scalar.rs @@ -1,6 +1,6 @@ use core::ops::Mul; -use elliptic_curve::bigint::{ArrayEncoding, ByteArray, U256, U512}; +use elliptic_curve::bigint::{ArrayEncoding, ByteArray, U256, U384, U512}; use elliptic_curve::{Curve, CurveArithmetic, Field, Group, ScalarPrimitive}; use generic_ec_core::{ Additive, CurveGenerator, FromUniformBytes, IntegerEncoding, Invertible, Multiplicative, One, @@ -108,6 +108,17 @@ impl FromUniformBytes for RustCryptoScalar { BytesModOrder::from_be_bytes_mod_order(bytes) } } +#[cfg(feature = "secp384r1")] +impl FromUniformBytes for RustCryptoScalar { + /// 64 bytes + /// + /// `L = ceil((ceil(log2(q)) + k) / 8) = ceil((384 + 128) / 8) = 64` bytes are enough to + /// guarantee the uniform distribution + type Bytes = [u8; 64]; + fn from_uniform_bytes(bytes: &Self::Bytes) -> Self { + BytesModOrder::from_be_bytes_mod_order(bytes) + } +} #[cfg(feature = "stark")] impl FromUniformBytes for RustCryptoScalar { /// 48 bytes @@ -265,6 +276,22 @@ where } } +impl Reduce<48> for RustCryptoScalar +where + E::Scalar: elliptic_curve::ops::Reduce, +{ + fn from_be_array_mod_order(bytes: &[u8; 48]) -> Self { + Self(elliptic_curve::ops::Reduce::::reduce( + U384::from_be_byte_array((*bytes).into()), + )) + } + fn from_le_array_mod_order(bytes: &[u8; 48]) -> Self { + Self(elliptic_curve::ops::Reduce::::reduce( + U384::from_le_byte_array((*bytes).into()), + )) + } +} + /// Choice of algorithm for computing bytes mod curve order. Efficient algorithm /// is different for different curves. pub(super) trait BytesModOrder { @@ -290,6 +317,15 @@ impl BytesModOrder for RustCryptoScalar { crate::utils::scalar_from_le_bytes_mod_order_reducing_32(bytes, &Self(p256::Scalar::ONE)) } } +#[cfg(feature = "secp384r1")] +impl BytesModOrder for RustCryptoScalar { + fn from_be_bytes_mod_order(bytes: &[u8]) -> Self { + crate::utils::scalar_from_be_bytes_mod_order_reducing_48(bytes, &Self(p384::Scalar::ONE)) + } + fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { + crate::utils::scalar_from_le_bytes_mod_order_reducing_48(bytes, &Self(p384::Scalar::ONE)) + } +} #[cfg(feature = "stark")] impl BytesModOrder for RustCryptoScalar { fn from_be_bytes_mod_order(bytes: &[u8]) -> Self { diff --git a/generic-ec-curves/src/utils.rs b/generic-ec-curves/src/utils.rs index b621f90..0288e17 100644 --- a/generic-ec-curves/src/utils.rs +++ b/generic-ec-curves/src/utils.rs @@ -130,6 +130,114 @@ where } } +/// Interprets `bytes` as little-endian encoding of an integer, takes it modulo curve (prime) +/// order and returns scalar `S` +/// +/// Works with scalars for which only [`Reduce<48>`][Reduce] is defined. +/// +/// Takes: +/// * Little-endian `bytes` representation of the integer +/// * Scalar `one = 1` +pub fn scalar_from_le_bytes_mod_order_reducing_48(bytes: &[u8], one: &S) -> S +where + S: Default + Copy, + S: Reduce<48>, + S: generic_ec_core::Additive + generic_ec_core::Multiplicative, +{ + let len = bytes.len(); + match len { + ..=47 => { + let mut padded = [0u8; 48]; + padded[..len].copy_from_slice(bytes); + S::from_le_array_mod_order(&padded) + } + 48 => { + #[allow(clippy::expect_used)] + let bytes: &[u8; 48] = bytes.try_into().expect("we checked that bytes len == 48"); + S::from_le_array_mod_order(bytes) + } + 49.. => { + let two_to_384 = S::add(&S::from_le_array_mod_order(&[0xff; 48]), one); + + let chunks = bytes.chunks_exact(48); + let remainder = if !chunks.remainder().is_empty() { + Some(scalar_from_le_bytes_mod_order_reducing_48::( + chunks.remainder(), + one, + )) + } else { + None + }; + + let chunks = chunks.rev().map(|chunk| { + #[allow(clippy::expect_used)] + let chunk: &[u8; 48] = chunk.try_into().expect("wrong chunk size"); + S::from_le_array_mod_order(chunk) + }); + + remainder + .into_iter() + .chain(chunks) + .reduce(|acc, int| S::add(&S::mul(&acc, &two_to_384), &int)) + .unwrap_or_default() + } + } +} + +/// Interprets `bytes` as big-endian encoding of an integer, takes it modulo curve (prime) +/// order and returns scalar `S` +/// +/// Works with scalars for which only [`Reduce<48>`][Reduce] is defined. +/// +/// Takes: +/// * Big-endian `bytes` representation of the integer +/// * Scalar `one = 1` +pub fn scalar_from_be_bytes_mod_order_reducing_48(bytes: &[u8], one: &S) -> S +where + S: Default + Copy, + S: Reduce<48>, + S: generic_ec_core::Additive + generic_ec_core::Multiplicative, +{ + let len = bytes.len(); + match len { + ..=47 => { + let mut padded = [0u8; 48]; + padded[48 - len..].copy_from_slice(bytes); + S::from_be_array_mod_order(&padded) + } + 48 => { + #[allow(clippy::expect_used)] + let bytes: &[u8; 48] = bytes.try_into().expect("we checked that bytes len == 48"); + S::from_be_array_mod_order(bytes) + } + 49.. => { + let two_to_384 = S::add(&S::from_be_array_mod_order(&[0xff; 48]), one); + + let chunks = bytes.rchunks_exact(48); + let remainder = if !chunks.remainder().is_empty() { + Some(scalar_from_be_bytes_mod_order_reducing_48::( + chunks.remainder(), + one, + )) + } else { + None + }; + + let chunks = chunks.rev().map(|chunk| { + #[allow(clippy::expect_used)] + let chunk: &[u8; 48] = chunk.try_into().expect("wrong chunk size"); + S::from_be_array_mod_order(chunk) + }); + + remainder + .into_iter() + .chain(chunks) + .reduce(|acc, int| S::add(&S::mul(&acc, &two_to_384), &int)) + .unwrap_or_default() + } + } +} + /// Interprets `bytes` as little-endian encoding of an integer, takes it modulo curve (prime) /// order and returns scalar `S` /// diff --git a/generic-ec/Cargo.toml b/generic-ec/Cargo.toml index 4f06ee9..c54f9f2 100644 --- a/generic-ec/Cargo.toml +++ b/generic-ec/Cargo.toml @@ -53,9 +53,10 @@ udigest = ["dep:udigest"] curves = ["generic-ec-curves"] curve-secp256k1 = ["curves", "generic-ec-curves/secp256k1"] curve-secp256r1 = ["curves", "generic-ec-curves/secp256r1"] +curve-secp384r1 = ["curves", "generic-ec-curves/secp384r1"] curve-stark = ["curves", "generic-ec-curves/stark"] curve-ed25519 = ["curves", "generic-ec-curves/ed25519", "curve25519"] -all-curves = ["curve-secp256k1", "curve-secp256r1", "curve-stark", "curve-ed25519"] +all-curves = ["curve-secp256k1", "curve-secp256r1", "curve-secp384r1", "curve-stark", "curve-ed25519"] hash-to-scalar = ["dep:rand_hash", "dep:digest", "udigest"] diff --git a/generic-ec/src/lib.rs b/generic-ec/src/lib.rs index 6700d14..82fd7e3 100644 --- a/generic-ec/src/lib.rs +++ b/generic-ec/src/lib.rs @@ -71,11 +71,13 @@ //! |--------------|--------------------|-------------------| //! | secp256k1 | `curve-secp256k1` | [RustCrypto/k256] | //! | secp256r1 | `curve-secp256r1` | [RustCrypto/p256] | +//! | secp384r1 | `curve-secp384r1` | [RustCrypto/p384] | //! | stark-curve | `curve-stark` | [Dfns/stark] | //! | Ed25519 | `curve-ed25519` | [curve25519-dalek]| //! //! [RustCrypto/k256]: https://github.com/RustCrypto/elliptic-curves/tree/master/k256 //! [RustCrypto/p256]: https://github.com/RustCrypto/elliptic-curves/tree/master/p256 +//! [RustCrypto/p384]: https://github.com/RustCrypto/elliptic-curves/tree/master/p384 //! [Dfns/stark]: https://github.com/LFDT-Lockness/stark-curve/ //! [curve25519-dalek]: https://docs.rs/curve25519-dalek/ //! @@ -277,6 +279,9 @@ pub mod curves { #[cfg(feature = "curve-secp256r1")] #[cfg_attr(docsrs, doc(cfg(feature = "curve-secp256r1")))] pub use generic_ec_curves::Secp256r1; + #[cfg(feature = "curve-secp384r1")] + #[cfg_attr(docsrs, doc(cfg(feature = "curve-secp384r1")))] + pub use generic_ec_curves::Secp384r1; #[cfg(feature = "curve-stark")] #[cfg_attr(docsrs, doc(cfg(feature = "curve-stark")))] pub use generic_ec_curves::Stark; @@ -351,6 +356,8 @@ pub mod curves { secp256k1: Secp256k1, #[cfg(feature = "curve-secp256r1")] secp256r1: Secp256r1, + #[cfg(feature = "curve-secp384r1")] + secp384r1: Secp384r1, #[cfg(feature = "curve-stark")] stark: Stark, #[cfg(feature = "curve-ed25519")] diff --git a/generic-ec/src/multiscalar/straus.rs b/generic-ec/src/multiscalar/straus.rs index 381340d..dfdf1e0 100644 --- a/generic-ec/src/multiscalar/straus.rs +++ b/generic-ec/src/multiscalar/straus.rs @@ -320,6 +320,8 @@ mod tests { mod secp256k1 {} #[instantiate_tests()] mod secp256r1 {} + #[instantiate_tests()] + mod secp384r1 {} #[instantiate_tests()] mod stark {} #[instantiate_tests()] diff --git a/tests/benches/multiscalar.rs b/tests/benches/multiscalar.rs index 478f069..eb34be0 100644 --- a/tests/benches/multiscalar.rs +++ b/tests/benches/multiscalar.rs @@ -15,6 +15,7 @@ fn multiscalar(c: &mut criterion::Criterion) { multiscalar_for_curve::(c, &mut rng, "secp256k1"); multiscalar_for_curve::(c, &mut rng, "secp256r1"); + multiscalar_for_curve::(c, &mut rng, "secp384r1"); multiscalar_for_curve::(c, &mut rng, "stark"); multiscalar_for_curve::(c, &mut rng, "ed25519"); diff --git a/tests/benches/random.rs b/tests/benches/random.rs index 5d24e53..1f2b416 100644 --- a/tests/benches/random.rs +++ b/tests/benches/random.rs @@ -8,6 +8,7 @@ fn random(c: &mut criterion::Criterion) { random_for_curve::(c, &mut rng, "secp256k1"); random_for_curve::(c, &mut rng, "secp256r1"); + random_for_curve::(c, &mut rng, "secp384r1"); random_for_curve::(c, &mut rng, "stark"); random_for_curve::(c, &mut rng, "ed25519"); } diff --git a/tests/tests/curves.rs b/tests/tests/curves.rs index 4af1888..21ce32b 100644 --- a/tests/tests/curves.rs +++ b/tests/tests/curves.rs @@ -248,6 +248,9 @@ mod tests { #[instantiate_tests()] mod secp256r1 {} + #[instantiate_tests()] + mod secp384r1 {} + #[instantiate_tests()] mod stark {} @@ -287,6 +290,9 @@ mod scalar_reduce { #[instantiate_tests()] mod secp256r1_32 {} + #[instantiate_tests()] + mod secp384r1_48 {} + #[instantiate_tests()] mod stark_32 {} @@ -299,7 +305,7 @@ mod scalar_reduce { #[generic_tests::define] mod coordinates { use generic_ec::coords::{HasAffineX, HasAffineXAndParity, HasAffineXY, HasAffineY}; - use generic_ec::curves::{Secp256k1, Secp256r1, Stark}; + use generic_ec::curves::{Secp256k1, Secp256r1, Secp384r1, Stark}; use generic_ec::{Curve, Point, Scalar}; use rand_dev::DevRng; @@ -353,6 +359,9 @@ mod coordinates { #[instantiate_tests()] mod secp256r1 {} + #[instantiate_tests()] + mod secp384r1 {} + #[instantiate_tests()] mod stark {} } diff --git a/tests/tests/dlog_eq.rs b/tests/tests/dlog_eq.rs index 304abe4..911df4e 100644 --- a/tests/tests/dlog_eq.rs +++ b/tests/tests/dlog_eq.rs @@ -40,6 +40,8 @@ mod interactive { mod secp256k1 {} #[instantiate_tests()] mod secp256r1 {} + #[instantiate_tests()] + mod secp384r1 {} #[instantiate_tests()] mod stark {} #[instantiate_tests()] @@ -84,6 +86,8 @@ mod non_interactive { mod secp256k1_sha256 {} #[instantiate_tests()] mod secp256r1_sha256 {} + #[instantiate_tests()] + mod secp384r1_sha256 {} #[instantiate_tests()] mod stark_sha256 {} #[instantiate_tests()] diff --git a/tests/tests/multiscalar.rs b/tests/tests/multiscalar.rs index 88af298..0fe55a4 100644 --- a/tests/tests/multiscalar.rs +++ b/tests/tests/multiscalar.rs @@ -3,7 +3,7 @@ mod tests { use core::iter; use generic_ec::{ - curves::{Ed25519, Secp256k1, Secp256r1, Stark}, + curves::{Ed25519, Secp256k1, Secp256r1, Secp384r1, Stark}, multiscalar::{Dalek, MultiscalarMul, Naive, Straus}, Curve, Point, Scalar, }; @@ -35,6 +35,8 @@ mod tests { mod secp256k1_straus {} #[instantiate_tests()] mod secp256r1_straus {} + #[instantiate_tests()] + mod secp384r1_straus {} #[instantiate_tests()] mod stark_straus {} #[instantiate_tests()] diff --git a/tests/tests/serde.rs b/tests/tests/serde.rs index e5b9070..055251d 100644 --- a/tests/tests/serde.rs +++ b/tests/tests/serde.rs @@ -418,6 +418,9 @@ mod tests { #[instantiate_tests()] mod secp256r1 {} + #[instantiate_tests()] + mod secp384r1 {} + #[instantiate_tests()] mod stark {} } From 28fa09ad2fd5009658a2c5a865a83adac295f1a4 Mon Sep 17 00:00:00 2001 From: Shridhar Panigrahi Date: Mon, 16 Mar 2026 23:51:32 +0530 Subject: [PATCH 4/5] Address maintainer review feedback - Remove `secp384r1` from the `Sha256` cfg import guard; P-384 uses `sha2::Sha384`, not `Sha256` (reported by survived) - Replace the duplicated `scalar_from_{le,be}_bytes_mod_order_reducing_32` and `_48` helpers with a single pair generic over `const N: usize`, reducing code duplication (suggested by maurges) Signed-off-by: Shridhar Panigrahi --- generic-ec-curves/src/rust_crypto/mod.rs | 2 +- generic-ec-curves/src/rust_crypto/scalar.rs | 24 +- generic-ec-curves/src/utils.rs | 246 ++++++-------------- 3 files changed, 84 insertions(+), 188 deletions(-) diff --git a/generic-ec-curves/src/rust_crypto/mod.rs b/generic-ec-curves/src/rust_crypto/mod.rs index c462f01..1ee5e0b 100644 --- a/generic-ec-curves/src/rust_crypto/mod.rs +++ b/generic-ec-curves/src/rust_crypto/mod.rs @@ -20,7 +20,7 @@ use generic_ec_core::{ use subtle::{ConditionallySelectable, ConstantTimeEq}; use zeroize::{DefaultIsZeroes, Zeroize}; -#[cfg(any(feature = "secp256k1", feature = "secp256r1", feature = "secp384r1", feature = "stark"))] +#[cfg(any(feature = "secp256k1", feature = "secp256r1", feature = "stark"))] use sha2::Sha256; pub use self::{curve_name::CurveName, point::RustCryptoPoint, scalar::RustCryptoScalar}; diff --git a/generic-ec-curves/src/rust_crypto/scalar.rs b/generic-ec-curves/src/rust_crypto/scalar.rs index 64b1d21..d15bcba 100644 --- a/generic-ec-curves/src/rust_crypto/scalar.rs +++ b/generic-ec-curves/src/rust_crypto/scalar.rs @@ -311,31 +311,43 @@ impl BytesModOrder for RustCryptoScalar { #[cfg(feature = "secp256r1")] impl BytesModOrder for RustCryptoScalar { fn from_be_bytes_mod_order(bytes: &[u8]) -> Self { - crate::utils::scalar_from_be_bytes_mod_order_reducing_32(bytes, &Self(p256::Scalar::ONE)) + crate::utils::scalar_from_be_bytes_mod_order_reducing::<_, 32>( + bytes, + &Self(p256::Scalar::ONE), + ) } fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { - crate::utils::scalar_from_le_bytes_mod_order_reducing_32(bytes, &Self(p256::Scalar::ONE)) + crate::utils::scalar_from_le_bytes_mod_order_reducing::<_, 32>( + bytes, + &Self(p256::Scalar::ONE), + ) } } #[cfg(feature = "secp384r1")] impl BytesModOrder for RustCryptoScalar { fn from_be_bytes_mod_order(bytes: &[u8]) -> Self { - crate::utils::scalar_from_be_bytes_mod_order_reducing_48(bytes, &Self(p384::Scalar::ONE)) + crate::utils::scalar_from_be_bytes_mod_order_reducing::<_, 48>( + bytes, + &Self(p384::Scalar::ONE), + ) } fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { - crate::utils::scalar_from_le_bytes_mod_order_reducing_48(bytes, &Self(p384::Scalar::ONE)) + crate::utils::scalar_from_le_bytes_mod_order_reducing::<_, 48>( + bytes, + &Self(p384::Scalar::ONE), + ) } } #[cfg(feature = "stark")] impl BytesModOrder for RustCryptoScalar { fn from_be_bytes_mod_order(bytes: &[u8]) -> Self { - crate::utils::scalar_from_be_bytes_mod_order_reducing_32( + crate::utils::scalar_from_be_bytes_mod_order_reducing::<_, 32>( bytes, &Self(stark_curve::Scalar::ONE), ) } fn from_le_bytes_mod_order(bytes: &[u8]) -> Self { - crate::utils::scalar_from_le_bytes_mod_order_reducing_32( + crate::utils::scalar_from_le_bytes_mod_order_reducing::<_, 32>( bytes, &Self(stark_curve::Scalar::ONE), ) diff --git a/generic-ec-curves/src/utils.rs b/generic-ec-curves/src/utils.rs index 0288e17..15d56b1 100644 --- a/generic-ec-curves/src/utils.rs +++ b/generic-ec-curves/src/utils.rs @@ -133,216 +133,100 @@ where /// Interprets `bytes` as little-endian encoding of an integer, takes it modulo curve (prime) /// order and returns scalar `S` /// -/// Works with scalars for which only [`Reduce<48>`][Reduce] is defined. +/// Works with scalars for which [`Reduce`][Reduce] is defined. /// /// Takes: /// * Little-endian `bytes` representation of the integer /// * Scalar `one = 1` -pub fn scalar_from_le_bytes_mod_order_reducing_48(bytes: &[u8], one: &S) -> S +pub fn scalar_from_le_bytes_mod_order_reducing(bytes: &[u8], one: &S) -> S where S: Default + Copy, - S: Reduce<48>, + S: Reduce, S: generic_ec_core::Additive + generic_ec_core::Multiplicative, { let len = bytes.len(); - match len { - ..=47 => { - let mut padded = [0u8; 48]; - padded[..len].copy_from_slice(bytes); - S::from_le_array_mod_order(&padded) - } - 48 => { - #[allow(clippy::expect_used)] - let bytes: &[u8; 48] = bytes.try_into().expect("we checked that bytes len == 48"); - S::from_le_array_mod_order(bytes) - } - 49.. => { - let two_to_384 = S::add(&S::from_le_array_mod_order(&[0xff; 48]), one); - - let chunks = bytes.chunks_exact(48); - let remainder = if !chunks.remainder().is_empty() { - Some(scalar_from_le_bytes_mod_order_reducing_48::( - chunks.remainder(), - one, - )) - } else { - None - }; - - let chunks = chunks.rev().map(|chunk| { - #[allow(clippy::expect_used)] - let chunk: &[u8; 48] = chunk.try_into().expect("wrong chunk size"); - S::from_le_array_mod_order(chunk) - }); + if len < N { + let mut padded = [0u8; N]; + padded[..len].copy_from_slice(bytes); + S::from_le_array_mod_order(&padded) + } else if len == N { + #[allow(clippy::expect_used)] + let bytes: &[u8; N] = bytes.try_into().expect("we checked that bytes len == N"); + S::from_le_array_mod_order(bytes) + } else { + let two_to_8n = S::add(&S::from_le_array_mod_order(&[0xff; N]), one); - remainder - .into_iter() - .chain(chunks) - .reduce(|acc, int| S::add(&S::mul(&acc, &two_to_384), &int)) - .unwrap_or_default() - } - } -} + let chunks = bytes.chunks_exact(N); + let remainder = if !chunks.remainder().is_empty() { + Some(scalar_from_le_bytes_mod_order_reducing::( + chunks.remainder(), + one, + )) + } else { + None + }; -/// Interprets `bytes` as big-endian encoding of an integer, takes it modulo curve (prime) -/// order and returns scalar `S` -/// -/// Works with scalars for which only [`Reduce<48>`][Reduce] is defined. -/// -/// Takes: -/// * Big-endian `bytes` representation of the integer -/// * Scalar `one = 1` -pub fn scalar_from_be_bytes_mod_order_reducing_48(bytes: &[u8], one: &S) -> S -where - S: Default + Copy, - S: Reduce<48>, - S: generic_ec_core::Additive + generic_ec_core::Multiplicative, -{ - let len = bytes.len(); - match len { - ..=47 => { - let mut padded = [0u8; 48]; - padded[48 - len..].copy_from_slice(bytes); - S::from_be_array_mod_order(&padded) - } - 48 => { + let chunks = chunks.rev().map(|chunk| { #[allow(clippy::expect_used)] - let bytes: &[u8; 48] = bytes.try_into().expect("we checked that bytes len == 48"); - S::from_be_array_mod_order(bytes) - } - 49.. => { - let two_to_384 = S::add(&S::from_be_array_mod_order(&[0xff; 48]), one); - - let chunks = bytes.rchunks_exact(48); - let remainder = if !chunks.remainder().is_empty() { - Some(scalar_from_be_bytes_mod_order_reducing_48::( - chunks.remainder(), - one, - )) - } else { - None - }; + let chunk: &[u8; N] = chunk.try_into().expect("wrong chunk size"); + S::from_le_array_mod_order(chunk) + }); - let chunks = chunks.rev().map(|chunk| { - #[allow(clippy::expect_used)] - let chunk: &[u8; 48] = chunk.try_into().expect("wrong chunk size"); - S::from_be_array_mod_order(chunk) - }); - - remainder - .into_iter() - .chain(chunks) - .reduce(|acc, int| S::add(&S::mul(&acc, &two_to_384), &int)) - .unwrap_or_default() - } - } -} - -/// Interprets `bytes` as little-endian encoding of an integer, takes it modulo curve (prime) -/// order and returns scalar `S` -/// -/// Works with scalars for which only [`Reduce<32>`][Reduce] is defined. -/// -/// Takes: -/// * Little-endian `bytes` representation of the integer -/// * Scalar `one = 1` -pub fn scalar_from_le_bytes_mod_order_reducing_32(bytes: &[u8], one: &S) -> S -where - S: Default + Copy, - S: Reduce<32>, - S: generic_ec_core::Additive + generic_ec_core::Multiplicative, -{ - let len = bytes.len(); - match len { - ..=31 => { - let mut padded = [0u8; 32]; - padded[..len].copy_from_slice(bytes); - S::from_le_array_mod_order(&padded) - } - 32 => { - #[allow(clippy::expect_used)] - let bytes: &[u8; 32] = bytes.try_into().expect("we checked that bytes len == 32"); - S::from_le_array_mod_order(bytes) - } - 33.. => { - let two_to_256 = S::add(&S::from_le_array_mod_order(&[0xff; 32]), one); - - let chunks = bytes.chunks_exact(32); - let remainder = if !chunks.remainder().is_empty() { - Some(scalar_from_le_bytes_mod_order_reducing_32::( - chunks.remainder(), - one, - )) - } else { - None - }; - - let chunks = chunks.rev().map(|chunk| { - #[allow(clippy::expect_used)] - let chunk: &[u8; 32] = chunk.try_into().expect("wrong chunk size"); - S::from_le_array_mod_order(chunk) - }); - - remainder - .into_iter() - .chain(chunks) - .reduce(|acc, int| S::add(&S::mul(&acc, &two_to_256), &int)) - .unwrap_or_default() - } + remainder + .into_iter() + .chain(chunks) + .reduce(|acc, int| S::add(&S::mul(&acc, &two_to_8n), &int)) + .unwrap_or_default() } } /// Interprets `bytes` as big-endian encoding of an integer, takes it modulo curve (prime) /// order and returns scalar `S` /// -/// Works with scalars for which only [`Reduce<32>`][Reduce] is defined. +/// Works with scalars for which [`Reduce`][Reduce] is defined. /// /// Takes: /// * Big-endian `bytes` representation of the integer /// * Scalar `one = 1` -pub fn scalar_from_be_bytes_mod_order_reducing_32(bytes: &[u8], one: &S) -> S +pub fn scalar_from_be_bytes_mod_order_reducing(bytes: &[u8], one: &S) -> S where S: Default + Copy, - S: Reduce<32>, + S: Reduce, S: generic_ec_core::Additive + generic_ec_core::Multiplicative, { let len = bytes.len(); - match len { - ..=31 => { - let mut padded = [0u8; 32]; - padded[32 - len..].copy_from_slice(bytes); - S::from_be_array_mod_order(&padded) - } - 32 => { - #[allow(clippy::expect_used)] - let bytes: &[u8; 32] = bytes.try_into().expect("we checked that bytes len == 32"); - S::from_be_array_mod_order(bytes) - } - 33.. => { - let two_to_256 = S::add(&S::from_be_array_mod_order(&[0xff; 32]), one); + if len < N { + let mut padded = [0u8; N]; + padded[N - len..].copy_from_slice(bytes); + S::from_be_array_mod_order(&padded) + } else if len == N { + #[allow(clippy::expect_used)] + let bytes: &[u8; N] = bytes.try_into().expect("we checked that bytes len == N"); + S::from_be_array_mod_order(bytes) + } else { + let two_to_8n = S::add(&S::from_be_array_mod_order(&[0xff; N]), one); - let chunks = bytes.rchunks_exact(32); - let remainder = if !chunks.remainder().is_empty() { - Some(scalar_from_be_bytes_mod_order_reducing_32::( - chunks.remainder(), - one, - )) - } else { - None - }; + let chunks = bytes.rchunks_exact(N); + let remainder = if !chunks.remainder().is_empty() { + Some(scalar_from_be_bytes_mod_order_reducing::( + chunks.remainder(), + one, + )) + } else { + None + }; - let chunks = chunks.rev().map(|chunk| { - #[allow(clippy::expect_used)] - let chunk: &[u8; 32] = chunk.try_into().expect("wrong chunk size"); - S::from_be_array_mod_order(chunk) - }); + let chunks = chunks.rev().map(|chunk| { + #[allow(clippy::expect_used)] + let chunk: &[u8; N] = chunk.try_into().expect("wrong chunk size"); + S::from_be_array_mod_order(chunk) + }); - remainder - .into_iter() - .chain(chunks) - .reduce(|acc, int| S::add(&S::mul(&acc, &two_to_256), &int)) - .unwrap_or_default() - } + remainder + .into_iter() + .chain(chunks) + .reduce(|acc, int| S::add(&S::mul(&acc, &two_to_8n), &int)) + .unwrap_or_default() } } @@ -363,7 +247,7 @@ mod tests { ); assert_eq!( expected, - super::scalar_from_be_bytes_mod_order_reducing_32(&x.to_be_bytes(), one).0 + super::scalar_from_be_bytes_mod_order_reducing::<_, 32>(&x.to_be_bytes(), one).0 ); assert_eq!( @@ -372,7 +256,7 @@ mod tests { ); assert_eq!( expected, - super::scalar_from_le_bytes_mod_order_reducing_32(&x.to_le_bytes(), one).0 + super::scalar_from_le_bytes_mod_order_reducing::<_, 32>(&x.to_le_bytes(), one).0 ); } } From 5f327421c60ed6d83894b110bb3b8c649d701387 Mon Sep 17 00:00:00 2001 From: Shridhar Panigrahi Date: Fri, 20 Mar 2026 16:30:28 +0530 Subject: [PATCH 5/5] Sync README.md with lib.rs docs via cargo rdme Signed-off-by: Shridhar Panigrahi --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6d56dd8..42bf3e4 100644 --- a/README.md +++ b/README.md @@ -73,11 +73,13 @@ Crate provides support for following elliptic curves out of box: |--------------|--------------------|-------------------| | secp256k1 | `curve-secp256k1` | [RustCrypto/k256] | | secp256r1 | `curve-secp256r1` | [RustCrypto/p256] | +| secp384r1 | `curve-secp384r1` | [RustCrypto/p384] | | stark-curve | `curve-stark` | [Dfns/stark] | | Ed25519 | `curve-ed25519` | [curve25519-dalek]| [RustCrypto/k256]: https://github.com/RustCrypto/elliptic-curves/tree/master/k256 [RustCrypto/p256]: https://github.com/RustCrypto/elliptic-curves/tree/master/p256 +[RustCrypto/p384]: https://github.com/RustCrypto/elliptic-curves/tree/master/p384 [Dfns/stark]: https://github.com/LFDT-Lockness/stark-curve/ [curve25519-dalek]: https://docs.rs/curve25519-dalek/