Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5ee77c2
Rename to `MontgomeryPoint` to `MontgomeryXpoint`
daxpedda Jul 18, 2025
95d3a59
Move `MontgomeryXpoint` to its own module
daxpedda Jul 18, 2025
4395460
Implement `Eq` for `ProjectiveMontgomeryXpoint`
daxpedda Jul 18, 2025
c4f304b
Change `MontgomeryPoint` scalar multiplication output to `ProjectiveM…
daxpedda Jul 18, 2025
869d8f4
Add y-coordinate recovery
daxpedda Jul 18, 2025
9984f67
Make `ProjectiveMontgomeryXpoint::identity()` an associated const
daxpedda Jul 18, 2025
f1ad279
Add `ProjectiveMontgomeryXpoint::GENERATOR`
daxpedda Jul 18, 2025
d625aa6
Document source of `montgomery::differential_add_and_double`
daxpedda Jul 18, 2025
f7e6343
Expose Montgomery ladder with additional output internally
daxpedda Jul 18, 2025
505d07b
Add `ProjectiveMontgomeryXpoint::double()`
daxpedda Jul 18, 2025
4b34d77
Add full-coordinate `MontgomeryPoint` skeleton
daxpedda Jul 18, 2025
c1de016
Drop `CurveArithmetic` requirement from `CurveWithScalar`
daxpedda Jul 18, 2025
1e14595
Add `MontgomeryScalar`
daxpedda Jul 18, 2025
72728a8
Add arithmetic operations to Montgomery
daxpedda Jul 18, 2025
16d8b04
Add Montgomery <-> Edwards conversions
daxpedda Jul 18, 2025
416725b
Implement `CurveArithmetic` for Curve448
daxpedda Jul 18, 2025
7367229
Implement `Reduce` for `MontgomeryScalar`
daxpedda Jul 19, 2025
a0a92ba
Implement hash2curve for `Curve448`
daxpedda Jul 19, 2025
0ffef93
Add hash2curve to `ProjectiveMontgomeryXpoint`
daxpedda Jul 19, 2025
c1d8781
Test Montgomery -> Edwards through hash2curve
daxpedda Jul 19, 2025
16762d9
Rename `MontgomeryPoint` fields from `x|y` to `U|V`
daxpedda Jul 26, 2025
98983ea
Rename `MontgomeryPoint` to `AffineMontgomeryPoint`
daxpedda Jul 26, 2025
3522ca5
Not every full-coordinate Montgomery point is valid
daxpedda Aug 3, 2025
7f9f3f4
Add benchmarks
daxpedda Sep 2, 2025
4071bb7
Optimize Montgomery <-> Edwards conversions
daxpedda Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions ed448-goldilocks/benches/bench.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
use ed448_goldilocks::{
Decaf448, DecafPoint, DecafScalar, Ed448, EdwardsPoint, EdwardsScalar, MontgomeryPoint,
Curve448, Decaf448, DecafPoint, DecafScalar, Ed448, EdwardsPoint, EdwardsScalar,
MontgomeryScalar, MontgomeryXpoint, ProjectiveMontgomeryPoint, ProjectiveMontgomeryXpoint,
};
use elliptic_curve::group::GroupEncoding;
use elliptic_curve::{Field, Group};
Expand Down Expand Up @@ -123,22 +124,76 @@ pub fn decaf448(c: &mut Criterion) {
group.finish();
}

pub fn curve448(c: &mut Criterion) {
let mut group = c.benchmark_group("Curve448");

group.bench_function("scalar multiplication", |b| {
b.iter_batched(
|| {
let point = ProjectiveMontgomeryPoint::try_from_rng(&mut OsRng).unwrap();
let scalar = MontgomeryScalar::try_from_rng(&mut OsRng).unwrap();
(point, scalar)
},
|(point, scalar)| point * scalar,
BatchSize::SmallInput,
)
});

group.bench_function("point addition", |b| {
b.iter_batched(
|| {
let p1 = ProjectiveMontgomeryPoint::try_from_rng(&mut OsRng).unwrap();
let p2 = ProjectiveMontgomeryPoint::try_from_rng(&mut OsRng).unwrap();
(p1, p2)
},
|(p1, p2)| p1 + p2,
BatchSize::SmallInput,
)
});

group.bench_function("encode_to_curve", |b| {
b.iter_batched(
|| {
let mut msg = [0; 64];
OsRng.try_fill_bytes(&mut msg).unwrap();
msg
},
|msg| Curve448::encode_from_bytes(&msg, b"test DST"),
BatchSize::SmallInput,
)
});

group.finish();
}

pub fn x448(c: &mut Criterion) {
let mut group = c.benchmark_group("X448");

group.bench_function("scalar multiplication", |b| {
b.iter_batched(
|| {
let mut point = MontgomeryPoint::default();
let mut point = MontgomeryXpoint::default();
OsRng.try_fill_bytes(&mut point.0).unwrap();
let scalar = EdwardsScalar::try_from_rng(&mut OsRng).unwrap();
let scalar = MontgomeryScalar::try_from_rng(&mut OsRng).unwrap();
(point, scalar)
},
|(point, scalar)| &point * &scalar,
BatchSize::SmallInput,
)
});

group.bench_function("encode_to_curve", |b| {
b.iter_batched(
|| {
let mut msg = [0; 64];
OsRng.try_fill_bytes(&mut msg).unwrap();
msg
},
|msg| ProjectiveMontgomeryXpoint::encode_with_defaults(&msg, b"test DST"),
BatchSize::SmallInput,
)
});

group.finish();
}

Expand Down
31 changes: 0 additions & 31 deletions ed448-goldilocks/src/edwards/affine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,37 +95,6 @@ impl AffinePoint {
}
}

pub(crate) fn isogeny(&self) -> Self {
let x = self.x;
let y = self.y;
let mut t0 = x.square(); // x^2
let t1 = t0 + FieldElement::ONE; // x^2+1
t0 -= FieldElement::ONE; // x^2-1
let mut t2 = y.square(); // y^2
t2 = t2.double(); // 2y^2
let t3 = x.double(); // 2x

let mut t4 = t0 * y; // y(x^2-1)
t4 = t4.double(); // 2y(x^2-1)
let xNum = t4.double(); // xNum = 4y(x^2-1)

let mut t5 = t0.square(); // x^4-2x^2+1
t4 = t5 + t2; // x^4-2x^2+1+2y^2
let xDen = t4 + t2; // xDen = x^4-2x^2+1+4y^2

t5 *= x; // x^5-2x^3+x
t4 = t2 * t3; // 4xy^2
let yNum = t4 - t5; // yNum = -(x^5-2x^3+x-4xy^2)

t4 = t1 * t2; // 2x^2y^2+2y^2
let yDen = t5 - t4; // yDen = x^5-2x^3+x-2x^2y^2-2y^2

Self {
x: xNum * xDen.invert(),
y: yNum * yDen.invert(),
}
}

/// Standard compression; store Y and sign of X
pub fn compress(&self) -> CompressedEdwardsY {
let affine_x = self.x;
Expand Down
57 changes: 46 additions & 11 deletions ed448-goldilocks/src/edwards/extended.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,8 @@ impl EdwardsPoint {
T: FieldElement::ZERO,
};

/// Convert this point to [`MontgomeryPoint`]
pub fn to_montgomery(&self) -> MontgomeryPoint {
/// Convert this point to [`MontgomeryXpoint`]
pub fn to_montgomery_x(&self) -> MontgomeryXpoint {
// u = y^2 * [(1-dy^2)/(1-y^2)]

let affine = self.to_affine();
Expand All @@ -322,7 +322,31 @@ impl EdwardsPoint {

let u = yy * (FieldElement::ONE - dyy) * (FieldElement::ONE - yy).invert();

MontgomeryPoint(u.to_bytes())
MontgomeryXpoint(u.to_bytes())
}

/// Convert this point to [`AffineMontgomeryPoint`]
// See https://www.rfc-editor.org/rfc/rfc7748#section-4.2 4-isogeny maps
pub fn to_montgomery(&self) -> AffineMontgomeryPoint {
// u = y^2/x^2
// v = (2 - x^2 - y^2)*y/x^3

let affine = self.to_affine();

let xx = affine.x.square();
let yy = affine.y.square();

let v_den = xx * affine.x;
let den = (xx * v_den).invert();

let u = yy * v_den * den;
let v = (FieldElement::TWO - xx - yy) * affine.y * xx * den;

AffineMontgomeryPoint::conditional_select(
&AffineMontgomeryPoint::new(u, v),
&AffineMontgomeryPoint::IDENTITY,
self.ct_eq(&Self::IDENTITY),
)
}

/// Generic scalar multiplication to compute s*P
Expand Down Expand Up @@ -786,6 +810,7 @@ mod tests {
use hex_literal::hex;
use proptest::{prop_assert_eq, property_test};
use rand_core::{OsRng, TryRngCore};
use sha3::Shake256;

fn hex_to_field(hex: &'static str) -> FieldElement {
assert_eq!(hex.len(), 56 * 2);
Expand Down Expand Up @@ -946,9 +971,7 @@ mod tests {
];

for (msg, x, y) in MSGS {
let p =
hash2curve::hash_from_bytes::<Ed448, ExpandMsgXof<sha3::Shake256>>(&[msg], &[DST])
.unwrap();
let p = Ed448::hash_from_bytes(msg, DST).unwrap();
assert_eq!(p.is_on_curve().unwrap_u8(), 1u8);
let p = p.to_affine();
let mut xx = [0u8; 56];
Expand Down Expand Up @@ -985,11 +1008,7 @@ mod tests {
];

for (msg, x, y) in MSGS {
let p = hash2curve::encode_from_bytes::<Ed448, ExpandMsgXof<sha3::Shake256>>(
&[msg],
&[DST],
)
.unwrap();
let p = Ed448::encode_from_bytes(msg, DST).unwrap();
assert_eq!(p.is_on_curve().unwrap_u8(), 1u8);
let p = p.to_affine();
let mut xx = [0u8; 56];
Expand All @@ -1000,6 +1019,22 @@ mod tests {
yy.reverse();
assert_eq!(p.x.to_bytes(), xx);
assert_eq!(p.y.to_bytes(), yy);

// Test Montgomery to Edwards conversion.
// See https://github.com/cfrg/draft-irtf-cfrg-hash-to-curve/blob/664b13592116cecc9e52fb192dcde0ade36f904e/poc/ell2_opt_3mod4.sage#L243-L245.
let conv_p =
ProjectiveMontgomeryXpoint::encode::<ExpandMsgXof<Shake256>>(&[msg], &[DST])
.unwrap()
.to_affine();
let conv_p1 = conv_p.to_edwards(Choice::from(0)).unwrap();
let conv_p2 = conv_p.to_edwards(Choice::from(1)).unwrap();
assert!(conv_p1.x == p.x || conv_p2.x == p.x);
assert!(conv_p1.y == p.y || conv_p2.y == p.y);

let conv_p =
AffinePoint::from(Curve448::encode_from_bytes(msg, DST).unwrap().to_affine());
assert_eq!(conv_p.x, p.x);
assert_eq!(conv_p.y, p.y);
}
}

Expand Down
16 changes: 3 additions & 13 deletions ed448-goldilocks/src/edwards/scalar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use crate::Ed448;
use crate::field::{CurveWithScalar, ORDER, Scalar, ScalarBytes, WideScalarBytes};

use elliptic_curve::array::Array;
use elliptic_curve::bigint::{Limb, NonZero, U448, U704};
use elliptic_curve::consts::{U57, U84, U88};
use elliptic_curve::bigint::{Limb, U448};
use elliptic_curve::consts::{U57, U84};
use elliptic_curve::ops::Reduce;
use elliptic_curve::scalar::FromUintUnchecked;
use subtle::{Choice, ConstantTimeEq, CtOption};
Expand Down Expand Up @@ -79,17 +79,7 @@ impl From<&EdwardsScalar> for elliptic_curve::scalar::ScalarBits<Ed448> {

impl Reduce<Array<u8, U84>> for EdwardsScalar {
fn reduce(value: &Array<u8, U84>) -> Self {
const SEMI_WIDE_MODULUS: NonZero<U704> = NonZero::<U704>::new_unwrap(U704::from_be_hex(
"00000000000000000000000000000000000000000000000000000000000000003fffffffffffffffffffffffffffffffffffffffffffffffffffffff7cca23e9c44edb49aed63690216cc2728dc58f552378c292ab5844f3",
));
let mut tmp = Array::<u8, U88>::default();
tmp[4..].copy_from_slice(&value[..]);

let mut num = U704::from_be_slice(&tmp[..]);
num %= SEMI_WIDE_MODULUS;
let mut words = [0; U448::LIMBS];
words.copy_from_slice(&num.to_words()[..U448::LIMBS]);
Scalar::new(U448::from_words(words))
Self::from_okm_u84(value)
}
}

Expand Down
45 changes: 38 additions & 7 deletions ed448-goldilocks/src/field/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use core::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign};

use super::{ConstMontyType, MODULUS};
use crate::{
AffinePoint, Decaf448, DecafPoint, Ed448, EdwardsPoint,
curve::twedwards::extended::ExtendedPoint as TwistedExtendedPoint,
AffineMontgomeryPoint, AffinePoint, Curve448, Decaf448, DecafPoint, Ed448, EdwardsPoint,
ProjectiveMontgomeryPoint, curve::twedwards::extended::ExtendedPoint as TwistedExtendedPoint,
};
use elliptic_curve::ops::Reduce;
use elliptic_curve::{
Expand Down Expand Up @@ -196,7 +196,7 @@ impl MapToCurve for Ed448 {
type Length = U84;

fn map_to_curve(element: FieldElement) -> EdwardsPoint {
element.map_to_curve_elligator2().isogeny().to_edwards()
AffinePoint::from(element.map_to_curve_elligator2_curve448()).to_edwards()
}
}

Expand Down Expand Up @@ -272,6 +272,16 @@ impl Field for FieldElement {
}
}

impl MapToCurve for Curve448 {
type SecurityLevel = U28;
type FieldElement = FieldElement;
type Length = U84;

fn map_to_curve(element: FieldElement) -> ProjectiveMontgomeryPoint {
element.map_to_curve_elligator2_curve448().into()
}
}

impl FieldElement {
pub const A_PLUS_TWO_OVER_FOUR: Self = Self(ConstMontyType::new(&U448::from_be_hex(
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000098aa",
Expand Down Expand Up @@ -383,6 +393,10 @@ impl FieldElement {
Self(self.0.double())
}

pub fn triple(&self) -> Self {
self.double() + self
}

/// Computes the inverse square root of a field element
/// Returns the result and a boolean to indicate whether self
/// was a Quadratic residue
Expand Down Expand Up @@ -436,7 +450,7 @@ impl FieldElement {
Self(self.0.div_by_2())
}

pub(crate) fn map_to_curve_elligator2(&self) -> AffinePoint {
pub(crate) fn map_to_curve_elligator2_curve448(&self) -> AffineMontgomeryPoint {
let mut t1 = self.square(); // 1. t1 = u^2
t1 *= Self::Z; // 2. t1 = Z * t1 // Z * u^2
let e1 = t1.ct_eq(&Self::MINUS_ONE); // 3. e1 = t1 == -1 // exceptional case: Z * u^2 == -1
Expand All @@ -456,7 +470,26 @@ impl FieldElement {
let mut y = y2.sqrt(); // 17. y = sqrt(y2)
let e3 = y.is_negative(); // 18. e3 = sgn0(y) == 1
y.conditional_negate(e2 ^ e3); // y = CMOV(-y, y, e2 xor e3)
AffinePoint { x, y }
AffineMontgomeryPoint::new(x, y)
}

// See https://www.rfc-editor.org/rfc/rfc9380.html#name-curve448-q-3-mod-4-k-1.
// Without y-coordinate.
pub(crate) fn map_to_curve_elligator2_curve448_x(&self) -> FieldElement {
let mut t1 = self.square(); // 1. t1 = u^2
t1 *= Self::Z; // 2. t1 = Z * t1 // Z * u^2
let e1 = t1.ct_eq(&Self::MINUS_ONE); // 3. e1 = t1 == -1 // exceptional case: Z * u^2 == -1
t1.conditional_assign(&Self::ZERO, e1); // 4. t1 = CMOV(t1, 0, e1) // if t1 == -1, set t1 = 0
let mut x1 = t1 + Self::ONE; // 5. x1 = t1 + 1
x1 = x1.invert(); // 6. x1 = inv0(x1)
x1 *= -Self::J; // 7. x1 = -A * x1 // x1 = -A / (1 + Z * u^2)
let mut gx1 = x1 + Self::J; // 8. gx1 = x1 + A
gx1 *= x1; // 9. gx1 = gx1 * x1
gx1 += Self::ONE; // 10. gx1 = gx1 + B
gx1 *= x1; // 11. gx1 = gx1 * x1 // gx1 = x1^3 + A * x1^2 + B * x1
let x2 = -x1 - Self::J; // 12. x2 = -x1 - A
let e2 = gx1.is_square(); // 14. e2 = is_square(gx1)
Self::conditional_select(&x2, &x1, e2) // 15. x = CMOV(x2, x1, e2) // If is_square(gx1), x = x1, else x = x2
}

// See https://www.shiftleft.org/papers/decaf/decaf.pdf#section.A.3.
Expand Down Expand Up @@ -533,15 +566,13 @@ mod tests {
.unwrap();
let mut data = Array::<u8, U84>::default();
expander.fill_bytes(&mut data).unwrap();
// TODO: This should be `Curve448FieldElement`.
let u0 = FieldElement::reduce(&data);
let mut e_u0 = *expected_u0;
e_u0.reverse();
let mut e_u1 = *expected_u1;
e_u1.reverse();
assert_eq!(u0.to_bytes(), e_u0);
expander.fill_bytes(&mut data).unwrap();
// TODO: This should be `Curve448FieldElement`.
let u1 = FieldElement::reduce(&data);
assert_eq!(u1.to_bytes(), e_u1);
}
Expand Down
Loading