Add crate for working with short Weierstrass elliptic curves

This commit is contained in:
Luke Parker
2025-08-28 08:20:31 -04:00
parent 220bcbc592
commit f2d399ba1e
12 changed files with 528 additions and 2 deletions

View File

@@ -36,6 +36,7 @@ jobs:
-p multiexp \ -p multiexp \
-p schnorr-signatures \ -p schnorr-signatures \
-p prime-field \ -p prime-field \
-p short-weierstrass \
-p secq256k1 \ -p secq256k1 \
-p embedwards25519 \ -p embedwards25519 \
-p dkg \ -p dkg \

27
Cargo.lock generated
View File

@@ -474,7 +474,7 @@ dependencies = [
"alloy-rlp", "alloy-rlp",
"alloy-serde", "alloy-serde",
"alloy-sol-types", "alloy-sol-types",
"itertools 0.13.0", "itertools 0.14.0",
"serde", "serde",
"serde_json", "serde_json",
"serde_with", "serde_with",
@@ -2517,7 +2517,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
dependencies = [ dependencies = [
"data-encoding", "data-encoding",
"syn 2.0.106", "syn 1.0.109",
] ]
[[package]] [[package]]
@@ -2988,6 +2988,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"blake2", "blake2",
"ciphersuite 0.4.2", "ciphersuite 0.4.2",
"curve25519-dalek",
"dalek-ff-group", "dalek-ff-group",
"ec-divisors", "ec-divisors",
"ff-group-tests", "ff-group-tests",
@@ -2997,6 +2998,7 @@ dependencies = [
"hex-literal", "hex-literal",
"prime-field", "prime-field",
"rand_core 0.6.4", "rand_core 0.6.4",
"short-weierstrass",
"std-shims", "std-shims",
"zeroize", "zeroize",
] ]
@@ -4559,6 +4561,15 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.15"
@@ -10180,6 +10191,7 @@ dependencies = [
"prime-field", "prime-field",
"schnorr-signatures", "schnorr-signatures",
"secq256k1", "secq256k1",
"short-weierstrass",
] ]
[[package]] [[package]]
@@ -10911,6 +10923,17 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "short-weierstrass"
version = "0.1.0"
dependencies = [
"ff",
"group",
"rand_core 0.6.4",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.6" version = "1.4.6"

View File

@@ -40,6 +40,7 @@ members = [
"crypto/schnorr", "crypto/schnorr",
"crypto/prime-field", "crypto/prime-field",
"crypto/short-weierstrass",
"crypto/evrf/secq256k1", "crypto/evrf/secq256k1",
"crypto/evrf/embedwards25519", "crypto/evrf/embedwards25519",

View File

@@ -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_export]
macro_rules! odd_prime_field { macro_rules! odd_prime_field {
( (

View File

@@ -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 <lukeparker5132@gmail.com>"]
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"]

View File

@@ -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.

View File

@@ -0,0 +1,3 @@
# Short Weierstrass
A library for working with elliptic curves represented in short Weierstrass form.

View File

@@ -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<C: ShortWeierstrass> {
pub(crate) x: C::FieldElement,
pub(crate) y: C::FieldElement,
}
impl<C: ShortWeierstrass> Clone for Affine<C> {
fn clone(&self) -> Self {
*self
}
}
impl<C: ShortWeierstrass> Copy for Affine<C> {}
impl<C: ShortWeierstrass> Affine<C> {
pub fn try_from(projective: Projective<C>) -> CtOption<Self> {
projective.z.invert().map(|z_inv| Self { x: projective.x * z_inv, y: projective.y * z_inv })
}
}
impl<C: ShortWeierstrass> Default for Affine<C> {
fn default() -> Self {
C::GENERATOR
}
}
impl<C: ShortWeierstrass> DefaultIsZeroes for Affine<C> {}
impl<C: ShortWeierstrass> ConstantTimeEq for Affine<C> {
fn ct_eq(&self, other: &Self) -> Choice {
self.x.ct_eq(&other.x) & self.y.ct_eq(&other.y)
}
}
impl<C: ShortWeierstrass> PartialEq for Affine<C> {
fn eq(&self, other: &Self) -> bool {
self.ct_eq(other).into()
}
}
impl<C: ShortWeierstrass> Eq for Affine<C> {}
impl<C: ShortWeierstrass> ConditionallySelectable for Affine<C> {
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<C: ShortWeierstrass> Neg for Affine<C> {
type Output = Self;
fn neg(mut self) -> Self::Output {
self.y = -self.y;
self
}
}
impl<C: ShortWeierstrass> ConditionallyNegatable for Affine<C> {
fn conditional_negate(&mut self, negate: Choice) {
self.y = <_>::conditional_select(&self.y, &-self.y, negate);
}
}
impl<C: ShortWeierstrass> Affine<C> {
/// 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::<C::FieldElement>::from(y_square.sqrt()) else { continue };
if (rng.next_u64() % 2) == 1 {
y = -y;
}
return Self { x, y };
}
}
}

View File

@@ -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<Self>;
/// The scalar type.
///
/// This may be omitted by specifying `()`.
type Scalar;
}

View File

@@ -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<C: ShortWeierstrass> {
pub(crate) x: C::FieldElement,
pub(crate) y: C::FieldElement,
pub(crate) z: C::FieldElement,
}
impl<C: ShortWeierstrass> Clone for Projective<C> {
fn clone(&self) -> Self {
*self
}
}
impl<C: ShortWeierstrass> Copy for Projective<C> {}
impl<C: ShortWeierstrass> From<Affine<C>> for Projective<C> {
fn from(affine: Affine<C>) -> Self {
Self { x: affine.x, y: affine.y, z: C::FieldElement::ONE }
}
}
impl<C: ShortWeierstrass> Default for Projective<C> {
fn default() -> Self {
Self::IDENTITY
}
}
impl<C: ShortWeierstrass> DefaultIsZeroes for Projective<C> {}
impl<C: ShortWeierstrass> ConstantTimeEq for Projective<C> {
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<C: ShortWeierstrass> PartialEq for Projective<C> {
fn eq(&self, other: &Self) -> bool {
self.ct_eq(other).into()
}
}
impl<C: ShortWeierstrass> Eq for Projective<C> {}
impl<C: ShortWeierstrass> ConditionallySelectable for Projective<C> {
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<C: ShortWeierstrass> Neg for Projective<C> {
type Output = Self;
fn neg(mut self) -> Self::Output {
self.y = -self.y;
self
}
}
impl<C: ShortWeierstrass> ConditionallyNegatable for Projective<C> {
fn conditional_negate(&mut self, negate: Choice) {
self.y = <_>::conditional_select(&self.y, &-self.y, negate);
}
}
impl<C: ShortWeierstrass> Add for Projective<C> {
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<C: ShortWeierstrass> Sub for Projective<C> {
type Output = Self;
fn sub(self, p2: Self) -> Self {
self + -p2
}
}
impl<C: ShortWeierstrass> AddAssign for Projective<C> {
fn add_assign(&mut self, p2: Self) {
*self = *self + p2;
}
}
impl<C: ShortWeierstrass> SubAssign for Projective<C> {
fn sub_assign(&mut self, p2: Self) {
*self = *self - p2;
}
}
impl<C: ShortWeierstrass> Add<&Self> for Projective<C> {
type Output = Self;
fn add(self, p2: &Self) -> Self {
self + *p2
}
}
impl<C: ShortWeierstrass> Sub<&Self> for Projective<C> {
type Output = Self;
fn sub(self, p2: &Self) -> Self {
self - *p2
}
}
impl<C: ShortWeierstrass> AddAssign<&Self> for Projective<C> {
fn add_assign(&mut self, p2: &Self) {
*self = *self + p2;
}
}
impl<C: ShortWeierstrass> SubAssign<&Self> for Projective<C> {
fn sub_assign(&mut self, p2: &Self) {
*self = *self - p2;
}
}
impl<C: ShortWeierstrass> Projective<C> {
/// 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<C: ShortWeierstrass> Sum for Projective<C> {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
let mut res = Self::IDENTITY;
for item in iter {
res += item;
}
res
}
}
impl<'a, C: ShortWeierstrass> Sum<&'a Self> for Projective<C> {
fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
let mut res = Self::IDENTITY;
for item in iter {
res += item;
}
res
}
}
impl<C: ShortWeierstrass<Scalar = ()>> Projective<C> {
/// 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<C: ShortWeierstrass<Scalar: PrimeFieldBits>, S: Borrow<C::Scalar>> Mul<S> for Projective<C> {
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<C: ShortWeierstrass<Scalar: PrimeFieldBits>, S: Borrow<C::Scalar>> MulAssign<S>
for Projective<C>
{
fn mul_assign(&mut self, scalar: S) {
*self = *self * scalar.borrow();
}
}
/*
impl<C: ShortWeierstrass<Scalar: PrimeFieldBits>> Mul<&C::Scalar> for Projective<C> {
type Output = Self;
fn mul(self, scalar: &C::Scalar) -> Self {
self * *scalar
}
}
impl<C: ShortWeierstrass<Scalar: PrimeFieldBits>> MulAssign<&C::Scalar> for Projective<C> {
fn mul_assign(&mut self, scalar: &C::Scalar) {
*self *= *scalar;
}
}
*/
impl<C: ShortWeierstrass<Scalar: PrimeFieldBits>> Group for Projective<C> {
type Scalar = C::Scalar;
fn random(rng: impl RngCore) -> Self {
Self::from(Affine::<C>::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()
}
}

View File

@@ -29,6 +29,7 @@ multiexp = { path = "../../crypto/multiexp", default-features = false, features
schnorr-signatures = { path = "../../crypto/schnorr", default-features = false } schnorr-signatures = { path = "../../crypto/schnorr", default-features = false }
prime-field = { path = "../../crypto/prime-field", default-features = false, features = ["alloc"] } 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 } secq256k1 = { path = "../../crypto/evrf/secq256k1", default-features = false }
embedwards25519 = { path = "../../crypto/evrf/embedwards25519", default-features = false } embedwards25519 = { path = "../../crypto/evrf/embedwards25519", default-features = false }

View File

@@ -12,6 +12,7 @@ pub use multiexp;
pub use schnorr_signatures; pub use schnorr_signatures;
pub use prime_field; pub use prime_field;
pub use short_weierstrass;
pub use secq256k1; pub use secq256k1;
pub use embedwards25519; pub use embedwards25519;