From 62bb75e09a1fa9c09bfe0f8f6cd1d64c881be38a Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Thu, 28 Aug 2025 23:07:22 -0400 Subject: [PATCH] Move secq256k1 to short-weierstrass --- Cargo.lock | 4 +- crypto/evrf/secq256k1/Cargo.toml | 9 +- crypto/evrf/secq256k1/src/lib.rs | 139 +++++++-- crypto/evrf/secq256k1/src/point.rs | 439 ---------------------------- crypto/short-weierstrass/Cargo.toml | 3 +- 5 files changed, 122 insertions(+), 472 deletions(-) delete mode 100644 crypto/evrf/secq256k1/src/point.rs diff --git a/Cargo.lock b/Cargo.lock index b85dabde..02cae007 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9542,14 +9542,15 @@ version = "0.1.0" dependencies = [ "blake2", "ciphersuite 0.4.2", - "ec-divisors", "ff-group-tests", "generalized-bulletproofs-ec-gadgets", + "generic-array 1.2.0", "hex", "hex-literal", "k256", "prime-field", "rand_core 0.6.4", + "short-weierstrass", "std-shims", ] @@ -10928,7 +10929,6 @@ version = "0.1.0" dependencies = [ "ec-divisors", "ff", - "generic-array 1.2.0", "group", "rand_core 0.6.4", "subtle", diff --git a/crypto/evrf/secq256k1/Cargo.toml b/crypto/evrf/secq256k1/Cargo.toml index ee6d9f9b..43ad777c 100644 --- a/crypto/evrf/secq256k1/Cargo.toml +++ b/crypto/evrf/secq256k1/Cargo.toml @@ -18,13 +18,14 @@ hex-literal = { version = "0.4", default-features = false } std-shims = { version = "0.1", path = "../../../common/std-shims", default-features = false, optional = true } +generic-array = { version = "1", default-features = false } k256 = { version = "0.13", default-features = false, features = ["arithmetic"] } prime-field = { path = "../../prime-field", default-features = false } +short-weierstrass = { path = "../../short-weierstrass", 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 = "a6f8797007e768488568b821435cf5006517a962", default-features = false } -generalized-bulletproofs-ec-gadgets = { 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, optional = true } [dev-dependencies] hex = "0.4" @@ -34,6 +35,6 @@ rand_core = { version = "0.6", features = ["std"] } ff-group-tests = { path = "../../ff-group-tests" } [features] -alloc = ["std-shims", "k256/alloc", "prime-field/alloc", "ciphersuite/alloc"] -std = ["std-shims/std", "k256/std", "prime-field/std", "blake2/std", "ciphersuite/std", "ec-divisors/std", "generalized-bulletproofs-ec-gadgets/std"] +alloc = ["std-shims", "generic-array/alloc", "k256/alloc", "prime-field/alloc", "short-weierstrass/alloc", "ciphersuite/alloc", "generalized-bulletproofs-ec-gadgets"] +std = ["alloc", "std-shims/std", "k256/std", "prime-field/std", "blake2/std", "ciphersuite/std", "generalized-bulletproofs-ec-gadgets/std"] default = ["std"] diff --git a/crypto/evrf/secq256k1/src/lib.rs b/crypto/evrf/secq256k1/src/lib.rs index 98fe02d7..5ec9e529 100644 --- a/crypto/evrf/secq256k1/src/lib.rs +++ b/crypto/evrf/secq256k1/src/lib.rs @@ -4,12 +4,17 @@ #[allow(unused_imports)] use std_shims::prelude::*; -#[cfg(any(feature = "alloc", feature = "std"))] +#[cfg(feature = "alloc")] use std_shims::io::{self, Read}; +// Doesn't use the `generic-array 0.14` exported by `k256::elliptic_curve` as we need `1.0` +use generic_array::{ + typenum::{U, U33}, + GenericArray, +}; use k256::elliptic_curve::{ + subtle::{Choice, ConstantTimeEq, ConditionallySelectable}, zeroize::Zeroize, - generic_array::typenum::U, group::{ ff::{PrimeField, FromUniformBytes}, Group, @@ -25,34 +30,85 @@ prime_field::odd_prime_field!( pub use k256::Scalar as FieldElement; -mod point; -pub use point::Point; +use short_weierstrass::{ShortWeierstrass, Affine, Projective}; -pub(crate) fn u8_from_bool(bit_ref: &mut bool) -> u8 { - use core::hint::black_box; - use prime_field::zeroize::Zeroize; - - let bit_ref = black_box(bit_ref); - - 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 -} - -/// Ciphersuite for Secq256k1. -/// -/// 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)] pub struct Secq256k1; impl Zeroize for Secq256k1 { fn zeroize(&mut self) {} } + +impl ShortWeierstrass for Secq256k1 { + type FieldElement = FieldElement; + const A: FieldElement = FieldElement::ZERO; + const B: FieldElement = { + let two = FieldElement::ONE.add(&FieldElement::ONE); + let four = two.add(&two); + let six = four.add(&two); + six.add(&FieldElement::ONE) + }; + const GENERATOR: Affine = Affine::from_xy_unchecked(FieldElement::ONE, { + let y_be = + hex_literal::hex!("0c7c97045a2074634909abdf82c9bd0248916189041f2af0c1b800d1ffc278c0"); + let mut res = FieldElement::ZERO; + let mut i = 0; + while i < 32 { + let mut j = 0; + while j < 8 { + // Shift over the existing result + res = res.add(&res); + // Add this bit, if set + if ((y_be[i] >> (8 - 1 - j)) & 1) == 1 { + res = res.add(&FieldElement::ONE); + } + j += 1; + } + i += 1; + } + res + }); + type Scalar = Scalar; + + type Repr = GenericArray; + // Use an all-zero encoding for the identity as `0` isn't the `x` coordinate of an on-curve point + // This uses `unsafe` to construct a `GenericArray` at compile-time as `` + const IDENTITY: Self::Repr = GenericArray::from_array([0; 33]); + fn compress(x: Self::FieldElement, odd_y: Choice) -> Self::Repr { + // If `y` is odd, followed by the big-endian `x` coordinate + let mut res = GenericArray::default(); + res[0] = odd_y.unwrap_u8(); + { + let res: &mut [u8] = res.as_mut(); + res[1 ..].copy_from_slice(x.to_repr().as_ref()); + } + res + } + fn decode_compressed(bytes: &Self::Repr) -> (::Repr, Choice) { + // Parse out if `y` is odd + let odd_y = Choice::from(bytes[0] & 1); + // Check if the extra byte was malleated + let invalid = !bytes[0].ct_eq(&odd_y.unwrap_u8()); + + // Copy the alleged `x` coordinate, overwriting with `0xffffff...` if the sign byte was + // malleated (causing the `x` coordinate to be invalid) + let mut x = ::Repr::default(); + { + let x: &mut [u8] = x.as_mut(); + for i in 0 .. 32 { + x[i] = <_>::conditional_select(&bytes[1 + i], &u8::MAX, invalid); + } + } + + (x, odd_y) + } + // No points have a torsion element as this a prime-order curve + fn has_torsion_element(_point: Projective) -> Choice { + 0.into() + } +} + +pub type Point = Projective; + impl ciphersuite::Ciphersuite for Secq256k1 { type F = Scalar; type G = Point; @@ -64,6 +120,10 @@ impl ciphersuite::Ciphersuite for Secq256k1 { 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( @@ -73,7 +133,7 @@ impl ciphersuite::Ciphersuite for Secq256k1 { // 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; @@ -87,6 +147,35 @@ impl ciphersuite::Ciphersuite for Secq256k1 { } } +#[cfg(feature = "alloc")] impl generalized_bulletproofs_ec_gadgets::DiscreteLogParameter for Secq256k1 { type ScalarBits = U<{ Scalar::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::GroupEncoding; + assert_eq!( + Point::generator(), + Point::from_bytes(GenericArray::from_slice(&hex_literal::hex!( + "000000000000000000000000000000000000000000000000000000000000000001" + ))) + .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() { + Point::random(&mut rand_core::OsRng); +} diff --git a/crypto/evrf/secq256k1/src/point.rs b/crypto/evrf/secq256k1/src/point.rs deleted file mode 100644 index e78c236b..00000000 --- a/crypto/evrf/secq256k1/src/point.rs +++ /dev/null @@ -1,439 +0,0 @@ -use core::{ - borrow::Borrow, - ops::{DerefMut, Add, AddAssign, Neg, Sub, SubAssign, Mul, MulAssign}, - iter::Sum, -}; - -use k256::elliptic_curve::{ - zeroize::Zeroize, - subtle::{Choice, CtOption, ConstantTimeEq, ConditionallySelectable, ConditionallyNegatable}, - generic_array::{typenum::U33, GenericArray}, - rand_core::RngCore, - group::{ - ff::{Field, PrimeField, PrimeFieldBits}, - Group, GroupEncoding, - prime::PrimeGroup, - }, -}; - -use crate::{u8_from_bool, Scalar, FieldElement}; - -fn recover_y(x: FieldElement) -> CtOption { - // x**3 + B since a = 0 - ((x.square() * x) + FieldElement::from(7u64)).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; - - // Identity or equivalent - (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::ZERO; - let B = FieldElement::from(7u64); - 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 = GenericArray::default(); - 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!("0000000000000000000000000000000000000000000000000000000000000001") - .into(), - ) - .unwrap(), - y: FieldElement::from_repr( - hex_literal::hex!("0C7C97045A2074634909ABDF82C9BD0248916189041F2AF0C1B800D1FFC278C0") - .into(), - ) - .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 - - let a = FieldElement::ZERO; - - let X1 = self.x; - let Y1 = self.y; - let Z1 = self.z; - - let XX = X1 * X1; - let ZZ = Z1 * Z1; - let w = (a * ZZ) + XX.double() + XX; - let s = (Y1 * Z1).double(); - let ss = s * s; - let sss = s * ss; - let R = Y1 * s; - let RR = R * R; - let B = X1 + R; - let B = (B * B) - XX - RR; - let h = (w * w) - 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 = GenericArray; - - fn from_bytes(bytes: &Self::Repr) -> CtOption { - // Extract and clear the sign bit - let sign = Choice::from(bytes[0] & 1); - - // Parse x, recover y - FieldElement::from_repr(*GenericArray::from_slice(&bytes[1 ..])).and_then(|x| { - let is_identity = x.is_zero(); - - let y = recover_y(x).map(|mut y| { - y.conditional_negate(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 and the sign byte wasn't malleated - CtOption::conditional_select( - &CtOption::new(Point::identity(), 0.into()), - &point, - not_negative_zero & ((bytes[0] & 1).ct_eq(&bytes[0])), - ) - }) - } - - 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 *GenericArray::from_slice(&[0; 33]); - }; - let x = self.x * z; - let y = self.y * z; - - let mut res = *GenericArray::from_slice(&[0; 33]); - res[1 ..].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[0] |= y_sign; - res - } -} - -impl PrimeGroup for Point {} - -impl ec_divisors::DivisorCurve for Point { - type FieldElement = FieldElement; - type XyPoint = ec_divisors::Projective; - - 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 - } - - fn a() -> Self::FieldElement { - FieldElement::from(0u64) - } - fn b() -> Self::FieldElement { - FieldElement::from(7u64) - } - - 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(GenericArray::from_slice(&hex_literal::hex!( - "000000000000000000000000000000000000000000000000000000000000000001" - ))) - .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/short-weierstrass/Cargo.toml b/crypto/short-weierstrass/Cargo.toml index 1a4a9a38..a6e63331 100644 --- a/crypto/short-weierstrass/Cargo.toml +++ b/crypto/short-weierstrass/Cargo.toml @@ -21,10 +21,9 @@ 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", "generic-array", "ec-divisors"] +alloc = ["zeroize/alloc", "rand_core/alloc", "ff/alloc", "group/alloc", "ec-divisors"] std = ["alloc", "zeroize/std", "subtle/std", "rand_core/std", "ff/std"] default = ["std"]