diff --git a/Cargo.toml b/Cargo.toml index 49b7821..7eb191d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ pasta_curves = { version = "0.4", default-features = false } rand_core = { version = "0.6", default-features = false } serde = { version = "1", optional = true, features = ["derive"] } thiserror = { version = "1.0", optional = true } +frost-rerandomized = { git = "https://github.com/ZcashFoundation/frost.git", rev = "ffe5c57a1729c933b3ec8766ec96d2e6976a7ece", optional = true } [dependencies.zeroize] version = "1" @@ -40,12 +41,14 @@ optional = true [dev-dependencies] bincode = "1" criterion = "0.4" +hex = "0.4.3" proptest-derive = "0.3" lazy_static = "1.4" proptest = "1.0" rand = "0.8" rand_chacha = "0.3" serde_json = "1.0" +frost-rerandomized = { git = "https://github.com/ZcashFoundation/frost.git", rev = "ffe5c57a1729c933b3ec8766ec96d2e6976a7ece", features=["test-impl"] } # `alloc` is only used in test code [dev-dependencies.pasta_curves] @@ -53,9 +56,10 @@ features = ["alloc"] [features] std = ["blake2b_simd/std", "thiserror", "zeroize", "alloc", - "serde"] # conditional compilation for serde not complete (issue #9) + "serde", "frost-rerandomized"] # conditional compilation for serde not complete (issue #9) alloc = ["hex"] nightly = [] +frost = ["frost-rerandomized"] default = ["std"] [[bench]] diff --git a/src/frost.rs b/src/frost.rs new file mode 100644 index 0000000..042beab --- /dev/null +++ b/src/frost.rs @@ -0,0 +1,4 @@ +//! RedDSA with FROST. + +pub mod redjubjub; +pub mod redpallas; diff --git a/src/frost/redjubjub.rs b/src/frost/redjubjub.rs new file mode 100644 index 0000000..6178f58 --- /dev/null +++ b/src/frost/redjubjub.rs @@ -0,0 +1,406 @@ +//! Rerandomized FROST with Jubjub curve. +#![allow(non_snake_case)] +#![deny(missing_docs)] + +use group::GroupEncoding; +#[cfg(feature = "alloc")] +use group::{ff::Field as FFField, ff::PrimeField}; + +use frost_rerandomized::{ + frost_core::{frost, Ciphersuite, Field, FieldError, Group, GroupError}, + RandomizedParams, +}; + +use rand_core::{CryptoRng, RngCore}; + +use crate::{hash::HStar, private::Sealed, sapling}; + +/// An error type for the FROST(Jubjub, BLAKE2b-512) ciphersuite. +pub type Error = frost_rerandomized::frost_core::Error; + +/// An implementation of the FROST(Jubjub, BLAKE2b-512) ciphersuite scalar field. +#[derive(Clone, Copy)] +pub struct JubjubScalarField; + +impl Field for JubjubScalarField { + type Scalar = jubjub::Scalar; + + type Serialization = [u8; 32]; + + fn zero() -> Self::Scalar { + Self::Scalar::zero() + } + + fn one() -> Self::Scalar { + Self::Scalar::one() + } + + fn invert(scalar: &Self::Scalar) -> Result { + // [`Jubjub::Scalar`]'s Eq/PartialEq does a constant-time comparison using + // `ConstantTimeEq` + if *scalar == ::zero() { + Err(FieldError::InvalidZeroScalar) + } else { + Ok(Self::Scalar::invert(scalar).unwrap()) + } + } + + fn random(rng: &mut R) -> Self::Scalar { + Self::Scalar::random(rng) + } + + fn serialize(scalar: &Self::Scalar) -> Self::Serialization { + scalar.to_bytes() + } + + fn little_endian_serialize(scalar: &Self::Scalar) -> Self::Serialization { + Self::serialize(scalar) + } + + fn deserialize(buf: &Self::Serialization) -> Result { + match Self::Scalar::from_repr(*buf).into() { + Some(s) => Ok(s), + None => Err(FieldError::MalformedScalar), + } + } +} + +/// An implementation of the FROST(Jubjub, BLAKE2b-512) ciphersuite group. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct JubjubGroup; + +impl Group for JubjubGroup { + type Field = JubjubScalarField; + + type Element = jubjub::ExtendedPoint; + + type Serialization = [u8; 32]; + + fn cofactor() -> ::Scalar { + Self::Field::one() + } + + fn identity() -> Self::Element { + Self::Element::identity() + } + + fn generator() -> Self::Element { + sapling::SpendAuth::basepoint() + } + + fn serialize(element: &Self::Element) -> Self::Serialization { + element.to_bytes() + } + + fn deserialize(buf: &Self::Serialization) -> Result { + let point = Self::Element::from_bytes(buf); + + match Option::::from(point) { + Some(point) => { + if point == Self::identity() { + Err(GroupError::InvalidIdentityElement) + } else if point.is_torsion_free().into() { + Ok(point) + } else { + Err(GroupError::InvalidNonPrimeOrderElement) + } + } + None => Err(GroupError::MalformedElement), + } + } +} + +/// An implementation of the FROST(Jubjub, BLAKE2b-512) ciphersuite. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct JubjubBlake2b512; + +impl Ciphersuite for JubjubBlake2b512 { + type Group = JubjubGroup; + + type HashOutput = [u8; 64]; + + type SignatureSerialization = [u8; 64]; + + /// H1 for FROST(Jubjub, BLAKE2b-512) + fn H1(m: &[u8]) -> <::Field as Field>::Scalar { + HStar::::new(b"FROST_RedJubjubR") + .update(m) + .finalize() + } + + /// H2 for FROST(Jubjub, BLAKE2b-512) + fn H2(m: &[u8]) -> <::Field as Field>::Scalar { + HStar::::default().update(m).finalize() + } + + /// H3 for FROST(Jubjub, BLAKE2b-512) + fn H3(m: &[u8]) -> <::Field as Field>::Scalar { + HStar::::new(b"FROST_RedJubjubN") + .update(m) + .finalize() + } + + /// H4 for FROST(Jubjub, BLAKE2b-512) + fn H4(m: &[u8]) -> Self::HashOutput { + let mut state = blake2b_simd::Params::new() + .hash_length(64) + .personal(b"FROST_RedJubjubM") + .to_state(); + *state.update(m).finalize().as_array() + } + + /// H5 for FROST(Jubjub, BLAKE2b-512) + fn H5(m: &[u8]) -> Self::HashOutput { + let mut state = blake2b_simd::Params::new() + .hash_length(64) + .personal(b"FROST_RedJubjubC") + .to_state(); + *state.update(m).finalize().as_array() + } + + /// HDKG for FROST(Jubjub, BLAKE2b-512) + fn HDKG(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some( + HStar::::new(b"FROST_RedJubjubD") + .update(m) + .finalize(), + ) + } +} + +// Shorthand alias for the ciphersuite +type J = JubjubBlake2b512; + +/// A FROST(Jubjub, BLAKE2b-512) participant identifier. +pub type Identifier = frost::Identifier; + +/// FROST(Jubjub, BLAKE2b-512) keys, key generation, key shares. +pub mod keys { + use alloc::vec::Vec; + + use super::*; + + /// Allows all participants' keys to be generated using a central, trusted + /// dealer. + pub fn keygen_with_dealer( + max_signers: u16, + min_signers: u16, + mut rng: RNG, + ) -> Result<(Vec, PublicKeyPackage), Error> { + frost::keys::keygen_with_dealer(max_signers, min_signers, &mut rng) + } + + /// Secret and public key material generated by a dealer performing + /// [`keygen_with_dealer`]. + /// + /// # Security + /// + /// To derive a FROST(Jubjub, BLAKE2b-512) keypair, the receiver of the [`SecretShare`] *must* call + /// .into(), which under the hood also performs validation. + pub type SecretShare = frost::keys::SecretShare; + + /// A FROST(Jubjub, BLAKE2b-512) 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; + + pub mod dkg { + #![doc = include_str!("./redjubjub/dkg.md")] + 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 Round1SecretPackage = frost::keys::dkg::Round1SecretPackage; + + /// 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 Round1Package = frost::keys::dkg::Round1Package; + + /// 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 Round2SecretPackage = frost::keys::dkg::Round2SecretPackage; + + /// 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 Round2Package = frost::keys::dkg::Round2Package; + + /// Performs the first part of the distributed key generation protocol + /// for the given participant. + /// + /// It returns the [`Round1SecretPackage`] that must be kept in memory + /// by the participant for the other steps, and the [`Round1Package`] that + /// must be sent to other participants. + pub fn keygen_part1( + identifier: Identifier, + max_signers: u16, + min_signers: u16, + mut rng: R, + ) -> Result<(Round1SecretPackage, Round1Package), Error> { + frost::keys::dkg::keygen_part1(identifier, max_signers, min_signers, &mut rng) + } + + /// Performs the second part of the distributed key generation protocol + /// for the participant holding the given [`Round1SecretPackage`], + /// given the received [`Round1Package`]s received from the other participants. + /// + /// It returns the [`Round2SecretPackage`] that must be kept in memory + /// by the participant for the final step, and the [`Round2Package`]s that + /// must be sent to other participants. + pub fn keygen_part2( + secret_package: Round1SecretPackage, + round1_packages: &[Round1Package], + ) -> Result<(Round2SecretPackage, Vec), Error> { + frost::keys::dkg::keygen_part2(secret_package, round1_packages) + } + + /// Performs the third and final part of the distributed key generation protocol + /// for the participant holding the given [`Round2SecretPackage`], + /// given the received [`Round1Package`]s and [`Round2Package`]s received from + /// the other participants. + /// + /// 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 keygen_part3( + round2_secret_package: &Round2SecretPackage, + round1_packages: &[Round1Package], + round2_packages: &[Round2Package], + ) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::dkg::keygen_part3(round2_secret_package, round1_packages, round2_packages) + } + } +} + +/// FROST(Jubjub, BLAKE2b-512) Round 1 functionality and types. +pub mod round1 { + use frost_rerandomized::frost_core::frost::keys::SigningShare; + + use super::*; + /// Comprised of FROST(Jubjub, BLAKE2b-512) 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; + + /// 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( + participant_identifier: frost::Identifier, + secret: &SigningShare, + rng: &mut RNG, + ) -> (SigningNonces, SigningCommitments) + where + RNG: CryptoRng + RngCore, + { + frost::round1::commit::(participant_identifier, secret, rng) + } +} + +/// Generated by the coordinator of the signing operation and distributed to +/// each signing party. +pub type SigningPackage = frost::SigningPackage; + +/// FROST(Jubjub, BLAKE2b-512) Round 2 functionality and types, for signature share generation. +pub mod round2 { + use super::*; + + /// A FROST(Jubjub, BLAKE2b-512) 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; + + /// Generated by the coordinator of the signing operation and distributed to + /// each signing party + pub type SigningPackage = frost::SigningPackage; + + /// 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, + randomizer_point: &<::Group as Group>::Element, + ) -> Result { + frost_rerandomized::sign( + signing_package, + signer_nonces, + key_package, + randomizer_point, + ) + } +} + +/// A Schnorr signature on FROST(Jubjub, BLAKE2b-512). +pub type Signature = frost_rerandomized::frost_core::Signature; + +/// Verifies each FROST(Jubjub, BLAKE2b-512) 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: &round2::SigningPackage, + signature_shares: &[round2::SignatureShare], + pubkeys: &keys::PublicKeyPackage, + randomized_params: &RandomizedParams, +) -> Result { + frost_rerandomized::aggregate( + signing_package, + signature_shares, + pubkeys, + randomized_params, + ) +} + +/// A signing key for a Schnorr signature on FROST(Jubjub, BLAKE2b-512). +pub type SigningKey = frost_rerandomized::frost_core::SigningKey; + +/// A valid verifying key for Schnorr signatures on FROST(Jubjub, BLAKE2b-512). +pub type VerifyingKey = frost_rerandomized::frost_core::VerifyingKey; diff --git a/src/frost/redjubjub/README.md b/src/frost/redjubjub/README.md new file mode 100644 index 0000000..843ae35 --- /dev/null +++ b/src/frost/redjubjub/README.md @@ -0,0 +1,98 @@ +An implementation of Schnorr signatures on the Jubjub curve 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 +use reddsa::frost::redjubjub as frost; +use rand::thread_rng; +use std::collections::HashMap; + +let mut rng = thread_rng(); +let max_signers = 5; +let min_signers = 3; +let (shares, pubkeys) = frost::keys::keygen_with_dealer(max_signers, min_signers, &mut rng)?; + +// Verifies the secret shares from the dealer and stores them in a HashMap. +// In practice, each KeyPackage must be sent to its respective participant +// through a confidential and authenticated channel. +let key_packages: HashMap<_, _> = shares + .into_iter() + .map(|share| Ok((share.identifier, frost::keys::KeyPackage::try_from(share)?))) + .collect::>()?; + +let mut nonces = HashMap::new(); +let mut commitments = HashMap::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 as u16 + 1) { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + // Generate one (1) nonce and one SigningCommitments instance for each + // participant, up to _threshold_. + let (nonce, commitment) = frost::round1::commit( + participant_identifier, + key_packages[&participant_identifier].secret_share(), + &mut rng, + ); + // In practice, the nonces and commitments must be sent to the coordinator + // (or to every other participant if there is no coordinator) using + // an authenticated channel. + nonces.insert(participant_identifier, nonce); + commitments.insert(participant_identifier, commitment); +} + +// This is what the signature aggregator / coordinator needs to do: +// - decide what message to sign +// - take one (unused) commitment per signing participant +let mut signature_shares = Vec::new(); +let message = "message to sign".as_bytes(); +let comms = commitments.clone().into_values().collect(); +// In practice, the SigningPackage must be sent to all participants +// involved in the current signing (at least min_signers participants), +// using an authenticated channel (and confidential if the message is secret). +let signing_package = frost::SigningPackage::new(comms, message.to_vec()); + +//////////////////////////////////////////////////////////////////////////// +// 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.keys() { + let key_package = &key_packages[participant_identifier]; + + let nonces_to_use = &nonces[participant_identifier]; + + // Each participant generates their signature share. + let signature_share = frost::round2::sign(&signing_package, nonces_to_use, key_package)?; + + // In practice, the signature share must be sent to the Coordinator + // using an authenticated channel. + signature_shares.push(signature_share); +} + +//////////////////////////////////////////////////////////////////////////// +// Aggregation: collects the signing shares from all participants, +// generates the final signature. +//////////////////////////////////////////////////////////////////////////// + +// Aggregate (also verifies the signature shares) +let group_signature = frost::aggregate(&signing_package, &signature_shares[..], &pubkeys)?; + +// Check that the threshold signature can be verified by the group public +// key (the verification key). +assert!(pubkeys + .group_public + .verify(message, &group_signature) + .is_ok()); + +# Ok::<(), frost::Error>(()) +``` diff --git a/src/frost/redjubjub/dkg.md b/src/frost/redjubjub/dkg.md new file mode 100644 index 0000000..b3f7660 --- /dev/null +++ b/src/frost/redjubjub/dkg.md @@ -0,0 +1,157 @@ +# 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 a 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 [`Round1Package`]) to every other. In the second round, each receiver gets +their own package (a [`Round2Package`]). + +Between part 1 and 2, each participant needs to hold onto a [`Round1SecretPackage`] +that MUST be kept secret. Between part 2 and 3, each participant needs to hold +onto a [`Round2SecretPackage`]. + +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 +use rand::thread_rng; +use std::collections::HashMap; + +use reddsa::frost::redjubjub as frost; + +let mut rng = thread_rng(); + +//////////////////////////////////////////////////////////////////////////// +// Key generation, Round 1 +//////////////////////////////////////////////////////////////////////////// + +let max_signers = 5; +let min_signers = 3; + +// 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 = HashMap::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 = HashMap::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"); + let (secret_package, round1_package) = frost::keys::dkg::keygen_part1( + participant_identifier, + max_signers, + min_signers, + &mut rng, + )?; + + // 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, secret_package); + + // "Send" the round 1 package to all other participants. In this + // test this is simulated using a HashMap; 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(Vec::new) + .push(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 = HashMap::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 = HashMap::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 (round2_secret_package, round2_packages) = frost::keys::dkg::keygen_part2( + round1_secret_packages + .remove(&participant_identifier) + .unwrap(), + &received_round1_packages[&participant_identifier], + )?; + + // 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 HashMap; 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 round2_package in round2_packages { + received_round2_packages + .entry(round2_package.receiver_identifier) + .or_insert_with(Vec::new) + .push(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 = HashMap::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 = HashMap::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 (key_package, pubkey_package_for_participant) = frost::keys::dkg::keygen_part3( + &round2_secret_packages[&participant_identifier], + &received_round1_packages[&participant_identifier], + &received_round2_packages[&participant_identifier], + )?; + key_packages.insert(participant_identifier, key_package); + pubkey_packages.insert(participant_identifier, pubkey_package_for_participant); +} + +// With its own key package and the pubkey package, each participant can now proceed +// to sign with FROST. +# Ok::<(), frost::Error>(()) +``` diff --git a/src/frost/redpallas.rs b/src/frost/redpallas.rs new file mode 100644 index 0000000..d206289 --- /dev/null +++ b/src/frost/redpallas.rs @@ -0,0 +1,408 @@ +//! Rerandomized FROST with Pallas curve. +#![allow(non_snake_case)] +#![deny(missing_docs)] + +use group::GroupEncoding; +#[cfg(feature = "alloc")] +use group::{ff::Field as FFField, ff::PrimeField, Group as FFGroup}; +use pasta_curves::pallas; + +use frost_rerandomized::{ + frost_core::{frost, Ciphersuite, Field, FieldError, Group, GroupError}, + RandomizedParams, +}; + +use rand_core::{CryptoRng, RngCore}; + +use crate::{hash::HStar, orchard, private::Sealed}; + +/// An error type for the FROST(Pallas, BLAKE2b-512) ciphersuite. +pub type Error = frost_rerandomized::frost_core::Error; + +/// An implementation of the FROST(Pallas, BLAKE2b-512) ciphersuite scalar field. +#[derive(Clone, Copy)] +pub struct PallasScalarField; + +impl Field for PallasScalarField { + type Scalar = pallas::Scalar; + + type Serialization = [u8; 32]; + + fn zero() -> Self::Scalar { + Self::Scalar::zero() + } + + fn one() -> Self::Scalar { + Self::Scalar::one() + } + + fn invert(scalar: &Self::Scalar) -> Result { + // [`pallas::Scalar`]'s Eq/PartialEq does a constant-time comparison using + // `ConstantTimeEq` + if *scalar == ::zero() { + Err(FieldError::InvalidZeroScalar) + } else { + Ok(Self::Scalar::invert(scalar).unwrap()) + } + } + + fn random(rng: &mut R) -> Self::Scalar { + Self::Scalar::random(rng) + } + + fn serialize(scalar: &Self::Scalar) -> Self::Serialization { + // to_repr() endianess is implementation-specific, but this is OK since + // it is specific to [`pallas::Scalar`] which uses little-endian and that + // is what we want. + scalar.to_repr() + } + + fn little_endian_serialize(scalar: &Self::Scalar) -> Self::Serialization { + Self::serialize(scalar) + } + + fn deserialize(buf: &Self::Serialization) -> Result { + match pallas::Scalar::from_repr(*buf).into() { + Some(s) => Ok(s), + None => Err(FieldError::MalformedScalar), + } + } +} + +/// An implementation of the FROST(Pallas, BLAKE2b-512) ciphersuite group. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct PallasGroup; + +impl Group for PallasGroup { + type Field = PallasScalarField; + + type Element = pallas::Point; + + type Serialization = [u8; 32]; + + fn cofactor() -> ::Scalar { + Self::Field::one() + } + + fn identity() -> Self::Element { + Self::Element::identity() + } + + fn generator() -> Self::Element { + orchard::SpendAuth::basepoint() + } + + fn serialize(element: &Self::Element) -> Self::Serialization { + element.to_bytes() + } + + fn deserialize(buf: &Self::Serialization) -> Result { + let point = Self::Element::from_bytes(buf); + + match Option::::from(point) { + Some(point) => { + if point == Self::identity() { + Err(GroupError::InvalidIdentityElement) + } else { + Ok(point) + } + } + None => Err(GroupError::MalformedElement), + } + } +} + +/// An implementation of the FROST(Pallas, BLAKE2b-512) ciphersuite. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct PallasBlake2b512; + +impl Ciphersuite for PallasBlake2b512 { + type Group = PallasGroup; + + type HashOutput = [u8; 64]; + + type SignatureSerialization = [u8; 64]; + + /// H1 for FROST(Pallas, BLAKE2b-512) + fn H1(m: &[u8]) -> <::Field as Field>::Scalar { + HStar::::new(b"FROST_RedPallasR") + .update(m) + .finalize() + } + + /// H2 for FROST(Pallas, BLAKE2b-512) + fn H2(m: &[u8]) -> <::Field as Field>::Scalar { + HStar::::default().update(m).finalize() + } + + /// H3 for FROST(Pallas, BLAKE2b-512) + fn H3(m: &[u8]) -> <::Field as Field>::Scalar { + HStar::::new(b"FROST_RedPallasN") + .update(m) + .finalize() + } + + /// H4 for FROST(Pallas, BLAKE2b-512) + fn H4(m: &[u8]) -> Self::HashOutput { + let mut state = blake2b_simd::Params::new() + .hash_length(64) + .personal(b"FROST_RedPallasM") + .to_state(); + *state.update(m).finalize().as_array() + } + + /// H5 for FROST(Pallas, BLAKE2b-512) + fn H5(m: &[u8]) -> Self::HashOutput { + let mut state = blake2b_simd::Params::new() + .hash_length(64) + .personal(b"FROST_RedPallasC") + .to_state(); + *state.update(m).finalize().as_array() + } + + /// HDKG for FROST(Pallas, BLAKE2b-512) + fn HDKG(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some( + HStar::::new(b"FROST_RedPallasD") + .update(m) + .finalize(), + ) + } +} + +// Shorthand alias for the ciphersuite +type P = PallasBlake2b512; + +/// A FROST(Pallas, BLAKE2b-512) participant identifier. +pub type Identifier = frost::Identifier

; + +/// FROST(Pallas, BLAKE2b-512) keys, key generation, key shares. +pub mod keys { + use alloc::vec::Vec; + + use super::*; + + /// Allows all participants' keys to be generated using a central, trusted + /// dealer. + pub fn keygen_with_dealer( + max_signers: u16, + min_signers: u16, + mut rng: RNG, + ) -> Result<(Vec, PublicKeyPackage), Error> { + frost::keys::keygen_with_dealer(max_signers, min_signers, &mut rng) + } + + /// Secret and public key material generated by a dealer performing + /// [`keygen_with_dealer`]. + /// + /// # Security + /// + /// To derive a FROST(Pallas, BLAKE2b-512) keypair, the receiver of the [`SecretShare`] *must* call + /// .into(), which under the hood also performs validation. + pub type SecretShare = frost::keys::SecretShare

; + + /// A FROST(Pallas, BLAKE2b-512) 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

; + + pub mod dkg { + #![doc = include_str!("./redpallas/dkg.md")] + 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 Round1SecretPackage = frost::keys::dkg::Round1SecretPackage

; + + /// 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 Round1Package = frost::keys::dkg::Round1Package

; + + /// 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 Round2SecretPackage = frost::keys::dkg::Round2SecretPackage

; + + /// 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 Round2Package = frost::keys::dkg::Round2Package

; + + /// Performs the first part of the distributed key generation protocol + /// for the given participant. + /// + /// It returns the [`Round1SecretPackage`] that must be kept in memory + /// by the participant for the other steps, and the [`Round1Package`] that + /// must be sent to other participants. + pub fn keygen_part1( + identifier: Identifier, + max_signers: u16, + min_signers: u16, + mut rng: R, + ) -> Result<(Round1SecretPackage, Round1Package), Error> { + frost::keys::dkg::keygen_part1(identifier, max_signers, min_signers, &mut rng) + } + + /// Performs the second part of the distributed key generation protocol + /// for the participant holding the given [`Round1SecretPackage`], + /// given the received [`Round1Package`]s received from the other participants. + /// + /// It returns the [`Round2SecretPackage`] that must be kept in memory + /// by the participant for the final step, and the [`Round2Package`]s that + /// must be sent to other participants. + pub fn keygen_part2( + secret_package: Round1SecretPackage, + round1_packages: &[Round1Package], + ) -> Result<(Round2SecretPackage, Vec), Error> { + frost::keys::dkg::keygen_part2(secret_package, round1_packages) + } + + /// Performs the third and final part of the distributed key generation protocol + /// for the participant holding the given [`Round2SecretPackage`], + /// given the received [`Round1Package`]s and [`Round2Package`]s received from + /// the other participants. + /// + /// 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 keygen_part3( + round2_secret_package: &Round2SecretPackage, + round1_packages: &[Round1Package], + round2_packages: &[Round2Package], + ) -> Result<(KeyPackage, PublicKeyPackage), Error> { + frost::keys::dkg::keygen_part3(round2_secret_package, round1_packages, round2_packages) + } + } +} + +/// FROST(Pallas, BLAKE2b-512) Round 1 functionality and types. +pub mod round1 { + use frost_rerandomized::frost_core::frost::keys::SigningShare; + + use super::*; + /// Comprised of FROST(Pallas, BLAKE2b-512) 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

; + + /// 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( + participant_identifier: frost::Identifier

, + secret: &SigningShare

, + rng: &mut RNG, + ) -> (SigningNonces, SigningCommitments) + where + RNG: CryptoRng + RngCore, + { + frost::round1::commit::(participant_identifier, secret, rng) + } +} + +/// Generated by the coordinator of the signing operation and distributed to +/// each signing party. +pub type SigningPackage = frost::SigningPackage

; + +/// FROST(Pallas, BLAKE2b-512) Round 2 functionality and types, for signature share generation. +pub mod round2 { + use super::*; + + /// A FROST(Pallas, BLAKE2b-512) 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

; + + /// Generated by the coordinator of the signing operation and distributed to + /// each signing party + pub type SigningPackage = frost::SigningPackage

; + + /// 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, + randomizer_point: &<

::Group as Group>::Element, + ) -> Result { + frost_rerandomized::sign( + signing_package, + signer_nonces, + key_package, + randomizer_point, + ) + } +} + +/// A Schnorr signature on FROST(Pallas, BLAKE2b-512). +pub type Signature = frost_rerandomized::frost_core::Signature

; + +/// Verifies each FROST(Pallas, BLAKE2b-512) 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: &round2::SigningPackage, + signature_shares: &[round2::SignatureShare], + pubkeys: &keys::PublicKeyPackage, + randomized_params: &RandomizedParams

, +) -> Result { + frost_rerandomized::aggregate( + signing_package, + signature_shares, + pubkeys, + randomized_params, + ) +} + +/// A signing key for a Schnorr signature on FROST(Pallas, BLAKE2b-512). +pub type SigningKey = frost_rerandomized::frost_core::SigningKey

; + +/// A valid verifying key for Schnorr signatures on FROST(Pallas, BLAKE2b-512). +pub type VerifyingKey = frost_rerandomized::frost_core::VerifyingKey

; diff --git a/src/frost/redpallas/README.md b/src/frost/redpallas/README.md new file mode 100644 index 0000000..8bb7263 --- /dev/null +++ b/src/frost/redpallas/README.md @@ -0,0 +1,98 @@ +An implementation of Schnorr signatures on the Pallas curve 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 +use reddsa::frost::redpallas as frost; +use rand::thread_rng; +use std::collections::HashMap; + +let mut rng = thread_rng(); +let max_signers = 5; +let min_signers = 3; +let (shares, pubkeys) = frost::keys::keygen_with_dealer(max_signers, min_signers, &mut rng)?; + +// Verifies the secret shares from the dealer and stores them in a HashMap. +// In practice, each KeyPackage must be sent to its respective participant +// through a confidential and authenticated channel. +let key_packages: HashMap<_, _> = shares + .into_iter() + .map(|share| Ok((share.identifier, frost::keys::KeyPackage::try_from(share)?))) + .collect::>()?; + +let mut nonces = HashMap::new(); +let mut commitments = HashMap::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 as u16 + 1) { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + // Generate one (1) nonce and one SigningCommitments instance for each + // participant, up to _threshold_. + let (nonce, commitment) = frost::round1::commit( + participant_identifier, + key_packages[&participant_identifier].secret_share(), + &mut rng, + ); + // In practice, the nonces and commitments must be sent to the coordinator + // (or to every other participant if there is no coordinator) using + // an authenticated channel. + nonces.insert(participant_identifier, nonce); + commitments.insert(participant_identifier, commitment); +} + +// This is what the signature aggregator / coordinator needs to do: +// - decide what message to sign +// - take one (unused) commitment per signing participant +let mut signature_shares = Vec::new(); +let message = "message to sign".as_bytes(); +let comms = commitments.clone().into_values().collect(); +// In practice, the SigningPackage must be sent to all participants +// involved in the current signing (at least min_signers participants), +// using an authenticated channel (and confidential if the message is secret). +let signing_package = frost::SigningPackage::new(comms, message.to_vec()); + +//////////////////////////////////////////////////////////////////////////// +// 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.keys() { + let key_package = &key_packages[participant_identifier]; + + let nonces_to_use = &nonces[participant_identifier]; + + // Each participant generates their signature share. + let signature_share = frost::round2::sign(&signing_package, nonces_to_use, key_package)?; + + // In practice, the signature share must be sent to the Coordinator + // using an authenticated channel. + signature_shares.push(signature_share); +} + +//////////////////////////////////////////////////////////////////////////// +// Aggregation: collects the signing shares from all participants, +// generates the final signature. +//////////////////////////////////////////////////////////////////////////// + +// Aggregate (also verifies the signature shares) +let group_signature = frost::aggregate(&signing_package, &signature_shares[..], &pubkeys)?; + +// Check that the threshold signature can be verified by the group public +// key (the verification key). +assert!(pubkeys + .group_public + .verify(message, &group_signature) + .is_ok()); + +# Ok::<(), frost::Error>(()) +``` diff --git a/src/frost/redpallas/dkg.md b/src/frost/redpallas/dkg.md new file mode 100644 index 0000000..d349029 --- /dev/null +++ b/src/frost/redpallas/dkg.md @@ -0,0 +1,157 @@ +# 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 a 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 [`Round1Package`]) to every other. In the second round, each receiver gets +their own package (a [`Round2Package`]). + +Between part 1 and 2, each participant needs to hold onto a [`Round1SecretPackage`] +that MUST be kept secret. Between part 2 and 3, each participant needs to hold +onto a [`Round2SecretPackage`]. + +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 +use rand::thread_rng; +use std::collections::HashMap; + +use reddsa::frost::redpallas as frost; + +let mut rng = thread_rng(); + +//////////////////////////////////////////////////////////////////////////// +// Key generation, Round 1 +//////////////////////////////////////////////////////////////////////////// + +let max_signers = 5; +let min_signers = 3; + +// 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 = HashMap::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 = HashMap::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"); + let (secret_package, round1_package) = frost::keys::dkg::keygen_part1( + participant_identifier, + max_signers, + min_signers, + &mut rng, + )?; + + // 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, secret_package); + + // "Send" the round 1 package to all other participants. In this + // test this is simulated using a HashMap; 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(Vec::new) + .push(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 = HashMap::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 = HashMap::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 (round2_secret_package, round2_packages) = frost::keys::dkg::keygen_part2( + round1_secret_packages + .remove(&participant_identifier) + .unwrap(), + &received_round1_packages[&participant_identifier], + )?; + + // 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 HashMap; 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 round2_package in round2_packages { + received_round2_packages + .entry(round2_package.receiver_identifier) + .or_insert_with(Vec::new) + .push(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 = HashMap::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 = HashMap::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 (key_package, pubkey_package_for_participant) = frost::keys::dkg::keygen_part3( + &round2_secret_packages[&participant_identifier], + &received_round1_packages[&participant_identifier], + &received_round2_packages[&participant_identifier], + )?; + key_packages.insert(participant_identifier, key_package); + pubkey_packages.insert(participant_identifier, pubkey_package_for_participant); +} + +// With its own key package and the pubkey package, each participant can now proceed +// to sign with FROST. +# Ok::<(), frost::Error>(()) +``` diff --git a/src/hash.rs b/src/hash.rs index a6d52a7..cdf4d99 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -34,6 +34,19 @@ impl Default for HStar { } impl HStar { + // Only used by FROST code + #[allow(unused)] + pub(crate) fn new(personalization_string: &[u8]) -> Self { + let state = Params::new() + .hash_length(64) + .personal(personalization_string) + .to_state(); + Self { + state, + _marker: PhantomData::default(), + } + } + /// Add `data` to the hash, and return `Self` for chaining. pub fn update(&mut self, data: impl AsRef<[u8]>) -> &mut Self { self.state.update(data.as_ref()); diff --git a/src/lib.rs b/src/lib.rs index ee689f5..b7b9603 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,8 @@ extern crate std; pub mod batch; mod constants; mod error; +#[cfg(feature = "frost")] +pub mod frost; mod hash; pub mod orchard; pub mod sapling; diff --git a/src/signing_key.rs b/src/signing_key.rs index 266a2c9..773c4ff 100644 --- a/src/signing_key.rs +++ b/src/signing_key.rs @@ -31,8 +31,8 @@ pub struct SigningKey { pk: VerificationKey, } -impl<'a, T: SigType> From<&'a SigningKey> for VerificationKey { - fn from(sk: &'a SigningKey) -> VerificationKey { +impl From<&SigningKey> for VerificationKey { + fn from(sk: &SigningKey) -> VerificationKey { sk.pk } } diff --git a/tests/frost_redjubjub.rs b/tests/frost_redjubjub.rs new file mode 100644 index 0000000..79ee8c5 --- /dev/null +++ b/tests/frost_redjubjub.rs @@ -0,0 +1,85 @@ +#![cfg(feature = "frost")] + +use frost_rerandomized::frost_core::{Ciphersuite, Group, GroupError}; +use rand::thread_rng; + +use reddsa::{frost::redjubjub::JubjubBlake2b512, sapling}; + +#[test] +fn check_sign_with_dealer() { + let rng = thread_rng(); + + frost_rerandomized::frost_core::tests::check_sign_with_dealer::(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); + + // Check that the threshold signature can be verified by the `reddsa` crate + // public key (interoperability test) + + let sig = { + let bytes: [u8; 64] = group_signature.to_bytes().as_ref().try_into().unwrap(); + reddsa::Signature::::from(bytes) + }; + let pk_bytes = { + let bytes: [u8; 32] = group_pubkey.to_bytes().as_ref().try_into().unwrap(); + reddsa::VerificationKeyBytes::::from(bytes) + }; + + // Check that the verification key is a valid RedDSA verification key. + let pub_key = reddsa::VerificationKey::try_from(pk_bytes) + .expect("The test verification key to be well-formed."); + + // Check that signature validation has the expected result. + assert!(pub_key.verify(&msg, &sig).is_ok()); +} + +#[test] +fn check_sign_with_dkg() { + let rng = thread_rng(); + + frost_rerandomized::frost_core::tests::check_sign_with_dkg::(rng); +} + +#[test] +fn check_deserialize_identity() { + let encoded_identity = ::Group::serialize( + &::Group::identity(), + ); + let r = ::Group::deserialize(&encoded_identity); + assert_eq!(r, Err(GroupError::InvalidIdentityElement)); +} + +#[test] +fn check_deserialize_non_canonical() { + let encoded_generator = ::Group::serialize( + &::Group::generator(), + ); + let r = ::Group::deserialize(&encoded_generator); + assert!(r.is_ok()); + + // This is x = p + 3 which is non-canonical and maps to a valid point. + let encoded_point = + hex::decode("04000000fffffffffe5bfeff02a4bd5305d8a10908d83933487d9d2953a7ed73") + .unwrap() + .try_into() + .unwrap(); + let r = ::Group::deserialize(&encoded_point); + assert_eq!(r, Err(GroupError::MalformedElement)); +} + +#[test] +fn check_deserialize_non_prime_order() { + let encoded_point = + hex::decode("0300000000000000000000000000000000000000000000000000000000000000") + .unwrap() + .try_into() + .unwrap(); + let r = ::Group::deserialize(&encoded_point); + assert_eq!(r, Err(GroupError::InvalidNonPrimeOrderElement)); +} diff --git a/tests/frost_redpallas.rs b/tests/frost_redpallas.rs new file mode 100644 index 0000000..09b7441 --- /dev/null +++ b/tests/frost_redpallas.rs @@ -0,0 +1,74 @@ +#![cfg(feature = "frost")] + +use frost_rerandomized::frost_core::{Ciphersuite, Group, GroupError}; +use rand::thread_rng; + +use reddsa::{frost::redpallas::PallasBlake2b512, orchard}; + +#[test] +fn check_sign_with_dealer() { + let rng = thread_rng(); + + frost_rerandomized::frost_core::tests::check_sign_with_dealer::(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); + + // Check that the threshold signature can be verified by the `reddsa` crate + // public key (interoperability test) + + let sig = { + let bytes: [u8; 64] = group_signature.to_bytes().as_ref().try_into().unwrap(); + reddsa::Signature::::from(bytes) + }; + let pk_bytes = { + let bytes: [u8; 32] = group_pubkey.to_bytes().as_ref().try_into().unwrap(); + reddsa::VerificationKeyBytes::::from(bytes) + }; + + // Check that the verification key is a valid RedDSA verification key. + let pub_key = reddsa::VerificationKey::try_from(pk_bytes) + .expect("The test verification key to be well-formed."); + + // Check that signature validation has the expected result. + assert!(pub_key.verify(&msg, &sig).is_ok()); +} + +#[test] +fn check_sign_with_dkg() { + let rng = thread_rng(); + + frost_rerandomized::frost_core::tests::check_sign_with_dkg::(rng); +} + +#[test] +fn check_deserialize_identity() { + let encoded_identity = ::Group::serialize( + &::Group::identity(), + ); + let r = ::Group::deserialize(&encoded_identity); + assert_eq!(r, Err(GroupError::InvalidIdentityElement)); +} + +#[test] +fn check_deserialize_non_canonical() { + let encoded_generator = ::Group::serialize( + &::Group::generator(), + ); + let r = ::Group::deserialize(&encoded_generator); + assert!(r.is_ok()); + + // This is x = p + 3 which is non-canonical and maps to a valid point. + let encoded_point = + hex::decode("04000000ed302d991bf94c09fc98462200000000000000000000000000000040") + .unwrap() + .try_into() + .unwrap(); + let r = ::Group::deserialize(&encoded_point); + assert_eq!(r, Err(GroupError::MalformedElement)); +}