diff --git a/Cargo.toml b/Cargo.toml index 08346be9..658adb74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "frost-p256", "frost-ristretto255", "frost-secp256k1", + "frost-secp256k1-tr", "frost-rerandomized", "gencode" ] diff --git a/frost-core/src/batch.rs b/frost-core/src/batch.rs index 3aa1e806..a30a109e 100644 --- a/frost-core/src/batch.rs +++ b/frost-core/src/batch.rs @@ -33,10 +33,14 @@ where where M: AsRef<[u8]>, { - // Compute c now to avoid dependency on the msg lifetime. - let c = crate::challenge(&sig.R, &vk, msg.as_ref())?; - - Ok(Self { vk, sig, c }) + let (msg, sig, vk) = ::pre_verify(msg.as_ref(), &sig, &vk)?; + let c = ::challenge(&sig.R, &vk, &msg)?; + + Ok(Self { + vk: *vk, + sig: *sig, + c, + }) } } diff --git a/frost-core/src/keys/dkg.rs b/frost-core/src/keys/dkg.rs index 60b33cdc..dcdb9b6c 100644 --- a/frost-core/src/keys/dkg.rs +++ b/frost-core/src/keys/dkg.rs @@ -355,8 +355,7 @@ pub(crate) fn compute_proof_of_knowledge // > a_{i0} by calculating σ_i = (R_i, μ_i), such that k ← Z_q, R_i = g^k, // > c_i = H(i, Φ, g^{a_{i0}} , R_i), μ_i = k + a_{i0} · c_i, with Φ being // > a context string to prevent replay attacks. - let k = <::Field>::random(&mut rng); - let R_i = ::generator() * k; + let (k, R_i) = ::generate_nonce(&mut rng); let c_i = challenge::(identifier, &commitment.verifying_key()?, &R_i)?; let a_i0 = *coefficients .first() @@ -570,5 +569,5 @@ pub fn part3( min_signers: round2_secret_package.min_signers, }; - Ok((key_package, public_key_package)) + C::post_dkg(key_package, public_key_package) } diff --git a/frost-core/src/lib.rs b/frost-core/src/lib.rs index a01cd3c2..7d78e2df 100644 --- a/frost-core/src/lib.rs +++ b/frost-core/src/lib.rs @@ -25,6 +25,7 @@ use alloc::{ use derive_getters::Getters; #[cfg(any(test, feature = "test-impl"))] use hex::FromHex; +use keys::PublicKeyPackage; use rand_core::{CryptoRng, RngCore}; use serialization::SerializableScalar; use zeroize::Zeroize; @@ -64,11 +65,7 @@ pub use verifying_key::VerifyingKey; /// /// [challenge]: https://datatracker.ietf.org/doc/html/rfc9591#name-signature-challenge-computa #[derive(Copy, Clone)] -#[cfg_attr(feature = "internals", visibility::make(pub))] -#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] -pub(crate) struct Challenge( - pub(crate) <::Field as Field>::Scalar, -); +pub struct Challenge(pub(crate) <::Field as Field>::Scalar); impl Challenge where @@ -138,6 +135,8 @@ where /// Generates a random nonzero scalar. /// /// It assumes that the Scalar Eq/PartialEq implementation is constant-time. +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] pub(crate) fn random_nonzero(rng: &mut R) -> Scalar { loop { let scalar = <::Field>::random(rng); @@ -192,9 +191,7 @@ where /// /// #[derive(Clone, PartialEq, Eq)] -#[cfg_attr(feature = "internals", visibility::make(pub))] -#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] -pub(crate) struct BindingFactor(Scalar); +pub struct BindingFactor(Scalar); impl BindingFactor where @@ -469,9 +466,7 @@ where /// The product of all signers' individual commitments, published as part of the /// final signature. #[derive(Clone, PartialEq, Eq)] -#[cfg_attr(feature = "internals", visibility::make(pub))] -#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] -pub(crate) struct GroupCommitment(pub(crate) Element); +pub struct GroupCommitment(pub(crate) Element); impl GroupCommitment where @@ -483,6 +478,12 @@ where pub(crate) fn to_element(self) -> ::Element { self.0 } + + /// Return the underlying element. + #[cfg(feature = "internals")] + pub fn from_element(element: Element) -> Self { + Self(element) + } } /// Generates the group commitment which is published as part of the joint @@ -584,12 +585,15 @@ where return Err(Error::UnknownIdentifier); } + let (signing_package, signature_shares, pubkeys) = + ::pre_aggregate(signing_package, signature_shares, pubkeys)?; + // Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the // binding factor. let binding_factor_list: BindingFactorList = - compute_binding_factor_list(signing_package, &pubkeys.verifying_key, &[])?; + compute_binding_factor_list(&signing_package, &pubkeys.verifying_key, &[])?; // Compute the group commitment from signing commitments produced in round one. - let group_commitment = compute_group_commitment(signing_package, &binding_factor_list)?; + let group_commitment = compute_group_commitment(&signing_package, &binding_factor_list)?; // The aggregation of the signature shares by summing them up, resulting in // a plain Schnorr signature. @@ -619,10 +623,10 @@ where #[cfg(feature = "cheater-detection")] if verification_result.is_err() { detect_cheater( - group_commitment, - pubkeys, - signing_package, - signature_shares, + &group_commitment, + &pubkeys, + &signing_package, + &signature_shares, &binding_factor_list, )?; } @@ -637,17 +641,17 @@ where /// Each share is verified to find the cheater #[cfg(feature = "cheater-detection")] fn detect_cheater( - group_commitment: GroupCommitment, + group_commitment: &GroupCommitment, pubkeys: &keys::PublicKeyPackage, signing_package: &SigningPackage, signature_shares: &BTreeMap, round2::SignatureShare>, binding_factor_list: &BindingFactorList, ) -> Result<(), Error> { // Compute the per-message challenge. - let challenge = crate::challenge::( + let challenge = ::challenge( &group_commitment.0, &pubkeys.verifying_key, - signing_package.message().as_slice(), + signing_package.message(), )?; // Verify the signature shares. @@ -663,6 +667,7 @@ fn detect_cheater( *identifier, signing_package, binding_factor_list, + group_commitment, signature_share, verifying_share, challenge, @@ -688,24 +693,44 @@ pub fn verify_signature_share( signing_package: &SigningPackage, verifying_key: &VerifyingKey, ) -> Result<(), Error> { + // In order to reuse `pre_aggregate()`, we need to create some "dummy" containers + let signature_shares = BTreeMap::from([(identifier, *signature_share)]); + let verifying_shares = BTreeMap::from([(identifier, *verifying_share)]); + let public_key_package = PublicKeyPackage::new(verifying_shares, *verifying_key); + + let (signing_package, signature_shares, pubkeys) = + ::pre_aggregate(signing_package, &signature_shares, &public_key_package)?; + + // Extract the processed values back from the "dummy" containers + let verifying_share = pubkeys + .verifying_shares() + .get(&identifier) + .expect("pre_aggregate() must keep the identifiers"); + let verifying_key = pubkeys.verifying_key(); + let signature_share = signature_shares + .get(&identifier) + .expect("pre_aggregate() must keep the identifiers"); + // Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the // binding factor. let binding_factor_list: BindingFactorList = - compute_binding_factor_list(signing_package, verifying_key, &[])?; + compute_binding_factor_list(&signing_package, verifying_key, &[])?; + // Compute the group commitment from signing commitments produced in round one. - let group_commitment = compute_group_commitment(signing_package, &binding_factor_list)?; + let group_commitment = compute_group_commitment(&signing_package, &binding_factor_list)?; // Compute the per-message challenge. - let challenge = crate::challenge::( - &group_commitment.to_element(), + let challenge = ::challenge( + &group_commitment.clone().to_element(), verifying_key, signing_package.message().as_slice(), )?; verify_signature_share_precomputed( identifier, - signing_package, + &signing_package, &binding_factor_list, + &group_commitment, signature_share, verifying_share, challenge, @@ -720,6 +745,7 @@ fn verify_signature_share_precomputed( signature_share_identifier: Identifier, signing_package: &SigningPackage, binding_factor_list: &BindingFactorList, + group_commitment: &GroupCommitment, signature_share: &round2::SignatureShare, verifying_share: &keys::VerifyingShare, challenge: Challenge, @@ -735,7 +761,10 @@ fn verify_signature_share_precomputed( .ok_or(Error::UnknownIdentifier)? .to_group_commitment_share(binding_factor); - signature_share.verify( + // Compute relation values to verify this signature share. + ::verify_share( + group_commitment, + signature_share, signature_share_identifier, &R_share, verifying_share, diff --git a/frost-core/src/round1.rs b/frost-core/src/round1.rs index 27e1c229..694043b2 100644 --- a/frost-core/src/round1.rs +++ b/frost-core/src/round1.rs @@ -54,10 +54,16 @@ where Self::nonce_generate_from_random_bytes(secret, random_bytes) } + /// Create a nonce from a scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] fn from_scalar(scalar: <<::Group as Group>::Field as Field>::Scalar) -> Self { Self(SerializableScalar(scalar)) } + /// Convert a nonce into a scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] pub(crate) fn to_scalar( self, ) -> <<::Group as Group>::Field as Field>::Scalar { @@ -358,6 +364,20 @@ where #[derive(Clone, Copy, PartialEq)] pub struct GroupCommitmentShare(pub(super) Element); +impl GroupCommitmentShare { + /// Create from an element. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn from_element(element: Element) -> Self { + Self(element) + } + + /// Return the underlying element. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn to_element(self) -> Element { + self.0 + } +} + /// Encode the list of group signing commitments. /// /// Implements [`encode_group_commitment_list()`] from the spec. diff --git a/frost-core/src/round2.rs b/frost-core/src/round2.rs index 7b0fd450..3d863996 100644 --- a/frost-core/src/round2.rs +++ b/frost-core/src/round2.rs @@ -4,7 +4,7 @@ use core::fmt::{self, Debug}; use crate as frost; use crate::{ - challenge, Challenge, Ciphersuite, Error, Field, Group, {round1, *}, + Challenge, Ciphersuite, Error, Field, Group, {round1, *}, }; /// A participant's signature share, which the coordinator will aggregate with all other signer's @@ -71,7 +71,8 @@ where challenge: &Challenge, ) -> Result<(), Error> { if (::generator() * self.to_scalar()) - != (group_commitment_share.0 + (verifying_share.to_element() * challenge.0 * lambda_i)) + != (group_commitment_share.to_element() + + (verifying_share.to_element() * challenge.0 * lambda_i)) { return Err(Error::InvalidSignatureShare { culprit: identifier, @@ -96,7 +97,7 @@ where /// Compute the signature share for a signing operation. #[cfg_attr(feature = "internals", visibility::make(pub))] #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] -fn compute_signature_share( +pub(super) fn compute_signature_share( signer_nonces: &round1::SigningNonces, binding_factor: BindingFactor, lambda_i: <<::Group as Group>::Field as Field>::Scalar, @@ -142,34 +143,38 @@ pub fn sign( return Err(Error::IncorrectCommitment); } + let (signing_package, signer_nonces, key_package) = + ::pre_sign(signing_package, signer_nonces, key_package)?; + // Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the // binding factor. let binding_factor_list: BindingFactorList = - compute_binding_factor_list(signing_package, &key_package.verifying_key, &[])?; + compute_binding_factor_list(&signing_package, &key_package.verifying_key, &[])?; let binding_factor: frost::BindingFactor = binding_factor_list .get(&key_package.identifier) .ok_or(Error::UnknownIdentifier)? .clone(); // Compute the group commitment from signing commitments produced in round one. - let group_commitment = compute_group_commitment(signing_package, &binding_factor_list)?; + let group_commitment = compute_group_commitment(&signing_package, &binding_factor_list)?; // Compute Lagrange coefficient. - let lambda_i = frost::derive_interpolating_value(key_package.identifier(), signing_package)?; + let lambda_i = frost::derive_interpolating_value(key_package.identifier(), &signing_package)?; // Compute the per-message challenge. - let challenge = challenge::( + let challenge = ::challenge( &group_commitment.0, &key_package.verifying_key, - signing_package.message.as_slice(), + signing_package.message(), )?; // Compute the Schnorr signature share. - let signature_share = compute_signature_share( - signer_nonces, + let signature_share = ::compute_signature_share( + &group_commitment, + &signer_nonces, binding_factor, lambda_i, - key_package, + &key_package, challenge, ); diff --git a/frost-core/src/signature.rs b/frost-core/src/signature.rs index 7150b085..91d3ade9 100644 --- a/frost-core/src/signature.rs +++ b/frost-core/src/signature.rs @@ -1,11 +1,12 @@ //! Schnorr signatures over prime order groups (or subgroups) use alloc::{string::ToString, vec::Vec}; +use derive_getters::Getters; use crate::{Ciphersuite, Element, Error, Field, Group, Scalar}; /// A Schnorr signature over some prime order group (or subgroup). -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Eq, PartialEq, Getters)] pub struct Signature { /// The commitment `R` to the signature nonce. pub(crate) R: Element, @@ -29,8 +30,10 @@ where Self { R, z } } - /// Converts bytes as [`Ciphersuite::SignatureSerialization`] into a `Signature`. - pub fn deserialize(bytes: &[u8]) -> Result> { + /// Converts default-encoded bytes as + /// [`Ciphersuite::SignatureSerialization`] into a `Signature`. + #[cfg(feature = "internals")] + pub fn default_deserialize(bytes: &[u8]) -> 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). @@ -66,8 +69,14 @@ where }) } - /// Converts this signature to its byte serialization. - pub fn serialize(&self) -> Result, Error> { + /// Converts bytes as [`Ciphersuite::SignatureSerialization`] into a `Signature`. + pub fn deserialize(bytes: &[u8]) -> Result> { + C::deserialize_signature(bytes) + } + + /// Converts this signature to its default byte serialization. + #[cfg(feature = "internals")] + pub fn default_serialize(&self) -> Result, Error> { let mut bytes = Vec::::new(); bytes.extend(::serialize(&self.R)?.as_ref()); @@ -75,6 +84,11 @@ where Ok(bytes) } + + /// Converts this signature to its byte serialization. + pub fn serialize(&self) -> Result, Error> { + ::serialize_signature(self) + } } #[cfg(feature = "serde")] diff --git a/frost-core/src/signing_key.rs b/frost-core/src/signing_key.rs index 4ba306ed..93a096c3 100644 --- a/frost-core/src/signing_key.rs +++ b/frost-core/src/signing_key.rs @@ -5,8 +5,8 @@ use alloc::vec::Vec; use rand_core::{CryptoRng, RngCore}; use crate::{ - random_nonzero, serialization::SerializableScalar, Ciphersuite, Error, Field, Group, Scalar, - Signature, VerifyingKey, + random_nonzero, serialization::SerializableScalar, Challenge, Ciphersuite, Error, Field, Group, + Scalar, Signature, VerifyingKey, }; /// A signing key for a Schnorr signature on a FROST [`Ciphersuite::Group`]. @@ -40,13 +40,20 @@ where } /// Create a signature `msg` using this `SigningKey`. - pub fn sign(&self, mut rng: R, msg: &[u8]) -> Signature { - let k = random_nonzero::(&mut rng); + pub fn sign(&self, rng: R, message: &[u8]) -> Signature { + ::single_sign(self, rng, message) + } + + /// Create a signature `msg` using this `SigningKey` using the default + /// signing. + #[cfg(feature = "internals")] + pub fn default_sign(&self, mut rng: R, message: &[u8]) -> Signature { + let public = VerifyingKey::::from(*self); - let R = ::generator() * k; + let (k, R) = ::generate_nonce(&mut rng); // Generate Schnorr challenge - let c = crate::challenge::(&R, &VerifyingKey::::from(*self), msg).expect("should not return error since that happens only if one of the inputs is the identity. R is not since k is nonzero. The verifying_key is not because signing keys are not allowed to be zero."); + let c: Challenge = ::challenge(&R, &public, message).expect("should not return error since that happens only if one of the inputs is the identity. R is not since k is nonzero. The verifying_key is not because signing keys are not allowed to be zero."); let z = k + (c.0 * self.scalar); diff --git a/frost-core/src/tests/ciphersuite_generic.rs b/frost-core/src/tests/ciphersuite_generic.rs index b41ef443..3271f26a 100644 --- a/frost-core/src/tests/ciphersuite_generic.rs +++ b/frost-core/src/tests/ciphersuite_generic.rs @@ -10,7 +10,6 @@ use crate::{ keys::PublicKeyPackage, Error, Field, Group, Identifier, Signature, SigningKey, SigningPackage, VerifyingKey, }; -use alloc::borrow::ToOwned; use alloc::vec::Vec; use rand_core::{CryptoRng, RngCore}; diff --git a/frost-core/src/traits.rs b/frost-core/src/traits.rs index 3e0d9365..4e3f959d 100644 --- a/frost-core/src/traits.rs +++ b/frost-core/src/traits.rs @@ -5,10 +5,18 @@ use core::{ ops::{Add, Mul, Sub}, }; -use alloc::vec::Vec; +use alloc::{borrow::Cow, collections::BTreeMap, vec::Vec}; use rand_core::{CryptoRng, RngCore}; -use crate::{Error, FieldError, GroupError, Signature, VerifyingKey}; +use crate::{ + challenge, + keys::{KeyPackage, PublicKeyPackage, VerifyingShare}, + random_nonzero, + round1::{self}, + round2::{self, SignatureShare}, + BindingFactor, Challenge, Error, FieldError, GroupCommitment, GroupError, Identifier, + Signature, SigningKey, SigningPackage, VerifyingKey, +}; /// A prime order finite field GF(q) over which all scalar values for our prime order group can be /// multiplied are defined. @@ -213,22 +221,205 @@ pub trait Ciphersuite: Copy + Clone + PartialEq + Debug + 'static { None } - /// Verify a signature for this ciphersuite. The default implementation uses the "cofactored" - /// equation (it multiplies by the cofactor returned by [`Group::cofactor()`]). + // The following are optional methods that allow customizing steps of the + // protocol if required. + + /// Optional. Do regular (non-FROST) signing with a [`SigningKey`]. Called + /// by [`SigningKey::sign()`]. This is not used by FROST. Can be overriden + /// if required which is useful if FROST signing has been changed by the + /// other Ciphersuite trait methods and regular signing should be changed + /// accordingly to match. + fn single_sign( + signing_key: &SigningKey, + rng: R, + message: &[u8], + ) -> Signature { + signing_key.default_sign(rng, message) + } + + /// Optional. Verify a signature for this ciphersuite. Called by + /// [`VerifyingKey::verify()`]. The default implementation uses the + /// "cofactored" equation (it multiplies by the cofactor returned by + /// [`Group::cofactor()`]). /// /// # Cryptographic Safety /// - /// You may override this to provide a tailored implementation, but if the ciphersuite defines it, - /// it must also multiply by the cofactor to comply with the RFC. Note that batch verification - /// (see [`crate::batch::Verifier`]) also uses the default implementation regardless whether a - /// tailored implementation was provided. + /// You may override this to provide a tailored implementation, but if the + /// ciphersuite defines it, it must also multiply by the cofactor to comply + /// with the RFC. Note that batch verification (see + /// [`crate::batch::Verifier`]) also uses the default implementation + /// regardless whether a tailored implementation was provided. fn verify_signature( - msg: &[u8], + message: &[u8], signature: &Signature, public_key: &VerifyingKey, ) -> Result<(), Error> { - let c = crate::challenge::(&signature.R, public_key, msg)?; + let (message, signature, public_key) = ::pre_verify(message, signature, public_key)?; + + let c = ::challenge(&signature.R, &public_key, &message)?; + + public_key.verify_prehashed(c, &signature) + } + + /// Optional. Pre-process [`round2::sign()`] inputs. The default + /// implementation returns them as-is. [`Cow`] is used so implementations + /// can choose to return the same passed reference or a modified clone. + #[allow(clippy::type_complexity)] + fn pre_sign<'a>( + signing_package: &'a SigningPackage, + signer_nonces: &'a round1::SigningNonces, + key_package: &'a KeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, round1::SigningNonces>, + Cow<'a, KeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signer_nonces), + Cow::Borrowed(key_package), + )) + } + + /// Optional. Pre-process [`crate::aggregate()`] and + /// [`crate::verify_signature_share()`] inputs. In the latter case, "dummy" + /// container BTreeMap and PublicKeyPackage are passed with the relevant + /// values. The default implementation returns them as-is. [`Cow`] is used + /// so implementations can choose to return the same passed reference or a + /// modified clone. + #[allow(clippy::type_complexity)] + fn pre_aggregate<'a>( + signing_package: &'a SigningPackage, + signature_shares: &'a BTreeMap, round2::SignatureShare>, + public_key_package: &'a PublicKeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, BTreeMap, round2::SignatureShare>>, + Cow<'a, PublicKeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signature_shares), + Cow::Borrowed(public_key_package), + )) + } + + /// Optional. Pre-process [`VerifyingKey::verify()`] inputs. The default + /// implementation returns them as-is. [`Cow`] is used so implementations + /// can choose to return the same passed reference or a modified clone. + #[allow(clippy::type_complexity)] + fn pre_verify<'a>( + msg: &'a [u8], + signature: &'a Signature, + public_key: &'a VerifyingKey, + ) -> Result< + ( + Cow<'a, [u8]>, + Cow<'a, Signature>, + Cow<'a, VerifyingKey>, + ), + Error, + > { + Ok(( + Cow::Borrowed(msg), + Cow::Borrowed(signature), + Cow::Borrowed(public_key), + )) + } + + /// Optional. Generate a nonce and a commitment to it. Used by + /// [`SigningKey`] for regular (non-FROST) signing and internally by the DKG + /// to generate proof-of-knowledge signatures. + fn generate_nonce( + rng: &mut R, + ) -> ( + <::Field as Field>::Scalar, + ::Element, + ) { + let k = random_nonzero::(rng); + let R = ::generator() * k; + (k, R) + } + + /// Optional. Generates the challenge as is required for Schnorr signatures. + /// Called by [`round2::sign()`] and [`crate::aggregate()`]. + fn challenge( + R: &Element, + verifying_key: &VerifyingKey, + message: &[u8], + ) -> Result, Error> { + challenge(R, verifying_key, message) + } + + /// Optional. Compute the signature share for a particular signer on a given + /// challenge. Called by [`round2::sign()`]. + fn compute_signature_share( + _group_commitment: &GroupCommitment, + signer_nonces: &round1::SigningNonces, + binding_factor: BindingFactor, + lambda_i: <::Field as Field>::Scalar, + key_package: &KeyPackage, + challenge: Challenge, + ) -> round2::SignatureShare { + round2::compute_signature_share( + signer_nonces, + binding_factor, + lambda_i, + key_package, + challenge, + ) + } + + /// Optional. Verify a signing share. Called by [`crate::aggregate()`] if + /// cheater detection is enabled. + fn verify_share( + _group_commitment: &GroupCommitment, + signature_share: &SignatureShare, + identifier: Identifier, + group_commitment_share: &round1::GroupCommitmentShare, + verifying_share: &VerifyingShare, + lambda_i: Scalar, + challenge: &Challenge, + ) -> Result<(), Error> { + signature_share.verify( + identifier, + group_commitment_share, + verifying_share, + lambda_i, + challenge, + ) + } + + /// Optional. Converts a signature to its + /// [`Ciphersuite::SignatureSerialization`] in bytes. + /// + /// The default implementation serializes a signature by serializing its `R` + /// point and `z` component independently, and then concatenating them. + fn serialize_signature(signature: &Signature) -> Result, Error> { + signature.default_serialize() + } + + /// Optional. Converts bytes as [`Ciphersuite::SignatureSerialization`] into + /// a `Signature`. + /// + /// The default implementation assumes the serialization is a serialized `R` + /// point followed by a serialized `z` component with no padding or extra + /// fields. + fn deserialize_signature(bytes: &[u8]) -> Result, Error> { + Signature::::default_deserialize(bytes) + } - public_key.verify_prehashed(c, signature) + /// Post-process the output of the DKG for a given participant. + fn post_dkg( + key_package: KeyPackage, + public_key_package: PublicKeyPackage, + ) -> Result<(KeyPackage, PublicKeyPackage), Error> { + Ok((key_package, public_key_package)) } } diff --git a/frost-core/src/verifying_key.rs b/frost-core/src/verifying_key.rs index c3a1cfc5..24f05401 100644 --- a/frost-core/src/verifying_key.rs +++ b/frost-core/src/verifying_key.rs @@ -51,6 +51,8 @@ where /// Verify a purported `signature` with a pre-hashed [`Challenge`] made by this verification /// key. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] pub(crate) fn verify_prehashed( &self, challenge: Challenge, diff --git a/frost-ed25519/tests/helpers/samples.json b/frost-ed25519/tests/helpers/samples.json index 3402fbe7..a61e0c4b 100644 --- a/frost-ed25519/tests/helpers/samples.json +++ b/frost-ed25519/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "2a00000000000000000000000000000000000000000000000000000000000000", + "proof_of_knowledge": "5866666666666666666666666666666666666666666666666666666666666666498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a", "element1": "5866666666666666666666666666666666666666666666666666666666666666", "element2": "c9a3f86aae465f0e56513864510f3997561fa2c9e85ea21dc2292309f3cd6022", "scalar1": "498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a" -} \ No newline at end of file +} diff --git a/frost-ed25519/tests/helpers/samples.rs b/frost-ed25519/tests/helpers/samples.rs index 6f22aed3..4d785e5c 100644 --- a/frost-ed25519/tests/helpers/samples.rs +++ b/frost-ed25519/tests/helpers/samples.rs @@ -109,17 +109,12 @@ pub fn public_key_package() -> PublicKeyPackage { /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(&serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } diff --git a/frost-ed448/tests/helpers/samples.json b/frost-ed448/tests/helpers/samples.json index 36e86286..f93c3e9c 100644 --- a/frost-ed448/tests/helpers/samples.json +++ b/frost-ed448/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "2a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "proof_of_knowledge": "14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f69004d83e51cb78150c2380ad9b3a18148166024e4c9db3cdf82466d3153aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2a00", "element1": "14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f6900", "element2": "ed8693eacdfbeada6ba0cdd1beb2bcbb98302a3a8365650db8c4d88a726de3b7d74d8835a0d76e03b0c2865020d659b38d04d74a63e905ae80", "scalar1": "4d83e51cb78150c2380ad9b3a18148166024e4c9db3cdf82466d3153aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2a00" -} \ No newline at end of file +} diff --git a/frost-ed448/tests/helpers/samples.rs b/frost-ed448/tests/helpers/samples.rs index ec556736..afb26d50 100644 --- a/frost-ed448/tests/helpers/samples.rs +++ b/frost-ed448/tests/helpers/samples.rs @@ -109,17 +109,12 @@ pub fn public_key_package() -> PublicKeyPackage { /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(&serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } diff --git a/frost-p256/tests/helpers/samples.json b/frost-p256/tests/helpers/samples.json index 928e355c..3fe4c698 100644 --- a/frost-p256/tests/helpers/samples.json +++ b/frost-p256/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "proof_of_knowledge": "036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296aaaaaaaa00000000aaaaaaaaaaaaaaaa7def51c91a0fbf034d26872ca84218e1", "element1": "036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", "element2": "037cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc47669978", "scalar1": "aaaaaaaa00000000aaaaaaaaaaaaaaaa7def51c91a0fbf034d26872ca84218e1" -} \ No newline at end of file +} diff --git a/frost-p256/tests/helpers/samples.rs b/frost-p256/tests/helpers/samples.rs index 340bc763..432af475 100644 --- a/frost-p256/tests/helpers/samples.rs +++ b/frost-p256/tests/helpers/samples.rs @@ -109,17 +109,12 @@ pub fn public_key_package() -> PublicKeyPackage { /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(&serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } diff --git a/frost-ristretto255/tests/helpers/samples.json b/frost-ristretto255/tests/helpers/samples.json index bb80d1b8..1fff1337 100644 --- a/frost-ristretto255/tests/helpers/samples.json +++ b/frost-ristretto255/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "2a00000000000000000000000000000000000000000000000000000000000000", + "proof_of_knowledge": "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a", "element1": "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76", "element2": "6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919", "scalar1": "498d4e9311420c903913a56c94a694b8aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0a" -} \ No newline at end of file +} diff --git a/frost-ristretto255/tests/helpers/samples.rs b/frost-ristretto255/tests/helpers/samples.rs index f598c53f..da9e9919 100644 --- a/frost-ristretto255/tests/helpers/samples.rs +++ b/frost-ristretto255/tests/helpers/samples.rs @@ -109,17 +109,12 @@ pub fn public_key_package() -> PublicKeyPackage { /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(&serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } diff --git a/frost-secp256k1-tr/Cargo.toml b/frost-secp256k1-tr/Cargo.toml new file mode 100644 index 00000000..99d93687 --- /dev/null +++ b/frost-secp256k1-tr/Cargo.toml @@ -0,0 +1,68 @@ +[package] +name = "frost-secp256k1-tr" +edition = "2021" +# When releasing to crates.io: +# - Update CHANGELOG.md +# - Create git tag. +version = "2.0.0-rc.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"] +description = "A Schnorr signature scheme over the secp256k1 curve that supports FROST and Taproot." + +[package.metadata.docs.rs] +features = ["serde"] +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +document-features = "0.2.7" +frost-core = { path = "../frost-core", version = "2.0.0-rc.0", default-features = false } +frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0-rc.0", default-features = false } +k256 = { version = "0.13.0", features = ["arithmetic", "expose-field", "hash2curve"], default-features = false } +serde = { version = "1.0.160", features = ["derive"], optional = true } +rand_core = "0.6" +sha2 = { version = "0.10.2", default-features = false } + +[dev-dependencies] +criterion = "0.5" +frost-core = { path = "../frost-core", version = "2.0.0-rc.0", features = ["test-impl"] } +frost-rerandomized = { path = "../frost-rerandomized", version = "2.0.0-rc.0", features = ["test-impl"] } +insta = { version = "1.31.0", features = ["yaml"] } +hex = { version = "0.4.3", default-features = false, features = ["alloc"] } +lazy_static = "1.4" +proptest = "1.0" +rand = "0.8" +rand_chacha = "0.3" +secp256k1 = "0.30.0" +serde_json = "1.0" + +[features] +nightly = [] +default = ["serialization", "cheater-detection", "std"] +#! ## Features +## Enable standard library support. +std = ["frost-core/std"] +## Enable `serde` support for types that need to be communicated. You +## can use `serde` to serialize structs with any encoder that supports +## `serde` (e.g. JSON with `serde_json`). +serde = ["frost-core/serde", "dep:serde"] +## Enable a default serialization format. Enables `serde`. +serialization = ["serde", "frost-core/serialization", "frost-rerandomized/serialization"] +## Enable cheater detection +cheater-detection = ["frost-core/cheater-detection", "frost-rerandomized/cheater-detection"] + +[lib] +# Disables non-criterion benchmark which is not used; prevents errors +# when using criterion-specific flags +bench = false + +[[bench]] +name = "bench" +harness = false diff --git a/frost-secp256k1-tr/README.md b/frost-secp256k1-tr/README.md new file mode 100644 index 00000000..f4d2205f --- /dev/null +++ b/frost-secp256k1-tr/README.md @@ -0,0 +1,121 @@ +An implementation of Schnorr signatures on the secp256k1 curve (Taproot) for both single and threshold numbers +of signers (FROST). + +## Example: key generation with trusted dealer and FROST signing + +Creating a key with a trusted dealer and splitting into shares; then signing a message +and aggregating the signature. Note that the example just simulates a distributed +scenario in a single thread and it abstracts away any communication between peers. + + +```rust +# // ANCHOR: tkg_gen +use frost_secp256k1_tr as frost; +use rand::thread_rng; +use std::collections::BTreeMap; + +let mut rng = thread_rng(); +let max_signers = 5; +let min_signers = 3; +let (shares, pubkey_package) = frost::keys::generate_with_dealer( + max_signers, + min_signers, + frost::keys::IdentifierList::Default, + &mut rng, +)?; +# // ANCHOR_END: tkg_gen + +// Verifies the secret shares from the dealer and store them in a BTreeMap. +// In practice, the KeyPackages must be sent to its respective participants +// through a confidential and authenticated channel. +let mut key_packages: BTreeMap<_, _> = BTreeMap::new(); + +for (identifier, secret_share) in shares { + # // ANCHOR: tkg_verify + let key_package = frost::keys::KeyPackage::try_from(secret_share)?; + # // ANCHOR_END: tkg_verify + key_packages.insert(identifier, key_package); +} + +let mut nonces_map = BTreeMap::new(); +let mut commitments_map = BTreeMap::new(); + +//////////////////////////////////////////////////////////////////////////// +// Round 1: generating nonces and signing commitments for each participant +//////////////////////////////////////////////////////////////////////////// + +// In practice, each iteration of this loop will be executed by its respective participant. +for participant_index in 1..=min_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + let key_package = &key_packages[&participant_identifier]; + // Generate one (1) nonce and one SigningCommitments instance for each + // participant, up to _threshold_. + # // ANCHOR: round1_commit + let (nonces, commitments) = frost::round1::commit( + key_package.signing_share(), + &mut rng, + ); + # // ANCHOR_END: round1_commit + // In practice, the nonces must be kept by the participant to use in the + // next round, while the commitment must be sent to the coordinator + // (or to every other participant if there is no coordinator) using + // an authenticated channel. + nonces_map.insert(participant_identifier, nonces); + commitments_map.insert(participant_identifier, commitments); +} + +// 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 = BTreeMap::new(); +# // ANCHOR: round2_package +let message = "message to sign".as_bytes(); +# // In practice, the SigningPackage must be sent to all participants +# // involved in the current signing (at least min_signers participants), +# // using an authenticate channel (and confidential if the message is secret). +let signing_package = frost::SigningPackage::new(commitments_map, message); +# // ANCHOR_END: round2_package + +//////////////////////////////////////////////////////////////////////////// +// Round 2: each participant generates their signature share +//////////////////////////////////////////////////////////////////////////// + +// In practice, each iteration of this loop will be executed by its respective participant. +for participant_identifier in nonces_map.keys() { + let key_package = &key_packages[participant_identifier]; + + let nonces = &nonces_map[participant_identifier]; + + // Each participant generates their signature share. + # // ANCHOR: round2_sign + let signature_share = frost::round2::sign(&signing_package, nonces, key_package)?; + # // ANCHOR_END: round2_sign + + // In practice, the signature share must be sent to the Coordinator + // using an authenticated channel. + signature_shares.insert(*participant_identifier, signature_share); +} + +//////////////////////////////////////////////////////////////////////////// +// Aggregation: collects the signing shares from all participants, +// generates the final signature. +//////////////////////////////////////////////////////////////////////////// + +// Aggregate (also verifies the signature shares) +# // ANCHOR: aggregate +let group_signature = frost::aggregate(&signing_package, &signature_shares, &pubkey_package)?; +# // ANCHOR_END: aggregate + + +// Check that the threshold signature can be verified by the group public +// key (the verification key). +# // ANCHOR: verify +let is_signature_valid = pubkey_package + .verifying_key() + .verify(message, &group_signature) + .is_ok(); +# // ANCHOR_END: verify +assert!(is_signature_valid); + +# Ok::<(), frost::Error>(()) +``` diff --git a/frost-secp256k1-tr/benches/bench.rs b/frost-secp256k1-tr/benches/bench.rs new file mode 100644 index 00000000..e9097bdd --- /dev/null +++ b/frost-secp256k1-tr/benches/bench.rs @@ -0,0 +1,19 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use rand::thread_rng; + +use frost_secp256k1_tr::*; + +fn bench_secp256k1_batch_verify(c: &mut Criterion) { + let mut rng = thread_rng(); + + frost_core::benches::bench_batch_verify::(c, "secp256k1", &mut rng); +} + +fn bench_secp256k1_sign(c: &mut Criterion) { + let mut rng = thread_rng(); + + frost_core::benches::bench_sign::(c, "secp256k1", &mut rng); +} + +criterion_group!(benches, bench_secp256k1_batch_verify, bench_secp256k1_sign); +criterion_main!(benches); diff --git a/frost-secp256k1-tr/dkg.md b/frost-secp256k1-tr/dkg.md new file mode 100644 index 00000000..217cbc99 --- /dev/null +++ b/frost-secp256k1-tr/dkg.md @@ -0,0 +1,168 @@ +# Distributed Key Generation (DKG) + +The DKG module supports generating FROST key shares in a distributed manner, +without a trusted dealer. + +Before starting, each participant needs an unique identifier, which can be built from +a `u16`. The process in which these identifiers are allocated is up to the application. + +The distributed key generation process has 3 parts, with 2 communication rounds +between them, in which each participant needs to send a "package" to every other +participant. In the first round, each participant sends the same package +(a [`round1::Package`]) to every other. In the second round, each receiver gets +their own package (a [`round2::Package`]). + +Between part 1 and 2, each participant needs to hold onto a [`round1::SecretPackage`] +that MUST be kept secret. Between part 2 and 3, each participant needs to hold +onto a [`round2::SecretPackage`]. + +After the third part, each participant will get a [`KeyPackage`] with their +long-term secret share that must be kept secret, and a [`PublicKeyPackage`] +that is public (and will be the same between all participants). With those +they can proceed to sign messages with FROST. + + +## Example + +```rust +# // ANCHOR: dkg_import +use rand::thread_rng; +use std::collections::BTreeMap; + +use frost_secp256k1_tr as frost; + +let mut rng = thread_rng(); + +let max_signers = 5; +let min_signers = 3; +# // ANCHOR_END: dkg_import + +//////////////////////////////////////////////////////////////////////////// +// Key generation, Round 1 +//////////////////////////////////////////////////////////////////////////// + +// Keep track of each participant's round 1 secret package. +// In practice each participant will keep its copy; no one +// will have all the participant's packages. +let mut round1_secret_packages = BTreeMap::new(); + +// Keep track of all round 1 packages sent to the given participant. +// This is used to simulate the broadcast; in practice the packages +// will be sent through some communication channel. +let mut received_round1_packages = BTreeMap::new(); + +// For each participant, perform the first part of the DKG protocol. +// In practice, each participant will perform this on their own environments. +for participant_index in 1..=max_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + # // ANCHOR: dkg_part1 + let (round1_secret_package, round1_package) = frost::keys::dkg::part1( + participant_identifier, + max_signers, + min_signers, + &mut rng, + )?; + # // ANCHOR_END: dkg_part1 + + // Store the participant's secret package for later use. + // In practice each participant will store it in their own environment. + round1_secret_packages.insert(participant_identifier, round1_secret_package); + + // "Send" the round 1 package to all other participants. In this + // test this is simulated using a BTreeMap; in practice this will be + // sent through some communication channel. + for receiver_participant_index in 1..=max_signers { + if receiver_participant_index == participant_index { + continue; + } + let receiver_participant_identifier: frost::Identifier = receiver_participant_index + .try_into() + .expect("should be nonzero"); + received_round1_packages + .entry(receiver_participant_identifier) + .or_insert_with(BTreeMap::new) + .insert(participant_identifier, round1_package.clone()); + } +} + +//////////////////////////////////////////////////////////////////////////// +// Key generation, Round 2 +//////////////////////////////////////////////////////////////////////////// + +// Keep track of each participant's round 2 secret package. +// In practice each participant will keep its copy; no one +// will have all the participant's packages. +let mut round2_secret_packages = BTreeMap::new(); + +// Keep track of all round 2 packages sent to the given participant. +// This is used to simulate the broadcast; in practice the packages +// will be sent through some communication channel. +let mut received_round2_packages = BTreeMap::new(); + +// For each participant, perform the second part of the DKG protocol. +// In practice, each participant will perform this on their own environments. +for participant_index in 1..=max_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + let round1_secret_package = round1_secret_packages + .remove(&participant_identifier) + .unwrap(); + let round1_packages = &received_round1_packages[&participant_identifier]; + # // ANCHOR: dkg_part2 + let (round2_secret_package, round2_packages) = + frost::keys::dkg::part2(round1_secret_package, round1_packages)?; + # // ANCHOR_END: dkg_part2 + + // Store the participant's secret package for later use. + // In practice each participant will store it in their own environment. + round2_secret_packages.insert(participant_identifier, round2_secret_package); + + // "Send" the round 2 package to all other participants. In this + // test this is simulated using a BTreeMap; in practice this will be + // sent through some communication channel. + // Note that, in contrast to the previous part, here each other participant + // gets its own specific package. + for (receiver_identifier, round2_package) in round2_packages { + received_round2_packages + .entry(receiver_identifier) + .or_insert_with(BTreeMap::new) + .insert(participant_identifier, round2_package); + } +} + +//////////////////////////////////////////////////////////////////////////// +// Key generation, final computation +//////////////////////////////////////////////////////////////////////////// + +// Keep track of each participant's long-lived key package. +// In practice each participant will keep its copy; no one +// will have all the participant's packages. +let mut key_packages = BTreeMap::new(); + +// Keep track of each participant's public key package. +// In practice, if there is a Coordinator, only they need to store the set. +// If there is not, then all candidates must store their own sets. +// All participants will have the same exact public key package. +let mut pubkey_packages = BTreeMap::new(); + +// For each participant, perform the third part of the DKG protocol. +// In practice, each participant will perform this on their own environments. +for participant_index in 1..=max_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + let round2_secret_package = &round2_secret_packages[&participant_identifier]; + let round1_packages = &received_round1_packages[&participant_identifier]; + let round2_packages = &received_round2_packages[&participant_identifier]; + # // ANCHOR: dkg_part3 + let (key_package, pubkey_package) = frost::keys::dkg::part3( + round2_secret_package, + round1_packages, + round2_packages, + )?; + # // ANCHOR_END: dkg_part3 + key_packages.insert(participant_identifier, key_package); + pubkey_packages.insert(participant_identifier, pubkey_package); +} + +// With its own key package and the pubkey package, each participant can now proceed +// to sign with FROST. +# Ok::<(), frost::Error>(()) +``` diff --git a/frost-secp256k1-tr/src/keys/dkg.rs b/frost-secp256k1-tr/src/keys/dkg.rs new file mode 100644 index 00000000..9ea40b26 --- /dev/null +++ b/frost-secp256k1-tr/src/keys/dkg.rs @@ -0,0 +1,103 @@ +#![doc = include_str!("../../dkg.md")] +use super::*; + +/// DKG Round 1 structures. +pub mod round1 { + use super::*; + + /// The secret package that must be kept in memory by the participant + /// between the first and second parts of the DKG protocol (round 1). + /// + /// # Security + /// + /// This package MUST NOT be sent to other participants! + pub type SecretPackage = frost::keys::dkg::round1::SecretPackage; + + /// The package that must be broadcast by each participant to all other participants + /// between the first and second parts of the DKG protocol (round 1). + pub type Package = frost::keys::dkg::round1::Package; +} + +/// DKG Round 2 structures. +pub mod round2 { + use super::*; + + /// The secret package that must be kept in memory by the participant + /// between the second and third parts of the DKG protocol (round 2). + /// + /// # Security + /// + /// This package MUST NOT be sent to other participants! + pub type SecretPackage = frost::keys::dkg::round2::SecretPackage; + + /// A package that must be sent by each participant to some other participants + /// in Round 2 of the DKG protocol. Note that there is one specific package + /// for each specific recipient, in contrast to Round 1. + /// + /// # Security + /// + /// The package must be sent on an *confidential* and *authenticated* channel. + pub type Package = frost::keys::dkg::round2::Package; +} + +/// Performs the first part of the distributed key generation protocol +/// for the given participant. +/// +/// It returns the [`round1::SecretPackage`] that must be kept in memory +/// by the participant for the other steps, and the [`round1::Package`] that +/// must be sent to each other participant in the DKG run. +pub fn part1( + identifier: Identifier, + max_signers: u16, + min_signers: u16, + mut rng: R, +) -> Result<(round1::SecretPackage, round1::Package), Error> { + frost::keys::dkg::part1(identifier, max_signers, min_signers, &mut rng) +} + +/// Performs the second part of the distributed key generation protocol for the +/// participant holding the given [`round1::SecretPackage`], given the received +/// [`round1::Package`]s received from the other participants. +/// +/// `round1_packages` maps the identifier of each other participant to the +/// [`round1::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round1::Package`] came from the participant +/// with that identifier. +/// +/// It returns the [`round2::SecretPackage`] that must be kept in memory by the +/// participant for the final step, and the map of [`round2::Package`]s that +/// must be sent to each other participant who has the given identifier in the +/// map key. +pub fn part2( + secret_package: round1::SecretPackage, + round1_packages: &BTreeMap, +) -> Result<(round2::SecretPackage, BTreeMap), Error> { + frost::keys::dkg::part2(secret_package, round1_packages) +} + +/// Performs the third and final part of the distributed key generation protocol +/// for the participant holding the given [`round2::SecretPackage`], given the +/// received [`round1::Package`]s and [`round2::Package`]s received from the +/// other participants. +/// +/// `round1_packages` must be the same used in [`part2()`]. +/// +/// `round2_packages` maps the identifier of each other participant to the +/// [`round2::Package`] they sent to the current participant (the owner of +/// `secret_package`). These identifiers must come from whatever mapping the +/// coordinator has between communication channels and participants, i.e. they +/// must have assurance that the [`round2::Package`] came from the participant +/// with that identifier. +/// +/// It returns the [`KeyPackage`] that has the long-lived key share for the +/// participant, and the [`PublicKeyPackage`]s that has public information about +/// all participants; both of which are required to compute FROST signatures. +pub fn part3( + round2_secret_package: &round2::SecretPackage, + round1_packages: &BTreeMap, + round2_packages: &BTreeMap, +) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::dkg::part3(round2_secret_package, round1_packages, round2_packages) +} diff --git a/frost-secp256k1-tr/src/keys/refresh.rs b/frost-secp256k1-tr/src/keys/refresh.rs new file mode 100644 index 00000000..c270fc20 --- /dev/null +++ b/frost-secp256k1-tr/src/keys/refresh.rs @@ -0,0 +1,35 @@ +//! Refresh Shares +//! +//! Implements the functionality to refresh a share. This requires the participation +//! of all the remaining signers. This can be done using a Trusted Dealer or +//! DKG (not yet implemented) + +use crate::{frost, Ciphersuite, CryptoRng, Error, Identifier, RngCore}; +use alloc::vec::Vec; + +use super::{KeyPackage, PublicKeyPackage, SecretShare}; + +/// Refreshes shares using a trusted dealer +pub fn compute_refreshing_shares( + old_pub_key_package: PublicKeyPackage, + max_signers: u16, + min_signers: u16, + identifiers: &[Identifier], + mut rng: &mut R, +) -> Result<(Vec, PublicKeyPackage), Error> { + frost::keys::refresh::compute_refreshing_shares( + old_pub_key_package, + max_signers, + min_signers, + identifiers, + &mut rng, + ) +} + +/// Each participant refreshed their shares +pub fn refresh_share( + zero_share: SecretShare, + current_share: &KeyPackage, +) -> Result { + frost::keys::refresh::refresh_share(zero_share, current_share) +} diff --git a/frost-secp256k1-tr/src/keys/repairable.rs b/frost-secp256k1-tr/src/keys/repairable.rs new file mode 100644 index 00000000..9b538030 --- /dev/null +++ b/frost-secp256k1-tr/src/keys/repairable.rs @@ -0,0 +1,103 @@ +//! Repairable Threshold Scheme +//! +//! Implements the Repairable Threshold Scheme (RTS) from . +//! The RTS is used to help a signer (participant) repair their lost share. This is achieved +//! using a subset of the other signers know here as `helpers`. + +use alloc::collections::BTreeMap; + +// This is imported separately to make `gencode` work. +// (if it were below, the position of the import would vary between ciphersuites +// after `cargo fmt`) +use crate::{frost, Ciphersuite, CryptoRng, Identifier, RngCore, Scalar}; +use crate::{Error, Secp256K1Sha256TR}; + +use super::{SecretShare, VerifiableSecretSharingCommitment}; + +/// Step 1 of RTS. +/// +/// Generates the "delta" values from `helper_i` to help `participant` recover their share +/// where `helpers` contains the identifiers of all the helpers (including `helper_i`), and `share_i` +/// is the share of `helper_i`. +/// +/// Returns a BTreeMap mapping which value should be sent to which participant. +pub fn repair_share_step_1( + helpers: &[Identifier], + share_i: &SecretShare, + rng: &mut R, + participant: Identifier, +) -> Result, Error> { + frost::keys::repairable::repair_share_step_1(helpers, share_i, rng, participant) +} + +/// Step 2 of RTS. +/// +/// Generates the `sigma` values from all `deltas` received from `helpers` +/// to help `participant` recover their share. +/// `sigma` is the sum of all received `delta` and the `delta_i` generated for `helper_i`. +/// +/// Returns a scalar +pub fn repair_share_step_2(deltas_j: &[Scalar]) -> Scalar { + frost::keys::repairable::repair_share_step_2::(deltas_j) +} + +/// Step 3 of RTS +/// +/// The `participant` sums all `sigma_j` received to compute the `share`. The `SecretShare` +/// is made up of the `identifier`and `commitment` of the `participant` as well as the +/// `value` which is the `SigningShare`. +pub fn repair_share_step_3( + sigmas: &[Scalar], + identifier: Identifier, + commitment: &VerifiableSecretSharingCommitment, +) -> SecretShare { + frost::keys::repairable::repair_share_step_3(sigmas, identifier, commitment) +} + +#[cfg(test)] +mod tests { + + use lazy_static::lazy_static; + use rand::thread_rng; + use serde_json::Value; + + use crate::Secp256K1Sha256TR; + + lazy_static! { + pub static ref REPAIR_SHARE: Value = + serde_json::from_str(include_str!("../../tests/helpers/repair-share.json").trim()) + .unwrap(); + } + + #[test] + fn check_repair_share_step_1() { + let rng = thread_rng(); + + frost_core::tests::repairable::check_repair_share_step_1::(rng); + } + + #[test] + fn check_repair_share_step_2() { + frost_core::tests::repairable::check_repair_share_step_2::( + &REPAIR_SHARE, + ); + } + + #[test] + fn check_repair_share_step_3() { + let rng = thread_rng(); + frost_core::tests::repairable::check_repair_share_step_3::( + rng, + &REPAIR_SHARE, + ); + } + + #[test] + fn check_repair_share_step_1_fails_with_invalid_min_signers() { + let rng = thread_rng(); + frost_core::tests::repairable::check_repair_share_step_1_fails_with_invalid_min_signers::< + Secp256K1Sha256TR, + _, + >(rng); + } +} diff --git a/frost-secp256k1-tr/src/lib.rs b/frost-secp256k1-tr/src/lib.rs new file mode 100644 index 00000000..56aa52ca --- /dev/null +++ b/frost-secp256k1-tr/src/lib.rs @@ -0,0 +1,923 @@ +#![allow(non_snake_case)] +#![deny(missing_docs)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc = include_str!("../README.md")] +#![doc = document_features::document_features!()] + +extern crate alloc; + +use alloc::borrow::Cow; +use alloc::borrow::ToOwned; +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use frost_rerandomized::RandomizedCiphersuite; +use k256::{ + elliptic_curve::{ + bigint::U256, + group::prime::PrimeCurveAffine, + hash2curve::{hash_to_field, ExpandMsgXmd}, + point::AffineCoordinates, + sec1::{FromEncodedPoint, ToEncodedPoint}, + Field as FFField, PrimeField, ScalarPrimitive, + }, + AffinePoint, ProjectivePoint, Scalar, +}; +use rand_core::{CryptoRng, RngCore}; +use sha2::{Digest, Sha256}; + +use frost_core::{self as frost, random_nonzero}; + +use keys::EvenY; +use keys::Tweak; + +#[cfg(test)] +mod tests; + +// Re-exports in our public API +#[cfg(feature = "serde")] +pub use frost_core::serde; +pub use frost_core::{ + Challenge, Ciphersuite, Element, Field, FieldError, Group, GroupCommitment, GroupError, +}; +pub use rand_core; + +/// An error. +pub type Error = frost_core::Error; + +/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite scalar field. +#[derive(Clone, Copy)] +pub struct Secp256K1ScalarField; + +impl Field for Secp256K1ScalarField { + type Scalar = Scalar; + + type Serialization = [u8; 32]; + + fn zero() -> Self::Scalar { + Scalar::ZERO + } + + fn one() -> Self::Scalar { + Scalar::ONE + } + + fn invert(scalar: &Self::Scalar) -> Result { + // [`Scalar`]'s Eq/PartialEq does a constant-time comparison + if *scalar == ::zero() { + Err(FieldError::InvalidZeroScalar) + } else { + Ok(scalar.invert().unwrap()) + } + } + + fn random(rng: &mut R) -> Self::Scalar { + Scalar::random(rng) + } + + fn serialize(scalar: &Self::Scalar) -> Self::Serialization { + scalar.to_bytes().into() + } + + fn deserialize(buf: &Self::Serialization) -> Result { + let field_bytes: &k256::FieldBytes = buf.into(); + match Scalar::from_repr(*field_bytes).into() { + Some(s) => Ok(s), + None => Err(FieldError::MalformedScalar), + } + } + + fn little_endian_serialize(scalar: &Self::Scalar) -> Self::Serialization { + let mut array = Self::serialize(scalar); + array.reverse(); + array + } +} + +/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite group. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Secp256K1Group; + +impl Group for Secp256K1Group { + type Field = Secp256K1ScalarField; + + type Element = ProjectivePoint; + + /// [SEC 1][1] serialization of a compressed point in secp256k1 takes 33 bytes + /// (1-byte prefix and 32 bytes for the coordinate). + /// + /// Note that, in the SEC 1 spec, the identity is encoded as a single null byte; + /// but here we pad with zeroes. This is acceptable as the identity _should_ never + /// be serialized in FROST, else we error. + /// + /// [1]: https://secg.org/sec1-v2.pdf + type Serialization = [u8; 33]; + + fn cofactor() -> ::Scalar { + Scalar::ONE + } + + fn identity() -> Self::Element { + ProjectivePoint::IDENTITY + } + + fn generator() -> Self::Element { + ProjectivePoint::GENERATOR + } + + fn serialize(element: &Self::Element) -> Result { + if *element == Self::identity() { + return Err(GroupError::InvalidIdentityElement); + } + let mut fixed_serialized = [0; 33]; + let serialized_point = element.to_affine().to_encoded_point(true); + let serialized = serialized_point.as_bytes(); + fixed_serialized.copy_from_slice(serialized); + Ok(fixed_serialized) + } + + fn deserialize(buf: &Self::Serialization) -> Result { + let encoded_point = + k256::EncodedPoint::from_bytes(buf).map_err(|_| GroupError::MalformedElement)?; + + match Option::::from(AffinePoint::from_encoded_point(&encoded_point)) { + Some(point) => { + if point.is_identity().into() { + // This is actually impossible since the identity is encoded a a single byte + // which will never happen since we receive a 33-byte buffer. + // We leave the check for consistency. + Err(GroupError::InvalidIdentityElement) + } else { + Ok(ProjectivePoint::from(point)) + } + } + None => Err(GroupError::MalformedElement), + } + } +} + +fn hash_to_array(inputs: &[&[u8]]) -> [u8; 32] { + let mut h = Sha256::new(); + for i in inputs { + h.update(i); + } + let mut output = [0u8; 32]; + output.copy_from_slice(h.finalize().as_slice()); + output +} + +fn hash_to_scalar(domain: &[u8], msg: &[u8]) -> Scalar { + let mut u = [Secp256K1ScalarField::zero()]; + hash_to_field::, Scalar>(&[msg], &[domain], &mut u) + .expect("should never return error according to error cases described in ExpandMsgXmd"); + u[0] +} + +/// Context string from the ciphersuite in the [spec]. +/// +/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-1 +const CONTEXT_STRING: &str = "FROST-secp256k1-SHA256-TR-v1"; + +/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Secp256K1Sha256TR; + +/// Digest the hasher to a Scalar +fn hasher_to_scalar(hasher: Sha256) -> Scalar { + let sp = ScalarPrimitive::new(U256::from_be_slice(&hasher.finalize())).unwrap(); + Scalar::from(&sp) +} + +/// Create a BIP340 compliant tagged hash +fn tagged_hash(tag: &str) -> Sha256 { + let mut hasher = Sha256::new(); + let mut tag_hasher = Sha256::new(); + tag_hasher.update(tag.as_bytes()); + let tag_hash = tag_hasher.finalize(); + hasher.update(tag_hash); + hasher.update(tag_hash); + hasher +} + +/// Create a BIP341 compliant taproot tweak +fn tweak>( + public_key: &<::Group as Group>::Element, + merkle_root: Option, +) -> Scalar { + match merkle_root { + None => Secp256K1ScalarField::zero(), + Some(root) => { + let mut hasher = tagged_hash("TapTweak"); + hasher.update(public_key.to_affine().x()); + hasher.update(root.as_ref()); + hasher_to_scalar(hasher) + } + } +} + +// Negate a Nonce +fn negate_nonce(nonce: &frost_core::round1::Nonce) -> frost_core::round1::Nonce { + frost_core::round1::Nonce::::from_scalar(-nonce.to_scalar()) +} + +// Negate a SigningNonces +fn negate_nonces(signing_nonces: &round1::SigningNonces) -> round1::SigningNonces { + // TODO: this recomputes commitments which is expensive, and not needed. + // Create an `internals` SigningNonces::from_nonces_and_commitments or + // something similar. + round1::SigningNonces::from_nonces( + negate_nonce(signing_nonces.hiding()), + negate_nonce(signing_nonces.binding()), + ) +} + +impl Ciphersuite for Secp256K1Sha256TR { + const ID: &'static str = CONTEXT_STRING; + + type Group = Secp256K1Group; + + type HashOutput = [u8; 32]; + + type SignatureSerialization = [u8; 64]; + + /// H1 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.1 + fn H1(m: &[u8]) -> <::Field as Field>::Scalar { + hash_to_scalar((CONTEXT_STRING.to_owned() + "rho").as_bytes(), m) + } + + /// H2 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.2 + fn H2(m: &[u8]) -> <::Field as Field>::Scalar { + let mut hasher = tagged_hash("BIP0340/challenge"); + hasher.update(m); + hasher_to_scalar(hasher) + } + + /// H3 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.3 + fn H3(m: &[u8]) -> <::Field as Field>::Scalar { + hash_to_scalar((CONTEXT_STRING.to_owned() + "nonce").as_bytes(), m) + } + + /// H4 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.4 + fn H4(m: &[u8]) -> Self::HashOutput { + hash_to_array(&[CONTEXT_STRING.as_bytes(), b"msg", m]) + } + + /// H5 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.5 + fn H5(m: &[u8]) -> Self::HashOutput { + hash_to_array(&[CONTEXT_STRING.as_bytes(), b"com", m]) + } + + /// HDKG for FROST(secp256k1, SHA-256) + fn HDKG(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar( + (CONTEXT_STRING.to_owned() + "dkg").as_bytes(), + m, + )) + } + + /// HID for FROST(secp256k1, SHA-256) + fn HID(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar( + (CONTEXT_STRING.to_owned() + "id").as_bytes(), + m, + )) + } + + // Sign, negating the key if required by BIP-340. + fn single_sign( + signing_key: &SigningKey, + rng: R, + message: &[u8], + ) -> Signature { + let signing_key = signing_key.into_even_y(None); + signing_key.default_sign(rng, message) + } + + // Preprocess sign inputs, negating the keys in the KeyPackage if required + // by BIP-340. + fn pre_sign<'a>( + signing_package: &'a SigningPackage, + signer_nonces: &'a round1::SigningNonces, + key_package: &'a keys::KeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, round1::SigningNonces>, + Cow<'a, keys::KeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signer_nonces), + Cow::Owned(key_package.clone().into_even_y(None)), + )) + } + + // Preprocess sign inputs, negating the keys in the PublicKeyPackage if + // required by BIP-340. + fn pre_aggregate<'a>( + signing_package: &'a SigningPackage, + signature_shares: &'a BTreeMap, + public_key_package: &'a keys::PublicKeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, BTreeMap>, + Cow<'a, keys::PublicKeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signature_shares), + Cow::Owned(public_key_package.clone().into_even_y(None)), + )) + } + + // Preprocess verify inputs, negating the VerifyingKey and `signature.R` if required by + // BIP-340. + fn pre_verify<'a>( + message: &'a [u8], + signature: &'a Signature, + public_key: &'a VerifyingKey, + ) -> Result<(Cow<'a, [u8]>, Cow<'a, Signature>, Cow<'a, VerifyingKey>), Error> { + let public_key = public_key.into_even_y(None); + let signature = signature.into_even_y(None); + Ok(( + Cow::Borrowed(message), + Cow::Owned(signature), + Cow::Owned(public_key), + )) + } + + // Generate a nonce, negating it if required by BIP-340. + fn generate_nonce( + rng: &mut R, + ) -> ( + <::Field as Field>::Scalar, + ::Element, + ) { + let k = random_nonzero::(rng); + let R = ::generator() * k; + if R.to_affine().y_is_odd().into() { + (-k, -R) + } else { + (k, R) + } + } + + // Compute the challenge. Per BIP-340, only the X coordinate of R and + // verifying_key are hashed, unlike vanilla FROST. + fn challenge( + R: &Element, + verifying_key: &VerifyingKey, + message: &[u8], + ) -> Result, Error> { + let mut preimage = vec![]; + preimage.extend_from_slice(&R.to_affine().x()); + preimage.extend_from_slice(&verifying_key.to_element().to_affine().x()); + preimage.extend_from_slice(message); + Ok(Challenge::from_scalar(S::H2(&preimage[..]))) + } + + /// Compute a signature share, negating the nonces if required by BIP-340. + fn compute_signature_share( + group_commitment: &GroupCommitment, + signer_nonces: &round1::SigningNonces, + binding_factor: frost::BindingFactor, + lambda_i: <::Field as Field>::Scalar, + key_package: &frost::keys::KeyPackage, + challenge: Challenge, + ) -> round2::SignatureShare { + let signer_nonces = if !group_commitment.has_even_y() { + negate_nonces(signer_nonces) + } else { + signer_nonces.clone() + }; + + frost::round2::compute_signature_share( + &signer_nonces, + binding_factor, + lambda_i, + key_package, + challenge, + ) + } + + /// Verify a signature share, negating the group commitment share if + /// required by BIP-340. + fn verify_share( + group_commitment: &GroupCommitment, + signature_share: &frost_core::round2::SignatureShare, + identifier: Identifier, + group_commitment_share: &frost_core::round1::GroupCommitmentShare, + verifying_share: &frost_core::keys::VerifyingShare, + lambda_i: Scalar, + challenge: &Challenge, + ) -> Result<(), Error> { + let group_commitment_share = if !group_commitment.has_even_y() { + frost_core::round1::GroupCommitmentShare::from_element( + -group_commitment_share.to_element(), + ) + } else { + *group_commitment_share + }; + signature_share.verify( + identifier, + &group_commitment_share, + verifying_share, + lambda_i, + challenge, + ) + } + + /// Serialize a signature in compact BIP340 format, with an x-only R point. + fn serialize_signature(signature: &Signature) -> Result, Error> { + let R_bytes = Self::Group::serialize(signature.R())?; + let z_bytes = ::Field::serialize(signature.z()); + + let mut bytes = vec![0u8; 64]; + bytes[..32].copy_from_slice(&R_bytes[1..]); + bytes[32..].copy_from_slice(&z_bytes); + Ok(bytes) + } + + /// Deserialize a signature in compact BIP340 format, with an x-only R point. + fn deserialize_signature(bytes: &[u8]) -> Result { + if bytes.len() != 64 { + return Err(Error::MalformedSignature); + } + + let mut R_bytes = [0u8; 33]; + R_bytes[0] = 0x02; // taproot signatures always have an even R point + R_bytes[1..].copy_from_slice(&bytes[..32]); + + let mut z_bytes = [0u8; 32]; + z_bytes.copy_from_slice(&bytes[32..]); + + let R = Self::Group::deserialize(&R_bytes)?; + let z = ::Field::deserialize(&z_bytes)?; + + Ok(Signature::new(R, z)) + } + + /// Post-process the DKG output. We add an unusable taproot tweak to the + /// group key computed by a DKG run, to prevent peers from inserting rogue + /// tapscript tweaks into the group's joint public key. + fn post_dkg( + key_package: keys::KeyPackage, + public_key_package: keys::PublicKeyPackage, + ) -> Result<(keys::KeyPackage, keys::PublicKeyPackage), Error> { + // From BIP-341: + // > If the spending conditions do not require a script path, the output + // > key should commit to an unspendable script path instead of having + // > no script path. This can be achieved by computing the output key + // > point as Q = P + int(hashTapTweak(bytes(P)))G. + let merkle_root = [0u8; 0]; + Ok(( + key_package.tweak(Some(&merkle_root)), + public_key_package.tweak(Some(&merkle_root)), + )) + } +} + +impl RandomizedCiphersuite for Secp256K1Sha256TR { + fn hash_randomizer(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar( + (CONTEXT_STRING.to_owned() + "randomizer").as_bytes(), + m, + )) + } +} + +type S = Secp256K1Sha256TR; + +/// A FROST(secp256k1, SHA-256) participant identifier. +pub type Identifier = frost::Identifier; + +/// FROST(secp256k1, SHA-256) keys, key generation, key shares. +pub mod keys { + use super::*; + use std::collections::BTreeMap; + + /// The identifier list to use when generating key shares. + pub type IdentifierList<'a> = frost::keys::IdentifierList<'a, S>; + + /// Allows all participants' keys to be generated using a central, trusted + /// dealer. + pub fn generate_with_dealer( + max_signers: u16, + min_signers: u16, + identifiers: IdentifierList, + mut rng: RNG, + ) -> Result<(BTreeMap, PublicKeyPackage), Error> { + frost::keys::generate_with_dealer(max_signers, min_signers, identifiers, &mut rng) + } + + /// Splits an existing key into FROST shares. + /// + /// This is identical to [`generate_with_dealer`] but receives an existing key + /// instead of generating a fresh one. This is useful in scenarios where + /// the key needs to be generated externally or must be derived from e.g. a + /// seed phrase. + pub fn split( + secret: &SigningKey, + max_signers: u16, + min_signers: u16, + identifiers: IdentifierList, + rng: &mut R, + ) -> Result<(BTreeMap, PublicKeyPackage), Error> { + frost::keys::split(secret, max_signers, min_signers, identifiers, rng) + } + + /// Recompute the secret from t-of-n secret shares using Lagrange interpolation. + /// + /// This can be used if for some reason the original key must be restored; e.g. + /// if threshold signing is not required anymore. + /// + /// This is NOT required to sign with FROST; the whole point of FROST is being + /// able to generate signatures only using the shares, without having to + /// reconstruct the original key. + /// + /// The caller is responsible for providing at least `min_signers` shares; + /// if less than that is provided, a different key will be returned. + pub fn reconstruct(secret_shares: &[KeyPackage]) -> Result { + frost::keys::reconstruct(secret_shares) + } + + /// Secret and public key material generated by a dealer performing + /// [`generate_with_dealer`]. + /// + /// # Security + /// + /// To derive a FROST(secp256k1, SHA-256) keypair, the receiver of the [`SecretShare`] *must* call + /// .into(), which under the hood also performs validation. + pub type SecretShare = frost::keys::SecretShare; + + /// A secret scalar value representing a signer's share of the group secret. + pub type SigningShare = frost::keys::SigningShare; + + /// A public group element that represents a single signer's public verification share. + pub type VerifyingShare = frost::keys::VerifyingShare; + + /// A FROST(secp256k1, SHA-256) keypair, which can be generated either by a trusted dealer or using + /// a DKG. + /// + /// When using a central dealer, [`SecretShare`]s are distributed to + /// participants, who then perform verification, before deriving + /// [`KeyPackage`]s, which they store to later use during signing. + pub type KeyPackage = frost::keys::KeyPackage; + + /// Public data that contains all the signers' public keys as well as the + /// group public key. + /// + /// Used for verification purposes before publishing a signature. + pub type PublicKeyPackage = frost::keys::PublicKeyPackage; + + /// Contains the commitments to the coefficients for our secret polynomial _f_, + /// used to generate participants' key shares. + /// + /// [`VerifiableSecretSharingCommitment`] contains a set of commitments to the coefficients (which + /// themselves are scalars) for a secret polynomial f, where f is used to + /// generate each ith participant's key share f(i). Participants use this set of + /// commitments to perform verifiable secret sharing. + /// + /// Note that participants MUST be assured that they have the *same* + /// [`VerifiableSecretSharingCommitment`], either by performing pairwise comparison, or by using + /// some agreed-upon public location for publication, where each participant can + /// ensure that they received the correct (and same) value. + pub type VerifiableSecretSharingCommitment = frost::keys::VerifiableSecretSharingCommitment; + + /// Trait for ensuring the group public key has an even Y coordinate. + /// + /// In BIP-320, public keys are encoded with only the X coordinate, which + /// means that two Y coordinates are possible. The specification says that + /// the coordinate which is even must be used. Alternatively, something + /// equivalent can be accomplished by simply converting any existing + /// (non-encoded) public key to have an even Y coordinate. + /// + /// This trait is used to enable this procedure, by changing the private and + /// public keys to ensure that the public key has a even Y coordinate. This + /// is done by simply negating both keys if Y is even (in a field, negating + /// is equivalent to computing p - x where p is the prime modulus. Since p + /// is odd, if x is odd then the result will be even). Fortunately this + /// works even after Shamir secret sharing, in the individual signing and + /// verifying shares, since it's linear. + pub trait EvenY { + /// Return if the given type has a group public key with an even Y + /// coordinate. + fn has_even_y(&self) -> bool; + + /// Convert the given type to make sure the group public key has an even + /// Y coordinate. `is_even` can be specified if evenness was already + /// determined beforehand. + fn into_even_y(self, is_even: Option) -> Self; + } + + impl EvenY for PublicKeyPackage { + fn has_even_y(&self) -> bool { + let verifying_key = self.verifying_key(); + (!verifying_key.to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + // Negate verifying key + let verifying_key = VerifyingKey::new(-self.verifying_key().to_element()); + // Recreate verifying share map with negated VerifyingShares + // values. + let verifying_shares: BTreeMap<_, _> = self + .verifying_shares() + .iter() + .map(|(i, vs)| { + let vs = VerifyingShare::new(-vs.to_element()); + (*i, vs) + }) + .collect(); + PublicKeyPackage::new(verifying_shares, verifying_key) + } else { + self + } + } + } + + impl EvenY for KeyPackage { + fn has_even_y(&self) -> bool { + let verifying_key = self.verifying_key(); + (!verifying_key.to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + // Negate all components + let verifying_key = VerifyingKey::new(-self.verifying_key().to_element()); + let signing_share = SigningShare::new(-self.signing_share().to_scalar()); + let verifying_share = VerifyingShare::new(-self.verifying_share().to_element()); + KeyPackage::new( + *self.identifier(), + signing_share, + verifying_share, + verifying_key, + *self.min_signers(), + ) + } else { + self + } + } + } + + impl EvenY for VerifyingKey { + fn has_even_y(&self) -> bool { + (!self.to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + VerifyingKey::new(-self.to_element()) + } else { + self + } + } + } + + impl EvenY for GroupCommitment { + fn has_even_y(&self) -> bool { + (!self.clone().to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + Self::from_element(-self.to_element()) + } else { + self + } + } + } + + impl EvenY for Signature { + fn has_even_y(&self) -> bool { + (!self.R().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + Self::new(-*self.R(), *self.z()) + } else { + self + } + } + } + + impl EvenY for SigningKey { + fn has_even_y(&self) -> bool { + (!Into::::into(self) + .to_element() + .to_affine() + .y_is_odd()) + .into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + SigningKey::from_scalar(-self.to_scalar()) + .expect("the original SigningKey must be nonzero") + } else { + self + } + } + } + + /// Trait for tweaking a key component following BIP-341 + pub trait Tweak: EvenY { + /// Convert the given type to add a tweak. + fn tweak>(self, merkle_root: Option) -> Self; + } + + impl Tweak for PublicKeyPackage { + fn tweak>(self, merkle_root: Option) -> Self { + let t = tweak(&self.verifying_key().to_element(), merkle_root); + let tp = ProjectivePoint::GENERATOR * t; + let public_key_package = self.into_even_y(None); + let verifying_key = + VerifyingKey::new(public_key_package.verifying_key().to_element() + tp); + // Recreate verifying share map with negated VerifyingShares + // values. + let verifying_shares: BTreeMap<_, _> = public_key_package + .verifying_shares() + .iter() + .map(|(i, vs)| { + let vs = VerifyingShare::new(vs.to_element() + tp); + (*i, vs) + }) + .collect(); + PublicKeyPackage::new(verifying_shares, verifying_key) + } + } + + impl Tweak for KeyPackage { + fn tweak>(self, merkle_root: Option) -> Self { + let t = tweak(&self.verifying_key().to_element(), merkle_root); + let tp = ProjectivePoint::GENERATOR * t; + let key_package = self.into_even_y(None); + let verifying_key = VerifyingKey::new(key_package.verifying_key().to_element() + tp); + let signing_share = SigningShare::new(key_package.signing_share().to_scalar() + t); + let verifying_share = + VerifyingShare::new(key_package.verifying_share().to_element() + tp); + KeyPackage::new( + *key_package.identifier(), + signing_share, + verifying_share, + verifying_key, + *key_package.min_signers(), + ) + } + } + + pub mod dkg; + pub mod repairable; +} + +/// FROST(secp256k1, SHA-256) Round 1 functionality and types. +pub mod round1 { + use crate::keys::SigningShare; + + use super::*; + + /// Comprised of FROST(secp256k1, SHA-256) hiding and binding nonces. + /// + /// Note that [`SigningNonces`] must be used *only once* for a signing + /// operation; re-using nonces will result in leakage of a signer's long-lived + /// signing key. + pub type SigningNonces = frost::round1::SigningNonces; + + /// Published by each participant in the first round of the signing protocol. + /// + /// This step can be batched if desired by the implementation. Each + /// SigningCommitment can be used for exactly *one* signature. + pub type SigningCommitments = frost::round1::SigningCommitments; + + /// A commitment to a signing nonce share. + pub type NonceCommitment = frost::round1::NonceCommitment; + + /// Performed once by each participant selected for the signing operation. + /// + /// Generates the signing nonces and commitments to be used in the signing + /// operation. + pub fn commit(secret: &SigningShare, rng: &mut RNG) -> (SigningNonces, SigningCommitments) + where + RNG: CryptoRng + RngCore, + { + frost::round1::commit::(secret, rng) + } +} + +/// Generated by the coordinator of the signing operation and distributed to +/// each signing party. +pub type SigningPackage = frost::SigningPackage; + +/// FROST(secp256k1, SHA-256) Round 2 functionality and types, for signature share generation. +pub mod round2 { + use keys::Tweak; + + use super::*; + + /// A FROST(secp256k1, SHA-256) participant's signature share, which the Coordinator will aggregate with all other signer's + /// shares into the joint signature. + pub type SignatureShare = frost::round2::SignatureShare; + + /// Performed once by each participant selected for the signing operation. + /// + /// 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. + pub fn sign( + signing_package: &SigningPackage, + signer_nonces: &round1::SigningNonces, + key_package: &keys::KeyPackage, + ) -> Result { + frost::round2::sign(signing_package, signer_nonces, key_package) + } + + /// Same as [`sign()`], but using a Taproot tweak as specified in BIP-341. + pub fn sign_with_tweak( + signing_package: &SigningPackage, + signer_nonces: &round1::SigningNonces, + key_package: &keys::KeyPackage, + merkle_root: Option<&[u8]>, + ) -> Result { + if merkle_root.is_some() { + let key_package = key_package.clone().tweak(merkle_root); + frost::round2::sign(signing_package, signer_nonces, &key_package) + } else { + frost::round2::sign(signing_package, signer_nonces, key_package) + } + } +} + +/// A Schnorr signature on FROST(secp256k1, SHA-256). +pub type Signature = frost_core::Signature; + +/// Verifies each FROST(secp256k1, SHA-256) 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 Schnorr +/// 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: &SigningPackage, + signature_shares: &BTreeMap, + public_key_package: &keys::PublicKeyPackage, +) -> Result { + frost::aggregate(signing_package, signature_shares, public_key_package) +} + +/// Same as [`aggregate()`], but using a Taproot tweak as specified in BIP-341. +pub fn aggregate_with_tweak( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, + public_key_package: &keys::PublicKeyPackage, + merkle_root: Option<&[u8]>, +) -> Result { + if merkle_root.is_some() { + let public_key_package = public_key_package.clone().tweak(merkle_root); + frost::aggregate(signing_package, signature_shares, &public_key_package) + } else { + frost::aggregate(signing_package, signature_shares, public_key_package) + } +} + +/// A signing key for a Schnorr signature on FROST(secp256k1, SHA-256). +pub type SigningKey = frost_core::SigningKey; + +/// A valid verifying key for Schnorr signatures on FROST(secp256k1, SHA-256). +pub type VerifyingKey = frost_core::VerifyingKey; diff --git a/frost-secp256k1-tr/src/tests.rs b/frost-secp256k1-tr/src/tests.rs new file mode 100644 index 00000000..15a3e184 --- /dev/null +++ b/frost-secp256k1-tr/src/tests.rs @@ -0,0 +1,5 @@ +mod batch; +mod coefficient_commitment; +mod deserialize; +mod proptests; +mod vss_commitment; diff --git a/frost-secp256k1-tr/src/tests/batch.rs b/frost-secp256k1-tr/src/tests/batch.rs new file mode 100644 index 00000000..f88793a3 --- /dev/null +++ b/frost-secp256k1-tr/src/tests/batch.rs @@ -0,0 +1,24 @@ +use rand::thread_rng; + +use crate::*; + +#[test] +fn check_batch_verify() { + let rng = thread_rng(); + + frost_core::tests::batch::batch_verify::(rng); +} + +#[test] +fn check_bad_batch_verify() { + let rng = thread_rng(); + + frost_core::tests::batch::bad_batch_verify::(rng); +} + +#[test] +fn empty_batch_verify() { + let rng = thread_rng(); + + frost_core::tests::batch::empty_batch_verify::(rng); +} diff --git a/frost-secp256k1-tr/src/tests/coefficient_commitment.rs b/frost-secp256k1-tr/src/tests/coefficient_commitment.rs new file mode 100644 index 00000000..a63259c2 --- /dev/null +++ b/frost-secp256k1-tr/src/tests/coefficient_commitment.rs @@ -0,0 +1,46 @@ +use lazy_static::lazy_static; +use rand::thread_rng; +use serde_json::Value; + +use crate::*; + +// Tests for serialization and deserialization of CoefficientCommitment + +lazy_static! { + pub static ref ELEMENTS: Value = + serde_json::from_str(include_str!("../../tests/helpers/elements.json").trim()).unwrap(); +} + +#[test] +fn check_serialization_of_coefficient_commitment() { + let rng = thread_rng(); + frost_core::tests::coefficient_commitment::check_serialization_of_coefficient_commitment::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_create_coefficient_commitment() { + let rng = thread_rng(); + frost_core::tests::coefficient_commitment::check_create_coefficient_commitment::< + Secp256K1Sha256TR, + _, + >(rng); +} +#[test] +fn check_create_coefficient_commitment_error() { + frost_core::tests::coefficient_commitment::check_create_coefficient_commitment_error::< + Secp256K1Sha256TR, + >(&ELEMENTS); +} + +#[test] +fn check_get_value_of_coefficient_commitment() { + let rng = thread_rng(); + + frost_core::tests::coefficient_commitment::check_get_value_of_coefficient_commitment::< + Secp256K1Sha256TR, + _, + >(rng); +} diff --git a/frost-secp256k1-tr/src/tests/deserialize.rs b/frost-secp256k1-tr/src/tests/deserialize.rs new file mode 100644 index 00000000..7d4c630b --- /dev/null +++ b/frost-secp256k1-tr/src/tests/deserialize.rs @@ -0,0 +1,38 @@ +use crate::*; + +#[test] +fn check_deserialize_non_canonical() { + let mut encoded_generator = ::Group::serialize( + &::Group::generator(), + ) + .unwrap(); + + let r = ::Group::deserialize(&encoded_generator); + assert!(r.is_ok()); + + // The first byte should be 0x02 or 0x03. Set other value to + // create a non-canonical encoding. + encoded_generator[0] = 0xFF; + let r = ::Group::deserialize(&encoded_generator); + assert_eq!(r, Err(GroupError::MalformedElement)); + + // Besides the first byte, it is still possible to get non-canonical encodings. + // This is x = p + 2 which is non-canonical and maps to a valid prime-order point. + let encoded_point = + hex::decode("02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc31") + .unwrap() + .try_into() + .unwrap(); + let r = ::Group::deserialize(&encoded_point); + assert_eq!(r, Err(GroupError::MalformedElement)); +} + +#[test] +fn check_deserialize_identity() { + // The identity is actually encoded as a single byte; but the API does not + // allow us to change that. Try to send something similar. + let encoded_identity = [0u8; 33]; + + let r = ::Group::deserialize(&encoded_identity); + assert_eq!(r, Err(GroupError::MalformedElement)); +} diff --git a/frost-secp256k1-tr/src/tests/proptests.rs b/frost-secp256k1-tr/src/tests/proptests.rs new file mode 100644 index 00000000..cad88c33 --- /dev/null +++ b/frost-secp256k1-tr/src/tests/proptests.rs @@ -0,0 +1,33 @@ +use crate::*; +use frost_core::tests::proptests::{tweak_strategy, SignatureCase}; +use proptest::prelude::*; + +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 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(rng, msg.to_vec()); + + // Apply tweaks to each case. + for t in &tweaks { + sig.apply_tweak(t); + } + + assert!(sig.check()); + } + + +} diff --git a/frost-secp256k1-tr/src/tests/vss_commitment.rs b/frost-secp256k1-tr/src/tests/vss_commitment.rs new file mode 100644 index 00000000..80fb1ca7 --- /dev/null +++ b/frost-secp256k1-tr/src/tests/vss_commitment.rs @@ -0,0 +1,42 @@ +use lazy_static::lazy_static; +use rand::thread_rng; +use serde_json::Value; + +use crate::*; + +// Tests for serialization and deserialization VerifiableSecretSharingCommitment + +lazy_static! { + pub static ref ELEMENTS: Value = + serde_json::from_str(include_str!("../../tests/helpers/elements.json").trim()).unwrap(); +} + +#[test] +fn check_serialize_vss_commitment() { + let rng = thread_rng(); + frost_core::tests::vss_commitment::check_serialize_vss_commitment::(rng); +} + +#[test] +fn check_deserialize_vss_commitment() { + let rng = thread_rng(); + frost_core::tests::vss_commitment::check_deserialize_vss_commitment::( + rng, + ); +} + +#[test] +fn check_deserialize_vss_commitment_error() { + let rng = thread_rng(); + frost_core::tests::vss_commitment::check_deserialize_vss_commitment_error::( + rng, &ELEMENTS, + ); +} + +#[test] +fn check_compute_public_key_package() { + let rng = thread_rng(); + frost_core::tests::vss_commitment::check_compute_public_key_package::( + rng, + ); +} diff --git a/frost-secp256k1-tr/tests/common_traits_tests.rs b/frost-secp256k1-tr/tests/common_traits_tests.rs new file mode 100644 index 00000000..81b97a95 --- /dev/null +++ b/frost-secp256k1-tr/tests/common_traits_tests.rs @@ -0,0 +1,74 @@ +#![cfg(feature = "serde")] + +mod helpers; + +use frost_secp256k1_tr::SigningKey; +use helpers::samples; +use rand::thread_rng; + +#[allow(clippy::unnecessary_literal_unwrap)] +fn check_common_traits_for_type(v: T) { + // Make sure can be debug-printed. This also catches if the Debug does not + // have an endless recursion (a popular mistake). + println!("{:?}", v); + // Test Clone and Eq + assert_eq!(v, v.clone()); + // Make sure it can be unwrapped in a Result (which requires Debug). + let e: Result = Ok(v.clone()); + assert_eq!(v, e.unwrap()); +} + +#[test] +fn check_signing_key_common_traits() { + let mut rng = thread_rng(); + let signing_key = SigningKey::new(&mut rng); + check_common_traits_for_type(signing_key); +} + +#[test] +fn check_signing_commitments_common_traits() { + let commitments = samples::signing_commitments(); + check_common_traits_for_type(commitments); +} + +#[test] +fn check_signing_package_common_traits() { + let signing_package = samples::signing_package(); + check_common_traits_for_type(signing_package); +} + +#[test] +fn check_signature_share_common_traits() { + let signature_share = samples::signature_share(); + check_common_traits_for_type(signature_share); +} + +#[test] +fn check_secret_share_common_traits() { + let secret_share = samples::secret_share(); + check_common_traits_for_type(secret_share); +} + +#[test] +fn check_key_package_common_traits() { + let key_package = samples::key_package(); + check_common_traits_for_type(key_package); +} + +#[test] +fn check_public_key_package_common_traits() { + let public_key_package = samples::public_key_package(); + check_common_traits_for_type(public_key_package); +} + +#[test] +fn check_round1_package_common_traits() { + let round1_package = samples::round1_package(); + check_common_traits_for_type(round1_package); +} + +#[test] +fn check_round2_package_common_traits() { + let round2_package = samples::round2_package(); + check_common_traits_for_type(round2_package); +} diff --git a/frost-secp256k1-tr/tests/helpers/elements.json b/frost-secp256k1-tr/tests/helpers/elements.json new file mode 100644 index 00000000..e8cd4082 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/elements.json @@ -0,0 +1,5 @@ +{ + "elements": { + "invalid_element": "123456afdf4a7f88885ab26b20d18edb7d4d9589812a6cf1a5a1a09d3808dae5d8" + } +} diff --git a/frost-secp256k1-tr/tests/helpers/mod.rs b/frost-secp256k1-tr/tests/helpers/mod.rs new file mode 100644 index 00000000..0de6147c --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/mod.rs @@ -0,0 +1,24 @@ +// Required since each integration test is compiled as a separated crate, +// and each one uses only part of the module. +#![allow(dead_code)] + +use frost_secp256k1_tr::Secp256K1Sha256TR; +use secp256k1::Secp256k1; + +pub mod samples; + +pub fn verify_signature( + msg: &[u8], + group_signature: &frost_core::Signature, + group_pubkey: &frost_core::VerifyingKey, +) { + let secp = Secp256k1::new(); + let sig = secp256k1::schnorr::Signature::from_byte_array( + group_signature.serialize().unwrap().try_into().unwrap(), + ); + let pubkey = secp256k1::XOnlyPublicKey::from_byte_array( + &group_pubkey.serialize().unwrap()[1..33].try_into().unwrap(), + ) + .unwrap(); + secp.verify_schnorr(&sig, msg, &pubkey).unwrap(); +} diff --git a/frost-secp256k1-tr/tests/helpers/repair-share.json b/frost-secp256k1-tr/tests/helpers/repair-share.json new file mode 100644 index 00000000..f4db9bc8 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/repair-share.json @@ -0,0 +1,15 @@ +{ + "scalar_generation": { + "random_scalar_1": "1847f6c4a85096e5dbc9e200c9691c5164f8e276d32d4a54ebaf4275474a1403", + "random_scalar_2": "eac5595269d108812eaa865bf62c703a2c128a61fa3bd4dc837b9314bc515204", + "random_scalar_3": "5b3b6084e41c273a39a8d9bbbd87fbcd626c07030142bf78c6c91247bf175700", + "random_scalar_sum": "5e48b09bf63dc6a1441d42187d1d885a38c896f51f633e6e76218944f27c7bc6" + }, + "sigma_generation": { + "sigma_1": "ec3aa83140065181d75b746bfd6bbbbaf212bdfbb3a91670f924d1ca899cbc0c", + "sigma_2": "5dd288d659e0a2dd3ef7523a9cc4f80f4a7f919e9980005c7fbec0961d3fb500", + "sigma_3": "3e62e7461db9ca1ed2f1549a8114bbc87fa9242ce0012ed3f9ac9dcf23f4c30a", + "sigma_4": "684c44e7aba416a1982a8db8ec2a3095f5cc6a3f958a4716b69ae76524dd7200", + "sigma_sum": "f0bc5d356344d51f816ea8fa076fa029f7590120136bec7c6958b9081f7864d5" + } +} diff --git a/frost-secp256k1-tr/tests/helpers/samples.json b/frost-secp256k1-tr/tests/helpers/samples.json new file mode 100644 index 00000000..1cc174a9 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/samples.json @@ -0,0 +1,7 @@ +{ + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "proof_of_knowledge": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "element1": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "element2": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", + "scalar1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" +} diff --git a/frost-secp256k1-tr/tests/helpers/samples.rs b/frost-secp256k1-tr/tests/helpers/samples.rs new file mode 100644 index 00000000..11ef99e3 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/samples.rs @@ -0,0 +1,128 @@ +//! Generate sample, fixed instances of structs for testing. + +use std::collections::BTreeMap; + +use frost_core::{round1::Nonce, Ciphersuite, Element, Group, Scalar}; +use frost_secp256k1_tr::{ + keys::{ + dkg::{round1, round2}, + KeyPackage, PublicKeyPackage, SecretShare, SigningShare, VerifiableSecretSharingCommitment, + VerifyingShare, + }, + round1::{NonceCommitment, SigningCommitments, SigningNonces}, + round2::SignatureShare, + Field, Signature, SigningPackage, VerifyingKey, +}; + +type C = frost_secp256k1_tr::Secp256K1Sha256TR; + +fn element1() -> Element { + ::Group::generator() +} + +fn element2() -> Element { + element1() + element1() +} + +fn scalar1() -> Scalar { + let one = <::Group as Group>::Field::one(); + let three = one + one + one; + // To return a fixed non-small number, get the inverse of 3 + <::Group as Group>::Field::invert(&three) + .expect("nonzero elements have inverses") +} + +/// Generate a sample SigningCommitments. +pub fn signing_nonces() -> SigningNonces { + let serialized_scalar1 = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_scalar2 = <::Group as Group>::Field::serialize(&scalar1()); + let hiding_nonce = Nonce::deserialize(serialized_scalar1.as_ref()).unwrap(); + let binding_nonce = Nonce::deserialize(serialized_scalar2.as_ref()).unwrap(); + + SigningNonces::from_nonces(hiding_nonce, binding_nonce) +} + +/// Generate a sample SigningCommitments. +pub fn signing_commitments() -> SigningCommitments { + let serialized_element1 = ::Group::serialize(&element1()).unwrap(); + let serialized_element2 = ::Group::serialize(&element2()).unwrap(); + let hiding_nonce_commitment = + NonceCommitment::deserialize(serialized_element1.as_ref()).unwrap(); + let binding_nonce_commitment = + NonceCommitment::deserialize(serialized_element2.as_ref()).unwrap(); + + SigningCommitments::new(hiding_nonce_commitment, binding_nonce_commitment) +} + +/// Generate a sample SigningPackage. +pub fn signing_package() -> SigningPackage { + let identifier = 42u16.try_into().unwrap(); + let commitments = BTreeMap::from([(identifier, signing_commitments())]); + let message = "hello world".as_bytes(); + + SigningPackage::new(commitments, message) +} + +/// Generate a sample SignatureShare. +pub fn signature_share() -> SignatureShare { + let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + + SignatureShare::deserialize(serialized_scalar.as_ref()).unwrap() +} + +/// Generate a sample SecretShare. +pub fn secret_share() -> SecretShare { + let identifier = 42u16.try_into().unwrap(); + let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); + let vss_commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + + SecretShare::new(identifier, signing_share, vss_commitment) +} + +/// Generate a sample KeyPackage. +pub fn key_package() -> KeyPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); + + KeyPackage::new(identifier, signing_share, verifying_share, verifying_key, 2) +} + +/// Generate a sample PublicKeyPackage. +pub fn public_key_package() -> PublicKeyPackage { + let identifier = 42u16.try_into().unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_share = VerifyingShare::deserialize(serialized_element.as_ref()).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let verifying_key = VerifyingKey::deserialize(serialized_element.as_ref()).unwrap(); + let verifying_shares = BTreeMap::from([(identifier, verifying_share)]); + + PublicKeyPackage::new(verifying_shares, verifying_key) +} + +/// Generate a sample round1::Package. +pub fn round1_package() -> round1::Package { + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + + let serialized_element = ::Group::serialize(&element1()).unwrap(); + let vss_commitment = + VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); + + round1::Package::new(vss_commitment, signature) +} + +/// Generate a sample round2::Package. +pub fn round2_package() -> round2::Package { + let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let signing_share = SigningShare::deserialize(serialized_scalar.as_ref()).unwrap(); + + round2::Package::new(signing_share) +} diff --git a/frost-secp256k1-tr/tests/helpers/vectors-big-identifier.json b/frost-secp256k1-tr/tests/helpers/vectors-big-identifier.json new file mode 100644 index 00000000..2fedbec2 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/vectors-big-identifier.json @@ -0,0 +1,77 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(secp256k1, SHA-256)", + "group": "secp256k1", + "hash": "SHA-256" + }, + "inputs": { + "participant_list": [ + 1, + 3 + ], + "group_secret_key": "0d004150d27c3bf2a42f312683d35fac7394b1e9e318249c1bfe7f0795a83114", + "verifying_key_key": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4f", + "message": "74657374", + "share_polynomial_coefficients": [ + "fbf85eadae3058ea14f19148bb72b45e4399c0b16028acaf0395c9b03c823579" + ], + "participant_shares": [ + { + "identifier": 1, + "participant_share": "08f89ffe80ac94dcb920c26f3f46140bfc7f95b493f8310f5fc1ea2b01f4254c" + }, + { + "identifier": 2, + "participant_share": "04f0feac2edcedc6ce1253b7fab8c86b856a797f44d83d82a385554e6e401984" + }, + { + "identifier": 3, + "participant_share": "00e95d59dd0d46b0e303e500b62b7ccb0e555d49f5b849f5e748c071da8c0dbc" + } + ] + }, + "round_one_outputs": { + "outputs": [ + { + "identifier": 1, + "hiding_nonce_randomness": "bda8e748e599187762cff956f03dc6ea13fc8e04491a0427b7e6e78600f41c52", + "binding_nonce_randomness": "2ca682429bf05df435b9927b8edb1d748278f3e42fa11ef358e49bbf4a1b780d", + "hiding_nonce": "58cd30723da418156fe9b71870a118e0bbc3d0353ba7c760f9bbc8d60c3dab29", + "binding_nonce": "c22289cc43b82ed938d4b2288efb7381c405fb59f5d43bddc543d98838c60b19", + "hiding_nonce_commitment": "024e34ab3a7ad6b4563dbfe97e9f1206b3378cceb2502491ed0fb709765e1e5ba8", + "binding_nonce_commitment": "03d4b1f3a61dc67e64dfb4abfccabb712f1f6914a6ec9b67749d171370453192cb", + "binding_factor_input": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4fc709887b880e002210593616e086c2a0652d18bad338a3d3987251602e45e0a5a875faacc4377f0b6e4638c16db01b5f88070873ee789f5e9e6acf91f52fdd030000000000000000000000000000000000000000000000000000000000000001", + "binding_factor": "55a3e44879db6daf00c81eb28e828869560c0901f347baff524f1c91a6669604" + }, + { + "identifier": 3, + "hiding_nonce_randomness": "70818dd5170672c4a4285fd593d4f222417f941f3118e1244955e7a1098a35d8", + "binding_nonce_randomness": "74ca2da071ed4a2a6cad5087d6758b48a558ab5861c61117fee05757e4b1309e", + "hiding_nonce": "a4109db0a5db30fac8cd1f4e272ff02e08258928f067d82c63d97279b114514a", + "binding_nonce": "ce3837bd963f0d81002279f7bb9eefceac64435f638885c2beae6f1dd881fd9e", + "hiding_nonce_commitment": "02d768658a1b94225645401a1512b803657770c7a21bf9ccccccfa09930a44951b", + "binding_nonce_commitment": "034570a4e5217ee8770a28401185f50b4fce4d3f3933a3af9df7ab39b42381d0eb", + "binding_factor_input": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4fc709887b880e002210593616e086c2a0652d18bad338a3d3987251602e45e0a5a875faacc4377f0b6e4638c16db01b5f88070873ee789f5e9e6acf91f52fdd030000000000000000000000000000000000000000000000000000000000000003", + "binding_factor": "444c1cc7cfe48f4577cce65488f337a9cc8c33dea4cfa986eb590bd8e2b1fa2d" + } + ] + }, + "round_two_outputs": { + "outputs": [ + { + "identifier": 1, + "sig_share": "2ffc305d1694fd84108b84d98306a1af807c6ad9bc3a2d8e448a09643202a15b" + }, + { + "identifier": 3, + "sig_share": "a8c392566ea29e852b4080a028bf5547166c87e703e4fb7136d4ebef65f99b3f" + } + ] + }, + "final_output": { + "sig": "0c776a9516a77808b70a31e74f1464814a6fcf897fb3a6bd84c7a9a9a7a5bcb8d8bfc2b385379c093bcc0579abc5f6f696e8f2c0c01f28ff7b5ef55397fc3c9a" + } +} diff --git a/frost-secp256k1-tr/tests/helpers/vectors.json b/frost-secp256k1-tr/tests/helpers/vectors.json new file mode 100644 index 00000000..2fedbec2 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/vectors.json @@ -0,0 +1,77 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(secp256k1, SHA-256)", + "group": "secp256k1", + "hash": "SHA-256" + }, + "inputs": { + "participant_list": [ + 1, + 3 + ], + "group_secret_key": "0d004150d27c3bf2a42f312683d35fac7394b1e9e318249c1bfe7f0795a83114", + "verifying_key_key": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4f", + "message": "74657374", + "share_polynomial_coefficients": [ + "fbf85eadae3058ea14f19148bb72b45e4399c0b16028acaf0395c9b03c823579" + ], + "participant_shares": [ + { + "identifier": 1, + "participant_share": "08f89ffe80ac94dcb920c26f3f46140bfc7f95b493f8310f5fc1ea2b01f4254c" + }, + { + "identifier": 2, + "participant_share": "04f0feac2edcedc6ce1253b7fab8c86b856a797f44d83d82a385554e6e401984" + }, + { + "identifier": 3, + "participant_share": "00e95d59dd0d46b0e303e500b62b7ccb0e555d49f5b849f5e748c071da8c0dbc" + } + ] + }, + "round_one_outputs": { + "outputs": [ + { + "identifier": 1, + "hiding_nonce_randomness": "bda8e748e599187762cff956f03dc6ea13fc8e04491a0427b7e6e78600f41c52", + "binding_nonce_randomness": "2ca682429bf05df435b9927b8edb1d748278f3e42fa11ef358e49bbf4a1b780d", + "hiding_nonce": "58cd30723da418156fe9b71870a118e0bbc3d0353ba7c760f9bbc8d60c3dab29", + "binding_nonce": "c22289cc43b82ed938d4b2288efb7381c405fb59f5d43bddc543d98838c60b19", + "hiding_nonce_commitment": "024e34ab3a7ad6b4563dbfe97e9f1206b3378cceb2502491ed0fb709765e1e5ba8", + "binding_nonce_commitment": "03d4b1f3a61dc67e64dfb4abfccabb712f1f6914a6ec9b67749d171370453192cb", + "binding_factor_input": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4fc709887b880e002210593616e086c2a0652d18bad338a3d3987251602e45e0a5a875faacc4377f0b6e4638c16db01b5f88070873ee789f5e9e6acf91f52fdd030000000000000000000000000000000000000000000000000000000000000001", + "binding_factor": "55a3e44879db6daf00c81eb28e828869560c0901f347baff524f1c91a6669604" + }, + { + "identifier": 3, + "hiding_nonce_randomness": "70818dd5170672c4a4285fd593d4f222417f941f3118e1244955e7a1098a35d8", + "binding_nonce_randomness": "74ca2da071ed4a2a6cad5087d6758b48a558ab5861c61117fee05757e4b1309e", + "hiding_nonce": "a4109db0a5db30fac8cd1f4e272ff02e08258928f067d82c63d97279b114514a", + "binding_nonce": "ce3837bd963f0d81002279f7bb9eefceac64435f638885c2beae6f1dd881fd9e", + "hiding_nonce_commitment": "02d768658a1b94225645401a1512b803657770c7a21bf9ccccccfa09930a44951b", + "binding_nonce_commitment": "034570a4e5217ee8770a28401185f50b4fce4d3f3933a3af9df7ab39b42381d0eb", + "binding_factor_input": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4fc709887b880e002210593616e086c2a0652d18bad338a3d3987251602e45e0a5a875faacc4377f0b6e4638c16db01b5f88070873ee789f5e9e6acf91f52fdd030000000000000000000000000000000000000000000000000000000000000003", + "binding_factor": "444c1cc7cfe48f4577cce65488f337a9cc8c33dea4cfa986eb590bd8e2b1fa2d" + } + ] + }, + "round_two_outputs": { + "outputs": [ + { + "identifier": 1, + "sig_share": "2ffc305d1694fd84108b84d98306a1af807c6ad9bc3a2d8e448a09643202a15b" + }, + { + "identifier": 3, + "sig_share": "a8c392566ea29e852b4080a028bf5547166c87e703e4fb7136d4ebef65f99b3f" + } + ] + }, + "final_output": { + "sig": "0c776a9516a77808b70a31e74f1464814a6fcf897fb3a6bd84c7a9a9a7a5bcb8d8bfc2b385379c093bcc0579abc5f6f696e8f2c0c01f28ff7b5ef55397fc3c9a" + } +} diff --git a/frost-secp256k1-tr/tests/helpers/vectors_dkg.json b/frost-secp256k1-tr/tests/helpers/vectors_dkg.json new file mode 100644 index 00000000..9fcbc839 --- /dev/null +++ b/frost-secp256k1-tr/tests/helpers/vectors_dkg.json @@ -0,0 +1,41 @@ +{ + "config": { + "MAX_PARTICIPANTS": 3, + "MIN_PARTICIPANTS": 2, + "name": "FROST(secp256k1, SHA-256)", + "group": "secp256k1", + "hash": "SHA-256" + }, + "inputs": { + "verifying_key": "03849089de77b56bd35fcbfc70bf38e73448131090acc75d538a5cea63cc3dcefe", + "1": { + "identifier": 1, + "signing_key": "68e3f6904c6043973515a36bf7801a71597da35733f21305d75a5234f06e4529", + "coefficient": "25d2d840a3e2718a431ec69e14ee8a015b000d43c7a9868060f01d5aa52a19d1", + "vss_commitments": ["03e7ba4acb164d2bd5eba4f47b3a788109ddb3f88f1181792424fa332123a25ea8", "037495e920a1f032916193aa80ea97a4c3a611dec9ab47ccc969deb664f5f88bbe"], + "proof_of_knowledge": "6689a8d414eb4961308e21f8caa1045236efded4f3de9209dc07547e88be3b42e192de9bed27fb78a7a4d4e35a0422f11f52631b8e66d69e609398eaff2770b8", + "signing_shares": { + "2": "1dd3cb3e2370e6af22917415f0ad584514807b58b3cc40d2230a26e115f02771", + "3": "dd25ee86acd01f996618aa0d1153f5e8fbc929a8e8a18b8f0a15f91d087217e2" + }, + "verifying_share": "02a8bf413b5d7af0e692fba967540cde8009f161a4d721f8c88649c1933bbb7531", + "signing_share": "f1be455a8ec9ab86ef8438f23a5cfdf70153aa2785d4bebba83e0840403e4bf3" + }, + "2": { + "identifier": 2, + "signing_key": "2619be8223b23e0453ddc630a4d164e81f7d8a9e07af33c4d4d02190df8bec13", + "coefficient": "f7ba0cbbffbea8aaceb3ade54bdbf35bafb1cda15b65ad490e0c63dd069a7c9f", + "vss_commitments": ["03ef10370a008cd95e179dc51e2cb7828f30b72d254e5166484f927c84ab326582", "022ce0dac0db217ba326fbbe3e6132d45e2a4bfa0a0c3790d91eacce9a1c2d6a10"], + "proof_of_knowledge": "a319dd51cf64b3896c22f54154812d4ae76cfa95f46f53ef69241fd702456fef32da76cc93d3a541ca495b723e793ee90c32440da5f314e2e58a2dc30550314a", + "verifying_share": "029ecb3a4db28a82e7b8d600d42711b02790dde3f063f0ecec6f812c1c5d7dcefc" + }, + "3": { + "identifier": 3, + "signing_key": "9a267f4cde8087a6eca0969425846209b41b515b73195ebbeeef8a991103f1ec", + "coefficient": "42ff6f39ce4f97f279781378ebcf93df47add84d75882cd31b266e83f76e25f6", + "vss_commitments": ["02da186c3863c5600b471a2799cb6f15ae4d8315a2f225c177798880e75ac820a0", "03e6a36e7fa4b117c1aa428886672e3a35d926bb4c585a9b07d8ee9a3387420067"], + "proof_of_knowledge": "6e115d9e63fd15d432b380ccf1ec4ed03340fcf96caeae8985aedb5f905b1a65dc422ffe5878988fbbc55454857736c7755d9c8f5ee6822c8833ea21d54dba36", + "verifying_share": "02c98b3c2e9f4bde4cf90dc9c7be639e5adda6ea09fc605239880a22cb836f7145" + } + } +} diff --git a/frost-secp256k1-tr/tests/integration_tests.rs b/frost-secp256k1-tr/tests/integration_tests.rs new file mode 100644 index 00000000..9187285b --- /dev/null +++ b/frost-secp256k1-tr/tests/integration_tests.rs @@ -0,0 +1,359 @@ +use frost_secp256k1_tr::*; +use lazy_static::lazy_static; +use rand::thread_rng; +use serde_json::Value; + +#[test] +fn check_zero_key_fails() { + frost_core::tests::ciphersuite_generic::check_zero_key_fails::(); +} + +#[test] +fn check_sign_with_dkg() { + let rng = thread_rng(); + + frost_core::tests::ciphersuite_generic::check_sign_with_dkg::(rng); +} + +#[test] +fn check_dkg_part1_fails_with_invalid_signers_min_signers() { + let rng = thread_rng(); + + let min_signers = 1; + let max_signers = 3; + let error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_dkg_part1_fails_with_min_signers_greater_than_max() { + let rng = thread_rng(); + + let min_signers = 3; + let max_signers = 2; + let error: frost_core::Error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_dkg_part1_fails_with_invalid_signers_max_signers() { + let rng = thread_rng(); + + let min_signers = 3; + let max_signers = 1; + let error = Error::InvalidMaxSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_rts() { + let rng = thread_rng(); + + frost_core::tests::repairable::check_rts::(rng); +} + +#[test] +fn check_refresh_shares_with_dealer() { + let rng = thread_rng(); + + frost_core::tests::refresh::check_refresh_shares_with_dealer::(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_serialisation() { + let rng = thread_rng(); + + frost_core::tests::refresh::check_refresh_shares_with_dealer_serialisation::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_public_key_package() { + let rng = thread_rng(); + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_public_key_package::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_min_signers() { + let rng = thread_rng(); + let identifiers = vec![ + Identifier::try_from(1).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(4).unwrap(), + Identifier::try_from(5).unwrap(), + ]; + let min_signers = 1; + let max_signers = 4; + let error = Error::InvalidMinSigners; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(max_signers, min_signers, &identifiers, error, rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_unequal_num_identifiers_and_max_signers() { + let rng = thread_rng(); + let identifiers = vec![ + Identifier::try_from(1).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(4).unwrap(), + Identifier::try_from(5).unwrap(), + ]; + let min_signers = 3; + let max_signers = 3; + let error: frost_core::Error = Error::IncorrectNumberOfIdentifiers; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(max_signers, min_signers, &identifiers, error, rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_min_signers_greater_than_max() { + let rng = thread_rng(); + let identifiers = vec![ + Identifier::try_from(1).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(4).unwrap(), + Identifier::try_from(5).unwrap(), + ]; + let min_signers = 6; + let max_signers = 4; + let error: frost_core::Error = Error::InvalidMinSigners; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(max_signers, min_signers, &identifiers, error, rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_max_signers() { + let rng = thread_rng(); + let identifiers = vec![Identifier::try_from(1).unwrap()]; + let min_signers = 3; + let max_signers = 1; + let error = Error::InvalidMaxSigners; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(max_signers, min_signers, &identifiers, error, rng); +} + +#[test] +fn check_refresh_shares_with_dealer_fails_with_invalid_identifier() { + let rng = thread_rng(); + let identifiers = vec![ + Identifier::try_from(8).unwrap(), + Identifier::try_from(3).unwrap(), + Identifier::try_from(4).unwrap(), + Identifier::try_from(6).unwrap(), + ]; + let min_signers = 2; + let max_signers = 4; + let error = Error::UnknownIdentifier; + + frost_core::tests::refresh::check_refresh_shares_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(max_signers, min_signers, &identifiers, error, rng); +} + +#[test] +fn check_sign_with_dealer() { + let rng = thread_rng(); + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer::(rng); +} + +#[test] +fn check_sign_with_dealer_fails_with_invalid_min_signers() { + let rng = thread_rng(); + + let min_signers = 1; + let max_signers = 3; + let error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_sign_with_dealer_fails_with_min_signers_greater_than_max() { + let rng = thread_rng(); + + let min_signers = 3; + let max_signers = 2; + let error: frost_core::Error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_sign_with_dealer_fails_with_invalid_max_signers() { + let rng = thread_rng(); + + let min_signers = 3; + let max_signers = 1; + let error = Error::InvalidMaxSigners; + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +/// This is testing that Shamir's secret sharing to compute and arbitrary +/// value is working. +#[test] +fn check_share_generation_secp256k1_tr_sha256() { + let rng = thread_rng(); + frost_core::tests::ciphersuite_generic::check_share_generation::(rng); +} + +#[test] +fn check_share_generation_fails_with_invalid_min_signers() { + let rng = thread_rng(); + + let min_signers = 0; + let max_signers = 3; + let error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_share_generation_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_share_generation_fails_with_min_signers_greater_than_max() { + let rng = thread_rng(); + + let min_signers = 3; + let max_signers = 2; + let error: frost_core::Error = Error::InvalidMinSigners; + + frost_core::tests::ciphersuite_generic::check_share_generation_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +#[test] +fn check_share_generation_fails_with_invalid_max_signers() { + let rng = thread_rng(); + + let min_signers = 3; + let max_signers = 0; + let error = Error::InvalidMaxSigners; + + frost_core::tests::ciphersuite_generic::check_share_generation_fails_with_invalid_signers::< + Secp256K1Sha256TR, + _, + >(min_signers, max_signers, error, rng); +} + +lazy_static! { + pub static ref VECTORS: Value = + serde_json::from_str(include_str!("../tests/helpers/vectors.json").trim()) + .expect("Test vector is valid JSON"); + pub static ref VECTORS_BIG_IDENTIFIER: Value = + serde_json::from_str(include_str!("../tests/helpers/vectors-big-identifier.json").trim()) + .expect("Test vector is valid JSON"); + pub static ref VECTORS_DKG: Value = + serde_json::from_str(include_str!("../tests/helpers/vectors_dkg.json").trim()) + .expect("Test vector is valid JSON"); +} + +#[test] +fn check_sign_with_test_vectors() { + frost_core::tests::vectors::check_sign_with_test_vectors::(&VECTORS); +} + +#[test] +fn check_sign_with_test_vectors_dkg() { + frost_core::tests::vectors_dkg::check_dkg_keygen::(&VECTORS_DKG); +} + +#[test] +fn check_sign_with_test_vectors_with_big_identifiers() { + frost_core::tests::vectors::check_sign_with_test_vectors::( + &VECTORS_BIG_IDENTIFIER, + ); +} + +#[test] +fn check_error_culprit() { + frost_core::tests::ciphersuite_generic::check_error_culprit::(); +} + +#[test] +fn check_identifier_derivation() { + frost_core::tests::ciphersuite_generic::check_identifier_derivation::(); +} + +// Explicit test which is used in a documentation snippet +#[test] +#[allow(unused_variables)] +fn check_identifier_generation() -> Result<(), Error> { + // ANCHOR: dkg_identifier + let participant_identifier = Identifier::try_from(7u16)?; + let participant_identifier = Identifier::derive("alice@example.com".as_bytes())?; + // ANCHOR_END: dkg_identifier + Ok(()) +} + +#[test] +fn check_sign_with_dealer_and_identifiers() { + let rng = thread_rng(); + + frost_core::tests::ciphersuite_generic::check_sign_with_dealer_and_identifiers::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_sign_with_missing_identifier() { + let rng = thread_rng(); + frost_core::tests::ciphersuite_generic::check_sign_with_missing_identifier::< + Secp256K1Sha256TR, + _, + >(rng); +} + +#[test] +fn check_sign_with_incorrect_commitments() { + let rng = thread_rng(); + frost_core::tests::ciphersuite_generic::check_sign_with_incorrect_commitments::< + Secp256K1Sha256TR, + _, + >(rng); +} diff --git a/frost-secp256k1-tr/tests/interoperability_tests.rs b/frost-secp256k1-tr/tests/interoperability_tests.rs new file mode 100644 index 00000000..b2e3f9a0 --- /dev/null +++ b/frost-secp256k1-tr/tests/interoperability_tests.rs @@ -0,0 +1,53 @@ +use frost_secp256k1_tr::*; + +use crate::Secp256K1Sha256TR; +use rand::thread_rng; + +mod helpers; + +#[test] +fn check_interoperability_in_regular_sign() { + let mut rng = thread_rng(); + + for _ in 0..256 { + let signing_key = SigningKey::new(&mut rng); + let verifying_key = signing_key.into(); + let signature = signing_key.sign(&mut rng, b"message"); + helpers::verify_signature(b"message", &signature, &verifying_key); + } +} + +#[test] +fn check_interoperability_in_sign_with_dkg() { + let rng = thread_rng(); + + // Test with multiple keys/signatures to better exercise the key generation + // and the interoperability check. A smaller number of iterations is used + // because DKG takes longer and otherwise the test would be too slow. + for _ in 0..32 { + let (message, group_signature, group_pubkey) = + frost_core::tests::ciphersuite_generic::check_sign_with_dkg::( + rng.clone(), + ); + + helpers::verify_signature(&message, &group_signature, &group_pubkey); + } +} + +#[test] +fn check_interoperability_in_sign_with_dealer() { + let rng = thread_rng(); + + // Test with multiple keys/signatures to better exercise the key generation + // and the interoperability check. + for _ in 0..256 { + let (message, group_signature, group_pubkey) = + frost_core::tests::ciphersuite_generic::check_sign_with_dealer::( + rng.clone(), + ); + + // Check that the threshold signature can be verified by the `ed25519_dalek` crate + // public key (interoperability test) + helpers::verify_signature(&message, &group_signature, &group_pubkey); + } +} diff --git a/frost-secp256k1-tr/tests/recreation_tests.rs b/frost-secp256k1-tr/tests/recreation_tests.rs new file mode 100644 index 00000000..477de37e --- /dev/null +++ b/frost-secp256k1-tr/tests/recreation_tests.rs @@ -0,0 +1,133 @@ +//! Test for recreating packages from their components, which shows that they +//! can be serialized and deserialized as the user wishes. + +use frost_secp256k1_tr::{ + keys::{ + dkg::{round1, round2}, + KeyPackage, PublicKeyPackage, SecretShare, + }, + round1::{SigningCommitments, SigningNonces}, + round2::SignatureShare, + SigningPackage, +}; + +mod helpers; + +use helpers::samples; + +/// Check if SigningNonces can be recreated. +#[test] +fn check_signing_nonces_recreation() { + let nonces = samples::signing_nonces(); + let hiding = nonces.hiding(); + let binding = nonces.binding(); + let new_nonces = SigningNonces::from_nonces(*hiding, *binding); + assert!(nonces == new_nonces); +} + +/// Check if SigningCommitments can be recreated. +#[test] +fn check_signing_commitments_recreation() { + let commitments = samples::signing_commitments(); + let hiding = commitments.hiding(); + let binding = commitments.binding(); + let new_commitments = SigningCommitments::new(*hiding, *binding); + assert!(commitments == new_commitments); +} + +/// Check if SigningPackage can be recreated. +#[test] +fn check_signing_package_recreation() { + let signing_package = samples::signing_package(); + + let commitments = signing_package.signing_commitments(); + let message = signing_package.message(); + + let new_signing_package = SigningPackage::new(commitments.clone(), message); + assert!(signing_package == new_signing_package); +} + +/// Check if SignatureShare can be recreated. +#[test] +fn check_signature_share_recreation() { + let signature_share = samples::signature_share(); + + let encoded = signature_share.serialize(); + + let new_signature_share = SignatureShare::deserialize(&encoded).unwrap(); + assert!(signature_share == new_signature_share); +} + +/// Check if SecretShare can be recreated. +#[test] +fn check_secret_share_recreation() { + let secret_share = samples::secret_share(); + + let identifier = secret_share.identifier(); + let value = secret_share.signing_share(); + let commitment = secret_share.commitment(); + + let new_secret_share = SecretShare::new(*identifier, *value, commitment.clone()); + + assert!(secret_share == new_secret_share); +} + +/// Check if KeyPackage can be recreated. +#[test] +fn check_key_package_recreation() { + let key_package = samples::key_package(); + + let identifier = key_package.identifier(); + let signing_share = key_package.signing_share(); + let verifying_share = key_package.verifying_share(); + let verifying_key = key_package.verifying_key(); + let min_signers = key_package.min_signers(); + + let new_key_package = KeyPackage::new( + *identifier, + *signing_share, + *verifying_share, + *verifying_key, + *min_signers, + ); + + assert!(key_package == new_key_package); +} + +/// Check if PublicKeyPackage can be recreated. +#[test] +fn check_public_key_package_recreation() { + let public_key_package = samples::public_key_package(); + + let verifying_shares = public_key_package.verifying_shares(); + let verifying_key = public_key_package.verifying_key(); + + let new_public_key_package = PublicKeyPackage::new(verifying_shares.clone(), *verifying_key); + + assert!(public_key_package == new_public_key_package); +} + +/// Check if round1::Package can be recreated. +#[test] +fn check_round1_package_recreation() { + let round1_package = samples::round1_package(); + + let vss_commitment = round1_package.commitment(); + let signature = round1_package.proof_of_knowledge(); + + let new_round1_package = round1::Package::new(vss_commitment.clone(), *signature); + + assert!(round1_package == new_round1_package); +} + +/// Check if round2::Package can be recreated. +#[test] +fn check_round2_package_recreation() { + let round2_package = samples::round2_package(); + + let signing_share = round2_package.signing_share(); + + let new_round2_package = round2::Package::new(*signing_share); + + assert!(round2_package == new_round2_package); +} diff --git a/frost-secp256k1-tr/tests/rerandomized_tests.rs b/frost-secp256k1-tr/tests/rerandomized_tests.rs new file mode 100644 index 00000000..67e14313 --- /dev/null +++ b/frost-secp256k1-tr/tests/rerandomized_tests.rs @@ -0,0 +1,10 @@ +use frost_secp256k1_tr::Secp256K1Sha256TR; +use rand::thread_rng; + +#[test] +fn check_randomized_sign_with_dealer() { + let rng = thread_rng(); + + let (_msg, _group_signature, _group_pubkey) = + frost_rerandomized::tests::check_randomized_sign_with_dealer::(rng); +} diff --git a/frost-secp256k1-tr/tests/serde_tests.rs b/frost-secp256k1-tr/tests/serde_tests.rs new file mode 100644 index 00000000..62a70e70 --- /dev/null +++ b/frost-secp256k1-tr/tests/serde_tests.rs @@ -0,0 +1,632 @@ +#![cfg(feature = "serde")] + +mod helpers; + +use frost_secp256k1_tr::{ + keys::{ + dkg::{round1, round2}, + KeyPackage, PublicKeyPackage, SecretShare, + }, + round1::SigningCommitments, + round2::SignatureShare, + SigningPackage, +}; + +use helpers::samples; + +#[test] +fn check_signing_commitments_serialization() { + let commitments = samples::signing_commitments(); + + let json = serde_json::to_string_pretty(&commitments).unwrap(); + println!("{}", json); + + let decoded_commitments: SigningCommitments = serde_json::from_str(&json).unwrap(); + assert!(commitments == decoded_commitments); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }"#; + let decoded_commitments: SigningCommitments = serde_json::from_str(json).unwrap(); + assert!(commitments == decoded_commitments); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Wrong ciphersuite + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST(Wrong, SHA-512)" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "0000000000000000000000000000000000000000000000000000000000000000", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST(Ed25519, SHA-512)" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_signing_package_serialization() { + let signing_package = samples::signing_package(); + + let json = serde_json::to_string_pretty(&signing_package).unwrap(); + println!("{}", json); + + let decoded_signing_package: SigningPackage = serde_json::from_str(&json).unwrap(); + assert!(signing_package == decoded_signing_package); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "000000000000000000000000000000000000000000000000000000000000002a": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64" + }"#; + let decoded_signing_package: SigningPackage = serde_json::from_str(json).unwrap(); + assert!(signing_package == decoded_signing_package); + + // Invalid identifier + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "0000000000000000000000000000000000000000000000000000000000000000": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "000000000000000000000000000000000000000000000000000000000000002a": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "000000000000000000000000000000000000000000000000000000000000002a": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_commitments": { + "000000000000000000000000000000000000000000000000000000000000002a": { + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "hiding": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "binding": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + } + }, + "message": "68656c6c6f20776f726c64", + "extra": 1 + } + "#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_signature_share_serialization() { + let signature_share = samples::signature_share(); + + let json = serde_json::to_string_pretty(&signature_share).unwrap(); + println!("{}", json); + + let decoded_signature_share: SignatureShare = serde_json::from_str(&json).unwrap(); + assert!(signature_share == decoded_signature_share); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + let decoded_commitments: SignatureShare = serde_json::from_str(json).unwrap(); + assert!(signature_share == decoded_commitments); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + } + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_secret_share_serialization() { + let secret_share = samples::secret_share(); + + let json = serde_json::to_string_pretty(&secret_share).unwrap(); + println!("{}", json); + + let decoded_secret_share: SecretShare = serde_json::from_str(&json).unwrap(); + assert!(secret_share == decoded_secret_share); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + let decoded_secret_share: SecretShare = serde_json::from_str(json).unwrap(); + assert!(secret_share == decoded_secret_share); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid identifier + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "0000000000000000000000000000000000000000000000000000000000000000", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "foo": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + "extra": 1, + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_key_package_serialization() { + let key_package = samples::key_package(); + + let json = serde_json::to_string_pretty(&key_package).unwrap(); + println!("{}", json); + + let decoded_key_package: KeyPackage = serde_json::from_str(&json).unwrap(); + assert!(key_package == decoded_key_package); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "min_signers": 2 + }"#; + let decoded_key_package: KeyPackage = serde_json::from_str(json).unwrap(); + assert!(key_package == decoded_key_package); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid identifier + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "0000000000000000000000000000000000000000000000000000000000000000", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "min_signers": 2 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "foo": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "verifying_share": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "extra_field": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid version + let invalid_json = r#"{ + "header": { + "version": 1, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "secret_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "public": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "group_public": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "min_signers": 2 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_public_key_package_serialization() { + let public_key_package = samples::public_key_package(); + + let json = serde_json::to_string_pretty(&public_key_package).unwrap(); + println!("{}", json); + + let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(&json).unwrap(); + assert!(public_key_package == decoded_public_key_package); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + let decoded_public_key_package: PublicKeyPackage = serde_json::from_str(json).unwrap(); + assert!(public_key_package == decoded_public_key_package); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid identifier + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "0000000000000000000000000000000000000000000000000000000000000000": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "foo": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "verifying_shares": { + "000000000000000000000000000000000000000000000000000000000000002a": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "verifying_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_round1_package_serialization() { + let round1_package = samples::round1_package(); + + let json = serde_json::to_string_pretty(&round1_package).unwrap(); + println!("{}", json); + + let decoded_round1_package: round1::Package = serde_json::from_str(&json).unwrap(); + assert!(round1_package == decoded_round1_package); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ], + "proof_of_knowledge": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + let decoded_round1_package: round1::Package = serde_json::from_str(json).unwrap(); + assert!(round1_package == decoded_round1_package); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ], + "foo": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ] + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "commitment": [ + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ], + "proof_of_knowledge": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} + +#[test] +fn check_round2_package_serialization() { + let round2_package = samples::round2_package(); + + let json = serde_json::to_string_pretty(&round2_package).unwrap(); + println!("{}", json); + + let decoded_round2_package: round2::Package = serde_json::from_str(&json).unwrap(); + assert!(round2_package == decoded_round2_package); + + let json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + let decoded_round2_package: round2::Package = serde_json::from_str(json).unwrap(); + assert!(round2_package == decoded_round2_package); + + let invalid_json = "{}"; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Invalid field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "foo": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Missing field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + } + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); + + // Extra field + let invalid_json = r#"{ + "header": { + "version": 0, + "ciphersuite": "FROST-secp256k1-SHA256-TR-v1" + }, + "signing_share": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", + "extra": 1 + }"#; + assert!(serde_json::from_str::(invalid_json).is_err()); +} diff --git a/frost-secp256k1-tr/tests/serialization_tests.rs b/frost-secp256k1-tr/tests/serialization_tests.rs new file mode 100644 index 00000000..8f93cfb5 --- /dev/null +++ b/frost-secp256k1-tr/tests/serialization_tests.rs @@ -0,0 +1,105 @@ +#![cfg(feature = "serialization")] + +mod helpers; + +use frost_secp256k1_tr::{ + keys::{ + dkg::{round1, round2}, + KeyPackage, PublicKeyPackage, SecretShare, + }, + round1::{SigningCommitments, SigningNonces}, + round2::SignatureShare, + SigningPackage, +}; + +use helpers::samples; +use insta::assert_snapshot; + +#[test] +fn check_signing_nonces_postcard_serialization() { + let nonces = samples::signing_nonces(); + let bytes: Vec<_> = nonces.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!(nonces, SigningNonces::deserialize(&bytes).unwrap()); +} + +#[test] +fn check_signing_commitments_postcard_serialization() { + let commitments = samples::signing_commitments(); + let bytes: Vec<_> = commitments.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + commitments, + SigningCommitments::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_signing_package_postcard_serialization() { + let signing_package = samples::signing_package(); + let bytes: Vec<_> = signing_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + signing_package, + SigningPackage::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_signature_share_postcard_serialization() { + let signature_share = samples::signature_share(); + let bytes = signature_share.serialize(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + signature_share, + SignatureShare::deserialize(&bytes).unwrap() + ); +} +#[test] +fn check_secret_share_postcard_serialization() { + let secret_share = samples::secret_share(); + let bytes: Vec<_> = secret_share.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!(secret_share, SecretShare::deserialize(&bytes).unwrap()); +} + +#[test] +fn check_key_package_postcard_serialization() { + let key_package = samples::key_package(); + let bytes: Vec<_> = key_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!(key_package, KeyPackage::deserialize(&bytes).unwrap()); +} + +#[test] +fn check_public_key_package_postcard_serialization() { + let public_key_package = samples::public_key_package(); + let bytes: Vec<_> = public_key_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + public_key_package, + PublicKeyPackage::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_round1_package_postcard_serialization() { + let round1_package = samples::round1_package(); + let bytes: Vec<_> = round1_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round1_package, + round1::Package::deserialize(&bytes).unwrap() + ); +} + +#[test] +fn check_round2_package_postcard_serialization() { + let round2_package = samples::round2_package(); + let bytes: Vec<_> = round2_package.serialize().unwrap(); + assert_snapshot!(hex::encode(&bytes)); + assert_eq!( + round2_package, + round2::Package::deserialize(&bytes).unwrap() + ); +} diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_key_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_key_package_postcard_serialization.snap new file mode 100644 index 00000000..ca169f4c --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_key_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3000000000000000000000000000000000000000000000000000000000000002aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b810279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179802 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_public_key_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_public_key_package_postcard_serialization.snap new file mode 100644 index 00000000..5600403f --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_public_key_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab301000000000000000000000000000000000000000000000000000000000000002a0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round1_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round1_package_postcard_serialization.snap new file mode 100644 index 00000000..9099b14e --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round1_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3010279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817984079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round2_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round2_package_postcard_serialization.snap new file mode 100644 index 00000000..218294fb --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_round2_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_secret_share_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_secret_share_postcard_serialization.snap new file mode 100644 index 00000000..82e3585a --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_secret_share_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3000000000000000000000000000000000000000000000000000000000000002aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81010279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signature_share_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signature_share_postcard_serialization.snap new file mode 100644 index 00000000..aa7a5030 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signature_share_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(bytes)" +--- +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_commitments_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_commitments_postcard_serialization.snap new file mode 100644 index 00000000..66962d3c --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_commitments_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab30279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179802c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_nonces_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_nonces_postcard_serialization.snap new file mode 100644 index 00000000..537b8e38 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_nonces_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab3aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b8100230f8ab3034c7ff4f2ba8603998339c8e42675ceac23ef2e9623fdb260b24b1c944a2ea1a9034c7ff4f2ba8603998339c8e42675ceac23ef2e9623fdb260b24b1c944a2ea1a9 diff --git a/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_package_postcard_serialization.snap b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_package_postcard_serialization.snap new file mode 100644 index 00000000..b398e5e1 --- /dev/null +++ b/frost-secp256k1-tr/tests/snapshots/serialization_tests__check_signing_package_postcard_serialization.snap @@ -0,0 +1,5 @@ +--- +source: frost-secp256k1-tr/tests/serialization_tests.rs +expression: "hex::encode(&bytes)" +--- +00230f8ab301000000000000000000000000000000000000000000000000000000000000002a00230f8ab30279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179802c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee50b68656c6c6f20776f726c64 diff --git a/frost-secp256k1-tr/tests/tweaking_tests.rs b/frost-secp256k1-tr/tests/tweaking_tests.rs new file mode 100644 index 00000000..3fc74aef --- /dev/null +++ b/frost-secp256k1-tr/tests/tweaking_tests.rs @@ -0,0 +1,149 @@ +use std::{error::Error, vec}; + +use k256::elliptic_curve::point::AffineCoordinates; +use k256::ProjectivePoint; +use keys::Tweak; +use sha2::{Digest, Sha256}; + +use frost_secp256k1_tr::*; + +mod helpers; + +#[test] +fn check_tweaked_sign_with_dealer() -> Result<(), Box> { + use frost_secp256k1_tr as frost; + use rand::thread_rng; + use std::collections::BTreeMap; + + let merkle_root: Vec = vec![12; 32]; + + let mut rng = thread_rng(); + let max_signers = 5; + let min_signers = 3; + let (shares, pubkey_package) = frost::keys::generate_with_dealer( + max_signers, + min_signers, + frost::keys::IdentifierList::Default, + &mut rng, + )?; + let mut key_packages: BTreeMap<_, _> = BTreeMap::new(); + for (identifier, secret_share) in shares { + let key_package = frost::keys::KeyPackage::try_from(secret_share)?; + key_packages.insert(identifier, key_package); + } + + let mut nonces_map = BTreeMap::new(); + let mut commitments_map = BTreeMap::new(); + + for participant_index in 1..=min_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + let key_package = &key_packages[&participant_identifier]; + let (nonces, commitments) = frost::round1::commit(key_package.signing_share(), &mut rng); + nonces_map.insert(participant_identifier, nonces); + commitments_map.insert(participant_identifier, commitments); + } + + let mut signature_shares = BTreeMap::new(); + let message = "message to sign".as_bytes(); + let signing_package = frost::SigningPackage::new(commitments_map, message); + + for participant_identifier in nonces_map.keys() { + let key_package = &key_packages[participant_identifier]; + let nonces = &nonces_map[participant_identifier]; + let signature_share = frost::round2::sign_with_tweak( + &signing_package, + nonces, + key_package, + Some(&merkle_root), + )?; + signature_shares.insert(*participant_identifier, signature_share); + } + + let group_signature = frost::aggregate_with_tweak( + &signing_package, + &signature_shares, + &pubkey_package, + Some(&merkle_root), + )?; + + pubkey_package + .verifying_key() + .verify(message, &group_signature) + .expect_err("signature should not be valid for untweaked pubkey_package"); + + let pubkey_package_tweaked = pubkey_package.clone().tweak(Some(&merkle_root)); + pubkey_package_tweaked + .verifying_key() + .verify(message, &group_signature) + .expect("signature should be valid for tweaked pubkey_package"); + + helpers::verify_signature( + message, + &group_signature, + pubkey_package_tweaked.verifying_key(), + ); + + // Confirm the internal (untweaked) group key can be provided to access + // script spending paths under the output (tweaked) group key. + let (expected_parity, expected_tr_output_pubkey) = taproot_tweak_pubkey( + pubkey_package + .verifying_key() + .to_element() + .to_affine() + .x() + .into(), + &merkle_root, + ); + + let tr_output_point = pubkey_package_tweaked + .verifying_key() + .to_element() + .to_affine(); + + let tr_output_pubkey: [u8; 32] = tr_output_point.x().into(); + let tr_output_parity: bool = tr_output_point.y_is_odd().into(); + + assert_eq!( + tr_output_pubkey, expected_tr_output_pubkey, + "taproot output pubkey does not match" + ); + + assert_eq!( + tr_output_parity, expected_parity, + "taproot output pubkey parity bit does not match" + ); + + Ok(()) +} + +/// Emulates the BIP341 helper function: +/// +/// def taproot_tweak_pubkey(pubkey, h): +/// t = int_from_bytes(tagged_hash("TapTweak", pubkey + h)) +/// if t >= SECP256K1_ORDER: +/// raise ValueError +/// P = lift_x(int_from_bytes(pubkey)) +/// if P is None: +/// raise ValueError +/// Q = point_add(P, point_mul(G, t)) +/// return 0 if has_even_y(Q) else 1, bytes_from_int(x(Q)) +/// +fn taproot_tweak_pubkey(pubkey: [u8; 32], merkle_root: &[u8]) -> (bool, [u8; 32]) { + let prefix = Sha256::digest(b"TapTweak"); + let tweak_hash = Sha256::new() + .chain_update(prefix) + .chain_update(prefix) + .chain_update(pubkey) + .chain_update(merkle_root) + .finalize(); + let t = k256::Scalar::from( + k256::elliptic_curve::ScalarPrimitive::new(k256::U256::from_be_slice(&tweak_hash)).unwrap(), + ); + + let mut pubkey_even_bytes = [0x02; 33]; + pubkey_even_bytes[1..].copy_from_slice(&pubkey); + let pubkey_even = Secp256K1Group::deserialize(&pubkey_even_bytes).unwrap(); + + let tr_output_key = (pubkey_even + ProjectivePoint::GENERATOR * t).to_affine(); + (tr_output_key.y_is_odd().into(), tr_output_key.x().into()) +} diff --git a/frost-secp256k1/tests/helpers/samples.json b/frost-secp256k1/tests/helpers/samples.json index 210c6f27..54f6e1e1 100644 --- a/frost-secp256k1/tests/helpers/samples.json +++ b/frost-secp256k1/tests/helpers/samples.json @@ -1,6 +1,7 @@ { "identifier": "000000000000000000000000000000000000000000000000000000000000002a", + "proof_of_knowledge": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81", "element1": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", "element2": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", "scalar1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9d1c9e899ca306ad27fe1945de0242b81" -} \ No newline at end of file +} diff --git a/frost-secp256k1/tests/helpers/samples.rs b/frost-secp256k1/tests/helpers/samples.rs index 3afe78f9..11b84085 100644 --- a/frost-secp256k1/tests/helpers/samples.rs +++ b/frost-secp256k1/tests/helpers/samples.rs @@ -109,17 +109,12 @@ pub fn public_key_package() -> PublicKeyPackage { /// Generate a sample round1::Package. pub fn round1_package() -> round1::Package { - let serialized_scalar = <::Group as Group>::Field::serialize(&scalar1()); + let serialized_signature = Signature::new(element1(), scalar1()).serialize().unwrap(); + let signature = Signature::deserialize(&serialized_signature).unwrap(); + let serialized_element = ::Group::serialize(&element1()).unwrap(); - let serialized_signature = serialized_element - .as_ref() - .iter() - .chain(serialized_scalar.as_ref().iter()) - .cloned() - .collect::>(); let vss_commitment = VerifiableSecretSharingCommitment::deserialize(vec![serialized_element]).unwrap(); - let signature = Signature::deserialize(&serialized_signature).unwrap(); round1::Package::new(vss_commitment, signature) } diff --git a/gencode/src/main.rs b/gencode/src/main.rs index eda52d4e..8a420fca 100644 --- a/gencode/src/main.rs +++ b/gencode/src/main.rs @@ -121,17 +121,14 @@ fn write_docs( // To be able to replace the documentation properly, start from the end, which // will keep the string positions consistent for (old_name, _, old_start, old_end) in old_docs.iter().rev() { - let new_doc = docs - .get(old_name) - .unwrap_or_else(|| { - panic!( - "documentation for {} is not available in base file", - old_name - ) - }) - .1 - .clone(); - + let new_doc = docs.get(old_name).map(|v| v.1.clone()); + let Some(new_doc) = new_doc else { + eprintln!( + "WARNING: documentation for {} is not available in base file. This can mean it's a specific type for the ciphersuite, or that there is a bug in gencode", + old_name + ); + continue; + }; // Replaces ciphersuite-references in documentation let mut new_doc = new_doc.to_string(); for (old_n, new_n) in zip(original_suite_strings.iter(), new_suite_strings.iter()) { @@ -227,7 +224,13 @@ fn main() -> ExitCode { &std::fs::read_to_string(format!("{original_folder}/tests/helpers/samples.json")).unwrap(), ) .unwrap(); - for key in &["identifier", "element1", "element2", "scalar1"] { + for key in &[ + "identifier", + "proof_of_knowledge", + "element1", + "element2", + "scalar1", + ] { original_strings.push(samples[key].as_str().unwrap().to_owned()); } let original_strings: Vec<&str> = original_strings.iter().map(|s| s.as_ref()).collect(); @@ -290,6 +293,19 @@ fn main() -> ExitCode { "", ], ), + ( + "frost-secp256k1-tr", + &[ + "Secp256K1Sha256TR", + "secp256k1 curve (Taproot)", + "Secp256K1", + "FROST(secp256k1, SHA-256)", + "FROST-secp256k1-SHA256-TR-v1", + "secp256k1_tr_sha256", + "secp256k1_tr", + "", + ], + ), ] { // Some test use "sample" values. To make these tests work for another ciphersuites, // these values must be replaced. To make it cleaner, the strings are @@ -300,7 +316,13 @@ fn main() -> ExitCode { &std::fs::read_to_string(format!("{folder}/tests/helpers/samples.json")).unwrap(), ) .unwrap(); - for key in &["identifier", "element1", "element2", "scalar1"] { + for key in &[ + "identifier", + "proof_of_knowledge", + "element1", + "element2", + "scalar1", + ] { replacement_strings.push(samples[key].as_str().unwrap().to_owned()); } let replacement_strings: Vec<&str> =