A Rust implementation of FROST: Flexible Round-Optimised Schnorr Threshold signatures by Chelsea Komlo and Ian Goldberg. This library provides tools for threshold signature schemes, enhancing cryptographic robustness by distributing signing authority among multiple participants.
FROST-Dalek provides an implementation of threshold Schnorr signatures, where a threshold number of participants must collaborate to create a valid signature on a message. Key features include:
- Flexible configuration of participants and threshold values
- Multi-round key generation via Distributed Key Generation (DKG) and trusted dealer setups
- Round-optimized partial signing and aggregation
However, please note that this library is experimental and has known limitations. It may require adjustments before being used in production.
It can only be used for communicating within one program on one machine, and therfore it is not suitable for a distributed setups, and should only be used for further development or testing purposes.
- Threshold Signature Creation: Configurable support for
nparticipants, where at leasttparticipants must sign to generate a valid signature. - Flexible Setup: Allows both DKG (for a decentralized setup) and dealer-based key generation (for trusted setups).
- No-std Compliance: Most of the crate is
no_stdcompatible, though some parts requirestddue to dependencies on collections such asHashMap.
- No Communication: The library does not include a communication layer for distributed setups, requiring users to implement serialization and data exchange.
- Private Struct Members: Some struct fields are private, particularly in the structs than need to be communicated between participants, namely
Participant,SecretShareandPartialThresholdSignature. Because they need to be serialized and sent between participants, this needs to be addressed. - API Stability: This code is experimental and subject to change. Compatibility with future versions may be impacted as FROST protocol designs evolve.
- Add serialization support: The
Participant,SecretShareandPartialThresholdSignaturestructs need to be de/serializable so they can be broadcasted to all or sent to specific participants. Ideally support for various serialization formats should be added. This can be done by offering features such asserdeorbincode. - Add transport layer: In addition to the above, a transport layer is needed to enable communication between participants. To fully align with decenteralization principles, this would ideally be done using p2p communication, which can be facilitated by the rust-libp2p crate.
- Expose trusted dealer keygen implementation: At the moment, the
DealtParticipantcan only be used internally, as it's fields are not public and it has no implementation. This prevents any usage and serialization. Since a trusted dealer is explicitly one way to perform key generation, (with the other being distributed key generation), this should be made possible. - Offer a better API to divide secret shares among other participants: Currently the library user needs to figure out how to distribute each participant's secret shares among other participants, where failure to do so correctly causes verification errors. This logic should be solved by the library and offered as a function of the keygen module.
/// A commitment to the dealer's secret polynomial coefficients for Feldman's
/// verifiable secret sharing scheme.
#[derive(Clone, Debug)]
pub struct VerifiableSecretSharingCommitment(pub(crate) Vec<RistrettoPoint>);
/// A participant created by a trusted dealer.
///
/// This can be used to create the participants' keys and secret shares without
/// having to do secret sharing or zero-knowledge proofs. It's mostly provided
/// for testing and debugging purposes, but there is nothing wrong with using it
/// if you have trust in the dealer to not forge rogue signatures.
#[derive(Clone, Debug)]
pub struct DealtParticipant {
pub(crate) secret_share: SecretShare,
pub(crate) public_key: IndividualPublicKey,
pub(crate) group_key: RistrettoPoint,
}- Create meaningful error types: Currently there is no custom error types defined in the library, and errros are in most cases a Unit type (
Err(())) or a data structure. An error enum defining the valid error cases should be created with conversions to and from other errors where applicable.
impl DistributedKeyGeneration<RoundOne> {
//...
pub fn new(/*...*/) -> Result<Self, Vec<u32>>
//...
pub fn to_round_two(/*...*/) -> Result<DistributedKeyGeneration<RoundTwo>, ()>{}
//...
if my_secret_shares.len() != self.state.parameters.n as usize - 1 {
return Err(());
}- The secret key index should and might as well be public, as it can be obtained by calling
sk.to_public().indexwhich is an unnecessary overhead
/// A secret key, used by one participant in a threshold signature scheme, to sign a message.
#[derive(Debug, Zeroize)]
#[zeroize(drop)]
pub struct SecretKey {
/// The participant index to which this key belongs.
pub(crate) index: u32,
/// The participant's long-lived secret share of the group signing key.
pub(crate) key: Scalar,
}
impl SecretKey {
/// Derive the corresponding public key for this secret key.
pub fn to_public(&self) -> IndividualPublicKey {
let share = &RISTRETTO_BASEPOINT_TABLE * &self.key;
IndividualPublicKey {
index: self.index,
share,
}
}
}- The proof verification api seems unnecessarily cumbersome, since it is using members/fns of the
Participantstruct, namelyproof_of_secret_key,indexandpublic_key()
participant.proof_of_secret_key.verify(&participant.index, &participant.public_key().unwrap());It can probably be replaced with:
impl Participant {
...
fn verify_knowledge_of_secret_key(&self, index: u32, public_key: RistrettoPoint) -> Result<(), ()> {
self.proof_of_secret_key.verify(&index, self.public_key()?)
}
}Add frost-dalek to your Cargo.toml:
[dependencies]
frost-dalek = "0.2.3"- Initial participant creation and publication:
Each participant creates a
Participantinstance, which returns aCoefficientsandParticipantstructs, and publishes only theParticipant
let parameters = Parameters { n: 5, t: 3 };
let participant_index = 1;
let (participant, coefficients) = Participant::new(¶meters, participant_index);
// Publish the participant
publish_participant(&participant);- Collect other participants: Each participant collects all other participants:
for i in 1..=n {
if i != participant_index {
let handle = task::spawn(async move {
println!("Waiting for participant {}", i);
loop {
match read_published_participant(i)
//...- Initialize DKG round 1: Each participant initiates the first DKG round, passing the coefficients and other participants to it
let dkg = DistributedKeyGeneration::new(
¶meters,
&participant.index,
&coefficients,
&mut other_participants,
).map_err(|e| Error::MisbehavingDkg(e))?; - Secret share broadcast: Each participant broadcasts their secret shares
if let Ok(their_secret_shares) = dkg.their_secret_shares() {
publish_their_secret_shares(participant_index, their_secret_shares);
}And collects the other participants' secret shares
let my_secret_shares = collect_my_secret_shares(participant_index, parameters);- Progress to DKG round 2: Each participant initiates the second DKG round, passing their secret shares to it
let dkg_round2 = dkg.to_round_two(my_secret_shares)
.map_err(|_| Error::InsufficientSecretShares);- Finish DKG: Each participant completes the DKG process, deriving the group public key and individual secret key.
let pk = participant.public_key().expect("Participant has no public key");
let (group_key, secret_key) = dkg_round2.finish(pk).map_err(|_| Error::DkgFinishFailure);And publish the public key:
publish_public_key(participant_index, public_comshares, pk);- Sign message: Each participant signs a message using their individual secret key share, producing a partial signature.
let message_hash = compute_message_hash(&CONTEXT[..], &MESSAGE[..]);
let partial_signature = secret_key
.sign(&message_hash, &group_key, &mut secret_comshares, 0, signers)
.map_err(|e| Error::Signing(e))?;
// Publish the partial signature
publish_partial_signature(participant_index, partial_signature).await?;- Aggregate Signatures: The designated aggregator, which could be anyone, potentially first come first served, collects the partial signatures and assembles them into a complete threshold signature.
let mut aggregator = SignatureAggregator::new(parameters, group_key, &CONTEXT[..], &MESSAGE[..]);
for partial_sig in collect_partial_signatures(parameters).await? {
aggregator.include_partial_signature(partial_sig);
}
let threshold_signature = aggregator.aggregate()?;- Verify signature: Each participant verifies the threshold signature using the group public key to ensure authenticity.
let is_verified = threshold_signature.verify(&group_key, &message_hash)?;
println!("Signature verification result: {}", is_verified);