Move secq256k1 to short-weierstrass

This commit is contained in:
Luke Parker
2025-08-28 23:07:22 -04:00
parent 45bd376c08
commit 62bb75e09a
5 changed files with 122 additions and 472 deletions

4
Cargo.lock generated
View File

@@ -9542,14 +9542,15 @@ version = "0.1.0"
dependencies = [ dependencies = [
"blake2", "blake2",
"ciphersuite 0.4.2", "ciphersuite 0.4.2",
"ec-divisors",
"ff-group-tests", "ff-group-tests",
"generalized-bulletproofs-ec-gadgets", "generalized-bulletproofs-ec-gadgets",
"generic-array 1.2.0",
"hex", "hex",
"hex-literal", "hex-literal",
"k256", "k256",
"prime-field", "prime-field",
"rand_core 0.6.4", "rand_core 0.6.4",
"short-weierstrass",
"std-shims", "std-shims",
] ]
@@ -10928,7 +10929,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"ec-divisors", "ec-divisors",
"ff", "ff",
"generic-array 1.2.0",
"group", "group",
"rand_core 0.6.4", "rand_core 0.6.4",
"subtle", "subtle",

View File

@@ -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 } 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"] } k256 = { version = "0.13", default-features = false, features = ["arithmetic"] }
prime-field = { path = "../../prime-field", default-features = false } prime-field = { path = "../../prime-field", default-features = false }
short-weierstrass = { path = "../../short-weierstrass", default-features = false }
blake2 = { version = "0.10", default-features = false } blake2 = { version = "0.10", default-features = false }
ciphersuite = { path = "../../ciphersuite", version = "0.4", 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, optional = true }
generalized-bulletproofs-ec-gadgets = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a6f8797007e768488568b821435cf5006517a962", default-features = false }
[dev-dependencies] [dev-dependencies]
hex = "0.4" hex = "0.4"
@@ -34,6 +35,6 @@ rand_core = { version = "0.6", features = ["std"] }
ff-group-tests = { path = "../../ff-group-tests" } ff-group-tests = { path = "../../ff-group-tests" }
[features] [features]
alloc = ["std-shims", "k256/alloc", "prime-field/alloc", "ciphersuite/alloc"] alloc = ["std-shims", "generic-array/alloc", "k256/alloc", "prime-field/alloc", "short-weierstrass/alloc", "ciphersuite/alloc", "generalized-bulletproofs-ec-gadgets"]
std = ["std-shims/std", "k256/std", "prime-field/std", "blake2/std", "ciphersuite/std", "ec-divisors/std", "generalized-bulletproofs-ec-gadgets/std"] std = ["alloc", "std-shims/std", "k256/std", "prime-field/std", "blake2/std", "ciphersuite/std", "generalized-bulletproofs-ec-gadgets/std"]
default = ["std"] default = ["std"]

View File

@@ -4,12 +4,17 @@
#[allow(unused_imports)] #[allow(unused_imports)]
use std_shims::prelude::*; use std_shims::prelude::*;
#[cfg(any(feature = "alloc", feature = "std"))] #[cfg(feature = "alloc")]
use std_shims::io::{self, Read}; 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::{ use k256::elliptic_curve::{
subtle::{Choice, ConstantTimeEq, ConditionallySelectable},
zeroize::Zeroize, zeroize::Zeroize,
generic_array::typenum::U,
group::{ group::{
ff::{PrimeField, FromUniformBytes}, ff::{PrimeField, FromUniformBytes},
Group, Group,
@@ -25,34 +30,85 @@ prime_field::odd_prime_field!(
pub use k256::Scalar as FieldElement; pub use k256::Scalar as FieldElement;
mod point; use short_weierstrass::{ShortWeierstrass, Affine, Projective};
pub use point::Point;
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)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Secq256k1; pub struct Secq256k1;
impl Zeroize for Secq256k1 { impl Zeroize for Secq256k1 {
fn zeroize(&mut self) {} 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<Self> = 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<u8, U33>;
// 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) -> (<Self::FieldElement as PrimeField>::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 = <Self::FieldElement as PrimeField>::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<Self>) -> Choice {
0.into()
}
}
pub type Point = Projective<Secq256k1>;
impl ciphersuite::Ciphersuite for Secq256k1 { impl ciphersuite::Ciphersuite for Secq256k1 {
type F = Scalar; type F = Scalar;
type G = Point; type G = Point;
@@ -64,6 +120,10 @@ impl ciphersuite::Ciphersuite for Secq256k1 {
Point::generator() 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 { fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
use blake2::Digest; use blake2::Digest;
<Scalar as FromUniformBytes<64>>::from_uniform_bytes( <Scalar as FromUniformBytes<64>>::from_uniform_bytes(
@@ -73,7 +133,7 @@ impl ciphersuite::Ciphersuite for Secq256k1 {
// We override the provided impl, which compares against the reserialization, because // We override the provided impl, which compares against the reserialization, because
// we already require canonicity // we already require canonicity
#[cfg(any(feature = "alloc", feature = "std"))] #[cfg(feature = "alloc")]
#[allow(non_snake_case)] #[allow(non_snake_case)]
fn read_G<R: Read>(reader: &mut R) -> io::Result<Self::G> { fn read_G<R: Read>(reader: &mut R) -> io::Result<Self::G> {
use ciphersuite::group::GroupEncoding; use ciphersuite::group::GroupEncoding;
@@ -87,6 +147,35 @@ impl ciphersuite::Ciphersuite for Secq256k1 {
} }
} }
#[cfg(feature = "alloc")]
impl generalized_bulletproofs_ec_gadgets::DiscreteLogParameter for Secq256k1 { impl generalized_bulletproofs_ec_gadgets::DiscreteLogParameter for Secq256k1 {
type ScalarBits = U<{ Scalar::NUM_BITS as usize }>; 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::<Secq256k1>::decompress(FieldElement::ZERO, 1.into()).is_none()));
}
// Checks random won't infinitely loop
#[test]
fn random() {
Point::random(&mut rand_core::OsRng);
}

View File

@@ -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<FieldElement> {
// 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<Point> for Point {
fn sum<I: Iterator<Item = Point>>(iter: I) -> Point {
let mut res = Self::identity();
for i in iter {
res += i;
}
res
}
}
impl<'a> Sum<&'a Point> for Point {
fn sum<I: Iterator<Item = &'a Point>>(iter: I) -> Point {
Point::sum(iter.cloned())
}
}
impl Mul<Scalar> 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<Scalar> 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<u8, U33>;
fn from_bytes(bytes: &Self::Repr) -> CtOption<Self> {
// 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<Self> {
Point::from_bytes(bytes)
}
fn to_bytes(&self) -> Self::Repr {
let Some(z) = Option::<FieldElement>::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<Self>;
fn interpolator_for_scalar_mul() -> impl Borrow<ec_divisors::Interpolator<Self::FieldElement>> {
static PRECOMPUTE: std_shims::sync::LazyLock<ec_divisors::Interpolator<FieldElement>> =
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::<FieldElement>::from(recover_y(FieldElement::ZERO)).is_none());
}
// Checks random won't infinitely loop
#[test]
fn random() {
Point::random(&mut rand_core::OsRng);
}

View File

@@ -21,10 +21,9 @@ rand_core = { version = "0.6", default-features = false }
ff = { version = "0.13", default-features = false, features = ["bits"] } ff = { version = "0.13", default-features = false, features = ["bits"] }
group = { version = "0.13", default-features = false } 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 } ec-divisors = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a6f8797007e768488568b821435cf5006517a962", default-features = false, optional = true }
[features] [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"] std = ["alloc", "zeroize/std", "subtle/std", "rand_core/std", "ff/std"]
default = ["std"] default = ["std"]