diff --git a/schnorr_fun/src/frost/chilldkg.rs b/schnorr_fun/src/frost/chilldkg.rs new file mode 100644 index 0000000..b02af72 --- /dev/null +++ b/schnorr_fun/src/frost/chilldkg.rs @@ -0,0 +1,1266 @@ +//! Our take on the WIP *[ChillDKG: Distributed Key Generation for FROST][ChillDKG]* spec +//! +//! ChillDKG is a modular distributed key generation protocol. At the end all the intended parties +//! have a valid `t-of-n` [Shamir secret sharing] of a secret key without requiring a trusted party +//! or even an honest majority. +//! +//! The [WIP spec][ChillDKG] defines two roles: +//! +//! - *Coordinator*: A central party who relays and aggregates messages between the other parties. +//! - *Participants*: The parties who provide secret input and receive secret shares as output from the protocol. +//! +//! In this implementation we split "participants" into two further roles: +//! +//! - *Contributors*: parties that provide secret input into the key generation +//! - *Receivers*: parties that receive a secret share from the protocol. +//! +//! We see a benefit to having parties that provide secret input but do not receive secret output. +//! The main example of this is having the coordinator itself be an *Contributor* too. In the context +//! of a Bitcoin hardware wallet, the coordinator is usually the only party with access to the +//! internet therefore, if the coordinator contributes input honestly, even if all the non-internet +//! connected devices are malicious the *remote* adversary (who set the code of the malicious +//! device) will not know the secret key. In fact, the adversary would have to recover `t` devices +//! and extract their internal state to reconstruct the key. This is nice, because *in theory* and +//! in this limited sense it gives the attacker no advantage from controlling the code of the +//! signing devices (anyone who wants to reconstruct the key already needs `t` shares). +//! +//! ## Variants +//! +//! The spec comes in three variants: +//! +//! - [`simplepedpop`]: bare bones FROST key generation +//! - [`encpedpop`]: Adds encryption to the secret input so the coordinator can aggregate encrypted secret shares. +//! - [`certpedpop`]: `encpedpop` where each party also certifies the output so they can cryptographically convince each other that the key generation was successful. +//! +//! [ChillDKG]: https://github.com/BlockstreamResearch/bip-frost-dkg +use crate::{frost::*, Schnorr}; +use alloc::{ + collections::{BTreeMap, BTreeSet}, + vec::Vec, +}; +use secp256kfun::{ + hash::{Hash32, HashAdd}, + nonce::NonceGen, + poly, + prelude::*, + rand_core, KeyPair, +}; + +/// SimplePedPop is a bare bones secure distributed key generation algorithm that leaves a lot left +/// up to the application. +/// +/// The application must figure out: +/// +/// - How to secretly transport secret share contribution from each contributor to their intended destination +/// - Checking that each party got the correct output by comparing [`AggKeygenInput::cert_bytes`] on each of them. +/// +/// [`AggKeygenInput::cert_bytes`]: simplepedpop::AggKeygenInput::cert_bytes +pub mod simplepedpop { + use super::*; + use crate::{Message, Signature}; + use alloc::{ + collections::{BTreeMap, BTreeSet}, + vec::Vec, + }; + use secp256kfun::hash::Hash32; + + /// A party that generates secret input to the key generation. You need at least one of these + /// and if at least one of these parties is honest then the final secret key will not be known by an + /// attacker (unless they obtain `t` shares!). + #[derive(Clone, Debug)] + #[cfg_attr( + feature = "bincode", + derive(crate::fun::bincode::Encode, crate::fun::bincode::Decode), + bincode(crate = "crate::fun::bincode") + )] + #[cfg_attr( + feature = "serde", + derive(crate::fun::serde::Deserialize, crate::fun::serde::Serialize), + serde(crate = "crate::fun::serde") + )] + pub struct Contributor { + my_key_contrib: Point, + my_index: u32, + } + + impl Contributor { + /// Generates the keygen input for a party at `my_index`. Note that `my_index` + /// has nothing to do with the "receiver" index (the `PartyIndex` of share receivers). If + /// there are `n` `KeyGenInputParty`s then each party must be assigned an index from `0` to `n-1`. + /// + /// This method return `Self` to retain the state of the protocol which is needded to verify + /// the aggregated input later on. + pub fn gen_keygen_input( + schnorr: &Schnorr, + threshold: u32, + share_receivers: &BTreeSet, + my_index: u32, + rng: &mut impl rand_core::RngCore, + ) -> (Self, KeygenInput, SecretKeygenInput) + where + H: Hash32, + NG: NonceGen, + { + let secret_poly = poly::scalar::generate(threshold as usize, rng); + let pop_keypair = KeyPair::new_xonly(secret_poly[0]); + // XXX The thing that's singed differs from the spec + let pop = schnorr.sign(&pop_keypair, Message::::empty()); + let com = poly::scalar::to_point_poly(&secret_poly); + + let shares = share_receivers + .iter() + .map(|index| (*index, poly::scalar::eval(&secret_poly, *index))) + .collect(); + let self_ = Self { + my_key_contrib: com[0], + my_index, + }; + let msg = KeygenInput { com, pop }; + (self_, msg, shares) + } + + /// Verifies that the coordinator has honestly included this party's input into the + /// aggregated input. + /// + /// This passing by itself doesn't mean that the key generation was successful. All + /// `Contributor`s must agree on this fact and all parties must have received the same + /// `AggKeygenInput` and validated it. + pub fn verify_agg_input( + self, + agg_input: &AggKeygenInput, + ) -> Result<(), ContributionDidntMatch> { + let my_got_contrib = agg_input + .key_contrib + .get(self.my_index as usize) + .map(|(point, _)| *point); + let my_expected_contrib = self.my_key_contrib; + if Some(my_expected_contrib) != my_got_contrib { + return Err(ContributionDidntMatch); + } + + Ok(()) + } + } + + /// Produced by [`Contributor::gen_keygen_input`]. This is sent from the each + /// `Contributor` to the *coordinator*. + #[cfg_attr( + feature = "bincode", + derive(crate::fun::bincode::Encode, crate::fun::bincode::Decode), + bincode(crate = "crate::fun::bincode") + )] + #[cfg_attr( + feature = "serde", + derive(crate::fun::serde::Deserialize, crate::fun::serde::Serialize), + serde(crate = "crate::fun::serde") + )] + #[derive(Clone, Debug, PartialEq)] + pub struct KeygenInput { + /// The polynomial commitment of the contributor. + pub com: Vec, + /// Their proof-of-possession signature on the first coefficient. + pub pop: Signature, + } + + /// Map from party index to secret share contribution from the [`Contributor`]. + /// + /// Each entry in the map must be sent to the corresponding party. + pub type SecretKeygenInput = BTreeMap>; + + /// Stores the state of the coordinator as it aggregates inputs from [`Contributor`]s. + #[derive(Clone, Debug, PartialEq)] + pub struct Coordinator { + threshold: u32, + inputs: BTreeMap>, + } + + impl Coordinator { + /// Creates a new coordinator with: + /// + /// - `threshold`: of key we're trying to generate + /// - `n_contributors`: The number of [`Contributor`]s + pub fn new(threshold: u32, n_contributors: u32) -> Self { + assert!(threshold > 0); + Self { + threshold, + inputs: (0..n_contributors).map(|i| (i, None)).collect(), + } + } + + /// Adds an `input` from a [`Contributor`]. + /// + /// Note verifying this is the correct input from the correct party is up to your application! + pub fn add_input( + &mut self, + schnorr: &Schnorr, + from: u32, + input: KeygenInput, + ) -> Result<(), &'static str> { + let entry = match self.inputs.get_mut(&from) { + Some(maybe_input) => match maybe_input { + Some(_) => return Err("we already have input from this party"), + none => none, + }, + None => return Err("no input expected from this party"), + }; + if input.com.len() != self.threshold as usize { + return Err("input has the wrong threshold"); + } + + let (first_coeff_even_y, _) = input.com[0].into_point_with_even_y(); + if !schnorr.verify(&first_coeff_even_y, Message::::empty(), &input.pop) { + return Err("☠ pop didn't verify"); + } + *entry = Some(input); + + Ok(()) + } + + /// Which [`Contributor`]s are we missing input from. + pub fn missing_from(&self) -> BTreeSet { + self.inputs + .iter() + .filter_map(|(index, input)| match input { + None => Some(*index), + Some(_) => None, + }) + .collect() + } + + /// Has the coordinator received input from each [`Contributor`]. + pub fn is_finished(&self) -> bool { + self.inputs.values().all(|v| v.is_some()) + } + + /// Try and finish input aggregation step. + /// + /// Returns `None` if [`is_finished`] returns `false`. + /// + /// [`is_finished`]: Self::is_finished + pub fn finish(self) -> Option { + if !self.is_finished() { + return None; + } + let inputs = self.inputs.into_values().flatten().collect::>(); + // The "key contributions" are separated out and treated specially since they can't be + // aggregated by the coordinator since each one needs to be validated against a + // proof-of-possesson. + let key_contrib = inputs + .iter() + .map(|message| (message.com[0], message.pop)) + .collect(); + + // The rest of the coefficients can be aggregated + let mut agg_poly = + vec![Point::::zero(); self.threshold as usize - 1]; + for message in inputs { + for (i, com) in message.com[1..].iter().enumerate() { + agg_poly[i] += com + } + } + + let agg_poly = poly::point::normalize(agg_poly).collect::>(); + + Some(AggKeygenInput { + key_contrib, + agg_poly, + }) + } + } + + /// Key generation inputs after being aggregated by the coordinator + #[derive(Clone, Debug, PartialEq)] + #[cfg_attr( + feature = "bincode", + derive(crate::fun::bincode::Encode, crate::fun::bincode::Decode), + bincode(crate = "crate::fun::bincode") + )] + #[cfg_attr( + feature = "serde", + derive(crate::fun::serde::Deserialize, crate::fun::serde::Serialize), + serde(crate = "crate::fun::serde") + )] + pub struct AggKeygenInput { + /// The key contribution from each [`Contributor`] + pub key_contrib: Vec<(Point, Signature)>, + /// The aggregated non-constant term polynomial + pub agg_poly: Vec>, + } + + impl AggKeygenInput { + /// Gets the `SharedKey` that this aggregated input produces. + /// + /// ## Security + /// + /// ⚠ Just because you can call this doesn't mean you can use the `SharedKey` securely yet! + /// + /// You have to have checked that all parties (contributors and receivers) think it's valid + /// *and* have the same copy first. + pub fn shared_key(&self) -> SharedKey { + let public_key = self + .key_contrib + .iter() + .fold(Point::zero(), |agg, (point, _)| g!(agg + point)) + .normalize(); + let mut poly = self.agg_poly.clone(); + poly.insert(0, public_key); + SharedKey::from_poly(poly) + } + + /// The *certification* bytes. Checking all parties have the same output of this function is + /// enough to check they have the same `AggKeygenInput`. + /// + /// In `simplepedpop` this is just the coefficients of the polynomial. + pub fn cert_bytes(&self) -> Vec { + let mut cert_bytes = vec![]; + cert_bytes.extend((self.agg_poly.len() as u32).to_be_bytes()); + for coeff in self.shared_key().point_polynomial() { + cert_bytes.extend(coeff.to_bytes()); + } + cert_bytes + } + } + + /// Receive secret share after summing the secret input from each [`Contributor`] with + /// [`collect_secret_inputs`] and getting the `AggKeygenInput` from the coordinator. + /// + /// This also validates `agg_input`. + pub fn receive_share( + schnorr: &Schnorr, + agg_input: &AggKeygenInput, + secret_share: SecretShare, + ) -> Result, ReceiveShareError> + where + H: Hash32, + { + for (key_contrib, pop) in &agg_input.key_contrib { + let (first_coeff_even_y, _) = key_contrib.into_point_with_even_y(); + if !schnorr.verify(&first_coeff_even_y, Message::::empty(), pop) { + return Err(ReceiveShareError::InvalidPop); + } + } + + let shared_key = agg_input.shared_key(); + + let paired_secret_share = shared_key + .pair_secret_share(secret_share) + .ok_or(ReceiveShareError::InvalidSecretShare)?; + + Ok(paired_secret_share) + } + + /// Collect the secret inputs from each [`Contributor`] destined for a particular a party at `PartyIndex`. + pub fn collect_secret_inputs( + my_index: PartyIndex, + secret_share_inputs: impl IntoIterator>, + ) -> SecretShare { + let mut sum = s!(0); + for share in secret_share_inputs { + sum += share; + } + + SecretShare { + index: my_index, + share: sum, + } + } + + /// Simulate running a key generation with `simplepedpop`. + /// + /// This calls all the other functions defined in this module to get the whole job done on a + /// single computer by simulating all the other parties. + pub fn simulate_keygen( + schnorr: &Schnorr, + threshold: u32, + n_receivers: u32, + n_generators: u32, + rng: &mut impl rand_core::RngCore, + ) -> (SharedKey, Vec>) + where + H: Hash32, + NG: NonceGen, + { + let share_receivers = (1..=n_receivers) + .map(|i| PartyIndex::from(NonZeroU32::new(i).unwrap())) + .collect::>(); + + let mut aggregator = Coordinator::new(threshold, n_generators); + let mut contributors = vec![]; + let mut secret_inputs = BTreeMap::>>::default(); + + for i in 0..n_generators { + let (contributor, to_coordinator, shares) = + Contributor::gen_keygen_input(schnorr, threshold, &share_receivers, i, rng); + + contributors.push(contributor); + aggregator.add_input(schnorr, i, to_coordinator).unwrap(); + + for (receiver_index, share) in shares { + secret_inputs.entry(receiver_index).or_default().push(share); + } + } + + let agg_input = aggregator.finish().unwrap(); + + for contributor in contributors { + contributor.verify_agg_input(&agg_input).unwrap(); + } + + let mut paired_shares = vec![]; + + for receiver in share_receivers { + let secret_share = + collect_secret_inputs(receiver, secret_inputs.remove(&receiver).unwrap()); + let paired_share = receive_share(schnorr, &agg_input, secret_share).unwrap(); + paired_shares.push(paired_share.non_zero().unwrap()); + } + + (agg_input.shared_key().non_zero().unwrap(), paired_shares) + } + + /// The input the contributor provided has been manipulated + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct ContributionDidntMatch; + + impl core::fmt::Display for ContributionDidntMatch { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "the contribution assigned to us was not what we contributed" + ) + } + } + + #[cfg(feature = "std")] + impl std::error::Error for ContributionDidntMatch {} + + /// The [`AggKeygenInput`] was invalid so a valid secret share couldn't be extracted. + #[derive(Clone, Copy, Debug, PartialEq)] + pub enum ReceiveShareError { + /// Invalid POP for one of the contributions + InvalidPop, + /// The secret share we got was invalid + InvalidSecretShare, + } + + impl core::fmt::Display for ReceiveShareError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", match self { + ReceiveShareError::InvalidPop => "Invalid POP for one of the contributions", + ReceiveShareError::InvalidSecretShare => + "The share extracted from the key generation was invalid", + }) + } + } +} + +/// `encpedpop` is built on top of [`simplepedpop`] to add share encryption. +/// +/// Each per recipient secret key is explicitly encrypted to each recipient and sent through the +/// coordinator. This simplifies things a bit since all messages are to or from the coordinator. The +/// coordinator also aggregates the ciphertexts so communication is reduced to linear in the number +/// of participants. +/// +/// The application still must figure out when all parties agree on the [`AggKeygenInput`] before +/// using it. +/// +/// [`AggKeygenInput`]: encpedpop::AggKeygenInput +pub mod encpedpop { + use super::{simplepedpop, *}; + use crate::frost::{PairedSecretShare, PartyIndex, SecretShare, SharedKey}; + + /// A party that generates secret input to the key generation. You need at least one of these + /// and if at least one of these parties is honest then the final secret key will not be known by an + /// attacker (unless they obtain `t` shares!). + #[derive(Clone, Debug)] + #[cfg_attr( + feature = "bincode", + derive(crate::fun::bincode::Encode, crate::fun::bincode::Decode), + bincode(crate = "crate::fun::bincode") + )] + #[cfg_attr( + feature = "serde", + derive(crate::fun::serde::Deserialize, crate::fun::serde::Serialize), + serde(crate = "crate::fun::serde") + )] + pub struct Contributor { + inner: simplepedpop::Contributor, + } + + impl Contributor { + /// Generates the keygen input for a party at `my_index`. Note that `my_index` + /// has nothing to do with the "receiver" index (the `PartyIndex` of share receivers). If + /// there are `n` `KeyGenInputParty`s then each party must be assigned an index from `0` to `n-1`. + /// + /// This method return `Self` to retain the state of the protocol which is needded to verify + /// the aggregated input later on. + pub fn gen_keygen_input( + schnorr: &Schnorr, + threshold: u32, + receiver_encryption_keys: &BTreeMap, + my_index: u32, + rng: &mut impl rand_core::RngCore, + ) -> (Self, KeygenInput) + where + H: Hash32, + NG: NonceGen, + { + let multi_nonce_keypair = KeyPair::::new(Scalar::random(rng)); + + let share_receivers = receiver_encryption_keys.keys().cloned().collect(); + let (inner_state, inner_keygen_input, mut shares) = + simplepedpop::Contributor::gen_keygen_input( + schnorr, + threshold, + &share_receivers, + my_index, + rng, + ); + let encryption_jobs = receiver_encryption_keys + .iter() + .map(|(receiver, encryption_key)| { + ( + *receiver, + (*encryption_key, shares.remove(receiver).unwrap()), + ) + }) + .collect(); + assert!(shares.is_empty()); + let encrypted_shares = encrypt::(encryption_jobs, multi_nonce_keypair); + let keygen_input = KeygenInput { + inner: inner_keygen_input, + encrypted_shares, + encryption_nonce: multi_nonce_keypair.public_key(), + }; + + (Contributor { inner: inner_state }, keygen_input) + } + + /// Verifies that the coordinator has honestly included this party's input into the + /// aggregated input. + /// + /// This passing by itself doesn't mean that the key generation was successful. All + /// `Contributor`s must agree on this fact and all parties must have received the same + /// `AggKeygenInput` and validated it. + pub fn verify_agg_input( + self, + agg_keygen_input: &AggKeygenInput, + ) -> Result<(), simplepedpop::ContributionDidntMatch> { + self.inner.verify_agg_input(&agg_keygen_input.inner)?; + Ok(()) + } + } + + /// Key generation inputs after being aggregated by the coordinator + #[derive(Clone, Debug, PartialEq)] + #[cfg_attr( + feature = "bincode", + derive(crate::fun::bincode::Encode, crate::fun::bincode::Decode), + bincode(crate = "crate::fun::bincode") + )] + #[cfg_attr( + feature = "serde", + derive(crate::fun::serde::Deserialize, crate::fun::serde::Serialize), + serde(crate = "crate::fun::serde") + )] + pub struct AggKeygenInput { + inner: simplepedpop::AggKeygenInput, + encrypted_shares: BTreeMap)>, + encryption_nonces: Vec, + } + + impl AggKeygenInput { + /// Gets the `SharedKey` that this aggregated input produces. + /// + /// ## Security + /// + /// ⚠ Just because you can call this doesn't mean you can use the `SharedKey` securely yet! + /// + /// You have to have checked that all parties (contributors and receivers) think it's valid + /// *and* have the same copy first. + pub fn shared_key(&self) -> SharedKey { + self.inner.shared_key() + } + + /// The *certification* bytes. Checking all parties have the same output of this function is + /// enough to check they have the same `AggKeygenInput`. + pub fn cert_bytes(&self) -> Vec { + let mut cert_bytes = self.inner.cert_bytes(); + cert_bytes.extend((self.encryption_nonces.len() as u32).to_be_bytes()); + cert_bytes.extend( + self.encryption_nonces + .iter() + .flat_map(|nonce| nonce.to_bytes()), + ); + cert_bytes.extend((self.encrypted_shares.len() as u32).to_be_bytes()); + for (party_index, (encryption_key, encrypted_share)) in &self.encrypted_shares { + cert_bytes.extend(party_index.to_bytes()); + cert_bytes.extend(encryption_key.to_bytes()); + cert_bytes.extend(encrypted_share.to_bytes()); + } + cert_bytes + } + + /// Get the encryption key for every party + pub fn encryption_keys(&self) -> impl Iterator + '_ { + self.encrypted_shares + .iter() + .map(|(party_index, (ek, _))| (*party_index, *ek)) + } + + /// Certify the `AggKeygenInput`. If all parties certify this then the keygen was + /// successful. + pub fn certify( + &self, + schnorr: &Schnorr, + keypair: &KeyPair, + ) -> Signature + where + H: Hash32, + NG: NonceGen, + { + schnorr.sign( + keypair, + Message::::plain("BIP DKG/cert", self.cert_bytes().as_ref()), + ) + } + + /// Verify that another party has certified the keygen. If you collect certifications from + /// all parties then the keygen was successful + pub fn verify_cert( + &self, + schnorr: &Schnorr, + cert_key: Point, + signature: Signature, + ) -> bool { + schnorr.verify( + &cert_key, + Message::::plain("BIP DKG/cert", self.cert_bytes().as_ref()), + &signature, + ) + } + + /// Recover a share with the decryption key from the `AggKeygenInput`. + pub fn recover_share( + &self, + party_index: PartyIndex, + encryption_keypair: &KeyPair, + ) -> Result { + let (expected_public_key, agg_ciphertext) = self + .encrypted_shares + .get(&party_index) + .ok_or("No party at party_index existed")?; + + if *expected_public_key != encryption_keypair.public_key() { + return Err("this isn't the right encryption keypair for this share"); + } + let secret_share = decrypt::( + party_index, + encryption_keypair, + &self.encryption_nonces, + *agg_ciphertext, + ); + + let paired_secret_share = self + .shared_key() + .pair_secret_share(SecretShare { + index: party_index, + share: secret_share, + }) + .ok_or("the secret share recovered didn't match what was expected")?; + + paired_secret_share + .non_zero() + .ok_or("the shared secret was zero") + } + } + + /// Produced by [`Contributor::gen_keygen_input`]. This is sent from the each + /// `Contributor` to the *coordinator*. + #[cfg_attr( + feature = "bincode", + derive(crate::fun::bincode::Encode, crate::fun::bincode::Decode), + bincode(crate = "crate::fun::bincode") + )] + #[cfg_attr( + feature = "serde", + derive(crate::fun::serde::Deserialize, crate::fun::serde::Serialize), + serde(crate = "crate::fun::serde") + )] + #[derive(Clone, Debug, PartialEq)] + pub struct KeygenInput { + /// The input from the inner protocol + pub inner: simplepedpop::KeygenInput, + /// The shares encrypted for each receiving party + pub encrypted_shares: BTreeMap>, + /// The multi-encryption nonce for the encryptions in `encrypted_shares` + pub encryption_nonce: Point, + } + + /// Stores the state of the coordinator as it aggregates inputs from [`Contributor`]s. + #[derive(Clone, Debug, PartialEq)] + pub struct Coordinator { + inner: simplepedpop::Coordinator, + agg_encrypted_shares: BTreeMap)>, + encryption_nonces: Vec, + } + + impl Coordinator { + /// Creates a new coordinator with: + /// + /// - `threshold`: of key we're trying to generate + /// - `n_contributors`: The number of [`Contributor`]s + /// - `receiver_encryption_keys`: The encryption keys of each of the share receivers. + pub fn new( + threshold: u32, + n_contribtors: u32, + receiver_encryption_keys: &BTreeMap, + ) -> Self { + let agg_encrypted_shares = receiver_encryption_keys + .iter() + .map(|(&receiver, encryption_key)| (receiver, (*encryption_key, Scalar::zero()))) + .collect(); + Self { + inner: simplepedpop::Coordinator::new(threshold, n_contribtors), + agg_encrypted_shares, + encryption_nonces: Default::default(), + } + } + + /// Adds an `input` from a [`Contributor`]. + /// + /// Note verifying this is the correct input from the correct party is up to your application! + pub fn add_input( + &mut self, + schnorr: &Schnorr, + from: u32, + input: KeygenInput, + ) -> Result<(), &'static str> { + if self.inner.is_finished() { + return Err("all inputs have already been collected"); + } + let mut check_missing = self.agg_encrypted_shares.keys().collect::>(); + + for dest in input.encrypted_shares.keys() { + if !self.agg_encrypted_shares.contains_key(dest) { + return Err("included share for unknown party"); + } + check_missing.remove(dest); + } + + if !check_missing.is_empty() { + return Err("didn't have share for all parties"); + } + + // ⚠ only do mutations after we're sure everything is OK + self.inner.add_input(schnorr, from, input.inner)?; + + for (dest, encrypted_share_contrib) in input.encrypted_shares { + let agg_encrypted_share = &mut self.agg_encrypted_shares.get_mut(&dest).unwrap().1; + *agg_encrypted_share += encrypted_share_contrib; + } + + self.encryption_nonces.push(input.encryption_nonce); + + Ok(()) + } + + /// Which [`Contributor`]s are we missing input from. + pub fn missing_from(&self) -> BTreeSet { + self.inner.missing_from() + } + + /// Has the coordinator received input from each [`Contributor`]. + pub fn is_finished(&self) -> bool { + self.inner.is_finished() + } + + /// Try and finish input aggregation step. + /// + /// Returns `None` if [`is_finished`] returns `false`. + /// + /// [`is_finished`]: Self::is_finished + pub fn finish(self) -> Option { + let inner = self.inner.finish()?; + Some(AggKeygenInput { + inner, + encrypted_shares: self.agg_encrypted_shares, + encryption_nonces: self.encryption_nonces, + }) + } + } + + /// Extract our secret share from the `AggKeygenInput`. + /// + /// This also validates `agg_input`. + pub fn receive_share( + schnorr: &Schnorr, + my_index: PartyIndex, + encryption_keypair: &KeyPair, + agg_input: &AggKeygenInput, + ) -> Result, simplepedpop::ReceiveShareError> + where + H: Hash32, + { + let encrypted_share = agg_input + .encrypted_shares + .get(&my_index) + .map(|(_pk, share)| *share) + .unwrap_or_default(); + let share_scalar = decrypt::( + my_index, + encryption_keypair, + &agg_input.encryption_nonces, + encrypted_share, + ); + let secret_share = SecretShare { + index: my_index, + share: share_scalar, + }; + let paired_secret_share = + simplepedpop::receive_share(schnorr, &agg_input.inner, secret_share)?; + + Ok(paired_secret_share) + } + + fn encrypt( + encryption_jobs: BTreeMap)>, + multi_nonce_keypair: KeyPair, + ) -> BTreeMap> { + encryption_jobs + .iter() + .map(|(dest, (encryption_key, share))| { + let dh_key = g!(multi_nonce_keypair.secret_key() * encryption_key).normalize(); + // SPEC DEVIATION: Hash inputs are as defined in "Multi-recipient Encryption, Revisited" by Pinto et al. + let pad = Scalar::from_hash(H::default().add(dh_key).add(encryption_key).add(dest)); + let payload = s!(pad + share).public(); + (*dest, payload) + }) + .collect() + } + + fn decrypt( + my_index: PartyIndex, + encryption_keypair: &KeyPair, + multi_nocnes: &[Point], + mut agg_ciphertext: Scalar, + ) -> Scalar { + for nonce in multi_nocnes { + let dh_key = g!(encryption_keypair.secret_key() * nonce).normalize(); + let pad = Scalar::from_hash( + H::default() + .add(dh_key) + .add(encryption_keypair.public_key()) + .add(my_index), + ); + agg_ciphertext -= pad; + } + agg_ciphertext.secret() + } + + /// Simulate running a key generation with `encpedpop`. + /// + /// This calls all the other functions defined in this module to get the whole job done on a + /// single computer by simulating all the other parties. + pub fn simulate_keygen( + schnorr: &Schnorr, + threshold: u32, + n_receivers: u32, + n_generators: u32, + rng: &mut impl rand_core::RngCore, + ) -> (SharedKey, Vec>) + where + H: Hash32, + NG: NonceGen, + { + let share_receivers = (1..=n_receivers) + .map(|i| Scalar::from(i).non_zero().unwrap()) + .collect::>(); + + let receiver_enckeys = share_receivers + .iter() + .cloned() + .map(|party_index| (party_index, KeyPair::new(Scalar::random(rng)))) + .collect::>(); + + let public_receiver_enckeys = receiver_enckeys + .iter() + .map(|(party_index, enckeypair)| (*party_index, enckeypair.public_key())) + .collect::>(); + + let (contributors, to_coordinator_messages): (Vec, Vec) = (0 + ..n_generators) + .map(|i| { + Contributor::gen_keygen_input(schnorr, threshold, &public_receiver_enckeys, i, rng) + }) + .unzip(); + + let mut aggregator = Coordinator::new(threshold, n_generators, &public_receiver_enckeys); + + for (i, to_coordinator_message) in to_coordinator_messages.into_iter().enumerate() { + aggregator + .add_input(schnorr, i as u32, to_coordinator_message) + .unwrap(); + } + + let agg_input = aggregator.finish().unwrap(); + for contributor in contributors { + contributor.verify_agg_input(&agg_input).unwrap(); + } + + let mut paired_secret_shares = vec![]; + for (party_index, enckey) in receiver_enckeys { + let paired_secret_share = + receive_share(schnorr, party_index, &enckey, &agg_input).unwrap(); + paired_secret_shares.push(paired_secret_share.non_zero().unwrap()); + } + + let shared_key = agg_input.shared_key().non_zero().unwrap(); + (shared_key, paired_secret_shares) + } +} + +/// `certpedpop` is built on top of [`encpedpop`] to add certification of the outcome. +/// +/// In [`encpedpop`] and [`simplepedpop`] it's left up to the application to figure out whether all +/// the parties agree on the `AggKeygenInput`. In `certpedpop` the relevant methods return +/// certification signatures on `AggKeygenInput` once they've been validated so they can be +/// collected by the share receivers. Once the share receivers have got all the certificates they +/// can finally output the key. +/// +/// Certificates are collected from other share receivers as well as `Contributor`s. +pub mod certpedpop { + use super::*; + + /// A party that generates secret input to the key generation. You need at least one of these + /// and if at least one of these parties is honest then the final secret key will not be known by an + /// attacker (unless they obtain `t` shares!). + pub struct Contributor { + inner: encpedpop::Contributor, + } + + /// Produced by [`Contributor::gen_keygen_input`]. This is sent from the each + /// `Contributor` to the *coordinator*. + pub type KeygenInput = encpedpop::KeygenInput; + /// Key generation inputs after being aggregated by the coordinator + pub type AggKeygenInput = encpedpop::AggKeygenInput; + /// The certification signatures from each certifying party (both contributors and share receivers). + pub type Certificate = BTreeMap, Signature>; + + impl Contributor { + /// Generates the keygen input for a party at `my_index`. Note that `my_index` + /// has nothing to do with the "receiver" index (the `PartyIndex` of share receivers). If + /// there are `n` `KeyGenInputParty`s then each party must be assigned an index from `0` to `n-1`. + /// + /// This method return `Self` to retain the state of the protocol which is needded to verify + /// the aggregated input later on. + pub fn gen_keygen_input( + schnorr: &Schnorr, + threshold: u32, + receiver_encryption_keys: &BTreeMap, + my_index: u32, + rng: &mut impl rand_core::RngCore, + ) -> (Self, KeygenInput) { + let (inner, message) = encpedpop::Contributor::gen_keygen_input( + schnorr, + threshold, + receiver_encryption_keys, + my_index, + rng, + ); + (Self { inner }, message) + } + + /// Verifies that the coordinator has honestly included this party's input into the + /// aggregated input and returns a certification signature to that effect. + /// + /// This passing by itself doesn't mean that the key generation was successful. You must + /// first collect the signatures from all the certifying parties (contributors and share + /// receivers). + pub fn verify_agg_input( + self, + schnorr: &Schnorr, + agg_keygen_input: &AggKeygenInput, + cert_keypair: &KeyPair, + ) -> Result { + self.inner.verify_agg_input(agg_keygen_input)?; + let sig = agg_keygen_input.certify(schnorr, cert_keypair); + Ok(sig) + } + } + + /// A key generation session that has been certified by each certifying party (contributors and share receivers). + #[derive(Clone, Debug, PartialEq)] + pub struct CertifiedKeygen { + input: AggKeygenInput, + certificate: Certificate, + } + + impl CertifiedKeygen { + /// Recover a share from a certified key generation with the decryption key. + /// + /// This checks that the `encryption_keypair` has signed the key generation first. + pub fn recover_share( + &self, + schnorr: &Schnorr, + party_index: PartyIndex, + encryption_keypair: KeyPair, + ) -> Result { + let cert_key = encryption_keypair.public_key().into_point_with_even_y().0; + let my_cert = self + .certificate + .get(&cert_key) + .ok_or("I haven't certified this keygen")?; + if !self.input.verify_cert(schnorr, cert_key, *my_cert) { + return Err("my certification was invalid"); + } + self.input + .recover_share::(party_index, &encryption_keypair) + } + + /// Gets the inner `encpedpop::AggKeygenInput`. + pub fn inner(&self) -> &AggKeygenInput { + &self.input + } + } + + pub use encpedpop::Coordinator; + + /// Stores the state of share recipient who first receives their share and then waits to get + /// signatures from all the certifying parties on the keygeneration before accepting it. + pub struct ShareReceiver { + paired_secret_share: PairedSecretShare, + agg_input: AggKeygenInput, + } + + impl ShareReceiver { + /// Extract your `encryption_keypair` and certify the key generation. Before you actually + /// can use the share you must call [`finalize`] with a completed certificate. + /// + /// [`finalize`]: Self::finalize + pub fn receive_share( + schnorr: &Schnorr, + my_index: PartyIndex, + encryption_keypair: &KeyPair, + agg_input: &AggKeygenInput, + ) -> Result<(Self, Signature), simplepedpop::ReceiveShareError> + where + H: Hash32, + NG: NonceGen, + { + let paired_secret_share = + encpedpop::receive_share(schnorr, my_index, encryption_keypair, agg_input)?; + let sig = agg_input.certify(schnorr, &(*encryption_keypair).into()); + let self_ = Self { + paired_secret_share, + agg_input: agg_input.clone(), + }; + Ok((self_, sig)) + } + + /// Check the certificate contains a signature from each certifying party. + /// + /// By default every share receiver is a certifying party but you must also get + /// certifications from the [`Contributor`]s for security. Their keys are passed in as + /// `contributor_keys`. + pub fn finalize( + self, + schnorr: &Schnorr, + certificate: Certificate, + contributor_keys: &[Point], + ) -> Result<(CertifiedKeygen, PairedSecretShare), &'static str> { + let cert_keys = self + .agg_input + .encryption_keys() + .map(|(_, encryption_key)| encryption_key.into_point_with_even_y().0) + .chain(contributor_keys.iter().cloned()); + for cert_key in cert_keys { + match certificate.get(&cert_key) { + Some(sig) => { + if !self.agg_input.verify_cert(schnorr, cert_key, *sig) { + return Err("certification signature was invalid"); + } + } + None => return Err("missing certification signature"), + } + } + + let certified_keygen = CertifiedKeygen { + input: self.agg_input, + certificate, + }; + + Ok((certified_keygen, self.paired_secret_share)) + } + } + + /// Simulate running a key generation with `certpedpop`. + /// + /// This calls all the other functions defined in this module to get the whole job done on a + /// single computer by simulating all the other parties. + pub fn simulate_keygen( + schnorr: &Schnorr, + threshold: u32, + n_receivers: u32, + n_generators: u32, + rng: &mut impl rand_core::RngCore, + ) -> (CertifiedKeygen, Vec<(PairedSecretShare, KeyPair)>) { + let share_receivers = (1..=n_receivers) + .map(|i| Scalar::from(i).non_zero().unwrap()) + .collect::>(); + + let mut receiver_enckeys = share_receivers + .iter() + .cloned() + .map(|party_index| (party_index, KeyPair::new(Scalar::random(rng)))) + .collect::>(); + + let public_receiver_enckeys = receiver_enckeys + .iter() + .map(|(party_index, enckeypair)| (*party_index, enckeypair.public_key())) + .collect::>(); + + let (contributors, to_coordinator_messages): (Vec, Vec) = (0 + ..n_generators) + .map(|i| { + Contributor::gen_keygen_input(schnorr, threshold, &public_receiver_enckeys, i, rng) + }) + .unzip(); + + let contributor_keys = (0..n_generators) + .map(|_| KeyPair::new_xonly(Scalar::random(rng))) + .collect::>(); + let contributor_public_keys = contributor_keys + .iter() + .map(KeyPair::public_key) + .collect::>(); + + let mut aggregator = Coordinator::new(threshold, n_generators, &public_receiver_enckeys); + + for (i, to_coordinator_message) in to_coordinator_messages.into_iter().enumerate() { + aggregator + .add_input(schnorr, i as u32, to_coordinator_message) + .unwrap(); + } + + let agg_input = aggregator.finish().unwrap(); + let mut certificate = BTreeMap::default(); + + for (contributor, keypair) in contributors.into_iter().zip(contributor_keys.iter()) { + let sig = contributor + .verify_agg_input(schnorr, &agg_input, keypair) + .unwrap(); + certificate.insert(keypair.public_key(), sig); + } + + let mut paired_secret_shares = vec![]; + let mut share_receivers = vec![]; + for (party_index, enckey) in &receiver_enckeys { + let (share_receiver, cert) = + ShareReceiver::receive_share(schnorr, *party_index, enckey, &agg_input).unwrap(); + certificate.insert(enckey.public_key().into_point_with_even_y().0, cert); + share_receivers.push(share_receiver); + } + + let certified_keygen = CertifiedKeygen { + input: agg_input.clone(), + certificate: certificate.clone(), + }; + + for share_receiver in share_receivers { + let (certified, paired_secret_share) = share_receiver + .finalize(schnorr, certificate.clone(), &contributor_public_keys) + .unwrap(); + assert_eq!(certified, certified_keygen); + paired_secret_shares.push(( + paired_secret_share.non_zero().unwrap(), + receiver_enckeys + .remove(&paired_secret_share.index()) + .unwrap(), + )); + } + + (certified_keygen, paired_secret_shares) + } + + /// There was a problem with the keygen certificate so the key generation can't be trusted. + #[derive(Clone, Debug, Copy, PartialEq)] + pub enum CertificateError { + /// A certificate was invalid + InvalidCert { + /// The key that had the invalid cert + key: Point, + }, + /// A certificate was missing + Missing { + /// They key whose cert was missing + key: Point, + }, + } + + impl core::fmt::Display for CertificateError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + CertificateError::InvalidCert { key } => { + write!(f, "certificate for key {} was invalid", key) + } + CertificateError::Missing { key } => { + write!(f, "certificate for key {} was missing", key) + } + } + } + } + + #[cfg(feature = "std")] + impl std::error::Error for CertificateError {} +} + +#[cfg(test)] +mod test { + use super::*; + use proptest::{ + prelude::*, + test_runner::{RngAlgorithm, TestRng}, + }; + use secp256kfun::proptest; + + proptest! { + #[test] + fn simplepedpop_run_simulate_keygen( + (n_receivers, threshold) in (1u32..=4).prop_flat_map(|n| (Just(n), 1u32..=n)), + n_generators in 1u32..5, + ) { + let schnorr = crate::new_with_deterministic_nonces::(); + let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); + + simplepedpop::simulate_keygen(&schnorr, threshold, n_receivers, n_generators, &mut rng); + } + + #[test] + fn encpedpop_run_simulate_keygen( + (n_receivers, threshold) in (1u32..=4).prop_flat_map(|n| (Just(n), 1u32..=n)), + n_generators in 1u32..5, + ) { + let schnorr = crate::new_with_deterministic_nonces::(); + let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); + + encpedpop::simulate_keygen(&schnorr, threshold, n_receivers, n_generators, &mut rng); + } + + #[test] + fn certified_run_simulate_keygen( + (n_receivers, threshold) in (1u32..=4).prop_flat_map(|n| (Just(n), 1u32..=n)), + n_generators in 1u32..5, + ) { + let schnorr = crate::new_with_deterministic_nonces::(); + let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); + + let (certified_keygen, paired_secret_shares_and_keys) = certpedpop::simulate_keygen(&schnorr, threshold, n_receivers, n_generators, &mut rng); + + for (paired_secret_share, encryption_keypair) in paired_secret_shares_and_keys { + let recovered = certified_keygen.recover_share(&schnorr, paired_secret_share.index(), encryption_keypair).unwrap(); + assert_eq!(paired_secret_share, recovered); + } + } + } +} diff --git a/schnorr_fun/src/frost/mod.rs b/schnorr_fun/src/frost/mod.rs index 89ebe21..3c8257e 100644 --- a/schnorr_fun/src/frost/mod.rs +++ b/schnorr_fun/src/frost/mod.rs @@ -6,93 +6,33 @@ //! use schnorr_fun::binonce::NonceKeyPair; //! use schnorr_fun::fun::{s, poly}; //! use schnorr_fun::{ -//! frost, +//! frost::{self, chilldkg::simplepedpop}, //! Message, //! }; //! use std::collections::BTreeMap; //! use rand_chacha::ChaCha20Rng; //! use sha2::Sha256; -//! // use sha256 to produce deterministic nonces -- be careful! +//! //! let frost = frost::new_with_deterministic_nonces::(); -//! // Use randomness from ThreadRng to create synthetic nonces -- harder to make a mistake. -//! let frost = frost::new_with_synthetic_nonces::(); -//! // We need an RNG for key generation -- don't use ThreadRng in practice see note below. -//! let mut rng = rand::thread_rng(); -//! // we're doing a 2 out of 3 -//! let threshold = 2; -//! // Generate our secret scalar polynomial we'll use in the key generation protocol -//! let my_secret_poly = poly::scalar::generate(threshold, &mut rng); -//! let my_public_poly = poly::scalar::to_point_poly(&my_secret_poly); -//! # let secret_poly2 = poly::scalar::generate(threshold, &mut rng); -//! # let secret_poly3 = poly::scalar::generate(threshold, &mut rng); -//! # let public_poly2 = poly::scalar::to_point_poly(&secret_poly2); -//! # let public_poly3 = poly::scalar::to_point_poly(&secret_poly3); //! -//! // Party indices can be any non-zero scalar -//! let my_index = s!(1).public(); -//! let party_index2 = s!(2).public(); -//! let party_index3 = s!(3).public(); -//! // share our public point poly, and receive the point polys from other participants -//! let public_polys_received = BTreeMap::from_iter([ -//! (my_index, my_public_poly), -//! (party_index2, public_poly2), -//! (party_index3, public_poly3), -//! ]); -//! // (optionally) construct my_polys so we don't trust what's in public_poly_received for our index (in case it has been replaced with something malicious) -//! let my_polys = BTreeMap::from_iter([(my_index, &my_secret_poly)]); -//! let keygen = frost.new_keygen(public_polys_received, &my_polys).expect("something wrong with what was provided by other parties"); -//! // Generate secret shares for others and proof-of-possession to protect against rogue key attacks. -//! // We need pass a message to sign for the proof-of-possession. We choose the keygen -//! // id here but anything works (you can even use the empty message). -//! let keygen_id = frost.keygen_id(&keygen); -//! let pop_message = Message::raw(&keygen_id); -//! let (mut shares_i_generated, my_pop) = frost.create_shares_and_pop(&keygen, &my_secret_poly, pop_message); -//! # let (shares2, pop2) = frost.create_shares_and_pop(&keygen, &secret_poly2, pop_message); -//! # let (shares3, pop3) = frost.create_shares_and_pop(&keygen, &secret_poly3, pop_message); -//! // Now we send the corresponding shares we generated to the other parties along with our proof-of-possession. -//! // Eventually we'll receive shares from the others and combine them to create our secret key share: -//! # let share_and_pop_from_2 = (shares2.get(&my_index).unwrap().clone(), pop2.clone()); -//! # let share_and_pop_from_3 = (shares3.get(&my_index).unwrap().clone(), pop3.clone()); -//! # let received_shares3 = BTreeMap::from_iter([ -//! # (my_index, (shares_i_generated.get(&party_index3).unwrap().clone(), my_pop.clone())), -//! # (party_index2, (shares2.get(&party_index3).unwrap().clone(), pop2.clone())), -//! # (party_index3, (shares3.get(&party_index3).unwrap().clone(), pop3.clone())), -//! # ]); -//! let share_i_generated_for_myself = (shares_i_generated.remove(&my_index).unwrap(), my_pop); -//! let my_shares = BTreeMap::from_iter([ -//! (my_index, share_i_generated_for_myself), -//! (party_index2, share_and_pop_from_2), -//! (party_index3, share_and_pop_from_3) -//! ]); -//! // finish keygen by verifying the shares we received, verifying all proofs-of-possession, -//! // and calculate our long-lived secret share of the joint FROST key. -//! # let (secret_share3, _frost_key3) = frost -//! # .finish_keygen( -//! # keygen.clone(), -//! # party_index3, -//! # received_shares3, -//! # Message::raw(&frost.keygen_id(&keygen)), -//! # ) -//! # .unwrap(); -//! let (my_secret_share, shared_key) = frost -//! .finish_keygen( -//! keygen, -//! my_index, -//! my_shares, -//! pop_message, -//! ) -//! .expect("something was wrong with the shares we received"); -//! // ⚠️ At this point you probably want to check out of band that all the other parties -//! // received their secret shares correctly and have the same view of the protocol -//! // (e.g same keygen_id). If they all give the OK then we're ready to use the key and do some signing! +//! // This runs a 2-of-3 key generation on a single computer which means it's a trusted party. +//! // See the documentation/API of the protocols in `chilldkg` to see how to distrubute the key generation properly. +//! let (shared_key, secret_shares) = simplepedpop::simulate_keygen(&frost.schnorr, 2, 3,3, &mut rand::thread_rng()); +//! let my_secret_share = secret_shares[0]; +//! let my_index = my_secret_share.index(); +//! # let secret_share2 = secret_shares[1]; +//! # let secret_share3 = secret_shares[2]; +//! # let party_index3 = secret_share3.index(); +//! +//! //! // With signing we'll have at least one party be the "coordinator" (steps marked with 🐙) -//! // In this example we'll be the coordinator (but it doesn't have to be on eof the signing parties) +//! // In this example we'll be the coordinator (but it doesn't have to be one of the signing parties) //! let xonly_shared_key = shared_key.into_xonly(); // this is the key signatures will be valid under //! let xonly_my_secret_share = my_secret_share.into_xonly(); //! # let xonly_secret_share3 = secret_share3.into_xonly(); //! let message = Message::plain("my-app", b"chancellor on brink of second bailout for banks"); //! // Generate nonces for this signing session (and send them to coordinator somehow) -//! // ⚠️ session_id MUST be different for every signing attempt to avoid nonce reuse (if using deterministic nonces). +//! // ⚠ session_id MUST be different for every signing attempt to avoid nonce reuse (if using deterministic nonces). //! let session_id = b"signing-ominous-message-about-banks-attempt-1".as_slice(); //! let mut nonce_rng: ChaCha20Rng = frost.seed_nonce_rng(my_secret_share, session_id); //! let my_nonce = frost.gen_nonce(&mut nonce_rng); @@ -136,55 +76,15 @@ //! //! The original scheme was introduced in *[FROST: Flexible Round-Optimized Schnorr Threshold //! Signatures][FROST]*. A more satisfying security proof was provided in *[Security of Multi- and Threshold -//! Signatures]*. -//! -//! > ⚠️ At this stage this implementation is for API exploration purposes only. The way it is -//! > currently implemented is not proven secure. -//! -//! ## Polynomial Generation -//! -//! The FROST key generation protocol takes as input a *secret* polynomial of degree `threshold - 1`. -//! We represent a polynomial as a `Vec` where each [`Scalar`] represents a coefficient in the polynomial. -//! -//! The security of the protocol is only guaranteed if you sample your secret polynomial uniformly -//! at random from the perspective of the other parties. There is little advantage to using -//! deterministic randomness for this except to be able to reproduce the key generation with every -//! party's long term static secret key. In theory a more compelling answer to reproducing shares is -//! to use simple MPC protocol to produce a share for any party given a threshold number of parties. -//! This protocol isn't implemented here yet. -//! -//! This library doesn't provide a default policy with regards to polynomial generation but here we -//! give an example of a robust way to generate your secret scalar polynomial that should make sense -//! in most applications: -//! -//! ``` -//! use schnorr_fun::{frost, fun::{ Scalar, poly, nonce, Tag, derive_nonce_rng }}; -//! use sha2::Sha256; -//! use rand_chacha::ChaCha20Rng; -//! -//! let static_secret_key = /* from local storage */ -//! # Scalar::random(&mut rand::thread_rng()); -//! let nonce_gen = nonce::Synthetic::>::default().tag(b"my-app-name/frost/keygen"); -//! let mut poly_rng = derive_nonce_rng! { -//! // use synthetic nonces that add system randomness in -//! nonce_gen => nonce_gen, -//! // Use your static secret key to add further protection -//! secret => static_secret_key, -//! // session id should be unique for each key generation session -//! public => ["frost_key_session_1053"], -//! seedable_rng => ChaCha20Rng -//! }; -//! -//! let threshold = 3; -//! let my_secret_poly: Vec = poly::scalar::generate(threshold, &mut poly_rng); -//! ``` +//! Signatures]*. This implementation follows most closely *[Practical Schnorr Threshold Signatures Without the Algebraic Group Model]*. //! -//! Note that if a key generation session fails you should always start a fresh session with a -//! different session id (but you can use the same nonce_gen). +//! > ⚠ CAUTION ⚠: We *think* that this follows the scheme in the "Practical" paper which is proven secure but +//! > we haven't put a lot of effort into verifying this yet. //! //! [FROST]: //! [secp256k1-zkp]: //! [Security of Multi- and Threshold Signatures]: +//! [Practical Schnorr Threshold Signatures Without the Algebraic Group Model]: https://eprint.iacr.org/2023/899 //! [`musig`]: crate::musig //! [`Scalar`]: crate::fun::Scalar @@ -194,22 +94,18 @@ mod share; pub use share::*; mod session; pub use session::*; - +pub mod chilldkg; pub use crate::binonce::{Nonce, NonceKeyPair}; use crate::{binonce, Message, Schnorr, Signature}; -use alloc::{ - collections::{BTreeMap, BTreeSet}, - vec::Vec, -}; +use alloc::collections::{BTreeMap, BTreeSet}; use core::num::NonZeroU32; use secp256kfun::{ - derive_nonce_rng, g, + derive_nonce_rng, hash::{Hash32, HashAdd, Tag}, - marker::*, nonce::{self, NonceGen}, poly, + prelude::*, rand_core::{RngCore, SeedableRng}, - s, Point, Scalar, G, }; /// The index of a party's secret share. @@ -226,17 +122,14 @@ pub type PartyIndex = Scalar; /// /// Type parameters: /// -/// - `H`: hash type for challenges, keygen_id, and binding coefficient. -/// - `NG`: nonce generator for proofs-of-possessions and FROST nonces +/// - `H`: hash type for challenges, and binding coefficient. +/// - `NG`: nonce generator for FROST nonces (only used if you explicitly call nonce generation functions). #[derive(Clone)] pub struct Frost { /// The instance of the Schnorr signature scheme. pub schnorr: Schnorr, /// The hash used to generate the nonce binding coefficient when signing. binding_hash: H, - /// The hash used to generate the `keygen_id` - keygen_id_hash: H, - /// Nonce generator. /// Usually a tagged clone of the schnorr nonce generator. nonce_gen: NG, } @@ -300,123 +193,13 @@ where pub fn new(schnorr: Schnorr) -> Self { Self { binding_hash: H::default().tag(b"frost/binding"), - keygen_id_hash: H::default().tag(b"frost/keygenid"), nonce_gen: schnorr.nonce_gen().clone().tag(b"frost"), schnorr, } } } -/// A KeyGen (distributed key generation) session -/// -/// Created using [`Frost::new_keygen`] -/// -/// [`Frost::new_keygen`] -#[derive(Clone, Debug)] -pub struct KeyGen { - frost_poly: SharedKey, - point_polys: BTreeMap>, -} - -impl KeyGen { - /// Return the number of parties in the KeyGen - pub fn n_parties(&self) -> usize { - self.point_polys.len() - } -} - -/// First round keygen errors -#[derive(Debug, Clone)] -pub enum NewKeyGenError { - /// Received polynomial is of differing length. - PolyDifferentLength(PartyIndex), - /// Number of parties is less than the length of polynomials specifying the threshold. - NotEnoughParties, - /// Frost key is zero. Computationally unreachable *if* all parties are honest. - ZeroFrostKey, -} - -impl core::fmt::Display for NewKeyGenError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use NewKeyGenError::*; - match self { - PolyDifferentLength(i) => write!(f, "polynomial commitment from party at index {i} was a different length"), - NotEnoughParties => write!(f, "the number of parties was less than the threshold"), - ZeroFrostKey => write!(f, "The frost public key was zero. Computationally unreachable, one party is acting maliciously."), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for NewKeyGenError {} - -/// Second round KeyGen errors -#[derive(Debug, Clone)] -pub enum FinishKeyGenError { - /// Secret share and proof of possession was not provided for this party - MissingShare(PartyIndex), - /// Secret share does not match what we expected - InvalidShare(PartyIndex), - /// proof-of-possession does not match the expected. Incorrect ordering? - InvalidProofOfPossession(PartyIndex), -} - -impl core::fmt::Display for FinishKeyGenError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - use FinishKeyGenError::*; - match self { - MissingShare(i) => write!(f, "secret share was not provided for party {i}"), - InvalidShare(i) => write!( - f, - "the secret share at index {i} does not match the expected evaluation \ - of their point polynomial at our index. Check that the order and our index is correct" - ), - &InvalidProofOfPossession(i) => write!( - f, - "the proof-of-possession provided by party at index {i} was invalid, check ordering." - ), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for FinishKeyGenError {} - impl Frost { - /// Convienence method to generate secret shares and proof-of-possession to be shared with other - /// participants. Each secret share needs to be securely communicated to the intended - /// participant but the proof of possession (schnorr signature) can be publically shared with - /// everyone. - pub fn create_shares_and_pop( - &self, - keygen: &KeyGen, - scalar_poly: &[Scalar], - pop_message: Message, - ) -> (BTreeMap>, Signature) { - ( - keygen - .point_polys - .keys() - .map(|party_index| (*party_index, self.create_share(scalar_poly, *party_index))) - .collect(), - self.create_proof_of_possession(scalar_poly, pop_message), - ) - } - - /// Create proof-of-possession to prove ownership of the first term in our scalar polynomial. - /// This does a Schnorr signature over the given message under the first term of the polynomial - /// using the internal [`Schnorr`] instance. - /// - /// [`Schnorr`]: crate::Schnorr - pub fn create_proof_of_possession( - &self, - scalar_poly: &[Scalar], - message: Message, - ) -> Signature { - let key_pair = self.schnorr.new_keypair(scalar_poly[0]); - self.schnorr.sign(&key_pair, message) - } - /// Seed a random number generator to be used for FROST nonces. /// /// ** ⚠ WARNING ⚠**: This method is unstable and easy to use incorrectly. The seed it uses for @@ -459,246 +242,9 @@ impl Frost { ); rng } - - /// Run the key generation protocol while simulating the parties internally. - /// - /// This can be used to do generate a "trusted setup" FROST key (but it is extremely inefficient - /// for this purpose). It returns the joint `SharedKey` along with the secret keys for each - /// party. - pub fn simulate_keygen( - &self, - threshold: usize, - n_parties: usize, - rng: &mut impl RngCore, - ) -> (SharedKey, Vec>) { - let scalar_polys = (0..n_parties) - .map(|i| { - ( - Scalar::from_non_zero_u32(NonZeroU32::new((i + 1) as u32).expect("we added 1")) - .public(), - poly::scalar::generate(threshold, rng), - ) - }) - .collect::>(); - - let keygen = self.new_keygen(Default::default(), &scalar_polys).unwrap(); - let mut shares = scalar_polys - .into_iter() - .map(|(party_index, sp)| { - ( - party_index, - self.create_shares_and_pop(&keygen, &sp, Message::::empty()), - ) - }) - .collect::>(); - // collect the received shares for each party - let received_shares = keygen - .point_polys - .keys() - .map(|receiver_party_index| { - let received = shares - .iter_mut() - .map(|(gen_party_index, (party_shares, pop))| { - ( - *gen_party_index, - (party_shares.remove(receiver_party_index).unwrap(), *pop), - ) - }) - .collect::>(); - - (*receiver_party_index, received) - }) - .collect::>(); - - let mut frost_key = None; - // finish keygen for each party - let secret_shares = received_shares - .into_iter() - .map(|(party_index, received_shares)| { - let (secret_share, _frost_key) = self - .finish_keygen( - keygen.clone(), - party_index, - received_shares, - Message::::empty(), - ) - .unwrap(); - - frost_key = Some(_frost_key); - secret_share - }) - .collect(); - - (frost_key.unwrap(), secret_shares) - } } impl Frost { - /// Generate an id for the key generation by hashing the party indicies and their point - /// polynomials - pub fn keygen_id(&self, keygen: &KeyGen) -> [u8; 32] { - let mut keygen_hash = self.keygen_id_hash.clone(); - keygen_hash.update((keygen.point_polys.len() as u32).to_be_bytes().as_ref()); - for (index, poly) in &keygen.point_polys { - keygen_hash.update(index.to_bytes().as_ref()); - for point in poly { - keygen_hash.update(point.to_bytes().as_ref()); - } - } - keygen_hash.finalize_fixed().into() - } - - /// Collect all the public polynomials commitments into a [`KeyGen`] to produce a [`SharedKey`]. - /// - /// It is crucial that at least one of these polynomials was not adversarially produced - /// otherwise the adversary will know the eventual secret key. - /// - /// As a safety mechanism `local_secret_polys` allows you to pass in the secret scalar - /// polynomials you control which will be converted into the public form internally. This way - /// you don't trust what's in `point_polys` for the entries that you control. This protects - /// against a malicious adversary who publishes a `point_polys` which replaces your entries with - /// polynomial commitments it creates. If you don't use `local_secret_polys` you have to do - /// protect against this in your application. - /// - /// Note that in any sensibly designed key generation `local_secret_polys` will only have one - /// entry as there is no security benefit of one party controlling multiple key generation - /// polynomials. If an entry is in both `point_polys` and `local_secret_polys` it will be - /// silently overwritten with the one from `local_secret_polys`. - pub fn new_keygen( - &self, - mut point_polys: BTreeMap>, - local_secret_polys: &BTreeMap, - ) -> Result - where - S: AsRef<[Scalar]>, - { - for (party_id, scalar_poly) in local_secret_polys { - let image = poly::scalar::to_point_poly(scalar_poly.as_ref()); - let _existing = point_polys.insert(*party_id, image); - if let Some(_existing) = _existing { - debug_assert_eq!(_existing, poly::scalar::to_point_poly(scalar_poly.as_ref())); - } - } - let len_first_poly = point_polys - .iter() - .next() - .map(|(_, poly)| poly.len()) - .ok_or(NewKeyGenError::NotEnoughParties)?; - { - if let Some((i, _)) = point_polys - .iter() - .find(|(_, point_poly)| point_poly.len() != len_first_poly) - { - return Err(NewKeyGenError::PolyDifferentLength(*i)); - } - - // Number of parties is less than the length of polynomials specifying the threshold - if point_polys.len() < len_first_poly { - return Err(NewKeyGenError::NotEnoughParties); - } - } - - let mut joint_poly = (0..len_first_poly) - .map(|_| Point::::zero()) - .collect::>(); - - for poly in point_polys.values() { - for i in 0..len_first_poly { - joint_poly[i] += poly[i]; - } - } - - let frost_poly = SharedKey::from_poly( - joint_poly - .into_iter() - .map(|coef| coef.normalize()) - .collect(), - ) - .non_zero() - .ok_or(NewKeyGenError::ZeroFrostKey)?; - - Ok(KeyGen { - point_polys, - frost_poly, - }) - } - - /// Verify a key generation without being a key-owning party - pub fn finish_keygen_coordinator( - &self, - keygen: KeyGen, - proofs_of_possession: BTreeMap, - proof_of_possession_msg: Message, - ) -> Result, FinishKeyGenError> { - for (party_index, poly) in &keygen.point_polys { - let pop = proofs_of_possession - .get(party_index) - .ok_or(FinishKeyGenError::MissingShare(*party_index))?; - let (even_poly_point, _) = poly[0].into_point_with_even_y(); - - if !self - .schnorr - .verify(&even_poly_point, proof_of_possession_msg, pop) - { - return Err(FinishKeyGenError::InvalidProofOfPossession(*party_index)); - } - } - - Ok(keygen.frost_poly) - } - - /// Combine all received shares into your long-lived secret share. - /// - /// The `secret_shares` includes your own share as well as shares from each of the other - /// parties. The `secret_shares` are validated to match the expected result by evaluating their - /// polynomial at our participant index. Each participant's proof-of-possession is verified - /// against what they provided in the first round of key generation. - /// - /// The proof-of-possession message should be the unique keygen_id unless chosen otherwise. - /// - /// # Return value - /// - /// Your secret share and the [`SharedKey`] - pub fn finish_keygen( - &self, - keygen: KeyGen, - my_index: PartyIndex, - secret_shares: BTreeMap, Signature)>, - proof_of_possession_msg: Message, - ) -> Result<(PairedSecretShare, SharedKey), FinishKeyGenError> { - let mut total_secret_share = s!(0); - - for (party_index, poly) in &keygen.point_polys { - let (secret_share, pop) = secret_shares - .get(party_index) - .ok_or(FinishKeyGenError::MissingShare(*party_index))?; - let (even_poly_point, _) = poly[0].into_point_with_even_y(); - - if !self - .schnorr - .verify(&even_poly_point, proof_of_possession_msg, pop) - { - return Err(FinishKeyGenError::InvalidProofOfPossession(*party_index)); - } - - let expected_public_share = poly::point::eval(poly, my_index); - if g!(secret_share * G) != expected_public_share { - return Err(FinishKeyGenError::InvalidShare(*party_index)); - } - total_secret_share += secret_share; - } - - let secret_share = SecretShare { - index: my_index, - share: total_secret_share, - }; - - let secret_share_with_image = - PairedSecretShare::new(secret_share, keygen.frost_poly.public_key()); - - Ok((secret_share_with_image, keygen.frost_poly)) - } - /// Aggregate the nonces of the signers so you can start a [`party_sign_session`] without a /// coordinator. /// @@ -718,7 +264,7 @@ impl Frost { agg_binonce: binonce::Nonce, message: Message, ) -> PartySignSession { - let binding_coeff = self.binding_coefficient(public_key, agg_binonce, message); + let binding_coeff = self.binding_coefficient(public_key, agg_binonce, message, &parties); let (final_nonce, binonce_needs_negation) = agg_binonce.bind(binding_coeff); let challenge = self.schnorr.challenge(&final_nonce, &public_key, message); @@ -752,7 +298,12 @@ impl Frost { let agg_binonce = binonce::Nonce::aggregate(nonces.values().cloned()); - let binding_coeff = self.binding_coefficient(shared_key.public_key(), agg_binonce, message); + let binding_coeff = self.binding_coefficient( + shared_key.public_key(), + agg_binonce, + message, + &nonces.keys().cloned().collect(), + ); let (final_nonce, binonce_needs_negation) = agg_binonce.bind(binding_coeff); let challenge = self @@ -778,12 +329,15 @@ impl Frost { public_key: Point, agg_binonce: Nonce, message: Message, + parties: &BTreeSet, ) -> Scalar { Scalar::from_hash( self.binding_hash .clone() - .add(agg_binonce) .add(public_key) + .add((parties.len() as u32).to_be_bytes()) + .add(parties) + .add(agg_binonce) .add(message), ) .public() @@ -839,12 +393,14 @@ where mod test { use super::*; + use chilldkg::simplepedpop; use sha2::Sha256; #[test] fn zero_agg_nonce_results_in_G() { let frost = new_with_deterministic_nonces::(); - let (frost_poly, _shares) = frost.simulate_keygen(2, 3, &mut rand::thread_rng()); + let (frost_poly, _shares) = + simplepedpop::simulate_keygen(&frost.schnorr, 2, 3, 3, &mut rand::thread_rng()); let nonce = NonceKeyPair::random(&mut rand::thread_rng()).public(); let mut malicious_nonce = nonce; malicious_nonce.conditional_negate(true); diff --git a/schnorr_fun/src/frost/share.rs b/schnorr_fun/src/frost/share.rs index ccb22ab..09e3336 100644 --- a/schnorr_fun/src/frost/share.rs +++ b/schnorr_fun/src/frost/share.rs @@ -506,7 +506,7 @@ use super::PartyIndex; #[cfg(test)] mod test { use super::*; - use crate::frost; + use crate::frost::{self, chilldkg::simplepedpop}; use alloc::vec::Vec; use secp256kfun::{ g, @@ -519,14 +519,14 @@ mod test { proptest! { #[test] fn recover_secret( - (parties, threshold) in (1usize..=10).prop_flat_map(|n| (Just(n), 1usize..=n)), + (parties, threshold) in (1u32..=10).prop_flat_map(|n| (Just(n), 1u32..=n)), ) { use rand::seq::SliceRandom; let frost = frost::new_with_deterministic_nonces::(); let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); - let (frost_poly, shares) = frost.simulate_keygen(threshold, parties, &mut rng); - let chosen = shares.choose_multiple(&mut rng, threshold).cloned() + let (frost_poly, shares) = simplepedpop::simulate_keygen(&frost.schnorr, threshold, parties , parties , &mut rng); + let chosen = shares.choose_multiple(&mut rng, threshold as usize).cloned() .map(|paired_share| paired_share.secret_share).collect::>(); let secret = SecretShare::recover_secret(&chosen); prop_assert_eq!(g!(secret * G), frost_poly.public_key()); diff --git a/schnorr_fun/tests/frost_prop.rs b/schnorr_fun/tests/frost_prop.rs index 20e426d..fee5b46 100644 --- a/schnorr_fun/tests/frost_prop.rs +++ b/schnorr_fun/tests/frost_prop.rs @@ -1,4 +1,5 @@ #![cfg(feature = "alloc")] +use chilldkg::encpedpop; use rand::seq::SliceRandom; use rand_chacha::ChaCha20Rng; use schnorr_fun::{ @@ -19,18 +20,18 @@ proptest! { #[test] fn frost_prop_test( - (n_parties, threshold) in (2usize..=4).prop_flat_map(|n| (Just(n), 2usize..=n)), + (n_parties, threshold) in (2u32..=4).prop_flat_map(|n| (Just(n), 2u32..=n)), add_tweak in option::of(any::>()), xonly_add_tweak in option::of(any::>()), mul_tweak in option::of(any::>()), xonly_mul_tweak in option::of(any::>()) ) { - let proto = new_with_deterministic_nonces::(); + let frost = new_with_deterministic_nonces::(); assert!(threshold <= n_parties); // // create some scalar polynomial for each party let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); - let (mut shared_key, mut secret_shares) = proto.simulate_keygen(threshold, n_parties, &mut rng); + let (mut shared_key, mut secret_shares) = encpedpop::simulate_keygen(&frost.schnorr, threshold, n_parties, n_parties, &mut rng); if let Some(tweak) = add_tweak { for secret_share in &mut secret_shares { @@ -68,8 +69,8 @@ proptest! { } // use a boolean mask for which t participants are signers - let mut signer_mask = vec![true; threshold]; - signer_mask.append(&mut vec![false; n_parties - threshold]); + let mut signer_mask = vec![true; threshold as usize]; + signer_mask.append(&mut vec![false; (n_parties - threshold) as usize]); // shuffle the mask for random signers signer_mask.shuffle(&mut rng); @@ -81,8 +82,8 @@ proptest! { let message = Message::plain("test", b"test"); let mut secret_nonces: BTreeMap<_, _> = secret_shares_of_signers.iter().map(|paired_secret_share| { - (paired_secret_share.secret_share().index, proto.gen_nonce::( - &mut proto.seed_nonce_rng( + (paired_secret_share.secret_share().index, frost.gen_nonce::( + &mut frost.seed_nonce_rng( *paired_secret_share, sid, ))) @@ -91,13 +92,13 @@ proptest! { let public_nonces = secret_nonces.iter().map(|(signer_index, sn)| (*signer_index, sn.public())).collect::>(); - let coord_signing_session = proto.coordinator_sign_session( + let coord_signing_session = frost.coordinator_sign_session( &xonly_shared_key, public_nonces, message ); - let party_signing_session = proto.party_sign_session( + let party_signing_session = frost.party_sign_session( xonly_shared_key.public_key(), coord_signing_session.parties(), coord_signing_session.agg_binonce(), @@ -122,7 +123,7 @@ proptest! { ); assert_eq!(coord_signing_session.verify_and_combine_signature_shares(&xonly_shared_key, signatures), Ok(combined_sig)); - assert!(proto.schnorr.verify( + assert!(frost.schnorr.verify( &xonly_shared_key.public_key(), message, &combined_sig diff --git a/secp256kfun/src/hash.rs b/secp256kfun/src/hash.rs index 47538c7..bddd7d6 100644 --- a/secp256kfun/src/hash.rs +++ b/secp256kfun/src/hash.rs @@ -150,6 +150,15 @@ impl HashInto for alloc::collections::BTreeMap { } } +#[cfg(feature = "alloc")] +impl HashInto for alloc::collections::BTreeSet { + fn hash_into(self, hash: &mut impl digest::Update) { + for item in self { + item.hash_into(hash) + } + } +} + /// Extension trait for [`digest::Update`] to make adding things to the hash convenient. pub trait HashAdd { /// Converts something that implements [`HashInto`] to bytes and then incorporate the result into the digest (`self`). diff --git a/secp256kfun/src/scalar.rs b/secp256kfun/src/scalar.rs index 620b5c2..21ca529 100644 --- a/secp256kfun/src/scalar.rs +++ b/secp256kfun/src/scalar.rs @@ -2,6 +2,7 @@ use crate::{backend, hash::HashInto, marker::*, op}; use core::{ marker::PhantomData, + num::NonZeroU32, ops::{AddAssign, MulAssign, SubAssign}, }; use digest::{self, generic_array::typenum::U32}; @@ -224,6 +225,13 @@ impl Scalar { pub fn mark_zero_choice(self) -> Scalar { Scalar::from_inner(self.0) } + + /// Converts a [`NonZeroU32`] into a `Scalar`. + /// + /// [`NonZeroU32`]: core::num::NonZeroU32 + pub fn from_non_zero_u32(int: core::num::NonZeroU32) -> Self { + Self::from_inner(backend::BackendScalar::from_u32(int.get())) + } } impl Scalar { @@ -262,13 +270,6 @@ impl Scalar { .non_zero() .expect("computationally unreachable") } - - /// Converts a [`NonZeroU32`] into a `Scalar`. - /// - /// [`NonZeroU32`]: core::num::NonZeroU32 - pub fn from_non_zero_u32(int: core::num::NonZeroU32) -> Self { - Self::from_inner(backend::BackendScalar::from_u32(int.get())) - } } impl Scalar { @@ -353,6 +354,12 @@ impl From for Scalar { } } +impl From for Scalar { + fn from(int: NonZeroU32) -> Self { + Self::from_inner(backend::BackendScalar::from_u32(int.into())) + } +} + crate::impl_fromstr_deserialize! { name => "secp256k1 scalar", fn from_bytes(bytes: [u8;32]) -> Option> {