From 71114d29d954205e3f58404b6267a977682bfe46 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Wed, 13 Aug 2025 21:23:21 -0600 Subject: [PATCH 1/5] [WIP] Scalar::div_by_2 --- .../src/backend/serial/u32/scalar.rs | 28 ++++++++++++--- .../src/backend/serial/u64/scalar.rs | 29 +++++++++++++--- curve25519-dalek/src/scalar.rs | 34 +++++++++++++++++++ 3 files changed, 81 insertions(+), 10 deletions(-) diff --git a/curve25519-dalek/src/backend/serial/u32/scalar.rs b/curve25519-dalek/src/backend/serial/u32/scalar.rs index cc21139bc..4c34ee2d4 100644 --- a/curve25519-dalek/src/backend/serial/u32/scalar.rs +++ b/curve25519-dalek/src/backend/serial/u32/scalar.rs @@ -197,15 +197,33 @@ impl Scalar29 { } // conditionally add l if the difference is negative + difference.conditional_add_l(Choice::from((borrow >> 31) as u8)); + difference + } + + pub(crate) fn conditional_add_l(&mut self, condition: Choice) -> u32 { let mut carry: u32 = 0; + let mask = (1u32 << 29) - 1; + for i in 0..9 { - let underflow = Choice::from((borrow >> 31) as u8); - let addend = u32::conditional_select(&0, &constants::L[i], underflow); - carry = (carry >> 29) + difference[i] + addend; - difference[i] = carry & mask; + let addend = u32::conditional_select(&0, &constants::L[i], condition); + carry = (carry >> 29) + self[i] + addend; + self[i] = carry & mask; } + carry + } - difference + /// Compute a raw in-place carrying right shift over the limbs. + #[inline(always)] + pub(crate) fn shr1_assign(&mut self) -> u32 { + let mut carry: u32 = 0; + for i in (0..9).rev() { + let limb = self[i]; + let next_carry = limb & 1; + self[i] = (limb >> 1) | (carry << 28); + carry = next_carry; + } + carry } /// Compute `a * b`. diff --git a/curve25519-dalek/src/backend/serial/u64/scalar.rs b/curve25519-dalek/src/backend/serial/u64/scalar.rs index 5bcbb72c3..f4eb4a2af 100644 --- a/curve25519-dalek/src/backend/serial/u64/scalar.rs +++ b/curve25519-dalek/src/backend/serial/u64/scalar.rs @@ -186,15 +186,34 @@ impl Scalar52 { } // conditionally add l if the difference is negative + difference.conditional_add_l(Choice::from((borrow >> 63) as u8)); + difference + } + + pub(crate) fn conditional_add_l(&mut self, condition: Choice) -> u64 { let mut carry: u64 = 0; + let mask = (1u64 << 52) - 1; + for i in 0..5 { - let underflow = Choice::from((borrow >> 63) as u8); - let addend = u64::conditional_select(&0, &constants::L[i], underflow); - carry = (carry >> 52) + difference[i] + addend; - difference[i] = carry & mask; + let addend = u64::conditional_select(&0, &constants::L[i], condition); + carry = (carry >> 52) + self[i] + addend; + self[i] = carry & mask; } - difference + carry + } + + /// Compute a raw in-place carrying right shift over the limbs. + #[inline(always)] + pub(crate) fn shr1_assign(&mut self) -> u64 { + let mut carry: u64 = 0; + for i in (0..5).rev() { + let limb = self[i]; + let next_carry = limb & 1; + self[i] = (limb >> 1) | (carry << 51); + carry = next_carry; + } + carry } /// Compute `a * b` diff --git a/curve25519-dalek/src/scalar.rs b/curve25519-dalek/src/scalar.rs index fc8939fb4..f88a2af57 100644 --- a/curve25519-dalek/src/scalar.rs +++ b/curve25519-dalek/src/scalar.rs @@ -831,6 +831,21 @@ impl Scalar { ret } + /// Compute `b` such that `b + b = a mod modulus`. + pub fn div_by_2(&self) -> Self { + // We are looking for such `b` that `b + b = a mod modulus`. + // Two possibilities: + // - if `a` is even, we can just divide by 2; + // - if `a` is odd, we divide `(a + modulus)` by 2. + let is_odd = Choice::from(self.as_bytes()[0] & 1); + let mut scalar = self.unpack(); + scalar.conditional_add_l(is_odd); + + // TODO(tarcieri): propagate carry + let _carry = scalar.shr1_assign(); + scalar.pack() + } + /// Get the bits of the scalar, in little-endian order pub(crate) fn bits_le(&self) -> impl DoubleEndedIterator + '_ { (0..256).map(|i| { @@ -1677,6 +1692,25 @@ pub(crate) mod test { } } + #[test] + fn div_by_2() { + // test a range of small scalars + for i in 0u64..32 { + let scalar = Scalar::from(i); + let double = scalar + scalar; + let dividend = double.div_by_2(); + assert_eq!(scalar, dividend); + } + + // test odd value near the order + let scalar = Scalar::ZERO - Scalar::from(2u64); + #[cfg(feature = "group")] + assert!(bool::from(scalar.is_odd())); + + let dividend = scalar.div_by_2(); + assert_eq!(scalar, dividend + dividend); + } + #[test] fn reduce() { let biggest = Scalar::from_bytes_mod_order([0xff; 32]); From b65d0823f239b196b9cc67aca91c80a43b859f94 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Wed, 13 Aug 2025 21:52:51 -0600 Subject: [PATCH 2/5] debug_assert that carry is 0 --- curve25519-dalek/src/scalar.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/curve25519-dalek/src/scalar.rs b/curve25519-dalek/src/scalar.rs index f88a2af57..cb04781d9 100644 --- a/curve25519-dalek/src/scalar.rs +++ b/curve25519-dalek/src/scalar.rs @@ -841,8 +841,9 @@ impl Scalar { let mut scalar = self.unpack(); scalar.conditional_add_l(is_odd); - // TODO(tarcieri): propagate carry - let _carry = scalar.shr1_assign(); + let carry = scalar.shr1_assign(); + debug_assert_eq!(carry, 0); + scalar.pack() } From 58c8f39a754e64b2f6fba3602a3dc6046f62f4c3 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Wed, 13 Aug 2025 21:55:23 -0600 Subject: [PATCH 3/5] revise tests --- curve25519-dalek/src/scalar.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/curve25519-dalek/src/scalar.rs b/curve25519-dalek/src/scalar.rs index cb04781d9..7fc6e2783 100644 --- a/curve25519-dalek/src/scalar.rs +++ b/curve25519-dalek/src/scalar.rs @@ -843,7 +843,7 @@ impl Scalar { let carry = scalar.shr1_assign(); debug_assert_eq!(carry, 0); - + scalar.pack() } @@ -1698,18 +1698,16 @@ pub(crate) mod test { // test a range of small scalars for i in 0u64..32 { let scalar = Scalar::from(i); - let double = scalar + scalar; - let dividend = double.div_by_2(); - assert_eq!(scalar, dividend); + let dividend = scalar.div_by_2(); + assert_eq!(scalar, dividend + dividend); } - // test odd value near the order - let scalar = Scalar::ZERO - Scalar::from(2u64); - #[cfg(feature = "group")] - assert!(bool::from(scalar.is_odd())); - - let dividend = scalar.div_by_2(); - assert_eq!(scalar, dividend + dividend); + // test a range of scalars near the modulus + for i in 0u64..32 { + let scalar = Scalar::ZERO - Scalar::from(i); + let dividend = scalar.div_by_2(); + assert_eq!(scalar, dividend + dividend); + } } #[test] From 25b424a974f57987acb3869dc1f4fcef58443b37 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 14 Aug 2025 14:27:33 +0200 Subject: [PATCH 4/5] Test multiply by half scalar, double and compress (#804) --- curve25519-dalek/src/ristretto.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/curve25519-dalek/src/ristretto.rs b/curve25519-dalek/src/ristretto.rs index ca5fa88ec..e67664ec0 100644 --- a/curve25519-dalek/src/ristretto.rs +++ b/curve25519-dalek/src/ristretto.rs @@ -1867,6 +1867,35 @@ mod test { } } + #[test] + #[cfg(all(feature = "alloc", feature = "rand_core", feature = "group"))] + fn multiply_double_and_compress_1024_random_points() { + use ff::Field; + use group::Group; + let mut rng = OsRng; + + let mut scalars: Vec = (0..1024) + .map(|_| Scalar::try_from_rng(&mut rng).unwrap()) + .collect(); + scalars[500] = Scalar::ZERO; + + let mut points: Vec = (0..1024) + .map(|_| RistrettoPoint::try_from_rng(&mut rng).unwrap()) + .collect(); + points[500] = ::identity(); + + let multiplied_points: Vec = scalars + .iter() + .zip(&points) + .map(|(scalar, point)| scalar.div_by_2() * point) + .collect(); + let compressed = RistrettoPoint::double_and_compress_batch(&multiplied_points); + + for ((s, P), P2_compressed) in scalars.iter().zip(points).zip(compressed) { + assert_eq!(P2_compressed, (s * P).compress()); + } + } + #[test] #[cfg(feature = "alloc")] fn vartime_precomputed_vs_nonprecomputed_multiscalar() { From 11b61a1030a5c07e8990971f961608fbd8454127 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Mon, 18 Aug 2025 18:23:21 +0200 Subject: [PATCH 5/5] Test `div_by_2` with `proptest` (#806) --- curve25519-dalek/Cargo.toml | 1 + curve25519-dalek/src/ristretto.rs | 58 +++++++++++++++++-------------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/curve25519-dalek/Cargo.toml b/curve25519-dalek/Cargo.toml index 1fbd72d00..72bff1de7 100644 --- a/curve25519-dalek/Cargo.toml +++ b/curve25519-dalek/Cargo.toml @@ -41,6 +41,7 @@ sha2 = { version = "0.11.0-rc.0", default-features = false } bincode = "1" criterion = { version = "0.5", features = ["html_reports"] } hex = "0.4.2" +proptest = "1" rand = "0.9" rand_core = { version = "0.9", default-features = false, features = ["os_rng"] } diff --git a/curve25519-dalek/src/ristretto.rs b/curve25519-dalek/src/ristretto.rs index e67664ec0..8b867930d 100644 --- a/curve25519-dalek/src/ristretto.rs +++ b/curve25519-dalek/src/ristretto.rs @@ -1321,6 +1321,8 @@ impl Zeroize for RistrettoPoint { mod test { use super::*; use crate::edwards::CompressedEdwardsY; + #[cfg(feature = "group")] + use proptest::prelude::*; use rand_core::{OsRng, TryRngCore}; @@ -1867,32 +1869,36 @@ mod test { } } - #[test] - #[cfg(all(feature = "alloc", feature = "rand_core", feature = "group"))] - fn multiply_double_and_compress_1024_random_points() { - use ff::Field; - use group::Group; - let mut rng = OsRng; - - let mut scalars: Vec = (0..1024) - .map(|_| Scalar::try_from_rng(&mut rng).unwrap()) - .collect(); - scalars[500] = Scalar::ZERO; - - let mut points: Vec = (0..1024) - .map(|_| RistrettoPoint::try_from_rng(&mut rng).unwrap()) - .collect(); - points[500] = ::identity(); - - let multiplied_points: Vec = scalars - .iter() - .zip(&points) - .map(|(scalar, point)| scalar.div_by_2() * point) - .collect(); - let compressed = RistrettoPoint::double_and_compress_batch(&multiplied_points); - - for ((s, P), P2_compressed) in scalars.iter().zip(points).zip(compressed) { - assert_eq!(P2_compressed, (s * P).compress()); + #[cfg(feature = "group")] + proptest! { + #[test] + fn multiply_double_and_compress_random_points( + p1 in any::<[u8; 64]>(), + p2 in any::<[u8; 64]>(), + s1 in any::<[u8; 32]>(), + s2 in any::<[u8; 32]>(), + ) { + use group::Group; + + let scalars = [ + Scalar::from_bytes_mod_order(s1), + Scalar::ZERO, + Scalar::from_bytes_mod_order(s2), + ]; + + let points = [ + RistrettoPoint::from_uniform_bytes(&p1), + ::identity(), + RistrettoPoint::from_uniform_bytes(&p2), + ]; + + let multiplied_points: [_; 3] = + core::array::from_fn(|i| scalars[i].div_by_2() * points[i]); + let compressed = RistrettoPoint::double_and_compress_batch(&multiplied_points); + + for ((s, P), P2_compressed) in scalars.iter().zip(points).zip(compressed) { + prop_assert_eq!(P2_compressed, (s * P).compress()); + } } }