diff --git a/Cargo.lock b/Cargo.lock index a147a0e7..73ee4cde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4492,6 +4492,24 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "minimal-ed448" +version = "0.1.0" +dependencies = [ + "crypto-bigint", + "dalek-ff-group", + "digest 0.10.3", + "ff", + "generic-array 0.14.6", + "group", + "hex", + "hex-literal", + "lazy_static", + "rand_core 0.6.3", + "subtle", + "zeroize", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4531,10 +4549,12 @@ dependencies = [ "group", "hex", "k256", + "minimal-ed448", "multiexp", "p256", "rand_core 0.6.3", "sha2 0.10.2", + "sha3 0.10.2", "thiserror", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index 7d5384bb..2b0c67ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,8 @@ members = [ "crypto/transcript", "crypto/dalek-ff-group", + "crypto/ed448", + "crypto/multiexp", "crypto/dleq", @@ -33,6 +35,7 @@ group = { opt-level = 3 } crypto-bigint = { opt-level = 3 } dalek-ff-group = { opt-level = 3 } +minimal-ed448 = { opt-level = 3 } multiexp = { opt-level = 3 } diff --git a/crypto/dalek-ff-group/src/field.rs b/crypto/dalek-ff-group/src/field.rs index 68f61572..d249cc10 100644 --- a/crypto/dalek-ff-group/src/field.rs +++ b/crypto/dalek-ff-group/src/field.rs @@ -8,7 +8,7 @@ use crypto_bigint::{Encoding, U256, U512}; use ff::{Field, PrimeField, FieldBits, PrimeFieldBits}; -use crate::{choice, constant_time, math_op, math, from_wrapper, from_uint}; +use crate::{choice, constant_time, math, from_uint}; const FIELD_MODULUS: U256 = U256::from_be_hex("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed"); diff --git a/crypto/dalek-ff-group/src/lib.rs b/crypto/dalek-ff-group/src/lib.rs index c2d795bd..93628246 100644 --- a/crypto/dalek-ff-group/src/lib.rs +++ b/crypto/dalek-ff-group/src/lib.rs @@ -122,7 +122,7 @@ macro_rules! math_op { } #[doc(hidden)] -#[macro_export] +#[macro_export(local_inner_macros)] macro_rules! math { ($Value: ident, $Factor: ident, $add: expr, $sub: expr, $mul: expr) => { math_op!($Value, $Value, Add, add, AddAssign, add_assign, $add); @@ -131,6 +131,8 @@ macro_rules! math { }; } +#[doc(hidden)] +#[macro_export(local_inner_macros)] macro_rules! math_neg { ($Value: ident, $Factor: ident, $add: expr, $sub: expr, $mul: expr) => { math!($Value, $Factor, $add, $sub, $mul); @@ -157,7 +159,7 @@ macro_rules! from_wrapper { } #[doc(hidden)] -#[macro_export] +#[macro_export(local_inner_macros)] macro_rules! from_uint { ($wrapper: ident, $inner: ident) => { from_wrapper!($wrapper, $inner, u8); diff --git a/crypto/ed448/Cargo.toml b/crypto/ed448/Cargo.toml new file mode 100644 index 00000000..c25b8a2a --- /dev/null +++ b/crypto/ed448/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "minimal-ed448" +version = "0.1.0" +description = "Unaudited, inefficient implementation of Ed448 in Rust" +license = "MIT" +repository = "https://github.com/serai-dex/serai" +authors = ["Luke Parker "] +keywords = ["ed448", "ff", "group"] +edition = "2021" + +[dependencies] +hex-literal = "0.3" +lazy_static = "1" + +rand_core = "0.6" +digest = "0.10" + +zeroize = { version = "1.3", features = ["zeroize_derive"] } +subtle = "2.4" + +ff = "0.12" +group = "0.12" + +generic-array = "0.14" +crypto-bigint = {version = "0.4", features = ["zeroize"] } + +dalek-ff-group = { path = "../dalek-ff-group", version = "^0.1.2" } + +[dev-dependencies] +hex = "0.4" diff --git a/crypto/ed448/LICENSE b/crypto/ed448/LICENSE new file mode 100644 index 00000000..f05b748b --- /dev/null +++ b/crypto/ed448/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 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. diff --git a/crypto/ed448/README.md b/crypto/ed448/README.md new file mode 100644 index 00000000..ac9270e6 --- /dev/null +++ b/crypto/ed448/README.md @@ -0,0 +1,9 @@ +# Minimal Ed448 + +Inefficient, barebones implementation of Ed448 bound to the ff/group API, +rejecting torsion to achieve a PrimeGroup definition. This likely should not be +used and was only done so another library under Serai could confirm its +completion. It is minimally tested, yet should be correct for what it has. +Multiple functions remain unimplemented. + +constant time and no_std. diff --git a/crypto/ed448/src/backend.rs b/crypto/ed448/src/backend.rs new file mode 100644 index 00000000..b4110d0e --- /dev/null +++ b/crypto/ed448/src/backend.rs @@ -0,0 +1,165 @@ +#[doc(hidden)] +#[macro_export] +macro_rules! field { + ($FieldName: ident, $MODULUS: ident, $WIDE_MODULUS: ident, $NUM_BITS: literal) => { + use core::ops::{Add, AddAssign, Neg, Sub, SubAssign, Mul, MulAssign}; + + use rand_core::RngCore; + + use subtle::{Choice, CtOption, ConstantTimeEq, ConditionallySelectable}; + + use generic_array::{typenum::U57, GenericArray}; + use crypto_bigint::Encoding; + + use ff::{Field, PrimeField, FieldBits, PrimeFieldBits}; + + use dalek_ff_group::{constant_time, from_uint, math}; + + fn reduce(x: U1024) -> U512 { + U512::from_le_slice(&x.reduce(&$WIDE_MODULUS).unwrap().to_le_bytes()[.. 64]) + } + + constant_time!($FieldName, U512); + math!( + $FieldName, + $FieldName, + |x, y| U512::add_mod(&x, &y, &$MODULUS.0), + |x, y| U512::sub_mod(&x, &y, &$MODULUS.0), + |x, y| { + let wide = U512::mul_wide(&x, &y); + reduce(U1024::from((wide.1, wide.0))) + } + ); + from_uint!($FieldName, U512); + + impl Neg for $FieldName { + type Output = $FieldName; + fn neg(self) -> $FieldName { + *$MODULUS - self + } + } + + impl<'a> Neg for &'a $FieldName { + type Output = $FieldName; + fn neg(self) -> Self::Output { + (*self).neg() + } + } + + lazy_static! { + pub(crate) static ref ZERO: $FieldName = $FieldName(U512::ZERO); + pub(crate) static ref ONE: $FieldName = $FieldName(U512::ONE); + pub(crate) static ref TWO: $FieldName = $FieldName(U512::ONE.saturating_add(&U512::ONE)); + } + + impl $FieldName { + pub fn pow(&self, other: $FieldName) -> $FieldName { + let mut table = [*ONE; 16]; + table[1] = *self; + for i in 2 .. 16 { + table[i] = table[i - 1] * self; + } + + let mut res = *ONE; + let mut bits = 0; + for (i, bit) in other.to_le_bits().iter().rev().enumerate() { + bits <<= 1; + let bit = *bit as u8; + assert_eq!(bit | 1, 1); + bits |= bit; + + if ((i + 1) % 4) == 0 { + if i != 3 { + for _ in 0 .. 4 { + res *= res; + } + } + res *= table[usize::from(bits)]; + bits = 0; + } + } + res + } + } + + impl Field for $FieldName { + fn random(mut rng: impl RngCore) -> Self { + let mut bytes = [0; 128]; + rng.fill_bytes(&mut bytes); + $FieldName(reduce(U1024::from_le_slice(bytes.as_ref()))) + } + + fn zero() -> Self { + *ZERO + } + fn one() -> Self { + *ONE + } + fn square(&self) -> Self { + *self * self + } + fn double(&self) -> Self { + *self + self + } + + fn invert(&self) -> CtOption { + CtOption::new(self.pow(-*TWO), !self.is_zero()) + } + + fn sqrt(&self) -> CtOption { + unimplemented!() + } + + fn is_zero(&self) -> Choice { + self.ct_eq(&ZERO) + } + fn cube(&self) -> Self { + *self * self * self + } + fn pow_vartime>(&self, _exp: S) -> Self { + unimplemented!() + } + } + + impl PrimeField for $FieldName { + type Repr = GenericArray; + const NUM_BITS: u32 = $NUM_BITS; + const CAPACITY: u32 = $NUM_BITS - 1; + fn from_repr(bytes: Self::Repr) -> CtOption { + let res = $FieldName(U512::from_le_slice(&[bytes.as_ref(), [0; 7].as_ref()].concat())); + CtOption::new(res, res.0.add_mod(&U512::ZERO, &$MODULUS.0).ct_eq(&res.0)) + } + fn to_repr(&self) -> Self::Repr { + let mut repr = GenericArray::::default(); + repr.copy_from_slice(&self.0.to_le_bytes()[.. 57]); + repr + } + + // True for both the Ed448 Scalar field and FieldElement field + const S: u32 = 1; + fn is_odd(&self) -> Choice { + (self.to_repr()[0] & 1).into() + } + fn multiplicative_generator() -> Self { + unimplemented!() + } + fn root_of_unity() -> Self { + unimplemented!() + } + } + + impl PrimeFieldBits for $FieldName { + type ReprBits = [u8; 56]; + + fn to_le_bits(&self) -> FieldBits { + let mut repr = [0; 56]; + repr.copy_from_slice(&self.to_repr()[.. 56]); + repr.into() + } + + fn char_le_bits() -> FieldBits { + MODULUS.to_le_bits() + } + } + }; +} diff --git a/crypto/ed448/src/field.rs b/crypto/ed448/src/field.rs new file mode 100644 index 00000000..05931d14 --- /dev/null +++ b/crypto/ed448/src/field.rs @@ -0,0 +1,64 @@ +use core::ops::Div; + +use lazy_static::lazy_static; + +use zeroize::Zeroize; + +use crypto_bigint::{NonZero, U512, U1024}; + +use crate::field; + +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Zeroize)] +pub struct FieldElement(pub(crate) U512); + +// 2**448 - 2**224 - 1 +lazy_static! { + pub static ref MODULUS: FieldElement = FieldElement(U512::from_be_hex(concat!( + "00000000000000", + "00", + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffe", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ))); + static ref WIDE_MODULUS: U1024 = { + let res = U1024::from((U512::ZERO, MODULUS.0)); + debug_assert_eq!(MODULUS.0.to_le_bytes()[..], res.to_le_bytes()[.. 64]); + res + }; +} + +field!(FieldElement, MODULUS, WIDE_MODULUS, 448); + +lazy_static! { + pub(crate) static ref Q_4: FieldElement = FieldElement( + MODULUS.0.saturating_add(&U512::ONE).div(NonZero::new(TWO.0.saturating_add(&TWO.0)).unwrap()) + ); +} + +#[test] +fn repr() { + assert_eq!(FieldElement::from_repr(FieldElement::one().to_repr()).unwrap(), FieldElement::one()); +} + +#[test] +fn one_two() { + assert_eq!(FieldElement::one() * FieldElement::one().double(), FieldElement::from(2u8)); + assert_eq!( + FieldElement::from_repr(FieldElement::from(2u8).to_repr()).unwrap(), + FieldElement::from(2u8) + ); +} + +#[test] +fn pow() { + assert_eq!(FieldElement::one().pow(FieldElement::one()), FieldElement::one()); + let two = FieldElement::one().double(); + assert_eq!(two.pow(two), two.double()); + + let three = two + FieldElement::one(); + assert_eq!(three.pow(three), three * three * three); +} + +#[test] +fn invert() { + assert_eq!(FieldElement::one().invert().unwrap(), FieldElement::one()); +} diff --git a/crypto/ed448/src/lib.rs b/crypto/ed448/src/lib.rs new file mode 100644 index 00000000..d0f6d14d --- /dev/null +++ b/crypto/ed448/src/lib.rs @@ -0,0 +1,6 @@ +#![no_std] + +mod backend; +pub mod scalar; +pub mod field; +pub mod point; diff --git a/crypto/ed448/src/point.rs b/crypto/ed448/src/point.rs new file mode 100644 index 00000000..02089b46 --- /dev/null +++ b/crypto/ed448/src/point.rs @@ -0,0 +1,380 @@ +use core::{ + ops::{Add, AddAssign, Neg, Sub, SubAssign, Mul, MulAssign}, + iter::Sum, +}; + +use lazy_static::lazy_static; + +use rand_core::RngCore; + +use zeroize::Zeroize; +use subtle::{Choice, CtOption, ConstantTimeEq, ConditionallySelectable, ConditionallyNegatable}; + +use ff::{Field, PrimeField, PrimeFieldBits}; +use group::{Group, GroupEncoding, prime::PrimeGroup}; + +use crate::{ + scalar::{Scalar, MODULUS as SCALAR_MODULUS}, + field::{FieldElement, Q_4}, +}; + +lazy_static! { + static ref D: FieldElement = -FieldElement::from(39081u16); +} + +fn recover_x(y: FieldElement) -> CtOption { + let ysq = y.square(); + #[allow(non_snake_case)] + let D_ysq = *D * ysq; + (D_ysq - FieldElement::one()).invert().and_then(|inverted| { + let temp = (ysq - FieldElement::one()) * inverted; + let mut x = temp.pow(*Q_4); + x.conditional_negate(x.is_odd()); + + let xsq = x.square(); + CtOption::new(x, (xsq + ysq).ct_eq(&(FieldElement::one() + (xsq * D_ysq)))) + }) +} + +#[derive(Clone, Copy, Debug, Zeroize)] +pub struct Point { + x: FieldElement, + y: FieldElement, + z: FieldElement, +} + +#[rustfmt::skip] +lazy_static! { + static ref G_Y: FieldElement = FieldElement::from_repr( + hex_literal::hex!( + "14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f6900" + ) + .into() + ) + .unwrap(); + + static ref G: Point = Point { x: recover_x(*G_Y).unwrap(), y: *G_Y, z: FieldElement::one() }; +} + +impl ConstantTimeEq for Point { + fn ct_eq(&self, other: &Self) -> Choice { + let x1 = self.x * other.z; + let x2 = other.x * self.z; + + let y1 = self.y * other.z; + let y2 = other.y * self.z; + + x1.ct_eq(&x2) & y1.ct_eq(&y2) + } +} + +impl PartialEq for Point { + fn eq(&self, other: &Point) -> bool { + self.ct_eq(other).into() + } +} + +impl Eq for Point {} + +impl ConditionallySelectable for Point { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + Point { + x: FieldElement::conditional_select(&a.x, &b.x, choice), + y: FieldElement::conditional_select(&a.y, &b.y, choice), + z: FieldElement::conditional_select(&a.z, &b.z, choice), + } + } +} + +impl Add for Point { + type Output = Point; + fn add(self, other: Self) -> Self { + // 12 muls, 7 additions, 4 negations + let xcp = self.x * other.x; + let ycp = self.y * other.y; + let zcp = self.z * other.z; + #[allow(non_snake_case)] + let B = zcp.square(); + #[allow(non_snake_case)] + let E = *D * xcp * ycp; + #[allow(non_snake_case)] + let F = B - E; + #[allow(non_snake_case)] + let G_ = B + E; + + Point { + x: zcp * F * ((self.x + self.y) * (other.x + other.y) - xcp - ycp), + y: zcp * G_ * (ycp - xcp), + z: F * G_, + } + } +} + +impl AddAssign for Point { + fn add_assign(&mut self, other: Point) { + *self = *self + other; + } +} + +impl Add<&Point> for Point { + type Output = Point; + fn add(self, other: &Point) -> Point { + self + *other + } +} + +impl AddAssign<&Point> for Point { + fn add_assign(&mut self, other: &Point) { + *self += *other; + } +} + +impl Neg for Point { + type Output = Point; + fn neg(self) -> Self { + Point { x: -self.x, y: self.y, z: self.z } + } +} + +impl Sub for Point { + type Output = Point; + #[allow(clippy::suspicious_arithmetic_impl)] + fn sub(self, other: Self) -> Self { + self + other.neg() + } +} + +impl SubAssign for Point { + fn sub_assign(&mut self, other: Point) { + *self = *self - other; + } +} + +impl Sub<&Point> for Point { + type Output = Point; + fn sub(self, other: &Point) -> Point { + self - *other + } +} + +impl SubAssign<&Point> for Point { + fn sub_assign(&mut self, other: &Point) { + *self -= *other; + } +} + +impl Group for Point { + type Scalar = Scalar; + // Ideally, this would be cryptographically secure, yet that's not a bound on the trait + // k256 also does this + fn random(rng: impl RngCore) -> Self { + Self::generator() * Scalar::random(rng) + } + fn identity() -> Self { + Point { x: FieldElement::zero(), y: FieldElement::one(), z: FieldElement::one() } + } + fn generator() -> Self { + *G + } + fn is_identity(&self) -> Choice { + self.ct_eq(&Self::identity()) + } + fn double(&self) -> Self { + // 7 muls, 7 additions, 4 negations + let xsq = self.x.square(); + let ysq = self.y.square(); + let zsq = self.z.square(); + let xy = self.x + self.y; + #[allow(non_snake_case)] + let F = xsq + ysq; + #[allow(non_snake_case)] + let J = F - zsq.double(); + Point { x: J * (xy.square() - xsq - ysq), y: F * (xsq - ysq), z: F * J } + } +} + +impl Sum for Point { + fn sum>(iter: I) -> Point { + let mut res = Self::identity(); + for i in iter { + res += i; + } + res + } +} + +impl<'a> Sum<&'a Point> for Point { + fn sum>(iter: I) -> Point { + Point::sum(iter.cloned()) + } +} + +impl Mul for Point { + type Output = Point; + fn mul(self, other: Scalar) -> Point { + // Precompute the optimal amount that's a multiple of 2 + let mut table = [Point::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, bit) in other.to_le_bits().iter().rev().enumerate() { + bits <<= 1; + let bit = *bit as u8; + assert_eq!(bit | 1, 1); + bits |= bit; + + if ((i + 1) % 4) == 0 { + if i != 3 { + for _ in 0 .. 4 { + res = res.double(); + } + } + res += table[usize::from(bits)]; + bits = 0; + } + } + res + } +} + +impl MulAssign for Point { + fn mul_assign(&mut self, other: Scalar) { + *self = *self * other; + } +} + +impl Mul<&Scalar> for Point { + type Output = Point; + fn mul(self, other: &Scalar) -> Point { + self * *other + } +} + +impl MulAssign<&Scalar> for Point { + fn mul_assign(&mut self, other: &Scalar) { + *self *= *other; + } +} + +impl Point { + pub fn is_torsion_free(&self) -> Choice { + (*self * *SCALAR_MODULUS).is_identity() + } +} + +impl GroupEncoding for Point { + type Repr = ::Repr; + + fn from_bytes(bytes: &Self::Repr) -> CtOption { + // Extract and clear the sign bit + let sign = Choice::from(bytes[56] >> 7); + let mut bytes = *bytes; + let mut_ref: &mut [u8] = bytes.as_mut(); + mut_ref[56] &= !(1 << 7); + + // Parse y, recover x + FieldElement::from_repr(bytes).and_then(|y| { + recover_x(y).and_then(|mut x| { + x.conditional_negate(x.is_odd().ct_eq(&!sign)); + let not_negative_zero = !(x.is_zero() & sign); + let point = Point { x, y, z: FieldElement::one() }; + CtOption::new(point, not_negative_zero & point.is_torsion_free()) + }) + }) + } + + fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { + Point::from_bytes(bytes) + } + + fn to_bytes(&self) -> Self::Repr { + let z = self.z.invert().unwrap(); + let x = self.x * z; + let y = self.y * z; + + let mut bytes = y.to_repr(); + let mut_ref: &mut [u8] = bytes.as_mut(); + mut_ref[56] |= x.is_odd().unwrap_u8() << 7; + bytes + } +} + +impl PrimeGroup for Point {} + +#[test] +fn identity() { + assert_eq!(Point::from_bytes(&Point::identity().to_bytes()).unwrap(), Point::identity()); + assert_eq!(Point::identity() + Point::identity(), Point::identity()); +} + +#[test] +fn addition_multiplication_serialization() { + let mut accum = Point::identity(); + for x in 1 .. 10 { + accum += Point::generator(); + let mul = Point::generator() * Scalar::from(x as u8); + assert_eq!(accum, mul); + assert_eq!(Point::from_bytes(&mul.to_bytes()).unwrap(), mul); + } +} + +#[rustfmt::skip] +#[test] +fn torsion() { + // Uses the originally suggested generator which had torsion + let old_y = FieldElement::from_repr( + hex_literal::hex!( + "12796c1532041525945f322e414d434467cfd5c57c9a9af2473b27758c921c4828b277ca5f2891fc4f3d79afdf29a64c72fb28b59c16fa5100" + ).into(), + ) + .unwrap(); + let old = Point { x: -recover_x(old_y).unwrap(), y: old_y, z: FieldElement::one() }; + assert!(bool::from(!old.is_torsion_free())); +} + +#[test] +fn vector() { + use generic_array::GenericArray; + + assert_eq!( + Point::generator().double(), + Point::from_bytes(GenericArray::from_slice( + &hex::decode( + "\ +ed8693eacdfbeada6ba0cdd1beb2bcbb98302a3a8365650db8c4d88a\ +726de3b7d74d8835a0d76e03b0c2865020d659b38d04d74a63e905ae\ +80" + ) + .unwrap() + )) + .unwrap() + ); + + assert_eq!( + Point::generator() * + Scalar::from_repr(*GenericArray::from_slice( + &hex::decode( + "\ +6298e1eef3c379392caaed061ed8a31033c9e9e3420726f23b404158\ +a401cd9df24632adfe6b418dc942d8a091817dd8bd70e1c72ba52f3c\ +00" + ) + .unwrap() + )) + .unwrap(), + Point::from_bytes(GenericArray::from_slice( + &hex::decode( + "\ +3832f82fda00ff5365b0376df705675b63d2a93c24c6e81d40801ba2\ +65632be10f443f95968fadb70d10786827f30dc001c8d0f9b7c1d1b0\ +00" + ) + .unwrap() + )) + .unwrap() + ); +} diff --git a/crypto/ed448/src/scalar.rs b/crypto/ed448/src/scalar.rs new file mode 100644 index 00000000..b0db88c0 --- /dev/null +++ b/crypto/ed448/src/scalar.rs @@ -0,0 +1,38 @@ +use lazy_static::lazy_static; + +use zeroize::Zeroize; + +use crypto_bigint::{U512, U1024}; + +pub use crate::field; + +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Zeroize)] +pub struct Scalar(pub(crate) U512); + +// 2**446 - 13818066809895115352007386748515426880336692474882178609894547503885 +lazy_static! { + pub static ref MODULUS: Scalar = Scalar(U512::from_be_hex(concat!( + "00000000000000", + "00", + "3fffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "7cca23e9c44edb49aed63690216cc2728dc58f552378c292ab5844f3", + ))); + static ref WIDE_MODULUS: U1024 = { + let res = U1024::from((U512::ZERO, MODULUS.0)); + debug_assert_eq!(MODULUS.0.to_le_bytes()[..], res.to_le_bytes()[.. 64]); + res + }; +} + +field!(Scalar, MODULUS, WIDE_MODULUS, 446); + +impl Scalar { + pub fn wide_reduce(bytes: [u8; 114]) -> Scalar { + Scalar(reduce(U1024::from_le_slice(&[bytes.as_ref(), &[0; 14]].concat()))) + } +} + +#[test] +fn invert() { + assert_eq!(Scalar::one().invert().unwrap(), Scalar::one()); +} diff --git a/crypto/frost/Cargo.toml b/crypto/frost/Cargo.toml index c75f4046..a240faf7 100644 --- a/crypto/frost/Cargo.toml +++ b/crypto/frost/Cargo.toml @@ -18,14 +18,18 @@ zeroize = { version = "1.3", features = ["zeroize_derive"] } hex = "0.4" sha2 = { version = "0.10", optional = true } +sha3 = { version = "0.10", optional = true } ff = "0.12" group = "0.12" +dalek-ff-group = { path = "../dalek-ff-group", version = "^0.1.2", optional = true } + elliptic-curve = { version = "0.12", features = ["hash2curve"], optional = true } p256 = { version = "0.11", features = ["arithmetic", "bits", "hash2curve"], optional = true } k256 = { version = "0.11", features = ["arithmetic", "bits", "hash2curve"], optional = true } -dalek-ff-group = { path = "../dalek-ff-group", version = "^0.1.2", optional = true } + +minimal-ed448 = { path = "../ed448", version = "0.1", optional = true } transcript = { package = "flexible-transcript", path = "../transcript", features = ["recommended"], version = "^0.1.3" } @@ -38,10 +42,12 @@ sha2 = "0.10" dalek-ff-group = { path = "../dalek-ff-group" } [features] -curves = ["sha2"] # All officially denoted curves use the SHA2 family of hashes -kp256 = ["elliptic-curve", "curves"] -p256 = ["kp256", "dep:p256"] -secp256k1 = ["kp256", "k256"] -dalek = ["curves", "dalek-ff-group"] +dalek = ["sha2", "dalek-ff-group"] ed25519 = ["dalek"] ristretto = ["dalek"] + +kp256 = ["sha2", "elliptic-curve"] +p256 = ["kp256", "dep:p256"] +secp256k1 = ["kp256", "k256"] + +ed448 = ["sha3", "minimal-ed448"] diff --git a/crypto/frost/src/curve/dalek.rs b/crypto/frost/src/curve/dalek.rs index 58cd63d3..dd04feda 100644 --- a/crypto/frost/src/curve/dalek.rs +++ b/crypto/frost/src/curve/dalek.rs @@ -2,6 +2,7 @@ use zeroize::Zeroize; use sha2::{Digest, Sha512}; +use group::Group; use dalek_ff_group::Scalar; use crate::{curve::Curve, algorithm::Hram}; @@ -12,13 +13,11 @@ macro_rules! dalek_curve { $Hram: ident, $Point: ident, - $POINT: ident, - $ID: literal, $CONTEXT: literal, $chal: literal, ) => { - use dalek_ff_group::{$Point, $POINT}; + use dalek_ff_group::$Point; #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] pub struct $Curve; @@ -35,7 +34,7 @@ macro_rules! dalek_curve { const ID: &'static [u8] = $ID; fn generator() -> Self::G { - $POINT + $Point::generator() } fn hash_to_vec(dst: &[u8], data: &[u8]) -> Vec { @@ -69,7 +68,6 @@ dalek_curve!( Ristretto, IetfRistrettoHram, RistrettoPoint, - RISTRETTO_BASEPOINT_POINT, b"ristretto", b"FROST-RISTRETTO255-SHA512-v8", b"chal", @@ -80,7 +78,6 @@ dalek_curve!( Ed25519, IetfEd25519Hram, EdwardsPoint, - ED25519_BASEPOINT_POINT, b"edwards25519", b"FROST-ED25519-SHA512-v8", b"", diff --git a/crypto/frost/src/curve/ed448.rs b/crypto/frost/src/curve/ed448.rs new file mode 100644 index 00000000..a3500c84 --- /dev/null +++ b/crypto/frost/src/curve/ed448.rs @@ -0,0 +1,62 @@ +use zeroize::Zeroize; + +use sha3::{digest::ExtendableOutput, Shake256}; + +use group::{Group, GroupEncoding}; +use minimal_ed448::{scalar::Scalar, point::Point}; + +use crate::{curve::Curve, algorithm::Hram}; + +const CONTEXT: &[u8] = b"FROST-ED448-SHAKE256-v8"; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub struct Ed448; +impl Ed448 { + fn hash(prefix: &[u8], context: &[u8], dst: &[u8], data: &[u8]) -> [u8; 114] { + let mut res = [0; 114]; + Shake256::digest_xof(&[prefix, context, dst, data].concat(), &mut res); + res + } +} + +impl Curve for Ed448 { + type F = Scalar; + type G = Point; + + const ID: &'static [u8] = b"ed448"; + + fn generator() -> Self::G { + Point::generator() + } + + fn hash_to_vec(dst: &[u8], data: &[u8]) -> Vec { + Self::hash(b"", CONTEXT, dst, data).as_ref().to_vec() + } + + fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F { + Scalar::wide_reduce(Self::hash(b"", CONTEXT, dst, data)) + } +} + +#[derive(Copy, Clone)] +pub struct Ietf8032Ed448Hram; +impl Ietf8032Ed448Hram { + #[allow(non_snake_case)] + pub fn hram(context: &[u8], R: &Point, A: &Point, m: &[u8]) -> Scalar { + Scalar::wide_reduce(Ed448::hash( + &[b"SigEd448".as_ref(), &[0, u8::try_from(context.len()).unwrap()]].concat(), + context, + b"", + &[R.to_bytes().as_ref(), A.to_bytes().as_ref(), m].concat(), + )) + } +} + +#[derive(Copy, Clone)] +pub struct NonIetfEd448Hram; +impl Hram for NonIetfEd448Hram { + #[allow(non_snake_case)] + fn hram(R: &Point, A: &Point, m: &[u8]) -> Scalar { + Ietf8032Ed448Hram::hram(&[CONTEXT, b"chal"].concat(), R, A, m) + } +} diff --git a/crypto/frost/src/curve/mod.rs b/crypto/frost/src/curve/mod.rs index 08e946a3..0db82213 100644 --- a/crypto/frost/src/curve/mod.rs +++ b/crypto/frost/src/curve/mod.rs @@ -24,6 +24,11 @@ pub use kp256::{Secp256k1, IetfSecp256k1Hram}; #[cfg(feature = "p256")] pub use kp256::{P256, IetfP256Hram}; +#[cfg(feature = "ed448")] +mod ed448; +#[cfg(feature = "ed448")] +pub use ed448::{Ed448, Ietf8032Ed448Hram, NonIetfEd448Hram}; + /// Set of errors for curve-related operations, namely encoding and decoding #[derive(Clone, Error, Debug)] pub enum CurveError { diff --git a/crypto/frost/src/tests/curve.rs b/crypto/frost/src/tests/curve.rs index c57b0568..078df28e 100644 --- a/crypto/frost/src/tests/curve.rs +++ b/crypto/frost/src/tests/curve.rs @@ -19,24 +19,26 @@ fn keys_serialization(rng: &mut R) { } } +// Test successful multiexp, with enough pairs to trigger its variety of algorithms +// Multiexp has its own tests, yet only against k256 and Ed25519 (which should be sufficient +// as-is to prove multiexp), and this doesn't hurt +pub fn test_multiexp(rng: &mut R) { + let mut pairs = Vec::with_capacity(1000); + let mut sum = C::G::identity(); + for _ in 0 .. 10 { + for _ in 0 .. 100 { + pairs.push((C::F::random(&mut *rng), C::generator() * C::F::random(&mut *rng))); + sum += pairs[pairs.len() - 1].1 * pairs[pairs.len() - 1].0; + } + assert_eq!(multiexp::multiexp(&pairs), sum); + assert_eq!(multiexp::multiexp_vartime(&pairs), sum); + } +} + pub fn test_curve(rng: &mut R) { // TODO: Test the Curve functions themselves - // Test successful multiexp, with enough pairs to trigger its variety of algorithms - // Multiexp has its own tests, yet only against k256 and Ed25519 (which should be sufficient - // as-is to prove multiexp), and this doesn't hurt - { - let mut pairs = Vec::with_capacity(1000); - let mut sum = C::G::identity(); - for _ in 0 .. 10 { - for _ in 0 .. 100 { - pairs.push((C::F::random(&mut *rng), C::generator() * C::F::random(&mut *rng))); - sum += pairs[pairs.len() - 1].1 * pairs[pairs.len() - 1].0; - } - assert_eq!(multiexp::multiexp(&pairs), sum); - assert_eq!(multiexp::multiexp_vartime(&pairs), sum); - } - } + test_multiexp::<_, C>(rng); // Test FROST key generation and serialization of FrostCore works as expected key_generation::<_, C>(rng); diff --git a/crypto/frost/src/tests/literal/ed448.rs b/crypto/frost/src/tests/literal/ed448.rs new file mode 100644 index 00000000..f0f5623c --- /dev/null +++ b/crypto/frost/src/tests/literal/ed448.rs @@ -0,0 +1,132 @@ +use std::io::Cursor; + +use rand_core::OsRng; + +use crate::{ + curve::{Curve, Ed448, Ietf8032Ed448Hram, NonIetfEd448Hram}, + schnorr::{SchnorrSignature, verify}, + tests::vectors::{Vectors, test_with_vectors}, +}; + +#[test] +fn ed448_8032_vector() { + let context = hex::decode("666f6f").unwrap(); + + #[allow(non_snake_case)] + let A = Ed448::read_G(&mut Cursor::new( + hex::decode( + "43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c".to_owned() + + "6798c0866aea01eb00742802b8438ea4cb82169c235160627b4c3a94" + + "80", + ) + .unwrap(), + )) + .unwrap(); + + let msg = hex::decode("03").unwrap(); + + let mut sig = Cursor::new( + hex::decode( + "d4f8f6131770dd46f40867d6fd5d5055de43541f8c5e35abbcd001b3".to_owned() + + "2a89f7d2151f7647f11d8ca2ae279fb842d607217fce6e042f6815ea" + + "00" + + "0c85741de5c8da1144a6a1aba7f96de42505d7a7298524fda538fccb" + + "bb754f578c1cad10d54d0d5428407e85dcbc98a49155c13764e66c3c" + + "00", + ) + .unwrap(), + ); + #[allow(non_snake_case)] + let R = Ed448::read_G(&mut sig).unwrap(); + let s = Ed448::read_F(&mut sig).unwrap(); + + assert!(verify( + A, + Ietf8032Ed448Hram::hram(&context, &R, &A, &msg), + &SchnorrSignature:: { R, s } + )); +} + +#[test] +fn ed448_non_ietf() { + test_with_vectors::<_, Ed448, NonIetfEd448Hram>( + &mut OsRng, + Vectors { + threshold: 2, + shares: &[ + concat!( + "4a2b2f5858a932ad3d3b18bd16e76ced3070d72fd79ae4402df201f5", + "25e754716a1bc1b87a502297f2a99d89ea054e0018eb55d39562fd01", + "00" + ), + concat!( + "2503d56c4f516444a45b080182b8a2ebbe4d9b2ab509f25308c88c0e", + "a7ccdc44e2ef4fc4f63403a11b116372438a1e287265cadeff1fcb07", + "00" + ), + concat!( + "00db7a8146f995db0a7cf844ed89d8e94c2b5f259378ff66e39d1728", + "28b264185ac4decf7219e4aa4478285b9c0eef4fccdf3eea69dd980d", + "00" + ), + ], + group_secret: concat!( + "6298e1eef3c379392caaed061ed8a31033c9e9e3420726f23b404158", + "a401cd9df24632adfe6b418dc942d8a091817dd8bd70e1c72ba52f3c", + "00" + ), + group_key: concat!( + "3832f82fda00ff5365b0376df705675b63d2a93c24c6e81d40801ba2", + "65632be10f443f95968fadb70d10786827f30dc001c8d0f9b7c1d1b0", + "00" + ), + + msg: "74657374", + included: &[1, 3], + nonces: &[ + [ + concat!( + "afa99ad5138f89d064c828ecb17accde77e4dc52e017c20b34d1db11", + "bdd0b17d2f4ec6ea7d5414df33977267c49b8d4b3b35c7f4a089db2f", + "00" + ), + concat!( + "c9c2f6119d5a7f60fc1a3517f08f3aced6f84f53cbcfa4709080858d", + "b8c8b49d4cb9921c4118f1961d4fb653ad5e320d175de3ee5258e904", + "00" + ), + ], + [ + concat!( + "a575cf9ae013b63204a56cc0bb0c21184eed6e42f448344e59153cf4", + "3798ad3b8c300a2c0ffa04ee7228a5c4ff84fcad4cf9616d1cd7fe0a", + "00" + ), + concat!( + "12419016a6c0d38a1d9d1eeb1455525d73a464113a9323fcfc75e5fb", + "7c1f17ad71ca2f2852b71f33950adedd7f8489551ad356ecf39a4d29", + "00" + ), + ], + ], + sig_shares: &[ + concat!( + "e88d1e9743ac059553de940131508205eff504816935f8c9d22a29df", + "4c541e4bb55d4c4a5c58dd65e6d2c421e35f2ddc7ea11095cffb3b16", + "00" + ), + concat!( + "d6ae2965ee86f925d38eedf0690ee54395243d244b59a5fece45cece", + "721867a00a6c7af9635c621ea09edad8fc26db5de4ce3aa4e7e7ea3f", + "00" + ), + ], + sig: "c07db58a26bd0c33930455f1923df2ffa50c3a1679e06a1940f84e0e".to_owned() + + "067bcec3e46008c3b4018b7b2563ba0f26740b7b5932883355e569f5" + + "00" + + "cbf7ef509f708697d1ddbc64289cfa27f4e36bf66ab34e04b84c2d31" + + "c06c85ebbfc9c643c0b43f8486719ffadf86083a63704b39b7e32616" + + "00", + }, + ); +} diff --git a/crypto/frost/src/tests/literal/kp256.rs b/crypto/frost/src/tests/literal/kp256.rs index 5f9e00a9..64c52c2a 100644 --- a/crypto/frost/src/tests/literal/kp256.rs +++ b/crypto/frost/src/tests/literal/kp256.rs @@ -1,6 +1,5 @@ use rand_core::OsRng; -#[cfg(any(feature = "secp256k1", feature = "p256"))] use crate::tests::vectors::{Vectors, test_with_vectors}; #[cfg(feature = "secp256k1")] @@ -11,7 +10,7 @@ use crate::curve::{P256, IetfP256Hram}; #[cfg(feature = "secp256k1")] #[test] -fn secp256k1_ietf() { +fn secp256k1_vectors() { test_with_vectors::<_, Secp256k1, IetfSecp256k1Hram>( &mut OsRng, Vectors { diff --git a/crypto/frost/src/tests/literal/mod.rs b/crypto/frost/src/tests/literal/mod.rs index 00fe0477..f825b95a 100644 --- a/crypto/frost/src/tests/literal/mod.rs +++ b/crypto/frost/src/tests/literal/mod.rs @@ -2,3 +2,5 @@ mod dalek; #[cfg(feature = "kp256")] mod kp256; +#[cfg(feature = "ed448")] +mod ed448;