diff --git a/ec/src/curve_twisted_hessian.rs b/ec/src/curve_twisted_hessian.rs new file mode 100644 index 0000000..dce6f83 --- /dev/null +++ b/ec/src/curve_twisted_hessian.rs @@ -0,0 +1,229 @@ +//! Elliptic curve definition in twisted Hessian form. +//! +//! # Equation +//! +//! We use the twisted Hessian model +//! +//! $$ +//! aX^3 + Y^3 + Z^3 = 3dXYZ, +//! $$ +//! +//! together with the affine chart +//! +//! $$ +//! ax^3 + y^3 + 1 = 3dxy. +//! $$ +//! +//! The neutral element is +//! +//! $$O = (0 : -1 : 1).$$ +//! +//! # Smoothness +//! +//! The twisted Hessian curve is nonsingular when +//! +//! $$a \neq 0 \quad\text{and}\quad d^3 \neq a.$$ +//! +//! # References +//! +//! - Thomas Decru and Sabrina Kunzweiler, +//! *Tripling on Hessian curves via isogeny decomposition*, 2026. +//! - Daniel J. Bernstein, Chitchanok Chuengsatiansup, +//! David Kohel, and Tanja Lange, +//! *Twisted Hessian curves*, LATINCRYPT 2015. + +use core::fmt; + +use fp::field_ops::{FieldOps, FieldRandom}; +use fp::{ref_field_impl, ref_field_trait_impl}; +use subtle::{Choice, ConditionallySelectable, ConstantTimeEq}; + +use crate::curve_ops::Curve; +use crate::point_twisted_hessian::TwistedHessianPoint; + +/// A twisted Hessian curve +/// +/// $$aX^3 + Y^3 + Z^3 = 3dXYZ.$$ +#[derive(Debug, Clone, PartialEq, Eq, Copy)] +pub struct TwistedHessianCurve { + /// The twisting parameter `a`. + pub a: F, + /// The Hessian parameter `d`. + pub d: F, +} + +impl fmt::Display for TwistedHessianCurve +where + F: FieldOps + fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + write!( + f, + "TwistedHessianCurve {{\n aX^3 + Y^3 + Z^3 = 3dXYZ\n a = {}\n d = {}\n}}", + self.a, self.d + ) + } else { + write!(f, "({})X^3 + Y^3 + Z^3 = 3({})XYZ", self.a, self.d) + } + } +} + +ref_field_impl! { + impl TwistedHessianCurve { + /// Construct a twisted Hessian curve. + pub fn new(a: F, d: F) -> Self { + assert!(Self::is_smooth(&a, &d), "singular twisted Hessian curve"); + Self { a, d } + } + + /// Construct a twisted Hessian curve in twisted normal form (`d = 1`). + pub fn new_normal_form(a: F) -> Self { + Self::new(a, F::one()) + } + + /// Return `true` if the twisted Hessian discriminant is nonzero. + pub fn is_smooth(a: &F, d: &F) -> bool { + if bool::from(a.is_zero()) { + return false; + } + + let d2 = ::square(d); + let d3 = d * &d2; + d3 != a.clone() + } + + /// Check whether the affine point `(x, y)` satisfies + /// + /// $$ax^3 + y^3 + 1 = 3dxy.$$ + pub fn contains_affine(&self, x: &F, y: &F) -> bool { + let x2 = ::square(x); + let y2 = ::square(y); + let x3 = x * &x2; + let y3 = y * &y2; + + let lhs = &(&self.a * &x3) + &(&y3 + &F::one()); + let rhs = &(&F::from_u64(3) * &self.d) * &(x * y); + lhs == rhs + } + + /// Check whether the projective point `(X:Y:Z)` satisfies + /// + /// $$aX^3 + Y^3 + Z^3 = 3dXYZ.$$ + pub fn contains_projective(&self, x: &F, y: &F, z: &F) -> bool { + let x2 = ::square(x); + let y2 = ::square(y); + let z2 = ::square(z); + let x3 = x * &x2; + let y3 = y * &y2; + let z3 = z * &z2; + + let lhs = &(&self.a * &x3) + &(&y3 + &z3); + let rhs = &(&F::from_u64(3) * &self.d) * &(x * &(y * z)); + lhs == rhs + } + + /// Return `[a, d]`. + pub fn a_invariants(&self) -> [F; 2] { + [self.a.clone(), self.d.clone()] + } + + /// Return the neutral element `(0:-1:1)`. + pub fn neutral_point(&self) -> TwistedHessianPoint { + TwistedHessianPoint::identity() + } + + /// Best-effort random point sampling in the affine chart `Z = 1`. + pub fn random_point( + &self, + rng: &mut (impl rand::CryptoRng + rand::Rng), + ) -> TwistedHessianPoint { + loop { + let x = F::random(rng); + let y = F::random(rng); + if self.contains_affine(&x, &y) { + let p = TwistedHessianPoint::from_affine(x, y); + debug_assert!(self.is_on_curve(&p)); + return p; + } + } + } + } +} + +ref_field_trait_impl! { + impl Curve for TwistedHessianCurve { + type BaseField = F; + type Point = TwistedHessianPoint; + + fn is_on_curve(&self, point: &Self::Point) -> bool { + self.contains_projective(&point.x, &point.y, &point.z) + } + + fn random_point(&self, rng: &mut (impl rand::CryptoRng + rand::Rng)) -> Self::Point { + TwistedHessianCurve::random_point(self, rng) + } + + fn j_invariant(&self) -> F { + // Corollary 2.10 in Decru--Kunzweiler (2026): + // j(H_{a,d}) = a^{-1} * [ 3 d (8a + d^3) / (d^3 - a) ]^3. + let d2 = ::square(&self.d); + let d3 = &self.d * &d2; + + let eight_a = &F::from_u64(8) * &self.a; + let inner_num = &(&F::from_u64(3) * &self.d) * &(&eight_a + &d3); + let inner_den = &d3 - &self.a; + let inner = &inner_num + * &inner_den + .invert() + .into_option() + .expect("twisted Hessian j-invariant denominator must be invertible"); + let inner_cubed = &inner * &::square(&inner); + + &self.a + .invert() + .into_option() + .expect("a must be invertible on a nonsingular twisted Hessian curve") + * &inner_cubed + } + + fn a_invariants(&self) -> Vec { + TwistedHessianCurve::a_invariants(self).to_vec() + } + } +} + +impl ConditionallySelectable for TwistedHessianCurve +where + F: FieldOps + Copy, +{ + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + Self { + a: F::conditional_select(&a.a, &b.a, choice), + d: F::conditional_select(&a.d, &b.d, choice), + } + } + + fn conditional_assign(&mut self, other: &Self, choice: Choice) { + self.a.conditional_assign(&other.a, choice); + self.d.conditional_assign(&other.d, choice); + } + + fn conditional_swap(a: &mut Self, b: &mut Self, choice: Choice) { + F::conditional_swap(&mut a.a, &mut b.a, choice); + F::conditional_swap(&mut a.d, &mut b.d, choice); + } +} + +impl ConstantTimeEq for TwistedHessianCurve +where + F: FieldOps + Copy + ConstantTimeEq, +{ + fn ct_eq(&self, other: &Self) -> Choice { + self.a.ct_eq(&other.a) & self.d.ct_eq(&other.d) + } + + fn ct_ne(&self, other: &Self) -> Choice { + !self.ct_eq(other) + } +} diff --git a/ec/src/lib.rs b/ec/src/lib.rs index afa8cb3..8d729a2 100644 --- a/ec/src/lib.rs +++ b/ec/src/lib.rs @@ -23,4 +23,8 @@ pub mod point_hessian; /// Point used in the Legendre form. pub mod point_legendre; ///! Elliptic curve definition in Legendre form. -pub mod curve_legendre; \ No newline at end of file +pub mod curve_legendre; +/// Twisted Hessian curves. +pub mod curve_twisted_hessian; +/// Projective points on twisted Hessian curves. +pub mod point_twisted_hessian; diff --git a/ec/src/point_twisted_hessian.rs b/ec/src/point_twisted_hessian.rs new file mode 100644 index 0000000..44e52c6 --- /dev/null +++ b/ec/src/point_twisted_hessian.rs @@ -0,0 +1,316 @@ +//! Projective points on a twisted Hessian curve. +//! +//! We represent points on +//! +//! $$aX^3 + Y^3 + Z^3 = 3dXYZ$$ +//! +//! by projective triples `(X:Y:Z)`. +//! +//! The formulas implemented here follow the twisted Hessian group law from +//! Decru--Kunzweiler (2026), ยง2.2 / Remark 2.11. + +use core::fmt; + +use subtle::{Choice, ConditionallySelectable, ConstantTimeEq}; + +use crate::curve_ops::Curve; +use crate::curve_twisted_hessian::TwistedHessianCurve; +use crate::point_ops::{PointAdd, PointOps}; +use fp::field_ops::{FieldOps, FieldRandom}; +use fp::{ref_field_impl, ref_field_trait_impl}; + +/// A projective point `(X:Y:Z)` on a twisted Hessian curve. +#[derive(Debug, Clone, Copy)] +pub struct TwistedHessianPoint { + /// Projective `X` coordinate. + pub x: F, + /// Projective `Y` coordinate. + pub y: F, + /// Projective `Z` coordinate. + pub z: F, +} + +impl fmt::Display for TwistedHessianPoint +where + F: FieldOps + fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_identity() { + if f.alternate() { + write!(f, "TwistedHessianPoint {{ O = (0:-1:1) }}") + } else { + write!(f, "O") + } + } else if self.is_zero_projective() { + if f.alternate() { + write!(f, "TwistedHessianPoint {{ invalid = (0:0:0) }}") + } else { + write!(f, "(0:0:0)") + } + } else if let Some((x_aff, y_aff)) = self.to_affine() { + if f.alternate() { + write!( + f, + "TwistedHessianPoint {{ X:Y:Z = ({}:{}:{}), x = {}, y = {} }}", + self.x, self.y, self.z, x_aff, y_aff + ) + } else { + write!(f, "({}, {})", x_aff, y_aff) + } + } else if f.alternate() { + write!( + f, + "TwistedHessianPoint {{ X:Y:Z = ({}:{}:{}) }}", + self.x, self.y, self.z + ) + } else { + write!(f, "({}:{}:{})", self.x, self.y, self.z) + } + } +} + +ref_field_trait_impl! { + impl PartialEq for TwistedHessianPoint { + fn eq(&self, other: &Self) -> bool { + let self_zero = self.is_zero_projective(); + let other_zero = other.is_zero_projective(); + if self_zero || other_zero { + return self_zero && other_zero; + } + + &self.x * &other.y == &other.x * &self.y + && &self.x * &other.z == &other.x * &self.z + && &self.y * &other.z == &other.y * &self.z + } + } +} + +ref_field_trait_impl! { + impl Eq for TwistedHessianPoint {} +} + +impl TwistedHessianPoint { + /// Construct a projective twisted Hessian point without validation. + pub fn new(x: F, y: F, z: F) -> Self { + Self { x, y, z } + } + + /// Construct the affine point `(x, y)` as `(x:y:1)`. + pub fn from_affine(x: F, y: F) -> Self { + Self { x, y, z: F::one() } + } + + /// Return the neutral element `(0:-1:1)`. + pub fn identity() -> Self { + let one = F::one(); + let minus_one = ::negate(&one); + Self { + x: F::zero(), + y: minus_one, + z: F::one(), + } + } + + /// Return `true` when the point is the neutral element. + pub fn is_identity(&self) -> bool { + if self.is_zero_projective() { + return false; + } + + bool::from(self.x.is_zero()) && F::add(&self.y, &self.z) == F::zero() + } + + /// Return `true` if this is the invalid projective triple `(0:0:0)`. + pub fn is_zero_projective(&self) -> bool { + bool::from(self.x.is_zero()) + && bool::from(self.y.is_zero()) + && bool::from(self.z.is_zero()) + } + + /// Return `true` if the point lies on the line at infinity. + pub fn is_at_infinity(&self) -> bool { + bool::from(self.z.is_zero()) && !self.is_zero_projective() + } + + /// Convert a finite projective point to affine coordinates. + pub fn to_affine(&self) -> Option<(F, F)> { + self.z.invert().into_option().map(|zinv| { + ( + ::mul(&self.x, &zinv), + ::mul(&self.y, &zinv), + ) + }) + } +} + +impl ConditionallySelectable for TwistedHessianPoint +where + F: FieldOps + Copy, +{ + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + Self { + x: F::conditional_select(&a.x, &b.x, choice), + y: F::conditional_select(&a.y, &b.y, choice), + z: F::conditional_select(&a.z, &b.z, choice), + } + } + + fn conditional_assign(&mut self, other: &Self, choice: Choice) { + self.x.conditional_assign(&other.x, choice); + self.y.conditional_assign(&other.y, choice); + self.z.conditional_assign(&other.z, choice); + } + + fn conditional_swap(a: &mut Self, b: &mut Self, choice: Choice) { + F::conditional_swap(&mut a.x, &mut b.x, choice); + F::conditional_swap(&mut a.y, &mut b.y, choice); + F::conditional_swap(&mut a.z, &mut b.z, choice); + } +} + +ref_field_trait_impl! { + impl ConstantTimeEq for TwistedHessianPoint { + fn ct_eq(&self, other: &Self) -> Choice { + let self_zero = self.is_zero_projective(); + let other_zero = other.is_zero_projective(); + if self_zero || other_zero { + return Choice::from((self_zero && other_zero) as u8); + } + + (&self.x * &other.y).ct_eq(&(&other.x * &self.y)) + & (&self.x * &other.z).ct_eq(&(&other.x * &self.z)) + & (&self.y * &other.z).ct_eq(&(&other.y * &self.z)) + } + + fn ct_ne(&self, other: &Self) -> Choice { + !self.ct_eq(other) + } + } +} + +ref_field_impl! { + impl TwistedHessianPoint { + /// Negation on a twisted Hessian curve: + /// + /// $$-(X:Y:Z) = (X:Z:Y).$$ + pub fn negate(&self, _curve: &TwistedHessianCurve) -> Self { + Self::new(self.x.clone(), self.z.clone(), self.y.clone()) + } + + /// First addition formula from Remark 2.11. + fn add_formula_1(&self, other: &Self) -> Self { + let x1_sq = ::square(&self.x); + let y1_sq = ::square(&self.y); + let z1_sq = ::square(&self.z); + let x2_sq = ::square(&other.x); + let y2_sq = ::square(&other.y); + let z2_sq = ::square(&other.z); + + let x3 = &(&x1_sq * &(&other.y * &other.z)) - &(&x2_sq * &(&self.y * &self.z)); + let y3 = &(&z1_sq * &(&other.x * &other.y)) - &(&z2_sq * &(&self.x * &self.y)); + let z3 = &(&y1_sq * &(&other.x * &other.z)) - &(&y2_sq * &(&self.x * &self.z)); + + Self::new(x3, y3, z3) + } + + /// Second addition formula from Remark 2.11. + fn add_formula_2(&self, other: &Self, curve: &TwistedHessianCurve) -> Self { + let x1_sq = ::square(&self.x); + let y1_sq = ::square(&self.y); + let z1_sq = ::square(&self.z); + let x2_sq = ::square(&other.x); + let y2_sq = ::square(&other.y); + let z2_sq = ::square(&other.z); + + let x3 = &(&z2_sq * &(&self.x * &self.z)) - &(&y1_sq * &(&other.x * &other.y)); + let y3 = &(&y2_sq * &(&self.y * &self.z)) - &(&curve.a * &(&x1_sq * &(&other.x * &other.z))); + let z3 = &(&curve.a * &(&x2_sq * &(&self.x * &self.y))) - &(&z1_sq * &(&other.y * &other.z)); + + Self::new(x3, y3, z3) + } + + /// Add two projective twisted Hessian points. + /// + /// The first branch is used generically; the second branch covers the + /// complementary exceptional set. + pub fn add(&self, other: &Self, curve: &TwistedHessianCurve) -> Self { + if self.is_identity() { + return other.clone(); + } + if other.is_identity() { + return self.clone(); + } + + debug_assert!(curve.is_on_curve(self)); + debug_assert!(curve.is_on_curve(other)); + + let r = self.add_formula_1(other); + if !r.is_zero_projective() { + return r; + } + + let s = self.add_formula_2(other, curve); + if !s.is_zero_projective() { + return s; + } + + if other == &self.negate(curve) { + return Self::identity(); + } + + panic!("twisted Hessian addition failed for valid-looking inputs; both unified formula branches vanished"); + } + + /// Doubling via the unified addition formulas. + pub fn double(&self, curve: &TwistedHessianCurve) -> Self { + self.add(self, curve) + } + + /// Variable-time double-and-add scalar multiplication. + pub fn scalar_mul(&self, k: &[u64], curve: &TwistedHessianCurve) -> Self { + let mut result = Self::identity(); + + for &limb in k.iter().rev() { + for bit in (0..64).rev() { + let doubled = result.double(curve); + let added = doubled.add(self, curve); + let choice = Choice::from(((limb >> bit) & 1) as u8); + result = Self::conditional_select(&doubled, &added, choice); + } + } + + result + } + } +} + +ref_field_trait_impl! { + impl PointOps for TwistedHessianPoint { + type BaseField = F; + type Curve = TwistedHessianCurve; + + fn identity(_curve: &Self::Curve) -> Self { + TwistedHessianPoint::::identity() + } + + fn is_identity(&self) -> bool { + TwistedHessianPoint::::is_identity(self) + } + + fn negate(&self, curve: &Self::Curve) -> Self { + TwistedHessianPoint::::negate(self, curve) + } + + fn scalar_mul(&self, k: &[u64], curve: &Self::Curve) -> Self { + TwistedHessianPoint::::scalar_mul(self, k, curve) + } + } +} + +ref_field_trait_impl! { + impl PointAdd for TwistedHessianPoint { + fn add(&self, other: &Self, curve: &Self::Curve) -> Self { + TwistedHessianPoint::::add(self, other, curve) + } + } +} diff --git a/ec/tests/hessian_tests.rs b/ec/tests/hessian_tests.rs index d3b0cc2..c43c9c2 100644 --- a/ec/tests/hessian_tests.rs +++ b/ec/tests/hessian_tests.rs @@ -122,6 +122,15 @@ fn hessian_addition_is_associative_on_complete_curve() { } } +#[test] +fn hessian_double_matches_add_self() { + let curve = complete_curve(); + + for p in all_points(&curve) { + assert_eq!(p.double(&curve), p.add(&p, &curve), "[2]P mismatch for P={:?}", p); + } +} + #[test] fn hessian_scalar_mul_matches_repeated_addition() { let curve = complete_curve(); diff --git a/ec/tests/twisted_hessian_tests.rs b/ec/tests/twisted_hessian_tests.rs new file mode 100644 index 0000000..cdc2ef5 --- /dev/null +++ b/ec/tests/twisted_hessian_tests.rs @@ -0,0 +1,188 @@ +//! Integration tests for twisted Hessian curves. + +use crypto_bigint::{Uint, const_prime_monty_params}; + +use ec::curve_ops::Curve; +use ec::curve_twisted_hessian::TwistedHessianCurve; +use ec::point_ops::{PointAdd, PointOps}; +use ec::point_twisted_hessian::TwistedHessianPoint; +use fp::field_ops::FieldOps; +use fp::fp_element::FpElement; + +const_prime_monty_params!(Fp103Mod, Uint<1>, "0000000000000067", 2); +type F103 = FpElement; + +fn f(n: i64) -> F103 { + F103::from_u64(n.rem_euclid(103) as u64) +} + +fn test_curve() -> TwistedHessianCurve { + // Twisted normal form a = 8, d = 1. + // Since 1^3 != 8 in F_103, this curve is nonsingular. + TwistedHessianCurve::new_normal_form(f(8)) +} + +fn infinity_points(curve: &TwistedHessianCurve) -> Vec> { + let mut pts = Vec::new(); + + for y in 0..103i64 { + let p = TwistedHessianPoint::new(F103::one(), f(y), F103::zero()); + if curve.is_on_curve(&p) { + pts.push(p); + } + } + + pts +} + +fn all_points(curve: &TwistedHessianCurve) -> Vec> { + let mut pts = Vec::new(); + + for x in 0..103i64 { + for y in 0..103i64 { + if curve.contains_affine(&f(x), &f(y)) { + pts.push(TwistedHessianPoint::from_affine(f(x), f(y))); + } + } + } + + pts.extend(infinity_points(curve)); + pts +} + +#[test] +fn twisted_hessian_identity_is_on_curve() { + let curve = test_curve(); + let id = TwistedHessianPoint::::identity(); + assert!(curve.is_on_curve(&id)); + assert!(id.is_identity()); +} + +#[test] +fn twisted_hessian_infinity_points_are_on_curve() { + let curve = test_curve(); + let inf = infinity_points(&curve); + assert!(!inf.is_empty()); + + for p in inf { + assert!(curve.is_on_curve(&p)); + assert!(p.is_at_infinity()); + } +} + +#[test] +fn twisted_hessian_identity_is_neutral_for_all_points() { + let curve = test_curve(); + let id = TwistedHessianPoint::::identity(); + + for p in all_points(&curve) { + assert_eq!(p.add(&id, &curve), p, "P + O != P for P={:?}", p); + assert_eq!(id.add(&p, &curve), p, "O + P != P for P={:?}", p); + } +} + +#[test] +fn twisted_hessian_negation_works_for_all_points() { + let curve = test_curve(); + let id = TwistedHessianPoint::::identity(); + + for p in all_points(&curve) { + let neg = p.negate(&curve); + assert_eq!(p.add(&neg, &curve), id, "P + (-P) != O for P={:?}", p); + assert_eq!(neg.add(&p, &curve), id, "(-P) + P != O for P={:?}", p); + } +} + +#[test] +fn twisted_hessian_double_matches_add_self() { + let curve = test_curve(); + + for p in all_points(&curve) { + assert_eq!(p.double(&curve), p.add(&p, &curve), "[2]P mismatch for P={:?}", p); + } +} + +#[test] +fn twisted_hessian_addition_is_commutative() { + let curve = test_curve(); + let pts = all_points(&curve); + + for p in &pts { + for q in &pts { + assert_eq!( + p.add(q, &curve), + q.add(p, &curve), + "P+Q != Q+P for P={:#}, Q={:#}", + p, + q, + ); + } + } +} + +#[test] +fn twisted_hessian_addition_is_associative() { + let curve = test_curve(); + let pts = all_points(&curve); + + for p in &pts { + for q in &pts { + for r in &pts { + assert_eq!( + p.add(q, &curve).add(r, &curve), + p.add(&q.add(r, &curve), &curve), + "(P+Q)+R != P+(Q+R) for P={:#}, Q={:#}, R={:#}", + p, + q, + r, + ); + } + } + } +} + +#[test] +fn twisted_hessian_scalar_mul_matches_repeated_addition() { + let curve = test_curve(); + let p = TwistedHessianPoint::from_affine(f(16), f(101)); + assert!(curve.is_on_curve(&p)); + + let seven_p = p.scalar_mul(&[7], &curve); + let mut acc = TwistedHessianPoint::identity(); + for _ in 0..7 { + acc = acc.add(&p, &curve); + } + + assert_eq!(seven_p, acc); +} + +#[test] +fn twisted_hessian_group_order_annihilates_points() { + let curve = test_curve(); + let pts = all_points(&curve); + let order = pts.len() as u64; + + for p in pts.iter().take(12) { + assert_eq!( + p.scalar_mul(&[order], &curve), + TwistedHessianPoint::identity(), + "[order]P != O for P={:?}", + p, + ); + } +} + +#[test] +fn twisted_hessian_j_invariant_matches_formula() { + let curve = test_curve(); + + let d2 = ::square(&curve.d); + let d3 = &curve.d * &d2; + let num = &(&F103::from_u64(3) * &curve.d) * &(&(&F103::from_u64(8) * &curve.a) + &d3); + let den = &d3 - &curve.a; + let inner = &num * &den.invert().into_option().expect("nonzero denominator"); + let expected = &curve.a.invert().into_option().expect("a invertible") + * &(&inner * &::square(&inner)); + + assert_eq!(curve.j_invariant(), expected); +}