From f2d399ba1e2cfdf47d77b4f820103c44a80be177 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Thu, 28 Aug 2025 08:20:31 -0400 Subject: [PATCH] Add crate for working with short Weierstrass elliptic curves --- .github/workflows/crypto-tests.yml | 1 + Cargo.lock | 27 +- Cargo.toml | 1 + crypto/prime-field/src/lib.rs | 9 + crypto/short-weierstrass/Cargo.toml | 27 ++ crypto/short-weierstrass/LICENSE | 21 ++ crypto/short-weierstrass/README.md | 3 + crypto/short-weierstrass/src/affine.rs | 96 +++++++ crypto/short-weierstrass/src/lib.rs | 30 ++ crypto/short-weierstrass/src/projective.rs | 313 +++++++++++++++++++++ tests/no-std/Cargo.toml | 1 + tests/no-std/src/lib.rs | 1 + 12 files changed, 528 insertions(+), 2 deletions(-) create mode 100644 crypto/short-weierstrass/Cargo.toml create mode 100644 crypto/short-weierstrass/LICENSE create mode 100644 crypto/short-weierstrass/README.md create mode 100644 crypto/short-weierstrass/src/affine.rs create mode 100644 crypto/short-weierstrass/src/lib.rs create mode 100644 crypto/short-weierstrass/src/projective.rs diff --git a/.github/workflows/crypto-tests.yml b/.github/workflows/crypto-tests.yml index 508bcf43..e84d3e92 100644 --- a/.github/workflows/crypto-tests.yml +++ b/.github/workflows/crypto-tests.yml @@ -36,6 +36,7 @@ jobs: -p multiexp \ -p schnorr-signatures \ -p prime-field \ + -p short-weierstrass \ -p secq256k1 \ -p embedwards25519 \ -p dkg \ diff --git a/Cargo.lock b/Cargo.lock index 074a0e1d..8112abef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,7 +474,7 @@ dependencies = [ "alloy-rlp", "alloy-serde", "alloy-sol-types", - "itertools 0.13.0", + "itertools 0.14.0", "serde", "serde_json", "serde_with", @@ -2517,7 +2517,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.106", + "syn 1.0.109", ] [[package]] @@ -2988,6 +2988,7 @@ version = "0.1.0" dependencies = [ "blake2", "ciphersuite 0.4.2", + "curve25519-dalek", "dalek-ff-group", "ec-divisors", "ff-group-tests", @@ -2997,6 +2998,7 @@ dependencies = [ "hex-literal", "prime-field", "rand_core 0.6.4", + "short-weierstrass", "std-shims", "zeroize", ] @@ -4559,6 +4561,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -10180,6 +10191,7 @@ dependencies = [ "prime-field", "schnorr-signatures", "secq256k1", + "short-weierstrass", ] [[package]] @@ -10911,6 +10923,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "short-weierstrass" +version = "0.1.0" +dependencies = [ + "ff", + "group", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "signal-hook-registry" version = "1.4.6" diff --git a/Cargo.toml b/Cargo.toml index adb79596..285c9b10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ members = [ "crypto/schnorr", "crypto/prime-field", + "crypto/short-weierstrass", "crypto/evrf/secq256k1", "crypto/evrf/embedwards25519", diff --git a/crypto/prime-field/src/lib.rs b/crypto/prime-field/src/lib.rs index 48432f88..5bb5b96e 100644 --- a/crypto/prime-field/src/lib.rs +++ b/crypto/prime-field/src/lib.rs @@ -83,6 +83,15 @@ pub mod __prime_field_private { } } +/// Construct a odd-prime field. +/// +/// The length of the `modulus_as_be_hex` string will effect the size of the underlying +/// representation and the encoding. It MAY have a "0x" prefix, if preferred. +/// +/// `multiplicative_generator_as_be_hex` MAY have a "0x" prefix. It MAY be short and of a length +/// less than `modulus_as_be_hex`. +/// +/// `big_endian` controls if the encoded representation will be big-endian or not. #[macro_export] macro_rules! odd_prime_field { ( diff --git a/crypto/short-weierstrass/Cargo.toml b/crypto/short-weierstrass/Cargo.toml new file mode 100644 index 00000000..71a4f027 --- /dev/null +++ b/crypto/short-weierstrass/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "short-weierstrass" +version = "0.1.0" +description = "A library for working with curves in a short Weierstrass form" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/crypto/short-weierstrass" +authors = ["Luke Parker "] +keywords = ["ff", "group", "elliptic-curve", "weierstrass"] +edition = "2021" +rust-version = "1.79" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +zeroize = { version = "^1.5", default-features = false } +subtle = { version = "^2.4", default-features = false } +rand_core = { version = "0.6", default-features = false } + +ff = { version = "0.13", default-features = false, features = ["bits"] } +group = { version = "0.13", default-features = false } + +[features] +alloc = ["zeroize/alloc", "rand_core/alloc", "ff/alloc", "group/alloc"] +std = ["zeroize/std", "subtle/std", "rand_core/std", "ff/std"] +default = ["std"] diff --git a/crypto/short-weierstrass/LICENSE b/crypto/short-weierstrass/LICENSE new file mode 100644 index 00000000..32ff304a --- /dev/null +++ b/crypto/short-weierstrass/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2025 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crypto/short-weierstrass/README.md b/crypto/short-weierstrass/README.md new file mode 100644 index 00000000..925d56b3 --- /dev/null +++ b/crypto/short-weierstrass/README.md @@ -0,0 +1,3 @@ +# Short Weierstrass + +A library for working with elliptic curves represented in short Weierstrass form. diff --git a/crypto/short-weierstrass/src/affine.rs b/crypto/short-weierstrass/src/affine.rs new file mode 100644 index 00000000..b657e421 --- /dev/null +++ b/crypto/short-weierstrass/src/affine.rs @@ -0,0 +1,96 @@ +use core::ops::*; + +use subtle::{Choice, CtOption, ConstantTimeEq, ConditionallySelectable, ConditionallyNegatable}; +use zeroize::DefaultIsZeroes; + +use rand_core::RngCore; + +use group::ff::Field; + +use crate::{ShortWeierstrass, Projective}; + +/// A point represented with affine coordinates. +#[derive(Debug)] +pub struct Affine { + pub(crate) x: C::FieldElement, + pub(crate) y: C::FieldElement, +} +impl Clone for Affine { + fn clone(&self) -> Self { + *self + } +} +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 }) + } +} + +impl Default for Affine { + fn default() -> Self { + C::GENERATOR + } +} +impl DefaultIsZeroes for Affine {} + +impl ConstantTimeEq for Affine { + fn ct_eq(&self, other: &Self) -> Choice { + self.x.ct_eq(&other.x) & self.y.ct_eq(&other.y) + } +} +impl PartialEq for Affine { + fn eq(&self, other: &Self) -> bool { + self.ct_eq(other).into() + } +} +impl Eq for Affine {} + +impl ConditionallySelectable for Affine { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + let x = C::FieldElement::conditional_select(&a.x, &b.x, choice); + let y = C::FieldElement::conditional_select(&a.y, &b.y, choice); + Affine { x, y } + } +} + +impl Neg for Affine { + type Output = Self; + fn neg(mut self) -> Self::Output { + self.y = -self.y; + self + } +} + +impl ConditionallyNegatable for Affine { + fn conditional_negate(&mut self, negate: Choice) { + self.y = <_>::conditional_select(&self.y, &-self.y, negate); + } +} + +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 } } + + /// 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; + } + return Self { x, y }; + } + } +} diff --git a/crypto/short-weierstrass/src/lib.rs b/crypto/short-weierstrass/src/lib.rs new file mode 100644 index 00000000..84d4dcf4 --- /dev/null +++ b/crypto/short-weierstrass/src/lib.rs @@ -0,0 +1,30 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![no_std] +#![allow(non_snake_case)] + +use core::fmt::Debug; + +use zeroize::Zeroize; +use group::ff::PrimeField; + +mod affine; +pub use affine::Affine; +mod projective; +pub use projective::Projective; + +/// An elliptic curve represented in short Weierstrass form, with equation `y^2 = x^3 + A x + B`. +pub trait ShortWeierstrass: 'static + Sized + Debug { + /// The field the elliptic curve is defined over. + type FieldElement: Zeroize + PrimeField; + /// The constant `A` from the curve equation. + const A: Self::FieldElement; + /// The constant `B` from the curve equation. + const B: Self::FieldElement; + /// A generator of this elliptic curve. + const GENERATOR: Affine; + /// The scalar type. + /// + /// This may be omitted by specifying `()`. + type Scalar; +} diff --git a/crypto/short-weierstrass/src/projective.rs b/crypto/short-weierstrass/src/projective.rs new file mode 100644 index 00000000..c4386001 --- /dev/null +++ b/crypto/short-weierstrass/src/projective.rs @@ -0,0 +1,313 @@ +use core::{hint::black_box, borrow::Borrow, ops::*, iter::Sum}; + +use subtle::{Choice, ConstantTimeEq, ConditionallySelectable, ConditionallyNegatable}; +use zeroize::{Zeroize, DefaultIsZeroes}; + +use rand_core::RngCore; + +use group::{ + ff::{Field, PrimeFieldBits}, + Group, +}; + +use crate::{ShortWeierstrass, Affine}; + +/// A point represented with projective coordinates. +#[derive(Debug)] +pub struct Projective { + pub(crate) x: C::FieldElement, + pub(crate) y: C::FieldElement, + pub(crate) z: C::FieldElement, +} +impl Clone for Projective { + fn clone(&self) -> Self { + *self + } +} +impl Copy for Projective {} + +impl From> for Projective { + fn from(affine: Affine) -> Self { + Self { x: affine.x, y: affine.y, z: C::FieldElement::ONE } + } +} + +impl Default for Projective { + fn default() -> Self { + Self::IDENTITY + } +} +impl DefaultIsZeroes for Projective {} + +impl ConstantTimeEq for Projective { + fn ct_eq(&self, other: &Self) -> Choice { + let c1 = (self.x * other.z).ct_eq(&(other.x * self.z)); + let c2 = (self.y * other.z).ct_eq(&(other.y * self.z)); + c1 & c2 + } +} +impl PartialEq for Projective { + fn eq(&self, other: &Self) -> bool { + self.ct_eq(other).into() + } +} +impl Eq for Projective {} + +impl ConditionallySelectable for Projective { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + let x = C::FieldElement::conditional_select(&a.x, &b.x, choice); + let y = C::FieldElement::conditional_select(&a.y, &b.y, choice); + let z = C::FieldElement::conditional_select(&a.z, &b.z, choice); + Projective { x, y, z } + } +} + +impl Neg for Projective { + type Output = Self; + fn neg(mut self) -> Self::Output { + self.y = -self.y; + self + } +} + +impl ConditionallyNegatable for Projective { + fn conditional_negate(&mut self, negate: Choice) { + self.y = <_>::conditional_select(&self.y, &-self.y, negate); + } +} + +impl Add for Projective { + type Output = Self; + // add-1998-cmo-2 + /* + We don't use the complete formulas from 2015 as they require the curve is prime order, when we + do not. + */ + fn add(self, p2: Self) -> Self { + let Self { x: X1, y: Y1, z: Z1 } = self; + let Self { x: X2, y: Y2, z: Z2 } = p2; + + let Y1Z2 = Y1 * Z2; + let X1Z2 = X1 * Z2; + let Z1Z2 = Z1 * Z2; + let u = (Y2 * Z1) - Y1Z2; + let uu = u.square(); + let v = (X2 * Z1) - X1Z2; + let vv = v.square(); + let vvv = v * vv; + let R = vv * X1Z2; + let A = (uu * Z1Z2) - vvv - R.double(); + let X3 = v * A; + let Y3 = (u * (R - A)) - (vvv * Y1Z2); + let Z3 = vvv * Z1Z2; + + let res = Self { x: X3, y: Y3, z: Z3 }; + + let same_x_coordinate = (self.x * p2.z).ct_eq(&(p2.x * self.z)); + let same_y_coordinate = (self.y * p2.z).ct_eq(&(p2.y * self.z)); + let res = <_>::conditional_select( + &res, + &Self::IDENTITY, + (self.is_identity_internal() & p2.is_identity_internal()) | + (same_x_coordinate & (!same_y_coordinate)), + ); + let res = + <_>::conditional_select(&res, &self.double_internal(), same_x_coordinate & same_y_coordinate); + let res = <_>::conditional_select(&res, &p2, self.is_identity_internal()); + <_>::conditional_select(&res, &self, p2.is_identity_internal()) + } +} +impl Sub for Projective { + type Output = Self; + fn sub(self, p2: Self) -> Self { + self + -p2 + } +} +impl AddAssign for Projective { + fn add_assign(&mut self, p2: Self) { + *self = *self + p2; + } +} +impl SubAssign for Projective { + fn sub_assign(&mut self, p2: Self) { + *self = *self - p2; + } +} +impl Add<&Self> for Projective { + type Output = Self; + fn add(self, p2: &Self) -> Self { + self + *p2 + } +} +impl Sub<&Self> for Projective { + type Output = Self; + fn sub(self, p2: &Self) -> Self { + self - *p2 + } +} +impl AddAssign<&Self> for Projective { + fn add_assign(&mut self, p2: &Self) { + *self = *self + p2; + } +} +impl SubAssign<&Self> for Projective { + fn sub_assign(&mut self, p2: &Self) { + *self = *self - p2; + } +} + +impl Projective { + /// The additive identity, or point at infinity. + pub const IDENTITY: Self = + Projective { x: C::FieldElement::ZERO, y: C::FieldElement::ONE, z: C::FieldElement::ZERO }; + /// A generator of this elliptic curve. + pub const GENERATOR: Self = + Projective { x: C::GENERATOR.x, y: C::GENERATOR.y, z: C::FieldElement::ONE }; + + fn is_identity_internal(&self) -> Choice { + self.z.ct_eq(&C::FieldElement::ZERO) + } + + // dbl-1998-cmo-2 + fn double_internal(&self) -> Self { + let Self { x: X1, y: Y1, z: Z1 } = *self; + + let X1X1 = X1.square(); + let w = (C::A * Z1.square()) + X1X1.double() + X1X1; + let s = Y1 * Z1; + let ss = s.square(); + let sss = s * ss; + let R = Y1 * s; + let B = X1 * R; + let B4 = B.double().double(); + let h = w.square() - B4.double(); + let X3 = (h * s).double(); + let Y3 = w * (B4 - h) - R.square().double().double().double(); + let Z3 = sss.double().double().double(); + + let res = Self { x: X3, y: Y3, z: Z3 }; + <_>::conditional_select(&res, &Self::IDENTITY, self.is_identity_internal()) + } +} + +impl Sum for Projective { + fn sum>(iter: I) -> Self { + let mut res = Self::IDENTITY; + for item in iter { + res += item; + } + res + } +} + +impl<'a, C: ShortWeierstrass> Sum<&'a Self> for Projective { + fn sum>(iter: I) -> Self { + let mut res = Self::IDENTITY; + for item in iter { + res += item; + } + res + } +} + +impl> Projective { + /// Sample a random on-curve point with an unknown discrete logarithm w.r.t. any other points. + pub fn random(rng: impl RngCore) -> Self { + Self::from(Affine::random(rng)) + } + /// If this point is the additive identity. + pub fn is_identity(&self) -> Choice { + self.is_identity_internal() + } + /// The sum of this point with itself. + pub fn double(&self) -> Self { + self.double_internal() + } +} + +impl, S: Borrow> Mul for Projective { + type Output = Self; + fn mul(self, scalar: S) -> Self { + let mut table = [Self::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 scalar.borrow().to_le_bits().iter_mut().rev().enumerate() { + fn u8_from_bool(bit_ref: &mut bool) -> u8 { + 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 + } + + 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; + } + } + res + } +} +impl, S: Borrow> MulAssign + for Projective +{ + fn mul_assign(&mut self, scalar: S) { + *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 { + Self::from(Affine::::random(rng)) + } + fn identity() -> Self { + Self::IDENTITY + } + fn generator() -> Self { + C::GENERATOR.into() + } + fn is_identity(&self) -> Choice { + self.is_identity_internal() + } + fn double(&self) -> Self { + self.double_internal() + } +} diff --git a/tests/no-std/Cargo.toml b/tests/no-std/Cargo.toml index 7d5b41bf..03a51b90 100644 --- a/tests/no-std/Cargo.toml +++ b/tests/no-std/Cargo.toml @@ -29,6 +29,7 @@ multiexp = { path = "../../crypto/multiexp", default-features = false, features schnorr-signatures = { path = "../../crypto/schnorr", default-features = false } prime-field = { path = "../../crypto/prime-field", default-features = false, features = ["alloc"] } +short-weierstrass = { path = "../../crypto/short-weierstrass", default-features = false, features = ["alloc"] } secq256k1 = { path = "../../crypto/evrf/secq256k1", default-features = false } embedwards25519 = { path = "../../crypto/evrf/embedwards25519", default-features = false } diff --git a/tests/no-std/src/lib.rs b/tests/no-std/src/lib.rs index 81c7c189..b567f019 100644 --- a/tests/no-std/src/lib.rs +++ b/tests/no-std/src/lib.rs @@ -12,6 +12,7 @@ pub use multiexp; pub use schnorr_signatures; pub use prime_field; +pub use short_weierstrass; pub use secq256k1; pub use embedwards25519;