use core::{convert::TryFrom, cmp::min, fmt}; use rand_core::{RngCore, CryptoRng}; use ff::{Field, PrimeField}; use group::Group; use crate::{Curve, MultisigParams, MultisigKeys, FrostError}; #[allow(non_snake_case)] fn challenge(l: usize, context: &str, R: &[u8], Am: &[u8]) -> C::F { let mut c = Vec::with_capacity(8 + context.len() + R.len() + Am.len()); c.extend(&u64::try_from(l).unwrap().to_le_bytes()); c.extend(context.as_bytes()); c.extend(R); // R c.extend(Am); // A of the first commitment, which is what we're proving we have the private key // for // m of the rest of the commitments, authenticating them C::hash_to_F(&c) } // Implements steps 1 through 3 of round 1 of FROST DKG. Returns the coefficients, commitments, and // the serialized commitments to be broadcasted over an authenticated channel to all parties // TODO: This potentially could return a much more robust serialized message, including a signature // of its entirety. The issue is it can't use its own key as it has no chain of custody behind it. // While we could ask for a key to be passed in, explicitly declaring the needed for authenticated // communications in the API itself, systems will likely already provide a authenticated // communication method making this redundant. It also doesn't guarantee the system which passed // the key is correctly using it, meaning we can only minimize risk so much // One notable improvement would be to include the index in the message. While the system must // still track this to determine if it's ready for the next step, and to remove duplicates, it // would ensure no counterparties presume the same index and this system didn't mislabel a // counterparty fn generate_key_r1( rng: &mut R, params: &MultisigParams, context: &str, ) -> (Vec, Vec, Vec) { let mut coefficients = Vec::with_capacity(params.t); let mut commitments = Vec::with_capacity(params.t); let mut serialized = Vec::with_capacity((C::G_len() * params.t) + C::G_len() + C::F_len()); for j in 0 .. params.t { // Step 1: Generate t random values to form a polynomial with coefficients.push(C::F::random(&mut *rng)); // Step 3: Generate public commitments commitments.push(C::generator_table() * coefficients[j]); // Serialize them for publication serialized.extend(&C::G_to_bytes(&commitments[j])); } // Step 2: Provide a proof of knowledge // This can be deterministic as the PoK is a singleton never opened up to cooperative discussion // There's also no reason to spend the time and effort to make this deterministic besides a // general obsession with canonicity and determinism let k = C::F::random(rng); #[allow(non_snake_case)] let R = C::generator_table() * k; let c = challenge::(params.i, context, &C::G_to_bytes(&R), &serialized); let s = k + (coefficients[0] * c); serialized.extend(&C::G_to_bytes(&R)); serialized.extend(&C::F_to_le_bytes(&s)); // Step 4: Broadcast (coefficients, commitments, serialized) } // Verify the received data from the first round of key generation fn verify_r1( rng: &mut R, params: &MultisigParams, context: &str, our_commitments: Vec, serialized: &[Vec], ) -> Result>, FrostError> { // Deserialize all of the commitments, validating the input buffers as needed if serialized.len() != (params.n + 1) { Err( // Prevents a panic if serialized.len() == 0 FrostError::InvalidParticipantQuantity(params.n, serialized.len() - min(1, serialized.len())) )?; } // Expect a null set of commitments for index 0 so the vector is guaranteed to line up with // actual indexes. Even if we did the offset internally, the system would need to write the vec // with the same offset in mind. Therefore, this trick which is probably slightly less efficient // yet keeps everything simple is preferred if serialized[0] != vec![] { Err(FrostError::NonEmptyParticipantZero)?; } let commitments_len = params.t * C::G_len(); let mut commitments = Vec::with_capacity(params.n + 1); commitments.push(vec![]); let signature_len = C::G_len() + C::F_len(); let mut first = true; let mut scalars = Vec::with_capacity((params.n - 1) * 3); let mut points = Vec::with_capacity((params.n - 1) * 3); for l in 1 ..= params.n { if l == params.i { if serialized[l].len() != 0 { Err(FrostError::DuplicatedIndex(l))?; } commitments.push(vec![]); continue; } if serialized[l].len() != (commitments_len + signature_len) { // Return an error with an approximation for how many commitments were included // Prevents errors if not even the signature was included if serialized[l].len() < signature_len { Err(FrostError::InvalidCommitmentQuantity(l, params.t, 0))?; } Err( FrostError::InvalidCommitmentQuantity( l, params.t, // Could technically be x.y despite this returning x, yet any y is negligible // It could help with debugging to know a partial piece of data was read but this error // alone should be enough (serialized[l].len() - signature_len) / C::G_len() ) )?; } commitments.push(Vec::with_capacity(params.t)); for o in 0 .. params.t { commitments[l].push( C::G_from_slice( &serialized[l][(o * C::G_len()) .. ((o + 1) * C::G_len())] ).map_err(|_| FrostError::InvalidCommitment(l))? ); } // Step 5: Validate each proof of knowledge (prep) let mut u = C::F::one(); if !first { u = C::F::random(&mut *rng); } scalars.push(u); points.push( C::G_from_slice( &serialized[l][commitments_len .. commitments_len + C::G_len()] ).map_err(|_| FrostError::InvalidProofOfKnowledge(l))? ); scalars.push( -C::F_from_le_slice( &serialized[l][commitments_len + C::G_len() .. serialized[l].len()] ).map_err(|_| FrostError::InvalidProofOfKnowledge(l))? * u ); points.push(C::generator()); let c = challenge::( l, context, &serialized[l][commitments_len .. commitments_len + C::G_len()], &serialized[l][0 .. commitments_len] ); if first { scalars.push(c); first = false; } else { scalars.push(c * u); } points.push(commitments[l][0]); } // Step 5: Implementation // Uses batch verification to optimize the success case dramatically // On failure, the cost is now this + blame, yet that should happen infrequently if C::multiexp_vartime(&scalars, &points) != C::G::identity() { for l in 1 ..= params.n { if l == params.i { continue; } #[allow(non_snake_case)] let R = C::G_from_slice( &serialized[l][commitments_len .. commitments_len + C::G_len()] ).map_err(|_| FrostError::InvalidProofOfKnowledge(l))?; let s = C::F_from_le_slice( &serialized[l][commitments_len + C::G_len() .. serialized[l].len()] ).map_err(|_| FrostError::InvalidProofOfKnowledge(l))?; let c = challenge::( l, context, &serialized[l][commitments_len .. commitments_len + C::G_len()], &serialized[l][0 .. commitments_len] ); if R != ((C::generator_table() * s) + (commitments[l][0] * (C::F::zero() - &c))) { Err(FrostError::InvalidProofOfKnowledge(l))?; } } Err(FrostError::InternalError("batch validation is broken".to_string()))?; } // Write in our own commitments commitments[params.i] = our_commitments; Ok(commitments) } fn polynomial( coefficients: &[F], i: usize ) -> F { let i = F::from(u64::try_from(i).unwrap()); let mut share = F::zero(); for (idx, coefficient) in coefficients.iter().rev().enumerate() { share += coefficient; if idx != (coefficients.len() - 1) { share *= i; } } share } // Implements round 1, step 5 and round 2, step 1 of FROST key generation // Returns our secret share part, commitments for the next step, and a vector for each // counterparty to receive fn generate_key_r2( rng: &mut R, params: &MultisigParams, context: &str, coefficients: Vec, our_commitments: Vec, commitments: &[Vec], ) -> Result<(C::F, Vec>, Vec>), FrostError> { let commitments = verify_r1::(rng, params, context, our_commitments, commitments)?; // Step 1: Generate secret shares for all other parties let mut res = Vec::with_capacity(params.n + 1); res.push(vec![]); for i in 1 ..= params.n { // Don't push our own to the byte buffer which is meant to be sent around // An app developer could accidentally send it. Best to keep this black boxed if i == params.i { res.push(vec![]); continue } res.push(C::F_to_le_bytes(&polynomial(&coefficients, i))); } // Calculate our own share let share = polynomial(&coefficients, params.i); // The secret shares are discarded here, not cleared. While any system which leaves its memory // accessible is likely totally lost already, making the distinction meaningless when the key gen // system acts as the signer system and therefore actively holds the signing key anyways, it // should be overwritten with /dev/urandom in the name of security (which still doesn't meet // requirements for secure data deletion yet those requirements expect hardware access which is // far past what this library can reasonably counter) // TODO: Zero out the coefficients Ok((share, commitments, res)) } /// Finishes round 2 and returns both the secret share and the serialized public key. /// This key is not usable until all parties confirm they have completed the protocol without /// issue, yet simply confirming protocol completion without issue is enough to confirm the same /// key was generated as long as a lack of duplicated commitments was also confirmed when they were /// broadcasted initially fn complete_r2( params: MultisigParams, share: C::F, commitments: &[Vec], // Vec to preserve ownership serialized: Vec>, ) -> Result, FrostError> { // Step 2. Verify each share if serialized.len() != (params.n + 1) { Err( FrostError::InvalidParticipantQuantity(params.n, serialized.len() - min(1, serialized.len())) )?; } if (commitments[0].len() != 0) || (serialized[0].len() != 0) { Err(FrostError::NonEmptyParticipantZero)?; } // Deserialize them let mut shares: Vec = vec![C::F::zero()]; for i in 1 .. serialized.len() { if i == params.i { if serialized[i].len() != 0 { Err(FrostError::DuplicatedIndex(i))?; } shares.push(C::F::zero()); continue; } shares.push(C::F_from_le_slice(&serialized[i]).map_err(|_| FrostError::InvalidShare(i))?); } for l in 1 ..= params.n { if l == params.i { continue; } let i_scalar = C::F::from(u64::try_from(params.i).unwrap()); let mut exp = C::F::one(); let mut exps = Vec::with_capacity(params.t); for _ in 0 .. params.t { exps.push(exp); exp *= i_scalar; } // Doesn't use multiexp_vartime with -shares[l] due to not being able to push to commitments if C::multiexp_vartime(&exps, &commitments[l]) != (C::generator_table() * shares[l]) { Err(FrostError::InvalidCommitment(l))?; } } // TODO: Clear the original share let mut secret_share = share; for remote_share in shares { secret_share += remote_share; } let mut verification_shares = vec![C::G::identity()]; for i in 1 ..= params.n { let mut exps = vec![]; let mut cs = vec![]; for j in 1 ..= params.n { for k in 0 .. params.t { let mut exp = C::F::one(); for _ in 0 .. k { exp *= C::F::from(u64::try_from(i).unwrap()); } exps.push(exp); cs.push(commitments[j][k]); } } verification_shares.push(C::multiexp_vartime(&exps, &cs)); } debug_assert_eq!( C::generator_table() * secret_share, verification_shares[params.i] ); let mut group_key = C::G::identity(); for j in 1 ..= params.n { group_key += commitments[j][0]; } // TODO: Clear serialized and shares Ok(MultisigKeys { params, secret_share, group_key, verification_shares, offset: None } ) } /// State of a Key Generation machine #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum State { Fresh, GeneratedCoefficients, GeneratedSecretShares, Complete, } impl fmt::Display for State { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) } } /// State machine which manages key generation #[allow(non_snake_case)] pub struct StateMachine { params: MultisigParams, context: String, state: State, coefficients: Option>, our_commitments: Option>, secret: Option, commitments: Option>> } impl StateMachine { /// Creates a new machine to generate a key for the specified curve in the specified multisig // The context string must be unique among multisigs pub fn new(params: MultisigParams, context: String) -> StateMachine { StateMachine { params, context, state: State::Fresh, coefficients: None, our_commitments: None, secret: None, commitments: None } } /// Start generating a key according to the FROST DKG spec /// Returns a serialized list of commitments to be sent to all parties over an authenticated /// channel. If any party submits multiple sets of commitments, they MUST be treated as malicious pub fn generate_coefficients( &mut self, rng: &mut R ) -> Result, FrostError> { if self.state != State::Fresh { Err(FrostError::InvalidKeyGenTransition(State::Fresh, self.state))?; } let (coefficients, commitments, serialized) = generate_key_r1::( rng, &self.params, &self.context, ); self.coefficients = Some(coefficients); self.our_commitments = Some(commitments); self.state = State::GeneratedCoefficients; Ok(serialized) } /// Continue generating a key /// Takes in everyone else's commitments, which are expected to be in a Vec where participant /// index = Vec index. An empty vector is expected at index 0 to allow for this. An empty vector /// is also expected at index i which is locally handled. Returns a byte vector representing a /// secret share for each other participant which should be encrypted before sending pub fn generate_secret_shares( &mut self, rng: &mut R, commitments: Vec>, ) -> Result>, FrostError> { if self.state != State::GeneratedCoefficients { Err(FrostError::InvalidKeyGenTransition(State::GeneratedCoefficients, self.state))?; } let (secret, commitments, shares) = generate_key_r2::( rng, &self.params, &self.context, self.coefficients.take().unwrap(), self.our_commitments.take().unwrap(), &commitments, )?; self.secret = Some(secret); self.commitments = Some(commitments); self.state = State::GeneratedSecretShares; Ok(shares) } /// Complete key generation /// Takes in everyone elses' shares submitted to us as a Vec, expecting participant index = /// Vec index with an empty vector at index 0 and index i. Returns a byte vector representing the /// group's public key, while setting a valid secret share inside the machine. > t participants /// must report completion without issue before this key can be considered usable, yet you should /// wait for all participants to report as such pub fn complete( &mut self, shares: Vec>, ) -> Result, FrostError> { if self.state != State::GeneratedSecretShares { Err(FrostError::InvalidKeyGenTransition(State::GeneratedSecretShares, self.state))?; } let keys = complete_r2( self.params, self.secret.take().unwrap(), &self.commitments.take().unwrap(), shares, )?; self.state = State::Complete; Ok(keys) } pub fn params(&self) -> MultisigParams { self.params.clone() } pub fn state(&self) -> State { self.state } }