From da190759a9562d7e6e874738fb1291fc7a5dca04 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Thu, 28 Aug 2025 21:56:28 -0400 Subject: [PATCH] Move embedwards25519 over to short-weierstrass --- Cargo.lock | 11 +- crypto/ciphersuite/Cargo.toml | 2 + crypto/ciphersuite/src/lib.rs | 10 +- crypto/dkg/evrf/Cargo.toml | 12 +- crypto/dkg/evrf/src/proof/tape.rs | 6 +- crypto/evrf/embedwards25519/Cargo.toml | 10 +- crypto/evrf/embedwards25519/src/lib.rs | 115 ++++-- crypto/evrf/embedwards25519/src/point.rs | 441 --------------------- crypto/evrf/secq256k1/Cargo.toml | 4 +- crypto/evrf/secq256k1/src/lib.rs | 7 +- crypto/evrf/secq256k1/src/point.rs | 5 +- crypto/prime-field/Cargo.toml | 2 +- crypto/short-weierstrass/Cargo.toml | 7 +- crypto/short-weierstrass/src/affine.rs | 47 ++- crypto/short-weierstrass/src/lib.rs | 18 + crypto/short-weierstrass/src/projective.rs | 90 ++++- patches/ciphersuite/Cargo.toml | 1 + 17 files changed, 252 insertions(+), 536 deletions(-) delete mode 100644 crypto/evrf/embedwards25519/src/point.rs diff --git a/Cargo.lock b/Cargo.lock index 8112abef..b85dabde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2875,7 +2875,7 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ec-divisors" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide?rev=59e3ae73b51c214afbc304efca8d748b3da62977#59e3ae73b51c214afbc304efca8d748b3da62977" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=a6f8797007e768488568b821435cf5006517a962#a6f8797007e768488568b821435cf5006517a962" dependencies = [ "dalek-ff-group", "ff", @@ -2990,7 +2990,6 @@ dependencies = [ "ciphersuite 0.4.2", "curve25519-dalek", "dalek-ff-group", - "ec-divisors", "ff-group-tests", "generalized-bulletproofs-ec-gadgets", "generic-array 1.2.0", @@ -3728,7 +3727,7 @@ dependencies = [ [[package]] name = "generalized-bulletproofs" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide?rev=59e3ae73b51c214afbc304efca8d748b3da62977#59e3ae73b51c214afbc304efca8d748b3da62977" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=a6f8797007e768488568b821435cf5006517a962#a6f8797007e768488568b821435cf5006517a962" dependencies = [ "blake2", "ciphersuite 0.4.99", @@ -3743,7 +3742,7 @@ dependencies = [ [[package]] name = "generalized-bulletproofs-circuit-abstraction" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide?rev=59e3ae73b51c214afbc304efca8d748b3da62977#59e3ae73b51c214afbc304efca8d748b3da62977" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=a6f8797007e768488568b821435cf5006517a962#a6f8797007e768488568b821435cf5006517a962" dependencies = [ "ciphersuite 0.4.99", "generalized-bulletproofs", @@ -3754,7 +3753,7 @@ dependencies = [ [[package]] name = "generalized-bulletproofs-ec-gadgets" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide?rev=59e3ae73b51c214afbc304efca8d748b3da62977#59e3ae73b51c214afbc304efca8d748b3da62977" +source = "git+https://github.com/monero-oxide/monero-oxide?rev=a6f8797007e768488568b821435cf5006517a962#a6f8797007e768488568b821435cf5006517a962" dependencies = [ "ciphersuite 0.4.99", "generalized-bulletproofs-circuit-abstraction", @@ -10927,7 +10926,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" name = "short-weierstrass" version = "0.1.0" dependencies = [ + "ec-divisors", "ff", + "generic-array 1.2.0", "group", "rand_core 0.6.4", "subtle", diff --git a/crypto/ciphersuite/Cargo.toml b/crypto/ciphersuite/Cargo.toml index f86155aa..39217a49 100644 --- a/crypto/ciphersuite/Cargo.toml +++ b/crypto/ciphersuite/Cargo.toml @@ -40,6 +40,8 @@ ff-group-tests = { version = "0.13", path = "../ff-group-tests" } [features] alloc = ["std-shims"] std = [ + "alloc", + "std-shims/std", "rand_core/std", diff --git a/crypto/ciphersuite/src/lib.rs b/crypto/ciphersuite/src/lib.rs index 93f39c66..701cafb5 100644 --- a/crypto/ciphersuite/src/lib.rs +++ b/crypto/ciphersuite/src/lib.rs @@ -3,10 +3,10 @@ #![cfg_attr(not(feature = "std"), no_std)] use core::fmt::Debug; -#[cfg(any(feature = "alloc", feature = "std"))] +#[cfg(feature = "alloc")] #[allow(unused_imports)] use std_shims::prelude::*; -#[cfg(any(feature = "alloc", feature = "std"))] +#[cfg(feature = "alloc")] use std_shims::io::{self, Read}; use rand_core::{RngCore, CryptoRng}; @@ -23,7 +23,7 @@ use group::{ Group, GroupOps, prime::PrimeGroup, }; -#[cfg(any(feature = "alloc", feature = "std"))] +#[cfg(feature = "alloc")] use group::GroupEncoding; /// Unified trait defining a ciphersuite around an elliptic curve. @@ -69,7 +69,7 @@ pub trait Ciphersuite: } /// Read a canonical scalar from something implementing std::io::Read. - #[cfg(any(feature = "alloc", feature = "std"))] + #[cfg(feature = "alloc")] #[allow(non_snake_case)] fn read_F(reader: &mut R) -> io::Result { let mut encoding = ::Repr::default(); @@ -86,7 +86,7 @@ pub trait Ciphersuite: /// /// The provided implementation is safe so long as `GroupEncoding::to_bytes` always returns a /// canonical serialization. - #[cfg(any(feature = "alloc", feature = "std"))] + #[cfg(feature = "alloc")] #[allow(non_snake_case)] fn read_G(reader: &mut R) -> io::Result { let mut encoding = ::Repr::default(); diff --git a/crypto/dkg/evrf/Cargo.toml b/crypto/dkg/evrf/Cargo.toml index 739be1da..4c69f349 100644 --- a/crypto/dkg/evrf/Cargo.toml +++ b/crypto/dkg/evrf/Cargo.toml @@ -34,17 +34,17 @@ generic-array = { version = "1", default-features = false, features = ["alloc"] blake2 = { version = "0.10", default-features = false } rand_chacha = { version = "0.3", default-features = false } -generalized-bulletproofs = { git = "https://github.com/monero-oxide/monero-oxide", rev = "59e3ae73b51c214afbc304efca8d748b3da62977", default-features = false } -ec-divisors = { git = "https://github.com/monero-oxide/monero-oxide", rev = "59e3ae73b51c214afbc304efca8d748b3da62977", default-features = false } -generalized-bulletproofs-circuit-abstraction = { git = "https://github.com/monero-oxide/monero-oxide", rev = "59e3ae73b51c214afbc304efca8d748b3da62977", default-features = false } -generalized-bulletproofs-ec-gadgets = { git = "https://github.com/monero-oxide/monero-oxide", rev = "59e3ae73b51c214afbc304efca8d748b3da62977", default-features = false } +generalized-bulletproofs = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a6f8797007e768488568b821435cf5006517a962", default-features = false } +ec-divisors = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a6f8797007e768488568b821435cf5006517a962", default-features = false } +generalized-bulletproofs-circuit-abstraction = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a6f8797007e768488568b821435cf5006517a962", default-features = false } +generalized-bulletproofs-ec-gadgets = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a6f8797007e768488568b821435cf5006517a962", default-features = false } dkg = { path = ".." } ciphersuite-kp256 = { path = "../../ciphersuite/kp256", default-features = false, optional = true } secq256k1 = { path = "../../evrf/secq256k1", optional = true } dalek-ff-group = { path = "../../dalek-ff-group", default-features = false, optional = true } -embedwards25519 = { path = "../../evrf/embedwards25519", optional = true } +embedwards25519 = { path = "../../evrf/embedwards25519", default-features = false, features = ["alloc"], optional = true } [dev-dependencies] rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } @@ -52,7 +52,7 @@ rand = { version = "0.8", default-features = false, features = ["std"] } ciphersuite = { path = "../../ciphersuite", default-features = false, features = ["std"] } embedwards25519 = { path = "../../evrf/embedwards25519", default-features = false, features = ["std"] } dalek-ff-group = { path = "../../dalek-ff-group", default-features = false, features = ["std"] } -generalized-bulletproofs = { git = "https://github.com/monero-oxide/monero-oxide", rev = "59e3ae73b51c214afbc304efca8d748b3da62977", features = ["tests"] } +generalized-bulletproofs = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a6f8797007e768488568b821435cf5006517a962", features = ["tests"] } dkg-recovery = { path = "../recovery" } [features] diff --git a/crypto/dkg/evrf/src/proof/tape.rs b/crypto/dkg/evrf/src/proof/tape.rs index 3d39b735..50afb7ab 100644 --- a/crypto/dkg/evrf/src/proof/tape.rs +++ b/crypto/dkg/evrf/src/proof/tape.rs @@ -3,7 +3,7 @@ use core::marker::PhantomData; use generic_array::{sequence::GenericSequence, ArrayLength, GenericArray}; use generalized_bulletproofs_circuit_abstraction::Variable; -use generalized_bulletproofs_ec_gadgets::{DiscreteLogParameters, Divisor, PointWithDlog}; +use generalized_bulletproofs_ec_gadgets::{DiscreteLogParameter, Divisor, PointWithDlog}; use crate::Curves; @@ -50,11 +50,11 @@ impl Tape { Note the `x` coefficients are only from the power of two, and `i >= 1`. */ let dlog = - self.read_from_tape::<::ScalarBits>(); + self.read_from_tape::<::ScalarBits>(); struct PointIterator<'a, C: Curves>( &'a mut Tape, - GenericArray::ScalarBits>, + GenericArray::ScalarBits>, PhantomData, ); impl<'a, C: Curves> Iterator for PointIterator<'a, C> { diff --git a/crypto/evrf/embedwards25519/Cargo.toml b/crypto/evrf/embedwards25519/Cargo.toml index 55a979cf..baac48af 100644 --- a/crypto/evrf/embedwards25519/Cargo.toml +++ b/crypto/evrf/embedwards25519/Cargo.toml @@ -23,12 +23,14 @@ zeroize = { version = "^1.5", default-features = false, features = ["zeroize_der generic-array = { version = "1", default-features = false } prime-field = { path = "../../prime-field", default-features = false } +short-weierstrass = { path = "../../short-weierstrass", default-features = false } +curve25519-dalek = { version = "4", default-features = false, features = ["legacy_compatibility"] } dalek-ff-group = { path = "../../dalek-ff-group", version = "0.4", default-features = false } blake2 = { version = "0.10", default-features = false } ciphersuite = { path = "../../ciphersuite", version = "0.4", default-features = false } -ec-divisors = { git = "https://github.com/monero-oxide/monero-oxide", rev = "59e3ae73b51c214afbc304efca8d748b3da62977", default-features = false } -generalized-bulletproofs-ec-gadgets = { git = "https://github.com/monero-oxide/monero-oxide", rev = "59e3ae73b51c214afbc304efca8d748b3da62977", default-features = false } + +generalized-bulletproofs-ec-gadgets = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a6f8797007e768488568b821435cf5006517a962", default-features = false, optional = true } [dev-dependencies] hex = "0.4" @@ -38,6 +40,6 @@ rand_core = { version = "0.6", features = ["std"] } ff-group-tests = { path = "../../ff-group-tests" } [features] -alloc = ["std-shims", "zeroize/alloc", "prime-field/alloc", "ciphersuite/alloc"] -std = ["std-shims/std", "zeroize/std", "prime-field/std", "blake2/std", "ciphersuite/std", "ec-divisors/std", "generalized-bulletproofs-ec-gadgets/std"] +alloc = ["std-shims", "zeroize/alloc", "prime-field/alloc", "short-weierstrass/alloc", "curve25519-dalek/alloc", "ciphersuite/alloc", "generalized-bulletproofs-ec-gadgets"] +std = ["alloc", "std-shims/std", "zeroize/std", "prime-field/std", "short-weierstrass/std", "blake2/std", "ciphersuite/std", "generalized-bulletproofs-ec-gadgets/std"] default = ["std"] diff --git a/crypto/evrf/embedwards25519/src/lib.rs b/crypto/evrf/embedwards25519/src/lib.rs index a252167c..a0a96d57 100644 --- a/crypto/evrf/embedwards25519/src/lib.rs +++ b/crypto/evrf/embedwards25519/src/lib.rs @@ -7,12 +7,17 @@ use std_shims::prelude::*; #[cfg(any(feature = "alloc", feature = "std"))] use std_shims::io::{self, Read}; -use generic_array::typenum::{Sum, Diff, Quot, U, U1, U2}; +use prime_field::{subtle::Choice, zeroize::Zeroize}; use ciphersuite::group::{ - ff::{PrimeField, FromUniformBytes}, + ff::{Field, PrimeField, FromUniformBytes}, Group, }; +use curve25519_dalek::Scalar as DalekScalar; +pub use dalek_ff_group::Scalar as FieldElement; + +use short_weierstrass::{ShortWeierstrass, Affine, Projective}; + prime_field::odd_prime_field!( Scalar, "0fffffffffffffffffffffffffffffffe53f4debb78ff96877063f0306eef96b", @@ -20,33 +25,52 @@ prime_field::odd_prime_field!( false ); -pub use dalek_ff_group::Scalar as FieldElement; +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub struct Embedwards25519; -mod point; -pub use point::Point; +#[allow(deprecated)] // No other way to construct arbitrary `FieldElement` at compile-time :/ +impl ShortWeierstrass for Embedwards25519 { + type FieldElement = FieldElement; + const A: FieldElement = FieldElement(DalekScalar::from_bits(hex_literal::hex!( + "ead3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010" + ))); + const B: FieldElement = FieldElement(DalekScalar::from_bits(hex_literal::hex!( + "5f07603a853f20370b682036210d463e64903a23ea669d07ca26cfc13f594209" + ))); + const GENERATOR: Affine = Affine::from_xy_unchecked( + FieldElement::ONE, + FieldElement(DalekScalar::from_bits(hex_literal::hex!( + "2e4118080a484a3dfbafe2199a0e36b7193581d676c0dadfa376b0265616020c" + ))), + ); + type Scalar = Scalar; -pub(crate) fn u8_from_bool(bit_ref: &mut bool) -> u8 { - use core::hint::black_box; - use prime_field::zeroize::Zeroize; + type Repr = [u8; 32]; + // Use an all-zero encoding for the identity as `0` isn't the `x` coordinate of an on-curve point + const IDENTITY: [u8; 32] = [0; 32]; + fn compress(x: Self::FieldElement, odd_y: Choice) -> Self::Repr { + // The LE `x` coordinate, with if `y` is odd in the unused 256th bit + let mut res = [0; 32]; + res.as_mut().copy_from_slice(x.to_repr().as_ref()); + res[31] |= odd_y.unwrap_u8() << 7; + res + } + fn decode_compressed(bytes: &Self::Repr) -> (::Repr, Choice) { + // Extract and clear the sign bit + let mut bytes = *bytes; + let odd_y = Choice::from(bytes[31] >> 7); + bytes[31] &= u8::MAX >> 1; - let bit_ref = black_box(bit_ref); + // Copy from the point's representation to the field's + let mut repr = ::Repr::default(); + repr.as_mut().copy_from_slice(&bytes); - let mut bit = black_box(*bit_ref); - let res = black_box(u8::from(bit)); - bit.zeroize(); - debug_assert!((res | 1) == 1); - - bit_ref.zeroize(); - res + (repr, odd_y) + } } -/// Ciphersuite for Embedwards25519. -/// -/// hash_to_F is implemented with a naive concatenation of the dst and data, allowing transposition -/// between the two. This means `dst: b"abc", data: b"def"`, will produce the same scalar as -/// `dst: "abcdef", data: b""`. Please use carefully, not letting dsts be substrings of each other. -#[derive(Clone, Copy, PartialEq, Eq, Debug, zeroize::Zeroize)] -pub struct Embedwards25519; +pub type Point = Projective; + impl ciphersuite::Ciphersuite for Embedwards25519 { type F = Scalar; type G = Point; @@ -58,6 +82,10 @@ impl ciphersuite::Ciphersuite for Embedwards25519 { Point::generator() } + /// `hash_to_F` is implemented with a naive concatenation of the `dst` and `data`, allowing + /// transposition between the two. This means `dst: b"abc", data: b"def"`, will produce the same + /// scalar as `dst: "abcdef", data: b""`. Please use carefully, not letting `dst` valuess be + /// substrings of each other. fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F { use blake2::Digest; >::from_uniform_bytes( @@ -67,7 +95,7 @@ impl ciphersuite::Ciphersuite for Embedwards25519 { // We override the provided impl, which compares against the reserialization, because // we already require canonicity - #[cfg(any(feature = "alloc", feature = "std"))] + #[cfg(feature = "alloc")] #[allow(non_snake_case)] fn read_G(reader: &mut R) -> io::Result { use ciphersuite::group::GroupEncoding; @@ -81,9 +109,38 @@ impl ciphersuite::Ciphersuite for Embedwards25519 { } } -impl generalized_bulletproofs_ec_gadgets::DiscreteLogParameters for Embedwards25519 { - type ScalarBits = U<{ Scalar::NUM_BITS as usize }>; - type XCoefficients = Quot, U2>; - type XCoefficientsMinusOne = Diff; - type YxCoefficients = Diff, U1>, U2>, U2>; +#[cfg(feature = "alloc")] +impl generalized_bulletproofs_ec_gadgets::DiscreteLogParameter for Embedwards25519 { + type ScalarBits = generic_array::typenum::U<{ ::NUM_BITS as usize }>; +} + +#[test] +fn test_curve() { + ff_group_tests::group::test_prime_group_bits::<_, Point>(&mut rand_core::OsRng); +} + +#[test] +fn generator() { + use ciphersuite::group::{Group, GroupEncoding}; + assert_eq!( + Point::generator(), + Point::from_bytes(&hex_literal::hex!( + "0100000000000000000000000000000000000000000000000000000000000000" + )) + .unwrap() + ); +} + +#[test] +fn zero_x_is_off_curve() { + assert!(bool::from( + Affine::::decompress(FieldElement::ZERO, 1.into()).is_none() + )); +} + +// Checks random won't infinitely loop +#[test] +fn random() { + use ciphersuite::group::Group; + Point::random(&mut rand_core::OsRng); } diff --git a/crypto/evrf/embedwards25519/src/point.rs b/crypto/evrf/embedwards25519/src/point.rs deleted file mode 100644 index 4c2fb140..00000000 --- a/crypto/evrf/embedwards25519/src/point.rs +++ /dev/null @@ -1,441 +0,0 @@ -use core::{ - ops::{DerefMut, Add, AddAssign, Neg, Sub, SubAssign, Mul, MulAssign}, - iter::Sum, -}; - -use prime_field::{ - subtle::{Choice, CtOption, ConstantTimeEq, ConditionallySelectable}, - zeroize::Zeroize, - rand_core::RngCore, -}; - -use ciphersuite::group::{ - ff::{Field, PrimeField, PrimeFieldBits}, - Group, GroupEncoding, - prime::PrimeGroup, -}; - -use crate::{u8_from_bool, Scalar, FieldElement}; - -#[allow(non_snake_case)] -fn B() -> FieldElement { - FieldElement::from_repr(hex_literal::hex!( - "5f07603a853f20370b682036210d463e64903a23ea669d07ca26cfc13f594209" - )) - .unwrap() -} - -fn recover_y(x: FieldElement) -> CtOption { - // x**3 - 3 * x + B - ((x.square() * x) - (x.double() + x) + B()).sqrt() -} - -/// Point. -#[derive(Clone, Copy, Debug)] -#[repr(C)] -pub struct Point { - x: FieldElement, // / Z - y: FieldElement, // / Z - z: FieldElement, -} - -impl Zeroize for Point { - fn zeroize(&mut self) { - self.x.zeroize(); - self.y.zeroize(); - self.z.zeroize(); - let identity = Self::identity(); - self.x = identity.x; - self.y = identity.y; - self.z = identity.z; - } -} - -impl ConstantTimeEq for Point { - fn ct_eq(&self, other: &Self) -> Choice { - let x1 = self.x * other.z; - let x2 = other.x * self.z; - - let y1 = self.y * other.z; - let y2 = other.y * self.z; - - // Both identity or equivalent over their denominators - (self.z.is_zero() & other.z.is_zero()) | (x1.ct_eq(&x2) & y1.ct_eq(&y2)) - } -} - -impl PartialEq for Point { - fn eq(&self, other: &Point) -> bool { - self.ct_eq(other).into() - } -} - -impl Eq for Point {} - -impl ConditionallySelectable for Point { - fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { - Point { - x: FieldElement::conditional_select(&a.x, &b.x, choice), - y: FieldElement::conditional_select(&a.y, &b.y, choice), - z: FieldElement::conditional_select(&a.z, &b.z, choice), - } - } -} - -impl Add for Point { - type Output = Point; - #[allow(non_snake_case)] - fn add(self, other: Self) -> Self { - // add-2015-rcb - - let a = -FieldElement::from(3u64); - let B = B(); - let b3 = B + B + B; - - let X1 = self.x; - let Y1 = self.y; - let Z1 = self.z; - let X2 = other.x; - let Y2 = other.y; - let Z2 = other.z; - - let t0 = X1 * X2; - let t1 = Y1 * Y2; - let t2 = Z1 * Z2; - let t3 = X1 + Y1; - let t4 = X2 + Y2; - let t3 = t3 * t4; - let t4 = t0 + t1; - let t3 = t3 - t4; - let t4 = X1 + Z1; - let t5 = X2 + Z2; - let t4 = t4 * t5; - let t5 = t0 + t2; - let t4 = t4 - t5; - let t5 = Y1 + Z1; - let X3 = Y2 + Z2; - let t5 = t5 * X3; - let X3 = t1 + t2; - let t5 = t5 - X3; - let Z3 = a * t4; - let X3 = b3 * t2; - let Z3 = X3 + Z3; - let X3 = t1 - Z3; - let Z3 = t1 + Z3; - let Y3 = X3 * Z3; - let t1 = t0 + t0; - let t1 = t1 + t0; - let t2 = a * t2; - let t4 = b3 * t4; - let t1 = t1 + t2; - let t2 = t0 - t2; - let t2 = a * t2; - let t4 = t4 + t2; - let t0 = t1 * t4; - let Y3 = Y3 + t0; - let t0 = t5 * t4; - let X3 = t3 * X3; - let X3 = X3 - t0; - let t0 = t3 * t1; - let Z3 = t5 * Z3; - let Z3 = Z3 + t0; - Point { x: X3, y: Y3, z: Z3 } - } -} - -impl AddAssign for Point { - fn add_assign(&mut self, other: Point) { - *self = *self + other; - } -} - -impl Add<&Point> for Point { - type Output = Point; - fn add(self, other: &Point) -> Point { - self + *other - } -} - -impl AddAssign<&Point> for Point { - fn add_assign(&mut self, other: &Point) { - *self += *other; - } -} - -impl Neg for Point { - type Output = Point; - fn neg(self) -> Self { - Point { x: self.x, y: -self.y, z: self.z } - } -} - -impl Sub for Point { - type Output = Point; - #[allow(clippy::suspicious_arithmetic_impl)] - fn sub(self, other: Self) -> Self { - self + other.neg() - } -} - -impl SubAssign for Point { - fn sub_assign(&mut self, other: Point) { - *self = *self - other; - } -} - -impl Sub<&Point> for Point { - type Output = Point; - fn sub(self, other: &Point) -> Point { - self - *other - } -} - -impl SubAssign<&Point> for Point { - fn sub_assign(&mut self, other: &Point) { - *self -= *other; - } -} - -impl Group for Point { - type Scalar = Scalar; - fn random(mut rng: impl RngCore) -> Self { - loop { - let mut bytes = [0; 32]; - rng.fill_bytes(bytes.as_mut()); - let opt = Self::from_bytes(&bytes); - if opt.is_some().into() { - return opt.unwrap(); - } - } - } - fn identity() -> Self { - Point { x: FieldElement::ZERO, y: FieldElement::ONE, z: FieldElement::ZERO } - } - fn generator() -> Self { - // Point with the lowest valid x-coordinate - Point { - x: FieldElement::from_repr(hex_literal::hex!( - "0100000000000000000000000000000000000000000000000000000000000000" - )) - .unwrap(), - y: FieldElement::from_repr(hex_literal::hex!( - "2e4118080a484a3dfbafe2199a0e36b7193581d676c0dadfa376b0265616020c" - )) - .unwrap(), - z: FieldElement::ONE, - } - } - fn is_identity(&self) -> Choice { - self.z.ct_eq(&FieldElement::ZERO) - } - #[allow(non_snake_case)] - fn double(&self) -> Self { - // dbl-2007-bl-2 - let X1 = self.x; - let Y1 = self.y; - let Z1 = self.z; - - let w = (X1 - Z1) * (X1 + Z1); - let w = w.double() + w; - let s = (Y1 * Z1).double(); - let ss = s.square(); - let sss = s * ss; - let R = Y1 * s; - let RR = R.square(); - let B_ = (X1 * R).double(); - let h = w.square() - B_.double(); - let X3 = h * s; - let Y3 = w * (B_ - h) - RR.double(); - let Z3 = sss; - - let res = Self { x: X3, y: Y3, z: Z3 }; - // If self is identity, res will not be well-formed - // Accordingly, we return self if self was the identity - Self::conditional_select(&res, self, self.is_identity()) - } -} - -impl Sum for Point { - fn sum>(iter: I) -> Point { - let mut res = Self::identity(); - for i in iter { - res += i; - } - res - } -} - -impl<'a> Sum<&'a Point> for Point { - fn sum>(iter: I) -> Point { - Point::sum(iter.cloned()) - } -} - -impl Mul for Point { - type Output = Point; - fn mul(self, mut other: Scalar) -> Point { - // Precompute the optimal amount that's a multiple of 2 - let mut table = [Point::identity(); 16]; - table[1] = self; - for i in 2 .. 16 { - table[i] = table[i - 1] + self; - } - - let mut res = Self::identity(); - let mut bits = 0; - for (i, mut bit) in other.to_le_bits().iter_mut().rev().enumerate() { - bits <<= 1; - let mut bit = u8_from_bool(bit.deref_mut()); - bits |= bit; - bit.zeroize(); - - if ((i + 1) % 4) == 0 { - if i != 3 { - for _ in 0 .. 4 { - res = res.double(); - } - } - - let mut term = table[0]; - for (j, candidate) in table[1 ..].iter().enumerate() { - let j = j + 1; - term = Self::conditional_select(&term, candidate, usize::from(bits).ct_eq(&j)); - } - res += term; - bits = 0; - } - } - other.zeroize(); - res - } -} - -impl MulAssign for Point { - fn mul_assign(&mut self, other: Scalar) { - *self = *self * other; - } -} - -impl Mul<&Scalar> for Point { - type Output = Point; - fn mul(self, other: &Scalar) -> Point { - self * *other - } -} - -impl MulAssign<&Scalar> for Point { - fn mul_assign(&mut self, other: &Scalar) { - *self *= *other; - } -} - -impl GroupEncoding for Point { - type Repr = [u8; 32]; - - fn from_bytes(bytes: &Self::Repr) -> CtOption { - // Extract and clear the sign bit - let mut bytes = *bytes; - let sign = Choice::from(bytes[31] >> 7); - bytes[31] &= u8::MAX >> 1; - - // Parse x, recover y - FieldElement::from_repr(bytes).and_then(|x| { - let is_identity = x.is_zero(); - - let y = recover_y(x).map(|mut y| { - y = <_>::conditional_select(&y, &-y, y.is_odd().ct_eq(&!sign)); - y - }); - - // If this the identity, set y to 1 - let y = - CtOption::conditional_select(&y, &CtOption::new(FieldElement::ONE, 1.into()), is_identity); - // If this the identity, set y to 1 and z to 0 (instead of 1) - let z = <_>::conditional_select(&FieldElement::ONE, &FieldElement::ZERO, is_identity); - // Create the point if we have a y solution - let point = y.map(|y| Point { x, y, z }); - - let not_negative_zero = !(is_identity & sign); - // Only return the point if it isn't -0 - CtOption::conditional_select( - &CtOption::new(Point::identity(), 0.into()), - &point, - not_negative_zero, - ) - }) - } - - fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { - Point::from_bytes(bytes) - } - - fn to_bytes(&self) -> Self::Repr { - let Some(z) = Option::::from(self.z.invert()) else { - return [0; 32]; - }; - let x = self.x * z; - let y = self.y * z; - - let mut res = [0; 32]; - res.as_mut().copy_from_slice(&x.to_repr()); - - // The following conditional select normalizes the sign to 0 when x is 0 - let y_sign = u8::conditional_select(&y.is_odd().unwrap_u8(), &0, x.ct_eq(&FieldElement::ZERO)); - res[31] |= y_sign << 7; - res - } -} - -impl PrimeGroup for Point {} - -impl ec_divisors::DivisorCurve for Point { - type FieldElement = FieldElement; - type XyPoint = ec_divisors::Projective; - - fn interpolator_for_scalar_mul() -> &'static ec_divisors::Interpolator { - static PRECOMPUTE: std_shims::sync::LazyLock> = - std_shims::sync::LazyLock::new(|| { - ec_divisors::Interpolator::new(usize::try_from(130).unwrap()) - }); - &PRECOMPUTE - } - - fn a() -> Self::FieldElement { - -FieldElement::from(3u64) - } - fn b() -> Self::FieldElement { - B() - } - - fn to_xy(point: Self) -> Option<(Self::FieldElement, Self::FieldElement)> { - let z: Self::FieldElement = Option::from(point.z.invert())?; - Some((point.x * z, point.y * z)) - } -} - -#[test] -fn test_curve() { - ff_group_tests::group::test_prime_group_bits::<_, Point>(&mut rand_core::OsRng); -} - -#[test] -fn generator() { - assert_eq!( - Point::generator(), - Point::from_bytes(&hex_literal::hex!( - "0100000000000000000000000000000000000000000000000000000000000000" - )) - .unwrap() - ); -} - -#[test] -fn zero_x_is_invalid() { - assert!(Option::::from(recover_y(FieldElement::ZERO)).is_none()); -} - -// Checks random won't infinitely loop -#[test] -fn random() { - Point::random(&mut rand_core::OsRng); -} diff --git a/crypto/evrf/secq256k1/Cargo.toml b/crypto/evrf/secq256k1/Cargo.toml index 09c38411..ee6d9f9b 100644 --- a/crypto/evrf/secq256k1/Cargo.toml +++ b/crypto/evrf/secq256k1/Cargo.toml @@ -23,8 +23,8 @@ prime-field = { path = "../../prime-field", default-features = false } blake2 = { version = "0.10", default-features = false } ciphersuite = { path = "../../ciphersuite", version = "0.4", default-features = false } -ec-divisors = { git = "https://github.com/monero-oxide/monero-oxide", rev = "59e3ae73b51c214afbc304efca8d748b3da62977", default-features = false } -generalized-bulletproofs-ec-gadgets = { git = "https://github.com/monero-oxide/monero-oxide", rev = "59e3ae73b51c214afbc304efca8d748b3da62977", default-features = false } +ec-divisors = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a6f8797007e768488568b821435cf5006517a962", default-features = false } +generalized-bulletproofs-ec-gadgets = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a6f8797007e768488568b821435cf5006517a962", default-features = false } [dev-dependencies] hex = "0.4" diff --git a/crypto/evrf/secq256k1/src/lib.rs b/crypto/evrf/secq256k1/src/lib.rs index 2e9cd052..98fe02d7 100644 --- a/crypto/evrf/secq256k1/src/lib.rs +++ b/crypto/evrf/secq256k1/src/lib.rs @@ -9,7 +9,7 @@ use std_shims::io::{self, Read}; use k256::elliptic_curve::{ zeroize::Zeroize, - generic_array::typenum::{Sum, Diff, Quot, U, U1, U2}, + generic_array::typenum::U, group::{ ff::{PrimeField, FromUniformBytes}, Group, @@ -87,9 +87,6 @@ impl ciphersuite::Ciphersuite for Secq256k1 { } } -impl generalized_bulletproofs_ec_gadgets::DiscreteLogParameters for Secq256k1 { +impl generalized_bulletproofs_ec_gadgets::DiscreteLogParameter for Secq256k1 { type ScalarBits = U<{ Scalar::NUM_BITS as usize }>; - type XCoefficients = Quot, U2>; - type XCoefficientsMinusOne = Diff; - type YxCoefficients = Diff, U1>, U2>, U2>; } diff --git a/crypto/evrf/secq256k1/src/point.rs b/crypto/evrf/secq256k1/src/point.rs index 25502f19..e78c236b 100644 --- a/crypto/evrf/secq256k1/src/point.rs +++ b/crypto/evrf/secq256k1/src/point.rs @@ -1,4 +1,5 @@ use core::{ + borrow::Borrow, ops::{DerefMut, Add, AddAssign, Neg, Sub, SubAssign, Mul, MulAssign}, iter::Sum, }; @@ -389,12 +390,12 @@ impl ec_divisors::DivisorCurve for Point { type FieldElement = FieldElement; type XyPoint = ec_divisors::Projective; - fn interpolator_for_scalar_mul() -> &'static ec_divisors::Interpolator { + fn interpolator_for_scalar_mul() -> impl Borrow> { static PRECOMPUTE: std_shims::sync::LazyLock> = std_shims::sync::LazyLock::new(|| { ec_divisors::Interpolator::new(usize::try_from(130).unwrap()) }); - &PRECOMPUTE + &*PRECOMPUTE } fn a() -> Self::FieldElement { diff --git a/crypto/prime-field/Cargo.toml b/crypto/prime-field/Cargo.toml index 66fe5e60..5b5dc027 100644 --- a/crypto/prime-field/Cargo.toml +++ b/crypto/prime-field/Cargo.toml @@ -27,5 +27,5 @@ ff-group-tests = { version = "0.13", path = "../ff-group-tests", optional = true [features] alloc = ["zeroize/alloc", "crypto-bigint/alloc", "ff/alloc"] -std = ["zeroize/std", "subtle/std", "rand_core/std", "ff/std", "ff-group-tests"] +std = ["alloc", "zeroize/std", "subtle/std", "rand_core/std", "ff/std", "ff-group-tests"] default = ["std"] diff --git a/crypto/short-weierstrass/Cargo.toml b/crypto/short-weierstrass/Cargo.toml index 71a4f027..1a4a9a38 100644 --- a/crypto/short-weierstrass/Cargo.toml +++ b/crypto/short-weierstrass/Cargo.toml @@ -21,7 +21,10 @@ rand_core = { version = "0.6", default-features = false } ff = { version = "0.13", default-features = false, features = ["bits"] } group = { version = "0.13", default-features = false } +generic-array = { version = "1", default-features = false, optional = true } +ec-divisors = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a6f8797007e768488568b821435cf5006517a962", default-features = false, optional = true } + [features] -alloc = ["zeroize/alloc", "rand_core/alloc", "ff/alloc", "group/alloc"] -std = ["zeroize/std", "subtle/std", "rand_core/std", "ff/std"] +alloc = ["zeroize/alloc", "rand_core/alloc", "ff/alloc", "group/alloc", "generic-array", "ec-divisors"] +std = ["alloc", "zeroize/std", "subtle/std", "rand_core/std", "ff/std"] default = ["std"] diff --git a/crypto/short-weierstrass/src/affine.rs b/crypto/short-weierstrass/src/affine.rs index b657e421..afc58822 100644 --- a/crypto/short-weierstrass/src/affine.rs +++ b/crypto/short-weierstrass/src/affine.rs @@ -5,7 +5,7 @@ use zeroize::DefaultIsZeroes; use rand_core::RngCore; -use group::ff::Field; +use group::ff::{Field, PrimeField}; use crate::{ShortWeierstrass, Projective}; @@ -23,8 +23,14 @@ impl Clone for Affine { impl Copy for Affine {} impl Affine { - pub fn try_from(projective: Projective) -> CtOption { - projective.z.invert().map(|z_inv| Self { x: projective.x * z_inv, y: projective.y * z_inv }) + pub fn try_from(projective: &Projective) -> CtOption { + let z_inv = projective.z.invert().unwrap_or(C::FieldElement::ZERO); + let if_non_identity = Self { x: projective.x * z_inv, y: projective.y * z_inv }; + let identity = z_inv.ct_eq(&C::FieldElement::ZERO); + // If this isn't a valid affine point, switch to a valid affine point + let value = <_>::conditional_select(&if_non_identity, &C::GENERATOR, identity); + // This is valid if the projective point wasn't the identity + CtOption::new(value, !identity) } } @@ -73,24 +79,35 @@ impl Affine { /// Create an affine point from `x, y` coordinates, without performing any checks. /// /// This should NOT be used. It is solely intended for trusted data at compile-time. It MUST NOT - /// be used with any untrusted/unvalidated data. - pub const fn from_xy_unchecked(x: C::FieldElement, y: C::FieldElement) -> Self { Self { x, y } } + /// be used with any untrusted/unvalidated data. Providing any off-curve point may produce + /// completely undefined behavior. + pub const fn from_xy_unchecked(x: C::FieldElement, y: C::FieldElement) -> Self { + Self { x, y } + } + + /// Decompress a point from it's `x`-coordinate and the sign of the `y` coordinate. + pub fn decompress(x: C::FieldElement, odd_y: Choice) -> CtOption { + let y_square = ((x.square() + C::A) * x) + C::B; + y_square.sqrt().and_then(|mut y| { + y = <_>::conditional_select(&y, &-y, odd_y.ct_ne(&y.is_odd())); + CtOption::new(Self { x, y }, 1.into()) + }) + } /// The `x, y` coordinates of this point. pub fn coordinates(self) -> (C::FieldElement, C::FieldElement) { (self.x, self.y) } -/// Sample a random on-curve point with an unknown discrete logarithm w.r.t. any other points. -pub fn random(mut rng: impl RngCore) -> Self { - loop { - let x = C::FieldElement::random(&mut rng); - let y_square = ((x.square() + C::A) * x) + C::B; - let Some(mut y) = Option::::from(y_square.sqrt()) else { continue }; - if (rng.next_u64() % 2) == 1 { - y = -y; + /// Sample a random on-curve point with an unknown discrete logarithm w.r.t. any other points. + /// + /// This runs in time variable to the amount of samples required to find a point. + pub fn random(mut rng: impl RngCore) -> Self { + loop { + let x = C::FieldElement::random(&mut rng); + let y_is_odd = Choice::from((rng.next_u64() % 2) as u8); + let Some(res) = Option::::from(Self::decompress(x, y_is_odd)) else { continue }; + return res; } - return Self { x, y }; } - } } diff --git a/crypto/short-weierstrass/src/lib.rs b/crypto/short-weierstrass/src/lib.rs index 84d4dcf4..ca7ca6ca 100644 --- a/crypto/short-weierstrass/src/lib.rs +++ b/crypto/short-weierstrass/src/lib.rs @@ -5,6 +5,7 @@ use core::fmt::Debug; +use subtle::Choice; use zeroize::Zeroize; use group::ff::PrimeField; @@ -14,6 +15,10 @@ mod projective; pub use projective::Projective; /// An elliptic curve represented in short Weierstrass form, with equation `y^2 = x^3 + A x + B`. +/// +/// This elliptic curve is expected to be of prime order. If a generator of the elliptic curve has +/// a composite order, the elliptic curve is defined solely as its largest odd-prime-order +/// subgroup, further considered the entire group/elliptic curve. pub trait ShortWeierstrass: 'static + Sized + Debug { /// The field the elliptic curve is defined over. type FieldElement: Zeroize + PrimeField; @@ -27,4 +32,17 @@ pub trait ShortWeierstrass: 'static + Sized + Debug { /// /// This may be omitted by specifying `()`. type Scalar; + + /// The type used for encoding points. + type Repr: 'static + Send + Sync + Copy + Default + AsRef<[u8]> + AsMut<[u8]>; + /// The representation of the identity point. + const IDENTITY: Self::Repr; + /// Compress an affine point its byte encoding. + /// + /// The space of potential outputs MUST exclude `Self::IDENTITY`. + fn compress(x: Self::FieldElement, odd_y: Choice) -> Self::Repr; + /// Decode a compressed point. + /// + /// This is expected to return the `x` coordinate and if the `y` coordinate is odd. + fn decode_compressed(bytes: &Self::Repr) -> (::Repr, Choice); } diff --git a/crypto/short-weierstrass/src/projective.rs b/crypto/short-weierstrass/src/projective.rs index c4386001..203084e7 100644 --- a/crypto/short-weierstrass/src/projective.rs +++ b/crypto/short-weierstrass/src/projective.rs @@ -1,13 +1,14 @@ use core::{hint::black_box, borrow::Borrow, ops::*, iter::Sum}; -use subtle::{Choice, ConstantTimeEq, ConditionallySelectable, ConditionallyNegatable}; +use subtle::{Choice, CtOption, ConstantTimeEq, ConditionallySelectable, ConditionallyNegatable}; use zeroize::{Zeroize, DefaultIsZeroes}; use rand_core::RngCore; use group::{ - ff::{Field, PrimeFieldBits}, - Group, + ff::{Field, PrimeField, PrimeFieldBits}, + Group, GroupEncoding, + prime::PrimeGroup, }; use crate::{ShortWeierstrass, Affine}; @@ -280,19 +281,6 @@ impl, S: Borrow> MulAssig *self = *self * scalar.borrow(); } } -/* -impl> Mul<&C::Scalar> for Projective { - type Output = Self; - fn mul(self, scalar: &C::Scalar) -> Self { - self * *scalar - } -} -impl> MulAssign<&C::Scalar> for Projective { - fn mul_assign(&mut self, scalar: &C::Scalar) { - *self *= *scalar; - } -} -*/ impl> Group for Projective { type Scalar = C::Scalar; fn random(rng: impl RngCore) -> Self { @@ -311,3 +299,73 @@ impl> Group for Projective { self.double_internal() } } + +impl GroupEncoding for Projective { + type Repr = C::Repr; + fn from_bytes(bytes: &C::Repr) -> CtOption { + // If this point is the identity point + let identity = bytes.as_ref().ct_eq(C::IDENTITY.as_ref()); + + let (x, odd_y) = C::decode_compressed(bytes); + + // Parse x, recover y, return the result + C::FieldElement::from_repr(x).and_then(|x| { + let non_identity_on_curve_point = Affine::decompress(x, odd_y).map(Projective::from); + let identity = CtOption::new(Projective::IDENTITY, identity); + non_identity_on_curve_point.or_else(|| identity) + }) + } + fn from_bytes_unchecked(bytes: &C::Repr) -> CtOption { + Self::from_bytes(bytes) + } + fn to_bytes(&self) -> C::Repr { + let affine_on_curve = Affine::try_from(self); + let identity = affine_on_curve.is_none(); + + let compressed_if_not_identity = { + let affine_on_curve = affine_on_curve.unwrap_or(C::GENERATOR); + let (x, y) = affine_on_curve.coordinates(); + C::compress(x, y.is_odd()) + }; + + let mut res = C::Repr::default(); + { + let res = res.as_mut(); + for (dst, (if_not_identity, if_identity)) in + res.iter_mut().zip(compressed_if_not_identity.as_ref().iter().zip(C::IDENTITY.as_ref())) + { + *dst = <_>::conditional_select(if_not_identity, if_identity, identity); + } + } + res + } +} + +impl> PrimeGroup for Projective {} + +#[cfg(feature = "alloc")] +mod alloc { + use core::borrow::Borrow; + use ff::{PrimeField, PrimeFieldBits}; + use crate::{ShortWeierstrass, Affine, Projective}; + + impl> ec_divisors::DivisorCurve for Projective { + type FieldElement = C::FieldElement; + type XyPoint = ec_divisors::Projective; + + fn interpolator_for_scalar_mul() -> impl Borrow> { + ec_divisors::Interpolator::new((::NUM_BITS as usize).div_ceil(2) + 2) + } + + fn a() -> C::FieldElement { + C::A + } + fn b() -> C::FieldElement { + C::B + } + + fn to_xy(point: Self) -> Option<(C::FieldElement, C::FieldElement)> { + Option::>::from(Affine::try_from(&point)).map(Affine::<_>::coordinates) + } + } +} diff --git a/patches/ciphersuite/Cargo.toml b/patches/ciphersuite/Cargo.toml index 4743f37c..ade5a85e 100644 --- a/patches/ciphersuite/Cargo.toml +++ b/patches/ciphersuite/Cargo.toml @@ -22,6 +22,7 @@ dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = fals [features] alloc = ["ciphersuite/alloc"] std = [ + "alloc", "ciphersuite/std", "dalek-ff-group?/std", ]