Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions ed448-goldilocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ It is intended to be portable, fast, and safe.
```rust
use ed448_goldilocks::{Ed448, EdwardsPoint, CompressedEdwardsY, EdwardsScalar, sha3::Shake256};
use elliptic_curve::Field;
use elliptic_curve::group::GroupEncoding;
use hash2curve::{ExpandMsgXof, GroupDigest};
use rand_core::OsRng;

Expand All @@ -29,9 +30,9 @@ assert_eq!(public_key, EdwardsPoint::GENERATOR + EdwardsPoint::GENERATOR);

let secret_key = EdwardsScalar::try_from_rng(&mut OsRng).unwrap();
let public_key = EdwardsPoint::GENERATOR * &secret_key;
let compressed_public_key = public_key.compress();
let compressed_public_key = public_key.to_bytes();

assert_eq!(compressed_public_key.to_bytes().len(), 57);
assert_eq!(compressed_public_key.len(), 57);

let hashed_scalar = Ed448::hash_to_scalar::<ExpandMsgXof<Shake256>>(&[b"test"], &[b"edwards448_XOF:SHAKE256_ELL2_RO_"]).unwrap();
let input = hex_literal::hex!("c8c6c8f584e0c25efdb6af5ad234583c56dedd7c33e0c893468e96740fa0cf7f1a560667da40b7bde340a39252e89262fcf707d1180fd43400");
Expand All @@ -40,7 +41,7 @@ assert_eq!(hashed_scalar, expected_scalar);

let hashed_point = Ed448::hash_from_bytes::<ExpandMsgXof<Shake256>>(&[b"test"], &[b"edwards448_XOF:SHAKE256_ELL2_RO_"]).unwrap();
let expected = hex_literal::hex!("d15c4427b5c5611a53593c2be611fd3635b90272d331c7e6721ad3735e95dd8b9821f8e4e27501ce01aa3c913114052dce2e91e8ca050f4980");
let expected_point = CompressedEdwardsY(expected).decompress().unwrap();
let expected_point = CompressedEdwardsY(expected).decompress().unwrap().to_edwards();
assert_eq!(hashed_point, expected_point);

let hashed_point = EdwardsPoint::hash_with_defaults(b"test");
Expand Down
4 changes: 2 additions & 2 deletions ed448-goldilocks/src/edwards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
pub(crate) mod affine;
pub(crate) mod extended;
mod scalar;
pub use affine::AffinePoint;
pub use extended::{CompressedEdwardsY, EdwardsPoint};
pub use affine::{AffinePoint, CompressedEdwardsY};
pub use extended::EdwardsPoint;
pub use scalar::{EdwardsScalar, EdwardsScalarBytes, WideEdwardsScalarBytes};
281 changes: 278 additions & 3 deletions ed448-goldilocks/src/edwards/affine.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::field::FieldElement;
use crate::*;
use core::fmt::{Display, Formatter, LowerHex, Result as FmtResult, UpperHex};
use core::ops::Mul;
use elliptic_curve::{Error, Result, point::NonIdentity, zeroize::DefaultIsZeroes};
use subtle::{Choice, ConditionallySelectable, ConstantTimeEq};
use elliptic_curve::{Error, point::NonIdentity, zeroize::DefaultIsZeroes};
use rand_core::TryRngCore;
use subtle::{Choice, ConditionallyNegatable, ConditionallySelectable, ConstantTimeEq, CtOption};

/// Affine point on untwisted curve
#[derive(Copy, Clone, Debug)]
Expand Down Expand Up @@ -69,6 +71,21 @@ impl AffinePoint {
y: FieldElement::ONE,
};

/// Generate a random [`AffinePoint`].
pub fn try_from_rng<R>(rng: &mut R) -> Result<Self, R::Error>
where
R: TryRngCore + ?Sized,
{
let mut bytes = CompressedEdwardsY::default();

loop {
rng.try_fill_bytes(&mut bytes.0)?;
if let Some(point) = bytes.decompress().into() {
return Ok(point);
}
}
}

pub(crate) fn isogeny(&self) -> Self {
let x = self.x;
let y = self.y;
Expand Down Expand Up @@ -100,6 +117,33 @@ impl AffinePoint {
}
}

/// Standard compression; store Y and sign of X
pub fn compress(&self) -> CompressedEdwardsY {
let affine_x = self.x;
let affine_y = self.y;

let mut compressed_bytes = [0u8; 57];

let sign = affine_x.is_negative().unwrap_u8();

let y_bytes = affine_y.to_bytes();
compressed_bytes[..y_bytes.len()].copy_from_slice(&y_bytes[..]);
*compressed_bytes.last_mut().expect("at least one byte") = sign << 7;
CompressedEdwardsY(compressed_bytes)
}

/// Check if this point is on the curve
pub fn is_on_curve(&self) -> Choice {
// X^2 + Y^2 == 1 + D * X^2 * Y^2

let XX = self.x.square();
let YY = self.y.square();
let lhs = YY + XX;
let rhs = FieldElement::ONE + FieldElement::EDWARDS_D * XX * YY;

lhs.ct_eq(&rhs)
}

/// Convert to edwards extended point
pub fn to_edwards(&self) -> EdwardsPoint {
EdwardsPoint {
Expand Down Expand Up @@ -130,7 +174,7 @@ impl From<NonIdentity<AffinePoint>> for AffinePoint {
impl TryFrom<AffinePoint> for NonIdentity<AffinePoint> {
type Error = Error;

fn try_from(affine_point: AffinePoint) -> Result<Self> {
fn try_from(affine_point: AffinePoint) -> Result<Self, Error> {
NonIdentity::new(affine_point).into_option().ok_or(Error)
}
}
Expand All @@ -149,3 +193,234 @@ define_mul_variants!(
RHS = EdwardsScalar,
Output = EdwardsPoint
);

/// The compressed internal representation of a point on the Twisted Edwards Curve
pub type PointBytes = [u8; 57];

/// Represents a point on the Compressed Twisted Edwards Curve
/// in little endian format where the most significant bit is the sign bit
/// and the remaining 448 bits represent the y-coordinate
#[derive(Copy, Clone, Debug)]
pub struct CompressedEdwardsY(pub PointBytes);

impl elliptic_curve::zeroize::Zeroize for CompressedEdwardsY {
fn zeroize(&mut self) {
self.0.zeroize()
}
}

impl Display for CompressedEdwardsY {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
for b in &self.0[..] {
write!(f, "{b:02x}")?;
}
Ok(())
}
}

impl LowerHex for CompressedEdwardsY {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
for b in &self.0[..] {
write!(f, "{b:02x}")?;
}
Ok(())
}
}

impl UpperHex for CompressedEdwardsY {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
for b in &self.0[..] {
write!(f, "{b:02X}")?;
}
Ok(())
}
}

impl Default for CompressedEdwardsY {
fn default() -> Self {
Self([0u8; 57])
}
}

impl ConditionallySelectable for CompressedEdwardsY {
fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
let mut bytes = [0u8; 57];
for (i, byte) in bytes.iter_mut().enumerate() {
*byte = u8::conditional_select(&a.0[i], &b.0[i], choice);
}
Self(bytes)
}
}

impl ConstantTimeEq for CompressedEdwardsY {
fn ct_eq(&self, other: &Self) -> Choice {
self.0.ct_eq(&other.0)
}
}

impl PartialEq for CompressedEdwardsY {
fn eq(&self, other: &CompressedEdwardsY) -> bool {
self.ct_eq(other).into()
}
}

impl Eq for CompressedEdwardsY {}

impl AsRef<[u8]> for CompressedEdwardsY {
fn as_ref(&self) -> &[u8] {
&self.0[..]
}
}

impl AsRef<PointBytes> for CompressedEdwardsY {
fn as_ref(&self) -> &PointBytes {
&self.0
}
}

#[cfg(feature = "alloc")]
impl From<CompressedEdwardsY> for Vec<u8> {
fn from(value: CompressedEdwardsY) -> Self {
Self::from(&value)
}
}

#[cfg(feature = "alloc")]
impl From<&CompressedEdwardsY> for Vec<u8> {
fn from(value: &CompressedEdwardsY) -> Self {
value.0.to_vec()
}
}

#[cfg(feature = "alloc")]
impl TryFrom<Vec<u8>> for CompressedEdwardsY {
type Error = &'static str;

fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
Self::try_from(&value)
}
}

#[cfg(feature = "alloc")]
impl TryFrom<&Vec<u8>> for CompressedEdwardsY {
type Error = &'static str;

fn try_from(value: &Vec<u8>) -> Result<Self, Self::Error> {
Self::try_from(value.as_slice())
}
}

impl TryFrom<&[u8]> for CompressedEdwardsY {
type Error = &'static str;

fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
let bytes = <PointBytes>::try_from(value).map_err(|_| "Invalid length")?;
Ok(CompressedEdwardsY(bytes))
}
}

#[cfg(feature = "alloc")]
impl TryFrom<Box<[u8]>> for CompressedEdwardsY {
type Error = &'static str;

fn try_from(value: Box<[u8]>) -> Result<Self, Self::Error> {
Self::try_from(value.as_ref())
}
}

impl From<CompressedEdwardsY> for PointBytes {
fn from(value: CompressedEdwardsY) -> Self {
value.0
}
}

impl From<&CompressedEdwardsY> for PointBytes {
fn from(value: &CompressedEdwardsY) -> Self {
Self::from(*value)
}
}

#[cfg(feature = "serde")]
impl serdect::serde::Serialize for CompressedEdwardsY {
fn serialize<S: serdect::serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
serdect::array::serialize_hex_lower_or_bin(&self.0, s)
}
}

#[cfg(feature = "serde")]
impl<'de> serdect::serde::Deserialize<'de> for CompressedEdwardsY {
fn deserialize<D>(d: D) -> Result<Self, D::Error>
where
D: serdect::serde::Deserializer<'de>,
{
let mut arr = [0u8; 57];
serdect::array::deserialize_hex_or_bin(&mut arr, d)?;
Ok(CompressedEdwardsY(arr))
}
}

impl From<PointBytes> for CompressedEdwardsY {
fn from(point: PointBytes) -> Self {
Self(point)
}
}

impl CompressedEdwardsY {
/// The compressed generator point
pub const GENERATOR: Self = Self([
20, 250, 48, 242, 91, 121, 8, 152, 173, 200, 215, 78, 44, 19, 189, 253, 196, 57, 124, 230,
28, 255, 211, 58, 215, 194, 160, 5, 30, 156, 120, 135, 64, 152, 163, 108, 115, 115, 234,
75, 98, 199, 201, 86, 55, 32, 118, 136, 36, 188, 182, 110, 113, 70, 63, 105, 0,
]);
/// The compressed identity point
pub const IDENTITY: Self = Self([0u8; 57]);

/// Attempt to decompress to an `AffinePoint`.
///
/// Returns `None` if the input is not the \\(y\\)-coordinate of a
/// curve point.
pub fn decompress_unchecked(&self) -> CtOption<AffinePoint> {
// Safe to unwrap here as the underlying data structure is a slice
let (sign, b) = self.0.split_last().expect("slice is non-empty");

let mut y_bytes: [u8; 56] = [0; 56];
y_bytes.copy_from_slice(b);

// Recover x using y
let y = FieldElement::from_bytes(&y_bytes);
let yy = y.square();
let dyy = FieldElement::EDWARDS_D * yy;
let numerator = FieldElement::ONE - yy;
let denominator = FieldElement::ONE - dyy;

let (mut x, is_res) = FieldElement::sqrt_ratio(&numerator, &denominator);

// Compute correct sign of x
let compressed_sign_bit = Choice::from(sign >> 7);
let is_negative = x.is_negative();
x.conditional_negate(compressed_sign_bit ^ is_negative);

CtOption::new(AffinePoint { x, y }, is_res)
}

/// Attempt to decompress to an `AffinePoint`.
///
/// Returns `None`:
/// - if the input is not the \\(y\\)-coordinate of a curve point.
/// - if the input point is not on the curve.
/// - if the input point has nonzero torsion component.
pub fn decompress(&self) -> CtOption<AffinePoint> {
self.decompress_unchecked()
.and_then(|pt| CtOption::new(pt, pt.is_on_curve() & pt.to_edwards().is_torsion_free()))
}

/// View this `CompressedEdwardsY` as an array of bytes.
pub const fn as_bytes(&self) -> &PointBytes {
&self.0
}

/// Copy this `CompressedEdwardsY` to an array of bytes.
pub const fn to_bytes(&self) -> PointBytes {
self.0
}
}
Loading