diff --git a/curve25519/curve25519-cuda/src/lib.rs b/curve25519/curve25519-cuda/src/lib.rs index 5f3e222..5e923bc 100644 --- a/curve25519/curve25519-cuda/src/lib.rs +++ b/curve25519/curve25519-cuda/src/lib.rs @@ -59,15 +59,10 @@ fn le_bytes_to_u64x4(bytes: [u8; 32]) -> [u64; 4] { /// /// The identity is encoded as (0, 0) to match SPPARK `affine_t` rules. pub fn sw_point_to_sppark_affine(point: &SwPoint) -> SpparkAffine { - match point.to_affine_le_bytes() { - Some((x, y)) => SpparkAffine { - x: le_bytes_to_u64x4(x), - y: le_bytes_to_u64x4(y), - }, - None => SpparkAffine { - x: [0u64; 4], - y: [0u64; 4], - }, + let (x, y) = point.to_affine_le_bytes(); + SpparkAffine { + x: le_bytes_to_u64x4(x), + y: le_bytes_to_u64x4(y), } } @@ -81,13 +76,8 @@ pub fn scalar_to_sppark_scalar(scalar: &Scalar) -> SpparkScalar { /// Convert a short Weierstrass point into byte-serialized affine layout. pub fn sw_point_to_sppark_affine_bytes(point: &SwPoint) -> SpparkAffineBytes { - match point.to_affine_le_bytes() { - Some((x, y)) => SpparkAffineBytes { x, y }, - None => SpparkAffineBytes { - x: [0u8; 32], - y: [0u8; 32], - }, - } + let (x, y) = point.to_affine_le_bytes(); + SpparkAffineBytes { x, y } } /// Convert a scalar into byte-serialized layout. diff --git a/curve25519/solana-ed25519/src/short_weierstrass.rs b/curve25519/solana-ed25519/src/short_weierstrass.rs index 42a3c14..898c6ec 100644 --- a/curve25519/solana-ed25519/src/short_weierstrass.rs +++ b/curve25519/solana-ed25519/src/short_weierstrass.rs @@ -31,6 +31,12 @@ impl SwPoint { } /// Convert an Edwards point into the short Weierstrass model. + /// + /// The Edwards and short-Weierstrass models are related by a birational + /// map, so there are exceptional points that do not round-trip through + /// both directions. In particular, the non-identity Edwards point + /// `(0, -1)` maps to a short-Weierstrass affine point with `y == 0`; + /// [`Self::to_edwards`] rejects that exceptional affine point. pub fn from_edwards(point: &EdwardsPoint) -> Self { if point.is_identity() { return SwPoint::Identity; @@ -54,10 +60,17 @@ impl SwPoint { } /// Convert this point into an Edwards point, if defined. + /// + /// Returns `None` for affine coordinates that are not on the short-Weierstrass + /// curve and for exceptional affine points where the birational map is + /// undefined. pub fn to_edwards(&self) -> Option { match self { SwPoint::Identity => Some(EdwardsPoint::identity()), SwPoint::Affine { x, y } => { + if !affine_coordinates_on_curve(x, y) { + return None; + } if *y == FieldElement::ZERO { return None; } @@ -89,14 +102,18 @@ impl SwPoint { } /// Return affine coordinates as little-endian byte arrays. - pub fn to_affine_le_bytes(&self) -> Option<([u8; 32], [u8; 32])> { + /// + /// The point at infinity is encoded as the reserved all-zero pair. + pub fn to_affine_le_bytes(&self) -> ([u8; 32], [u8; 32]) { match self { - SwPoint::Identity => None, - SwPoint::Affine { x, y } => Some((x.to_bytes(), y.to_bytes())), + SwPoint::Identity => ([0u8; 32], [0u8; 32]), + SwPoint::Affine { x, y } => (x.to_bytes(), y.to_bytes()), } } /// Build a point from affine little-endian byte arrays. + /// + /// The all-zero pair is reserved as the point-at-infinity encoding. pub fn from_affine_le_bytes(x: [u8; 32], y: [u8; 32]) -> Option { if x == [0u8; 32] && y == [0u8; 32] { return Some(SwPoint::Identity); @@ -142,17 +159,19 @@ impl SwPoint { pub fn is_on_curve(&self) -> bool { match self { SwPoint::Identity => true, - SwPoint::Affine { x, y } => { - let y2 = y.square(); - let x2 = x.square(); - let x3 = &x2 * x; - let rhs = &x3 + &(&sw_a() * x); - y2 == &rhs + &sw_b() - } + SwPoint::Affine { x, y } => affine_coordinates_on_curve(x, y), } } } +fn affine_coordinates_on_curve(x: &FieldElement, y: &FieldElement) -> bool { + let y2 = y.square(); + let x2 = x.square(); + let x3 = &x2 * x; + let rhs = &x3 + &(&sw_a() * x); + y2 == &rhs + &sw_b() +} + fn double_affine(x: &FieldElement, y: &FieldElement) -> SwPoint { if *y == FieldElement::ZERO { return SwPoint::Identity; @@ -219,6 +238,8 @@ mod tests { use super::SwPoint; use super::{sw_a, sw_b}; use crate::constants; + use crate::edwards::EdwardsPoint; + use crate::field::FieldElement; use crate::scalar::Scalar; use rand::Rng; @@ -338,12 +359,12 @@ mod tests { #[test] fn sw_constants_match_expected() { - let expected_a = crate::field::FieldElement::from_bytes(&[ + let expected_a = FieldElement::from_bytes(&[ 0x44, 0xa1, 0x14, 0x49, 0x98, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x2a, ]); - let expected_b = crate::field::FieldElement::from_bytes(&[ + let expected_b = FieldElement::from_bytes(&[ 0x64, 0xc8, 0x10, 0x77, 0x9c, 0x5e, 0x0b, 0x26, 0xb4, 0x97, 0xd0, 0x5e, 0x42, 0x7b, 0x09, 0xed, 0x25, 0xb4, 0x97, 0xd0, 0x5e, 0x42, 0x7b, 0x09, 0xed, 0x25, 0xb4, 0x97, 0xd0, 0x5e, 0x42, 0x7b, @@ -352,4 +373,44 @@ mod tests { assert_eq!(sw_a(), expected_a); assert_eq!(sw_b(), expected_b); } + + #[test] + fn sw_to_edwards_rejects_off_curve_affine_input() { + let malformed = SwPoint::Affine { + x: FieldElement::ZERO, + y: FieldElement::ONE, + }; + + assert!(!malformed.is_on_curve()); + assert!(malformed.to_edwards().is_none()); + } + + #[test] + fn sw_to_edwards_rejects_order_two_exception() { + let order_two = EdwardsPoint { + X: FieldElement::ZERO, + Y: FieldElement::MINUS_ONE, + Z: FieldElement::ONE, + T: FieldElement::ZERO, + }; + let sw = SwPoint::from_edwards(&order_two); + + assert!(sw.is_on_curve()); + let SwPoint::Affine { y, .. } = sw else { + panic!("order-two point should map to affine"); + }; + assert_eq!(y, FieldElement::ZERO); + assert!(sw.to_edwards().is_none()); + } + + #[test] + fn sw_identity_affine_bytes_round_trip() { + let encoded = SwPoint::Identity.to_affine_le_bytes(); + + assert_eq!(encoded, ([0u8; 32], [0u8; 32])); + assert_eq!( + SwPoint::from_affine_le_bytes(encoded.0, encoded.1), + Some(SwPoint::Identity) + ); + } }