diff --git a/Cargo.lock b/Cargo.lock index 7034440ffee..e496532fc57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,6 +185,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "asn1-rs" version = "0.7.1" @@ -552,6 +558,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.9.2" @@ -572,6 +590,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "blake2b_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1395,7 +1424,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.106", ] [[package]] @@ -1439,6 +1468,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive-hex" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6618553c32cd1c1f4fbdb9418cc035f3168422f24406ebb08576f6db5ed6ec" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -1508,6 +1548,65 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +[[package]] +name = "dusk-bls12_381" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633aa835dd9e4db7ad5dc94a3f95d44ff5c6856ec3c83c15eb75366cfb5ee68a" +dependencies = [ + "blake2b_simd", + "dusk-bytes", + "ff", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "dusk-bytes" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d209b92f0741edf1d99369bd4c1a1ef2fd0a85e885220f9e3fb0df3c61337f" +dependencies = [ + "derive-hex", +] + +[[package]] +name = "dusk-jubjub" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128c27bc829702d8e418eda75908234783db295ba19de45cf9b5133aa2e0f0b6" +dependencies = [ + "bitvec", + "blake2b_simd", + "dusk-bls12_381", + "dusk-bytes", + "ff", + "group", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "dusk-poseidon" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66925505adc9db3671ebed59f5f2f196306efbe6ede8de9761e7830af5ddb84" +dependencies = [ + "dusk-bls12_381", + "dusk-jubjub", + "dusk-safe", +] + +[[package]] +name = "dusk-safe" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3954d110d0d0f20555048d7171c2e6dfb54fd9a4220d30cc73dc66a88af42d" +dependencies = [ + "zeroize", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -1675,6 +1774,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1807,6 +1917,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -2102,6 +2218,17 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.12" @@ -4153,6 +4280,10 @@ dependencies = [ "blst", "criterion", "digest 0.10.7", + "dusk-jubjub", + "dusk-poseidon", + "ff", + "group", "num-bigint", "num-rational", "num-traits", @@ -5348,6 +5479,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -6544,6 +6681,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.44" @@ -7535,7 +7678,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -7929,6 +8072,15 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x25519-dalek" version = "2.0.1" diff --git a/mithril-stm/Cargo.toml b/mithril-stm/Cargo.toml index a22098e9ffc..532dd062ce7 100644 --- a/mithril-stm/Cargo.toml +++ b/mithril-stm/Cargo.toml @@ -19,6 +19,13 @@ rug-backend = ["rug/default"] num-integer-backend = ["num-bigint", "num-rational", "num-traits"] benchmark-internals = [] # For benchmarking multi_sig future_proof_system = [] # For activating future proof systems +future_snark = [ + "ff", + "group", + "num-traits", + "dusk-poseidon", + "dusk-jubjub", +] # For activating snark features [dependencies] anyhow = { workspace = true } @@ -26,6 +33,11 @@ blake2 = "0.10.6" # Enforce blst portable feature for runtime detection of Intel ADX instruction set. blst = { version = "0.3.16", features = ["portable"] } digest = { workspace = true } +dusk-jubjub = { version = "0.15.1", optional = true } +dusk-poseidon = { version = "0.41.0", optional = true } +ff = { version = "0.13.1", optional = true } +group = { version = "0.13.0", optional = true } +num-traits = { version = "0.2.19", optional = true } rand_core = { workspace = true } rayon = { workspace = true } serde = { workspace = true } @@ -58,6 +70,11 @@ name = "multi_sig" harness = false required-features = ["benchmark-internals"] +[[bench]] +name = "schnorr_sig" +harness = false +required-features = ["future_snark"] + [[bench]] name = "stm" harness = false diff --git a/mithril-stm/benches/schnorr_sig.rs b/mithril-stm/benches/schnorr_sig.rs new file mode 100644 index 00000000000..94a2ca73aba --- /dev/null +++ b/mithril-stm/benches/schnorr_sig.rs @@ -0,0 +1,41 @@ +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use mithril_stm::{SchnorrSigningKey, SchnorrVerificationKey}; +use rand_chacha::ChaCha20Rng; +use rand_core::{RngCore, SeedableRng}; + +fn sign_and_verify(c: &mut Criterion, nr_sigs: usize) { + let mut group = c.benchmark_group("Schnorr".to_string()); + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let mut rng_sig = ChaCha20Rng::from_seed([1u8; 32]); + + let mut msg = [0u8; 32]; + rng.fill_bytes(&mut msg); + let mut mvks = Vec::new(); + let mut sigs = Vec::new(); + for _ in 0..nr_sigs { + let sk = SchnorrSigningKey::generate(&mut rng); + let vk = SchnorrVerificationKey::from(&sk); + let sig = sk.sign(&msg, &mut rng_sig).unwrap(); + sigs.push(sig); + mvks.push(vk); + } + + group.bench_function(BenchmarkId::new("Individual verif", nr_sigs), |b| { + b.iter(|| { + for (vk, sig) in mvks.iter().zip(sigs.iter()) { + assert!(sig.verify(&msg, vk).is_ok()); + } + }) + }); +} + +fn schnorr_benches(c: &mut Criterion) { + sign_and_verify(c, 856); +} + +criterion_group!(name = benches; + config = Criterion::default().nresamples(1000); + targets = + schnorr_benches +); +criterion_main!(benches); diff --git a/mithril-stm/src/error.rs b/mithril-stm/src/error.rs index 800777d2514..d9ebe45982b 100644 --- a/mithril-stm/src/error.rs +++ b/mithril-stm/src/error.rs @@ -7,6 +7,8 @@ use crate::aggregate_signature::AggregateSignatureType; use crate::bls_multi_signature::{ BlsSignature, BlsVerificationKey, BlsVerificationKeyProofOfPossession, }; +#[cfg(feature = "future_snark")] +use crate::{SchnorrSignature, SchnorrVerificationKey}; /// Error types for multi signatures. #[derive(Debug, thiserror::Error, Eq, PartialEq)] @@ -40,6 +42,23 @@ pub enum MultiSignatureError { VerificationKeyInfinity(Box), } +/// Error types for Schnorr signatures. +#[cfg(feature = "future_snark")] +#[derive(Debug, thiserror::Error, Eq, PartialEq)] +pub enum SchnorrSignatureError { + /// Invalid Single signature + #[error("Invalid Schnorr single signature")] + SignatureInvalid(Box), + + /// Invalid Verification key + #[error("Invalid Schnorr Verification key")] + VerificationKeyInvalid(Box), + + /// This error occurs when the the serialization of the raw bytes failed + #[error("Invalid bytes")] + SerializationError, +} + /// Error types related to merkle trees. #[derive(Debug, Clone, thiserror::Error)] pub enum MerkleTreeError { diff --git a/mithril-stm/src/lib.rs b/mithril-stm/src/lib.rs index 9030ebd3993..940a2a9ec7b 100644 --- a/mithril-stm/src/lib.rs +++ b/mithril-stm/src/lib.rs @@ -119,6 +119,8 @@ mod key_registration; mod merkle_tree; mod parameters; mod participant; +#[cfg(feature = "future_snark")] +mod schnorr_signature; mod single_signature; pub use aggregate_signature::{ @@ -138,6 +140,9 @@ pub use bls_multi_signature::{ BlsVerificationKeyProofOfPossession, }; +#[cfg(feature = "future_snark")] +pub use schnorr_signature::{SchnorrSignature, SchnorrSigningKey, SchnorrVerificationKey}; + /// The quantity of stake held by a party, represented as a `u64`. pub type Stake = u64; diff --git a/mithril-stm/src/schnorr_signature/mod.rs b/mithril-stm/src/schnorr_signature/mod.rs new file mode 100644 index 00000000000..7fbbd063ae9 --- /dev/null +++ b/mithril-stm/src/schnorr_signature/mod.rs @@ -0,0 +1,173 @@ +// TODO: Remove +#![allow(dead_code)] + +mod signature; +mod signing_key; +pub(super) mod utils; +mod verification_key; + +pub use signature::*; +pub use signing_key::*; +pub use utils::*; +pub use verification_key::*; + +use dusk_jubjub::Fq as JubjubBase; + +/// A DST (Domain Separation Tag) to distinguish between use of Poseidon hash +pub(crate) const DST_SIGNATURE: JubjubBase = JubjubBase::from_raw([0u64, 0, 0, 0]); + +#[cfg(test)] +mod tests { + + use super::*; + use dusk_jubjub::SubgroupPoint as JubjubSubgroup; + use ff::Field; + use group::Group; + use rand_chacha::ChaCha20Rng; + use rand_core::SeedableRng; + + use crate::schnorr_signature::{SchnorrSigningKey, SchnorrVerificationKey}; + + #[test] + fn test_get_coordinates() { + let seed = [0u8; 32]; + let mut rng = ChaCha20Rng::from_seed(seed); + let point = JubjubSubgroup::random(&mut rng); + + let (_x, _y) = get_coordinates_extended(point.into()); + } + + #[test] + fn test_generate_verification_key() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let sk = SchnorrSigningKey::generate(&mut rng); + let g = JubjubSubgroup::generator(); + let vk = g * sk.0; + + let vk_from_sk = SchnorrVerificationKey::from(&sk); + + assert_eq!(vk, vk_from_sk.0); + } + + #[test] + fn test_sign_and_verify() { + let msg = vec![0, 0, 0, 1]; + let seed = [0u8; 32]; + let mut rng = ChaCha20Rng::from_seed(seed); + let sk = SchnorrSigningKey::generate(&mut rng); + let vk = SchnorrVerificationKey::from(&sk); + + let sig = sk.sign(&msg, &mut rng).unwrap(); + + sig.verify(&msg, &vk).unwrap(); + } + + #[test] + fn test_invalid_sig() { + 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::generate(&mut rng); + let vk = SchnorrVerificationKey::from(&sk); + let sk2 = SchnorrSigningKey::generate(&mut rng); + let vk2 = SchnorrVerificationKey::from(&sk2); + + let sig = sk.sign(&msg, &mut rng).unwrap(); + let sig2 = sk.sign(&msg2, &mut rng).unwrap(); + + // Wrong verification key is used + let result1 = sig.verify(&msg, &vk2); + let result2 = sig2.verify(&msg, &vk); + + assert!( + result1.is_err(), + "Wrong verfication key used, test should fail." + ); + // Wrong message is verified + assert!(result2.is_err(), "Wrong message used, test should fail."); + } + + #[test] + fn serialize_deserialize_vk() { + let seed = 0; + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(seed); + let sk = SchnorrSigningKey::generate(&mut rng); + let vk = SchnorrVerificationKey::from(&sk); + + let vk_bytes = vk.to_bytes(); + let vk2 = SchnorrVerificationKey::from_bytes(&vk_bytes).unwrap(); + + assert_eq!(vk.0, vk2.0); + } + + #[test] + fn serialize_deserialize_sk() { + let seed = 0; + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(seed); + let sk = SchnorrSigningKey::generate(&mut rng); + + let sk_bytes: [u8; 32] = sk.to_bytes(); + let sk2 = SchnorrSigningKey::from_bytes(&sk_bytes).unwrap(); + + assert_eq!(sk, sk2); + } + + #[test] + fn serialize_deserialize_signature() { + let seed = 0; + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(seed); + let msg = vec![0, 0, 0, 1]; + let sk = SchnorrSigningKey::generate(&mut rng); + + 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); + } + + #[test] + fn from_bytes_signature_not_enough_bytes() { + let msg = vec![0u8; 95]; + + let result = SchnorrSignature::from_bytes(&msg); + + assert!(result.is_err()); + } + + #[test] + fn from_bytes_signing_key_not_enough_bytes() { + let msg = vec![0u8; 31]; + + let result = SchnorrSigningKey::from_bytes(&msg); + + assert!(result.is_err()); + } + + #[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::generate(&mut rng); + 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); + + assert!( + result1.is_ok(), + "Correct verification key used, test should pass." + ); + assert!( + result2.is_err(), + "Invalid verification key used, test should fail." + ); + } +} diff --git a/mithril-stm/src/schnorr_signature/signature.rs b/mithril-stm/src/schnorr_signature/signature.rs new file mode 100644 index 00000000000..c38b38f976d --- /dev/null +++ b/mithril-stm/src/schnorr_signature/signature.rs @@ -0,0 +1,152 @@ +use anyhow::{Context, anyhow}; + +use dusk_jubjub::{ + ExtendedPoint as JubjubExtended, Fr as JubjubScalar, SubgroupPoint as JubjubSubgroup, +}; +use dusk_poseidon::{Domain, Hash}; +use group::{Group, GroupEncoding}; + +use crate::{ + StmResult, + error::SchnorrSignatureError, + schnorr_signature::{ + DST_SIGNATURE, SchnorrVerificationKey, get_coordinates_several_points, is_on_curve, + }, +}; + +/// Structure of the Schnorr signature to use with the SNARK +/// +/// This signature includes a value `sigma` 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)] +pub struct SchnorrSignature { + /// Deterministic value depending on the message and secret key + pub(crate) sigma: JubjubExtended, + /// Part of the Schnorr signature depending on the secret key + pub(crate) signature: JubjubScalar, + /// Part of the Schnorr signature NOT depending on the secret key + // pub(crate) challenge: JubjubBase, + pub(crate) challenge: JubjubScalar, +} + +impl SchnorrSignature { + /// This function performs the verification of a Schnorr signature given the signature, the signed message + /// and a verification key derived from the secret key used to sign. + /// + /// Input: + /// - a Schnorr signature + /// - a message: some bytes + /// - a verification key: a value depending on the secret key + /// Output: + /// - 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 + /// - challenge_recomputed = Poseidon(DST || H(Sha256(msg)) || verification_key + /// || sigma || 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) + ))); + } + + let generator = JubjubSubgroup::generator(); + + // First hashing the message to a scalar then hashing it to a curve point + let msg_hash = JubjubExtended::hash_to_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 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; + + // 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, + random_point_1_recomputed, + random_point_2_recomputed.into(), + ]); + + let mut poseidon_input = vec![DST_SIGNATURE]; + poseidon_input.extend(points_coordinates); + + let challenge_recomputed = Hash::digest_truncated(Domain::Other, &poseidon_input)[0]; + + if challenge_recomputed != self.challenge { + return Err(anyhow!(SchnorrSignatureError::SignatureInvalid(Box::new( + *self + )))); + } + + Ok(()) + } + + /// Convert an `SchnorrSignature` to a byte representation. + 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[64..96].copy_from_slice(&self.challenge.to_bytes()); + + out + } + + /// Convert a string of bytes into a `SchnorrSignature`. + /// + /// Not sure the sigma, s and c creation can fail if the 96 bytes are correctly extracted. + pub fn from_bytes(bytes: &[u8]) -> StmResult { + if bytes.len() < 96 { + return Err(anyhow!(SchnorrSignatureError::SerializationError)) + .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.")?; + + Ok(Self { + sigma, + signature, + challenge, + }) + } +} diff --git a/mithril-stm/src/schnorr_signature/signing_key.rs b/mithril-stm/src/schnorr_signature/signing_key.rs new file mode 100644 index 00000000000..8457ef8b9fd --- /dev/null +++ b/mithril-stm/src/schnorr_signature/signing_key.rs @@ -0,0 +1,171 @@ +use anyhow::{Context, anyhow}; +use dusk_jubjub::{ + ExtendedPoint as JubjubExtended, Fr as JubjubScalar, SubgroupPoint as JubjubSubgroup, +}; +use dusk_poseidon::{Domain, Hash}; +use ff::Field; +use rand_core::{CryptoRng, RngCore}; + +use group::Group; + +use crate::{ + StmResult, + error::SchnorrSignatureError, + schnorr_signature::{ + DST_SIGNATURE, SchnorrSignature, SchnorrVerificationKey, get_coordinates_several_points, + }, +}; + +/// Schnorr Signing key, it is essentially a random scalar of the Jubjub scalar field +#[derive(Debug, Clone)] +pub struct SchnorrSigningKey(pub JubjubScalar); + +impl SchnorrSigningKey { + /// Generate a random scalar value to use as signing key + pub fn generate(rng: &mut (impl RngCore + CryptoRng)) -> Self { + loop { + let signing_key = JubjubScalar::random(&mut *rng); + if signing_key != JubjubScalar::ZERO { + return SchnorrSigningKey(signing_key); + } + } + } + + /// This function is an adapted version of the Schnorr signature scheme + /// and works with the Jubjub elliptic curve and the Poseidon hash function. + /// + /// Input: + /// - a message: some bytes + /// - a secret key: a element of the scalar field of the Jubjub curve + /// Output: + /// - a signature of the form (sigma, signature, challenge) where sigma is deterministic based + /// on the message and the secret key and the signature and challenge are computed using randomness + /// + /// The protocol computes: + /// - sigma = H(Sha256(msg)) * secret_key + /// - 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 + /// + /// Output the signature (sigma, signature, challenge) + /// + pub fn sign( + &self, + msg: &[u8], + rng: &mut (impl RngCore + CryptoRng), + ) -> StmResult { + // Use the subgroup generator to compute the curve points + let generator = JubjubSubgroup::generator(); + let verification_key = SchnorrVerificationKey::from(self); + + // First hashing the message to a scalar then hashing it to a curve point + let msg_hash = JubjubExtended::hash_to_point(msg); + + let sigma = msg_hash * self.0; + + // r1 = H(msg) * r, r2 = g * r + let random_scalar = JubjubScalar::random(rng); + let random_point_1 = msg_hash * random_scalar; + let random_point_2 = generator * random_scalar; + + // 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, + random_point_1, + random_point_2.into(), + ]); + + let mut poseidon_input = vec![DST_SIGNATURE]; + poseidon_input.extend(points_coordinates); + + let challenge = Hash::digest_truncated(Domain::Other, &poseidon_input)[0]; + + let signature = random_scalar - challenge * self.0; + + Ok(SchnorrSignature { + sigma, + signature, + challenge, + }) + } + + /// Convert a `SchnorrSigningKey` into a string of bytes. + pub(crate) fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes() + } + + /// Convert a string of bytes into a `SchnorrSigningKey`. + /// + /// The bytes must represent a Jubjub scalar or the conversion will fail + pub(crate) fn from_bytes(bytes: &[u8]) -> StmResult { + if bytes.len() < 32 { + return Err(anyhow!(SchnorrSignatureError::SerializationError)) + .with_context(|| "Not enough bytes provided to create a signing key."); + } + + let signing_key_bytes = bytes[0..32] + .try_into() + .map_err(|_| anyhow!(SchnorrSignatureError::SerializationError)) + .with_context(|| "Failed to obtain signing key's bytes.")?; + + // Jubjub returns a CtChoice so I convert it to an option that looses the const time property + match JubjubScalar::from_bytes(&signing_key_bytes).into_option() { + Some(signing_key) => Ok(Self(signing_key)), + None => Err(anyhow!(SchnorrSignatureError::SerializationError)), + } + } +} + +#[cfg(test)] +mod tests { + pub(crate) use super::*; + use rand_chacha::ChaCha20Rng; + use rand_core::SeedableRng; + + impl PartialEq for SchnorrSigningKey { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } + } + + #[test] + fn test_generate_signing_key() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let _sk = SchnorrSigningKey::generate(&mut rng); + } + + #[test] + fn test_to_from_bytes() { + let mut rng = ChaCha20Rng::from_seed([0u8; 32]); + let sk = SchnorrSigningKey::generate(&mut rng); + + let bytes = sk.to_bytes(); + let recovered_sk = SchnorrSigningKey::from_bytes(&bytes).unwrap(); + + assert_eq!(sk, recovered_sk); + } + + // For now failing test, maybe change it later depending on what + // we want for from_bytes + #[test] + fn failing_test_from_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 result = SchnorrSigningKey::from_bytes(&sk); + + assert!( + result.is_err(), + "Value is not a proper sk, test should fail." + ); + } +} diff --git a/mithril-stm/src/schnorr_signature/utils.rs b/mithril-stm/src/schnorr_signature/utils.rs new file mode 100644 index 00000000000..4eda067015f --- /dev/null +++ b/mithril-stm/src/schnorr_signature/utils.rs @@ -0,0 +1,90 @@ +use dusk_jubjub::{ + AffinePoint as JubjubAffine, EDWARDS_D, ExtendedPoint as JubjubExtended, Fq as JubjubBase, + Fr as JubjubScalar, +}; + +use ff::Field; + +use anyhow::{Context, Ok, anyhow}; + +use crate::{StmResult, error::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 a given point in an Extended form +/// +/// This is mainly use to feed the Poseidon hash function +pub fn get_coordinates_extended(point: JubjubExtended) -> (JubjubBase, JubjubBase) { + let point_affine_representation = JubjubAffine::from(point); + let x_coordinate = point_affine_representation.get_u(); + let y_coordinate = point_affine_representation.get_v(); + + (x_coordinate, y_coordinate) +} + +/// Extract the coordinates of given points in an Extended form +/// +/// This is mainly use 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 { + let mut points_coordinates = vec![]; + for p in points { + let point_affine_representation = JubjubAffine::from(p); + let x_coordinate = point_affine_representation.get_u(); + let y_coordinate = point_affine_representation.get_v(); + points_coordinates.push(x_coordinate); + points_coordinates.push(y_coordinate); + } + points_coordinates +} + +/// Convert an element of the BLS12-381 base field to one of the Jubjub base field +pub fn jubjub_base_to_scalar(x: &JubjubBase) -> StmResult { + let bytes = x.to_bytes(); + + if bytes.len() < 32 { + return Err(anyhow!(SchnorrSignatureError::SerializationError)) + .with_context(|| "Not enough bytes to convert to a jubjub scalar"); + } + + Ok(JubjubScalar::from_raw([ + u64::from_le_bytes( + bytes[0..8] + .try_into() + .map_err(|_| anyhow!(SchnorrSignatureError::SerializationError)) + .with_context(|| "Failed to convert bls scalar to jubjub scalar")?, + ), + u64::from_le_bytes( + bytes[8..16] + .try_into() + .map_err(|_| anyhow!(SchnorrSignatureError::SerializationError)) + .with_context(|| "Failed to convert bls scalar to jubjub scalar")?, + ), + u64::from_le_bytes( + bytes[16..24] + .try_into() + .map_err(|_| anyhow!(SchnorrSignatureError::SerializationError)) + .with_context(|| "Failed to convert bls scalar to jubjub scalar")?, + ), + u64::from_le_bytes( + bytes[24..32] + .try_into() + .map_err(|_| anyhow!(SchnorrSignatureError::SerializationError)) + .with_context(|| "Failed to convert bls scalar to jubjub scalar")?, + ), + ])) +} diff --git a/mithril-stm/src/schnorr_signature/verification_key.rs b/mithril-stm/src/schnorr_signature/verification_key.rs new file mode 100644 index 00000000000..071a0dc5fa4 --- /dev/null +++ b/mithril-stm/src/schnorr_signature/verification_key.rs @@ -0,0 +1,47 @@ +use anyhow::anyhow; +use dusk_jubjub::SubgroupPoint as JubjubSubgroup; +use group::{Group, GroupEncoding}; + +pub(crate) use crate::schnorr_signature::signing_key::SchnorrSigningKey; +use crate::{StmResult, error::SchnorrSignatureError}; + +/// 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); + +impl SchnorrVerificationKey { + /// Convert a `SchnorrVerificationKey` into a string of bytes. + pub(crate) fn to_bytes(self) -> [u8; 32] { + self.0.to_bytes() + } + + /// Convert a string of bytes into a `SchnorrVerificationKey`. + /// + /// The bytes must represent a Jubjub Subgroup point or the conversion will fail + pub(crate) fn from_bytes(bytes: &[u8]) -> StmResult { + let mut verification_key_bytes: [u8; 32] = [0u8; 32]; + verification_key_bytes.copy_from_slice( + bytes + .get(0..32) + .ok_or(anyhow!(SchnorrSignatureError::SerializationError))?, + ); + let point = JubjubSubgroup::from_bytes(&verification_key_bytes) + .into_option() + .ok_or(anyhow!(SchnorrSignatureError::SerializationError))?; + + Ok(SchnorrVerificationKey(point)) + } +} + +impl From<&SchnorrSigningKey> for SchnorrVerificationKey { + /// Convert a Shnorr 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(); + + SchnorrVerificationKey(generator * signing_key.0) + } +}