diff --git a/sign/frost/src/algorithm.rs b/sign/frost/src/algorithm.rs index 58325bf7..97f2fc6e 100644 --- a/sign/frost/src/algorithm.rs +++ b/sign/frost/src/algorithm.rs @@ -11,14 +11,12 @@ pub trait Algorithm: Clone { /// The resulting type of the signatures this algorithm will produce type Signature: Clone + Debug; - /// Context for this algorithm to be hashed into b, and therefore committed to - fn context(&self) -> Vec; - /// The amount of bytes from each participant's addendum to commit to fn addendum_commit_len() -> usize; /// Generate an addendum to FROST"s preprocessing stage fn preprocess_addendum( + &mut self, rng: &mut R, params: &sign::ParamsView, nonces: &[C::F; 2], @@ -30,10 +28,15 @@ pub trait Algorithm: Clone { params: &sign::ParamsView, l: usize, commitments: &[C::G; 2], - p: &C::F, serialized: &[u8], ) -> Result<(), FrostError>; + /// Context for this algorithm to be hashed into b, and therefore committed to + fn context(&self) -> Vec; + + /// Process the binding factor generated from all the committed to data + fn process_binding(&mut self, p: &C::F); + /// Sign a share with the given secret/nonce /// The secret will already have been its lagrange coefficient applied so it is the necessary /// key share @@ -92,15 +95,12 @@ pub struct SchnorrSignature { impl> Algorithm for Schnorr { type Signature = SchnorrSignature; - fn context(&self) -> Vec { - vec![] - } - fn addendum_commit_len() -> usize { 0 } fn preprocess_addendum( + &mut self, _: &mut R, _: &sign::ParamsView, _: &[C::F; 2], @@ -113,12 +113,17 @@ impl> Algorithm for Schnorr { _: &sign::ParamsView, _: usize, _: &[C::G; 2], - _: &C::F, _: &[u8], ) -> Result<(), FrostError> { Ok(()) } + fn context(&self) -> Vec { + vec![] + } + + fn process_binding(&mut self, _: &C::F) {} + fn sign_share( &mut self, params: &sign::ParamsView, diff --git a/sign/frost/src/sign.rs b/sign/frost/src/sign.rs index ec1cbaf4..efd911f2 100644 --- a/sign/frost/src/sign.rs +++ b/sign/frost/src/sign.rs @@ -138,7 +138,7 @@ struct PreprocessPackage { // a simpler UX fn preprocess>( rng: &mut R, - params: &Params, + params: &mut Params, ) -> PreprocessPackage { let nonces = [C::F::random(&mut *rng), C::F::random(&mut *rng)]; let commitments = [C::generator_table() * nonces[0], C::generator_table() * nonces[1]]; @@ -146,7 +146,7 @@ fn preprocess>( serialized.extend(&C::G_to_bytes(&commitments[1])); serialized.extend( - &A::preprocess_addendum( + ¶ms.algorithm.preprocess_addendum( rng, ¶ms.view, &nonces @@ -187,22 +187,18 @@ fn sign_with_share>( } let commitments_len = C::G_len() * 2; + // Allow algorithms to commit to more data than just the included nonces + // Not IETF draft compliant yet it doesn't prevent a compliant Schnorr algorithm from being used + // with this library, which does ship one let commit_len = commitments_len + A::addendum_commit_len(); #[allow(non_snake_case)] let mut B = Vec::with_capacity(multisig_params.n + 1); B.push(None); // Commitments + a presumed 32-byte hash of the message - let mut b: Vec = Vec::with_capacity((multisig_params.n * 2 * C::G_len()) + 32); - - // If the offset functionality provided by this library is in use, include it in the binding - // factor - if params.keys.offset.is_some() { - b.extend(&C::F_to_le_bytes(¶ms.keys.offset.unwrap())); - } - // Also include any context the algorithm may want to specify - b.extend(¶ms.algorithm.context()); + let mut b: Vec = Vec::with_capacity((multisig_params.t * 2 * C::G_len()) + 32); + // Parse the commitments and prepare the binding factor for l in 1 ..= multisig_params.n { if l == multisig_params.i { if commitments[l].is_some() { @@ -244,16 +240,13 @@ fn sign_with_share>( b.extend(&commitments[0 .. commit_len]); } - b.extend(&C::hash_msg(&msg)); - let b = C::hash_to_F(&b); - + // Process the commitments and addendums let view = ¶ms.view; for l in ¶ms.view.included { params.algorithm.process_addendum( view, *l, B[*l].as_ref().unwrap(), - &b, if *l == multisig_params.i { &our_preprocess.serialized[commitments_len .. our_preprocess.serialized.len()] } else { @@ -264,6 +257,34 @@ fn sign_with_share>( )?; } + // Finish the binding factor + b.extend(&C::hash_msg(&msg)); + + // If the following are used with certain lengths, it is possible to craft distinct + // commitments/messages/contexts with the same binding factor. While we can't length prefix the + // commitments, unfortunately, we can tag and length prefix the following + + // If the offset functionality provided by this library is in use, include it in the binding + // factor. Not compliant with the IETF spec which doesn't have a concept of offsets + if params.keys.offset.is_some() { + b.extend(b"offset"); + b.extend(u64::try_from(C::F_len()).unwrap().to_le_bytes()); + b.extend(&C::F_to_le_bytes(¶ms.keys.offset.unwrap())); + } + + // Also include any context the algorithm may want to specify. Again not compliant with the IETF + // spec which doesn't considered there may be signatures other than Schnorr being generated with + // FROST + let context = params.algorithm.context(); + if context.len() != 0 { + b.extend(b"context"); + b.extend(u64::try_from(context.len()).unwrap().to_le_bytes()); + b.extend(&context); + } + + let b = C::hash_to_F(&b); + params.algorithm.process_binding(&b); + #[allow(non_snake_case)] let mut Ris = vec![]; #[allow(non_snake_case)] @@ -405,7 +426,7 @@ impl> StateMachine { if self.state != State::Fresh { Err(FrostError::InvalidSignTransition(State::Fresh, self.state))?; } - let preprocess = preprocess::(rng, &self.params); + let preprocess = preprocess::(rng, &mut self.params); let serialized = preprocess.serialized.clone(); self.preprocess = Some(preprocess); self.state = State::Preprocessed;