From 143648e812c5566f5b58393366c2db95b88261ae Mon Sep 17 00:00:00 2001 From: daxpedda Date: Tue, 29 Jul 2025 22:17:12 +0200 Subject: [PATCH 1/2] Use `AffinePoint` where appropriate --- ed448-goldilocks/README.md | 7 +- ed448-goldilocks/src/edwards.rs | 4 +- ed448-goldilocks/src/edwards/affine.rs | 281 ++++++++++++++++++- ed448-goldilocks/src/edwards/extended.rs | 301 ++------------------- ed448-goldilocks/src/sign.rs | 8 +- ed448-goldilocks/src/sign/expanded.rs | 11 +- ed448-goldilocks/src/sign/signing_key.rs | 2 +- ed448-goldilocks/src/sign/verifying_key.rs | 7 +- 8 files changed, 332 insertions(+), 289 deletions(-) diff --git a/ed448-goldilocks/README.md b/ed448-goldilocks/README.md index 1cd23167e..98a5e2494 100644 --- a/ed448-goldilocks/README.md +++ b/ed448-goldilocks/README.md @@ -19,6 +19,7 @@ It is intended to be portable, fast, and safe. ```rust use ed448_goldilocks::{Ed448, EdwardsPoint, CompressedEdwardsY, EdwardsScalar, sha3::Shake256}; use elliptic_curve::Field; +use elliptic_curve::group::GroupEncoding; use hash2curve::{ExpandMsgXof, GroupDigest}; use rand_core::OsRng; @@ -29,9 +30,9 @@ assert_eq!(public_key, EdwardsPoint::GENERATOR + EdwardsPoint::GENERATOR); let secret_key = EdwardsScalar::try_from_rng(&mut OsRng).unwrap(); let public_key = EdwardsPoint::GENERATOR * &secret_key; -let compressed_public_key = public_key.compress(); +let compressed_public_key = public_key.to_bytes(); -assert_eq!(compressed_public_key.to_bytes().len(), 57); +assert_eq!(compressed_public_key.len(), 57); let hashed_scalar = Ed448::hash_to_scalar::>(&[b"test"], &[b"edwards448_XOF:SHAKE256_ELL2_RO_"]).unwrap(); let input = hex_literal::hex!("c8c6c8f584e0c25efdb6af5ad234583c56dedd7c33e0c893468e96740fa0cf7f1a560667da40b7bde340a39252e89262fcf707d1180fd43400"); @@ -40,7 +41,7 @@ assert_eq!(hashed_scalar, expected_scalar); let hashed_point = Ed448::hash_from_bytes::>(&[b"test"], &[b"edwards448_XOF:SHAKE256_ELL2_RO_"]).unwrap(); let expected = hex_literal::hex!("d15c4427b5c5611a53593c2be611fd3635b90272d331c7e6721ad3735e95dd8b9821f8e4e27501ce01aa3c913114052dce2e91e8ca050f4980"); -let expected_point = CompressedEdwardsY(expected).decompress().unwrap(); +let expected_point = CompressedEdwardsY(expected).decompress().unwrap().to_edwards(); assert_eq!(hashed_point, expected_point); let hashed_point = EdwardsPoint::hash_with_defaults(b"test"); diff --git a/ed448-goldilocks/src/edwards.rs b/ed448-goldilocks/src/edwards.rs index 4d5245ca8..5d9e0fe11 100644 --- a/ed448-goldilocks/src/edwards.rs +++ b/ed448-goldilocks/src/edwards.rs @@ -12,6 +12,6 @@ pub(crate) mod affine; pub(crate) mod extended; mod scalar; -pub use affine::AffinePoint; -pub use extended::{CompressedEdwardsY, EdwardsPoint}; +pub use affine::{AffinePoint, CompressedEdwardsY}; +pub use extended::EdwardsPoint; pub use scalar::{EdwardsScalar, EdwardsScalarBytes, WideEdwardsScalarBytes}; diff --git a/ed448-goldilocks/src/edwards/affine.rs b/ed448-goldilocks/src/edwards/affine.rs index acaf5f8b0..0762bea1c 100644 --- a/ed448-goldilocks/src/edwards/affine.rs +++ b/ed448-goldilocks/src/edwards/affine.rs @@ -1,8 +1,10 @@ use crate::field::FieldElement; use crate::*; +use core::fmt::{Display, Formatter, LowerHex, Result as FmtResult, UpperHex}; use core::ops::Mul; -use elliptic_curve::{Error, Result, point::NonIdentity, zeroize::DefaultIsZeroes}; -use subtle::{Choice, ConditionallySelectable, ConstantTimeEq}; +use elliptic_curve::{Error, point::NonIdentity, zeroize::DefaultIsZeroes}; +use rand_core::TryRngCore; +use subtle::{Choice, ConditionallyNegatable, ConditionallySelectable, ConstantTimeEq, CtOption}; /// Affine point on untwisted curve #[derive(Copy, Clone, Debug)] @@ -69,6 +71,21 @@ impl AffinePoint { y: FieldElement::ONE, }; + /// Generate a random [`AffinePoint`]. + pub fn try_from_rng(rng: &mut R) -> Result + where + R: TryRngCore + ?Sized, + { + let mut bytes = CompressedEdwardsY::default(); + + loop { + rng.try_fill_bytes(&mut bytes.0)?; + if let Some(point) = bytes.decompress().into() { + return Ok(point); + } + } + } + pub(crate) fn isogeny(&self) -> Self { let x = self.x; let y = self.y; @@ -100,6 +117,33 @@ impl AffinePoint { } } + /// Standard compression; store Y and sign of X + pub fn compress(&self) -> CompressedEdwardsY { + let affine_x = self.x; + let affine_y = self.y; + + let mut compressed_bytes = [0u8; 57]; + + let sign = affine_x.is_negative().unwrap_u8(); + + let y_bytes = affine_y.to_bytes(); + compressed_bytes[..y_bytes.len()].copy_from_slice(&y_bytes[..]); + *compressed_bytes.last_mut().expect("at least one byte") = sign << 7; + CompressedEdwardsY(compressed_bytes) + } + + /// Check if this point is on the curve + pub fn is_on_curve(&self) -> Choice { + // X^2 + Y^2 == 1 + D * X^2 * Y^2 + + let XX = self.x.square(); + let YY = self.y.square(); + let lhs = YY + XX; + let rhs = FieldElement::ONE + FieldElement::EDWARDS_D * XX * YY; + + lhs.ct_eq(&rhs) + } + /// Convert to edwards extended point pub fn to_edwards(&self) -> EdwardsPoint { EdwardsPoint { @@ -130,7 +174,7 @@ impl From> for AffinePoint { impl TryFrom for NonIdentity { type Error = Error; - fn try_from(affine_point: AffinePoint) -> Result { + fn try_from(affine_point: AffinePoint) -> Result { NonIdentity::new(affine_point).into_option().ok_or(Error) } } @@ -149,3 +193,234 @@ define_mul_variants!( RHS = EdwardsScalar, Output = EdwardsPoint ); + +/// The compressed internal representation of a point on the Twisted Edwards Curve +pub type PointBytes = [u8; 57]; + +/// Represents a point on the Compressed Twisted Edwards Curve +/// in little endian format where the most significant bit is the sign bit +/// and the remaining 448 bits represent the y-coordinate +#[derive(Copy, Clone, Debug)] +pub struct CompressedEdwardsY(pub PointBytes); + +impl elliptic_curve::zeroize::Zeroize for CompressedEdwardsY { + fn zeroize(&mut self) { + self.0.zeroize() + } +} + +impl Display for CompressedEdwardsY { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + for b in &self.0[..] { + write!(f, "{b:02x}")?; + } + Ok(()) + } +} + +impl LowerHex for CompressedEdwardsY { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + for b in &self.0[..] { + write!(f, "{b:02x}")?; + } + Ok(()) + } +} + +impl UpperHex for CompressedEdwardsY { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + for b in &self.0[..] { + write!(f, "{b:02X}")?; + } + Ok(()) + } +} + +impl Default for CompressedEdwardsY { + fn default() -> Self { + Self([0u8; 57]) + } +} + +impl ConditionallySelectable for CompressedEdwardsY { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + let mut bytes = [0u8; 57]; + for (i, byte) in bytes.iter_mut().enumerate() { + *byte = u8::conditional_select(&a.0[i], &b.0[i], choice); + } + Self(bytes) + } +} + +impl ConstantTimeEq for CompressedEdwardsY { + fn ct_eq(&self, other: &Self) -> Choice { + self.0.ct_eq(&other.0) + } +} + +impl PartialEq for CompressedEdwardsY { + fn eq(&self, other: &CompressedEdwardsY) -> bool { + self.ct_eq(other).into() + } +} + +impl Eq for CompressedEdwardsY {} + +impl AsRef<[u8]> for CompressedEdwardsY { + fn as_ref(&self) -> &[u8] { + &self.0[..] + } +} + +impl AsRef for CompressedEdwardsY { + fn as_ref(&self) -> &PointBytes { + &self.0 + } +} + +#[cfg(feature = "alloc")] +impl From for Vec { + fn from(value: CompressedEdwardsY) -> Self { + Self::from(&value) + } +} + +#[cfg(feature = "alloc")] +impl From<&CompressedEdwardsY> for Vec { + fn from(value: &CompressedEdwardsY) -> Self { + value.0.to_vec() + } +} + +#[cfg(feature = "alloc")] +impl TryFrom> for CompressedEdwardsY { + type Error = &'static str; + + fn try_from(value: Vec) -> Result { + Self::try_from(&value) + } +} + +#[cfg(feature = "alloc")] +impl TryFrom<&Vec> for CompressedEdwardsY { + type Error = &'static str; + + fn try_from(value: &Vec) -> Result { + Self::try_from(value.as_slice()) + } +} + +impl TryFrom<&[u8]> for CompressedEdwardsY { + type Error = &'static str; + + fn try_from(value: &[u8]) -> Result { + let bytes = ::try_from(value).map_err(|_| "Invalid length")?; + Ok(CompressedEdwardsY(bytes)) + } +} + +#[cfg(feature = "alloc")] +impl TryFrom> for CompressedEdwardsY { + type Error = &'static str; + + fn try_from(value: Box<[u8]>) -> Result { + Self::try_from(value.as_ref()) + } +} + +impl From for PointBytes { + fn from(value: CompressedEdwardsY) -> Self { + value.0 + } +} + +impl From<&CompressedEdwardsY> for PointBytes { + fn from(value: &CompressedEdwardsY) -> Self { + Self::from(*value) + } +} + +#[cfg(feature = "serde")] +impl serdect::serde::Serialize for CompressedEdwardsY { + fn serialize(&self, s: S) -> Result { + serdect::array::serialize_hex_lower_or_bin(&self.0, s) + } +} + +#[cfg(feature = "serde")] +impl<'de> serdect::serde::Deserialize<'de> for CompressedEdwardsY { + fn deserialize(d: D) -> Result + where + D: serdect::serde::Deserializer<'de>, + { + let mut arr = [0u8; 57]; + serdect::array::deserialize_hex_or_bin(&mut arr, d)?; + Ok(CompressedEdwardsY(arr)) + } +} + +impl From for CompressedEdwardsY { + fn from(point: PointBytes) -> Self { + Self(point) + } +} + +impl CompressedEdwardsY { + /// The compressed generator point + pub const GENERATOR: Self = Self([ + 20, 250, 48, 242, 91, 121, 8, 152, 173, 200, 215, 78, 44, 19, 189, 253, 196, 57, 124, 230, + 28, 255, 211, 58, 215, 194, 160, 5, 30, 156, 120, 135, 64, 152, 163, 108, 115, 115, 234, + 75, 98, 199, 201, 86, 55, 32, 118, 136, 36, 188, 182, 110, 113, 70, 63, 105, 0, + ]); + /// The compressed identity point + pub const IDENTITY: Self = Self([0u8; 57]); + + /// Attempt to decompress to an `AffinePoint`. + /// + /// Returns `None` if the input is not the \\(y\\)-coordinate of a + /// curve point. + pub fn decompress_unchecked(&self) -> CtOption { + // Safe to unwrap here as the underlying data structure is a slice + let (sign, b) = self.0.split_last().expect("slice is non-empty"); + + let mut y_bytes: [u8; 56] = [0; 56]; + y_bytes.copy_from_slice(b); + + // Recover x using y + let y = FieldElement::from_bytes(&y_bytes); + let yy = y.square(); + let dyy = FieldElement::EDWARDS_D * yy; + let numerator = FieldElement::ONE - yy; + let denominator = FieldElement::ONE - dyy; + + let (mut x, is_res) = FieldElement::sqrt_ratio(&numerator, &denominator); + + // Compute correct sign of x + let compressed_sign_bit = Choice::from(sign >> 7); + let is_negative = x.is_negative(); + x.conditional_negate(compressed_sign_bit ^ is_negative); + + CtOption::new(AffinePoint { x, y }, is_res) + } + + /// Attempt to decompress to an `AffinePoint`. + /// + /// Returns `None`: + /// - if the input is not the \\(y\\)-coordinate of a curve point. + /// - if the input point is not on the curve. + /// - if the input point has nonzero torsion component. + pub fn decompress(&self) -> CtOption { + self.decompress_unchecked() + .and_then(|pt| CtOption::new(pt, pt.is_on_curve() & pt.to_edwards().is_torsion_free())) + } + + /// View this `CompressedEdwardsY` as an array of bytes. + pub const fn as_bytes(&self) -> &PointBytes { + &self.0 + } + + /// Copy this `CompressedEdwardsY` to an array of bytes. + pub const fn to_bytes(&self) -> PointBytes { + self.0 + } +} diff --git a/ed448-goldilocks/src/edwards/extended.rs b/ed448-goldilocks/src/edwards/extended.rs index 1779fcb8d..86a22021d 100644 --- a/ed448-goldilocks/src/edwards/extended.rs +++ b/ed448-goldilocks/src/edwards/extended.rs @@ -6,6 +6,7 @@ use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use crate::curve::scalar_mul::variable_base; use crate::curve::twedwards::IsogenyMap; use crate::curve::twedwards::extended::ExtendedPoint as TwistedExtendedPoint; +use crate::edwards::affine::PointBytes; use crate::field::FieldElement; use crate::*; use elliptic_curve::{ @@ -17,244 +18,13 @@ use elliptic_curve::{ }; use hash2curve::ExpandMsgXof; use rand_core::TryRngCore; -use subtle::{Choice, ConditionallyNegatable, ConditionallySelectable, ConstantTimeEq, CtOption}; +use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; /// The default hash to curve domain separation tag pub const DEFAULT_HASH_TO_CURVE_SUITE: &[u8] = b"edwards448_XOF:SHAKE256_ELL2_RO_"; /// The default encode to curve domain separation tag pub const DEFAULT_ENCODE_TO_CURVE_SUITE: &[u8] = b"edwards448_XOF:SHAKE256_ELL2_NU_"; -/// The compressed internal representation of a point on the Twisted Edwards Curve -pub type PointBytes = [u8; 57]; - -/// Represents a point on the Compressed Twisted Edwards Curve -/// in little endian format where the most significant bit is the sign bit -/// and the remaining 448 bits represent the y-coordinate -#[derive(Copy, Clone, Debug)] -pub struct CompressedEdwardsY(pub PointBytes); - -impl elliptic_curve::zeroize::Zeroize for CompressedEdwardsY { - fn zeroize(&mut self) { - self.0.zeroize() - } -} - -impl Display for CompressedEdwardsY { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - for b in &self.0[..] { - write!(f, "{b:02x}")?; - } - Ok(()) - } -} - -impl LowerHex for CompressedEdwardsY { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - for b in &self.0[..] { - write!(f, "{b:02x}")?; - } - Ok(()) - } -} - -impl UpperHex for CompressedEdwardsY { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - for b in &self.0[..] { - write!(f, "{b:02X}")?; - } - Ok(()) - } -} - -impl Default for CompressedEdwardsY { - fn default() -> Self { - Self([0u8; 57]) - } -} - -impl ConditionallySelectable for CompressedEdwardsY { - fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { - let mut bytes = [0u8; 57]; - for (i, byte) in bytes.iter_mut().enumerate() { - *byte = u8::conditional_select(&a.0[i], &b.0[i], choice); - } - Self(bytes) - } -} - -impl ConstantTimeEq for CompressedEdwardsY { - fn ct_eq(&self, other: &Self) -> Choice { - self.0.ct_eq(&other.0) - } -} - -impl PartialEq for CompressedEdwardsY { - fn eq(&self, other: &CompressedEdwardsY) -> bool { - self.ct_eq(other).into() - } -} - -impl Eq for CompressedEdwardsY {} - -impl AsRef<[u8]> for CompressedEdwardsY { - fn as_ref(&self) -> &[u8] { - &self.0[..] - } -} - -impl AsRef for CompressedEdwardsY { - fn as_ref(&self) -> &PointBytes { - &self.0 - } -} - -#[cfg(feature = "alloc")] -impl From for Vec { - fn from(value: CompressedEdwardsY) -> Self { - Self::from(&value) - } -} - -#[cfg(feature = "alloc")] -impl From<&CompressedEdwardsY> for Vec { - fn from(value: &CompressedEdwardsY) -> Self { - value.0.to_vec() - } -} - -#[cfg(feature = "alloc")] -impl TryFrom> for CompressedEdwardsY { - type Error = &'static str; - - fn try_from(value: Vec) -> Result { - Self::try_from(&value) - } -} - -#[cfg(feature = "alloc")] -impl TryFrom<&Vec> for CompressedEdwardsY { - type Error = &'static str; - - fn try_from(value: &Vec) -> Result { - Self::try_from(value.as_slice()) - } -} - -impl TryFrom<&[u8]> for CompressedEdwardsY { - type Error = &'static str; - - fn try_from(value: &[u8]) -> Result { - let bytes = ::try_from(value).map_err(|_| "Invalid length")?; - Ok(CompressedEdwardsY(bytes)) - } -} - -#[cfg(feature = "alloc")] -impl TryFrom> for CompressedEdwardsY { - type Error = &'static str; - - fn try_from(value: Box<[u8]>) -> Result { - Self::try_from(value.as_ref()) - } -} - -impl From for PointBytes { - fn from(value: CompressedEdwardsY) -> Self { - value.0 - } -} - -impl From<&CompressedEdwardsY> for PointBytes { - fn from(value: &CompressedEdwardsY) -> Self { - Self::from(*value) - } -} - -#[cfg(feature = "serde")] -impl serdect::serde::Serialize for CompressedEdwardsY { - fn serialize(&self, s: S) -> Result { - serdect::array::serialize_hex_lower_or_bin(&self.0, s) - } -} - -#[cfg(feature = "serde")] -impl<'de> serdect::serde::Deserialize<'de> for CompressedEdwardsY { - fn deserialize(d: D) -> Result - where - D: serdect::serde::Deserializer<'de>, - { - let mut arr = [0u8; 57]; - serdect::array::deserialize_hex_or_bin(&mut arr, d)?; - Ok(CompressedEdwardsY(arr)) - } -} - -impl From for CompressedEdwardsY { - fn from(point: PointBytes) -> Self { - Self(point) - } -} - -impl CompressedEdwardsY { - /// The compressed generator point - pub const GENERATOR: Self = Self([ - 20, 250, 48, 242, 91, 121, 8, 152, 173, 200, 215, 78, 44, 19, 189, 253, 196, 57, 124, 230, - 28, 255, 211, 58, 215, 194, 160, 5, 30, 156, 120, 135, 64, 152, 163, 108, 115, 115, 234, - 75, 98, 199, 201, 86, 55, 32, 118, 136, 36, 188, 182, 110, 113, 70, 63, 105, 0, - ]); - /// The compressed identity point - pub const IDENTITY: Self = Self([0u8; 57]); - - /// Attempt to decompress to an `EdwardsPoint`. - /// - /// Returns `None` if the input is not the \\(y\\)-coordinate of a - /// curve point. - pub fn decompress_unchecked(&self) -> CtOption { - // Safe to unwrap here as the underlying data structure is a slice - let (sign, b) = self.0.split_last().expect("slice is non-empty"); - - let mut y_bytes: [u8; 56] = [0; 56]; - y_bytes.copy_from_slice(b); - - // Recover x using y - let y = FieldElement::from_bytes(&y_bytes); - let yy = y.square(); - let dyy = FieldElement::EDWARDS_D * yy; - let numerator = FieldElement::ONE - yy; - let denominator = FieldElement::ONE - dyy; - - let (mut x, is_res) = FieldElement::sqrt_ratio(&numerator, &denominator); - - // Compute correct sign of x - let compressed_sign_bit = Choice::from(sign >> 7); - let is_negative = x.is_negative(); - x.conditional_negate(compressed_sign_bit ^ is_negative); - - CtOption::new(AffinePoint { x, y }.to_edwards(), is_res) - } - - /// Attempt to decompress to an `EdwardsPoint`. - /// - /// Returns `None`: - /// - if the input is not the \\(y\\)-coordinate of a curve point. - /// - if the input point is not on the curve. - /// - if the input point has nonzero torsion component. - pub fn decompress(&self) -> CtOption { - self.decompress_unchecked() - .and_then(|pt| CtOption::new(pt, pt.is_on_curve() & pt.is_torsion_free())) - } - - /// View this `CompressedEdwardsY` as an array of bytes. - pub const fn as_bytes(&self) -> &PointBytes { - &self.0 - } - - /// Copy this `CompressedEdwardsY` to an array of bytes. - pub const fn to_bytes(&self) -> PointBytes { - self.0 - } -} - /// Represent points on the (untwisted) edwards curve using Extended Homogenous Projective Co-ordinates /// (x, y) -> (X/Z, Y/Z, Z, T) /// a = 1, d = -39081 @@ -342,15 +112,10 @@ impl Group for EdwardsPoint { where R: TryRngCore + ?Sized, { - let mut bytes = Array::default(); - loop { - rng.try_fill_bytes(&mut bytes)?; - if let Some(point) = Self::from_bytes(&bytes) - .into_option() - .filter(|&point| point != Self::IDENTITY) - { - return Ok(point); + let point = AffinePoint::try_from_rng(rng)?; + if point != AffinePoint::IDENTITY { + break Ok(point.into()); } } } @@ -378,17 +143,21 @@ impl GroupEncoding for EdwardsPoint { fn from_bytes(bytes: &Self::Repr) -> CtOption { let mut value = [0u8; 57]; value.copy_from_slice(bytes); - CompressedEdwardsY(value).decompress() + CompressedEdwardsY(value) + .decompress() + .map(|point| point.to_edwards()) } fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { let mut value = [0u8; 57]; value.copy_from_slice(bytes); - CompressedEdwardsY(value).decompress() + CompressedEdwardsY(value) + .decompress() + .map(|point| point.to_edwards()) } fn to_bytes(&self) -> Self::Repr { - Self::Repr::from(self.compress().0) + Self::Repr::from(self.to_affine().compress().0) } } @@ -420,7 +189,7 @@ impl From for Vec { #[cfg(feature = "alloc")] impl From<&EdwardsPoint> for Vec { fn from(value: &EdwardsPoint) -> Self { - value.compress().0.to_vec() + value.to_affine().compress().0.to_vec() } } @@ -465,7 +234,11 @@ impl TryFrom for EdwardsPoint { type Error = &'static str; fn try_from(value: PointBytes) -> Result { - Option::::from(CompressedEdwardsY(value).decompress()).ok_or("Invalid point") + CompressedEdwardsY(value) + .decompress() + .into_option() + .map(|point| point.to_edwards()) + .ok_or("Invalid point") } } @@ -479,7 +252,7 @@ impl TryFrom<&PointBytes> for EdwardsPoint { impl From for PointBytes { fn from(value: EdwardsPoint) -> Self { - value.compress().into() + value.to_affine().compress().into() } } @@ -587,24 +360,6 @@ impl EdwardsPoint { result } - /// Standard compression; store Y and sign of X - // XXX: This needs more docs and is `compress` the conventional function name? I think to_bytes/encode is? - pub fn compress(&self) -> CompressedEdwardsY { - let affine = self.to_affine(); - - let affine_x = affine.x; - let affine_y = affine.y; - - let mut compressed_bytes = [0u8; 57]; - - let sign = affine_x.is_negative().unwrap_u8(); - - let y_bytes = affine_y.to_bytes(); - compressed_bytes[..y_bytes.len()].copy_from_slice(&y_bytes[..]); - *compressed_bytes.last_mut().expect("at least one byte") = sign << 7; - CompressedEdwardsY(compressed_bytes) - } - /// Add two points //https://iacr.org/archive/asiacrypt2008/53500329/53500329.pdf (3.1) // These formulas are unified, so for now we can use it for doubling. Will refactor later for speed @@ -938,7 +693,7 @@ impl Mul<&EdwardsScalar> for &EdwardsPoint { #[cfg(feature = "serde")] impl serdect::serde::Serialize for EdwardsPoint { fn serialize(&self, s: S) -> Result { - self.compress().serialize(s) + self.to_affine().compress().serialize(s) } } @@ -949,7 +704,10 @@ impl<'de> serdect::serde::Deserialize<'de> for EdwardsPoint { D: serdect::serde::Deserializer<'de>, { let compressed = CompressedEdwardsY::deserialize(d)?; - Option::::from(compressed.decompress()) + compressed + .decompress() + .into_option() + .map(|point| point.to_edwards()) .ok_or_else(|| serdect::serde::de::Error::custom("invalid point")) } } @@ -961,7 +719,6 @@ mod tests { use super::*; use elliptic_curve::Field; use hex_literal::hex; - use rand_core::TryRngCore; fn hex_to_field(hex: &'static str) -> FieldElement { assert_eq!(hex.len(), 56 * 2); @@ -1038,7 +795,7 @@ mod tests { let y = hex_to_field( "ae05e9634ad7048db359d6205086c2b0036ed7a035884dd7b7e36d728ad8c4b80d6565833a2a3098bbbcb2bed1cda06bdaeafbcdea9386ed", ); - let generated = AffinePoint { x, y }.to_edwards(); + let generated = AffinePoint { x, y }; let decompressed_point = generated.compress().decompress(); assert!(>::into(decompressed_point.is_some())); @@ -1066,13 +823,13 @@ mod tests { let decompressed = compressed.decompress().unwrap(); assert_eq!( - decompressed.X, + decompressed.x, hex_to_field( "39c41cea305d737df00de8223a0d5f4d48c8e098e16e9b4b2f38ac353262e119cb5ff2afd6d02464702d9d01c9921243fc572f9c718e2527" ) ); assert_eq!( - decompressed.Y, + decompressed.y, hex_to_field( "a7ad5629142315c3c03730ab126380eb99a33cf01d06dfc3cf8ca3ae66bde9dc2d6d74f3dd3d05e1d41fd0233f032d967d8909b1536a9c64" ) @@ -1085,13 +842,13 @@ mod tests { let decompressed = compressed.decompress().unwrap(); assert_eq!( - decompressed.X, + decompressed.x, hex_to_field( "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ) ); assert_eq!( - decompressed.Y, + decompressed.y, hex_to_field( "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001" ) diff --git a/ed448-goldilocks/src/sign.rs b/ed448-goldilocks/src/sign.rs index 61862a9c1..530f046af 100644 --- a/ed448-goldilocks/src/sign.rs +++ b/ed448-goldilocks/src/sign.rs @@ -89,6 +89,7 @@ pub use verifying_key::*; use crate::{CompressedEdwardsY, EdwardsPoint, EdwardsScalar}; use elliptic_curve::array::Array; +use elliptic_curve::group::GroupEncoding; /// Length of a secret key in bytes pub const SECRET_KEY_LENGTH: usize = 57; @@ -118,7 +119,7 @@ impl From for Signature { fn from(inner: InnerSignature) -> Self { let mut s = [0u8; SECRET_KEY_LENGTH]; s.copy_from_slice(&inner.s.to_bytes_rfc_8032()); - Self::from_components(inner.r.compress(), s) + Self::from_components(inner.r.to_bytes(), s) } } @@ -129,7 +130,10 @@ impl TryFrom<&Signature> for InnerSignature { let s_bytes: &Array = (signature.s_bytes()).into(); let s = Option::from(EdwardsScalar::from_canonical_bytes(s_bytes)) .ok_or(SigningError::InvalidSignatureSComponent)?; - let r = Option::from(CompressedEdwardsY::from(*signature.r_bytes()).decompress()) + let r = CompressedEdwardsY::from(*signature.r_bytes()) + .decompress() + .into_option() + .map(|point| point.to_edwards()) .ok_or(SigningError::InvalidSignatureRComponent)?; Ok(Self { r, s }) } diff --git a/ed448-goldilocks/src/sign/expanded.rs b/ed448-goldilocks/src/sign/expanded.rs index d4d6cc03d..931649991 100644 --- a/ed448-goldilocks/src/sign/expanded.rs +++ b/ed448-goldilocks/src/sign/expanded.rs @@ -3,7 +3,10 @@ use crate::{ VerifyingKey, WideEdwardsScalarBytes, sign::{HASH_HEAD, InnerSignature}, }; -use elliptic_curve::zeroize::{Zeroize, ZeroizeOnDrop}; +use elliptic_curve::{ + group::GroupEncoding, + zeroize::{Zeroize, ZeroizeOnDrop}, +}; use sha3::{ Shake256, digest::{ExtendableOutput, ExtendableOutputReset, Update, XofReader}, @@ -61,7 +64,7 @@ impl ExpandedSecretKey { let point = EdwardsPoint::GENERATOR * scalar; let public_key = VerifyingKey { - compressed: point.compress(), + compressed: point.to_affine().compress(), point, }; @@ -124,7 +127,7 @@ impl ExpandedSecretKey { // R = r*B let big_r = EdwardsPoint::GENERATOR * r; - let compressed_r = big_r.compress(); + let compressed_r = big_r.to_bytes(); // SHAKE256(dom4(F, C) || R || A || PH(M), 114) -> scalar k reader = Shake256::default() @@ -132,7 +135,7 @@ impl ExpandedSecretKey { .chain([phflag]) .chain([ctx_len]) .chain(ctx) - .chain(compressed_r.as_bytes()) + .chain(compressed_r) .chain(self.public_key.compressed.as_bytes()) .chain(m) .finalize_xof(); diff --git a/ed448-goldilocks/src/sign/signing_key.rs b/ed448-goldilocks/src/sign/signing_key.rs index b82953f54..0cdd23064 100644 --- a/ed448-goldilocks/src/sign/signing_key.rs +++ b/ed448-goldilocks/src/sign/signing_key.rs @@ -14,7 +14,7 @@ use signature::Error; use subtle::{Choice, ConstantTimeEq}; #[cfg(feature = "pkcs8")] -use crate::{PUBLIC_KEY_LENGTH, edwards::extended::PointBytes}; +use crate::{PUBLIC_KEY_LENGTH, edwards::affine::PointBytes}; /// Ed448 secret key as defined in [RFC8032 ยง 5.2.5] /// diff --git a/ed448-goldilocks/src/sign/verifying_key.rs b/ed448-goldilocks/src/sign/verifying_key.rs index e3a73dafb..97933309f 100644 --- a/ed448-goldilocks/src/sign/verifying_key.rs +++ b/ed448-goldilocks/src/sign/verifying_key.rs @@ -1,7 +1,7 @@ //! Much of this code is borrowed from Thomas Pornin's [CRRL Project](https://github.com/pornin/crrl/blob/main/src/ed448.rs) //! and adapted to mirror `ed25519-dalek`'s API. -use crate::edwards::extended::PointBytes; +use crate::edwards::affine::PointBytes; use crate::sign::{HASH_HEAD, InnerSignature}; use crate::{ CompressedEdwardsY, Context, EdwardsPoint, EdwardsScalar, PreHash, Signature, SigningError, @@ -178,7 +178,10 @@ impl VerifyingKey { /// Construct a `VerifyingKey` from a slice of bytes. pub fn from_bytes(bytes: &PointBytes) -> Result { let compressed = CompressedEdwardsY(*bytes); - let point = Option::::from(compressed.decompress()) + let point = compressed + .decompress() + .into_option() + .map(|point| point.to_edwards()) .ok_or(SigningError::InvalidPublicKeyBytes)?; if point.is_identity().into() { return Err(SigningError::InvalidPublicKeyBytes.into()); From e3f2bb42b458321aa5599992ff9f1005eb655763 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Tue, 29 Jul 2025 22:27:37 +0200 Subject: [PATCH 2/2] Implement `BatchNormalize` for `EdwardsPoint` --- ed448-goldilocks/src/edwards/extended.rs | 106 ++++++++++++++++++++++- ed448-goldilocks/src/field/element.rs | 79 ++++++++++++++++- 2 files changed, 181 insertions(+), 4 deletions(-) diff --git a/ed448-goldilocks/src/edwards/extended.rs b/ed448-goldilocks/src/edwards/extended.rs index 86a22021d..d51ff1de1 100644 --- a/ed448-goldilocks/src/edwards/extended.rs +++ b/ed448-goldilocks/src/edwards/extended.rs @@ -10,10 +10,10 @@ use crate::edwards::affine::PointBytes; use crate::field::FieldElement; use crate::*; use elliptic_curve::{ - CurveGroup, Error, + BatchNormalize, CurveGroup, Error, array::Array, group::{Group, GroupEncoding, cofactor::CofactorGroup, prime::PrimeGroup}, - ops::LinearCombination, + ops::{BatchInvert, LinearCombination}, point::NonIdentity, }; use hash2curve::ExpandMsgXof; @@ -296,6 +296,14 @@ impl CurveGroup for EdwardsPoint { fn to_affine(&self) -> AffinePoint { self.to_affine() } + + #[cfg(feature = "alloc")] + #[inline] + fn batch_normalize(projective: &[Self], affine: &mut [Self::AffineRepr]) { + assert_eq!(projective.len(), affine.len()); + let mut zs = alloc::vec![FieldElement::ONE; projective.len()]; + batch_normalize_generic(projective, zs.as_mut_slice(), affine); + } } impl EdwardsPoint { @@ -714,11 +722,76 @@ impl<'de> serdect::serde::Deserialize<'de> for EdwardsPoint { impl elliptic_curve::zeroize::DefaultIsZeroes for EdwardsPoint {} +impl BatchNormalize<[EdwardsPoint; N]> for EdwardsPoint { + type Output = [::AffineRepr; N]; + + #[inline] + fn batch_normalize(points: &[Self; N]) -> [::AffineRepr; N] { + let zs = [FieldElement::ONE; N]; + let mut affine_points = [AffinePoint::IDENTITY; N]; + batch_normalize_generic(points, zs, &mut affine_points); + affine_points + } +} + +#[cfg(feature = "alloc")] +impl BatchNormalize<[EdwardsPoint]> for EdwardsPoint { + type Output = Vec<::AffineRepr>; + + #[inline] + fn batch_normalize(points: &[Self]) -> Vec<::AffineRepr> { + use alloc::vec; + + let mut zs = vec![FieldElement::ONE; points.len()]; + let mut affine_points = vec![AffinePoint::IDENTITY; points.len()]; + batch_normalize_generic(points, zs.as_mut_slice(), &mut affine_points); + affine_points + } +} + +/// Generic implementation of batch normalization. +fn batch_normalize_generic(points: &P, mut zs: Z, out: &mut O) +where + FieldElement: BatchInvert>, + P: AsRef<[EdwardsPoint]> + ?Sized, + Z: AsMut<[FieldElement]>, + I: AsRef<[FieldElement]>, + O: AsMut<[AffinePoint]> + ?Sized, +{ + let points = points.as_ref(); + let out = out.as_mut(); + + for (i, point) in points.iter().enumerate() { + // Even a single zero value will fail inversion for the entire batch. + // Put a dummy value (above `FieldElement::ONE`) so inversion succeeds + // and treat that case specially later-on. + zs.as_mut()[i].conditional_assign(&point.Z, !point.Z.ct_eq(&FieldElement::ZERO)); + } + + // This is safe to unwrap since we assured that all elements are non-zero + let zs_inverses = >::batch_invert(zs) + .expect("all elements should be non-zero"); + + for i in 0..out.len() { + // If the `z` coordinate is non-zero, we can use it to invert; + // otherwise it defaults to the `IDENTITY` value. + out[i] = AffinePoint::conditional_select( + &AffinePoint { + x: points[i].X * zs_inverses.as_ref()[i], + y: points[i].Y * zs_inverses.as_ref()[i], + }, + &AffinePoint::IDENTITY, + points[i].Z.ct_eq(&FieldElement::ZERO), + ); + } +} + #[cfg(test)] mod tests { use super::*; use elliptic_curve::Field; use hex_literal::hex; + use rand_core::OsRng; fn hex_to_field(hex: &'static str) -> FieldElement { assert_eq!(hex.len(), 56 * 2); @@ -1013,4 +1086,33 @@ mod tests { assert_eq!(computed_commitment, expected_commitment); } + + #[test] + fn batch_normalize() { + let points: [EdwardsPoint; 2] = [ + EdwardsPoint::try_from_rng(&mut OsRng).unwrap(), + EdwardsPoint::try_from_rng(&mut OsRng).unwrap(), + ]; + + let affine_points = >::batch_normalize(&points); + + for (point, affine_point) in points.into_iter().zip(affine_points) { + assert_eq!(affine_point, point.to_affine()); + } + } + + #[test] + #[cfg(feature = "alloc")] + fn batch_normalize_alloc() { + let points = alloc::vec![ + EdwardsPoint::try_from_rng(&mut OsRng).unwrap(), + EdwardsPoint::try_from_rng(&mut OsRng).unwrap(), + ]; + + let affine_points = >::batch_normalize(points.as_slice()); + + for (point, affine_point) in points.into_iter().zip(affine_points) { + assert_eq!(affine_point, point.to_affine()); + } + } } diff --git a/ed448-goldilocks/src/field/element.rs b/ed448-goldilocks/src/field/element.rs index 7e96a5367..dd4102c41 100644 --- a/ed448-goldilocks/src/field/element.rs +++ b/ed448-goldilocks/src/field/element.rs @@ -1,22 +1,29 @@ use core::fmt::{self, Debug, Display, Formatter, LowerHex, UpperHex}; +use core::iter::{Product, Sum}; use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use super::ConstMontyType; +use super::{ConstMontyType, MODULUS}; use crate::{ AffinePoint, Decaf448, DecafPoint, Ed448, EdwardsPoint, curve::twedwards::extended::ExtendedPoint as TwistedExtendedPoint, }; use elliptic_curve::{ + Field, array::Array, bigint::{ Integer, NonZero, U448, U704, consts::{U56, U84, U88}, + modular::ConstMontyParams, }, group::cofactor::CofactorGroup, zeroize::DefaultIsZeroes, }; use hash2curve::{FromOkm, MapToCurve}; -use subtle::{Choice, ConditionallyNegatable, ConditionallySelectable, ConstantTimeEq}; +use rand_core::TryRngCore; +use subtle::{ + Choice, ConditionallyNegatable, ConditionallySelectable, ConstantTimeEq, ConstantTimeLess, + CtOption, +}; #[derive(Clone, Copy, Default)] pub struct FieldElement(pub(crate) ConstMontyType); @@ -225,6 +232,68 @@ impl MapToCurve for Decaf448 { } } +impl Sum for FieldElement { + fn sum>(iter: I) -> Self { + iter.reduce(Add::add).unwrap_or(Self::ZERO) + } +} + +impl<'a> Sum<&'a FieldElement> for FieldElement { + fn sum>(iter: I) -> Self { + iter.copied().sum() + } +} + +impl Product for FieldElement { + fn product>(iter: I) -> Self { + iter.reduce(Mul::mul).unwrap_or(Self::ONE) + } +} + +impl<'a> Product<&'a FieldElement> for FieldElement { + fn product>(iter: I) -> Self { + iter.copied().product() + } +} + +impl Field for FieldElement { + const ZERO: Self = Self::ZERO; + const ONE: Self = Self::ONE; + + fn try_from_rng(rng: &mut R) -> Result { + let mut bytes = [0; 56]; + + loop { + rng.try_fill_bytes(&mut bytes)?; + if let Some(fe) = Self::from_repr(&bytes).into() { + return Ok(fe); + } + } + } + + fn square(&self) -> Self { + self.square() + } + + fn double(&self) -> Self { + self.double() + } + + fn invert(&self) -> CtOption { + CtOption::from(self.0.invert()).map(Self) + } + + fn sqrt(&self) -> CtOption { + let sqrt = self.sqrt(); + CtOption::new(sqrt, sqrt.square().ct_eq(self)) + } + + fn sqrt_ratio(num: &Self, div: &Self) -> (Choice, Self) { + let (result, is_square) = Self::sqrt_ratio(num, div); + (is_square, result) + } +} + impl FieldElement { pub const A_PLUS_TWO_OVER_FOUR: Self = Self(ConstMontyType::new(&U448::from_be_hex( "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000098aa", @@ -316,6 +385,12 @@ impl FieldElement { Self(ConstMontyType::new(&U448::from_le_slice(bytes))) } + pub fn from_repr(bytes: &[u8; 56]) -> CtOption { + let integer = U448::from_le_slice(bytes); + let is_some = integer.ct_lt(MODULUS::PARAMS.modulus()); + CtOption::new(Self(ConstMontyType::new(&integer)), is_some) + } + pub fn double(&self) -> Self { Self(self.0.double()) }