diff --git a/curve25519-dalek/benches/dalek_benchmarks.rs b/curve25519-dalek/benches/dalek_benchmarks.rs index 62a6280..0b2653e 100644 --- a/curve25519-dalek/benches/dalek_benchmarks.rs +++ b/curve25519-dalek/benches/dalek_benchmarks.rs @@ -22,6 +22,22 @@ mod edwards_benches { c.bench_function("EdwardsPoint compression", move |b| b.iter(|| B.compress())); } + #[cfg(feature = "alloc")] + fn compress_batch(c: &mut BenchmarkGroup) { + for batch_size in BATCH_SIZES { + c.bench_with_input( + BenchmarkId::new("Batch EdwardsPoint compression", batch_size), + &batch_size, + |b, &size| { + let mut rng = OsRng; + let points: Vec = + (0..size).map(|_| EdwardsPoint::random(&mut rng)).collect(); + b.iter(|| EdwardsPoint::compress_batch(&points)); + }, + ); + } + } + fn decompress(c: &mut BenchmarkGroup) { let B_comp = &constants::ED25519_BASEPOINT_COMPRESSED; c.bench_function("EdwardsPoint decompression", move |b| { @@ -62,6 +78,8 @@ mod edwards_benches { compress(&mut g); decompress(&mut g); + #[cfg(feature = "alloc")] + compress_batch(&mut g); consttime_fixed_base_scalar_mul(&mut g); consttime_variable_base_scalar_mul(&mut g); vartime_double_base_scalar_mul(&mut g); diff --git a/curve25519-dalek/src/edwards.rs b/curve25519-dalek/src/edwards.rs index d6672bd..d740d63 100644 --- a/curve25519-dalek/src/edwards.rs +++ b/curve25519-dalek/src/edwards.rs @@ -93,6 +93,7 @@ // affine and projective cakes and eat both of them too. #![allow(non_snake_case)] +use cfg_if::cfg_if; use core::array::TryFromSliceError; use core::borrow::Borrow; use core::fmt::Debug; @@ -101,8 +102,6 @@ use core::ops::{Add, Neg, Sub}; use core::ops::{AddAssign, SubAssign}; use core::ops::{Mul, MulAssign}; -use cfg_if::cfg_if; - #[cfg(feature = "digest")] use digest::{generic_array::typenum::U64, Digest}; @@ -112,7 +111,7 @@ use { subtle::CtOption, }; -#[cfg(feature = "group")] +#[cfg(any(test, feature = "rand_core"))] use rand_core::RngCore; use subtle::Choice; @@ -151,6 +150,8 @@ use crate::traits::{Identity, IsIdentity}; use crate::traits::MultiscalarMul; #[cfg(feature = "alloc")] use crate::traits::{VartimeMultiscalarMul, VartimePrecomputedMultiscalarMul}; +#[cfg(feature = "alloc")] +use alloc::vec::Vec; // ------------------------------------------------------------------------ // Compressed points @@ -567,9 +568,31 @@ impl EdwardsPoint { let recip = self.Z.invert(); let x = &self.X * &recip; let y = &self.Y * &recip; - let mut s: [u8; 32]; + Self::compress_affine(x, y) + } - s = y.as_bytes(); + /// Compress several `EdwardsPoint`s into `CompressedEdwardsY` format, using a batch inversion + /// for a significant speedup. + #[cfg(feature = "alloc")] + pub fn compress_batch(inputs: &[EdwardsPoint]) -> Vec { + let mut zs = inputs.iter().map(|input| input.Z).collect::>(); + FieldElement::batch_invert(&mut zs); + + inputs + .iter() + .zip(&zs) + .map(|(input, recip)| { + let x = &input.X * recip; + let y = &input.Y * recip; + Self::compress_affine(x, y) + }) + .collect() + } + + /// Compress affine Edwards coordinates into `CompressedEdwardsY` format. + #[inline] + fn compress_affine(x: FieldElement, y: FieldElement) -> CompressedEdwardsY { + let mut s = y.as_bytes(); s[31] ^= x.is_negative().unwrap_u8() << 7; CompressedEdwardsY(s) } @@ -605,6 +628,33 @@ impl EdwardsPoint { .expect("Montgomery conversion to Edwards point in Elligator failed") .mul_by_cofactor() } + + /// Return an `EdwardsPoint` chosen uniformly at random using a user-provided RNG. + /// + /// # Inputs + /// + /// * `rng`: any RNG which implements `RngCore` + /// + /// # Returns + /// + /// A random `EdwardsPoint`. + /// + /// # Implementation + /// + /// Uses rejection sampling, generating a random `CompressedEdwardsY` and then attempting point + /// decompression, rejecting invalid points. + #[cfg(any(test, feature = "rand_core"))] + pub fn random(mut rng: impl RngCore) -> Self { + let mut repr = CompressedEdwardsY([0u8; 32]); + loop { + rng.fill_bytes(&mut repr.0); + if let Some(p) = repr.decompress() { + if !IsIdentity::is_identity(&p) { + break p; + } + } + } + } } // ------------------------------------------------------------------------ @@ -1291,16 +1341,9 @@ impl Debug for EdwardsPoint { impl group::Group for EdwardsPoint { type Scalar = Scalar; - fn random(mut rng: impl RngCore) -> Self { - let mut repr = CompressedEdwardsY([0u8; 32]); - loop { - rng.fill_bytes(&mut repr.0); - if let Some(p) = repr.decompress() { - if !IsIdentity::is_identity(&p) { - break p; - } - } - } + fn random(rng: impl RngCore) -> Self { + // Call the inherent `pub fn random` defined above + Self::random(rng) } fn identity() -> Self { @@ -2019,6 +2062,31 @@ mod test { EdwardsPoint::identity().compress(), CompressedEdwardsY::identity() ); + + #[cfg(feature = "alloc")] + { + let compressed = EdwardsPoint::compress_batch(&[EdwardsPoint::identity()]); + assert_eq!(&compressed, &[CompressedEdwardsY::identity()]); + } + } + + #[cfg(feature = "alloc")] + #[test] + fn compress_batch() { + let mut rng = rand::thread_rng(); + + // TODO(tarcieri): proptests? + // Make some points deterministically then randomly + let mut points = (1u64..16) + .map(|n| constants::ED25519_BASEPOINT_POINT * Scalar::from(n)) + .collect::>(); + points.extend(core::iter::repeat_with(|| EdwardsPoint::random(&mut rng)).take(100)); + let compressed = EdwardsPoint::compress_batch(&points); + + // Check that the batch-compressed points match the individually compressed ones + for (point, compressed) in points.iter().zip(&compressed) { + assert_eq!(&point.compress(), compressed); + } } #[test]