add Pallas and Jubjub ciphersuites and FROST support (#33)
* clippy fixes; remove old FROST code * add RedPallas ciphersuite and FROST support * organized code * simplified version * remove randomized_frost; point to frost-randomized crate * move rerandomized test to frost-rerandomized; clean up dependencies; add 'frost' feature * remove stale comment * add Jubjub support * add torsion and identity checks where needed; tests * Apply suggestions from code review Co-authored-by: Marek <mail@marek.onl> * Apply suggestions from code review Co-authored-by: Marek <mail@marek.onl> * change Jubjub serialize() to use to_bytes(); add comment to Pallas serialize() * update frost-rerandomized version * unpin nightly Rust in coverage.yaml * fix conditional hex dependency * move FROST code inside frost folder * Apply suggestions from code review Co-authored-by: Marek <mail@marek.onl> --------- Co-authored-by: Marek <mail@marek.onl> Co-authored-by: Deirdre Connolly <deirdre@zfnd.org>
This commit is contained in:
parent
507dcdf695
commit
08bb408846
|
@ -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]]
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
//! RedDSA with FROST.
|
||||
|
||||
pub mod redjubjub;
|
||||
pub mod redpallas;
|
|
@ -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<JubjubBlake2b512>;
|
||||
|
||||
/// 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<Self::Scalar, FieldError> {
|
||||
// [`Jubjub::Scalar`]'s Eq/PartialEq does a constant-time comparison using
|
||||
// `ConstantTimeEq`
|
||||
if *scalar == <Self as Field>::zero() {
|
||||
Err(FieldError::InvalidZeroScalar)
|
||||
} else {
|
||||
Ok(Self::Scalar::invert(scalar).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
fn random<R: RngCore + CryptoRng>(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<Self::Scalar, FieldError> {
|
||||
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() -> <Self::Field as Field>::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<Self::Element, GroupError> {
|
||||
let point = Self::Element::from_bytes(buf);
|
||||
|
||||
match Option::<Self::Element>::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]) -> <<Self::Group as Group>::Field as Field>::Scalar {
|
||||
HStar::<sapling::SpendAuth>::new(b"FROST_RedJubjubR")
|
||||
.update(m)
|
||||
.finalize()
|
||||
}
|
||||
|
||||
/// H2 for FROST(Jubjub, BLAKE2b-512)
|
||||
fn H2(m: &[u8]) -> <<Self::Group as Group>::Field as Field>::Scalar {
|
||||
HStar::<sapling::SpendAuth>::default().update(m).finalize()
|
||||
}
|
||||
|
||||
/// H3 for FROST(Jubjub, BLAKE2b-512)
|
||||
fn H3(m: &[u8]) -> <<Self::Group as Group>::Field as Field>::Scalar {
|
||||
HStar::<sapling::SpendAuth>::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<<<Self::Group as Group>::Field as Field>::Scalar> {
|
||||
Some(
|
||||
HStar::<sapling::SpendAuth>::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<J>;
|
||||
|
||||
/// 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<RNG: RngCore + CryptoRng>(
|
||||
max_signers: u16,
|
||||
min_signers: u16,
|
||||
mut rng: RNG,
|
||||
) -> Result<(Vec<SecretShare>, 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<J>;
|
||||
|
||||
/// 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<J>;
|
||||
|
||||
/// 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<J>;
|
||||
|
||||
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<J>;
|
||||
|
||||
/// 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<J>;
|
||||
|
||||
/// 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<J>;
|
||||
|
||||
/// 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<J>;
|
||||
|
||||
/// 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<R: RngCore + CryptoRng>(
|
||||
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<Round2Package>), 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<J>;
|
||||
|
||||
/// 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<J>;
|
||||
|
||||
/// 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<RNG>(
|
||||
participant_identifier: frost::Identifier<J>,
|
||||
secret: &SigningShare<J>,
|
||||
rng: &mut RNG,
|
||||
) -> (SigningNonces, SigningCommitments)
|
||||
where
|
||||
RNG: CryptoRng + RngCore,
|
||||
{
|
||||
frost::round1::commit::<J, RNG>(participant_identifier, secret, rng)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generated by the coordinator of the signing operation and distributed to
|
||||
/// each signing party.
|
||||
pub type SigningPackage = frost::SigningPackage<J>;
|
||||
|
||||
/// 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<J>;
|
||||
|
||||
/// Generated by the coordinator of the signing operation and distributed to
|
||||
/// each signing party
|
||||
pub type SigningPackage = frost::SigningPackage<J>;
|
||||
|
||||
/// 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: &<<J as Ciphersuite>::Group as Group>::Element,
|
||||
) -> Result<SignatureShare, Error> {
|
||||
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<J>;
|
||||
|
||||
/// 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<J>,
|
||||
) -> Result<Signature, Error> {
|
||||
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<J>;
|
||||
|
||||
/// A valid verifying key for Schnorr signatures on FROST(Jubjub, BLAKE2b-512).
|
||||
pub type VerifyingKey = frost_rerandomized::frost_core::VerifyingKey<J>;
|
|
@ -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::<Result<_, _>>()?;
|
||||
|
||||
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>(())
|
||||
```
|
|
@ -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>(())
|
||||
```
|
|
@ -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<PallasBlake2b512>;
|
||||
|
||||
/// 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<Self::Scalar, FieldError> {
|
||||
// [`pallas::Scalar`]'s Eq/PartialEq does a constant-time comparison using
|
||||
// `ConstantTimeEq`
|
||||
if *scalar == <Self as Field>::zero() {
|
||||
Err(FieldError::InvalidZeroScalar)
|
||||
} else {
|
||||
Ok(Self::Scalar::invert(scalar).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
fn random<R: RngCore + CryptoRng>(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<Self::Scalar, FieldError> {
|
||||
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() -> <Self::Field as Field>::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<Self::Element, GroupError> {
|
||||
let point = Self::Element::from_bytes(buf);
|
||||
|
||||
match Option::<Self::Element>::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]) -> <<Self::Group as Group>::Field as Field>::Scalar {
|
||||
HStar::<orchard::SpendAuth>::new(b"FROST_RedPallasR")
|
||||
.update(m)
|
||||
.finalize()
|
||||
}
|
||||
|
||||
/// H2 for FROST(Pallas, BLAKE2b-512)
|
||||
fn H2(m: &[u8]) -> <<Self::Group as Group>::Field as Field>::Scalar {
|
||||
HStar::<orchard::SpendAuth>::default().update(m).finalize()
|
||||
}
|
||||
|
||||
/// H3 for FROST(Pallas, BLAKE2b-512)
|
||||
fn H3(m: &[u8]) -> <<Self::Group as Group>::Field as Field>::Scalar {
|
||||
HStar::<orchard::SpendAuth>::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<<<Self::Group as Group>::Field as Field>::Scalar> {
|
||||
Some(
|
||||
HStar::<orchard::SpendAuth>::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<P>;
|
||||
|
||||
/// 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<RNG: RngCore + CryptoRng>(
|
||||
max_signers: u16,
|
||||
min_signers: u16,
|
||||
mut rng: RNG,
|
||||
) -> Result<(Vec<SecretShare>, 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<P>;
|
||||
|
||||
/// 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<P>;
|
||||
|
||||
/// 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<P>;
|
||||
|
||||
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<P>;
|
||||
|
||||
/// 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<P>;
|
||||
|
||||
/// 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<P>;
|
||||
|
||||
/// 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<P>;
|
||||
|
||||
/// 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<R: RngCore + CryptoRng>(
|
||||
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<Round2Package>), 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<P>;
|
||||
|
||||
/// 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<P>;
|
||||
|
||||
/// 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<RNG>(
|
||||
participant_identifier: frost::Identifier<P>,
|
||||
secret: &SigningShare<P>,
|
||||
rng: &mut RNG,
|
||||
) -> (SigningNonces, SigningCommitments)
|
||||
where
|
||||
RNG: CryptoRng + RngCore,
|
||||
{
|
||||
frost::round1::commit::<P, RNG>(participant_identifier, secret, rng)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generated by the coordinator of the signing operation and distributed to
|
||||
/// each signing party.
|
||||
pub type SigningPackage = frost::SigningPackage<P>;
|
||||
|
||||
/// 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<P>;
|
||||
|
||||
/// Generated by the coordinator of the signing operation and distributed to
|
||||
/// each signing party
|
||||
pub type SigningPackage = frost::SigningPackage<P>;
|
||||
|
||||
/// 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: &<<P as Ciphersuite>::Group as Group>::Element,
|
||||
) -> Result<SignatureShare, Error> {
|
||||
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<P>;
|
||||
|
||||
/// 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<P>,
|
||||
) -> Result<Signature, Error> {
|
||||
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<P>;
|
||||
|
||||
/// A valid verifying key for Schnorr signatures on FROST(Pallas, BLAKE2b-512).
|
||||
pub type VerifyingKey = frost_rerandomized::frost_core::VerifyingKey<P>;
|
|
@ -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::<Result<_, _>>()?;
|
||||
|
||||
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>(())
|
||||
```
|
|
@ -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>(())
|
||||
```
|
13
src/hash.rs
13
src/hash.rs
|
@ -34,6 +34,19 @@ impl<T: SigType> Default for HStar<T> {
|
|||
}
|
||||
|
||||
impl<T: SigType> HStar<T> {
|
||||
// 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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -31,8 +31,8 @@ pub struct SigningKey<T: SigType> {
|
|||
pk: VerificationKey<T>,
|
||||
}
|
||||
|
||||
impl<'a, T: SigType> From<&'a SigningKey<T>> for VerificationKey<T> {
|
||||
fn from(sk: &'a SigningKey<T>) -> VerificationKey<T> {
|
||||
impl<T: SigType> From<&SigningKey<T>> for VerificationKey<T> {
|
||||
fn from(sk: &SigningKey<T>) -> VerificationKey<T> {
|
||||
sk.pk
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<JubjubBlake2b512, _>(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::<JubjubBlake2b512, _>(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::<sapling::SpendAuth>::from(bytes)
|
||||
};
|
||||
let pk_bytes = {
|
||||
let bytes: [u8; 32] = group_pubkey.to_bytes().as_ref().try_into().unwrap();
|
||||
reddsa::VerificationKeyBytes::<sapling::SpendAuth>::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::<JubjubBlake2b512, _>(rng);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_deserialize_identity() {
|
||||
let encoded_identity = <JubjubBlake2b512 as Ciphersuite>::Group::serialize(
|
||||
&<JubjubBlake2b512 as Ciphersuite>::Group::identity(),
|
||||
);
|
||||
let r = <JubjubBlake2b512 as Ciphersuite>::Group::deserialize(&encoded_identity);
|
||||
assert_eq!(r, Err(GroupError::InvalidIdentityElement));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_deserialize_non_canonical() {
|
||||
let encoded_generator = <JubjubBlake2b512 as Ciphersuite>::Group::serialize(
|
||||
&<JubjubBlake2b512 as Ciphersuite>::Group::generator(),
|
||||
);
|
||||
let r = <JubjubBlake2b512 as Ciphersuite>::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 = <JubjubBlake2b512 as Ciphersuite>::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 = <JubjubBlake2b512 as Ciphersuite>::Group::deserialize(&encoded_point);
|
||||
assert_eq!(r, Err(GroupError::InvalidNonPrimeOrderElement));
|
||||
}
|
|
@ -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::<PallasBlake2b512, _>(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::<PallasBlake2b512, _>(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::<orchard::SpendAuth>::from(bytes)
|
||||
};
|
||||
let pk_bytes = {
|
||||
let bytes: [u8; 32] = group_pubkey.to_bytes().as_ref().try_into().unwrap();
|
||||
reddsa::VerificationKeyBytes::<orchard::SpendAuth>::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::<PallasBlake2b512, _>(rng);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_deserialize_identity() {
|
||||
let encoded_identity = <PallasBlake2b512 as Ciphersuite>::Group::serialize(
|
||||
&<PallasBlake2b512 as Ciphersuite>::Group::identity(),
|
||||
);
|
||||
let r = <PallasBlake2b512 as Ciphersuite>::Group::deserialize(&encoded_identity);
|
||||
assert_eq!(r, Err(GroupError::InvalidIdentityElement));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_deserialize_non_canonical() {
|
||||
let encoded_generator = <PallasBlake2b512 as Ciphersuite>::Group::serialize(
|
||||
&<PallasBlake2b512 as Ciphersuite>::Group::generator(),
|
||||
);
|
||||
let r = <PallasBlake2b512 as Ciphersuite>::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 = <PallasBlake2b512 as Ciphersuite>::Group::deserialize(&encoded_point);
|
||||
assert_eq!(r, Err(GroupError::MalformedElement));
|
||||
}
|
Loading…
Reference in New Issue