From ffe5c57a1729c933b3ec8766ec96d2e6976a7ece Mon Sep 17 00:00:00 2001 From: Conrado Gouvea Date: Fri, 6 Jan 2023 01:26:13 -0300 Subject: [PATCH] Expose internals to support randomization (simplified) (#152) * changes required for randomization * simplified version * simplify compute_signature_share return * add frost-rerandomized crate * move rerandomized tests to frost-rerandomized from reddsa; remove unused deps * Update frost-core/src/frost.rs Co-authored-by: Deirdre Connolly --- Cargo.toml | 1 + frost-core/Cargo.toml | 2 + frost-core/src/frost.rs | 126 ++++++++++------- frost-core/src/frost/identifier.rs | 3 +- frost-core/src/frost/keys.rs | 5 +- frost-core/src/frost/round1.rs | 3 + frost-core/src/frost/round2.rs | 39 ++++-- frost-core/src/lib.rs | 12 ++ frost-core/src/signature.rs | 9 ++ frost-core/src/tests/vectors.rs | 5 +- frost-core/src/verifying_key.rs | 13 ++ frost-rerandomized/Cargo.toml | 31 +++++ frost-rerandomized/README.md | 23 ++++ frost-rerandomized/src/lib.rs | 209 +++++++++++++++++++++++++++++ frost-rerandomized/src/tests.rs | 121 +++++++++++++++++ 15 files changed, 537 insertions(+), 65 deletions(-) create mode 100644 frost-rerandomized/Cargo.toml create mode 100644 frost-rerandomized/README.md create mode 100644 frost-rerandomized/src/lib.rs create mode 100644 frost-rerandomized/src/tests.rs diff --git a/Cargo.toml b/Cargo.toml index cee05f3..06ac114 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,6 @@ members = [ "frost-p256", "frost-ristretto255", "frost-secp256k1", + "frost-rerandomized", "gendoc" ] diff --git a/frost-core/Cargo.toml b/frost-core/Cargo.toml index 16947a1..936ed6b 100644 --- a/frost-core/Cargo.toml +++ b/frost-core/Cargo.toml @@ -23,6 +23,7 @@ digest = "0.10" hex = { version = "0.4.3", features = ["serde"] } rand_core = "0.6" thiserror = "1.0" +visibility = "0.0.1" zeroize = { version = "1.5.4", default-features = false, features = ["derive"] } # Test dependencies used with the test-impl feature @@ -44,3 +45,4 @@ nightly = [] default = [] # Exposes ciphersuite-generic tests for other crates to use test-impl = ["proptest", "proptest-derive", "serde_json"] +internals = [] diff --git a/frost-core/src/frost.rs b/frost-core/src/frost.rs index e8c8c6d..72f5e33 100644 --- a/frost-core/src/frost.rs +++ b/frost-core/src/frost.rs @@ -12,11 +12,11 @@ use std::{ collections::{BTreeMap, HashMap}, - convert::TryFrom, fmt::{self, Debug}, ops::Index, }; +#[cfg(any(test, feature = "test-impl"))] use hex::FromHex; mod identifier; @@ -75,6 +75,12 @@ impl BindingFactorList where C: Ciphersuite, { + /// Create a new [`BindingFactorList`] from a vector of binding factors. + #[cfg(feature = "internals")] + pub fn new(binding_factors: BTreeMap, BindingFactor>) -> Self { + Self(binding_factors) + } + /// Return iterator through all factors. pub fn iter(&self) -> impl Iterator, &BindingFactor)> { self.0.iter() @@ -97,26 +103,28 @@ where } } -impl From<&SigningPackage> for BindingFactorList +/// [`compute_binding_factors`] in the spec +/// +/// [`compute_binding_factors`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-10.html#section-4.4 +#[cfg_attr(feature = "internals", visibility::make(pub))] +pub(crate) fn compute_binding_factor_list( + signing_package: &SigningPackage, + additional_prefix: &[u8], +) -> BindingFactorList where C: Ciphersuite, { - // [`compute_binding_factors`] in the spec - // - // [`compute_binding_factors`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-11.html#section-4.4 - fn from(signing_package: &SigningPackage) -> BindingFactorList { - let preimages = signing_package.binding_factor_preimages(); + let preimages = signing_package.binding_factor_preimages(additional_prefix); - BindingFactorList( - preimages - .iter() - .map(|(identifier, preimage)| { - let binding_factor = C::H1(preimage); - (*identifier, BindingFactor(binding_factor)) - }) - .collect(), - ) - } + BindingFactorList( + preimages + .iter() + .map(|(identifier, preimage)| { + let binding_factor = C::H1(preimage); + (*identifier, BindingFactor(binding_factor)) + }) + .collect(), + ) } #[cfg(any(test, feature = "test-impl"))] @@ -138,6 +146,7 @@ where // TODO: pub struct Lagrange(Scalar); /// Generates the lagrange coefficient for the i'th participant. +#[cfg_attr(feature = "internals", visibility::make(pub))] fn derive_lagrange_coeff( signer_id: &Identifier, signing_package: &SigningPackage, @@ -221,15 +230,20 @@ where &self.message } - /// Compute the preimages to H3 to compute the per-signer binding factors + /// Compute the preimages to H1 to compute the per-signer binding factors // We separate this out into its own method so it can be tested - pub fn binding_factor_preimages(&self) -> Vec<(Identifier, Vec)> { + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub fn binding_factor_preimages( + &self, + additional_prefix: &[u8], + ) -> Vec<(Identifier, Vec)> { let mut binding_factor_input_prefix = vec![]; binding_factor_input_prefix.extend_from_slice(C::H4(self.message.as_slice()).as_ref()); binding_factor_input_prefix.extend_from_slice( C::H5(&round1::encode_group_commitments(self.signing_commitments())[..]).as_ref(), ); + binding_factor_input_prefix.extend_from_slice(additional_prefix); self.signing_commitments() .iter() @@ -246,9 +260,20 @@ where /// The product of all signers' individual commitments, published as part of the /// final signature. -#[derive(PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] pub struct GroupCommitment(pub(super) Element); +impl GroupCommitment +where + C: Ciphersuite, +{ + /// Return the underlying element. + #[cfg(feature = "internals")] + pub fn to_element(self) -> ::Element { + self.0 + } +} + // impl Debug for GroupCommitment where C: Ciphersuite { // fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // f.debug_tuple("GroupCommitment") @@ -257,43 +282,41 @@ pub struct GroupCommitment(pub(super) Element); // } // } -impl TryFrom<&SigningPackage> for GroupCommitment +/// Generates the group commitment which is published as part of the joint +/// Schnorr signature. +/// +/// Implements [`compute_group_commitment`] from the spec. +/// +/// [`compute_group_commitment`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-10.html#section-4.5 +#[cfg_attr(feature = "internals", visibility::make(pub))] +fn compute_group_commitment( + signing_package: &SigningPackage, + binding_factor_list: &BindingFactorList, +) -> Result, Error> where C: Ciphersuite, { - type Error = Error; + let identity = ::identity(); - /// Generates the group commitment which is published as part of the joint - /// Schnorr signature. - /// - /// Implements [`compute_group_commitment`] from the spec. - /// - /// [`compute_group_commitment`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-11.html#section-4.5 - fn try_from(signing_package: &SigningPackage) -> Result, Error> { - let binding_factor_list: BindingFactorList = signing_package.into(); + let mut group_commitment = ::identity(); - let identity = ::identity(); - - let mut group_commitment = ::identity(); - - // Ala the sorting of B, just always sort by identifier in ascending order - // - // https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#encoding-operations-dep-encoding - for commitment in signing_package.signing_commitments() { - // The following check prevents a party from accidentally revealing their share. - // Note that the '&&' operator would be sufficient. - if identity == commitment.binding.0 || identity == commitment.hiding.0 { - return Err(Error::IdentityCommitment); - } - - let binding_factor = binding_factor_list[commitment.identifier].clone(); - - group_commitment = group_commitment - + (commitment.hiding.0 + (commitment.binding.0 * binding_factor.0)); + // Ala the sorting of B, just always sort by identifier in ascending order + // + // https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#encoding-operations-dep-encoding + for commitment in signing_package.signing_commitments() { + // The following check prevents a party from accidentally revealing their share. + // Note that the '&&' operator would be sufficient. + if identity == commitment.binding.0 || identity == commitment.hiding.0 { + return Err(Error::IdentityCommitment); } - Ok(GroupCommitment(group_commitment)) + let binding_factor = binding_factor_list[commitment.identifier].clone(); + + group_commitment = + group_commitment + (commitment.hiding.0 + (commitment.binding.0 * binding_factor.0)); } + + Ok(GroupCommitment(group_commitment)) } //////////////////////////////////////////////////////////////////////////////// @@ -325,10 +348,11 @@ where { // Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the // binding factor. - let binding_factor_list: BindingFactorList = signing_package.into(); + let binding_factor_list: BindingFactorList = + compute_binding_factor_list(signing_package, &[]); // Compute the group commitment from signing commitments produced in round one. - let group_commitment = GroupCommitment::::try_from(signing_package)?; + let group_commitment = compute_group_commitment(signing_package, &binding_factor_list)?; // Compute the per-message challenge. let challenge = crate::challenge::( diff --git a/frost-core/src/frost/identifier.rs b/frost-core/src/frost/identifier.rs index 660a52f..4017d1f 100644 --- a/frost-core/src/frost/identifier.rs +++ b/frost-core/src/frost/identifier.rs @@ -20,7 +20,8 @@ where C: Ciphersuite, { /// Serialize the identifier using the ciphersuite encoding. - pub fn serialize(&self) -> <::Field as Field>::Serialization { + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn serialize(&self) -> <::Field as Field>::Serialization { <::Field>::serialize(&self.0) } diff --git a/frost-core/src/frost/keys.rs b/frost-core/src/frost/keys.rs index 00511d3..5659a17 100644 --- a/frost-core/src/frost/keys.rs +++ b/frost-core/src/frost/keys.rs @@ -9,7 +9,9 @@ use std::{ iter, }; +#[cfg(any(test, feature = "test-impl"))] use hex::FromHex; + use rand_core::{CryptoRng, RngCore}; use zeroize::{DefaultIsZeroes, Zeroize}; @@ -542,7 +544,8 @@ pub(crate) fn generate_secret_shares( let (coefficients, commitment) = generate_secret_polynomial(secret, max_signers, min_signers, coefficients)?; - for id in (1..=max_signers as u16).map_while(|i| Identifier::::try_from(i).ok()) { + for idx in 1..=max_signers as u16 { + let id = Identifier::::try_from(idx)?; let value = evaluate_polynomial(id, &coefficients); secret_shares.push(SecretShare { diff --git a/frost-core/src/frost/round1.rs b/frost-core/src/frost/round1.rs index b9e4b7b..2f59f61 100644 --- a/frost-core/src/frost/round1.rs +++ b/frost-core/src/frost/round1.rs @@ -2,7 +2,9 @@ use std::fmt::{self, Debug}; +#[cfg(any(test, feature = "test-impl"))] use hex::FromHex; + use rand_core::{CryptoRng, RngCore}; use zeroize::Zeroize; @@ -225,6 +227,7 @@ where /// Computes the [signature commitment share] from these round one signing commitments. /// /// [signature commitment share]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-11.html#name-signature-share-verificatio + #[cfg_attr(feature = "internals", visibility::make(pub))] pub(super) fn to_group_commitment_share( self, binding_factor: &frost::BindingFactor, diff --git a/frost-core/src/frost/round2.rs b/frost-core/src/frost/round2.rs index 4adb571..26963a0 100644 --- a/frost-core/src/frost/round2.rs +++ b/frost-core/src/frost/round2.rs @@ -113,6 +113,25 @@ where } } +/// Compute the signature share for a signing operation. +#[cfg_attr(feature = "internals", visibility::make(pub))] +fn compute_signature_share( + signer_nonces: &round1::SigningNonces, + binding_factor: BindingFactor, + lambda_i: <<::Group as Group>::Field as Field>::Scalar, + key_package: &keys::KeyPackage, + challenge: Challenge, +) -> SignatureShare { + let z_share: <::Field as Field>::Scalar = signer_nonces.hiding.0 + + (signer_nonces.binding.0 * binding_factor.0) + + (lambda_i * key_package.secret_share.0 * challenge.0); + + SignatureShare:: { + identifier: *key_package.identifier(), + signature: SignatureResponse:: { z_share }, + } +} + // // Zeroizes `SignatureShare` to be the `Default` value on drop (when it goes out // // of scope). Luckily the derived `Default` includes the `Default` impl of // // Scalar, which is four 0u64's under the hood, and u16, which is @@ -138,12 +157,13 @@ pub fn sign( ) -> Result, Error> { // Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the // binding factor. - let binding_factor_list: frost::BindingFactorList = signing_package.into(); + let binding_factor_list: BindingFactorList = + compute_binding_factor_list(signing_package, &[]); let binding_factor: frost::BindingFactor = binding_factor_list[key_package.identifier].clone(); // Compute the group commitment from signing commitments produced in round one. - let group_commitment = GroupCommitment::::try_from(signing_package)?; + let group_commitment = compute_group_commitment(signing_package, &binding_factor_list)?; // Compute Lagrange coefficient. let lambda_i = frost::derive_lagrange_coeff(key_package.identifier(), signing_package)?; @@ -156,14 +176,13 @@ pub fn sign( ); // Compute the Schnorr signature share. - let z_share = signer_nonces.hiding.0 - + (signer_nonces.binding.0 * binding_factor.0) - + (lambda_i * key_package.secret_share.0 * challenge.0); - - let signature_share = SignatureShare:: { - identifier: *key_package.identifier(), - signature: SignatureResponse:: { z_share }, - }; + let signature_share = compute_signature_share( + signer_nonces, + binding_factor, + lambda_i, + key_package, + challenge, + ); Ok(signature_share) } diff --git a/frost-core/src/lib.rs b/frost-core/src/lib.rs index d3a7ca8..0493273 100644 --- a/frost-core/src/lib.rs +++ b/frost-core/src/lib.rs @@ -239,6 +239,17 @@ pub trait Ciphersuite: Copy + Clone + PartialEq + Debug { #[derive(Clone)] pub struct Challenge(pub(crate) <::Field as Field>::Scalar); +impl Challenge +where + C: Ciphersuite, +{ + /// Return the underlying scalar. + #[cfg(feature = "internals")] + pub fn to_scalar(self) -> <<::Group as Group>::Field as Field>::Scalar { + self.0 + } +} + impl Debug for Challenge where C: Ciphersuite, @@ -261,6 +272,7 @@ where /// /// [FROST]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-11.html#name-signature-challenge-computa /// [RFC]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-11.html#section-3.2 +#[cfg_attr(feature = "internals", visibility::make(pub))] fn challenge(R: &Element, verifying_key: &Element, msg: &[u8]) -> Challenge where C: Ciphersuite, diff --git a/frost-core/src/signature.rs b/frost-core/src/signature.rs index eba9a1d..32a5585 100644 --- a/frost-core/src/signature.rs +++ b/frost-core/src/signature.rs @@ -20,6 +20,15 @@ where C::Group: Group, ::Field: Field, { + /// Create a new Signature. + #[cfg(feature = "internals")] + pub fn new( + R: ::Element, + z: <::Field as Field>::Scalar, + ) -> Self { + Self { R, z } + } + /// Converts bytes as [`Ciphersuite::SignatureSerialization`] into a `Signature`. pub fn from_bytes(bytes: C::SignatureSerialization) -> Result> { // To compute the expected length of the encoded point, encode the generator diff --git a/frost-core/src/tests/vectors.rs b/frost-core/src/tests/vectors.rs index 166463a..b2fb6dd 100644 --- a/frost-core/src/tests/vectors.rs +++ b/frost-core/src/tests/vectors.rs @@ -280,11 +280,12 @@ pub fn check_sign_with_test_vectors(json_vectors: &Value) { let signing_package = frost::SigningPackage::new(signer_commitments_vec, message_bytes); - for (identifier, input) in signing_package.binding_factor_preimages().iter() { + for (identifier, input) in signing_package.binding_factor_preimages(&[]).iter() { assert_eq!(*input, binding_factor_inputs[identifier]); } - let binding_factor_list: frost::BindingFactorList = (&signing_package).into(); + let binding_factor_list: frost::BindingFactorList = + compute_binding_factor_list(&signing_package, &[]); for (identifier, binding_factor) in binding_factor_list.iter() { assert_eq!(*binding_factor, binding_factors[identifier]); diff --git a/frost-core/src/verifying_key.rs b/frost-core/src/verifying_key.rs index fb857dd..583dd35 100644 --- a/frost-core/src/verifying_key.rs +++ b/frost-core/src/verifying_key.rs @@ -1,5 +1,6 @@ use std::fmt::{self, Debug}; +#[cfg(any(test, feature = "test-impl"))] use hex::FromHex; use crate::{Challenge, Ciphersuite, Element, Error, Group, Signature}; @@ -23,6 +24,18 @@ where // VerifyingKey { element } // } + /// Create a new VerifyingKey from the given element. + #[cfg(feature = "internals")] + pub fn new(element: ::Element) -> Self { + Self { element } + } + + /// Return the underlying element. + #[cfg(feature = "internals")] + pub fn to_element(self) -> ::Element { + self.element + } + /// Deserialize from bytes pub fn from_bytes( bytes: ::Serialization, diff --git a/frost-rerandomized/Cargo.toml b/frost-rerandomized/Cargo.toml new file mode 100644 index 0000000..c8919fd --- /dev/null +++ b/frost-rerandomized/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "frost-rerandomized" +edition = "2021" +# When releasing to crates.io: +# - Update html_root_url +# - Update CHANGELOG.md +# - Create git tag. +version = "0.1.0" +authors = ["Deirdre Connolly ", "Chelsea Komlo ", + "Conrado Gouvea "] +readme = "README.md" +license = "MIT OR Apache-2.0" +repository = "https://github.com/ZcashFoundation/frost" +categories = ["cryptography"] +keywords = ["cryptography", "crypto", "threshold", "signature", "schnorr", "randomized"] +description = "Types and traits to support implementing a re-randomized variant of Flexible Round-Optimized Schnorr Threshold signature schemes (FROST)." + +[package.metadata.docs.rs] +features = ["nightly"] + +[dependencies] +frost-core = { path = "../frost-core", features = ["internals"] } +rand_core = "0.6" + +[dev-dependencies] + +[features] +nightly = [] +default = [] +# Exposes ciphersuite-generic tests for other crates to use +test-impl = ["frost-core/test-impl"] diff --git a/frost-rerandomized/README.md b/frost-rerandomized/README.md new file mode 100644 index 0000000..f33ef5b --- /dev/null +++ b/frost-rerandomized/README.md @@ -0,0 +1,23 @@ +# FROST (Flexible Round-Optimised Schnorr Threshold signatures) Rerandomized + +Base traits and types in Rust that implement ['Two-Round Threshold Schnorr Signatures with +FROST'](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/) generically for +`frost-core::Ciphersuite` implementations, with support for Zcash-compatible +RedDSA rerandomized signatures. + +## Status ⚠ + +The FROST specification is not yet finalized, and this codebase has not yet been audited or +released. The APIs and types in `frost-rerandomized` are subject to change. + +## Usage + +`frost-rerandomized` is similar to `frost-core`, but provides different +`sign()` and `aggregate()` functions adding support for rerandomized signatures. +End-users should not use `frost-rerandomized` if they want to sign and verify signatures, they +should use the crate specific to their ciphersuite/curve parameters that uses `frost-rerandomized` as a +dependency, such as [`reddsa`](https://github.com/ZcashFoundation/reddsa/). + +## Example + +See ciphersuite-specific modules, e.g. the ones in [`reddsa`](https://github.com/ZcashFoundation/reddsa/). diff --git a/frost-rerandomized/src/lib.rs b/frost-rerandomized/src/lib.rs new file mode 100644 index 0000000..ff34888 --- /dev/null +++ b/frost-rerandomized/src/lib.rs @@ -0,0 +1,209 @@ +//! Randomized FROST support. +//! +#![allow(non_snake_case)] + +#[cfg(any(test, feature = "test-impl"))] +pub mod tests; + +pub use frost_core; + +use frost_core::{ + frost::{self, keys::PublicKeyPackage}, + Ciphersuite, Error, Field, Group, VerifyingKey, +}; + +use rand_core::{CryptoRng, RngCore}; + +/// Performed once by each participant selected for the signing operation. +/// +/// Implements [`sign`] from the spec. +/// +/// Receives the message to be signed and a set of signing commitments and a set +/// of randomizing commitments to be used in that signing operation, including +/// that for this participant. +/// +/// Assumes the participant has already determined which nonce corresponds with +/// the commitment that was assigned by the coordinator in the SigningPackage. +/// +/// [`sign`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-10.html#name-round-two-signature-share-g +pub fn sign( + signing_package: &frost::SigningPackage, + signer_nonces: &frost::round1::SigningNonces, + key_package: &frost::keys::KeyPackage, + randomizer_point: &::Element, +) -> Result, Error> { + let public_key = key_package.group_public.to_element() + *randomizer_point; + + // Encodes the signing commitment list produced in round one as part of generating [`Rho`], the + // binding factor. + let binding_factor_list = frost::compute_binding_factor_list( + signing_package, + ::serialize(randomizer_point).as_ref(), + ); + + let rho: frost::BindingFactor = binding_factor_list[key_package.identifier].clone(); + + // Compute the group commitment from signing commitments produced in round one. + let group_commitment = frost::compute_group_commitment(signing_package, &binding_factor_list)?; + + // Compute Lagrange coefficient. + let lambda_i = frost::derive_lagrange_coeff(key_package.identifier(), signing_package)?; + + // Compute the per-message challenge. + let challenge = frost_core::challenge::( + &group_commitment.to_element(), + &public_key, + signing_package.message().as_slice(), + ); + + // Compute the Schnorr signature share. + let signature_share = frost::round2::compute_signature_share( + signer_nonces, + rho, + lambda_i, + key_package, + challenge, + ); + + Ok(signature_share) +} + +/// Verifies each participant's signature share, and if all are valid, +/// aggregates the shares into a signature to publish. +/// +/// Resulting signature is compatible with verification of a plain SpendAuth +/// signature. +/// +/// This operation is performed by a coordinator that can communicate with all +/// the signing participants before publishing the final signature. The +/// coordinator can be one of the participants or a semi-trusted third party +/// (who is trusted to not perform denial of service attacks, but does not learn +/// any secret information). Note that because the coordinator is trusted to +/// report misbehaving parties in order to avoid publishing an invalid +/// signature, if the coordinator themselves is a signer and misbehaves, they +/// can avoid that step. However, at worst, this results in a denial of +/// service attack due to publishing an invalid signature. +pub fn aggregate( + signing_package: &frost::SigningPackage, + signature_shares: &[frost::round2::SignatureShare], + pubkeys: &frost::keys::PublicKeyPackage, + randomized_params: &RandomizedParams, +) -> Result, Error> +where + C: Ciphersuite, +{ + let public_key = pubkeys.group_public.to_element() + *randomized_params.randomizer_point(); + + // Encodes the signing commitment list produced in round one as part of generating [`Rho`], the + // binding factor. + let binding_factor_list = frost::compute_binding_factor_list( + signing_package, + ::serialize(randomized_params.randomizer_point()).as_ref(), + ); + + // Compute the group commitment from signing commitments produced in round one. + let group_commitment = frost::compute_group_commitment(signing_package, &binding_factor_list)?; + + // Compute the per-message challenge. + let challenge = frost_core::challenge::( + &group_commitment.clone().to_element(), + &public_key, + signing_package.message().as_slice(), + ); + + // Verify the signature shares. + for signature_share in signature_shares { + // Look up the public key for this signer, where `signer_pubkey` = _G.ScalarBaseMult(s[i])_, + // and where s[i] is a secret share of the constant term of _f_, the secret polynomial. + let signer_pubkey = pubkeys + .signer_pubkeys + .get(&signature_share.identifier) + .unwrap(); + + // Compute Lagrange coefficient. + let lambda_i = frost::derive_lagrange_coeff(&signature_share.identifier, signing_package)?; + + let rho = binding_factor_list[signature_share.identifier].clone(); + + // Compute the commitment share. + let R_share = signing_package + .signing_commitment(&signature_share.identifier) + .to_group_commitment_share(&rho); + + // Compute relation values to verify this signature share. + signature_share.verify(&R_share, signer_pubkey, lambda_i, &challenge)?; + } + + // The aggregation of the signature shares by summing them up, resulting in + // a plain Schnorr signature. + // + // Implements [`aggregate`] from the spec. + // + // [`aggregate`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-10.html#section-5.3 + let mut z = <::Field as Field>::zero(); + + for signature_share in signature_shares { + z = z + signature_share.signature.z_share; + } + + z = z + challenge.to_scalar() * randomized_params.randomizer; + + Ok(frost_core::Signature::new(group_commitment.to_element(), z)) +} + +/// Randomized params for a signing instance of randomized FROST. +pub struct RandomizedParams { + /// The randomizer, also called `alpha` + randomizer: frost_core::Scalar, + /// The generator multiplied by the randomizer. + randomizer_point: ::Element, + /// The randomized group public key. The group public key added to the randomizer point. + randomized_group_public_key: frost_core::VerifyingKey, +} + +impl RandomizedParams +where + C: Ciphersuite, +{ + /// Create a new RandomizedParams for the given [`PublicKeyPackage`] + pub fn new( + public_key_package: &PublicKeyPackage, + mut rng: R, + ) -> Self { + let randomizer = <::Field as Field>::random(&mut rng); + let randomizer_point = ::generator() * randomizer; + + let group_public_point = public_key_package.group_public.to_element(); + + let randomized_group_public_point = group_public_point + randomizer_point; + let randomized_group_public_key = VerifyingKey::new(randomized_group_public_point); + + Self { + randomizer, + randomizer_point, + randomized_group_public_key, + } + } + + /// Return the randomizer. + /// + /// It can be useful to the coordinator, e.g. to generate the ZK proof + /// in Zcash. It MUST NOT be sent to other parties. + pub fn randomizer(&self) -> &frost_core::Scalar { + &self.randomizer + } + + /// Return the randomizer point. + /// + /// It must be sent by the coordinator to each participant when signing. + pub fn randomizer_point(&self) -> &::Element { + &self.randomizer_point + } + + /// Return the randomized group public key. + /// + /// It can be used to verify the final signature. + pub fn randomized_group_public_key(&self) -> &frost_core::VerifyingKey { + &self.randomized_group_public_key + } +} diff --git a/frost-rerandomized/src/tests.rs b/frost-rerandomized/src/tests.rs new file mode 100644 index 0000000..521bb47 --- /dev/null +++ b/frost-rerandomized/src/tests.rs @@ -0,0 +1,121 @@ +//! Ciphersuite-generic test functions for re-randomized FROST. + +use std::collections::HashMap; + +use crate::{frost_core::frost, frost_core::Ciphersuite, RandomizedParams}; +use frost_core::{Signature, VerifyingKey}; +use rand_core::{CryptoRng, RngCore}; + +/// Test re-randomized FROST signing with trusted dealer with a Ciphersuite. +/// Returns the signed message, generated signature, and the randomized public key +/// so that the caller can verify the signature with their own implementation. +pub fn check_randomized_sign_with_dealer( + mut rng: R, +) -> (Vec, Signature, VerifyingKey) { + //////////////////////////////////////////////////////////////////////////// + // Key generation + //////////////////////////////////////////////////////////////////////////// + + let max_signers = 5; + let min_signers = 3; + let (shares, pubkeys) = + frost::keys::keygen_with_dealer(max_signers, min_signers, &mut rng).unwrap(); + + // Verifies the secret shares from the dealer + let key_packages: HashMap, frost::keys::KeyPackage> = shares + .into_iter() + .map(|share| { + ( + share.identifier, + frost::keys::KeyPackage::try_from(share).unwrap(), + ) + }) + .collect(); + + let mut nonces: HashMap, frost::round1::SigningNonces> = HashMap::new(); + let mut commitments: HashMap, frost::round1::SigningCommitments> = + HashMap::new(); + + let randomizer_params = RandomizedParams::new(&pubkeys, &mut rng); + + //////////////////////////////////////////////////////////////////////////// + // Round 1: generating nonces and signing commitments for each participant + //////////////////////////////////////////////////////////////////////////// + + for participant_index in 1..(min_signers 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 _min_signers_. + let (nonce, commitment) = frost::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 = frost::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 = crate::sign( + &signing_package, + nonces_to_use, + key_package, + randomizer_params.randomizer_point(), + ) + .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 = crate::aggregate( + &signing_package, + &signature_shares[..], + &pubkeys, + &randomizer_params, + ); + + assert!(group_signature_res.is_ok()); + + let group_signature = group_signature_res.unwrap(); + + // Check that the threshold signature can be verified by the randomized group public + // key (the verification key). + assert!(randomizer_params + .randomized_group_public_key() + .verify(message, &group_signature) + .is_ok()); + + // Note that key_package.group_public can't be used to verify the signature + // since those are non-randomized. + + ( + message.to_owned(), + group_signature, + *randomizer_params.randomized_group_public_key(), + ) +}