diff --git a/Cargo.lock b/Cargo.lock index b285df629b8..8f40064a92b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4273,7 +4273,7 @@ dependencies = [ [[package]] name = "mithril-stm" -version = "0.8.0" +version = "0.8.1" dependencies = [ "anyhow", "blake2 0.10.6", diff --git a/mithril-stm/CHANGELOG.md b/mithril-stm/CHANGELOG.md index 5e06ebce7ee..97cd903d48d 100644 --- a/mithril-stm/CHANGELOG.md +++ b/mithril-stm/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.8.1 (12-22-2025) + +### Added + +- Jubjub wrapper is added for `schnorr_signature` module. + ## 0.8.0 (12-17-2025) ### Changed diff --git a/mithril-stm/Cargo.toml b/mithril-stm/Cargo.toml index 757553eec15..2bdaa0919df 100644 --- a/mithril-stm/Cargo.toml +++ b/mithril-stm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-stm" -version = "0.8.0" +version = "0.8.1" edition = { workspace = true } authors = { workspace = true } homepage = { workspace = true } diff --git a/mithril-stm/benches/schnorr_sig.rs b/mithril-stm/benches/schnorr_sig.rs index 4d76badbbb3..50b7680cf46 100644 --- a/mithril-stm/benches/schnorr_sig.rs +++ b/mithril-stm/benches/schnorr_sig.rs @@ -45,8 +45,8 @@ fn sign_and_verify(c: &mut Criterion, nr_sigs: usize) { let mut msks = Vec::new(); let mut sigs = Vec::new(); for _ in 0..nr_sigs { - let sk = SchnorrSigningKey::try_generate(&mut rng).unwrap(); - let vk = SchnorrVerificationKey::from(&sk); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + let vk = SchnorrVerificationKey::new_from_signing_key(sk.clone()).unwrap(); let sig = sk.sign(&msg, &mut rng_sig).unwrap(); sigs.push(sig); mvks.push(vk); diff --git a/mithril-stm/src/signature_scheme/schnorr_signature/error.rs b/mithril-stm/src/signature_scheme/schnorr_signature/error.rs index fa1701a4641..e7548036b88 100644 --- a/mithril-stm/src/signature_scheme/schnorr_signature/error.rs +++ b/mithril-stm/src/signature_scheme/schnorr_signature/error.rs @@ -1,5 +1,5 @@ #[cfg(feature = "future_snark")] -use super::{SchnorrSignature, SchnorrVerificationKey}; +use super::{PrimeOrderProjectivePoint, SchnorrSignature}; /// Error types for Schnorr signatures. #[cfg(feature = "future_snark")] @@ -9,19 +9,35 @@ pub enum SchnorrSignatureError { #[error("Invalid Schnorr single signature")] SignatureInvalid(Box), - /// Invalid Verification key - #[error("Invalid Schnorr Verification key")] - VerificationKeyInvalid(Box), - /// This error occurs when the serialization of the raw bytes failed #[error("Invalid bytes")] - SerializationError, + Serialization, + + /// This error occurs when the serialization of the scalar field bytes failed + #[error("Invalid scalar field element bytes")] + ScalarFieldElementSerialization, + + /// This error occurs when the serialization of the projective point bytes failed + #[error("Invalid projective point bytes")] + ProjectivePointSerialization, - /// This error occurs when the signing key fails to generate - #[error("Failed generation of the signing key")] - SigningKeyGenerationError, + /// This error occurs when the serialization of the prime order projective point bytes failed + #[error("Invalid prime order projective point bytes")] + PrimeOrderProjectivePointSerialization, /// This error occurs when the random scalar fails to generate during the signature #[error("Failed generation of the signature's random scalar")] - RandomScalarGenerationError, + RandomScalarGeneration, + + /// This error occurs when signing key is zero or one. + #[error("The signing key is invalid.")] + InvalidSigningKey, + + /// Given point is not on the curve + #[error("Given point is not on the curve")] + PointIsNotOnCurve(Box), + + /// Given point is not prime order + #[error("Given point is not prime order")] + PointIsNotPrimeOrder(Box), } diff --git a/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/curve_points.rs b/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/curve_points.rs new file mode 100644 index 00000000000..6dbd99eb824 --- /dev/null +++ b/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/curve_points.rs @@ -0,0 +1,434 @@ +use anyhow::anyhow; +use dusk_jubjub::{ + AffinePoint as JubjubAffinePoint, EDWARDS_D, ExtendedPoint as JubjubExtended, + SubgroupPoint as JubjubSubgroup, +}; +use group::{Group, GroupEncoding}; +use std::ops::{Add, Mul}; + +use super::{BaseFieldElement, ScalarFieldElement}; +use crate::{StmResult, signature_scheme::SchnorrSignatureError}; + +/// Represents a point in affine coordinates on the Jubjub curve +#[derive(Clone)] +pub(crate) struct AffinePoint(JubjubAffinePoint); + +impl AffinePoint { + /// Converts a projective point to its affine representation + pub(crate) fn from_projective_point(projective_point: ProjectivePoint) -> Self { + AffinePoint(JubjubAffinePoint::from(projective_point.0)) + } + + /// Retrieves the u-coordinate of the affine point + pub(crate) fn get_u(&self) -> BaseFieldElement { + BaseFieldElement(self.0.get_u()) + } + + /// Retrieves the v-coordinate of the affine point + pub(crate) fn get_v(&self) -> BaseFieldElement { + BaseFieldElement(self.0.get_v()) + } +} + +impl From<&PrimeOrderProjectivePoint> for AffinePoint { + /// Converts a prime order projective point to its affine representation + fn from(prime_order_projective_point: &PrimeOrderProjectivePoint) -> Self { + AffinePoint(JubjubAffinePoint::from(JubjubExtended::from( + prime_order_projective_point.0, + ))) + } +} + +/// Represents a point in projective coordinates on the Jubjub curve +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ProjectivePoint(pub(crate) JubjubExtended); + +impl ProjectivePoint { + /// Hashes input bytes to a projective point on the Jubjub curve + pub(crate) fn hash_to_projective_point(input: &[u8]) -> Self { + ProjectivePoint(JubjubExtended::hash_to_point(input)) + } + + /// Retrieves the (u, v) coordinates of the projective point in affine representation + pub(crate) fn get_coordinates(&self) -> (BaseFieldElement, BaseFieldElement) { + let affine_point = AffinePoint::from_projective_point(*self); + + (affine_point.get_u(), affine_point.get_v()) + } + + /// Converts the projective point to its byte representation + pub(crate) fn to_bytes(self) -> [u8; 32] { + self.0.to_bytes() + } + + /// Constructs a projective point from its byte representation + pub(crate) fn from_bytes(bytes: &[u8]) -> StmResult { + let mut projective_point_bytes = [0u8; 32]; + projective_point_bytes + .copy_from_slice(bytes.get(..32).ok_or(SchnorrSignatureError::Serialization)?); + + match JubjubExtended::from_bytes(&projective_point_bytes).into_option() { + Some(projective_point) => Ok(Self(projective_point)), + None => Err(anyhow!(SchnorrSignatureError::ProjectivePointSerialization)), + } + } + + /// Checks if the projective point is of prime order + pub(crate) fn is_prime_order(self) -> bool { + self.0.is_prime_order().into() + } +} + +impl Add for ProjectivePoint { + type Output = ProjectivePoint; + + /// Adds two projective points + fn add(self, other: ProjectivePoint) -> ProjectivePoint { + ProjectivePoint(self.0 + other.0) + } +} + +impl Mul for ScalarFieldElement { + type Output = ProjectivePoint; + + /// Multiplies a projective point by a scalar field element + /// Returns the resulting projective point + fn mul(self, point: ProjectivePoint) -> ProjectivePoint { + ProjectivePoint(point.0 * self.0) + } +} + +impl From for ProjectivePoint { + /// Converts a prime order projective point to a projective point + fn from(prime_order_projective_point: PrimeOrderProjectivePoint) -> Self { + ProjectivePoint(JubjubExtended::from(prime_order_projective_point.0)) + } +} + +/// Represents a point of prime order in projective coordinates on the Jubjub curve +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(crate) struct PrimeOrderProjectivePoint(pub(crate) JubjubSubgroup); + +impl PrimeOrderProjectivePoint { + /// Creates the generator point of the prime order subgroup + pub(crate) fn create_generator() -> Self { + PrimeOrderProjectivePoint(JubjubSubgroup::generator()) + } + + /// Checks if the given point is on the curve using its coordinates + pub(crate) fn is_on_curve(&self) -> StmResult { + let point_affine_representation = AffinePoint::from(self); + let (x, y) = ( + point_affine_representation.get_u(), + point_affine_representation.get_v(), + ); + let x_square = &x * &x; + let y_square = &y * &y; + + let lhs = &y_square - &x_square; + let rhs = (x_square * y_square) * BaseFieldElement(EDWARDS_D) + BaseFieldElement::get_one(); + + if lhs != rhs { + return Err(anyhow!(SchnorrSignatureError::PointIsNotOnCurve(Box::new( + *self + )))); + } + Ok(*self) + } + + /// Converts the prime order projective point to its byte representation + pub(crate) fn to_bytes(self) -> [u8; 32] { + self.0.to_bytes() + } + + /// Constructs a prime order projective point from its byte representation + pub(crate) fn from_bytes(bytes: &[u8]) -> StmResult { + let mut prime_order_projective_point_bytes = [0u8; 32]; + prime_order_projective_point_bytes + .copy_from_slice(bytes.get(..32).ok_or(SchnorrSignatureError::Serialization)?); + + match JubjubSubgroup::from_bytes(&prime_order_projective_point_bytes).into_option() { + Some(prime_order_projective_point) => Ok(Self(prime_order_projective_point)), + None => Err(anyhow!( + SchnorrSignatureError::PrimeOrderProjectivePointSerialization + )), + } + } +} + +impl Add for PrimeOrderProjectivePoint { + type Output = PrimeOrderProjectivePoint; + + /// Adds two prime order projective points + fn add(self, other: PrimeOrderProjectivePoint) -> PrimeOrderProjectivePoint { + PrimeOrderProjectivePoint(self.0 + other.0) + } +} + +impl Mul for ScalarFieldElement { + type Output = PrimeOrderProjectivePoint; + + /// Multiplies a prime order projective point by a scalar field element + /// Returns the resulting prime order projective point + fn mul(self, point: PrimeOrderProjectivePoint) -> PrimeOrderProjectivePoint { + PrimeOrderProjectivePoint(point.0 * self.0) + } +} + +#[cfg(test)] +mod tests { + use rand_chacha::ChaCha20Rng; + use rand_core::SeedableRng; + + use super::*; + + mod golden { + use super::*; + + const GOLDEN_JSON: &str = r#"[144, 52, 95, 161, 127, 253, 49, 32, 140, 217, 231, 207, 32, 238, 244, 196, 97, 241, 47, 95, 101, 9, 70, 136, 194, 66, 187, 253, 200, 32, 218, 43]"#; + + fn golden_value() -> PrimeOrderProjectivePoint { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let scalar = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let point = PrimeOrderProjectivePoint::create_generator(); + PrimeOrderProjectivePoint(point.0 * scalar.0) + } + + #[test] + fn golden_conversions() { + let value = serde_json::from_str(GOLDEN_JSON) + .expect("This JSON deserialization should not fail"); + assert_eq!(golden_value(), value); + + let serialized = + serde_json::to_string(&value).expect("This JSON serialization should not fail"); + let golden_serialized = serde_json::to_string(&golden_value()) + .expect("This JSON serialization should not fail"); + assert_eq!(golden_serialized, serialized); + } + } + + mod projective_point_arithmetic { + use super::*; + + #[test] + fn test_add() { + let mut rng = ChaCha20Rng::from_seed([1u8; 32]); + let scalar1 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let scalar2 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + + let point = ProjectivePoint::hash_to_projective_point(b"test_point"); + let p1 = scalar1 * point; + let p2 = scalar2 * point; + + let result = p1 + p2; + + let bytes = result.to_bytes(); + let recovered = ProjectivePoint::from_bytes(&bytes).unwrap(); + assert_eq!(result, recovered); + } + + #[test] + fn test_add_identity() { + let point = ProjectivePoint::hash_to_projective_point(b"test_point"); + let identity = ProjectivePoint(JubjubExtended::identity()); + + let result = point + identity; + assert_eq!(result, point); + } + + #[test] + fn test_add_commutativity() { + let mut rng = ChaCha20Rng::from_seed([2u8; 32]); + let scalar1 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let scalar2 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + + let point = ProjectivePoint::hash_to_projective_point(b"test_point"); + let p1 = scalar1 * point; + let p2 = scalar2 * point; + + assert_eq!(p1 + p2, p2 + p1); + } + + #[test] + fn test_add_associativity() { + let mut rng = ChaCha20Rng::from_seed([3u8; 32]); + let scalar1 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let scalar2 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let scalar3 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + + let point = ProjectivePoint::hash_to_projective_point(b"test_point"); + let p1 = scalar1 * point; + let p2 = scalar2 * point; + let p3 = scalar3 * point; + + assert_eq!((p1 + p2) + p3, p1 + (p2 + p3)); + } + + #[test] + fn test_scalar_mul() { + let mut rng = ChaCha20Rng::from_seed([4u8; 32]); + let scalar = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let point = ProjectivePoint::hash_to_projective_point(b"test_point"); + + let result = scalar * point; + + let bytes = result.to_bytes(); + let recovered = ProjectivePoint::from_bytes(&bytes).unwrap(); + assert_eq!(result, recovered); + } + + #[test] + fn test_scalar_mul_distributivity_over_point_addition() { + let mut rng = ChaCha20Rng::from_seed([5u8; 32]); + let scalar = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let point1 = ProjectivePoint::hash_to_projective_point(b"test_point_1"); + let point2 = ProjectivePoint::hash_to_projective_point(b"test_point_2"); + + let left = scalar * (point1 + point2); + let right = (scalar * point1) + (scalar * point2); + + assert_eq!(left, right); + } + + #[test] + fn test_scalar_mul_associativity() { + let mut rng = ChaCha20Rng::from_seed([6u8; 32]); + let scalar1 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let scalar2 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let point = ProjectivePoint::hash_to_projective_point(b"test_point"); + + let combined_scalar = scalar1 * scalar2; + let left = combined_scalar * point; + let right = scalar1 * (scalar2 * point); + + assert_eq!(left, right); + } + } + + mod prime_order_projective_point_arithmetic { + use super::*; + + #[test] + fn test_add() { + let mut rng = ChaCha20Rng::from_seed([7u8; 32]); + let scalar1 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let scalar2 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + + let generator = PrimeOrderProjectivePoint::create_generator(); + let p1 = scalar1 * generator; + let p2 = scalar2 * generator; + + let result = p1 + p2; + + let bytes = result.to_bytes(); + let recovered = PrimeOrderProjectivePoint::from_bytes(&bytes).unwrap(); + assert_eq!(result, recovered); + } + + #[test] + fn test_add_identity() { + let mut rng = ChaCha20Rng::from_seed([8u8; 32]); + let scalar = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let generator = PrimeOrderProjectivePoint::create_generator(); + let point = scalar * generator; + let identity = PrimeOrderProjectivePoint::default(); + + let result = point + identity; + assert_eq!(result, point); + } + + #[test] + fn test_add_commutativity() { + let mut rng = ChaCha20Rng::from_seed([9u8; 32]); + let scalar1 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let scalar2 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + + let generator = PrimeOrderProjectivePoint::create_generator(); + let p1 = scalar1 * generator; + let p2 = scalar2 * generator; + + assert_eq!(p1 + p2, p2 + p1); + } + + #[test] + fn test_add_associativity() { + let mut rng = ChaCha20Rng::from_seed([10u8; 32]); + let scalar1 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let scalar2 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let scalar3 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + + let generator = PrimeOrderProjectivePoint::create_generator(); + let p1 = scalar1 * generator; + let p2 = scalar2 * generator; + let p3 = scalar3 * generator; + + assert_eq!((p1 + p2) + p3, p1 + (p2 + p3)); + } + + #[test] + fn test_scalar_mul() { + let mut rng = ChaCha20Rng::from_seed([11u8; 32]); + let scalar = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let generator = PrimeOrderProjectivePoint::create_generator(); + + let result = scalar * generator; + + let bytes = result.to_bytes(); + let recovered = PrimeOrderProjectivePoint::from_bytes(&bytes).unwrap(); + assert_eq!(result, recovered); + } + + #[test] + fn test_scalar_mul_by_generator() { + let scalar = ScalarFieldElement(dusk_jubjub::Fr::one()); + let generator = PrimeOrderProjectivePoint::create_generator(); + + let result = scalar * generator; + assert_eq!(result, generator); + } + + #[test] + fn test_scalar_mul_distributivity_over_point_addition() { + let mut rng = ChaCha20Rng::from_seed([12u8; 32]); + let scalar = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let scalar1 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let scalar2 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + + let generator = PrimeOrderProjectivePoint::create_generator(); + let point1 = scalar1 * generator; + let point2 = scalar2 * generator; + + let left = scalar * (point1 + point2); + let right = (scalar * point1) + (scalar * point2); + + assert_eq!(left, right); + } + + #[test] + fn test_scalar_mul_associativity() { + let mut rng = ChaCha20Rng::from_seed([13u8; 32]); + let scalar1 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let scalar2 = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let generator = PrimeOrderProjectivePoint::create_generator(); + + let combined_scalar = scalar1 * scalar2; + let left = combined_scalar * generator; + let right = scalar1 * (scalar2 * generator); + + assert_eq!(left, right); + } + + #[test] + fn test_point_on_curve() { + let mut rng = ChaCha20Rng::from_seed([14u8; 32]); + let scalar = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let generator = PrimeOrderProjectivePoint::create_generator(); + + let point = scalar * generator; + + let result = point.is_on_curve().unwrap(); + assert_eq!(result, point); + } + } +} diff --git a/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/field_elements.rs b/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/field_elements.rs new file mode 100644 index 00000000000..dfb5a781148 --- /dev/null +++ b/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/field_elements.rs @@ -0,0 +1,319 @@ +use anyhow::anyhow; +use dusk_jubjub::{Fq as JubjubBase, Fr as JubjubScalar}; +use ff::Field; +use rand_core::{CryptoRng, RngCore}; +use std::ops::{Add, Mul, Sub}; + +use crate::{StmResult, signature_scheme::SchnorrSignatureError}; + +/// Represents an element in the base field of the Jubjub curve +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct BaseFieldElement(pub(crate) JubjubBase); + +impl BaseFieldElement { + /// Retrieves the multiplicative identity element of the base field + pub(crate) fn get_one() -> Self { + BaseFieldElement(JubjubBase::ONE) + } +} + +impl Add for BaseFieldElement { + type Output = BaseFieldElement; + + /// Adds two base field elements + fn add(self, other: BaseFieldElement) -> BaseFieldElement { + BaseFieldElement(self.0 + other.0) + } +} + +impl Sub for &BaseFieldElement { + type Output = BaseFieldElement; + + /// Subtracts one base field element from another + fn sub(self, other: &BaseFieldElement) -> BaseFieldElement { + BaseFieldElement(self.0 - other.0) + } +} + +impl Mul for BaseFieldElement { + type Output = BaseFieldElement; + + /// Multiplies two base field elements + fn mul(self, other: BaseFieldElement) -> BaseFieldElement { + BaseFieldElement(self.0 * other.0) + } +} + +impl Mul for &BaseFieldElement { + type Output = BaseFieldElement; + + /// Multiplies a base field element by another base field element + fn mul(self, other: &BaseFieldElement) -> BaseFieldElement { + BaseFieldElement(self.0 * other.0) + } +} + +/// Represents an element in the scalar field of the Jubjub curve +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ScalarFieldElement(pub(crate) JubjubScalar); + +impl ScalarFieldElement { + /// Generates a new random scalar field element + pub(crate) fn new_random_scalar(rng: &mut (impl RngCore + CryptoRng)) -> Self { + ScalarFieldElement(JubjubScalar::random(rng)) + } + + /// Checks if the scalar field element is zero + pub(crate) fn is_zero(&self) -> bool { + if self.0 == JubjubScalar::zero() { + return true; + } + false + } + + /// Checks if the scalar field element is one + pub(crate) fn is_one(&self) -> bool { + if self.0 == JubjubScalar::one() { + return true; + } + false + } + + /// Tries to generate a new random non-zero scalar field element in 100 attempts + /// + /// Returns an error if unable to generate a non-zero scalar after 100 attempts + pub(crate) fn new_random_nonzero_scalar( + rng: &mut (impl RngCore + CryptoRng), + ) -> StmResult { + for _ in 0..100 { + let random_scalar = Self::new_random_scalar(rng); + if !random_scalar.is_zero() { + return Ok(random_scalar); + } + } + Err(anyhow!(SchnorrSignatureError::RandomScalarGeneration)) + } + + /// Converts the scalar field element to its byte representation + pub(crate) fn to_bytes(self) -> [u8; 32] { + self.0.to_bytes() + } + + /// Constructs a scalar field element from its byte representation + pub(crate) fn from_bytes(bytes: &[u8]) -> StmResult { + let mut scalar_bytes = [0u8; 32]; + scalar_bytes.copy_from_slice( + bytes + .get(..32) + .ok_or(SchnorrSignatureError::ScalarFieldElementSerialization)?, + ); + + match JubjubScalar::from_bytes(&scalar_bytes).into_option() { + Some(scalar_field_element) => Ok(Self(scalar_field_element)), + None => Err(anyhow!( + SchnorrSignatureError::ScalarFieldElementSerialization + )), + } + } +} + +impl Mul for ScalarFieldElement { + type Output = ScalarFieldElement; + + /// Multiplies two scalar field elements + fn mul(self, other: ScalarFieldElement) -> ScalarFieldElement { + ScalarFieldElement(self.0 * other.0) + } +} + +impl Sub for ScalarFieldElement { + type Output = ScalarFieldElement; + + /// Subtracts one scalar field element from another + fn sub(self, other: ScalarFieldElement) -> ScalarFieldElement { + ScalarFieldElement(self.0 - other.0) + } +} + +#[cfg(test)] +mod tests { + use rand_chacha::ChaCha20Rng; + use rand_core::SeedableRng; + + use super::*; + + mod golden { + use super::*; + + const GOLDEN_JSON: &str = r#"[126, 191, 239, 197, 88, 151, 248, 254, 187, 143, 86, 35, 29, 62, 90, 13, 196, 71, 234, 5, 90, 124, 205, 194, 51, 192, 228, 133, 25, 140, 157, 7]"#; + + fn golden_value() -> ScalarFieldElement { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap() + } + + #[test] + fn golden_conversions() { + let value = serde_json::from_str(GOLDEN_JSON) + .expect("This JSON deserialization should not fail"); + assert_eq!(golden_value(), value); + + let serialized = + serde_json::to_string(&value).expect("This JSON serialization should not fail"); + let golden_serialized = serde_json::to_string(&golden_value()) + .expect("This JSON serialization should not fail"); + assert_eq!(golden_serialized, serialized); + } + } + + mod base_field_arithmetic { + use super::*; + + #[test] + fn test_add() { + let a = BaseFieldElement(JubjubBase::ONE); + let b = BaseFieldElement(JubjubBase::ONE); + let result = a + b; + assert_eq!(result, BaseFieldElement(JubjubBase::ONE + JubjubBase::ONE)); + } + + #[test] + fn test_add_with_zero() { + let a = BaseFieldElement(JubjubBase::ONE); + let zero = BaseFieldElement(JubjubBase::zero()); + let result = a.clone() + zero; + assert_eq!(result, a); + } + + #[test] + fn test_sub_references() { + let a = BaseFieldElement(JubjubBase::ONE + JubjubBase::ONE); + let b = BaseFieldElement(JubjubBase::ONE); + let result = &a - &b; + assert_eq!(result, BaseFieldElement(JubjubBase::ONE)); + } + + #[test] + fn test_sub_same_values() { + let a = BaseFieldElement(JubjubBase::ONE); + let b = BaseFieldElement(JubjubBase::ONE); + let result = &a - &b; + assert_eq!(result, BaseFieldElement(JubjubBase::zero())); + } + + #[test] + fn test_mul_owned() { + let a = BaseFieldElement(JubjubBase::ONE + JubjubBase::ONE); + let b = BaseFieldElement(JubjubBase::ONE + JubjubBase::ONE); + let result = a * b; + let expected = JubjubBase::ONE + JubjubBase::ONE; + assert_eq!(result, BaseFieldElement(expected * expected)); + } + + #[test] + fn test_mul_with_one() { + let a = BaseFieldElement(JubjubBase::ONE + JubjubBase::ONE); + let one = BaseFieldElement::get_one(); + let result = a.clone() * one; + assert_eq!(result, a); + } + + #[test] + fn test_mul_with_zero() { + let a = BaseFieldElement(JubjubBase::ONE); + let zero = BaseFieldElement(JubjubBase::zero()); + let result = a * zero; + assert_eq!(result, BaseFieldElement(JubjubBase::zero())); + } + + #[test] + fn test_mul_references() { + let a = BaseFieldElement(JubjubBase::ONE + JubjubBase::ONE); + let b = BaseFieldElement(JubjubBase::ONE + JubjubBase::ONE + JubjubBase::ONE); + let result = &a * &b; + let expected = (JubjubBase::ONE + JubjubBase::ONE) + * (JubjubBase::ONE + JubjubBase::ONE + JubjubBase::ONE); + assert_eq!(result, BaseFieldElement(expected)); + } + + #[test] + fn test_chained_operations() { + let a = BaseFieldElement(JubjubBase::ONE); + let b = BaseFieldElement(JubjubBase::ONE); + let c = BaseFieldElement(JubjubBase::ONE); + let result = (a + b) * c; + assert_eq!(result, BaseFieldElement(JubjubBase::ONE + JubjubBase::ONE)); + } + } + + mod scalar_field_arithmetic { + use super::*; + + #[test] + fn test_mul() { + let mut rng = ChaCha20Rng::from_seed([1u8; 32]); + let a = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let b = ScalarFieldElement(JubjubScalar::one()); + let result = a * b; + assert_eq!(result, a); + } + + #[test] + fn test_mul_with_zero() { + let mut rng = ChaCha20Rng::from_seed([2u8; 32]); + let a = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let zero = ScalarFieldElement(JubjubScalar::zero()); + let result = a * zero; + assert!(result.is_zero()); + } + + #[test] + fn test_mul_associativity() { + let mut rng = ChaCha20Rng::from_seed([3u8; 32]); + let a = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let b = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let c = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + + let result1 = (a * b) * c; + let result2 = a * (b * c); + assert_eq!(result1, result2); + } + + #[test] + fn test_sub() { + let mut rng = ChaCha20Rng::from_seed([4u8; 32]); + let a = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let result = a - a; + assert!(result.is_zero()); + } + + #[test] + fn test_sub_with_zero() { + let mut rng = ChaCha20Rng::from_seed([5u8; 32]); + let a = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let zero = ScalarFieldElement(JubjubScalar::zero()); + let result = a - zero; + assert_eq!(result, a); + } + + #[test] + fn test_sub_specific_values() { + let two = ScalarFieldElement(JubjubScalar::one() + JubjubScalar::one()); + let one = ScalarFieldElement(JubjubScalar::one()); + let result = two - one; + assert!(result.is_one()); + } + + #[test] + fn test_combined_operations() { + let mut rng = ChaCha20Rng::from_seed([6u8; 32]); + let a = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let b = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + let c = ScalarFieldElement::new_random_nonzero_scalar(&mut rng).unwrap(); + + let left = a * (b - c); + let right = (a * b) - (a * c); + assert_eq!(left, right); + } + } +} diff --git a/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/mod.rs b/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/mod.rs new file mode 100644 index 00000000000..f06633afc2b --- /dev/null +++ b/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/mod.rs @@ -0,0 +1,82 @@ +mod curve_points; +mod field_elements; +mod poseidon_digest; + +pub(crate) use curve_points::*; +pub(crate) use field_elements::*; +pub(crate) use poseidon_digest::*; + +use serde::{ + de::Visitor, + {Deserialize, Deserializer, Serialize, Serializer}, +}; + +// --------------------------------------------------------------------- +// Serde implementation +// --------------------------------------------------------------------- + +macro_rules! impl_serde { + ($st:ty,$visitor:ident,$size:expr) => { + impl Serialize for $st { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::SerializeTuple; + let mut seq = serializer.serialize_tuple($size)?; + for e in self.to_bytes().iter() { + seq.serialize_element(e)?; + } + seq.end() + } + } + + impl<'de> Deserialize<'de> for $st { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct $visitor; + + impl<'de> Visitor<'de> for $visitor { + type Value = $st; + + fn expecting( + &self, + formatter: &mut ::core::fmt::Formatter, + ) -> ::core::fmt::Result { + formatter.write_str(format!("a signature {}", stringify!($st)).as_str()) + } + + fn visit_seq(self, mut seq: A) -> Result<$st, A::Error> + where + A: serde::de::SeqAccess<'de>, + { + let mut bytes = [0u8; $size]; + for i in 0..$size { + bytes[i] = + seq.next_element()?.ok_or(serde::de::Error::invalid_length( + i, + &format!("expected bytes{}", $size.to_string()).as_str(), + ))?; + } + <$st>::from_bytes(&bytes).map_err(|_| { + serde::de::Error::custom( + &format!("deserialization failed [{}]", stringify!($st)).as_str(), + ) + }) + } + } + + deserializer.deserialize_tuple($size, $visitor) + } + } + }; +} +impl_serde!(ScalarFieldElement, ScalarFieldElementVisitor, 32); +impl_serde!( + PrimeOrderProjectivePoint, + PrimeOrderProjectivePointVisitor, + 32 +); +impl_serde!(ProjectivePoint, ProjectivePointVisitor, 32); diff --git a/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/poseidon_digest.rs b/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/poseidon_digest.rs new file mode 100644 index 00000000000..303969af29c --- /dev/null +++ b/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/poseidon_digest.rs @@ -0,0 +1,21 @@ +use dusk_jubjub::Fq as JubjubBase; +use dusk_poseidon::{Domain, Hash}; + +use super::{BaseFieldElement, ScalarFieldElement}; + +/// Domain Separation Tag (DST) for the Poseidon hash used in signature contexts. +const DST_SIGNATURE: JubjubBase = JubjubBase::from_raw([ + 0x5349_474E_5F44_5354, // "SIGN_DST" (ASCII), little-endian u64 + 0, + 0, + 0, +]); + +/// Computes a truncated Poseidon digest over the provided base field elements +/// Returns a scalar field element as the digest +pub(crate) fn compute_truncated_digest(input: &[BaseFieldElement]) -> ScalarFieldElement { + let mut poseidon_input = vec![DST_SIGNATURE]; + poseidon_input.extend(input.iter().map(|i| i.0).collect::>()); + + ScalarFieldElement(Hash::digest_truncated(Domain::Other, &poseidon_input)[0]) +} diff --git a/mithril-stm/src/signature_scheme/schnorr_signature/mod.rs b/mithril-stm/src/signature_scheme/schnorr_signature/mod.rs index 722e38d5df4..e9f944dae89 100644 --- a/mithril-stm/src/signature_scheme/schnorr_signature/mod.rs +++ b/mithril-stm/src/signature_scheme/schnorr_signature/mod.rs @@ -1,16 +1,214 @@ mod error; +mod jubjub; mod signature; mod signing_key; -mod utils; mod verification_key; pub use error::*; +pub(crate) use jubjub::*; pub use signature::*; pub use signing_key::*; -pub(crate) use utils::*; pub use verification_key::*; -use dusk_jubjub::Fq as JubjubBase; +#[cfg(test)] +mod tests { + use proptest::prelude::*; + use rand_chacha::ChaCha20Rng; + use rand_core::SeedableRng; -/// A DST (Domain Separation Tag) to distinguish between use of Poseidon hash -const DST_SIGNATURE: JubjubBase = JubjubBase::from_raw([0u64, 0, 0, 0]); + use crate::signature_scheme::{ + PrimeOrderProjectivePoint, ScalarFieldElement, SchnorrSignature, SchnorrSignatureError, + SchnorrSigningKey, SchnorrVerificationKey, + }; + + proptest! { + #![proptest_config(ProptestConfig::with_cases(50))] + + #[test] + fn verification_key(seed in any::<[u8;32]>()) { + // Valid generation check + let sk = SchnorrSigningKey::generate(&mut ChaCha20Rng::from_seed(seed)).unwrap(); + let g = PrimeOrderProjectivePoint::create_generator(); + let vk = sk.0 * g; + let vk_from_sk = SchnorrVerificationKey::new_from_signing_key(sk).unwrap(); + assert_eq!(vk, vk_from_sk.0); + + // Check if sk is 0 + let mut bytes = [0u8; 32]; + let sk = SchnorrSigningKey(ScalarFieldElement::from_bytes(&bytes).unwrap()); + SchnorrVerificationKey::new_from_signing_key(sk).expect_err("Verification key should not be generated from zero signing key"); + + // Check if sk is 1 + bytes[0] = 1; + let sk = SchnorrSigningKey(ScalarFieldElement::from_bytes(&bytes).unwrap()); + SchnorrVerificationKey::new_from_signing_key(sk).expect_err("Verification key should not be generated from one signing key"); + } + + #[test] + fn valid_signing_verification( + msg in prop::collection::vec(any::(), 1..128), + seed in any::<[u8;32]>(), + ) { + let sk_result = SchnorrSigningKey::generate(&mut ChaCha20Rng::from_seed(seed)); + assert!(sk_result.is_ok(), "Secret ket generation failed"); + let sk = sk_result.unwrap(); + + let vk = SchnorrVerificationKey::new_from_signing_key(sk.clone()).unwrap(); + + let sig_result = sk.sign(&msg, &mut ChaCha20Rng::from_seed(seed)); + assert!(sig_result.is_ok(), "Signature generation failed"); + + let sig = sig_result.unwrap(); + + assert!(sig.verify(&msg, &vk).is_ok(), "Verification failed."); + } + + #[test] + fn invalid_signature(msg in prop::collection::vec(any::(), 1..128), seed in any::<[u8;32]>()) { + let mut rng = ChaCha20Rng::from_seed(seed); + let sk1 = SchnorrSigningKey::generate(&mut rng).unwrap(); + let vk1 = SchnorrVerificationKey::new_from_signing_key(sk1).unwrap(); + let sk2 = SchnorrSigningKey::generate(&mut rng).unwrap(); + let fake_sig = sk2.sign(&msg, &mut rng).unwrap(); + + let error = fake_sig.verify(&msg, &vk1).expect_err("Fake signature should not be verified"); + + assert!( + matches!( + error.downcast_ref::(), + Some(SchnorrSignatureError::SignatureInvalid(_)) + ), + "Unexpected error: {error:?}" + ); + } + + #[test] + fn signing_key_to_from_bytes(seed in any::<[u8;32]>()) { + let mut rng = ChaCha20Rng::from_seed(seed); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + let mut sk_bytes = sk.to_bytes(); + + // Valid conversion + let recovered_sk = SchnorrSigningKey::from_bytes(&sk_bytes).unwrap(); + assert_eq!(sk.0, recovered_sk.0, "Recovered signing key does not match with the original!"); + + // Not enough bytes + let mut short_bytes = [0u8; 31]; + short_bytes.copy_from_slice(sk_bytes.get(..31).unwrap()); + let result = SchnorrSigningKey::from_bytes(&short_bytes).expect_err("From bytes conversion of signing key should fail"); + assert!( + matches!( + result.downcast_ref::(), + Some(SchnorrSignatureError::Serialization) + ), + "Unexpected error: {result:?}" + ); + + // Invalid bytes + sk_bytes[31] |= 0xff; + let result = SchnorrSigningKey::from_bytes(&sk_bytes).expect_err("From bytes conversion of signing key should fail"); + assert!( + matches!( + result.downcast_ref::(), + Some(SchnorrSignatureError::ScalarFieldElementSerialization) + ), + "Unexpected error: {result:?}" + ); + } + + #[test] + fn verification_key_to_from_bytes(seed in any::<[u8;32]>()) { + let mut rng = ChaCha20Rng::from_seed(seed); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + let vk = SchnorrVerificationKey::new_from_signing_key(sk.clone()).unwrap(); + let mut vk_bytes = vk.to_bytes(); + + // Valid conversion + let recovered_vk = SchnorrVerificationKey::from_bytes(&vk_bytes).unwrap(); + assert_eq!(vk.0, recovered_vk.0, "Recovered verification key does not match with the original!"); + + // Not enough bytes + let mut short_bytes = [0u8; 31]; + short_bytes.copy_from_slice(vk_bytes.get(..31).unwrap()); + let result = SchnorrVerificationKey::from_bytes(&short_bytes).expect_err("From bytes conversion of verification key should fail"); + assert!( + matches!( + result.downcast_ref::(), + Some(SchnorrSignatureError::Serialization) + ), + "Unexpected error: {result:?}" + ); + + // Invalid bytes + vk_bytes[31] |= 0xff; + let result = SchnorrVerificationKey::from_bytes(&vk_bytes).expect_err("From bytes conversion of verification key should fail"); + assert!( + matches!( + result.downcast_ref::(), + Some(SchnorrSignatureError::PrimeOrderProjectivePointSerialization) + ), + "Unexpected error: {result:?}" + ); + } + + #[test] + fn signature_to_from_bytes(msg in prop::collection::vec(any::(), 1..128), seed in any::<[u8;32]>()) { + let mut rng = ChaCha20Rng::from_seed(seed); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + let signature = sk.sign(&msg, &mut ChaCha20Rng::from_seed(seed)).unwrap(); + let signature_bytes = signature.to_bytes(); + + // Valid conversion + let recovered_signature = SchnorrSignature::from_bytes(&signature_bytes).unwrap(); + assert_eq!(signature, recovered_signature, "Recovered signature does not match with the original!"); + + // Test invalid `commitment_point` + let mut corrupted_bytes = signature_bytes; + corrupted_bytes[31] |= 0xff; + let result = SchnorrSignature::from_bytes(&corrupted_bytes).expect_err("From bytes conversion of signature should fail"); + assert!( + matches!( + result.downcast_ref::(), + Some(SchnorrSignatureError::ProjectivePointSerialization) + ), + "Unexpected error: {result:?}" + ); + + // Test invalid `response` + let mut corrupted_bytes = signature_bytes; + corrupted_bytes[63] |= 0xff; + let result = SchnorrSignature::from_bytes(&corrupted_bytes).expect_err("From bytes conversion should fail"); + assert!( + matches!( + result.downcast_ref::(), + Some(SchnorrSignatureError::ScalarFieldElementSerialization) + ), + "Unexpected error: {result:?}" + ); + + // Test invalid `challenge` + let mut corrupted_bytes = signature_bytes; + corrupted_bytes[95] |= 0xff; + let result = SchnorrSignature::from_bytes(&corrupted_bytes).expect_err("From bytes conversion should fail"); + assert!( + matches!( + result.downcast_ref::(), + Some(SchnorrSignatureError::ScalarFieldElementSerialization) + ), + "Unexpected error: {result:?}" + ); + + // Not enough bytes + let mut short_bytes = [0u8; 95]; + short_bytes.copy_from_slice(signature_bytes.get(..95).unwrap()); + let result = SchnorrSignature::from_bytes(&short_bytes).expect_err("From bytes conversion of signature should fail"); + assert!( + matches!( + result.downcast_ref::(), + Some(SchnorrSignatureError::Serialization) + ), + "Unexpected error: {result:?}" + ); + } + } +} diff --git a/mithril-stm/src/signature_scheme/schnorr_signature/signature.rs b/mithril-stm/src/signature_scheme/schnorr_signature/signature.rs index 4a16ebe156c..b46f669b920 100644 --- a/mithril-stm/src/signature_scheme/schnorr_signature/signature.rs +++ b/mithril-stm/src/signature_scheme/schnorr_signature/signature.rs @@ -1,31 +1,25 @@ use anyhow::{Context, anyhow}; -use dusk_jubjub::{ - ExtendedPoint as JubjubExtended, Fq as JubjubBase, Fr as JubjubScalar, - SubgroupPoint as JubjubSubgroup, -}; -use dusk_poseidon::{Domain, Hash}; -use group::{Group, GroupEncoding}; - -use crate::StmResult; +use serde::{Deserialize, Serialize}; use super::{ - DST_SIGNATURE, SchnorrSignatureError, SchnorrVerificationKey, get_coordinates_several_points, - is_on_curve, + BaseFieldElement, PrimeOrderProjectivePoint, ProjectivePoint, ScalarFieldElement, + SchnorrSignatureError, SchnorrVerificationKey, compute_truncated_digest, }; +use crate::StmResult; /// Structure of the Schnorr signature to use with the SNARK /// -/// This signature includes a value `sigma` which depends only on +/// This signature includes a value `commitment_point` which depends only on /// the message and the signing key. /// This value is used in the lottery process to determine the correct indices. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct SchnorrSignature { /// Deterministic value depending on the message and secret key - pub(crate) sigma: JubjubExtended, + pub(crate) commitment_point: ProjectivePoint, /// Part of the Schnorr signature depending on the secret key - pub(crate) signature: JubjubScalar, + pub(crate) response: ScalarFieldElement, /// Part of the Schnorr signature NOT depending on the secret key - pub(crate) challenge: JubjubScalar, + pub(crate) challenge: ScalarFieldElement, } impl SchnorrSignature { @@ -40,56 +34,50 @@ impl SchnorrSignature { /// - Ok(()) if the signature verifies and an error if not /// /// The protocol computes: - /// - msg_hash = H(Sha256(msg)) - /// - random_point_1_recomputed = msg_hash * signature + sigma * challenge - /// - random_point_2_recomputed = generator * signature + verification_key * challenge + /// - msg_hash_point = H(Sha256(msg)) + /// - random_point_1_recomputed = response * msg_hash_point + challenge * commitment_point + /// - random_point_2_recomputed = response * prime_order_generator_point + challenge * verification_key /// - challenge_recomputed = Poseidon(DST || H(Sha256(msg)) || verification_key - /// || sigma || random_point_1_recomputed || random_point_2_recomputed) + /// || commitment_point || random_point_1_recomputed || random_point_2_recomputed) /// /// Check: challenge == challenge_recomputed /// pub fn verify(&self, msg: &[u8], verification_key: &SchnorrVerificationKey) -> StmResult<()> { - // Check that the verification key is on the curve - if !is_on_curve(verification_key.0.into()) { - return Err(anyhow!(SchnorrSignatureError::VerificationKeyInvalid( - Box::new(*verification_key) - ))); - } + // Check that the verification key is valid + verification_key + .is_valid() + .with_context(|| "Signature verification failed due to invalid verification key")?; - let generator = JubjubSubgroup::generator(); + let prime_order_generator_point = PrimeOrderProjectivePoint::create_generator(); // First hashing the message to a scalar then hashing it to a curve point - let msg_hash = JubjubExtended::hash_to_point(msg); + let msg_hash_point = ProjectivePoint::hash_to_projective_point(msg); - // Computing R1 = H(msg) * s + sigma * c - let msg_hash_times_signature = msg_hash * self.signature; - let sigma_times_challenge = self.sigma * self.challenge; - let random_point_1_recomputed = msg_hash_times_signature + sigma_times_challenge; + // Computing random_point_1_recomputed = response * H(msg) + challenge * commitment_point + let random_point_1_recomputed = + self.response * msg_hash_point + self.challenge * self.commitment_point; - // Computing R2 = g * s + vk * c - let generator_times_signature = generator * self.signature; - let vk_times_challenge = verification_key.0 * self.challenge; - let random_point_2_recomputed = generator_times_signature + vk_times_challenge; + // Computing random_point_2_recomputed = response * prime_order_generator_point + challenge * vk + let random_point_2_recomputed = + self.response * prime_order_generator_point + self.challenge * verification_key.0; // Since the hash function takes as input scalar elements // We need to convert the EC points to their coordinates - let points_coordinates = get_coordinates_several_points(&[ - msg_hash, - verification_key.0.into(), - self.sigma, + let points_coordinates: Vec = [ + msg_hash_point, + ProjectivePoint::from(verification_key.0), + self.commitment_point, random_point_1_recomputed, - random_point_2_recomputed.into(), - ]); - - let mut poseidon_input = vec![DST_SIGNATURE]; - poseidon_input.extend( - points_coordinates - .into_iter() - .flat_map(|(x, y)| [x, y]) - .collect::>(), - ); + ProjectivePoint::from(random_point_2_recomputed), + ] + .iter() + .flat_map(|point| { + let (u, v) = point.get_coordinates(); + [u, v] + }) + .collect(); - let challenge_recomputed = Hash::digest_truncated(Domain::Other, &poseidon_input)[0]; + let challenge_recomputed = compute_truncated_digest(&points_coordinates); if challenge_recomputed != self.challenge { return Err(anyhow!(SchnorrSignatureError::SignatureInvalid(Box::new( @@ -100,11 +88,11 @@ impl SchnorrSignature { Ok(()) } - /// Convert an `SchnorrSignature` into bytes. + /// Convert a `SchnorrSignature` into bytes. pub fn to_bytes(self) -> [u8; 96] { let mut out = [0; 96]; - out[0..32].copy_from_slice(&self.sigma.to_bytes()); - out[32..64].copy_from_slice(&self.signature.to_bytes()); + out[0..32].copy_from_slice(&self.commitment_point.to_bytes()); + out[32..64].copy_from_slice(&self.response.to_bytes()); out[64..96].copy_from_slice(&self.challenge.to_bytes()); out @@ -113,40 +101,37 @@ impl SchnorrSignature { /// Convert bytes into a `SchnorrSignature`. pub fn from_bytes(bytes: &[u8]) -> StmResult { if bytes.len() < 96 { - return Err(anyhow!(SchnorrSignatureError::SerializationError)) + return Err(anyhow!(SchnorrSignatureError::Serialization)) .with_context(|| "Not enough bytes provided to create a signature."); } - let sigma_bytes = bytes[0..32] - .try_into() - .map_err(|_| anyhow!(SchnorrSignatureError::SerializationError)) - .with_context(|| "Failed to obtain sigma's bytes.")?; - let sigma = JubjubExtended::from_bytes(&sigma_bytes) - .into_option() - .ok_or(anyhow!(SchnorrSignatureError::SerializationError)) - .with_context(|| "Unable to convert bytes into a sigma value.")?; - - let signature_bytes = bytes[32..64] - .try_into() - .map_err(|_| anyhow!(SchnorrSignatureError::SerializationError)) - .with_context(|| "Failed to obtain signature's bytes.")?; - let signature = JubjubScalar::from_bytes(&signature_bytes) - .into_option() - .ok_or(anyhow!(SchnorrSignatureError::SerializationError)) - .with_context(|| "Unable to convert bytes into a signature value.")?; - - let challenge_bytes = bytes[64..96] - .try_into() - .map_err(|_| anyhow!(SchnorrSignatureError::SerializationError)) - .with_context(|| "Failed to obtain challenge's bytes.")?; - let challenge = JubjubScalar::from_bytes(&challenge_bytes) - .into_option() - .ok_or(anyhow!(SchnorrSignatureError::SerializationError)) - .with_context(|| "Unable to convert bytes into a challenge value.")?; + let commitment_point = ProjectivePoint::from_bytes( + bytes + .get(0..32) + .ok_or(SchnorrSignatureError::Serialization) + .with_context(|| "Could not get the bytes of `commitment_point`")?, + ) + .with_context(|| "Could not convert bytes to `commitment_point`")?; + + let response = ScalarFieldElement::from_bytes( + bytes + .get(32..64) + .ok_or(SchnorrSignatureError::Serialization) + .with_context(|| "Could not get the bytes of `response`")?, + ) + .with_context(|| "Could not convert the bytes to `response`")?; + + let challenge = ScalarFieldElement::from_bytes( + bytes + .get(64..96) + .ok_or(SchnorrSignatureError::Serialization) + .with_context(|| "Could not get the bytes of `challenge`")?, + ) + .with_context(|| "Could not convert bytes to `challenge`")?; Ok(Self { - sigma, - signature, + commitment_point, + response, challenge, }) } @@ -160,15 +145,29 @@ mod tests { use crate::signature_scheme::{SchnorrSignature, SchnorrSigningKey, SchnorrVerificationKey}; #[test] - fn invalid_sig() { + fn valid_signature_verification() { + let msg = vec![0, 0, 0, 1]; + let seed = [0u8; 32]; + let mut rng = ChaCha20Rng::from_seed(seed); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + let vk = SchnorrVerificationKey::new_from_signing_key(sk.clone()).unwrap(); + + let sig = sk.sign(&msg, &mut rng).unwrap(); + + sig.verify(&msg, &vk) + .expect("Valid signature should verify successfully"); + } + + #[test] + fn invalid_signature() { let msg = vec![0, 0, 0, 1]; let msg2 = vec![0, 0, 0, 2]; let seed = [0u8; 32]; let mut rng = ChaCha20Rng::from_seed(seed); - let sk = SchnorrSigningKey::try_generate(&mut rng).unwrap(); - let vk = SchnorrVerificationKey::from(&sk); - let sk2 = SchnorrSigningKey::try_generate(&mut rng).unwrap(); - let vk2 = SchnorrVerificationKey::from(&sk2); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + let vk = SchnorrVerificationKey::new_from_signing_key(sk.clone()).unwrap(); + let sk2 = SchnorrSigningKey::generate(&mut rng).unwrap(); + let vk2 = SchnorrVerificationKey::new_from_signing_key(sk2).unwrap(); let sig = sk.sign(&msg, &mut rng).unwrap(); let sig2 = sk.sign(&msg2, &mut rng).unwrap(); @@ -183,47 +182,92 @@ mod tests { } #[test] - fn serialize_deserialize_signature() { + fn from_bytes_signature_not_enough_bytes() { + let msg = vec![0u8; 95]; + let result = SchnorrSignature::from_bytes(&msg); + result.expect_err("Not enough bytes."); + } + + #[test] + fn from_bytes_signature_exact_size() { let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let msg = vec![1, 2, 3]; + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); - let msg = vec![0, 0, 0, 1]; - let sk = SchnorrSigningKey::try_generate(&mut rng).unwrap(); + let sig = sk.sign(&msg, &mut rng).unwrap(); + let sig_bytes: [u8; 96] = sig.to_bytes(); + + let sig_restored = SchnorrSignature::from_bytes(&sig_bytes).unwrap(); + assert_eq!(sig, sig_restored); + } + + #[test] + fn from_bytes_signature_extra_bytes() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let msg = vec![1, 2, 3]; + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); let sig = sk.sign(&msg, &mut rng).unwrap(); let sig_bytes: [u8; 96] = sig.to_bytes(); - let sig2 = SchnorrSignature::from_bytes(&sig_bytes).unwrap(); - assert_eq!(sig, sig2); + let mut extended_bytes = sig_bytes.to_vec(); + extended_bytes.extend_from_slice(&[0xFF; 10]); + + let sig_restored = SchnorrSignature::from_bytes(&extended_bytes).unwrap(); + assert_eq!(sig, sig_restored); } #[test] - fn from_bytes_signature_not_enough_bytes() { - let msg = vec![0u8; 95]; + fn to_bytes_is_deterministic() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let msg = vec![1, 2, 3]; + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); - let result = SchnorrSignature::from_bytes(&msg); + let sig = sk.sign(&msg, &mut rng).unwrap(); - result.expect_err("Not enough bytes."); + // Converting to bytes multiple times should give same result + let bytes1 = sig.to_bytes(); + let bytes2 = sig.to_bytes(); + + assert_eq!(bytes1, bytes2); } - mod golden { + #[test] + fn signature_roundtrip_preserves_verification() { + let mut rng = ChaCha20Rng::from_seed([42u8; 32]); + let msg = vec![5, 6, 7, 8, 9]; + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + let vk = SchnorrVerificationKey::new_from_signing_key(sk.clone()).unwrap(); + + // Create and verify original signature + let sig = sk.sign(&msg, &mut rng).unwrap(); + sig.verify(&msg, &vk).expect("Original signature should verify"); - use rand_chacha::ChaCha20Rng; - use rand_core::SeedableRng; + // Roundtrip through bytes + let sig_bytes = sig.to_bytes(); + let sig_restored = SchnorrSignature::from_bytes(&sig_bytes).unwrap(); - use crate::signature_scheme::{SchnorrSignature, SchnorrSigningKey}; + // Restored signature should still verify + sig_restored + .verify(&msg, &vk) + .expect("Restored signature should verify"); + } + + mod golden { + use super::*; const GOLDEN_BYTES: &[u8; 96] = &[ 143, 53, 198, 62, 178, 1, 88, 253, 21, 92, 100, 13, 72, 180, 198, 127, 39, 175, 102, - 69, 147, 249, 244, 224, 122, 121, 248, 68, 217, 242, 158, 113, 94, 57, 200, 241, 208, - 145, 251, 8, 92, 119, 163, 38, 81, 85, 54, 36, 193, 221, 254, 242, 21, 129, 110, 161, - 142, 184, 107, 156, 100, 34, 190, 9, 200, 20, 178, 142, 61, 253, 193, 11, 5, 180, 97, - 73, 125, 88, 162, 36, 30, 177, 225, 52, 136, 21, 138, 93, 81, 23, 19, 64, 82, 78, 229, - 3, + 69, 147, 249, 244, 224, 122, 121, 248, 68, 217, 242, 158, 113, 5, 81, 137, 228, 235, + 18, 112, 76, 71, 127, 44, 47, 60, 55, 144, 204, 254, 50, 67, 167, 67, 133, 79, 168, 10, + 153, 228, 114, 147, 64, 34, 9, 12, 75, 91, 200, 29, 62, 12, 245, 185, 181, 67, 251, + 210, 211, 37, 42, 204, 205, 133, 215, 235, 236, 193, 155, 2, 147, 83, 189, 148, 38, 71, + 0, ]; fn golden_value() -> SchnorrSignature { let mut rng = ChaCha20Rng::from_seed([0u8; 32]); - let sk = SchnorrSigningKey::try_generate(&mut rng).unwrap(); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); let msg = [0u8; 32]; sk.sign(&msg, &mut rng).unwrap() } @@ -239,4 +283,35 @@ mod tests { assert_eq!(golden_serialized, serialized); } } + + mod golden_json { + use super::*; + + const GOLDEN_JSON: &str = r#" + { + "commitment_point": [143, 53, 198, 62, 178, 1, 88, 253, 21, 92, 100, 13, 72, 180, 198, 127, 39, 175, 102, 69, 147, 249, 244, 224, 122, 121, 248, 68, 217, 242, 158, 113], + "response": [5, 81, 137, 228, 235, 18, 112, 76, 71, 127, 44, 47, 60, 55, 144, 204, 254, 50, 67, 167, 67, 133, 79, 168, 10, 153, 228, 114, 147, 64, 34, 9], + "challenge": [12, 75, 91, 200, 29, 62, 12, 245, 185, 181, 67, 251, 210, 211, 37, 42, 204, 205, 133, 215, 235, 236, 193, 155, 2, 147, 83, 189, 148, 38, 71, 0] + }"#; + + fn golden_value() -> SchnorrSignature { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + let msg = [0u8; 32]; + sk.sign(&msg, &mut rng).unwrap() + } + + #[test] + fn golden_conversions() { + let value = serde_json::from_str(GOLDEN_JSON) + .expect("This JSON deserialization should not fail"); + assert_eq!(golden_value(), value); + + let serialized = + serde_json::to_string(&value).expect("This JSON serialization should not fail"); + let golden_serialized = serde_json::to_string(&golden_value()) + .expect("This JSON serialization should not fail"); + assert_eq!(golden_serialized, serialized); + } + } } diff --git a/mithril-stm/src/signature_scheme/schnorr_signature/signing_key.rs b/mithril-stm/src/signature_scheme/schnorr_signature/signing_key.rs index 2bffc1dea70..e72df77f9d8 100644 --- a/mithril-stm/src/signature_scheme/schnorr_signature/signing_key.rs +++ b/mithril-stm/src/signature_scheme/schnorr_signature/signing_key.rs @@ -1,55 +1,46 @@ use anyhow::{Context, anyhow}; -use dusk_jubjub::{ - ExtendedPoint as JubjubExtended, Fq as JubjubBase, Fr as JubjubScalar, - SubgroupPoint as JubjubSubgroup, -}; -use dusk_poseidon::{Domain, Hash}; -use group::Group; use rand_core::{CryptoRng, RngCore}; - -use crate::StmResult; +use serde::{Deserialize, Serialize}; use super::{ - DST_SIGNATURE, SchnorrSignature, SchnorrSignatureError, SchnorrVerificationKey, - generate_non_zero_scalar, get_coordinates_several_points, + BaseFieldElement, PrimeOrderProjectivePoint, ProjectivePoint, ScalarFieldElement, + SchnorrSignature, SchnorrSignatureError, SchnorrVerificationKey, compute_truncated_digest, }; +use crate::StmResult; /// Schnorr Signing key, it is essentially a random scalar of the Jubjub scalar field -#[derive(Debug, Clone)] -pub struct SchnorrSigningKey(pub(crate) JubjubScalar); +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct SchnorrSigningKey(pub(crate) ScalarFieldElement); impl SchnorrSigningKey { /// Generate a random scalar value to use as signing key - pub fn try_generate(rng: &mut R) -> StmResult { - let r = generate_non_zero_scalar(rng); - match r { - Ok(s) => Ok(SchnorrSigningKey(s)), - Err(_) => Err(anyhow!(SchnorrSignatureError::SigningKeyGenerationError)) - .with_context(|| "Failed to generate a non zero schnorr signing key."), - } + pub fn generate(rng: &mut R) -> StmResult { + let scalar = ScalarFieldElement::new_random_nonzero_scalar(rng) + .with_context(|| "Failed to generate a non zero Schnorr signing key.")?; + Ok(SchnorrSigningKey(scalar)) } /// This function is an adapted version of the Schnorr signature scheme that includes - /// the computation of a deterministic value (called sigma) based on the message and the signing key + /// the computation of a deterministic value (called commitment_point) based on the message and the signing key /// and works with the Jubjub elliptic curve and the Poseidon hash function. /// /// Input: /// - a message: some bytes /// - a secret key: an element of the scalar field of the Jubjub curve /// Output: - /// - a signature of the form (sigma, signature, challenge): - /// - sigma is deterministic depending only on the message and secret key - /// - the signature and challenge depends on a random value generated during the signature + /// - a signature of the form (commitment_point, response, challenge): + /// - commitment_point is deterministic depending only on the message and secret key + /// - the response and challenge depends on a random value generated during the signature /// /// The protocol computes: - /// - sigma = H(Sha256(msg)) * secret_key + /// - commitment_point = secret_key * H(Sha256(msg)) /// - random_scalar, a random value - /// - random_point_1 = H(Sha256(msg)) * random_scalar - /// - random_point_2 = generator * random_scalar, where generator is a generator of the prime-order subgroup of Jubjub - /// - challenge = Poseidon(DST || H(Sha256(msg)) || verification_key || sigma || random_point_1 || random_point_2) - /// - signature = random_scalar - challenge * signing_key + /// - random_point_1 = random_scalar * H(Sha256(msg)) + /// - random_point_2 = random_scalar * prime_order_generator_point, where generator is a generator of the prime-order subgroup of Jubjub + /// - challenge = Poseidon(DST || H(Sha256(msg)) || verification_key || commitment_point || random_point_1 || random_point_2) + /// - response = random_scalar - challenge * signing_key /// - /// Output the signature (sigma, signature, challenge) + /// Output the signature (commitment_point, response, challenge) /// pub fn sign( &self, @@ -57,45 +48,45 @@ impl SchnorrSigningKey { rng: &mut R, ) -> StmResult { // Use the subgroup generator to compute the curve points - let generator = JubjubSubgroup::generator(); - let verification_key = SchnorrVerificationKey::from(self); + let prime_order_generator_point = PrimeOrderProjectivePoint::create_generator(); + let verification_key = SchnorrVerificationKey::new_from_signing_key(self.clone()) + .with_context(|| "Could not generate verification key from signing key.")?; // First hashing the message to a scalar then hashing it to a curve point - let msg_hash = JubjubExtended::hash_to_point(msg); + let msg_hash_point = ProjectivePoint::hash_to_projective_point(msg); - let sigma = msg_hash * self.0; + let commitment_point = self.0 * msg_hash_point; - // r1 = H(msg) * r, r2 = g * r - let random_scalar = generate_non_zero_scalar(rng) - .with_context(|| "Failed to generate a non zero scalar for the signature.")?; + let random_scalar = ScalarFieldElement::new_random_nonzero_scalar(rng) + .with_context(|| "Random scalar generation failed during signing.")?; - let random_point_1 = msg_hash * random_scalar; - let random_point_2 = generator * random_scalar; + let random_point_1 = random_scalar * msg_hash_point; + let random_point_2 = random_scalar * prime_order_generator_point; // Since the hash function takes as input scalar elements // We need to convert the EC points to their coordinates // The order must be preserved - let points_coordinates = get_coordinates_several_points(&[ - msg_hash, - verification_key.0.into(), - sigma, + let points_coordinates: Vec = [ + msg_hash_point, + ProjectivePoint::from(verification_key.0), + commitment_point, random_point_1, - random_point_2.into(), - ]); - - let mut poseidon_input = vec![DST_SIGNATURE]; - poseidon_input.extend( - points_coordinates - .into_iter() - .flat_map(|(x, y)| [x, y]) - .collect::>(), - ); - let challenge = Hash::digest_truncated(Domain::Other, &poseidon_input)[0]; - let signature = random_scalar - challenge * self.0; + ProjectivePoint::from(random_point_2), + ] + .iter() + .flat_map(|point| { + let (u, v) = point.get_coordinates(); + [u, v] + }) + .collect(); + + let challenge = compute_truncated_digest(&points_coordinates); + let challenge_times_sk = challenge * self.0; + let response = random_scalar - challenge_times_sk; Ok(SchnorrSignature { - sigma, - signature, + commitment_point, + response, challenge, }) } @@ -110,20 +101,13 @@ impl SchnorrSigningKey { /// The bytes must represent a Jubjub scalar or the conversion will fail pub fn from_bytes(bytes: &[u8]) -> StmResult { if bytes.len() < 32 { - return Err(anyhow!(SchnorrSignatureError::SerializationError)) - .with_context(|| "Not enough bytes provided to create a Schnorr signing key."); - } - - let signing_key_bytes = bytes[0..32] - .try_into() - .map_err(|_| anyhow!(SchnorrSignatureError::SerializationError)) - .with_context(|| "Failed to obtain the Schnorr signing key's bytes.")?; - - match JubjubScalar::from_bytes(&signing_key_bytes).into_option() { - Some(signing_key) => Ok(Self(signing_key)), - None => Err(anyhow!(SchnorrSignatureError::SerializationError)) - .with_context(|| "Failed to generate a Jubjub scalar from the given bytes."), + return Err(anyhow!(SchnorrSignatureError::Serialization)).with_context( + || "Not enough bytes provided to re-construct a Schnorr signing key.", + ); } + let scalar_field_element = ScalarFieldElement::from_bytes(bytes) + .with_context(|| "Could not construct Schnorr signing key from given bytes.")?; + Ok(SchnorrSigningKey(scalar_field_element)) } } @@ -132,68 +116,72 @@ mod tests { use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; - use super::*; + use crate::signature_scheme::SchnorrSigningKey; #[test] fn generate_signing_key() { let mut rng = ChaCha20Rng::from_seed([0u8; 32]); - let _sk = SchnorrSigningKey::try_generate(&mut rng); + let sk = SchnorrSigningKey::generate(&mut rng); + + assert!(sk.is_ok(), "Signing key generation should succeed"); } #[test] - fn sign_and_verify() { - let msg = vec![0, 0, 0, 1]; - let seed = [0u8; 32]; - let mut rng = ChaCha20Rng::from_seed(seed); - let sk = SchnorrSigningKey::try_generate(&mut rng).unwrap(); - let vk = SchnorrVerificationKey::from(&sk); + fn generate_different_keys() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let sk1 = SchnorrSigningKey::generate(&mut rng).unwrap(); + let sk2 = SchnorrSigningKey::generate(&mut rng).unwrap(); + + // Keys should be different + assert_ne!(sk1, sk2, "Different keys should be generated"); + } - let sig = sk.sign(&msg, &mut rng).unwrap(); + #[test] + fn from_bytes_not_enough_bytes() { + let bytes = vec![0u8; 31]; + let result = SchnorrSigningKey::from_bytes(&bytes); - sig.verify(&msg, &vk).unwrap(); + result.expect_err("Should fail with insufficient bytes"); } #[test] - fn to_from_bytes() { + fn from_bytes_exact_size() { let mut rng = ChaCha20Rng::from_seed([0u8; 32]); - let sk = SchnorrSigningKey::try_generate(&mut rng).unwrap(); - + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); let sk_bytes = sk.to_bytes(); - let recovered_sk = SchnorrSigningKey::from_bytes(&sk_bytes).unwrap(); - assert_eq!(sk.0, recovered_sk.0); + let sk_restored = SchnorrSigningKey::from_bytes(&sk_bytes).unwrap(); + + assert_eq!(sk, sk_restored); } - // Failing test as the generated bytes represent a value too big to be converted - // in this manner (greater than the jubjub modulus) #[test] - fn failing_test_from_bytes() { + fn from_bytes_extra_bytes() { let mut rng = ChaCha20Rng::from_seed([0u8; 32]); - let mut sk = [0; 32]; - rng.fill_bytes(&mut sk); - // Setting the msb to 1 to make sk bigger than the modulus - sk[0] |= 0xff; + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + let sk_bytes = sk.to_bytes(); + + let mut extended_bytes = sk_bytes.to_vec(); + extended_bytes.extend_from_slice(&[0xFF; 10]); - let result = SchnorrSigningKey::from_bytes(&sk); + let sk_restored = SchnorrSigningKey::from_bytes(&extended_bytes).unwrap(); - result.expect_err("Value is not a proper sk, test should fail."); + assert_eq!(sk, sk_restored); } #[test] - fn from_bytes_signing_key_not_enough_bytes() { - let msg = vec![0u8; 31]; + fn to_bytes_is_deterministic() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); - let result = SchnorrSigningKey::from_bytes(&msg); + let bytes1 = sk.to_bytes(); + let bytes2 = sk.to_bytes(); - result.expect_err("Not enough bytes."); + assert_eq!(bytes1, bytes2, "to_bytes should be deterministic"); } mod golden { - - use rand_chacha::ChaCha20Rng; - use rand_core::SeedableRng; - - use crate::signature_scheme::SchnorrSigningKey; + use super::*; const GOLDEN_BYTES: &[u8; 32] = &[ 126, 191, 239, 197, 88, 151, 248, 254, 187, 143, 86, 35, 29, 62, 90, 13, 196, 71, 234, @@ -202,7 +190,7 @@ mod tests { fn golden_value() -> SchnorrSigningKey { let mut rng = ChaCha20Rng::from_seed([0u8; 32]); - SchnorrSigningKey::try_generate(&mut rng).unwrap() + SchnorrSigningKey::generate(&mut rng).unwrap() } #[test] @@ -216,4 +204,28 @@ mod tests { assert_eq!(golden_serialized, serialized); } } + + mod golden_json { + use super::*; + + const GOLDEN_JSON: &str = r#"[126, 191, 239, 197, 88, 151, 248, 254, 187, 143, 86, 35, 29, 62, 90, 13, 196, 71, 234, 5, 90, 124, 205, 194, 51, 192, 228, 133, 25, 140, 157, 7]"#; + + fn golden_value() -> SchnorrSigningKey { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + SchnorrSigningKey::generate(&mut rng).unwrap() + } + + #[test] + fn golden_conversions() { + let value = serde_json::from_str(GOLDEN_JSON) + .expect("This JSON deserialization should not fail"); + assert_eq!(golden_value(), value); + + let serialized = + serde_json::to_string(&value).expect("This JSON serialization should not fail"); + let golden_serialized = serde_json::to_string(&golden_value()) + .expect("This JSON serialization should not fail"); + assert_eq!(golden_serialized, serialized); + } + } } diff --git a/mithril-stm/src/signature_scheme/schnorr_signature/utils.rs b/mithril-stm/src/signature_scheme/schnorr_signature/utils.rs deleted file mode 100644 index 606b60f1671..00000000000 --- a/mithril-stm/src/signature_scheme/schnorr_signature/utils.rs +++ /dev/null @@ -1,98 +0,0 @@ -use anyhow::{Context, anyhow}; -use dusk_jubjub::{ - AffinePoint as JubjubAffine, EDWARDS_D, ExtendedPoint as JubjubExtended, Fq as JubjubBase, - Fr as JubjubScalar, -}; -use ff::Field; -use group::Curve; -use rand_core::{CryptoRng, RngCore}; - -use crate::StmResult; - -use super::SchnorrSignatureError; - -/// Check if the given point is on the curve using its coordinates -pub fn is_on_curve(point: JubjubExtended) -> bool { - let point_affine_representation = JubjubAffine::from(point); - let (x, y) = ( - point_affine_representation.get_u(), - point_affine_representation.get_v(), - ); - let x_square = x.square(); - let y_square = y.square(); - - let lhs = y_square - x_square; - let rhs = JubjubBase::ONE + EDWARDS_D * x_square * y_square; - - lhs == rhs -} - -/// Extract the coordinates of given points in an Extended form -/// -/// This is mainly used to feed the Poseidon hash function, the order is maintained -/// from input to output which is important for the hash function -pub fn get_coordinates_several_points(points: &[JubjubExtended]) -> Vec<(JubjubBase, JubjubBase)> { - let mut points_affine = - vec![JubjubAffine::from_raw_unchecked(JubjubBase::ZERO, JubjubBase::ZERO); points.len()]; - JubjubExtended::batch_normalize(points, &mut points_affine); - - points_affine - .into_iter() - .map(|p| (p.get_u(), p.get_v())) - .collect::>() -} - -/// Generate a random non zero value from the scalar field of Jubjub -/// -/// Tries to generate 100 times a non zero value and returns an error if it fails to do so -pub fn generate_non_zero_scalar(rng: &mut R) -> StmResult { - for _ in 0..100 { - let random_scalar = JubjubScalar::random(&mut *rng); - if random_scalar != JubjubScalar::ZERO { - return Ok(random_scalar); - } - } - Err(anyhow!(SchnorrSignatureError::RandomScalarGenerationError)) - .with_context(|| "Failed to generate a non zero signing key after 100 attempts.") -} - -#[cfg(test)] -mod tests { - use dusk_jubjub::{AffinePoint as JubjubAffine, ExtendedPoint as JubjubExtended}; - use group::Group; - use rand_chacha::ChaCha20Rng; - use rand_core::SeedableRng; - - use super::*; - - #[test] - fn get_coordinates_from_several_points() { - let seed = [0u8; 32]; - let mut rng = ChaCha20Rng::from_seed(seed); - let points = vec![ - JubjubExtended::random(&mut rng), - JubjubExtended::random(&mut rng), - JubjubExtended::random(&mut rng), - ]; - - let coordinates = get_coordinates_several_points(&points); - - let mut coordinates_iter = coordinates.iter(); - for p in points.iter().take(3) { - let (x, y) = coordinates_iter.next().unwrap(); - let point_affine = JubjubAffine::from_raw_unchecked(*x, *y); - let point_extended = JubjubExtended::from_affine(point_affine); - assert_eq!(*p, point_extended); - } - } - - #[test] - fn generation_non_zero_scalar() { - let seed = [0u8; 32]; - let mut rng = ChaCha20Rng::from_seed(seed); - - let scalar = generate_non_zero_scalar(&mut rng).unwrap(); - - assert_ne!(scalar, JubjubScalar::ZERO); - } -} diff --git a/mithril-stm/src/signature_scheme/schnorr_signature/verification_key.rs b/mithril-stm/src/signature_scheme/schnorr_signature/verification_key.rs index 0e1787f3f96..a6b0fd7bd06 100644 --- a/mithril-stm/src/signature_scheme/schnorr_signature/verification_key.rs +++ b/mithril-stm/src/signature_scheme/schnorr_signature/verification_key.rs @@ -1,17 +1,41 @@ -use anyhow::{Context, anyhow}; -use dusk_jubjub::SubgroupPoint as JubjubSubgroup; -use group::{Group, GroupEncoding}; +use anyhow::{Context, Ok, anyhow}; +use serde::{Deserialize, Serialize}; +use super::{PrimeOrderProjectivePoint, ProjectivePoint, SchnorrSignatureError, SchnorrSigningKey}; use crate::StmResult; -use super::{SchnorrSignatureError, SchnorrSigningKey}; - /// Schnorr verification key, it consists of a point on the Jubjub curve /// vk = g * sk, where g is a generator -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct SchnorrVerificationKey(pub(crate) JubjubSubgroup); +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct SchnorrVerificationKey(pub(crate) PrimeOrderProjectivePoint); impl SchnorrVerificationKey { + /// Convert a Schnorr secret key into a verification key + /// + /// This is done by computing `vk = g * sk` where g is the generator + /// of the subgroup and sk is the schnorr secret key + pub fn new_from_signing_key(signing_key: SchnorrSigningKey) -> StmResult { + if signing_key.0.is_zero() | signing_key.0.is_one() { + return Err(anyhow!(SchnorrSignatureError::InvalidSigningKey)) + .with_context(|| "Verification key generation failed."); + } + let generator = PrimeOrderProjectivePoint::create_generator(); + + Ok(SchnorrVerificationKey(signing_key.0 * generator)) + } + + pub fn is_valid(&self) -> StmResult { + let projective_point = ProjectivePoint::from(self.0); + if !projective_point.is_prime_order() { + return Err(anyhow!(SchnorrSignatureError::PointIsNotPrimeOrder( + Box::new(self.0) + ))); + } + self.0.is_on_curve()?; + + Ok(*self) + } + /// Convert a `SchnorrVerificationKey` into bytes. pub fn to_bytes(self) -> [u8; 32] { self.0.to_bytes() @@ -22,98 +46,129 @@ impl SchnorrVerificationKey { /// The bytes must represent a Jubjub Subgroup point or the conversion will fail pub fn from_bytes(bytes: &[u8]) -> StmResult { if bytes.len() < 32 { - return Err(anyhow!(SchnorrSignatureError::SerializationError)).with_context( - || "Not enough bytes provided to create a Schnorr verification key.", + return Err(anyhow!(SchnorrSignatureError::Serialization)).with_context( + || "Not enough bytes provided to construct a Schnorr verification key.", ); } - let verification_key_bytes = bytes[0..32] - .try_into() - .map_err(|_| anyhow!(SchnorrSignatureError::SerializationError)) - .with_context(|| "Failed to obtain the Schnorr verification key's bytes.")?; - let point = JubjubSubgroup::from_bytes(&verification_key_bytes) - .into_option() - .ok_or(anyhow!(SchnorrSignatureError::SerializationError)) - .with_context(|| "Failed to create a JubjubSubgroup point from the given bytes.")?; - - Ok(SchnorrVerificationKey(point)) - } -} - -impl From<&SchnorrSigningKey> for SchnorrVerificationKey { - /// Convert a Schnorr secret key into a verification key - /// - /// This is done by computing `vk = g * sk` where g is the generator - /// of the subgroup and sk is the schnorr secret key - fn from(signing_key: &SchnorrSigningKey) -> Self { - let generator = JubjubSubgroup::generator(); + let prime_order_projective_point = PrimeOrderProjectivePoint::from_bytes(bytes) + .with_context(|| "Cannot construct Schnorr verification key from given bytes.")?; - SchnorrVerificationKey(generator * signing_key.0) + Ok(SchnorrVerificationKey(prime_order_projective_point)) } } #[cfg(test)] mod tests { - use dusk_jubjub::Fq as JubjubBase; - use dusk_jubjub::SubgroupPoint as JubjubSubgroup; - use ff::Field; - use group::Group; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; use crate::signature_scheme::{SchnorrSigningKey, SchnorrVerificationKey}; #[test] - fn generate_verification_key() { + fn create_verification_key_from_signing_key() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + + let vk = SchnorrVerificationKey::new_from_signing_key(sk); + + assert!( + vk.is_ok(), + "Verification key creation should succeed for valid signing key" + ); + } + + #[test] + fn different_signing_keys_produce_different_verification_keys() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let sk1 = SchnorrSigningKey::generate(&mut rng).unwrap(); + let sk2 = SchnorrSigningKey::generate(&mut rng).unwrap(); + + let vk1 = SchnorrVerificationKey::new_from_signing_key(sk1).unwrap(); + let vk2 = SchnorrVerificationKey::new_from_signing_key(sk2).unwrap(); + + assert_ne!( + vk1, vk2, + "Different signing keys should produce different verification keys" + ); + } + + #[test] + fn same_signing_key_produces_same_verification_key() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + + let vk1 = SchnorrVerificationKey::new_from_signing_key(sk.clone()).unwrap(); + let vk2 = SchnorrVerificationKey::new_from_signing_key(sk).unwrap(); + + assert_eq!( + vk1, vk2, + "Same signing key should produce same verification key" + ); + } + + #[test] + fn valid_verification_key_passes_validation() { let mut rng = ChaCha20Rng::from_seed([0u8; 32]); - let sk = SchnorrSigningKey::try_generate(&mut rng).unwrap(); - let g = JubjubSubgroup::generator(); - let vk = g * sk.0; + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + let vk = SchnorrVerificationKey::new_from_signing_key(sk).unwrap(); - let vk_from_sk = SchnorrVerificationKey::from(&sk); + let result = vk.is_valid(); - assert_eq!(vk, vk_from_sk.0); + assert!( + result.is_ok(), + "Valid verification key should pass validation" + ); } #[test] - fn verify_fail_verification_key_not_on_curve() { - let msg = vec![0, 0, 0, 1]; - let seed = [0u8; 32]; - let mut rng = ChaCha20Rng::from_seed(seed); - let sk = SchnorrSigningKey::try_generate(&mut rng).unwrap(); - let vk1 = SchnorrVerificationKey::from(&sk); - let sig = sk.sign(&msg, &mut rng).unwrap(); - let vk2 = SchnorrVerificationKey(JubjubSubgroup::from_raw_unchecked( - JubjubBase::ONE, - JubjubBase::ONE, - )); - - let result1 = sig.verify(&msg, &vk1); - let result2 = sig.verify(&msg, &vk2); - - result1.expect("Correct verification key used, test should pass."); - - result2.expect_err("Invalid verification key used, test should fail."); + fn from_bytes_not_enough_bytes() { + let bytes = vec![0u8; 31]; + let result = SchnorrVerificationKey::from_bytes(&bytes); + + result.expect_err("Should fail with insufficient bytes"); } #[test] - fn serialize_deserialize_vk() { - let seed = 0; - let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(seed); - let sk = SchnorrSigningKey::try_generate(&mut rng).unwrap(); - let vk = SchnorrVerificationKey::from(&sk); + fn from_bytes_exact_size() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + let vk = SchnorrVerificationKey::new_from_signing_key(sk).unwrap(); + let vk_bytes = vk.to_bytes(); + let vk_restored = SchnorrVerificationKey::from_bytes(&vk_bytes).unwrap(); + + assert_eq!(vk, vk_restored); + } + + #[test] + fn from_bytes_extra_bytes() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + let vk = SchnorrVerificationKey::new_from_signing_key(sk).unwrap(); let vk_bytes = vk.to_bytes(); - let vk2 = SchnorrVerificationKey::from_bytes(&vk_bytes).unwrap(); - assert_eq!(vk.0, vk2.0); + let mut extended_bytes = vk_bytes.to_vec(); + extended_bytes.extend_from_slice(&[0xFF; 10]); + + let vk_restored = SchnorrVerificationKey::from_bytes(&extended_bytes).unwrap(); + + assert_eq!(vk, vk_restored); } - mod golden { + #[test] + fn to_bytes_is_deterministic() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + let vk = SchnorrVerificationKey::new_from_signing_key(sk).unwrap(); - use rand_chacha::ChaCha20Rng; - use rand_core::SeedableRng; + let bytes1 = vk.to_bytes(); + let bytes2 = vk.to_bytes(); - use crate::signature_scheme::{SchnorrSigningKey, SchnorrVerificationKey}; + assert_eq!(bytes1, bytes2, "to_bytes should be deterministic"); + } + + mod golden { + use super::*; const GOLDEN_BYTES: &[u8; 32] = &[ 144, 52, 95, 161, 127, 253, 49, 32, 140, 217, 231, 207, 32, 238, 244, 196, 97, 241, 47, @@ -122,8 +177,8 @@ mod tests { fn golden_value() -> SchnorrVerificationKey { let mut rng = ChaCha20Rng::from_seed([0u8; 32]); - let sk = SchnorrSigningKey::try_generate(&mut rng).unwrap(); - SchnorrVerificationKey::from(&sk) + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + SchnorrVerificationKey::new_from_signing_key(sk).unwrap() } #[test] @@ -137,4 +192,29 @@ mod tests { assert_eq!(golden_serialized, serialized); } } + + mod golden_json { + use super::*; + + const GOLDEN_JSON: &str = r#"[144, 52, 95, 161, 127, 253, 49, 32, 140, 217, 231, 207, 32, 238, 244, 196, 97, 241, 47, 95, 101, 9, 70, 136, 194, 66, 187, 253, 200, 32, 218, 43]"#; + + fn golden_value() -> SchnorrVerificationKey { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let sk = SchnorrSigningKey::generate(&mut rng).unwrap(); + SchnorrVerificationKey::new_from_signing_key(sk).unwrap() + } + + #[test] + fn golden_conversions() { + let value = serde_json::from_str(GOLDEN_JSON) + .expect("This JSON deserialization should not fail"); + assert_eq!(golden_value(), value); + + let serialized = + serde_json::to_string(&value).expect("This JSON serialization should not fail"); + let golden_serialized = serde_json::to_string(&golden_value()) + .expect("This JSON serialization should not fail"); + assert_eq!(golden_serialized, serialized); + } + } }