diff --git a/frost-core/Cargo.toml b/frost-core/Cargo.toml index 709ce9b0..e0df64bd 100644 --- a/frost-core/Cargo.toml +++ b/frost-core/Cargo.toml @@ -19,6 +19,7 @@ features = ["nightly"] [dependencies] byteorder = "1.4" +debugless-unwrap = "0.0.4" digest = "0.10" hex = { version = "0.4.3", features = ["serde"] } rand_core = "0.6" @@ -26,6 +27,11 @@ serde = { version = "1", optional = true, features = ["derive"] } thiserror = "1.0" zeroize = { version = "1.5.4", default-features = false, features = ["derive"] } +# Test dependencies used with the test-impl feature +proptest = { version = "1.0", optional = true } +proptest-derive = { version = "0.3", optional = true } +serde_json = { version = "1.0", optional = true } + [dev-dependencies] curve25519-dalek = { version = "4.0.0-pre.1", features = ["serde"] } lazy_static = "1.4" @@ -38,3 +44,5 @@ sha2 = "0.10.2" [features] nightly = [] default = ["serde"] +# Exposes ciphersuite-generic tests for other crates to use +test-impl = ["proptest", "proptest-derive", "serde_json"] diff --git a/frost-core/src/lib.rs b/frost-core/src/lib.rs index 97df7d91..3c665b31 100644 --- a/frost-core/src/lib.rs +++ b/frost-core/src/lib.rs @@ -19,6 +19,8 @@ pub mod frost; mod scalar_mul; mod signature; mod signing_key; +#[cfg(any(test, feature = "test-impl"))] +pub mod tests; mod verifying_key; pub use error::Error; diff --git a/frost-core/src/signature.rs b/frost-core/src/signature.rs index b1dafe00..4e3ad7f1 100644 --- a/frost-core/src/signature.rs +++ b/frost-core/src/signature.rs @@ -1,8 +1,6 @@ //! Schnorr signatures over prime order groups (or subgroups) -use std::fmt::Debug; - -// use hex::FromHex; +use debugless_unwrap::DebuglessUnwrap; use crate::{Ciphersuite, Error, Field, Group}; @@ -23,11 +21,7 @@ where ::Field: Field, { /// Converts bytes as [`C::SignatureSerialization`] into a `Signature`. - pub fn from_bytes(bytes: C::SignatureSerialization) -> Result - where - <::Serialization as TryFrom>>::Error: Debug, - <<::Field as Field>::Serialization as TryFrom>>::Error: Debug, - { + pub fn from_bytes(bytes: C::SignatureSerialization) -> Result { // To compute the expected length of the encoded point, encode the generator // and get its length. Note that we can't use the identity because it can be encoded // shorter in some cases (e.g. P-256, which uses SEC1 encoding). @@ -57,16 +51,13 @@ where } /// Converts this signature to its [`C::SignatureSerialization`] in bytes. - pub fn to_bytes(&self) -> C::SignatureSerialization - where - <::SignatureSerialization as TryFrom>>::Error: Debug, - { + pub fn to_bytes(&self) -> C::SignatureSerialization { let mut bytes = vec![]; bytes.extend(::serialize(&self.R).as_ref()); bytes.extend(<::Field as Field>::serialize(&self.z).as_ref()); - bytes.try_into().unwrap() + bytes.try_into().debugless_unwrap() } } diff --git a/frost-core/tests/frost.rs b/frost-core/src/tests.rs similarity index 76% rename from frost-core/tests/frost.rs rename to frost-core/src/tests.rs index 534f4f05..95b88aa3 100644 --- a/frost-core/tests/frost.rs +++ b/frost-core/src/tests.rs @@ -1,16 +1,35 @@ +//! Ciphersuite-generic test functions. use std::{collections::HashMap, convert::TryFrom}; -use frost_core::frost::{self, Identifier}; -use rand::thread_rng; +use crate::frost::{self, Identifier}; +use rand_core::{CryptoRng, RngCore}; -mod common; +use crate::Ciphersuite; -use common::ciphersuite::Ristretto255Sha512 as R; +pub mod proptests; +pub mod vectors; -#[test] -fn check_sign_with_dealer() { - let mut rng = thread_rng(); +/// Test share generation with a Ciphersuite +pub fn check_share_generation(mut rng: R) { + let secret = frost::keys::Secret::::random(&mut rng); + let secret_shares = frost::keys::generate_secret_shares(&secret, 5, 3, rng).unwrap(); + + for secret_share in secret_shares.iter() { + assert_eq!(secret_share.verify(), Ok(())); + } + + assert_eq!( + frost::keys::reconstruct_secret::(secret_shares).unwrap(), + secret + ) +} + +/// Test FROST signing with trusted dealer with a Ciphersuite. +pub fn check_sign_with_dealer(mut rng: R) +where + ::Group: std::cmp::PartialEq, +{ //////////////////////////////////////////////////////////////////////////// // Key generation //////////////////////////////////////////////////////////////////////////// @@ -21,7 +40,7 @@ fn check_sign_with_dealer() { frost::keys::keygen_with_dealer(numsigners, threshold, &mut rng).unwrap(); // Verifies the secret shares from the dealer - let key_packages: HashMap, frost::keys::KeyPackage> = shares + let key_packages: HashMap, frost::keys::KeyPackage> = shares .into_iter() .map(|share| { ( @@ -31,8 +50,8 @@ fn check_sign_with_dealer() { }) .collect(); - let mut nonces: HashMap, frost::round1::SigningNonces> = HashMap::new(); - let mut commitments: HashMap, frost::round1::SigningCommitments> = + let mut nonces: HashMap, frost::round1::SigningNonces> = HashMap::new(); + let mut commitments: HashMap, frost::round1::SigningCommitments> = HashMap::new(); //////////////////////////////////////////////////////////////////////////// @@ -58,7 +77,7 @@ fn check_sign_with_dealer() { // This is what the signature aggregator / coordinator needs to do: // - decide what message to sign // - take one (unused) commitment per signing participant - let mut signature_shares: Vec> = Vec::new(); + let mut signature_shares: Vec> = Vec::new(); let message = "message to sign".as_bytes(); let comms = commitments.clone().into_values().collect(); let signing_package = frost::SigningPackage::new(comms, message.to_vec()); diff --git a/frost-core/tests/proptests.rs b/frost-core/src/tests/proptests.rs similarity index 56% rename from frost-core/tests/proptests.rs rename to frost-core/src/tests/proptests.rs index fa06cd7c..77ae3033 100644 --- a/frost-core/tests/proptests.rs +++ b/frost-core/src/tests/proptests.rs @@ -1,14 +1,12 @@ -use frost_core::*; +//! Ciphersuite-generic functions for proptests + +use crate::*; use proptest::prelude::*; use rand_core::{CryptoRng, RngCore}; -mod common; - -use common::ciphersuite::Ristretto255Sha512 as R; - /// A signature test-case, containing signature data and expected validity. #[derive(Clone, Debug)] -struct SignatureCase { +pub struct SignatureCase { msg: Vec, sig: Signature, vk: VerifyingKey, @@ -18,7 +16,7 @@ struct SignatureCase { /// A modification to a test-case. #[derive(Copy, Clone, Debug)] -enum Tweak { +pub enum Tweak { /// No-op, used to check that unchanged cases verify. None, /// Change the message the signature is defined for, invalidating the signature. @@ -41,7 +39,8 @@ impl SignatureCase where C: Ciphersuite, { - fn new(mut rng: R, msg: Vec) -> Self { + /// Create a new SignatureCase. + pub fn new(mut rng: R, msg: Vec) -> Self { let sk = SigningKey::::new(&mut rng); let sig = sk.sign(&mut rng, &msg); let vk = VerifyingKey::::from(&sk); @@ -55,24 +54,25 @@ where } } - // Check that signature verification succeeds or fails, as expected. - fn check(&self) -> bool { - // // The signature data is stored in (refined) byte types, but do a round trip - // // conversion to raw bytes to exercise those code paths. - // let sig = { - // let bytes: [u8; 64] = self.sig.into(); - // Signature::::from_bytes(bytes) - // }; + /// Check that signature verification succeeds or fails, as expected. + pub fn check(&self) -> bool { + // The signature data is stored in (refined) byte types, but do a round trip + // conversion to raw bytes to exercise those code paths. + let _sig = { + let bytes = self.sig.to_bytes(); + Signature::::from_bytes(bytes) + }; - // // Check that the verification key is a valid key. - // let pub_key = VerifyingKey::::from_bytes(pk_bytes) - // .expect("The test verification key to be well-formed."); + // Check that the verification key is a valid key. + let _pub_key = VerifyingKey::::from_bytes(self.vk.to_bytes()) + .expect("The test verification key to be well-formed."); // Check that signature validation has the expected result. self.is_valid == self.vk.verify(&self.msg, &self.sig).is_ok() } - fn apply_tweak(&mut self, tweak: &Tweak) { + /// Apply the given tweak to the signature test case. + pub fn apply_tweak(&mut self, tweak: &Tweak) { match tweak { Tweak::None => {} Tweak::ChangeMessage => { @@ -89,41 +89,11 @@ where } } -fn tweak_strategy() -> impl Strategy { +/// Tweak the proptest strategy +pub fn tweak_strategy() -> impl Strategy { prop_oneof![ 10 => Just(Tweak::None), 1 => Just(Tweak::ChangeMessage), 1 => Just(Tweak::ChangePubkey), ] } - -use rand_chacha::ChaChaRng; -use rand_core::SeedableRng; - -proptest! { - - #[test] - fn tweak_signature( - tweaks in prop::collection::vec(tweak_strategy(), (0,5)), - rng_seed in prop::array::uniform32(any::()), - ) { - // Use a deterministic RNG so that test failures can be reproduced. - // Seeding with 64 bits of entropy is INSECURE and this code should - // not be copied outside of this test! - let mut rng = ChaChaRng::from_seed(rng_seed); - - // Create a test case for each signature type. - let msg = b"test message for proptests"; - let mut sig = SignatureCase::::new(&mut rng, msg.to_vec()); - - - // Apply tweaks to each case. - for t in &tweaks { - sig.apply_tweak(t); - } - - assert!(sig.check()); - } - - -} diff --git a/frost-core/src/tests/vectors.rs b/frost-core/src/tests/vectors.rs new file mode 100644 index 00000000..5a741489 --- /dev/null +++ b/frost-core/src/tests/vectors.rs @@ -0,0 +1,265 @@ +//! Helper function for testing with test vectors. +use std::{collections::HashMap, str::FromStr}; + +use debugless_unwrap::DebuglessUnwrap; +use hex::{self, FromHex}; +use serde_json::Value; + +use crate::{ + frost::{self, keys::*, round1::*, round2::*, *}, + Ciphersuite, Field, Group, VerifyingKey, +}; + +/// Parse test vectors for a given ciphersuite. +#[allow(clippy::type_complexity)] +pub fn parse_test_vectors( + json_vectors: &Value, +) -> ( + VerifyingKey, + HashMap, KeyPackage>, + Vec, + HashMap, SigningNonces>, + HashMap, SigningCommitments>, + Vec, + Rho, + HashMap, SignatureShare>, + Vec, // Signature, +) { + let inputs = &json_vectors["inputs"]; + + let message = inputs["message"].as_str().unwrap(); + let message_bytes = hex::decode(message).unwrap(); + + let mut key_packages: HashMap, KeyPackage> = HashMap::new(); + + let possible_signers = json_vectors["inputs"]["signers"] + .as_object() + .unwrap() + .iter(); + + let group_public = + VerifyingKey::::from_hex(inputs["group_public_key"].as_str().unwrap()).unwrap(); + + for (i, secret_share) in possible_signers { + let secret = Secret::::from_hex(secret_share["signer_share"].as_str().unwrap()).unwrap(); + let signer_public = secret.into(); + + let key_package = KeyPackage:: { + identifier: u16::from_str(i).unwrap().try_into().unwrap(), + secret_share: secret, + public: signer_public, + group_public, + }; + + key_packages.insert(*key_package.identifier(), key_package); + } + + // Round one outputs + + let round_one_outputs = &json_vectors["round_one_outputs"]; + + let group_binding_factor_input = Vec::::from_hex( + round_one_outputs["group_binding_factor_input"] + .as_str() + .unwrap(), + ) + .unwrap(); + + let group_binding_factor = + Rho::::from_hex(round_one_outputs["group_binding_factor"].as_str().unwrap()).unwrap(); + + let mut signer_nonces: HashMap, SigningNonces> = HashMap::new(); + let mut signer_commitments: HashMap, SigningCommitments> = HashMap::new(); + + for (i, signer) in round_one_outputs["signers"].as_object().unwrap().iter() { + let identifier = u16::from_str(i).unwrap().try_into().unwrap(); + + let signing_nonces = SigningNonces:: { + hiding: Nonce::::from_hex(signer["hiding_nonce"].as_str().unwrap()).unwrap(), + binding: Nonce::::from_hex(signer["binding_nonce"].as_str().unwrap()).unwrap(), + }; + + signer_nonces.insert(identifier, signing_nonces); + + let signing_commitments = SigningCommitments:: { + identifier, + hiding: NonceCommitment::from_hex(signer["hiding_nonce_commitment"].as_str().unwrap()) + .unwrap(), + binding: NonceCommitment::from_hex( + signer["binding_nonce_commitment"].as_str().unwrap(), + ) + .unwrap(), + }; + + signer_commitments.insert(identifier, signing_commitments); + } + + // Round two outputs + + let round_two_outputs = &json_vectors["round_two_outputs"]; + + let mut signature_shares: HashMap, SignatureShare> = HashMap::new(); + + for (i, signer) in round_two_outputs["signers"].as_object().unwrap().iter() { + let sig_share = <::Field as Field>::Serialization::try_from( + hex::decode(signer["sig_share"].as_str().unwrap()).unwrap(), + ) + .debugless_unwrap(); + + let signature_share = SignatureShare:: { + identifier: u16::from_str(i).unwrap().try_into().unwrap(), + signature: SignatureResponse { + z_share: <::Field as Field>::deserialize(&sig_share).unwrap(), + }, + }; + + signature_shares.insert( + u16::from_str(i).unwrap().try_into().unwrap(), + signature_share, + ); + } + + // Final output + + let final_output = &json_vectors["final_output"]; + + let signature_bytes = FromHex::from_hex(final_output["sig"].as_str().unwrap()).unwrap(); + + ( + group_public, + key_packages, + message_bytes, + signer_nonces, + signer_commitments, + group_binding_factor_input, + group_binding_factor, + signature_shares, + signature_bytes, + ) +} + +/// Test with the given test vectors for a ciphersuite. +pub fn check_sign_with_test_vectors(json_vectors: &Value) +where + C::Group: PartialEq, +{ + let ( + group_public, + key_packages, + message_bytes, + signer_nonces, + signer_commitments, + group_binding_factor_input, + group_binding_factor, + signature_shares, + signature_bytes, + ) = parse_test_vectors(json_vectors); + + //////////////////////////////////////////////////////////////////////////// + // Key generation + //////////////////////////////////////////////////////////////////////////// + + for key_package in key_packages.values() { + assert_eq!( + *key_package.public(), + frost::keys::Public::from(*key_package.secret_share()) + ); + } + + ///////////////////////////////////////////////////////////////////////////// + // Round 1: generating nonces and signing commitments for each participant + ///////////////////////////////////////////////////////////////////////////// + + for (i, _) in signer_commitments.clone() { + // compute nonce commitments from nonces + let nonces = signer_nonces.get(&i).unwrap(); + let nonce_commitments = signer_commitments.get(&i).unwrap(); + + assert_eq!( + &frost::round1::NonceCommitment::from(nonces.hiding()), + nonce_commitments.hiding() + ); + + assert_eq!( + &frost::round1::NonceCommitment::from(nonces.binding()), + nonce_commitments.binding() + ); + } + + ///////////////////////////////////////////////////////////////////////////// + // Round 2: each participant generates their signature share + ///////////////////////////////////////////////////////////////////////////// + + let signer_commitments_vec = signer_commitments + .into_iter() + .map(|(_, signing_commitments)| signing_commitments) + .collect(); + + let signing_package = frost::SigningPackage::new(signer_commitments_vec, message_bytes); + + assert_eq!(signing_package.rho_preimage(), group_binding_factor_input); + + let rho: frost::Rho = (&signing_package).into(); + + assert_eq!(rho, group_binding_factor); + + let mut our_signature_shares: Vec> = Vec::new(); + + // Each participant generates their signature share + for identifier in signer_nonces.keys() { + let key_package = &key_packages[identifier]; + let nonces = &signer_nonces[identifier]; + + // Each participant generates their signature share. + let signature_share = frost::round2::sign(&signing_package, nonces, key_package).unwrap(); + + our_signature_shares.push(signature_share); + } + + for sig_share in our_signature_shares.clone() { + assert_eq!(sig_share, signature_shares[sig_share.identifier()]); + } + + let signer_pubkeys = key_packages + .into_iter() + .map(|(i, key_package)| (i, *key_package.public())) + .collect(); + + let pubkey_package = frost::keys::PublicKeyPackage { + signer_pubkeys, + group_public, + }; + + //////////////////////////////////////////////////////////////////////////// + // Aggregation: collects the signing shares from all participants, + // generates the final signature. + //////////////////////////////////////////////////////////////////////////// + + // Aggregate the FROST signature from test vector sig shares + let group_signature_result = frost::aggregate( + &signing_package, + &signature_shares + .values() + .cloned() + .collect::>>(), + &pubkey_package, + ); + + // Check that the aggregation passed signature share verification and generation + assert!(group_signature_result.is_ok()); + + // Check that the generated signature matches the test vector signature + let group_signature = group_signature_result.unwrap(); + assert_eq!(group_signature.to_bytes().as_ref(), signature_bytes); + + // Aggregate the FROST signature from our signature shares + let group_signature_result = + frost::aggregate(&signing_package, &our_signature_shares, &pubkey_package); + + // Check that the aggregation passed signature share verification and generation + assert!(group_signature_result.is_ok()); + + // Check that the generated signature matches the test vector signature + let group_signature = group_signature_result.unwrap(); + assert_eq!(group_signature.to_bytes().as_ref(), signature_bytes); +} diff --git a/frost-core/tests/common/mod.rs b/frost-core/tests/common/mod.rs index c311bc7b..0482efda 100644 --- a/frost-core/tests/common/mod.rs +++ b/frost-core/tests/common/mod.rs @@ -10,4 +10,3 @@ //! pub mod ciphersuite; -pub mod vectors; diff --git a/frost-core/tests/common/vectors.rs b/frost-core/tests/common/vectors.rs deleted file mode 100644 index 77f0d09c..00000000 --- a/frost-core/tests/common/vectors.rs +++ /dev/null @@ -1,150 +0,0 @@ -use std::{collections::HashMap, str::FromStr}; - -use curve25519_dalek::scalar::Scalar; -use hex::{self, FromHex}; -use lazy_static::lazy_static; -use serde_json::Value; - -use frost_core::{ - frost::{keys::*, round1::*, round2::*, *}, - VerifyingKey, -}; - -use super::ciphersuite::Ristretto255Sha512; - -lazy_static! { - pub static ref RISTRETTO255_SHA512: Value = - serde_json::from_str(include_str!("vectors.json").trim()) - .expect("Test vector is valid JSON"); -} - -#[allow(clippy::type_complexity)] -#[allow(dead_code)] -pub(crate) fn parse_test_vectors() -> ( - VerifyingKey, - HashMap, KeyPackage>, - &'static str, - Vec, - HashMap, SigningNonces>, - HashMap, SigningCommitments>, - Vec, - Rho, - HashMap, SignatureShare>, - Vec, // Signature, -) { - type R = Ristretto255Sha512; - - let inputs = &RISTRETTO255_SHA512["inputs"]; - - let message = inputs["message"].as_str().unwrap(); - let message_bytes = hex::decode(message).unwrap(); - - let mut key_packages: HashMap, KeyPackage> = HashMap::new(); - - let possible_signers = RISTRETTO255_SHA512["inputs"]["signers"] - .as_object() - .unwrap() - .iter(); - - let group_public = - VerifyingKey::::from_hex(inputs["group_public_key"].as_str().unwrap()).unwrap(); - - for (i, secret_share) in possible_signers { - let secret = Secret::::from_hex(secret_share["signer_share"].as_str().unwrap()).unwrap(); - let signer_public = secret.into(); - - let key_package = KeyPackage:: { - identifier: u16::from_str(i).unwrap().try_into().unwrap(), - secret_share: secret, - public: signer_public, - group_public, - }; - - key_packages.insert(*key_package.identifier(), key_package); - } - - // Round one outputs - - let round_one_outputs = &RISTRETTO255_SHA512["round_one_outputs"]; - - let group_binding_factor_input = Vec::::from_hex( - round_one_outputs["group_binding_factor_input"] - .as_str() - .unwrap(), - ) - .unwrap(); - - let group_binding_factor = - Rho::::from_hex(round_one_outputs["group_binding_factor"].as_str().unwrap()).unwrap(); - - let mut signer_nonces: HashMap, SigningNonces> = HashMap::new(); - let mut signer_commitments: HashMap, SigningCommitments> = HashMap::new(); - - for (i, signer) in round_one_outputs["signers"].as_object().unwrap().iter() { - let identifier = u16::from_str(i).unwrap().try_into().unwrap(); - - let signing_nonces = SigningNonces:: { - hiding: Nonce::::from_hex(signer["hiding_nonce"].as_str().unwrap()).unwrap(), - binding: Nonce::::from_hex(signer["binding_nonce"].as_str().unwrap()).unwrap(), - }; - - signer_nonces.insert(identifier, signing_nonces); - - let signing_commitments = SigningCommitments:: { - identifier, - hiding: NonceCommitment::from_hex(signer["hiding_nonce_commitment"].as_str().unwrap()) - .unwrap(), - binding: NonceCommitment::from_hex( - signer["binding_nonce_commitment"].as_str().unwrap(), - ) - .unwrap(), - }; - - signer_commitments.insert(identifier, signing_commitments); - } - - // Round two outputs - - let round_two_outputs = &RISTRETTO255_SHA512["round_two_outputs"]; - - let mut signature_shares: HashMap, SignatureShare> = HashMap::new(); - - for (i, signer) in round_two_outputs["signers"].as_object().unwrap().iter() { - let signature_share = SignatureShare:: { - identifier: u16::from_str(i).unwrap().try_into().unwrap(), - signature: SignatureResponse { - z_share: Scalar::from_canonical_bytes( - hex::decode(signer["sig_share"].as_str().unwrap()) - .unwrap() - .try_into() - .unwrap(), - ) - .unwrap(), - }, - }; - - signature_shares.insert( - u16::from_str(i).unwrap().try_into().unwrap(), - signature_share, - ); - } - - // Final output - - let final_output = &RISTRETTO255_SHA512["final_output"]; - - let signature_bytes = FromHex::from_hex(final_output["sig"].as_str().unwrap()).unwrap(); - - ( - group_public, - key_packages, - message, - message_bytes, - signer_nonces, - signer_commitments, - group_binding_factor_input, - group_binding_factor, - signature_shares, - signature_bytes, - ) -} diff --git a/frost-core/tests/tests.rs b/frost-core/tests/tests.rs index e0dca721..822a584a 100644 --- a/frost-core/tests/tests.rs +++ b/frost-core/tests/tests.rs @@ -1,154 +1,30 @@ -use frost_core::frost::{self}; +use lazy_static::lazy_static; use rand::thread_rng; +use serde_json::Value; mod common; -use common::{ciphersuite::*, vectors::*}; +use common::ciphersuite::*; + +lazy_static! { + pub static ref RISTRETTO255_SHA512: Value = + serde_json::from_str(include_str!("common/vectors.json").trim()) + .expect("Test vector is valid JSON"); +} /// This is testing that Shamir's secret sharing to compute and arbitrary /// value is working. #[test] fn check_share_generation_ristretto255_sha512() { - let mut rng = thread_rng(); - - let secret = frost::keys::Secret::::random(&mut rng); - - let secret_shares = frost::keys::generate_secret_shares(&secret, 5, 3, rng).unwrap(); - - for secret_share in secret_shares.iter() { - assert_eq!(secret_share.verify(), Ok(())); - } - - assert_eq!( - frost::keys::reconstruct_secret::(secret_shares).unwrap(), - secret - ) + let rng = thread_rng(); + frost_core::tests::check_share_generation::(rng); } #[test] fn check_sign_with_test_vectors() { - let ( - group_public, - key_packages, - _message, - message_bytes, - signer_nonces, - signer_commitments, - group_binding_factor_input, - group_binding_factor, - signature_shares, - signature_bytes, - ) = parse_test_vectors(); - - type R = Ristretto255Sha512; - - //////////////////////////////////////////////////////////////////////////// - // Key generation - //////////////////////////////////////////////////////////////////////////// - - for key_package in key_packages.values() { - assert_eq!( - *key_package.public(), - frost::keys::Public::from(*key_package.secret_share()) - ); - } - - ///////////////////////////////////////////////////////////////////////////// - // Round 1: generating nonces and signing commitments for each participant - ///////////////////////////////////////////////////////////////////////////// - - for (i, _) in signer_commitments.clone() { - // compute nonce commitments from nonces - let nonces = signer_nonces.get(&i).unwrap(); - let nonce_commitments = signer_commitments.get(&i).unwrap(); - - assert_eq!( - &frost::round1::NonceCommitment::from(nonces.hiding()), - nonce_commitments.hiding() - ); - - assert_eq!( - &frost::round1::NonceCommitment::from(nonces.binding()), - nonce_commitments.binding() - ); - } - - ///////////////////////////////////////////////////////////////////////////// - // Round 2: each participant generates their signature share - ///////////////////////////////////////////////////////////////////////////// - - let signer_commitments_vec = signer_commitments - .into_iter() - .map(|(_, signing_commitments)| signing_commitments) - .collect(); - - let signing_package = frost::SigningPackage::new(signer_commitments_vec, message_bytes); - - assert_eq!(signing_package.rho_preimage(), group_binding_factor_input); - - let rho: frost::Rho = (&signing_package).into(); - - assert_eq!(rho, group_binding_factor); - - let mut our_signature_shares: Vec> = Vec::new(); - - // Each participant generates their signature share - for identifier in signer_nonces.keys() { - let key_package = &key_packages[identifier]; - let nonces = &signer_nonces[identifier]; - - // Each participant generates their signature share. - let signature_share = frost::round2::sign(&signing_package, nonces, key_package).unwrap(); - - our_signature_shares.push(signature_share); - } - - for sig_share in our_signature_shares.clone() { - assert_eq!(sig_share, signature_shares[sig_share.identifier()]); - } - - let signer_pubkeys = key_packages - .into_iter() - .map(|(i, key_package)| (i, *key_package.public())) - .collect(); - - let pubkey_package = frost::keys::PublicKeyPackage { - signer_pubkeys, - group_public, - }; - - //////////////////////////////////////////////////////////////////////////// - // Aggregation: collects the signing shares from all participants, - // generates the final signature. - //////////////////////////////////////////////////////////////////////////// - - // Aggregate the FROST signature from test vector sig shares - let group_signature_result = frost::aggregate( - &signing_package, - &signature_shares - .values() - .cloned() - .collect::>>(), - &pubkey_package, - ); - - // Check that the aggregation passed signature share verification and generation - assert!(group_signature_result.is_ok()); - - // Check that the generated signature matches the test vector signature - let group_signature = group_signature_result.unwrap(); - assert_eq!(group_signature.to_bytes().to_vec(), signature_bytes); - - // Aggregate the FROST signature from our signature shares - let group_signature_result = - frost::aggregate(&signing_package, &our_signature_shares, &pubkey_package); - - // Check that the aggregation passed signature share verification and generation - assert!(group_signature_result.is_ok()); - - // Check that the generated signature matches the test vector signature - let group_signature = group_signature_result.unwrap(); - assert_eq!(group_signature.to_bytes().to_vec(), signature_bytes); + frost_core::tests::vectors::check_sign_with_test_vectors::( + &RISTRETTO255_SHA512, + ) } // This allows checking that to_scalar() works for all possible inputs; diff --git a/frost-p256/Cargo.toml b/frost-p256/Cargo.toml index 1985b3c0..f56bfafc 100644 --- a/frost-p256/Cargo.toml +++ b/frost-p256/Cargo.toml @@ -21,7 +21,7 @@ features = ["nightly"] byteorder = "1.4" p256 = { version = "0.11.1", features = ["hash2curve"] } digest = "0.10" -frost-core = { path = "../frost-core" } +frost-core = { path = "../frost-core", features = ["test-impl"] } hex = { version = "0.4.3", features = ["serde"] } rand_core = "0.6" serde = { version = "1", optional = true, features = ["derive"] } diff --git a/frost-p256/src/tests.rs b/frost-p256/src/tests.rs index cab6c8fa..3bcbaeec 100644 --- a/frost-p256/src/tests.rs +++ b/frost-p256/src/tests.rs @@ -1,153 +1,24 @@ +use lazy_static::lazy_static; use rand::thread_rng; +use serde_json::Value; use crate::*; -mod vectors; - -use vectors::*; +lazy_static! { + pub static ref P256_SHA256: Value = + serde_json::from_str(include_str!("tests/vectors.json").trim()) + .expect("Test vector is valid JSON"); +} /// This is testing that Shamir's secret sharing to compute and arbitrary /// value is working. #[test] fn check_share_generation_p256_sha256() { - let mut rng = thread_rng(); - - let secret = frost::keys::Secret::::random(&mut rng); - - let secret_shares = frost::keys::generate_secret_shares(&secret, 5, 3, rng).unwrap(); - - for secret_share in secret_shares.iter() { - assert_eq!(secret_share.verify(), Ok(())); - } - - assert_eq!( - frost::keys::reconstruct_secret::(secret_shares).unwrap(), - secret - ) + let rng = thread_rng(); + frost_core::tests::check_share_generation::(rng); } #[test] fn check_sign_with_test_vectors() { - let ( - group_public, - key_packages, - _message, - message_bytes, - signer_nonces, - signer_commitments, - group_binding_factor_input, - group_binding_factor, - signature_shares, - signature_bytes, - ) = parse_test_vectors(); - - type R = P256Sha256; - - //////////////////////////////////////////////////////////////////////////// - // Key generation - //////////////////////////////////////////////////////////////////////////// - - for key_package in key_packages.values() { - assert_eq!( - *key_package.public(), - frost::keys::Public::from(*key_package.secret_share()) - ); - } - - ///////////////////////////////////////////////////////////////////////////// - // Round 1: generating nonces and signing commitments for each participant - ///////////////////////////////////////////////////////////////////////////// - - for (i, _) in signer_commitments.clone() { - // compute nonce commitments from nonces - let nonces = signer_nonces.get(&i).unwrap(); - let nonce_commitments = signer_commitments.get(&i).unwrap(); - - assert_eq!( - &frost::round1::NonceCommitment::from(nonces.hiding()), - nonce_commitments.hiding() - ); - - assert_eq!( - &frost::round1::NonceCommitment::from(nonces.binding()), - nonce_commitments.binding() - ); - } - - ///////////////////////////////////////////////////////////////////////////// - // Round 2: each participant generates their signature share - ///////////////////////////////////////////////////////////////////////////// - - let signer_commitments_vec = signer_commitments - .into_iter() - .map(|(_, signing_commitments)| signing_commitments) - .collect(); - - let signing_package = frost::SigningPackage::new(signer_commitments_vec, message_bytes); - - assert_eq!(signing_package.rho_preimage(), group_binding_factor_input); - - let rho: frost::Rho = (&signing_package).into(); - - assert_eq!(rho, group_binding_factor); - - let mut our_signature_shares: Vec> = Vec::new(); - - // Each participant generates their signature share - for identifier in signer_nonces.keys() { - let key_package = &key_packages[identifier]; - let nonces = &signer_nonces[identifier]; - - // Each participant generates their signature share. - let signature_share = frost::round2::sign(&signing_package, nonces, key_package).unwrap(); - - our_signature_shares.push(signature_share); - } - - for sig_share in our_signature_shares.clone() { - assert_eq!(sig_share, signature_shares[sig_share.identifier()]); - } - - let signer_pubkeys = key_packages - .into_iter() - .map(|(i, key_package)| (i, *key_package.public())) - .collect(); - - let pubkey_package = frost::keys::PublicKeyPackage { - signer_pubkeys, - group_public, - }; - - //////////////////////////////////////////////////////////////////////////// - // Aggregation: collects the signing shares from all participants, - // generates the final signature. - //////////////////////////////////////////////////////////////////////////// - - // Aggregate the FROST signature from test vector sig shares - let group_signature_result = frost::aggregate( - &signing_package, - &signature_shares - .values() - .cloned() - .collect::>>(), - &pubkey_package, - ); - - // Check that the aggregation passed signature share verification and generation - assert!(group_signature_result.is_ok()); - - // Check that the generated signature matches the test vector signature - let group_signature = group_signature_result.unwrap(); - assert_eq!(group_signature.to_bytes().to_vec(), signature_bytes); - - // Aggregate the FROST signature from our signature shares - let group_signature_result = - frost::aggregate(&signing_package, &our_signature_shares, &pubkey_package); - - // Check that the aggregation passed signature share verification and generation - assert!(group_signature_result.is_ok()); - - // Check that the generated signature matches the test vector signature - let group_signature = group_signature_result.unwrap(); - assert_eq!(group_signature.to_bytes().to_vec(), signature_bytes); + frost_core::tests::vectors::check_sign_with_test_vectors::(&P256_SHA256) } diff --git a/frost-p256/src/tests/vectors.rs b/frost-p256/src/tests/vectors.rs deleted file mode 100644 index 755f9fdd..00000000 --- a/frost-p256/src/tests/vectors.rs +++ /dev/null @@ -1,146 +0,0 @@ -use std::{collections::HashMap, str::FromStr}; - -use hex::{self, FromHex}; -use lazy_static::lazy_static; -use p256::elliptic_curve::PrimeField; -use p256::{FieldBytes, Scalar}; -use serde_json::Value; - -use frost_core::{ - frost::{keys::*, round1::*, round2::*, *}, - VerifyingKey, -}; - -use crate::P256Sha256; - -lazy_static! { - pub static ref P256_SHA256: Value = serde_json::from_str(include_str!("vectors.json").trim()) - .expect("Test vector is valid JSON"); -} - -#[allow(clippy::type_complexity)] -#[allow(dead_code)] -pub(crate) fn parse_test_vectors() -> ( - VerifyingKey, - HashMap, KeyPackage>, - &'static str, - Vec, - HashMap, SigningNonces>, - HashMap, SigningCommitments>, - Vec, - Rho, - HashMap, SignatureShare>, - Vec, // Signature, -) { - type R = P256Sha256; - - let inputs = &P256_SHA256["inputs"]; - - let message = inputs["message"].as_str().unwrap(); - let message_bytes = hex::decode(message).unwrap(); - - let mut key_packages: HashMap, KeyPackage> = HashMap::new(); - - let possible_signers = P256_SHA256["inputs"]["signers"].as_object().unwrap().iter(); - - let group_public = - VerifyingKey::::from_hex(inputs["group_public_key"].as_str().unwrap()).unwrap(); - - for (i, secret_share) in possible_signers { - let secret = Secret::::from_hex(secret_share["signer_share"].as_str().unwrap()).unwrap(); - let signer_public = secret.into(); - - let key_package = KeyPackage:: { - identifier: u16::from_str(i).unwrap().try_into().unwrap(), - secret_share: secret, - public: signer_public, - group_public, - }; - - key_packages.insert(*key_package.identifier(), key_package); - } - - // Round one outputs - - let round_one_outputs = &P256_SHA256["round_one_outputs"]; - - let group_binding_factor_input = Vec::::from_hex( - round_one_outputs["group_binding_factor_input"] - .as_str() - .unwrap(), - ) - .unwrap(); - - let group_binding_factor = - Rho::::from_hex(round_one_outputs["group_binding_factor"].as_str().unwrap()).unwrap(); - - let mut signer_nonces: HashMap, SigningNonces> = HashMap::new(); - let mut signer_commitments: HashMap, SigningCommitments> = HashMap::new(); - - for (i, signer) in round_one_outputs["signers"].as_object().unwrap().iter() { - let identifier = u16::from_str(i).unwrap().try_into().unwrap(); - - let signing_nonces = SigningNonces:: { - hiding: Nonce::::from_hex(signer["hiding_nonce"].as_str().unwrap()).unwrap(), - binding: Nonce::::from_hex(signer["binding_nonce"].as_str().unwrap()).unwrap(), - }; - - signer_nonces.insert(identifier, signing_nonces); - - let signing_commitments = SigningCommitments:: { - identifier, - hiding: NonceCommitment::from_hex(signer["hiding_nonce_commitment"].as_str().unwrap()) - .unwrap(), - binding: NonceCommitment::from_hex( - signer["binding_nonce_commitment"].as_str().unwrap(), - ) - .unwrap(), - }; - - signer_commitments.insert(identifier, signing_commitments); - } - - // Round two outputs - - let round_two_outputs = &P256_SHA256["round_two_outputs"]; - - let mut signature_shares: HashMap, SignatureShare> = HashMap::new(); - - for (i, signer) in round_two_outputs["signers"].as_object().unwrap().iter() { - let signature_share = SignatureShare:: { - identifier: u16::from_str(i).unwrap().try_into().unwrap(), - signature: SignatureResponse { - z_share: Scalar::from_repr(*FieldBytes::from_slice( - hex::decode(signer["sig_share"].as_str().unwrap()) - .unwrap() - .as_slice(), - )) - .unwrap(), - }, - }; - - signature_shares.insert( - u16::from_str(i).unwrap().try_into().unwrap(), - signature_share, - ); - } - - // Final output - - let final_output = &P256_SHA256["final_output"]; - - let signature_bytes = FromHex::from_hex(final_output["sig"].as_str().unwrap()).unwrap(); - - ( - group_public, - key_packages, - message, - message_bytes, - signer_nonces, - signer_commitments, - group_binding_factor_input, - group_binding_factor, - signature_shares, - signature_bytes, - ) -} diff --git a/frost-p256/tests/frost.rs b/frost-p256/tests/frost.rs index d7350ba3..dd450706 100644 --- a/frost-p256/tests/frost.rs +++ b/frost-p256/tests/frost.rs @@ -1,98 +1,9 @@ -use std::{collections::HashMap, convert::TryFrom}; - use frost_p256::*; use rand::thread_rng; #[test] fn check_sign_with_dealer() { - let mut rng = thread_rng(); - - //////////////////////////////////////////////////////////////////////////// - // Key generation - //////////////////////////////////////////////////////////////////////////// - - let numsigners = 5; - let threshold = 3; - let (shares, pubkeys) = keys::keygen_with_dealer(numsigners, threshold, &mut rng).unwrap(); - - // Verifies the secret shares from the dealer - let key_packages: HashMap = shares - .into_iter() - .map(|share| (share.identifier, keys::KeyPackage::try_from(share).unwrap())) - .collect(); - - let mut nonces: HashMap = HashMap::new(); - let mut commitments: HashMap = HashMap::new(); - - //////////////////////////////////////////////////////////////////////////// - // Round 1: generating nonces and signing commitments for each participant - //////////////////////////////////////////////////////////////////////////// - - for participant_index in 1..(threshold as u16 + 1) { - let participant_identifier = participant_index.try_into().expect("should be nonzero"); - // Generate one (1) nonce and one SigningCommitments instance for each - // participant, up to _threshold_. - let (nonce, commitment) = round1::commit( - participant_identifier, - key_packages - .get(&participant_identifier) - .unwrap() - .secret_share(), - &mut rng, - ); - nonces.insert(participant_identifier, nonce); - commitments.insert(participant_identifier, commitment); - } - - // This is what the signature aggregator / coordinator needs to do: - // - decide what message to sign - // - take one (unused) commitment per signing participant - let mut signature_shares: Vec = Vec::new(); - let message = "message to sign".as_bytes(); - let comms = commitments.clone().into_values().collect(); - let signing_package = SigningPackage::new(comms, message.to_vec()); - - //////////////////////////////////////////////////////////////////////////// - // Round 2: each participant generates their signature share - //////////////////////////////////////////////////////////////////////////// - - for participant_identifier in nonces.keys() { - let key_package = key_packages.get(participant_identifier).unwrap(); - - let nonces_to_use = &nonces.get(participant_identifier).unwrap(); - - // Each participant generates their signature share. - let signature_share = round2::sign(&signing_package, nonces_to_use, key_package).unwrap(); - signature_shares.push(signature_share); - } - - //////////////////////////////////////////////////////////////////////////// - // Aggregation: collects the signing shares from all participants, - // generates the final signature. - //////////////////////////////////////////////////////////////////////////// - - // Aggregate (also verifies the signature shares) - let group_signature_res = aggregate(&signing_package, &signature_shares[..], &pubkeys); - - assert!(group_signature_res.is_ok()); - - let group_signature = group_signature_res.unwrap(); - - // Check that the threshold signature can be verified by the group public - // key (the verification key). - assert!(pubkeys - .group_public - .verify(message, &group_signature) - .is_ok()); - - // Check that the threshold signature can be verified by the group public - // key (the verification key) from SharePackage.group_public - for (participant_identifier, _) in nonces.clone() { - let key_package = key_packages.get(&participant_identifier).unwrap(); + let rng = thread_rng(); - assert!(key_package - .group_public - .verify(message, &group_signature) - .is_ok()); - } + frost_core::tests::check_sign_with_dealer::(rng); } diff --git a/frost-p256/tests/proptests.rs b/frost-p256/tests/proptests.rs index 9b9e7c72..55e95832 100644 --- a/frost-p256/tests/proptests.rs +++ b/frost-p256/tests/proptests.rs @@ -1,94 +1,6 @@ +use frost_core::tests::proptests::{tweak_strategy, SignatureCase}; use frost_p256::*; use proptest::prelude::*; -use rand_core::{CryptoRng, RngCore}; - -/// A signature test-case, containing signature data and expected validity. -#[derive(Clone, Debug)] -struct SignatureCase { - msg: Vec, - sig: Signature, - vk: VerifyingKey, - invalid_vk: VerifyingKey, - is_valid: bool, -} - -/// A modification to a test-case. -#[derive(Copy, Clone, Debug)] -enum Tweak { - /// No-op, used to check that unchanged cases verify. - None, - /// Change the message the signature is defined for, invalidating the signature. - ChangeMessage, - /// Change the public key the signature is defined for, invalidating the signature. - ChangePubkey, - /* XXX implement this -- needs to regenerate a custom signature because the - nonce commitment is fed into the hash, so it has to have torsion at signing - time. - /// Change the case to have a torsion component in the signature's `r` value. - AddTorsion, - */ - /* XXX implement this -- needs custom handling of field arithmetic. - /// Change the signature's `s` scalar to be unreduced (mod L), invalidating the signature. - UnreducedScalar, - */ -} - -impl SignatureCase { - fn new(mut rng: R, msg: Vec) -> Self { - let sk = SigningKey::new(&mut rng); - let sig = sk.sign(&mut rng, &msg); - let vk = VerifyingKey::from(&sk); - let invalid_vk = VerifyingKey::from(&SigningKey::new(&mut rng)); - Self { - msg, - sig, - vk, - invalid_vk, - is_valid: true, - } - } - - // Check that signature verification succeeds or fails, as expected. - fn check(&self) -> bool { - // The signature data is stored in (refined) byte types, but do a round trip - // conversion to raw bytes to exercise those code paths. - let _sig = { - let bytes = self.sig.to_bytes(); - Signature::from_bytes(bytes) - }; - - // Check that the verification key is a valid key. - let _pub_key = VerifyingKey::from_bytes(self.vk.to_bytes()) - .expect("The test verification key to be well-formed."); - - // Check that signature validation has the expected result. - self.is_valid == self.vk.verify(&self.msg, &self.sig).is_ok() - } - - fn apply_tweak(&mut self, tweak: &Tweak) { - match tweak { - Tweak::None => {} - Tweak::ChangeMessage => { - // Changing the message makes the signature invalid. - self.msg.push(90); - self.is_valid = false; - } - Tweak::ChangePubkey => { - // Changing the public key makes the signature invalid. - self.vk = self.invalid_vk; - self.is_valid = false; - } - } - } -} - -fn tweak_strategy() -> impl Strategy { - prop_oneof![ - 10 => Just(Tweak::None), - 1 => Just(Tweak::ChangeMessage), - 1 => Just(Tweak::ChangePubkey), - ] -} use rand_chacha::ChaChaRng; use rand_core::SeedableRng; @@ -107,7 +19,7 @@ proptest! { // Create a test case for each signature type. let msg = b"test message for proptests"; - let mut sig = SignatureCase::new(&mut rng, msg.to_vec()); + let mut sig = SignatureCase::::new(&mut rng, msg.to_vec()); // Apply tweaks to each case. for t in &tweaks { diff --git a/frost-ristretto255/Cargo.toml b/frost-ristretto255/Cargo.toml index cd23fd22..b513ce96 100644 --- a/frost-ristretto255/Cargo.toml +++ b/frost-ristretto255/Cargo.toml @@ -21,7 +21,7 @@ features = ["nightly"] byteorder = "1.4" curve25519-dalek = { version = "4.0.0-pre.1", features = ["serde"] } digest = "0.10" -frost-core = { path = "../frost-core" } +frost-core = { path = "../frost-core", features = ["test-impl"] } hex = { version = "0.4.3", features = ["serde"] } rand_core = "0.6" serde = { version = "1", optional = true, features = ["derive"] } diff --git a/frost-ristretto255/src/tests.rs b/frost-ristretto255/src/tests.rs index b166fb2e..5176af7b 100644 --- a/frost-ristretto255/src/tests.rs +++ b/frost-ristretto255/src/tests.rs @@ -1,153 +1,26 @@ +use lazy_static::lazy_static; use rand::thread_rng; +use serde_json::Value; use crate::*; -mod vectors; - -use vectors::*; +lazy_static! { + pub static ref RISTRETTO255_SHA512: Value = + serde_json::from_str(include_str!("tests/vectors.json").trim()) + .expect("Test vector is valid JSON"); +} /// This is testing that Shamir's secret sharing to compute and arbitrary /// value is working. #[test] fn check_share_generation_ristretto255_sha512() { - let mut rng = thread_rng(); - - let secret = frost::keys::Secret::::random(&mut rng); - - let secret_shares = frost::keys::generate_secret_shares(&secret, 5, 3, rng).unwrap(); - - for secret_share in secret_shares.iter() { - assert_eq!(secret_share.verify(), Ok(())); - } - - assert_eq!( - frost::keys::reconstruct_secret::(secret_shares).unwrap(), - secret - ) + let rng = thread_rng(); + frost_core::tests::check_share_generation::(rng); } #[test] fn check_sign_with_test_vectors() { - let ( - group_public, - key_packages, - _message, - message_bytes, - signer_nonces, - signer_commitments, - group_binding_factor_input, - group_binding_factor, - signature_shares, - signature_bytes, - ) = parse_test_vectors(); - - type R = Ristretto255Sha512; - - //////////////////////////////////////////////////////////////////////////// - // Key generation - //////////////////////////////////////////////////////////////////////////// - - for key_package in key_packages.values() { - assert_eq!( - *key_package.public(), - frost::keys::Public::from(*key_package.secret_share()) - ); - } - - ///////////////////////////////////////////////////////////////////////////// - // Round 1: generating nonces and signing commitments for each participant - ///////////////////////////////////////////////////////////////////////////// - - for (i, _) in signer_commitments.clone() { - // compute nonce commitments from nonces - let nonces = signer_nonces.get(&i).unwrap(); - let nonce_commitments = signer_commitments.get(&i).unwrap(); - - assert_eq!( - &frost::round1::NonceCommitment::from(nonces.hiding()), - nonce_commitments.hiding() - ); - - assert_eq!( - &frost::round1::NonceCommitment::from(nonces.binding()), - nonce_commitments.binding() - ); - } - - ///////////////////////////////////////////////////////////////////////////// - // Round 2: each participant generates their signature share - ///////////////////////////////////////////////////////////////////////////// - - let signer_commitments_vec = signer_commitments - .into_iter() - .map(|(_, signing_commitments)| signing_commitments) - .collect(); - - let signing_package = frost::SigningPackage::new(signer_commitments_vec, message_bytes); - - assert_eq!(signing_package.rho_preimage(), group_binding_factor_input); - - let rho: frost::Rho = (&signing_package).into(); - - assert_eq!(rho, group_binding_factor); - - let mut our_signature_shares: Vec> = Vec::new(); - - // Each participant generates their signature share - for identifier in signer_nonces.keys() { - let key_package = &key_packages[identifier]; - let nonces = &signer_nonces[identifier]; - - // Each participant generates their signature share. - let signature_share = frost::round2::sign(&signing_package, nonces, key_package).unwrap(); - - our_signature_shares.push(signature_share); - } - - for sig_share in our_signature_shares.clone() { - assert_eq!(sig_share, signature_shares[sig_share.identifier()]); - } - - let signer_pubkeys = key_packages - .into_iter() - .map(|(i, key_package)| (i, *key_package.public())) - .collect(); - - let pubkey_package = frost::keys::PublicKeyPackage { - signer_pubkeys, - group_public, - }; - - //////////////////////////////////////////////////////////////////////////// - // Aggregation: collects the signing shares from all participants, - // generates the final signature. - //////////////////////////////////////////////////////////////////////////// - - // Aggregate the FROST signature from test vector sig shares - let group_signature_result = frost::aggregate( - &signing_package, - &signature_shares - .values() - .cloned() - .collect::>>(), - &pubkey_package, - ); - - // Check that the aggregation passed signature share verification and generation - assert!(group_signature_result.is_ok()); - - // Check that the generated signature matches the test vector signature - let group_signature = group_signature_result.unwrap(); - assert_eq!(group_signature.to_bytes().to_vec(), signature_bytes); - - // Aggregate the FROST signature from our signature shares - let group_signature_result = - frost::aggregate(&signing_package, &our_signature_shares, &pubkey_package); - - // Check that the aggregation passed signature share verification and generation - assert!(group_signature_result.is_ok()); - - // Check that the generated signature matches the test vector signature - let group_signature = group_signature_result.unwrap(); - assert_eq!(group_signature.to_bytes().to_vec(), signature_bytes); + frost_core::tests::vectors::check_sign_with_test_vectors::( + &RISTRETTO255_SHA512, + ) } diff --git a/frost-ristretto255/src/tests/vectors.rs b/frost-ristretto255/src/tests/vectors.rs deleted file mode 100644 index d2f391cb..00000000 --- a/frost-ristretto255/src/tests/vectors.rs +++ /dev/null @@ -1,150 +0,0 @@ -use std::{collections::HashMap, str::FromStr}; - -use curve25519_dalek::scalar::Scalar; -use hex::{self, FromHex}; -use lazy_static::lazy_static; -use serde_json::Value; - -use frost_core::{ - frost::{keys::*, round1::*, round2::*, *}, - VerifyingKey, -}; - -use crate::Ristretto255Sha512; - -lazy_static! { - pub static ref RISTRETTO255_SHA512: Value = - serde_json::from_str(include_str!("vectors.json").trim()) - .expect("Test vector is valid JSON"); -} - -#[allow(clippy::type_complexity)] -#[allow(dead_code)] -pub(crate) fn parse_test_vectors() -> ( - VerifyingKey, - HashMap, KeyPackage>, - &'static str, - Vec, - HashMap, SigningNonces>, - HashMap, SigningCommitments>, - Vec, - Rho, - HashMap, SignatureShare>, - Vec, // Signature, -) { - type R = Ristretto255Sha512; - - let inputs = &RISTRETTO255_SHA512["inputs"]; - - let message = inputs["message"].as_str().unwrap(); - let message_bytes = hex::decode(message).unwrap(); - - let mut key_packages: HashMap, KeyPackage> = HashMap::new(); - - let possible_signers = RISTRETTO255_SHA512["inputs"]["signers"] - .as_object() - .unwrap() - .iter(); - - let group_public = - VerifyingKey::::from_hex(inputs["group_public_key"].as_str().unwrap()).unwrap(); - - for (i, secret_share) in possible_signers { - let secret = Secret::::from_hex(secret_share["signer_share"].as_str().unwrap()).unwrap(); - let signer_public = secret.into(); - - let key_package = KeyPackage:: { - identifier: u16::from_str(i).unwrap().try_into().unwrap(), - secret_share: secret, - public: signer_public, - group_public, - }; - - key_packages.insert(*key_package.identifier(), key_package); - } - - // Round one outputs - - let round_one_outputs = &RISTRETTO255_SHA512["round_one_outputs"]; - - let group_binding_factor_input = Vec::::from_hex( - round_one_outputs["group_binding_factor_input"] - .as_str() - .unwrap(), - ) - .unwrap(); - - let group_binding_factor = - Rho::::from_hex(round_one_outputs["group_binding_factor"].as_str().unwrap()).unwrap(); - - let mut signer_nonces: HashMap, SigningNonces> = HashMap::new(); - let mut signer_commitments: HashMap, SigningCommitments> = HashMap::new(); - - for (i, signer) in round_one_outputs["signers"].as_object().unwrap().iter() { - let identifier = u16::from_str(i).unwrap().try_into().unwrap(); - - let signing_nonces = SigningNonces:: { - hiding: Nonce::::from_hex(signer["hiding_nonce"].as_str().unwrap()).unwrap(), - binding: Nonce::::from_hex(signer["binding_nonce"].as_str().unwrap()).unwrap(), - }; - - signer_nonces.insert(identifier, signing_nonces); - - let signing_commitments = SigningCommitments:: { - identifier, - hiding: NonceCommitment::from_hex(signer["hiding_nonce_commitment"].as_str().unwrap()) - .unwrap(), - binding: NonceCommitment::from_hex( - signer["binding_nonce_commitment"].as_str().unwrap(), - ) - .unwrap(), - }; - - signer_commitments.insert(identifier, signing_commitments); - } - - // Round two outputs - - let round_two_outputs = &RISTRETTO255_SHA512["round_two_outputs"]; - - let mut signature_shares: HashMap, SignatureShare> = HashMap::new(); - - for (i, signer) in round_two_outputs["signers"].as_object().unwrap().iter() { - let signature_share = SignatureShare:: { - identifier: u16::from_str(i).unwrap().try_into().unwrap(), - signature: SignatureResponse { - z_share: Scalar::from_canonical_bytes( - hex::decode(signer["sig_share"].as_str().unwrap()) - .unwrap() - .try_into() - .unwrap(), - ) - .unwrap(), - }, - }; - - signature_shares.insert( - u16::from_str(i).unwrap().try_into().unwrap(), - signature_share, - ); - } - - // Final output - - let final_output = &RISTRETTO255_SHA512["final_output"]; - - let signature_bytes = FromHex::from_hex(final_output["sig"].as_str().unwrap()).unwrap(); - - ( - group_public, - key_packages, - message, - message_bytes, - signer_nonces, - signer_commitments, - group_binding_factor_input, - group_binding_factor, - signature_shares, - signature_bytes, - ) -} diff --git a/frost-ristretto255/tests/frost.rs b/frost-ristretto255/tests/frost.rs index a2431343..f6e4316e 100644 --- a/frost-ristretto255/tests/frost.rs +++ b/frost-ristretto255/tests/frost.rs @@ -1,98 +1,9 @@ -use std::{collections::HashMap, convert::TryFrom}; - use frost_ristretto255::*; use rand::thread_rng; #[test] fn check_sign_with_dealer() { - let mut rng = thread_rng(); - - //////////////////////////////////////////////////////////////////////////// - // Key generation - //////////////////////////////////////////////////////////////////////////// - - let numsigners = 5; - let threshold = 3; - let (shares, pubkeys) = keys::keygen_with_dealer(numsigners, threshold, &mut rng).unwrap(); - - // Verifies the secret shares from the dealer - let key_packages: HashMap = shares - .into_iter() - .map(|share| (share.identifier, keys::KeyPackage::try_from(share).unwrap())) - .collect(); - - let mut nonces: HashMap = HashMap::new(); - let mut commitments: HashMap = HashMap::new(); - - //////////////////////////////////////////////////////////////////////////// - // Round 1: generating nonces and signing commitments for each participant - //////////////////////////////////////////////////////////////////////////// - - for participant_index in 1..(threshold as u16 + 1) { - let participant_identifier = participant_index.try_into().expect("should be nonzero"); - // Generate one (1) nonce and one SigningCommitments instance for each - // participant, up to _threshold_. - let (nonce, commitment) = round1::commit( - participant_identifier, - key_packages - .get(&participant_identifier) - .unwrap() - .secret_share(), - &mut rng, - ); - nonces.insert(participant_identifier, nonce); - commitments.insert(participant_identifier, commitment); - } - - // This is what the signature aggregator / coordinator needs to do: - // - decide what message to sign - // - take one (unused) commitment per signing participant - let mut signature_shares: Vec = Vec::new(); - let message = "message to sign".as_bytes(); - let comms = commitments.clone().into_values().collect(); - let signing_package = SigningPackage::new(comms, message.to_vec()); - - //////////////////////////////////////////////////////////////////////////// - // Round 2: each participant generates their signature share - //////////////////////////////////////////////////////////////////////////// - - for participant_identifier in nonces.keys() { - let key_package = key_packages.get(participant_identifier).unwrap(); - - let nonces_to_use = &nonces.get(participant_identifier).unwrap(); - - // Each participant generates their signature share. - let signature_share = round2::sign(&signing_package, nonces_to_use, key_package).unwrap(); - signature_shares.push(signature_share); - } - - //////////////////////////////////////////////////////////////////////////// - // Aggregation: collects the signing shares from all participants, - // generates the final signature. - //////////////////////////////////////////////////////////////////////////// - - // Aggregate (also verifies the signature shares) - let group_signature_res = aggregate(&signing_package, &signature_shares[..], &pubkeys); - - assert!(group_signature_res.is_ok()); - - let group_signature = group_signature_res.unwrap(); - - // Check that the threshold signature can be verified by the group public - // key (the verification key). - assert!(pubkeys - .group_public - .verify(message, &group_signature) - .is_ok()); - - // Check that the threshold signature can be verified by the group public - // key (the verification key) from SharePackage.group_public - for (participant_identifier, _) in nonces.clone() { - let key_package = key_packages.get(&participant_identifier).unwrap(); + let rng = thread_rng(); - assert!(key_package - .group_public - .verify(message, &group_signature) - .is_ok()); - } + frost_core::tests::check_sign_with_dealer::(rng); } diff --git a/frost-ristretto255/tests/proptests.rs b/frost-ristretto255/tests/proptests.rs index 2d6c6b17..8371531a 100644 --- a/frost-ristretto255/tests/proptests.rs +++ b/frost-ristretto255/tests/proptests.rs @@ -1,94 +1,6 @@ +use frost_core::tests::proptests::{tweak_strategy, SignatureCase}; use frost_ristretto255::*; use proptest::prelude::*; -use rand_core::{CryptoRng, RngCore}; - -/// A signature test-case, containing signature data and expected validity. -#[derive(Clone, Debug)] -struct SignatureCase { - msg: Vec, - sig: Signature, - vk: VerifyingKey, - invalid_vk: VerifyingKey, - is_valid: bool, -} - -/// A modification to a test-case. -#[derive(Copy, Clone, Debug)] -enum Tweak { - /// No-op, used to check that unchanged cases verify. - None, - /// Change the message the signature is defined for, invalidating the signature. - ChangeMessage, - /// Change the public key the signature is defined for, invalidating the signature. - ChangePubkey, - /* XXX implement this -- needs to regenerate a custom signature because the - nonce commitment is fed into the hash, so it has to have torsion at signing - time. - /// Change the case to have a torsion component in the signature's `r` value. - AddTorsion, - */ - /* XXX implement this -- needs custom handling of field arithmetic. - /// Change the signature's `s` scalar to be unreduced (mod L), invalidating the signature. - UnreducedScalar, - */ -} - -impl SignatureCase { - fn new(mut rng: R, msg: Vec) -> Self { - let sk = SigningKey::new(&mut rng); - let sig = sk.sign(&mut rng, &msg); - let vk = VerifyingKey::from(&sk); - let invalid_vk = VerifyingKey::from(&SigningKey::new(&mut rng)); - Self { - msg, - sig, - vk, - invalid_vk, - is_valid: true, - } - } - - // Check that signature verification succeeds or fails, as expected. - fn check(&self) -> bool { - // The signature data is stored in (refined) byte types, but do a round trip - // conversion to raw bytes to exercise those code paths. - let _sig = { - let bytes = self.sig.to_bytes(); - Signature::from_bytes(bytes) - }; - - // Check that the verification key is a valid key. - let _pub_key = VerifyingKey::from_bytes(self.vk.to_bytes()) - .expect("The test verification key to be well-formed."); - - // Check that signature validation has the expected result. - self.is_valid == self.vk.verify(&self.msg, &self.sig).is_ok() - } - - fn apply_tweak(&mut self, tweak: &Tweak) { - match tweak { - Tweak::None => {} - Tweak::ChangeMessage => { - // Changing the message makes the signature invalid. - self.msg.push(90); - self.is_valid = false; - } - Tweak::ChangePubkey => { - // Changing the public key makes the signature invalid. - self.vk = self.invalid_vk; - self.is_valid = false; - } - } - } -} - -fn tweak_strategy() -> impl Strategy { - prop_oneof![ - 10 => Just(Tweak::None), - 1 => Just(Tweak::ChangeMessage), - 1 => Just(Tweak::ChangePubkey), - ] -} use rand_chacha::ChaChaRng; use rand_core::SeedableRng; @@ -107,7 +19,7 @@ proptest! { // Create a test case for each signature type. let msg = b"test message for proptests"; - let mut sig = SignatureCase::new(&mut rng, msg.to_vec()); + let mut sig = SignatureCase::::new(&mut rng, msg.to_vec()); // Apply tweaks to each case. for t in &tweaks {