diff --git a/crypto/dkg/src/lib.rs b/crypto/dkg/src/lib.rs index ce0ce400..4c63104f 100644 --- a/crypto/dkg/src/lib.rs +++ b/crypto/dkg/src/lib.rs @@ -365,6 +365,7 @@ impl ThresholdKeys { /// Offset the keys by a given scalar to allow for account and privacy schemes. /// This offset is ephemeral and will not be included when these keys are serialized. /// Keys offset multiple times will form a new offset of their sum. + #[must_use] pub fn offset(&self, offset: C::F) -> ThresholdKeys { let mut res = self.clone(); // Carry any existing offset diff --git a/crypto/frost/src/algorithm.rs b/crypto/frost/src/algorithm.rs index f676879b..ca6108dc 100644 --- a/crypto/frost/src/algorithm.rs +++ b/crypto/frost/src/algorithm.rs @@ -6,7 +6,7 @@ use rand_core::{RngCore, CryptoRng}; use transcript::Transcript; -use crate::{Curve, Participant, FrostError, ThresholdKeys, ThresholdView}; +use crate::{Participant, ThresholdKeys, ThresholdView, Curve, FrostError}; pub use schnorr::SchnorrSignature; /// Write an addendum to a writer. diff --git a/crypto/frost/src/tests/mod.rs b/crypto/frost/src/tests/mod.rs index 690be9d5..a0d2e8fe 100644 --- a/crypto/frost/src/tests/mod.rs +++ b/crypto/frost/src/tests/mod.rs @@ -5,11 +5,15 @@ use rand_core::{RngCore, CryptoRng}; pub use dkg::tests::{key_gen, recover_key}; use crate::{ - Curve, Participant, ThresholdKeys, - algorithm::Algorithm, + Curve, Participant, ThresholdKeys, FrostError, + algorithm::{Algorithm, Hram, Schnorr}, sign::{Writable, PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine}, }; +/// Tests for the nonce handling code. +pub mod nonces; +use nonces::{test_multi_nonce, test_invalid_commitment, test_invalid_dleq_proof}; + /// Vectorized test suite to ensure consistency. pub mod vectors; @@ -62,9 +66,8 @@ pub fn algorithm_machines>( .collect() } -// Run the commit step and generate signature shares -#[allow(clippy::type_complexity)] -pub(crate) fn commit_and_shares< +// Run the preprocess step +pub(crate) fn preprocess< R: RngCore + CryptoRng, M: PreprocessMachine, F: FnMut(&mut R, &mut HashMap), @@ -72,11 +75,7 @@ pub(crate) fn commit_and_shares< rng: &mut R, mut machines: HashMap, mut cache: F, - msg: &[u8], -) -> ( - HashMap>::SignatureMachine>, - HashMap>::SignatureShare>, -) { +) -> (HashMap, HashMap) { let mut commitments = HashMap::new(); let mut machines = machines .drain() @@ -93,6 +92,26 @@ pub(crate) fn commit_and_shares< cache(rng, &mut machines); + (machines, commitments) +} + +// Run the preprocess and generate signature shares +#[allow(clippy::type_complexity)] +pub(crate) fn preprocess_and_shares< + R: RngCore + CryptoRng, + M: PreprocessMachine, + F: FnMut(&mut R, &mut HashMap), +>( + rng: &mut R, + machines: HashMap, + cache: F, + msg: &[u8], +) -> ( + HashMap>::SignatureMachine>, + HashMap>::SignatureShare>, +) { + let (mut machines, commitments) = preprocess(rng, machines, cache); + let mut shares = HashMap::new(); let machines = machines .drain() @@ -120,7 +139,7 @@ fn sign_internal< cache: F, msg: &[u8], ) -> M::Signature { - let (mut machines, shares) = commit_and_shares(rng, machines, cache, msg); + let (mut machines, shares) = preprocess_and_shares(rng, machines, cache, msg); let mut signature = None; for (i, machine) in machines.drain() { @@ -172,3 +191,67 @@ pub fn sign( msg, ) } + +/// Test a basic Schnorr signature. +pub fn test_schnorr>(rng: &mut R) { + const MSG: &[u8] = b"Hello, World!"; + + let keys = key_gen(&mut *rng); + let machines = algorithm_machines(&mut *rng, Schnorr::::new(), &keys); + let sig = sign(&mut *rng, Schnorr::::new(), keys.clone(), machines, MSG); + let group_key = keys[&Participant::new(1).unwrap()].group_key(); + assert!(sig.verify(group_key, H::hram(&sig.R, &group_key, MSG))); +} + +// Test an offset Schnorr signature. +pub fn test_offset_schnorr>(rng: &mut R) { + const MSG: &[u8] = b"Hello, World!"; + + let mut keys = key_gen(&mut *rng); + let group_key = keys[&Participant::new(1).unwrap()].group_key(); + + let offset = C::F::from(5); + let offset_key = group_key + (C::generator() * offset); + for (_, keys) in keys.iter_mut() { + *keys = keys.offset(offset); + assert_eq!(keys.group_key(), offset_key); + } + + let machines = algorithm_machines(&mut *rng, Schnorr::::new(), &keys); + let sig = sign(&mut *rng, Schnorr::::new(), keys.clone(), machines, MSG); + let group_key = keys[&Participant::new(1).unwrap()].group_key(); + assert!(sig.verify(offset_key, H::hram(&sig.R, &group_key, MSG))); +} + +// Test blame for an invalid Schnorr signature share. +pub fn test_schnorr_blame>(rng: &mut R) { + const MSG: &[u8] = b"Hello, World!"; + + let keys = key_gen(&mut *rng); + let machines = algorithm_machines(&mut *rng, Schnorr::::new(), &keys); + + let (mut machines, shares) = preprocess_and_shares(&mut *rng, machines, |_, _| {}, MSG); + + for (i, machine) in machines.drain() { + let mut shares = clone_without(&shares, &i); + + // Select a random participant to give an invalid share + let participants = shares.keys().collect::>(); + let faulty = *participants + [usize::try_from(rng.next_u64() % u64::try_from(participants.len()).unwrap()).unwrap()]; + shares.get_mut(&faulty).unwrap().invalidate(); + + assert_eq!(machine.complete(shares).err(), Some(FrostError::InvalidShare(faulty))); + } +} + +// Run a variety of tests against a ciphersuite. +pub fn test_ciphersuite>(rng: &mut R) { + test_schnorr::(rng); + test_offset_schnorr::(rng); + test_schnorr_blame::(rng); + + test_multi_nonce::(rng); + test_invalid_commitment::(rng); + test_invalid_dleq_proof::(rng); +} diff --git a/crypto/frost/src/tests/nonces.rs b/crypto/frost/src/tests/nonces.rs new file mode 100644 index 00000000..134d4a28 --- /dev/null +++ b/crypto/frost/src/tests/nonces.rs @@ -0,0 +1,236 @@ +use std::io::{self, Read}; + +use zeroize::Zeroizing; + +use rand_core::{RngCore, CryptoRng, SeedableRng}; +use rand_chacha::ChaCha20Rng; + +use transcript::{Transcript, RecommendedTranscript}; + +use group::{ff::Field, Group, GroupEncoding}; + +use dleq::MultiDLEqProof; +pub use dkg::tests::{key_gen, recover_key}; + +use crate::{ + Curve, Participant, ThresholdView, ThresholdKeys, FrostError, + algorithm::Algorithm, + sign::{Writable, SignMachine}, + tests::{algorithm_machines, preprocess, sign}, +}; + +#[derive(Clone)] +struct MultiNonce { + transcript: RecommendedTranscript, + nonces: Option>>, +} + +impl MultiNonce { + fn new() -> MultiNonce { + MultiNonce { + transcript: RecommendedTranscript::new(b"FROST MultiNonce Algorithm Test"), + nonces: None, + } + } +} + +fn nonces() -> Vec> { + vec![ + vec![C::generator(), C::generator().double()], + vec![C::generator(), C::generator() * C::F::from(3), C::generator() * C::F::from(4)], + ] +} + +fn verify_nonces(nonces: &[Vec]) { + assert_eq!(nonces.len(), 2); + + // Each nonce should be a series of commitments, over some generators, which share a discrete log + // Since they share a discrete log, their only distinction should be the generator + // Above, the generators were created with a known relationship + // Accordingly, we can check here that relationship holds to make sure these commitments are well + // formed + assert_eq!(nonces[0].len(), 2); + assert_eq!(nonces[0][0].double(), nonces[0][1]); + + assert_eq!(nonces[1].len(), 3); + assert_eq!(nonces[1][0] * C::F::from(3), nonces[1][1]); + assert_eq!(nonces[1][0] * C::F::from(4), nonces[1][2]); + + assert!(nonces[0][0] != nonces[1][0]); +} + +impl Algorithm for MultiNonce { + type Transcript = RecommendedTranscript; + type Addendum = (); + type Signature = (); + + fn transcript(&mut self) -> &mut Self::Transcript { + &mut self.transcript + } + + fn nonces(&self) -> Vec> { + nonces::() + } + + fn preprocess_addendum(&mut self, _: &mut R, _: &ThresholdKeys) {} + + fn read_addendum(&self, _: &mut R) -> io::Result { + Ok(()) + } + + fn process_addendum( + &mut self, + _: &ThresholdView, + _: Participant, + _: (), + ) -> Result<(), FrostError> { + Ok(()) + } + + fn sign_share( + &mut self, + _: &ThresholdView, + nonce_sums: &[Vec], + nonces: Vec>, + _: &[u8], + ) -> C::F { + // Verify the nonce sums are as expected + verify_nonces::(nonce_sums); + + // Verify we actually have two nonces and that they're distinct + assert_eq!(nonces.len(), 2); + assert!(nonces[0] != nonces[1]); + + // Save the nonce sums for later so we can check they're consistent with the call to verify + assert!(self.nonces.is_none()); + self.nonces = Some(nonce_sums.to_vec()); + + // Sum the nonces so we can later check they actually have a relationship to nonce_sums + let mut res = C::F::zero(); + + // Weight each nonce + // This is probably overkill, since their unweighted forms would practically still require + // some level of crafting to pass a naive sum via malleability, yet this makes it more robust + for nonce in nonce_sums { + self.transcript.domain_separate(b"nonce"); + for commitment in nonce { + self.transcript.append_message(b"commitment", commitment.to_bytes()); + } + } + let mut rng = ChaCha20Rng::from_seed(self.transcript.clone().rng_seed(b"weight")); + + for nonce in nonces { + res += *nonce * C::F::random(&mut rng); + } + res + } + + #[must_use] + fn verify(&self, _: C::G, nonces: &[Vec], sum: C::F) -> Option { + verify_nonces::(nonces); + assert_eq!(&self.nonces.clone().unwrap(), nonces); + + // Make sure the nonce sums actually relate to the nonces + let mut res = C::G::identity(); + let mut rng = ChaCha20Rng::from_seed(self.transcript.clone().rng_seed(b"weight")); + for nonce in nonces { + res += nonce[0] * C::F::random(&mut rng); + } + assert_eq!(res, C::generator() * sum); + + Some(()) + } + + fn verify_share(&self, _: C::G, _: &[Vec], _: C::F) -> Result, ()> { + panic!("share verification triggered"); + } +} + +/// Test a multi-nonce, multi-generator algorithm. +// Specifically verifies this library can: +// 1) Generate multiple nonces +// 2) Provide the group nonces (nonce_sums) across multiple generators, still with the same +// discrete log +// 3) Provide algorithms with nonces which match the group nonces +pub fn test_multi_nonce(rng: &mut R) { + let keys = key_gen::(&mut *rng); + let machines = algorithm_machines(&mut *rng, MultiNonce::::new(), &keys); + sign(&mut *rng, MultiNonce::::new(), keys.clone(), machines, &[]); +} + +/// Test malleating a commitment for a nonce across generators causes the preprocess to error. +pub fn test_invalid_commitment(rng: &mut R) { + let keys = key_gen::(&mut *rng); + let machines = algorithm_machines(&mut *rng, MultiNonce::::new(), &keys); + let (machines, mut preprocesses) = preprocess(&mut *rng, machines, |_, _| {}); + + // Select a random participant to give an invalid commitment + let participants = preprocesses.keys().collect::>(); + let faulty = *participants + [usize::try_from(rng.next_u64() % u64::try_from(participants.len()).unwrap()).unwrap()]; + + // Grab their preprocess + let mut preprocess = preprocesses.remove(&faulty).unwrap(); + + // Mutate one of the commitments + let nonce = + preprocess.commitments.nonces.get_mut(usize::try_from(rng.next_u64()).unwrap() % 2).unwrap(); + let generators_len = nonce.generators.len(); + *nonce + .generators + .get_mut(usize::try_from(rng.next_u64()).unwrap() % generators_len) + .unwrap() + .0 + .get_mut(usize::try_from(rng.next_u64()).unwrap() % 2) + .unwrap() = C::G::random(&mut *rng); + + // The commitments are validated at time of deserialization (read_preprocess) + // Accordingly, serialize it and read it again to make sure that errors + assert!(machines + .iter() + .next() + .unwrap() + .1 + .read_preprocess::<&[u8]>(&mut preprocess.serialize().as_ref()) + .is_err()); +} + +/// Test malleating the DLEq proof for a preprocess causes it to error. +pub fn test_invalid_dleq_proof(rng: &mut R) { + let keys = key_gen::(&mut *rng); + let machines = algorithm_machines(&mut *rng, MultiNonce::::new(), &keys); + let (machines, mut preprocesses) = preprocess(&mut *rng, machines, |_, _| {}); + + // Select a random participant to give an invalid DLEq proof + let participants = preprocesses.keys().collect::>(); + let faulty = *participants + [usize::try_from(rng.next_u64() % u64::try_from(participants.len()).unwrap()).unwrap()]; + + // Invalidate it by replacing it with a completely different proof + let dlogs = [Zeroizing::new(C::F::random(&mut *rng)), Zeroizing::new(C::F::random(&mut *rng))]; + let mut preprocess = preprocesses.remove(&faulty).unwrap(); + preprocess.commitments.dleq = Some(MultiDLEqProof::prove( + &mut *rng, + &mut RecommendedTranscript::new(b"Invalid DLEq Proof"), + &nonces::(), + &dlogs, + )); + + assert!(machines + .iter() + .next() + .unwrap() + .1 + .read_preprocess::<&[u8]>(&mut preprocess.serialize().as_ref()) + .is_err()); + + // Also test None for a proof will cause an error + preprocess.commitments.dleq = None; + assert!(machines + .iter() + .next() + .unwrap() + .1 + .read_preprocess::<&[u8]>(&mut preprocess.serialize().as_ref()) + .is_err()); +} diff --git a/crypto/frost/src/tests/vectors.rs b/crypto/frost/src/tests/vectors.rs index 8441d364..c7aaf0ac 100644 --- a/crypto/frost/src/tests/vectors.rs +++ b/crypto/frost/src/tests/vectors.rs @@ -11,17 +11,15 @@ use rand_chacha::ChaCha20Rng; use group::{ff::PrimeField, GroupEncoding}; -use dkg::tests::key_gen; - use crate::{ curve::Curve, - Participant, ThresholdCore, ThresholdKeys, FrostError, - algorithm::{IetfTranscript, Schnorr, Hram}, + Participant, ThresholdCore, ThresholdKeys, + algorithm::{IetfTranscript, Hram, Schnorr}, sign::{ Writable, Nonce, GeneratorCommitments, NonceCommitments, Commitments, Preprocess, PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine, }, - tests::{clone_without, recover_key, algorithm_machines, commit_and_shares, sign}, + tests::{clone_without, recover_key, test_ciphersuite}, }; pub struct Vectors { @@ -147,36 +145,7 @@ pub fn test_with_vectors>( rng: &mut R, vectors: Vectors, ) { - // Test a basic Schnorr signature - { - let keys = key_gen(&mut *rng); - let machines = algorithm_machines(&mut *rng, Schnorr::::new(), &keys); - const MSG: &[u8] = b"Hello, World!"; - let sig = sign(&mut *rng, Schnorr::::new(), keys.clone(), machines, MSG); - let group_key = keys[&Participant::new(1).unwrap()].group_key(); - assert!(sig.verify(group_key, H::hram(&sig.R, &group_key, MSG))); - } - - // Test blame on an invalid Schnorr signature share - { - let keys = key_gen(&mut *rng); - let machines = algorithm_machines(&mut *rng, Schnorr::::new(), &keys); - const MSG: &[u8] = b"Hello, World!"; - - let (mut machines, mut shares) = commit_and_shares(&mut *rng, machines, |_, _| {}, MSG); - let faulty = *shares.keys().next().unwrap(); - shares.get_mut(&faulty).unwrap().invalidate(); - - for (i, machine) in machines.drain() { - if i == faulty { - continue; - } - assert_eq!( - machine.complete(clone_without(&shares, &i)).err(), - Some(FrostError::InvalidShare(faulty)) - ); - } - } + test_ciphersuite::(rng); // Test against the vectors let keys = vectors_to_multisig_keys::(&vectors);