From 0f403a6b570819c4f97971cae1d36dcbb902182f Mon Sep 17 00:00:00 2001 From: LLFourn Date: Thu, 25 Jul 2024 19:23:21 +1000 Subject: [PATCH 01/15] [core] Allow Point::is_zero on Z: ZeroChoice In case you want a non-zero point generically --- secp256kfun/src/point.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/secp256kfun/src/point.rs b/secp256kfun/src/point.rs index a4d20e76..71491ebc 100644 --- a/secp256kfun/src/point.rs +++ b/secp256kfun/src/point.rs @@ -251,6 +251,19 @@ impl Point { backend::BackendPoint::is_zero(&self.0) } + /// Convert a point that is marked as `Zero` to `NonZero`. + /// + /// If the point *was* actually zero ([`is_zero`] returns true) it returns `None`. + /// + /// [`is_zero`]: Point::is_zero + pub fn non_zero(self) -> Option> { + if self.is_zero() { + None + } else { + Some(Point::from_inner(self.0, self.1)) + } + } + pub(crate) const fn from_inner(backend_point: backend::Point, point_type: T) -> Self { Point(backend_point, point_type, PhantomData) } @@ -421,19 +434,6 @@ impl Point { } impl Point { - /// Convert a point that is marked as `Zero` to `NonZero`. - /// - /// If the point *was* actually zero ([`is_zero`] returns true) it returns `None`. - /// - /// [`is_zero`]: Point::is_zero - pub fn non_zero(self) -> Option> { - if self.is_zero() { - None - } else { - Some(Point::from_inner(self.0, self.1)) - } - } - /// Returns the [`identity element`] of the group A.K.A. the point at infinity. /// /// # Example From eb6dc14994d0dc99e1b1118d58d7128aff386ca5 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Thu, 25 Jul 2024 19:25:26 +1000 Subject: [PATCH 02/15] [frost] Make "tweaks" mutate Prior to this commit tweaks were being stored in a separate field inside `FrostKey` and were applied at signature combination time. In frostsnap we actually want to be able to do "one-way" tweaks where you forget the original polynomial (at least the first coefficient). This is not an API safety concern but we strictly don't want the original to exist anymore. The next problem we address is that we don't want you to need the full polynomial for signing. Instead we make the user provide a "paired" secret share which is the secret scalar and index paired with the shared key (first coefficient of polynomial). This is a convenient combination since it allows you to hash the shared key to figure out a tweak and then apply it to both the shared key and the shared secret. --- schnorr_fun/src/binonce.rs | 33 +- schnorr_fun/src/frost/frost_poly.rs | 210 ++++++++++++ schnorr_fun/src/frost/mod.rs | 506 +++++++++------------------- schnorr_fun/src/frost/share.rs | 152 ++++++++- schnorr_fun/tests/frost_prop.rs | 61 ++-- secp256kfun/src/poly.rs | 30 +- 6 files changed, 600 insertions(+), 392 deletions(-) create mode 100644 schnorr_fun/src/frost/frost_poly.rs diff --git a/schnorr_fun/src/binonce.rs b/schnorr_fun/src/binonce.rs index a16ac65e..daa1fee8 100644 --- a/schnorr_fun/src/binonce.rs +++ b/schnorr_fun/src/binonce.rs @@ -4,7 +4,7 @@ //! Your public nonces are derived from scalars which must be kept secret. //! Derived binonces should be unique and and must not be reused for signing under any circumstances //! as this can leak your secret key. -use secp256kfun::{g, marker::*, rand_core::RngCore, Point, Scalar, G}; +use secp256kfun::{g, hash::HashInto, marker::*, rand_core::RngCore, Point, Scalar, G}; /// A nonce (pair of points) that each party must share with the others in the first stage of signing. /// @@ -26,7 +26,7 @@ impl Nonce { } } -impl Nonce { +impl Nonce { /// Negate the two nonces pub fn conditional_negate(&mut self, needs_negation: bool) { self.0[0] = self.0[0].conditional_negate(needs_negation); @@ -42,6 +42,33 @@ impl Nonce { bytes[33..].copy_from_slice(self.0[1].to_bytes().as_ref()); bytes } + + /// Binds an aggregated binonce to a it's binding coefficient (which is produced differently for + /// different schemes) and produces the final nonce (the one that will go into the signature). + pub fn bind(&self, binding_coeff: Scalar) -> (Point, bool) { + g!(self.0[0] + binding_coeff * self.0[1]) + .normalize() + .non_zero() + .unwrap_or(Point::generator()) + .into_point_with_even_y() + } +} + +impl HashInto for Nonce { + fn hash_into(self, hash: &mut impl secp256kfun::digest::Digest) { + self.0.hash_into(hash) + } +} + +impl Nonce { + /// Adds a bunch of binonces together (one for each party signing usually). + pub fn aggregate(nonces: impl Iterator>) -> Self { + let agg = nonces.fold([Point::zero(); 2], |acc, nonce| { + [g!(acc[0] + nonce.0[0]), g!(acc[1] + nonce.0[1])] + }); + + Self([agg[0].normalize(), agg[1].normalize()]) + } } secp256kfun::impl_fromstr_deserialize! { @@ -52,7 +79,7 @@ secp256kfun::impl_fromstr_deserialize! { } secp256kfun::impl_display_serialize! { - fn to_bytes(nonce: &Nonce) -> [u8;66] { + fn to_bytes(nonce: &Nonce) -> [u8;66] { nonce.to_bytes() } } diff --git a/schnorr_fun/src/frost/frost_poly.rs b/schnorr_fun/src/frost/frost_poly.rs new file mode 100644 index 00000000..05225d80 --- /dev/null +++ b/schnorr_fun/src/frost/frost_poly.rs @@ -0,0 +1,210 @@ +use core::marker::PhantomData; + +use alloc::vec::Vec; +use secp256kfun::{poly, prelude::*}; + +use super::PartyIndex; +/// A polynomial +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr( + feature = "serde", + derive(crate::fun::serde::Serialize), + serde(crate = "crate::fun::serde") +)] +#[cfg_attr( + feature = "bincode", + derive(crate::fun::bincode::Encode), + bincode(crate = "crate::fun::bincode",) +)] +pub struct FrostPoly { + /// The public point polynomial that defines the access structure to the FROST key. + point_polynomial: Vec>, + #[cfg_attr(feature = "serde", serde(skip))] + ty: PhantomData, +} + +impl FrostPoly { + /// The verification shares of each party in the key. + /// + /// The verification share is the image of their secret share. + pub fn verification_share(&self, index: PartyIndex) -> Point { + poly::point::eval(&self.point_polynomial, index) + } + + /// The threshold number of participants required in a signing coalition to produce a valid signature. + pub fn threshold(&self) -> usize { + self.point_polynomial.len() + } + + /// The public image of the key's polynomial on the elliptic curve. + /// + /// Note: the first coefficient (index `0`) is guaranteed to be non-zero but the coefficients + /// may be. + pub fn point_polynomial(&self) -> Vec> { + self.point_polynomial.clone() + } +} + +impl FrostPoly { + /// The key that was shared with this polynomial defining the sharing. + /// + /// This is the first coefficient of the polynomial. + pub fn shared_key(&self) -> Point { + self.point_polynomial[0].non_zero().expect("invariant") + } + /// Constructor to create a `FrostPoly` from a vector of points. + /// + /// Returns `None` if the first coefficient is [`Point::zero`]. + pub fn from_poly(poly: Vec>) -> Option { + if poly.is_empty() { + return None; + } + + if poly[0].is_zero() { + return None; + } + + Some(Self { + point_polynomial: poly, + ty: PhantomData, + }) + } + + /// Create a `FrostPoly` from a set of verification shares. + pub fn from_verification_shares( + shares: &[(PartyIndex, Point)], + ) -> Self { + let poly = poly::point::interpolate(shares); + Self { + point_polynomial: poly::point::normalize(poly).collect(), + ty: PhantomData, + } + } + /// Convert the key into a BIP340 FrostKey. + /// + /// This is the [BIP340] compatible version of the key which you can put in a segwitv1 output. + /// + /// [BIP340]: https://bips.xyz/340 + pub fn into_xonly(mut self) -> FrostPoly { + let needs_negation = !self.shared_key().is_y_even(); + if needs_negation { + self.homomorphic_negate(); + debug_assert!(self.shared_key().is_y_even()); + } + FrostPoly { + point_polynomial: self.point_polynomial, + ty: PhantomData, + } + } + + /// Adds a scalar `tweak` to the shared key. + /// + /// This is useful for deriving unhardened child frost keys from a master frost public key using + /// [BIP32]. + /// + /// In order for `PairedSecretShare` s to be valid against the new key they will have to apply the same operation. + /// + /// ## Return value + /// + /// Returns a new [`FrostKey`] with the same parties but a different frost public key. + /// In the erroneous case that the tweak is exactly equal to the negation of the aggregate + /// secret key it returns `None`. + /// + /// [BIP32]: https://bips.xyz/32 + #[must_use] + pub fn homomorphic_add(mut self, tweak: Scalar) -> Option { + self.point_polynomial[0] = g!(self.point_polynomial[0] + tweak * G).normalize(); + if self.point_polynomial[0].is_zero() { + None + } else { + Some(self) + } + } + + /// Negates the polynomial + pub fn homomorphic_negate(&mut self) { + poly::point::negate(&mut self.point_polynomial) + } +} + +impl FrostPoly { + /// Applies an "XOnly" tweak to the FROST public key. + /// This is how you embed a taproot commitment into a frost public key + /// + /// Tweak the frost public key with a scalar so that the resulting key is equal to the + /// existing key plus `tweak * G` as an [`EvenY`] point. The tweak mutates the public key while still allowing + /// the original set of signers to sign under the new key. + /// + /// ## Return value + /// + /// Returns a new [`FrostKey`] with the same parties but a different frost public key. + /// In the erroneous case that the tweak is exactly equal to the negation of the aggregate + /// secret key it returns `None`. + pub fn xonly_homomorphic_add( + mut self, + tweak: Scalar, + ) -> Option { + self.point_polynomial[0] = g!(self.point_polynomial[0] + tweak * G).normalize(); + + let needs_negation = !self.point_polynomial[0].non_zero()?.is_y_even(); + if needs_negation { + poly::point::negate(&mut self.point_polynomial); + } + + Some(self) + } + + /// The public key that would have signatures verified against for this shared key. + pub fn shared_key(&self) -> Point { + let (even_y_point, _needs_negation) = self.point_polynomial[0] + .non_zero() + .expect("invariant") + .into_point_with_even_y(); + assert!(!_needs_negation); + even_y_point + } +} + +#[cfg(feature = "bincode")] +impl crate::fun::bincode::Decode for FrostPoly { + fn decode( + decoder: &mut D, + ) -> Result { + let poly = Vec::>::decode(decoder)?; + + if poly[0].is_zero() { + return Err(secp256kfun::bincode::error::DecodeError::Other( + "first coefficient of a frost polynomial can't be zero", + )); + } + + Ok(FrostPoly { + point_polynomial: poly, + ty: PhantomData, + }) + } +} + +#[cfg(feature = "serde")] +impl<'de> crate::fun::serde::Deserialize<'de> for FrostPoly { + fn deserialize(deserializer: D) -> Result + where + D: secp256kfun::serde::Deserializer<'de>, + { + let poly = Vec::>::deserialize(deserializer)?; + + if poly[0].is_zero() { + return Err(crate::fun::serde::de::Error::custom( + "first coefficient of a frost polynomial can't be zero", + )); + } + + Ok(Self { + point_polynomial: poly, + ty: PhantomData, + }) + } +} + +#[cfg(feature = "bincode")] +crate::fun::bincode::impl_borrow_decode!(FrostPoly); diff --git a/schnorr_fun/src/frost/mod.rs b/schnorr_fun/src/frost/mod.rs index d552281e..a69a8d0c 100644 --- a/schnorr_fun/src/frost/mod.rs +++ b/schnorr_fun/src/frost/mod.rs @@ -175,12 +175,17 @@ //! [`musig`]: crate::musig //! [`Scalar`]: crate::fun::Scalar +mod frost_poly; +pub use frost_poly::*; mod share; pub use share::*; pub use crate::binonce::{Nonce, NonceKeyPair}; -use crate::{Message, Schnorr, Signature}; -use alloc::{collections::BTreeMap, vec::Vec}; +use crate::{binonce, Message, Schnorr, Signature}; +use alloc::{ + collections::{BTreeMap, BTreeSet}, + vec::Vec, +}; use core::num::NonZeroU32; use secp256kfun::{ derive_nonce_rng, @@ -296,7 +301,7 @@ where /// [`Frost::new_keygen`] #[derive(Clone, Debug)] pub struct KeyGen { - frost_key: FrostKey, + frost_poly: FrostPoly, point_polys: BTreeMap>, } @@ -364,143 +369,6 @@ impl core::fmt::Display for FinishKeyGenError { #[cfg(feature = "std")] impl std::error::Error for FinishKeyGenError {} -/// A FROST key -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct FrostKey { - /// The joint public key of the frost multisignature. - /// - /// This public key will change as you add tweaks or convert [`into_xonly_key`]. - /// Only initially will the public key match the first coefficient of the public polynomial. - tweaked_public_key: Point, - /// The public point polynomial that defines the access structure to the FROST key. - point_polynomial: Vec>, - /// The tweak applied to this frost key, tracks the aggregate tweak. - tweak: Scalar, - /// Whether the secret keys need to be negated during signing (only used for EvenY keys). - needs_negation: bool, -} - -impl FrostKey { - /// The public key with all tweaks applied - pub fn public_key(&self) -> Point { - self.tweaked_public_key - } - - /// The verification shares of each party in the key. - /// - /// The verification share is the image of their secret share. - pub fn verification_share(&self, index: &PartyIndex) -> Point { - poly::point::eval(&self.point_polynomial, *index) - } - - /// The threshold number of participants required in a signing coalition to produce a valid signature. - pub fn threshold(&self) -> usize { - self.point_polynomial.len() - } - - /// The public image of the key's polynomial on the elliptic curve. - /// - /// Note: the first coefficient (index `0`) is guaranteed to be non-zero but the coefficients - /// may be. - pub fn point_polynomial(&self) -> Vec> { - self.point_polynomial.clone() - } -} - -impl FrostKey { - /// Convert the key into a BIP340 FrostKey. - /// - /// This is the [BIP340] compatible version of the key which you can put in a segwitv1 output. - /// - /// [BIP340]: https://bips.xyz/340 - pub fn into_xonly_key(self) -> FrostKey { - let (tweaked_public_key, needs_negation) = self.public_key().into_point_with_even_y(); - let mut tweak = self.tweak; - tweak.conditional_negate(needs_negation); - FrostKey { - tweaked_public_key, - point_polynomial: self.point_polynomial, - tweak, - needs_negation, - } - } - - /// Apply a plain tweak to the frost public key. - /// - /// This is useful for deriving unhardened child frost keys from a master frost public key using [BIP32]. - /// - /// Tweak the frost public key with a scalar so that the resulting key is equal to the - /// existing key plus `tweak * G`. The tweak mutates the public key while still allowing - /// the original set of signers to sign under the new key. - /// - /// ## Return value - /// - /// Returns a new [`FrostKey`] with the same parties but a different frost public key. - /// In the erroneous case that the tweak is exactly equal to the negation of the aggregate - /// secret key it returns `None`. - /// - /// [BIP32]: https://bips.xyz/32 - pub fn tweak(self, tweak: Scalar) -> Option { - let tweaked_public_key = g!(self.tweaked_public_key + tweak * G) - .normalize() - .non_zero()?; - let tweak = s!(self.tweak + tweak).public(); - - Some(FrostKey { - tweaked_public_key, - point_polynomial: self.point_polynomial, - tweak, - needs_negation: self.needs_negation, - }) - } - - /// Put the `FrostKey` into a form that can be encoded/decode. - /// - /// Note this encoding **ignores the tweaks that have been applied to the `FrostKey`**. To - /// restore tweaks you must reapply them after it's been [`decode`]d. - /// - /// [`decode`]: Self::decode - pub fn encode(&self) -> EncodedFrostKey { - EncodedFrostKey::from(self.clone()) - } - - /// Decode the `FrostKey` from an `EncodedFrostKey`. - pub fn decode(encoded_frost_key: EncodedFrostKey) -> Self { - Self::from(encoded_frost_key) - } -} - -impl FrostKey { - /// Applies an "XOnly" tweak to the FROST public key. - /// This is how you embed a taproot commitment into a frost public key - /// - /// Tweak the frost public key with a scalar so that the resulting key is equal to the - /// existing key plus `tweak * G` as an [`EvenY`] point. The tweak mutates the public key while still allowing - /// the original set of signers to sign under the new key. - /// - /// ## Return value - /// - /// Returns a new [`FrostKey`] with the same parties but a different frost public key. - /// In the erroneous case that the tweak is exactly equal to the negation of the aggregate - /// secret key it returns `None`. - pub fn tweak(self, tweak: Scalar) -> Option { - let (new_public_key, needs_negation) = g!(self.tweaked_public_key + tweak * G) - .normalize() - .non_zero()? - .into_point_with_even_y(); - let mut new_tweak = s!(self.tweak + tweak).public(); - new_tweak.conditional_negate(needs_negation); - let needs_negation = self.needs_negation ^ needs_negation; - - Some(Self { - tweaked_public_key: new_public_key, - point_polynomial: self.point_polynomial, - needs_negation, - tweak: new_tweak, - }) - } -} - impl + Clone, NG: NonceGen> 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 @@ -543,7 +411,7 @@ impl + Clone, NG: NonceGen> Frost { /// /// Parameters: /// - /// - `frost_key`: the joint public key we are signing under. This can be an `XOnly` or `Normal` + /// - `frost_key`: the joint public key we are signing under. This can be an `EvenY` or `Normal` /// It will return the same nonce regardless. /// - `secret`: you're secret key share for the `frost_key` /// - `session_id`: a string of bytes that is **unique for each signing attempt**. @@ -564,18 +432,16 @@ impl + Clone, NG: NonceGen> Frost { /// resulting rng for each input. pub fn seed_nonce_rng>( &self, - frost_key: &FrostKey, - secret: &Scalar, + paired_secret_share: PairedSecretShare, session_id: &[u8], ) -> R { let sid_len = (session_id.len() as u64).to_be_bytes(); - let threshold_bytes = (frost_key.threshold() as u64).to_be_bytes(); - let pk_bytes = frost_key.public_key().to_xonly_bytes(); + let pk_bytes = paired_secret_share.shared_key().to_xonly_bytes(); let rng: R = derive_nonce_rng!( nonce_gen => self.nonce_gen(), - secret => &secret, - public => [pk_bytes, threshold_bytes, sid_len, session_id], + secret => paired_secret_share.share(), + public => [pk_bytes, sid_len, session_id], seedable_rng => R ); rng @@ -591,7 +457,7 @@ impl + Clone, NG: NonceGen> Frost { threshold: usize, n_parties: usize, rng: &mut impl RngCore, - ) -> (FrostKey, Vec) { + ) -> (FrostPoly, Vec>) { let scalar_polys = (0..n_parties) .map(|i| { ( @@ -732,22 +598,17 @@ impl + Clone, NG> Frost { } } - let public_key = joint_poly[0] - .normalize() - .non_zero() - .ok_or(NewKeyGenError::ZeroFrostKey)?; + let frost_poly = FrostPoly::from_poly( + joint_poly + .into_iter() + .map(|coef| coef.normalize()) + .collect(), + ) + .ok_or(NewKeyGenError::ZeroFrostKey)?; Ok(KeyGen { point_polys, - frost_key: FrostKey { - tweaked_public_key: public_key, - point_polynomial: joint_poly - .into_iter() - .map(|coef| coef.normalize()) - .collect(), - tweak: Scalar::zero(), - needs_negation: false, - }, + frost_poly, }) } @@ -757,7 +618,7 @@ impl + Clone, NG> Frost { keygen: KeyGen, proofs_of_possession: BTreeMap, proof_of_possession_msg: Message, - ) -> Result, FinishKeyGenError> { + ) -> Result, FinishKeyGenError> { for (party_index, poly) in &keygen.point_polys { let pop = proofs_of_possession .get(party_index) @@ -772,7 +633,7 @@ impl + Clone, NG> Frost { } } - Ok(keygen.frost_key) + Ok(keygen.frost_poly) } /// Combine all receieved shares into your long-lived secret share. @@ -793,7 +654,7 @@ impl + Clone, NG> Frost { my_index: PartyIndex, secret_shares: BTreeMap, Signature)>, proof_of_possession_msg: Message, - ) -> Result<(SecretShare, FrostKey), FinishKeyGenError> { + ) -> Result<(PairedSecretShare, FrostPoly), FinishKeyGenError> { let mut total_secret_share = s!(0); for (party_index, poly) in &keygen.point_polys { @@ -816,73 +677,98 @@ impl + Clone, NG> Frost { total_secret_share += secret_share; } - Ok(( - SecretShare { - index: my_index, - secret: total_secret_share, - }, - keygen.frost_key, - )) + let secret_share = SecretShare { + index: my_index, + share: total_secret_share, + }; + + let secret_share_with_image = + PairedSecretShare::new(secret_share, keygen.frost_poly.shared_key()); + + Ok((secret_share_with_image, keygen.frost_poly)) + } + + /// Start party signing session + pub fn start_party_sign_session( + &self, + shared_key: Point, + parties: BTreeSet, + agg_binonce: Nonce, + message: Message, + ) -> PartySignSession { + let binding_coeff = self.binding_coefficient(shared_key, agg_binonce, message); + let (final_nonce, binonce_needs_negation) = agg_binonce.bind(binding_coeff); + let challenge = self.schnorr.challenge(&final_nonce, &shared_key, message); + + PartySignSession { + shared_key, + parties, + binding_coeff, + challenge, + binonce_needs_negation, + } } - /// Start a FROST signing session. + /// Start a FROST signing session as a *coordinator*. + /// + /// The corodinator must have collected nonces from each of the signers and pass them in as `nonces`. + /// From there /// - /// Each signing party must call this with the same arguments for it to succeeed. This means you - /// must all agree on each other's nonces before starting the sign session. In `nonces` each - /// item is the signer's index and their `Nonce`. It's length must be at least `threshold`. - /// Generating your own nonces can be done with [`Frost::gen_nonce`]. /// /// # Panics /// /// If the number of nonces is less than the threshold. - pub fn start_sign_session( + pub fn start_coordinator_sign_session( &self, - frost_key: &FrostKey, - nonces: BTreeMap, + frost_poly: &FrostPoly, + mut nonces: BTreeMap, message: Message, - ) -> SignSession { - let nonce_map = nonces; - - if nonce_map.len() < frost_key.threshold() { + ) -> CoordinatorSignSession { + if nonces.len() < frost_poly.threshold() { panic!("nonces' length was less than the threshold"); } - let agg_nonce = nonce_map - .iter() - .fold([Point::zero(); 2], |acc, (_, nonce)| { - [g!(acc[0] + nonce.0[0]), g!(acc[1] + nonce.0[1])] - }); + let agg_binonce = binonce::Nonce::aggregate(nonces.values().cloned()); - let agg_nonce = [agg_nonce[0].normalize(), agg_nonce[1].normalize()]; - - let binding_coeff = Scalar::from_hash( - self.binding_hash - .clone() - .add(agg_nonce[0]) - .add(agg_nonce[1]) - .add(frost_key.public_key()) - .add(message), - ); - let (agg_nonce, nonces_need_negation) = g!(agg_nonce[0] + binding_coeff * agg_nonce[1]) - .normalize() - .non_zero() - .unwrap_or(Point::generator()) - .into_point_with_even_y(); + let binding_coeff = self.binding_coefficient(frost_poly.shared_key(), agg_binonce, message); + let (final_nonce, binonce_needs_negation) = agg_binonce.bind(binding_coeff); let challenge = self .schnorr - .challenge(&agg_nonce, &frost_key.public_key(), message); + .challenge(&final_nonce, &frost_poly.shared_key(), message); + + for nonce in nonces.values_mut() { + nonce.conditional_negate(binonce_needs_negation); + } - SignSession { + CoordinatorSignSession { binding_coeff, - nonces_need_negation, - agg_nonce, + agg_binonce, + final_nonce, challenge, - nonces: nonce_map, + nonces, } } - /// Generates a partial signature share under the frost key using a secret share. + fn binding_coefficient( + &self, + public_key: Point, + agg_binonce: Nonce, + message: Message, + ) -> Scalar { + Scalar::from_hash( + self.binding_hash + .clone() + .add(agg_binonce) + .add(public_key) + .add(message), + ) + .public() + } + + /// Generates a signature share under the frost key using a secret share. + /// + /// The `secret_share` is taken as a `PairedSecretShare` to guarantee that the secret is aligned with an `EvenY` point. /// /// ## Return value /// @@ -890,32 +776,25 @@ impl + Clone, NG> Frost { /// /// ## Panics /// - /// Panics if the `secret_nonce` does not match the previously provided public nonce in the - /// `session`. + /// Panics if the shah pub fn sign( &self, - frost_key: &FrostKey, - session: &SignSession, - secret_share: &SecretShare, + session: &PartySignSession, + secret_share: &PairedSecretShare, secret_nonce: NonceKeyPair, ) -> Scalar { - let mut lambda = - poly::eval_basis_poly_at_0(secret_share.index, session.nonces.keys().cloned()); - assert_eq!( - *session - .nonces - .get(&secret_share.index) - .expect("my_index was not in session"), - secret_nonce.public(), - "secret nonce didn't match previously provided public nonce" - ); - lambda.conditional_negate(frost_key.needs_negation); + if session.shared_key != secret_share.shared_key() { + panic!("the share's shared key is not the same as the shared key of the session"); + } + let secret_share = secret_share.secret_share(); + let lambda = + poly::eval_basis_poly_at_0(secret_share.index, session.parties.iter().cloned()); let [mut r1, mut r2] = secret_nonce.secret; - r1.conditional_negate(session.nonces_need_negation); - r2.conditional_negate(session.nonces_need_negation); + r1.conditional_negate(session.binonce_needs_negation); + r2.conditional_negate(session.binonce_needs_negation); let b = &session.binding_coeff; - let x = &secret_share.secret; + let x = secret_share.share; let c = &session.challenge; s!(r1 + (r2 * b) + lambda * x * c).public() } @@ -927,24 +806,21 @@ impl + Clone, NG> Frost { /// Returns `bool`, true if partial signature is valid. pub fn verify_signature_share( &self, - frost_key: &FrostKey, - session: &SignSession, + frost_poly: &FrostPoly, + session: &CoordinatorSignSession, index: PartyIndex, signature_share: Scalar, ) -> bool { let s = signature_share; - let mut lambda = poly::eval_basis_poly_at_0(index, session.nonces.keys().cloned()); - lambda.conditional_negate(frost_key.needs_negation); + let lambda = poly::eval_basis_poly_at_0(index, session.nonces.keys().cloned()); let c = &session.challenge; let b = &session.binding_coeff; - let X = frost_key.verification_share(&index); + let X = frost_poly.verification_share(index); let [R1, R2] = session .nonces .get(&index) .expect("verifying party index that is not part of frost signing coalition") .0; - let R1 = R1.conditional_negate(session.nonces_need_negation); - let R2 = R2.conditional_negate(session.nonces_need_negation); g!(R1 + b * R2 + (c * lambda) * X - s * G).is_zero() } @@ -959,18 +835,16 @@ impl + Clone, NG> Frost { /// Returns a combined schnorr [`Signature`] on the message pub fn combine_signature_shares( &self, - frost_key: &FrostKey, - session: &SignSession, + session: &CoordinatorSignSession, signature_shares: Vec>, ) -> Signature { - let ck = s!(session.challenge * frost_key.tweak); let sum_s = signature_shares .into_iter() .reduce(|acc, partial_sig| s!(acc + partial_sig).public()) .unwrap_or(Scalar::zero()); Signature { - R: session.agg_nonce, - s: s!(sum_s + ck).public(), + R: session.final_nonce, + s: sum_s, } } } @@ -991,23 +865,53 @@ impl + Clone, NG> Frost { derive(crate::fun::serde::Deserialize, crate::fun::serde::Serialize), serde(crate = "crate::fun::serde") )] -pub struct SignSession { - binding_coeff: Scalar, - nonces_need_negation: bool, - agg_nonce: Point, +pub struct CoordinatorSignSession { + binding_coeff: Scalar, + agg_binonce: binonce::Nonce, + final_nonce: Point, challenge: Scalar, nonces: BTreeMap, } -impl SignSession { +impl CoordinatorSignSession { /// Fetch the participant indices for this signing session. /// /// ## Return value /// /// An iterator of participant indices - pub fn participants(&self) -> impl DoubleEndedIterator + '_ { - self.nonces.keys().copied() + pub fn participants(&self) -> BTreeSet { + self.nonces.keys().cloned().collect() } + + /// The aggregated nonce used to sign + pub fn agg_binonce(&self) -> binonce::Nonce { + self.agg_binonce + } + + /// The final nonce that will actually go on the blockchain + pub fn final_nonce(&self) -> Point { + self.final_nonce + } +} + +/// The session that is used to sign a message. +#[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 PartySignSession { + shared_key: Point, + parties: BTreeSet>, + challenge: Scalar, + binonce_needs_negation: bool, + binding_coeff: Scalar, } /// Constructor for a Frost instance using deterministic nonce generation. @@ -1055,106 +959,6 @@ where Frost::default() } -/// An encoded FROST key -/// -/// This encodes only stores the joint public polynomial. **It does not encode tweaks applied to the -/// `FrostKey`**. -#[derive(Clone, Debug, PartialEq, Eq)] -#[cfg_attr( - feature = "serde", - derive(crate::fun::serde::Serialize), - serde(crate = "crate::fun::serde") -)] -#[cfg_attr( - feature = "bincode", - derive(crate::fun::bincode::Encode), - bincode(crate = "crate::fun::bincode",) -)] -pub struct EncodedFrostKey { - /// The public point polynomial that defines the access structure to the FROST key. - point_polynomial: Vec>, -} - -impl EncodedFrostKey { - /// Traverse back to a FROST key to be used in signing - pub fn into_frost_key(&self) -> FrostKey { - self.clone().into() - } - - /// The threshold number of participants required in a signing coalition to produce a valid signature. - pub fn threshold(&self) -> usize { - self.point_polynomial.len() - } - - /// The public polynomial that defines the access structure to the FROST key. - pub fn point_polynomial(&self) -> Vec> { - self.point_polynomial.clone() - } -} - -#[cfg(feature = "bincode")] -impl crate::fun::bincode::Decode for EncodedFrostKey { - fn decode( - decoder: &mut D, - ) -> Result { - let poly = Vec::>::decode(decoder)?; - - if poly[0].is_zero() { - return Err(secp256kfun::bincode::error::DecodeError::Other( - "first coefficient of a frost polynomial can't be zero", - )); - } - - Ok(EncodedFrostKey { - point_polynomial: poly, - }) - } -} - -#[cfg(feature = "bincode")] -crate::fun::bincode::impl_borrow_decode!(EncodedFrostKey); - -#[cfg(feature = "serde")] -impl<'de> crate::fun::serde::Deserialize<'de> for EncodedFrostKey { - fn deserialize(deserializer: D) -> Result - where - D: secp256kfun::serde::Deserializer<'de>, - { - let poly = Vec::>::deserialize(deserializer)?; - - if poly[0].is_zero() { - return Err(crate::fun::serde::de::Error::custom( - "first coefficient of a frost polynomial can't be zero", - )); - } - - Ok(EncodedFrostKey { - point_polynomial: poly, - }) - } -} - -impl From for FrostKey { - fn from(from: EncodedFrostKey) -> Self { - FrostKey { - tweaked_public_key: from.point_polynomial[0] - .non_zero() - .expect("the frost public key can not be zero"), - point_polynomial: from.point_polynomial, - tweak: Scalar::zero(), - needs_negation: false, - } - } -} - -impl From> for EncodedFrostKey { - fn from(from: FrostKey) -> Self { - EncodedFrostKey { - point_polynomial: from.point_polynomial, - } - } -} - #[cfg(test)] mod test { @@ -1164,17 +968,17 @@ mod test { #[test] fn zero_agg_nonce_results_in_G() { let frost = new_with_deterministic_nonces::(); - let (frost_key, _shares) = frost.simulate_keygen(2, 3, &mut rand::thread_rng()); + let (frost_poly, _shares) = frost.simulate_keygen(2, 3, &mut rand::thread_rng()); let nonce = NonceKeyPair::random(&mut rand::thread_rng()).public(); let mut malicious_nonce = nonce; malicious_nonce.conditional_negate(true); - let session = frost.start_sign_session( - &frost_key.into_xonly_key(), + let session = frost.start_coordinator_sign_session( + &frost_poly.into_xonly(), BTreeMap::from_iter([(s!(1).public(), nonce), (s!(2).public(), malicious_nonce)]), Message::::plain("test", b"hello"), ); - assert_eq!(session.agg_nonce, *G); + assert_eq!(session.final_nonce(), *G); } } diff --git a/schnorr_fun/src/frost/share.rs b/schnorr_fun/src/frost/share.rs index 151acd16..c53a782b 100644 --- a/schnorr_fun/src/frost/share.rs +++ b/schnorr_fun/src/frost/share.rs @@ -1,4 +1,4 @@ -use secp256kfun::{marker::*, poly, Scalar}; +use secp256kfun::{poly, prelude::*}; /// A *[Shamir secret share]*. /// /// Each share is an `(x,y)` pair where `y = p(x)` for some polynomial `p`. With a sufficient @@ -55,7 +55,7 @@ pub struct SecretShare { /// value (other than 0). pub index: Scalar, /// The secret scalar which is the output of the polynomial evaluated at `index` - pub secret: Scalar, + pub share: Scalar, } impl SecretShare { @@ -63,18 +63,23 @@ impl SecretShare { pub fn recover_secret(shares: &[SecretShare]) -> Scalar { let index_and_secret = shares .iter() - .map(|share| (share.index, share.secret)) + .map(|share| (share.index, share.share)) .collect::>(); poly::scalar::interpolate_and_eval_poly_at_0(&index_and_secret[..]) } + /// The verification share for this secret share. + pub fn verification_share(&self) -> Point { + g!(self.share * G) + } + /// Encodes the secret share to 64 bytes. The first 32 is the index and the second 32 is the /// secret. pub fn to_bytes(&self) -> [u8; 64] { let mut bytes = [0u8; 64]; bytes[..32].copy_from_slice(self.index.to_bytes().as_ref()); - bytes[32..].copy_from_slice(self.secret.to_bytes().as_ref()); + bytes[32..].copy_from_slice(self.share.to_bytes().as_ref()); bytes } @@ -83,7 +88,7 @@ impl SecretShare { pub fn from_bytes(bytes: [u8; 64]) -> Option { Some(Self { index: Scalar::from_slice(&bytes[..32])?, - secret: Scalar::from_slice(&bytes[32..])?, + share: Scalar::from_slice(&bytes[32..])?, }) } } @@ -101,6 +106,122 @@ secp256kfun::impl_display_debug_serialize! { } } +#[derive(Copy, Clone, PartialEq, Eq)] +#[cfg_attr( + feature = "bincode", + derive(crate::fun::bincode::Encode, crate::fun::bincode::Decode), + bincode( + crate = "crate::fun::bincode", + encode_bounds = "Point: crate::fun::bincode::Encode", + decode_bounds = "Point: crate::fun::bincode::Decode", + borrow_decode_bounds = "Point: crate::fun::bincode::BorrowDecode<'__de>" + ) +)] +#[cfg_attr( + feature = "serde", + derive(crate::fun::serde::Deserialize, crate::fun::serde::Serialize), + serde( + crate = "crate::fun::serde", + bound( + deserialize = "Point: crate::fun::serde::de::Deserialize<'de>", + serialize = "Point: crate::fun::serde::Serialize" + ) + ) +)] +/// A secret share paired with the image of the secret for which it is a share of. +/// +/// This is useful so you can keep track of tweaks to the secret value and tweaks to the shared key +/// in tandem. +pub struct PairedSecretShare { + secret_share: SecretShare, + shared_key: Point, +} + +impl PairedSecretShare { + /// The index of the secret share + pub fn index(&self) -> PartyIndex { + self.secret_share.index + } + + /// The secret bit of the share + pub fn share(&self) -> Scalar { + self.secret_share.share + } + + /// The shared key this secret is for + pub fn shared_key(&self) -> Point { + self.shared_key + } + + /// The inner un-paired secret share. + /// + /// This exists since when you do a physical paper backup of a secret share you usually don't + /// record explicitly the entire shared key (maybe just a short identifier). + pub fn secret_share(&self) -> &SecretShare { + &self.secret_share + } +} + +impl AsRef for PairedSecretShare { + fn as_ref(&self) -> &SecretShare { + &self.secret_share + } +} + +impl PairedSecretShare { + /// Pair a secret share to a shared key + pub fn new(secret_share: SecretShare, shared_key: Point) -> Self { + Self { + secret_share, + shared_key, + } + } + + /// Add a `Scalar` to both the secret share and the paired shared key. + pub fn homomorphic_add( + mut self, + scalar: Scalar, + ) -> Option { + self.shared_key = g!(self.shared_key + scalar * G).normalize().non_zero()?; + self.secret_share.share = s!(self.secret_share.share + scalar); + Some(self) + } + + /// Create an XOnly secert share where the paired image is always an `EvenY` point. + pub fn into_xonly(mut self) -> PairedSecretShare { + let (shared_key, needs_negation) = self.shared_key.into_point_with_even_y(); + self.secret_share.share.conditional_negate(needs_negation); + + PairedSecretShare { + secret_share: self.secret_share, + shared_key, + } + } +} + +impl PairedSecretShare { + /// Add a scalar to the share such that share becomes a share of the previous secret plus + /// `scalar`. + /// + /// If the secret image were to become zero it returns `None` since this desinged to be + /// [`BIP340`] secret share which does not allow 0. + /// + /// [`BIP340`]: https://bips.xyz/bip340 + pub fn xonly_homomorphic_add( + mut self, + scalar: Scalar, + ) -> Option { + let (shared_key, needs_negation) = g!(self.shared_key + scalar * G) + .normalize() + .non_zero()? + .into_point_with_even_y(); + self.shared_key = shared_key; + self.secret_share.share = s!(self.secret_share.share + scalar); + self.secret_share.share.conditional_negate(needs_negation); + Some(self) + } +} + #[cfg(feature = "share_backup")] mod share_backup { use super::*; @@ -140,7 +261,7 @@ mod share_backup { }; let chars = self - .secret + .share .to_bytes() .into_iter() .chain(share_index_bytes.into_iter().flatten()) @@ -222,7 +343,7 @@ mod share_backup { .ok_or(BackupDecodeError::InvalidShareIndexScalar)?; Ok(SecretShare { - secret: secret_share, + share: secret_share, index: share_index, }) } @@ -275,8 +396,8 @@ mod share_backup { proptest! { #[test] - fn share_backup_roundtrip(index in any::>(), secret in any::>()) { - let orig = SecretShare { secret, index }; + fn share_backup_roundtrip(index in any::>(), share in any::>()) { + let orig = SecretShare { share, index }; let orig_encoded = orig.to_bech32_backup(); let decoded = SecretShare::from_bech32_backup(&orig_encoded).unwrap(); assert_eq!(orig, decoded) @@ -284,11 +405,11 @@ mod share_backup { #[test] - fn short_backup_length(secret in any::>(), share_index_u32 in 1u32..200) { + fn short_backup_length(share in any::>(), share_index_u32 in 1u32..200) { let index = Scalar::::from(share_index_u32).non_zero().unwrap().public(); let secret_share = SecretShare { index, - secret, + share, }; let backup = secret_share.to_bech32_backup(); @@ -307,6 +428,8 @@ mod share_backup { #[cfg(feature = "share_backup")] pub use share_backup::BackupDecodeError; +use super::PartyIndex; + #[cfg(test)] mod test { use super::*; @@ -329,10 +452,11 @@ mod test { let frost = frost::new_with_deterministic_nonces::(); let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); - let (frost_key, shares) = frost.simulate_keygen(threshold, parties, &mut rng); - let chosen = shares.choose_multiple(&mut rng, threshold).cloned().collect::>(); + let (frost_poly, shares) = frost.simulate_keygen(threshold, parties, &mut rng); + let chosen = shares.choose_multiple(&mut rng, threshold).cloned() + .map(|paired_share| paired_share.secret_share).collect::>(); let secret = SecretShare::recover_secret(&chosen); - prop_assert_eq!(g!(secret * G), frost_key.public_key()); + prop_assert_eq!(g!(secret * G), frost_poly.shared_key()); } } } diff --git a/schnorr_fun/tests/frost_prop.rs b/schnorr_fun/tests/frost_prop.rs index adfd9b56..5dd13f98 100644 --- a/schnorr_fun/tests/frost_prop.rs +++ b/schnorr_fun/tests/frost_prop.rs @@ -28,16 +28,27 @@ proptest! { // // create some scalar polynomial for each party let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); - let (mut frost_key, secret_shares) = proto.simulate_keygen(threshold, n_parties, &mut rng); + let (mut frost_poly, mut secret_shares) = proto.simulate_keygen(threshold, n_parties, &mut rng); if let Some(tweak) = plain_tweak { - frost_key = frost_key.tweak(tweak).unwrap(); + for secret_share in &mut secret_shares { + *secret_share = secret_share.homomorphic_add(tweak).unwrap(); + } + frost_poly = frost_poly.homomorphic_add(tweak).unwrap(); } - let mut frost_key = frost_key.into_xonly_key(); + let mut xonly_poly = frost_poly.into_xonly(); + let mut xonly_secret_shares = secret_shares.into_iter().map(|secret_share| secret_share.into_xonly()).collect::>(); if let Some(tweak) = xonly_tweak { - frost_key = frost_key.tweak(tweak).unwrap(); + xonly_poly = xonly_poly.xonly_homomorphic_add(tweak).unwrap(); + for secret_share in &mut xonly_secret_shares { + *secret_share = secret_share.xonly_homomorphic_add(tweak).unwrap(); + } + } + + for secret_share in &xonly_secret_shares { + assert_eq!(secret_share.shared_key(), xonly_poly.shared_key()); } // use a boolean mask for which t participants are signers @@ -46,55 +57,59 @@ proptest! { // shuffle the mask for random signers signer_mask.shuffle(&mut rng); - let secret_shares = signer_mask.into_iter().zip(secret_shares.into_iter()).filter(|(is_signer, _)| *is_signer) + let secret_shares_of_signers = signer_mask.into_iter().zip(xonly_secret_shares.into_iter()).filter(|(is_signer, _)| *is_signer) .map(|(_, secret_share)| secret_share).collect::>(); let sid = b"frost-prop-test".as_slice(); let message = Message::plain("test", b"test"); - let mut secret_nonces: BTreeMap<_, _> = secret_shares.iter().map(|secret_share| { - (secret_share.index, proto.gen_nonce::( + 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( - &frost_key, - &secret_share.secret, + *paired_secret_share, sid, ))) }).collect(); let public_nonces = secret_nonces.iter().map(|(signer_index, sn)| (*signer_index, sn.public())).collect::>(); - dbg!(&public_nonces); - let signing_session = proto.start_sign_session( - &frost_key, + let coord_signing_session = proto.start_coordinator_sign_session( + &xonly_poly, public_nonces, message ); + let party_signing_session = proto.start_party_sign_session( + xonly_poly.shared_key(), + coord_signing_session.participants(), + coord_signing_session.agg_binonce(), + message, + ); + let mut signatures = vec![]; - for secret_share in secret_shares { + for secret_share in secret_shares_of_signers { let sig = proto.sign( - &frost_key, - &signing_session, + &party_signing_session, &secret_share, - secret_nonces.remove(&secret_share.index).unwrap() + secret_nonces.remove(&secret_share.index()).unwrap() ); assert!(proto.verify_signature_share( - &frost_key, - &signing_session, - secret_share.index, + &xonly_poly, + &coord_signing_session, + secret_share.index(), sig) ); signatures.push(sig); } let combined_sig = proto.combine_signature_shares( - &frost_key, - &signing_session, - signatures); + &coord_signing_session, + signatures + ); assert!(proto.schnorr.verify( - &frost_key.public_key(), + &xonly_poly.shared_key(), message, &combined_sig )); diff --git a/secp256kfun/src/poly.rs b/secp256kfun/src/poly.rs index 7f1b95f7..addc8eaf 100644 --- a/secp256kfun/src/poly.rs +++ b/secp256kfun/src/poly.rs @@ -133,6 +133,13 @@ pub mod scalar { _ => None, }) } + + /// Negates a scalar polynomial + pub fn negate(poly: &mut [Scalar]) { + for coeff in poly { + *coeff = -*coeff; + } + } } /// Functions for dealing with point polynomials @@ -185,8 +192,12 @@ pub mod point { /// Panics if the indicies are not unique. /// /// A vector with a tail of zero coefficients means the interpolation was overdetermined. + #[allow(clippy::type_complexity)] pub fn interpolate( - index_and_point: &[(Scalar, Point)], + index_and_point: &[( + Scalar, + Point, + )], ) -> Vec> { let x_ms = index_and_point.iter().map(|(index, _)| *index); let mut interpolating_polynomial: Vec> = vec![]; @@ -201,6 +212,23 @@ pub mod point { interpolating_polynomial } + + /// Negates a scalar polynomial + pub fn negate(poly: &mut [Point]) + where + T: PointType, + { + for coeff in poly { + *coeff = -*coeff; + } + } + + /// Normalizes the points in a polynomial + pub fn normalize( + poly: impl IntoIterator>, + ) -> impl Iterator> { + poly.into_iter().map(|point| point.normalize()) + } } /// Returns an iterator of 1, x, x², x³ ... fn powers(x: Scalar) -> impl Iterator> { From bbaf252ec54eed66f11e5628551f312acbc818d0 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Thu, 25 Jul 2024 19:25:46 +1000 Subject: [PATCH 03/15] [musig] Make musig mod a bit more like frost mod --- schnorr_fun/src/musig.rs | 77 +++++++++++--------------- schnorr_fun/tests/musig_sign_verify.rs | 4 +- schnorr_fun/tests/musig_tweak.rs | 4 +- 3 files changed, 35 insertions(+), 50 deletions(-) diff --git a/schnorr_fun/src/musig.rs b/schnorr_fun/src/musig.rs index 6706c41f..c52206f3 100644 --- a/schnorr_fun/src/musig.rs +++ b/schnorr_fun/src/musig.rs @@ -71,8 +71,7 @@ //! //! [the excellent paper]: https://eprint.iacr.org/2020/1261.pdf //! [secp256k1-zkp]: https://github.com/ElementsProject/secp256k1-zkp/pull/131 -pub use crate::binonce::{Nonce, NonceKeyPair}; -use crate::{adaptor::EncryptedSignature, Message, Schnorr, Signature}; +use crate::{adaptor::EncryptedSignature, binonce, Message, Schnorr, Signature}; use alloc::vec::Vec; use secp256kfun::{ digest::{generic_array::typenum::U32, Digest}, @@ -116,8 +115,8 @@ impl MuSig { /// Generate nonces for creating signatures shares. /// /// ⚠ You must use a CAREFULLY CHOSEN nonce rng, see [`MuSig::seed_nonce_rng`] - pub fn gen_nonce(&self, nonce_rng: &mut R) -> NonceKeyPair { - NonceKeyPair::random(nonce_rng) + pub fn gen_nonce(&self, nonce_rng: &mut R) -> binonce::NonceKeyPair { + binonce::NonceKeyPair::random(nonce_rng) } } @@ -430,9 +429,9 @@ pub struct Adaptor { bincode(crate = "crate::fun::bincode") )] pub struct SignSession { - b: Scalar, + b: Scalar, c: Scalar, - public_nonces: Vec, + public_nonces: Vec, R: Point, nonce_needs_negation: bool, signing_type: T, @@ -455,15 +454,11 @@ impl + Clone, NG> MuSig { pub fn start_sign_session( &self, agg_key: &AggKey, - nonces: Vec, + nonces: Vec, message: Message<'_, Public>, ) -> SignSession { - let (b, c, public_nonces, R, nonce_needs_negation) = self._start_sign_session( - agg_key, - nonces, - message, - &Point::::zero(), - ); + let (b, c, public_nonces, R, nonce_needs_negation) = + self._start_sign_session(agg_key, nonces, message, Point::::zero()); SignSession { b, c, @@ -497,9 +492,9 @@ impl + Clone, NG> MuSig { pub fn start_encrypted_sign_session( &self, agg_key: &AggKey, - nonces: Vec, + nonces: Vec, message: Message<'_, Public>, - encryption_key: &Point, + encryption_key: Point, ) -> Option> { let (b, c, public_nonces, R, nonce_needs_negation) = self._start_sign_session(agg_key, nonces, message, encryption_key); @@ -519,51 +514,41 @@ impl + Clone, NG> MuSig { fn _start_sign_session( &self, agg_key: &AggKey, - nonces: Vec, + mut nonces: Vec, message: Message<'_, Public>, - encryption_key: &Point, + encryption_key: Point, ) -> ( + Scalar, Scalar, - Scalar, - Vec, + Vec, Point, bool, ) { - let mut Rs = nonces; - let agg_Rs = Rs.iter().fold([Point::zero(); 2], |acc, nonce| { - [g!(acc[0] + nonce.0[0]), g!(acc[1] + nonce.0[1])] - }); - let agg_Rs = Nonce::([ - g!(agg_Rs[0] + encryption_key).normalize(), - agg_Rs[1].normalize(), - ]); - - let b = { + let mut agg_binonce = binonce::Nonce::aggregate(nonces.iter().cloned()); + agg_binonce.0[0] = g!(agg_binonce.0[0] + encryption_key).normalize(); + + let binding_coeff = { let H = self.nonce_coeff_hash.clone(); Scalar::from_hash( - H.add(agg_Rs.to_bytes()) + H.add(agg_binonce) .add(agg_key.agg_public_key()) .add(message), ) } - .public() - .mark_zero(); + .public(); - let (R, r_needs_negation) = g!(agg_Rs.0[0] + b * agg_Rs.0[1]) - .normalize() - .non_zero() - .unwrap_or(Point::generator()) - .into_point_with_even_y(); - - for R_i in &mut Rs { - R_i.conditional_negate(r_needs_negation); - } + let (R, nonces_need_negation) = agg_binonce.bind(binding_coeff); let c = self .schnorr .challenge(&R, &agg_key.agg_public_key(), message); - (b, c, Rs, R, r_needs_negation) + // we may as well eagerly do + for nonce in &mut nonces { + nonce.conditional_negate(nonces_need_negation); + } + + (binding_coeff, c, nonces, R, nonces_need_negation) } /// Generates a partial signature (or partial encrypted signature depending on `T`) for the local_secret_nonce. @@ -573,7 +558,7 @@ impl + Clone, NG> MuSig { session: &SignSession, my_index: usize, keypair: &KeyPair, - local_secret_nonce: NonceKeyPair, + local_secret_nonce: binonce::NonceKeyPair, ) -> Scalar { assert_eq!( keypair.public_key(), @@ -898,7 +883,7 @@ mod test { &agg_key1, nonces.clone(), message, - &encryption_key + encryption_key ) .unwrap(); let p2_session = musig @@ -906,7 +891,7 @@ mod test { &agg_key2, nonces.clone(), message, - &encryption_key + encryption_key ) .unwrap(); let p3_session = musig @@ -914,7 +899,7 @@ mod test { &agg_key3, nonces, message, - &encryption_key + encryption_key ) .unwrap(); let p1_sig = musig.sign(&agg_key1, &p1_session, 0, &keypair1, p1_nonce); diff --git a/schnorr_fun/tests/musig_sign_verify.rs b/schnorr_fun/tests/musig_sign_verify.rs index 17827c62..c0c4abd1 100644 --- a/schnorr_fun/tests/musig_sign_verify.rs +++ b/schnorr_fun/tests/musig_sign_verify.rs @@ -1,9 +1,9 @@ #![cfg(feature = "serde")] use schnorr_fun::{ binonce, + binonce::NonceKeyPair, fun::{marker::*, serde, Point, Scalar}, - musig::{self, NonceKeyPair}, - Message, + musig, Message, }; static TEST_JSON: &str = include_str!("musig/sign_verify_vectors.json"); use secp256kfun::hex; diff --git a/schnorr_fun/tests/musig_tweak.rs b/schnorr_fun/tests/musig_tweak.rs index 4306a37d..b20e458b 100644 --- a/schnorr_fun/tests/musig_tweak.rs +++ b/schnorr_fun/tests/musig_tweak.rs @@ -4,9 +4,9 @@ use std::{rc::Rc, sync::Arc}; use schnorr_fun::{ binonce, + binonce::NonceKeyPair, fun::{marker::*, serde, Point, Scalar}, - musig::{self, NonceKeyPair}, - Message, + musig, Message, }; static TEST_JSON: &'static str = include_str!("musig/tweak_vectors.json"); use secp256kfun::hex; From 0e4ba72779154955b7333e235e07bc76a5d8f59b Mon Sep 17 00:00:00 2001 From: LLFourn Date: Fri, 26 Jul 2024 11:19:41 +1000 Subject: [PATCH 04/15] [frost] Add multiplicative tweaking by demand See: https://github.com/LLFourn/secp256kfun/issues/189 --- schnorr_fun/src/frost/frost_poly.rs | 31 ++++++++++++++++++++- schnorr_fun/src/frost/share.rs | 43 +++++++++++++++++++++-------- schnorr_fun/tests/frost_prop.rs | 26 +++++++++++++---- 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/schnorr_fun/src/frost/frost_poly.rs b/schnorr_fun/src/frost/frost_poly.rs index 05225d80..b06280ea 100644 --- a/schnorr_fun/src/frost/frost_poly.rs +++ b/schnorr_fun/src/frost/frost_poly.rs @@ -1,4 +1,4 @@ -use core::marker::PhantomData; +use core::{marker::PhantomData, ops::Deref}; use alloc::vec::Vec; use secp256kfun::{poly, prelude::*}; @@ -125,6 +125,16 @@ impl FrostPoly { pub fn homomorphic_negate(&mut self) { poly::point::negate(&mut self.point_polynomial) } + + /// Multiplies the shared key by a scalar. + /// + /// In otder for a [`PairedSecretShare`] to be valid against the new key they will have to apply + /// [the same operation](PairedSecretShare::xonly_homomorphic_mul). + pub fn homomorphic_mul(&mut self, tweak: Scalar) { + for coeff in &mut self.point_polynomial { + *coeff = g!(tweak * coeff.deref()).normalize(); + } + } } impl FrostPoly { @@ -154,6 +164,25 @@ impl FrostPoly { Some(self) } + /// Multiplies the shared key by a scalar. + /// + /// In otder for a `PairedSecretShare` to be valid against the new key they will have to apply + /// [the same operation](PairedSecretShare::xonly_homomorphic_mul). + pub fn xonly_homomorphic_mul(&mut self, mut tweak: Scalar) { + self.point_polynomial[0] = g!(tweak * self.point_polynomial[0]).normalize(); + let needs_negation = !self.point_polynomial[0] + .non_zero() + .expect("multiplication cannot be zero") + .is_y_even(); + + self.point_polynomial[0] = self.point_polynomial[0].conditional_negate(needs_negation); + tweak.conditional_negate(needs_negation); + + for coeff in &mut self.point_polynomial[1..] { + *coeff = g!(tweak * coeff.deref()).normalize(); + } + } + /// The public key that would have signatures verified against for this shared key. pub fn shared_key(&self) -> Point { let (even_y_point, _needs_negation) = self.point_polynomial[0] diff --git a/schnorr_fun/src/frost/share.rs b/schnorr_fun/src/frost/share.rs index c53a782b..d2824697 100644 --- a/schnorr_fun/src/frost/share.rs +++ b/schnorr_fun/src/frost/share.rs @@ -178,15 +178,24 @@ impl PairedSecretShare { } /// Add a `Scalar` to both the secret share and the paired shared key. - pub fn homomorphic_add( - mut self, - scalar: Scalar, - ) -> Option { - self.shared_key = g!(self.shared_key + scalar * G).normalize().non_zero()?; - self.secret_share.share = s!(self.secret_share.share + scalar); + /// If the share is a share of `secret` it becomes the share of the `secret + tweak`. + /// + /// This is useful for for deriving unhardened child frost keys from a root frost public key + /// using [BIP32] + /// + /// [BIP32]: https://bips.xyz/32 + pub fn homomorphic_add(mut self, tweak: Scalar) -> Option { + self.shared_key = g!(self.shared_key + tweak * G).normalize().non_zero()?; + self.secret_share.share = s!(self.secret_share.share + tweak); Some(self) } + /// Multiply the secret share by `scalar`. + pub fn homomorphic_mul(&mut self, tweak: Scalar) { + self.shared_key = g!(tweak * self.shared_key).normalize(); + self.secret_share.share = s!(tweak * self.secret_share.share); + } + /// Create an XOnly secert share where the paired image is always an `EvenY` point. pub fn into_xonly(mut self) -> PairedSecretShare { let (shared_key, needs_negation) = self.shared_key.into_point_with_even_y(); @@ -200,8 +209,9 @@ impl PairedSecretShare { } impl PairedSecretShare { - /// Add a scalar to the share such that share becomes a share of the previous secret plus - /// `scalar`. + /// Adds an "XOnly" tweak to the FROST public key. If the share is a share of `secret` it + /// becomes the share of the `secret + tweak` however the even-y coordinate of the shared key is + /// maintained by negating if need be. /// /// If the secret image were to become zero it returns `None` since this desinged to be /// [`BIP340`] secret share which does not allow 0. @@ -209,17 +219,28 @@ impl PairedSecretShare { /// [`BIP340`]: https://bips.xyz/bip340 pub fn xonly_homomorphic_add( mut self, - scalar: Scalar, + tweak: Scalar, ) -> Option { - let (shared_key, needs_negation) = g!(self.shared_key + scalar * G) + let (shared_key, needs_negation) = g!(self.shared_key + tweak * G) .normalize() .non_zero()? .into_point_with_even_y(); self.shared_key = shared_key; - self.secret_share.share = s!(self.secret_share.share + scalar); + self.secret_share.share = s!(self.secret_share.share + tweak); self.secret_share.share.conditional_negate(needs_negation); Some(self) } + + /// Multiply the secret share and paired key by `scalar` maintaing the paired key's even y + /// coordinate by negating them both (if need be). + pub fn xonly_homomorphic_mul(&mut self, mut tweak: Scalar) { + let (shared_key, needs_negation) = g!(tweak * self.shared_key) + .normalize() + .into_point_with_even_y(); + self.shared_key = shared_key; + tweak.conditional_negate(needs_negation); + self.secret_share.share *= tweak; + } } #[cfg(feature = "share_backup")] diff --git a/schnorr_fun/tests/frost_prop.rs b/schnorr_fun/tests/frost_prop.rs index 5dd13f98..db0c92d9 100644 --- a/schnorr_fun/tests/frost_prop.rs +++ b/schnorr_fun/tests/frost_prop.rs @@ -20,8 +20,10 @@ proptest! { #[test] fn frost_prop_test( (n_parties, threshold) in (2usize..=4).prop_flat_map(|n| (Just(n), 2usize..=n)), - plain_tweak in option::of(any::>()), - xonly_tweak in option::of(any::>()) + 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::(); assert!(threshold <= n_parties); @@ -30,25 +32,39 @@ proptest! { let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); let (mut frost_poly, mut secret_shares) = proto.simulate_keygen(threshold, n_parties, &mut rng); - if let Some(tweak) = plain_tweak { + if let Some(tweak) = add_tweak { for secret_share in &mut secret_shares { *secret_share = secret_share.homomorphic_add(tweak).unwrap(); } frost_poly = frost_poly.homomorphic_add(tweak).unwrap(); } + if let Some(mul_tweak) = mul_tweak { + frost_poly.homomorphic_mul(mul_tweak); + for secret_share in &mut secret_shares { + secret_share.homomorphic_mul(mul_tweak); + } + } + let mut xonly_poly = frost_poly.into_xonly(); let mut xonly_secret_shares = secret_shares.into_iter().map(|secret_share| secret_share.into_xonly()).collect::>(); - if let Some(tweak) = xonly_tweak { + if let Some(tweak) = xonly_add_tweak { xonly_poly = xonly_poly.xonly_homomorphic_add(tweak).unwrap(); for secret_share in &mut xonly_secret_shares { *secret_share = secret_share.xonly_homomorphic_add(tweak).unwrap(); } } + if let Some(xonly_mul_tweak) = xonly_mul_tweak { + xonly_poly.xonly_homomorphic_mul(xonly_mul_tweak); + for secret_share in &mut xonly_secret_shares { + secret_share.xonly_homomorphic_mul(xonly_mul_tweak); + } + } + for secret_share in &xonly_secret_shares { - assert_eq!(secret_share.shared_key(), xonly_poly.shared_key()); + assert_eq!(secret_share.shared_key(), xonly_poly.shared_key(), "shared key doesn't match"); } // use a boolean mask for which t participants are signers From b3318643250376b1acedfe99139858fd3025335c Mon Sep 17 00:00:00 2001 From: LLFourn Date: Fri, 26 Jul 2024 12:01:42 +1000 Subject: [PATCH 05/15] [poly] Reduce interpolated polynomial So it doesn't start with zero points. --- secp256kfun/src/poly.rs | 6 ++++++ secp256kfun/tests/poly.rs | 32 +++++++++++++++----------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/secp256kfun/src/poly.rs b/secp256kfun/src/poly.rs index addc8eaf..fe009aa7 100644 --- a/secp256kfun/src/poly.rs +++ b/secp256kfun/src/poly.rs @@ -210,6 +210,12 @@ pub mod point { self::add_in_place(&mut interpolating_polynomial, point_scaled_basis_poly); } + while interpolating_polynomial.len() > 1 + && interpolating_polynomial.last().unwrap().is_zero() + { + interpolating_polynomial.pop(); + } + interpolating_polynomial } diff --git a/secp256kfun/tests/poly.rs b/secp256kfun/tests/poly.rs index 47f5ff93..1284fb1c 100644 --- a/secp256kfun/tests/poly.rs +++ b/secp256kfun/tests/poly.rs @@ -1,5 +1,5 @@ #![cfg(feature = "alloc")] -use secp256kfun::{g, marker::*, poly, s, Point, G}; +use secp256kfun::{poly, prelude::*}; #[test] fn test_lagrange_lambda() { @@ -90,28 +90,26 @@ fn test_recover_overdetermined_poly() { let points = indicies .clone() .into_iter() - .map(|index| { - ( - index, - poly::point::eval(&poly, index.public()) - .normalize() - .non_zero() - .unwrap(), - ) - }) + .map(|index| (index, poly::point::eval(&poly, index.public()).normalize())) .collect::>(); let interpolation = poly::point::interpolate(&points); - let (interpolated_coeffs, zero_coeffs) = interpolation.split_at(poly.len()); - let n_extra_points = indicies.len() - poly.len(); + assert_eq!(interpolation, poly); +} + +#[test] +fn test_recover_zero_poly() { + let interpolation = poly::point::interpolate(&[ + (s!(1).public(), Point::::zero()), + (s!(2).public(), Point::::zero()), + ]); + assert_eq!( - (0..n_extra_points) - .map(|_| Point::::zero().public().normalize()) - .collect::>(), - zero_coeffs.to_vec() + interpolation, + vec![Point::::zero()], + "should not be empty vector" ); - assert_eq!(interpolated_coeffs, poly); } #[test] From 8a80c809fb822f385297255a9d0bb10dc5038a32 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Fri, 26 Jul 2024 17:27:06 +1000 Subject: [PATCH 06/15] [frost] Renaming and docs --- ecdsa_fun/src/adaptor/mod.rs | 4 +- schnorr_fun/src/adaptor/mod.rs | 4 +- schnorr_fun/src/frost/mod.rs | 144 ++++++------------ schnorr_fun/src/frost/session.rs | 71 +++++++++ schnorr_fun/src/frost/share.rs | 2 +- .../frost/{frost_poly.rs => shared_key.rs} | 55 ++++--- schnorr_fun/src/musig.rs | 4 +- schnorr_fun/tests/frost_prop.rs | 12 +- secp256kfun/src/macros.rs | 4 +- 9 files changed, 164 insertions(+), 136 deletions(-) create mode 100644 schnorr_fun/src/frost/session.rs rename schnorr_fun/src/frost/{frost_poly.rs => shared_key.rs} (81%) diff --git a/ecdsa_fun/src/adaptor/mod.rs b/ecdsa_fun/src/adaptor/mod.rs index c5ecf11c..06727eb2 100644 --- a/ecdsa_fun/src/adaptor/mod.rs +++ b/ecdsa_fun/src/adaptor/mod.rs @@ -234,9 +234,9 @@ impl, NG> Adaptor { /// There are two crucial things to understand when calling this: /// /// 1. You should be certain that the encrypted signature is what you think it is by calling - /// [`verify_encrypted_signature`] on it first. + /// [`verify_encrypted_signature`] on it first. /// 2. Once you give the decrypted signature to anyone who has seen `encrypted_signature` they will be - /// able to learn `decryption_key` by calling [`recover_decryption_key`]. + /// able to learn `decryption_key` by calling [`recover_decryption_key`]. /// /// See [synopsis] for an example /// diff --git a/schnorr_fun/src/adaptor/mod.rs b/schnorr_fun/src/adaptor/mod.rs index 660bc5c2..a188e478 100644 --- a/schnorr_fun/src/adaptor/mod.rs +++ b/schnorr_fun/src/adaptor/mod.rs @@ -153,9 +153,9 @@ pub trait Adaptor { /// There are two crucial things to understand when calling this: /// /// 1. You should be certain that the encrypted signature is what you think it is by calling - /// [`verify_encrypted_signature`] on it first. + /// [`verify_encrypted_signature`] on it first. /// 2. Once you give the decrypted signature to anyone who has seen `encrypted_signature` they will be - /// able to learn `decryption_key` by calling [`recover_decryption_key`]. + /// able to learn `decryption_key` by calling [`recover_decryption_key`]. /// /// See [synopsis] for an example /// diff --git a/schnorr_fun/src/frost/mod.rs b/schnorr_fun/src/frost/mod.rs index a69a8d0c..2dda37cd 100644 --- a/schnorr_fun/src/frost/mod.rs +++ b/schnorr_fun/src/frost/mod.rs @@ -74,7 +74,7 @@ //! # Message::raw(&frost.keygen_id(&keygen)), //! # ) //! # .unwrap(); -//! let (my_secret_share, frost_key) = frost +//! let (my_secret_share, shared_key) = frost //! .finish_keygen( //! keygen, //! my_index, @@ -85,28 +85,38 @@ //! // ⚠️ 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! -//! let xonly_frost_key = frost_key.into_xonly_key(); +//! // With signing we'll have at least one party be the "coordinator" (steps marked with 🐙) +//! // In this example we'll be the coordiantor (but it doesn't have to be on eof 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. +//! // 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). //! let session_id = b"signing-ominous-message-about-banks-attempt-1".as_slice(); -//! let mut nonce_rng: ChaCha20Rng = frost.seed_nonce_rng(&xonly_frost_key, &my_secret_share.secret, session_id); +//! let mut nonce_rng: ChaCha20Rng = frost.seed_nonce_rng(my_secret_share, session_id); //! let my_nonce = frost.gen_nonce(&mut nonce_rng); //! # let nonce3 = NonceKeyPair::random(&mut rand::thread_rng()); //! // share your public nonce with the other signing participant(s) receive public nonces //! # let received_nonce3 = nonce3.public(); +//! // 🐙 the coordinator has received these //! let nonces = BTreeMap::from_iter([(my_index, my_nonce.public()), (party_index3, received_nonce3)]); +//! let coord_session = frost.coordinator_sign_session(&xonly_shared_key, nonces, message); +//! // Parties receive the agg_nonce from the coordiantor and the list of perties +//! let agg_binonce = coord_session.agg_binonce(); +//! let parties = coord_session.parties(); //! // start a sign session with these nonces for a message -//! let session = frost.start_sign_session(&xonly_frost_key, nonces, message); +//! let sign_session = frost.party_sign_session(xonly_my_secret_share.shared_key(),parties, agg_binonce, message); //! // create a partial signature using our secret share and secret nonce -//! let my_sig_share = frost.sign(&xonly_frost_key, &session, &my_secret_share, my_nonce); -//! # let sig_share3 = frost.sign(&xonly_frost_key, &session, &secret_share3, nonce3); -//! // receive the partial signature(s) from the other participant(s) and verify -//! assert!(frost.verify_signature_share(&xonly_frost_key, &session, party_index3, sig_share3)); -//! // combine signature shares into a single signature that is valid under the FROST key -//! let combined_sig = frost.combine_signature_shares(&xonly_frost_key, &session, vec![my_sig_share, sig_share3]); +//! let my_sig_share = frost.sign(&sign_session, &xonly_my_secret_share, my_nonce); +//! # let sig_share3 = frost.sign(&sign_session, &xonly_secret_share3, nonce3); +//! // 🐙 receive the partial signature(s) from the other participant(s) and verify. +//! assert!(frost.verify_signature_share(&xonly_shared_key, &coord_session, party_index3, sig_share3)); +//! // 🐙 combine signature shares into a single signature that is valid under the FROST key +//! // It won't be necessarily be valid unless you verified each share. +//! let combined_sig = frost.combine_signature_shares(&coord_session, vec![my_sig_share, sig_share3]); //! assert!(frost.schnorr.verify( -//! &xonly_frost_key.public_key(), +//! &xonly_shared_key.key(), //! message, //! &combined_sig //! )); @@ -114,7 +124,7 @@ //! //! # Description //! -//! In FROST, multiple parties cooperatively generate a single joint public key ([`FrostKey`]) for +//! In FROST, multiple parties cooperatively generate a single joint public key ([`SharedKey`]) for //! creating Schnorr signatures. Unlike in [`musig`], only some threshold `t` of the `n` signers are //! required to generate a signature under the key (rather than all `n`). //! @@ -126,7 +136,7 @@ //! Signatures]*. //! //! > ⚠️ At this stage this implementation is for API exploration purposes only. The way it is -//! currently implemented is not proven secure. +//! > currently implemented is not proven secure. //! //! ## Polynomial Generation //! @@ -175,10 +185,12 @@ //! [`musig`]: crate::musig //! [`Scalar`]: crate::fun::Scalar -mod frost_poly; -pub use frost_poly::*; +mod shared_key; +pub use shared_key::*; mod share; pub use share::*; +mod session; +pub use session::*; pub use crate::binonce::{Nonce, NonceKeyPair}; use crate::{binonce, Message, Schnorr, Signature}; @@ -301,7 +313,7 @@ where /// [`Frost::new_keygen`] #[derive(Clone, Debug)] pub struct KeyGen { - frost_poly: FrostPoly, + frost_poly: SharedKey, point_polys: BTreeMap>, } @@ -450,14 +462,14 @@ impl + Clone, NG: NonceGen> Frost { /// 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 `FrostKey` along with the secret keys for each + /// 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, - ) -> (FrostPoly, Vec>) { + ) -> (SharedKey, Vec>) { let scalar_polys = (0..n_parties) .map(|i| { ( @@ -538,7 +550,7 @@ impl + Clone, NG> Frost { keygen_hash.finalize().into() } - /// Collect all the public polynomials commitments into a [`KeyGen`] to produce a [`FrostKey`]. + /// 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. @@ -598,7 +610,7 @@ impl + Clone, NG> Frost { } } - let frost_poly = FrostPoly::from_poly( + let frost_poly = SharedKey::from_poly( joint_poly .into_iter() .map(|coef| coef.normalize()) @@ -618,7 +630,7 @@ impl + Clone, NG> Frost { keygen: KeyGen, proofs_of_possession: BTreeMap, proof_of_possession_msg: Message, - ) -> Result, FinishKeyGenError> { + ) -> Result, FinishKeyGenError> { for (party_index, poly) in &keygen.point_polys { let pop = proofs_of_possession .get(party_index) @@ -647,14 +659,14 @@ impl + Clone, NG> Frost { /// /// # Return value /// - /// Your secret share and the [`FrostKey`] + /// 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, FrostPoly), FinishKeyGenError> { + ) -> Result<(PairedSecretShare, SharedKey), FinishKeyGenError> { let mut total_secret_share = s!(0); for (party_index, poly) in &keygen.point_polys { @@ -682,14 +694,13 @@ impl + Clone, NG> Frost { share: total_secret_share, }; - let secret_share_with_image = - PairedSecretShare::new(secret_share, keygen.frost_poly.shared_key()); + let secret_share_with_image = PairedSecretShare::new(secret_share, keygen.frost_poly.key()); Ok((secret_share_with_image, keygen.frost_poly)) } /// Start party signing session - pub fn start_party_sign_session( + pub fn party_sign_session( &self, shared_key: Point, parties: BTreeSet, @@ -718,9 +729,9 @@ impl + Clone, NG> Frost { /// # Panics /// /// If the number of nonces is less than the threshold. - pub fn start_coordinator_sign_session( + pub fn coordinator_sign_session( &self, - frost_poly: &FrostPoly, + frost_poly: &SharedKey, mut nonces: BTreeMap, message: Message, ) -> CoordinatorSignSession { @@ -730,12 +741,12 @@ impl + Clone, NG> Frost { let agg_binonce = binonce::Nonce::aggregate(nonces.values().cloned()); - let binding_coeff = self.binding_coefficient(frost_poly.shared_key(), agg_binonce, message); + let binding_coeff = self.binding_coefficient(frost_poly.key(), agg_binonce, message); let (final_nonce, binonce_needs_negation) = agg_binonce.bind(binding_coeff); let challenge = self .schnorr - .challenge(&final_nonce, &frost_poly.shared_key(), message); + .challenge(&final_nonce, &frost_poly.key(), message); for nonce in nonces.values_mut() { nonce.conditional_negate(binonce_needs_negation); @@ -806,7 +817,7 @@ impl + Clone, NG> Frost { /// Returns `bool`, true if partial signature is valid. pub fn verify_signature_share( &self, - frost_poly: &FrostPoly, + shared_key: &SharedKey, session: &CoordinatorSignSession, index: PartyIndex, signature_share: Scalar, @@ -815,7 +826,7 @@ impl + Clone, NG> Frost { let lambda = poly::eval_basis_poly_at_0(index, session.nonces.keys().cloned()); let c = &session.challenge; let b = &session.binding_coeff; - let X = frost_poly.verification_share(index); + let X = shared_key.verification_share(index); let [R1, R2] = session .nonces .get(&index) @@ -849,71 +860,6 @@ impl + Clone, NG> Frost { } } -/// A FROST signing session -/// -/// Created using [`Frost::start_sign_session`]. -/// -/// [`Frost::start_sign_session`] -#[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 CoordinatorSignSession { - binding_coeff: Scalar, - agg_binonce: binonce::Nonce, - final_nonce: Point, - challenge: Scalar, - nonces: BTreeMap, -} - -impl CoordinatorSignSession { - /// Fetch the participant indices for this signing session. - /// - /// ## Return value - /// - /// An iterator of participant indices - pub fn participants(&self) -> BTreeSet { - self.nonces.keys().cloned().collect() - } - - /// The aggregated nonce used to sign - pub fn agg_binonce(&self) -> binonce::Nonce { - self.agg_binonce - } - - /// The final nonce that will actually go on the blockchain - pub fn final_nonce(&self) -> Point { - self.final_nonce - } -} - -/// The session that is used to sign a message. -#[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 PartySignSession { - shared_key: Point, - parties: BTreeSet>, - challenge: Scalar, - binonce_needs_negation: bool, - binding_coeff: Scalar, -} - /// Constructor for a Frost instance using deterministic nonce generation. /// /// If you use deterministic nonce generation you will have to provide a unique session id to every signing session. @@ -973,7 +919,7 @@ mod test { let mut malicious_nonce = nonce; malicious_nonce.conditional_negate(true); - let session = frost.start_coordinator_sign_session( + let session = frost.coordinator_sign_session( &frost_poly.into_xonly(), BTreeMap::from_iter([(s!(1).public(), nonce), (s!(2).public(), malicious_nonce)]), Message::::plain("test", b"hello"), diff --git a/schnorr_fun/src/frost/session.rs b/schnorr_fun/src/frost/session.rs new file mode 100644 index 00000000..d73541ba --- /dev/null +++ b/schnorr_fun/src/frost/session.rs @@ -0,0 +1,71 @@ +use crate::{binonce, frost::PartyIndex}; +use alloc::collections::{BTreeMap, BTreeSet}; +use secp256kfun::prelude::*; +/// A FROST signing session +/// +/// Created using [`coordinator_sign_session`]. +/// +/// [`coordinator_sign_session`]: super::Frost::coordinator_sign_session +#[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 CoordinatorSignSession { + pub(crate) binding_coeff: Scalar, + pub(crate) agg_binonce: binonce::Nonce, + pub(crate) final_nonce: Point, + pub(crate) challenge: Scalar, + pub(crate) nonces: BTreeMap, +} + +impl CoordinatorSignSession { + /// Fetch the participant indices for this signing session. + /// + /// ## Return value + /// + /// An iterator of participant indices + pub fn parties(&self) -> BTreeSet { + self.nonces.keys().cloned().collect() + } + + /// The aggregated nonce used to sign + pub fn agg_binonce(&self) -> binonce::Nonce { + self.agg_binonce + } + + /// The final nonce that will actually go on the blockchain + pub fn final_nonce(&self) -> Point { + self.final_nonce + } +} + +/// The session that is used to sign a message. +/// +/// Created using [`party_sign_session`] +/// +/// [`party_sign_session`]: super::Frost::party_sign_session +#[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 PartySignSession { + pub(crate) shared_key: Point, + pub(crate) parties: BTreeSet>, + pub(crate) challenge: Scalar, + pub(crate) binonce_needs_negation: bool, + pub(crate) binding_coeff: Scalar, +} diff --git a/schnorr_fun/src/frost/share.rs b/schnorr_fun/src/frost/share.rs index d2824697..f29e68b8 100644 --- a/schnorr_fun/src/frost/share.rs +++ b/schnorr_fun/src/frost/share.rs @@ -477,7 +477,7 @@ mod test { let chosen = shares.choose_multiple(&mut rng, threshold).cloned() .map(|paired_share| paired_share.secret_share).collect::>(); let secret = SecretShare::recover_secret(&chosen); - prop_assert_eq!(g!(secret * G), frost_poly.shared_key()); + prop_assert_eq!(g!(secret * G), frost_poly.key()); } } } diff --git a/schnorr_fun/src/frost/frost_poly.rs b/schnorr_fun/src/frost/shared_key.rs similarity index 81% rename from schnorr_fun/src/frost/frost_poly.rs rename to schnorr_fun/src/frost/shared_key.rs index b06280ea..d380c26d 100644 --- a/schnorr_fun/src/frost/frost_poly.rs +++ b/schnorr_fun/src/frost/shared_key.rs @@ -16,14 +16,14 @@ use super::PartyIndex; derive(crate::fun::bincode::Encode), bincode(crate = "crate::fun::bincode",) )] -pub struct FrostPoly { +pub struct SharedKey { /// The public point polynomial that defines the access structure to the FROST key. point_polynomial: Vec>, #[cfg_attr(feature = "serde", serde(skip))] ty: PhantomData, } -impl FrostPoly { +impl SharedKey { /// The verification shares of each party in the key. /// /// The verification share is the image of their secret share. @@ -45,14 +45,15 @@ impl FrostPoly { } } -impl FrostPoly { +impl SharedKey { /// The key that was shared with this polynomial defining the sharing. /// /// This is the first coefficient of the polynomial. - pub fn shared_key(&self) -> Point { + pub fn key(&self) -> Point { self.point_polynomial[0].non_zero().expect("invariant") } - /// Constructor to create a `FrostPoly` from a vector of points. + /// Constructor to create a from a vector of points where each item represent a polynomial + /// coefficient. /// /// Returns `None` if the first coefficient is [`Point::zero`]. pub fn from_poly(poly: Vec>) -> Option { @@ -70,7 +71,15 @@ impl FrostPoly { }) } - /// Create a `FrostPoly` from a set of verification shares. + /// Create a shared key from a subset of verification shares. + /// + /// If all the verification shares are correct and you have at least a threshold of them then + /// you'll get the right answer. If you put in a wrong share you won't get the right answer! + /// + /// ## Security + /// + /// ⚠ You can't just take any random points you want and pass them in here and hope it's secure. + /// They need to be from a securely generated key. pub fn from_verification_shares( shares: &[(PartyIndex, Point)], ) -> Self { @@ -80,18 +89,18 @@ impl FrostPoly { ty: PhantomData, } } - /// Convert the key into a BIP340 FrostKey. + /// Convert the key into a BIP340 "x-only" SharedKey. /// /// This is the [BIP340] compatible version of the key which you can put in a segwitv1 output. /// /// [BIP340]: https://bips.xyz/340 - pub fn into_xonly(mut self) -> FrostPoly { - let needs_negation = !self.shared_key().is_y_even(); + pub fn into_xonly(mut self) -> SharedKey { + let needs_negation = !self.key().is_y_even(); if needs_negation { self.homomorphic_negate(); - debug_assert!(self.shared_key().is_y_even()); + debug_assert!(self.key().is_y_even()); } - FrostPoly { + SharedKey { point_polynomial: self.point_polynomial, ty: PhantomData, } @@ -106,7 +115,7 @@ impl FrostPoly { /// /// ## Return value /// - /// Returns a new [`FrostKey`] with the same parties but a different frost public key. + /// Returns a new [`SharedKey`] with the same parties but a different frost public key. /// In the erroneous case that the tweak is exactly equal to the negation of the aggregate /// secret key it returns `None`. /// @@ -128,8 +137,10 @@ impl FrostPoly { /// Multiplies the shared key by a scalar. /// - /// In otder for a [`PairedSecretShare`] to be valid against the new key they will have to apply - /// [the same operation](PairedSecretShare::xonly_homomorphic_mul). + /// In order for a [`PairedSecretShare`] to be valid against the new key they will have to apply + /// [the same operation](super::PairedSecretShare::xonly_homomorphic_mul). + /// + /// [`PairedSecretShare`]: super::PairedSecretShare pub fn homomorphic_mul(&mut self, tweak: Scalar) { for coeff in &mut self.point_polynomial { *coeff = g!(tweak * coeff.deref()).normalize(); @@ -137,7 +148,7 @@ impl FrostPoly { } } -impl FrostPoly { +impl SharedKey { /// Applies an "XOnly" tweak to the FROST public key. /// This is how you embed a taproot commitment into a frost public key /// @@ -147,7 +158,7 @@ impl FrostPoly { /// /// ## Return value /// - /// Returns a new [`FrostKey`] with the same parties but a different frost public key. + /// Returns a new [`SharedKey`] with the same parties but a different frost public key. /// In the erroneous case that the tweak is exactly equal to the negation of the aggregate /// secret key it returns `None`. pub fn xonly_homomorphic_add( @@ -167,7 +178,7 @@ impl FrostPoly { /// Multiplies the shared key by a scalar. /// /// In otder for a `PairedSecretShare` to be valid against the new key they will have to apply - /// [the same operation](PairedSecretShare::xonly_homomorphic_mul). + /// [the same operation](super::PairedSecretShare::xonly_homomorphic_mul). pub fn xonly_homomorphic_mul(&mut self, mut tweak: Scalar) { self.point_polynomial[0] = g!(tweak * self.point_polynomial[0]).normalize(); let needs_negation = !self.point_polynomial[0] @@ -184,7 +195,7 @@ impl FrostPoly { } /// The public key that would have signatures verified against for this shared key. - pub fn shared_key(&self) -> Point { + pub fn key(&self) -> Point { let (even_y_point, _needs_negation) = self.point_polynomial[0] .non_zero() .expect("invariant") @@ -195,7 +206,7 @@ impl FrostPoly { } #[cfg(feature = "bincode")] -impl crate::fun::bincode::Decode for FrostPoly { +impl crate::fun::bincode::Decode for SharedKey { fn decode( decoder: &mut D, ) -> Result { @@ -207,7 +218,7 @@ impl crate::fun::bincode::Decode for FrostPoly { )); } - Ok(FrostPoly { + Ok(SharedKey { point_polynomial: poly, ty: PhantomData, }) @@ -215,7 +226,7 @@ impl crate::fun::bincode::Decode for FrostPoly { } #[cfg(feature = "serde")] -impl<'de> crate::fun::serde::Deserialize<'de> for FrostPoly { +impl<'de> crate::fun::serde::Deserialize<'de> for SharedKey { fn deserialize(deserializer: D) -> Result where D: secp256kfun::serde::Deserializer<'de>, @@ -236,4 +247,4 @@ impl<'de> crate::fun::serde::Deserialize<'de> for FrostPoly { } #[cfg(feature = "bincode")] -crate::fun::bincode::impl_borrow_decode!(FrostPoly); +crate::fun::bincode::impl_borrow_decode!(SharedKey); diff --git a/schnorr_fun/src/musig.rs b/schnorr_fun/src/musig.rs index c52206f3..78142478 100644 --- a/schnorr_fun/src/musig.rs +++ b/schnorr_fun/src/musig.rs @@ -339,8 +339,8 @@ where /// - `agg_key`: the joint public key we are signing under. This can be an `XOnly` or `Normal`. /// It will return the same nonce regardless. /// - `secret`: you're secret key as part of `agg_key`. This **must be the secret key you are - /// going to sign with**. It cannot be an "untweaked" version of the signing key. It must be - /// exactly equal to the secret key you pass to [`sign`] (the MuSig specification requires this). + /// going to sign with**. It cannot be an "untweaked" version of the signing key. It must be + /// exactly equal to the secret key you pass to [`sign`] (the MuSig specification requires this). /// - `session_id`: a string of bytes that is **unique for each signing attempt**. /// /// The application should decide upon a unique `session_id` per call to this function. If the diff --git a/schnorr_fun/tests/frost_prop.rs b/schnorr_fun/tests/frost_prop.rs index db0c92d9..e08f1e1f 100644 --- a/schnorr_fun/tests/frost_prop.rs +++ b/schnorr_fun/tests/frost_prop.rs @@ -64,7 +64,7 @@ proptest! { } for secret_share in &xonly_secret_shares { - assert_eq!(secret_share.shared_key(), xonly_poly.shared_key(), "shared key doesn't match"); + assert_eq!(secret_share.shared_key(), xonly_poly.key(), "shared key doesn't match"); } // use a boolean mask for which t participants are signers @@ -91,15 +91,15 @@ proptest! { let public_nonces = secret_nonces.iter().map(|(signer_index, sn)| (*signer_index, sn.public())).collect::>(); - let coord_signing_session = proto.start_coordinator_sign_session( + let coord_signing_session = proto.coordinator_sign_session( &xonly_poly, public_nonces, message ); - let party_signing_session = proto.start_party_sign_session( - xonly_poly.shared_key(), - coord_signing_session.participants(), + let party_signing_session = proto.party_sign_session( + xonly_poly.key(), + coord_signing_session.parties(), coord_signing_session.agg_binonce(), message, ); @@ -125,7 +125,7 @@ proptest! { ); assert!(proto.schnorr.verify( - &xonly_poly.shared_key(), + &xonly_poly.key(), message, &combined_sig )); diff --git a/secp256kfun/src/macros.rs b/secp256kfun/src/macros.rs index 14243e84..13797b44 100644 --- a/secp256kfun/src/macros.rs +++ b/secp256kfun/src/macros.rs @@ -30,8 +30,8 @@ macro_rules! s { /// - ` + ` adds two points /// - ` - ` subtracts one point from another /// - ` .* ` does a [dot product](https://en.wikipedia.org/wiki/Dot_product) -/// between a list of points and scalars. If one list is shorter than the other then the excess -/// points or scalars will be multiplied by 0. See [`op::point_scalar_dot_product`]. +/// between a list of points and scalars. If one list is shorter than the other then the excess +/// points or scalars will be multiplied by 0. See [`op::point_scalar_dot_product`]. /// /// The terms of the expression can be any variable followed by simple method calls, attribute /// access etc. If your term involves more expressions (anything involving specifying types using From 2f00a45b88668e7215080f7f2c986484d4bf0b8e Mon Sep 17 00:00:00 2001 From: LLFourn Date: Mon, 29 Jul 2024 16:54:32 +1000 Subject: [PATCH 07/15] [frost] Make SharedKey and PairedSecretShare zeroable To make arithmetic API simpler at the cost of making the caller call `.non_zero()` etc --- schnorr_fun/src/frost/share.rs | 137 +++++++++--------- schnorr_fun/src/frost/shared_key.rs | 192 ++++++++++++-------------- schnorr_fun/tests/frost_prop.rs | 32 ++--- secp256kfun/src/marker/zero_choice.rs | 13 ++ 4 files changed, 191 insertions(+), 183 deletions(-) diff --git a/schnorr_fun/src/frost/share.rs b/schnorr_fun/src/frost/share.rs index f29e68b8..cd1a0287 100644 --- a/schnorr_fun/src/frost/share.rs +++ b/schnorr_fun/src/frost/share.rs @@ -112,9 +112,9 @@ secp256kfun::impl_display_debug_serialize! { derive(crate::fun::bincode::Encode, crate::fun::bincode::Decode), bincode( crate = "crate::fun::bincode", - encode_bounds = "Point: crate::fun::bincode::Encode", - decode_bounds = "Point: crate::fun::bincode::Decode", - borrow_decode_bounds = "Point: crate::fun::bincode::BorrowDecode<'__de>" + encode_bounds = "Point: crate::fun::bincode::Encode", + decode_bounds = "Point: crate::fun::bincode::Decode", + borrow_decode_bounds = "Point: crate::fun::bincode::BorrowDecode<'__de>" ) )] #[cfg_attr( @@ -123,8 +123,8 @@ secp256kfun::impl_display_debug_serialize! { serde( crate = "crate::fun::serde", bound( - deserialize = "Point: crate::fun::serde::de::Deserialize<'de>", - serialize = "Point: crate::fun::serde::Serialize" + deserialize = "Point: crate::fun::serde::de::Deserialize<'de>", + serialize = "Point: crate::fun::serde::Serialize" ) ) )] @@ -132,12 +132,12 @@ secp256kfun::impl_display_debug_serialize! { /// /// This is useful so you can keep track of tweaks to the secret value and tweaks to the shared key /// in tandem. -pub struct PairedSecretShare { +pub struct PairedSecretShare { secret_share: SecretShare, - shared_key: Point, + shared_key: Point, } -impl PairedSecretShare { +impl PairedSecretShare { /// The index of the secret share pub fn index(&self) -> PartyIndex { self.secret_share.index @@ -149,7 +149,7 @@ impl PairedSecretShare { } /// The shared key this secret is for - pub fn shared_key(&self) -> Point { + pub fn shared_key(&self) -> Point { self.shared_key } @@ -162,41 +162,87 @@ impl PairedSecretShare { } } -impl AsRef for PairedSecretShare { - fn as_ref(&self) -> &SecretShare { - &self.secret_share - } -} - -impl PairedSecretShare { +impl PairedSecretShare { /// Pair a secret share to a shared key - pub fn new(secret_share: SecretShare, shared_key: Point) -> Self { + pub fn new(secret_share: SecretShare, shared_key: Point) -> Self { Self { secret_share, shared_key, } } - /// Add a `Scalar` to both the secret share and the paired shared key. - /// If the share is a share of `secret` it becomes the share of the `secret + tweak`. + /// Adds a scalar `tweak` to the paired secret share. + /// + /// The returned `PairedSecretShare` represents a sharing of the original value + `tweak`. /// - /// This is useful for for deriving unhardened child frost keys from a root frost public key - /// using [BIP32] + /// This is useful for deriving unhardened child frost keys from a master frost public key using + /// [BIP32]. In cases like this since you know that the tweak was computed from a hash of the + /// original key you call [`non_zero`] and unwrap the `Option` since zero is computationally + /// unreachable. + /// + /// If you want to apply an "x-only" tweak you need to call this then [`non_zero`] and finally [`into_xonly`]. + /// + /// See also: [`SharedKey::homomorphic_add`] /// /// [BIP32]: https://bips.xyz/32 - pub fn homomorphic_add(mut self, tweak: Scalar) -> Option { - self.shared_key = g!(self.shared_key + tweak * G).normalize().non_zero()?; - self.secret_share.share = s!(self.secret_share.share + tweak); - Some(self) + /// [`non_zero`]: Self::non_zero + /// [`into_xonly`]: Self::into_xonly + /// [`SharedKey::homomorphic_add`]: crate::frost::SharedKey::homomorphic_add + #[must_use] + pub fn homomorphic_add( + self, + tweak: Scalar, + ) -> PairedSecretShare { + let PairedSecretShare { + mut secret_share, + shared_key, + } = self; + let shared_key = g!(shared_key + tweak * G).normalize(); + secret_share.share = s!(secret_share.share + tweak); + PairedSecretShare { + shared_key, + secret_share, + } } /// Multiply the secret share by `scalar`. - pub fn homomorphic_mul(&mut self, tweak: Scalar) { - self.shared_key = g!(tweak * self.shared_key).normalize(); - self.secret_share.share = s!(tweak * self.secret_share.share); + #[must_use] + pub fn homomorphic_mul(self, tweak: Scalar) -> PairedSecretShare { + let PairedSecretShare { + shared_key, + mut secret_share, + } = self; + + let shared_key = g!(tweak * shared_key).normalize(); + secret_share.share = s!(tweak * self.secret_share.share); + PairedSecretShare { + secret_share, + shared_key, + } } + /// Converts a `PairedSecretShare` to a `PairedSecretShare`. + /// + /// If the paired shared key *was* actually zero ([`is_zero`] returns true) it returns `None`. + /// + /// [`is_zero`]: Point::is_zero + #[must_use] + pub fn non_zero(self) -> Option> { + Some(PairedSecretShare { + secret_share: self.secret_share, + shared_key: self.shared_key.non_zero()?, + }) + } + + /// Is the key this is a share of zero + pub fn is_zero(&self) -> bool { + self.shared_key.is_zero() + } +} + +impl PairedSecretShare { /// Create an XOnly secert share where the paired image is always an `EvenY` point. + #[must_use] pub fn into_xonly(mut self) -> PairedSecretShare { let (shared_key, needs_negation) = self.shared_key.into_point_with_even_y(); self.secret_share.share.conditional_negate(needs_negation); @@ -208,41 +254,6 @@ impl PairedSecretShare { } } -impl PairedSecretShare { - /// Adds an "XOnly" tweak to the FROST public key. If the share is a share of `secret` it - /// becomes the share of the `secret + tweak` however the even-y coordinate of the shared key is - /// maintained by negating if need be. - /// - /// If the secret image were to become zero it returns `None` since this desinged to be - /// [`BIP340`] secret share which does not allow 0. - /// - /// [`BIP340`]: https://bips.xyz/bip340 - pub fn xonly_homomorphic_add( - mut self, - tweak: Scalar, - ) -> Option { - let (shared_key, needs_negation) = g!(self.shared_key + tweak * G) - .normalize() - .non_zero()? - .into_point_with_even_y(); - self.shared_key = shared_key; - self.secret_share.share = s!(self.secret_share.share + tweak); - self.secret_share.share.conditional_negate(needs_negation); - Some(self) - } - - /// Multiply the secret share and paired key by `scalar` maintaing the paired key's even y - /// coordinate by negating them both (if need be). - pub fn xonly_homomorphic_mul(&mut self, mut tweak: Scalar) { - let (shared_key, needs_negation) = g!(tweak * self.shared_key) - .normalize() - .into_point_with_even_y(); - self.shared_key = shared_key; - tweak.conditional_negate(needs_negation); - self.secret_share.share *= tweak; - } -} - #[cfg(feature = "share_backup")] mod share_backup { use super::*; diff --git a/schnorr_fun/src/frost/shared_key.rs b/schnorr_fun/src/frost/shared_key.rs index d380c26d..66f7c472 100644 --- a/schnorr_fun/src/frost/shared_key.rs +++ b/schnorr_fun/src/frost/shared_key.rs @@ -16,14 +16,14 @@ use super::PartyIndex; derive(crate::fun::bincode::Encode), bincode(crate = "crate::fun::bincode",) )] -pub struct SharedKey { +pub struct SharedKey { /// The public point polynomial that defines the access structure to the FROST key. point_polynomial: Vec>, #[cfg_attr(feature = "serde", serde(skip))] - ty: PhantomData, + ty: PhantomData<(T, Z)>, } -impl SharedKey { +impl SharedKey { /// The verification shares of each party in the key. /// /// The verification share is the image of their secret share. @@ -43,157 +43,141 @@ impl SharedKey { pub fn point_polynomial(&self) -> Vec> { self.point_polynomial.clone() } -} - -impl SharedKey { - /// The key that was shared with this polynomial defining the sharing. - /// - /// This is the first coefficient of the polynomial. - pub fn key(&self) -> Point { - self.point_polynomial[0].non_zero().expect("invariant") - } - /// Constructor to create a from a vector of points where each item represent a polynomial - /// coefficient. - /// - /// Returns `None` if the first coefficient is [`Point::zero`]. - pub fn from_poly(poly: Vec>) -> Option { - if poly.is_empty() { - return None; - } - if poly[0].is_zero() { - return None; - } - - Some(Self { - point_polynomial: poly, + /// Type unsafe: you have to make sure the polynomial fits the type parameters + fn from_inner(point_polynomial: Vec>) -> Self { + SharedKey { + point_polynomial, ty: PhantomData, - }) + } } - /// Create a shared key from a subset of verification shares. - /// - /// If all the verification shares are correct and you have at least a threshold of them then - /// you'll get the right answer. If you put in a wrong share you won't get the right answer! + /// Converts a `SharedKey` that's was marked as `Zero` to `NonZero`. /// - /// ## Security + /// If the shared key *was* actually zero ([`is_zero`] returns true) it returns `None`. /// - /// ⚠ You can't just take any random points you want and pass them in here and hope it's secure. - /// They need to be from a securely generated key. - pub fn from_verification_shares( - shares: &[(PartyIndex, Point)], - ) -> Self { - let poly = poly::point::interpolate(shares); - Self { - point_polynomial: poly::point::normalize(poly).collect(), - ty: PhantomData, + /// [`is_zero`]: Self::is_zero + pub fn non_zero(self) -> Option> { + if self.point_polynomial[0].is_zero() { + return None; } + + Some(SharedKey::from_inner(self.point_polynomial)) } - /// Convert the key into a BIP340 "x-only" SharedKey. - /// - /// This is the [BIP340] compatible version of the key which you can put in a segwitv1 output. + + /// Whether the shared key is actually zero. i.e. the first coefficient of the sharing polynomial [`is_zero`]. /// - /// [BIP340]: https://bips.xyz/340 - pub fn into_xonly(mut self) -> SharedKey { - let needs_negation = !self.key().is_y_even(); - if needs_negation { - self.homomorphic_negate(); - debug_assert!(self.key().is_y_even()); - } - SharedKey { - point_polynomial: self.point_polynomial, - ty: PhantomData, - } + /// [`is_zero`]: secp256kfun::Point::is_zero + pub fn is_zero(&self) -> bool { + self.point_polynomial[0].is_zero() } /// Adds a scalar `tweak` to the shared key. /// + /// The returned `SharedKey` represents a sharing of the original value + `tweak`. + /// /// This is useful for deriving unhardened child frost keys from a master frost public key using - /// [BIP32]. + /// [BIP32]. In cases like this since you know that the tweak was computed from a hash of the + /// original key you call [`non_zero`] and unwrap the `Option` since zero is computationally + /// unreachable. /// /// In order for `PairedSecretShare` s to be valid against the new key they will have to apply the same operation. /// - /// ## Return value - /// - /// Returns a new [`SharedKey`] with the same parties but a different frost public key. - /// In the erroneous case that the tweak is exactly equal to the negation of the aggregate - /// secret key it returns `None`. + /// If you want to apply an "x-only" tweak you need to call this then [`non_zero`] and finally [`into_xonly`]. /// /// [BIP32]: https://bips.xyz/32 + /// [`non_zero`]: Self::non_zero + /// [`into_xonly`]: Self::into_xonly #[must_use] - pub fn homomorphic_add(mut self, tweak: Scalar) -> Option { + pub fn homomorphic_add( + mut self, + tweak: Scalar, + ) -> SharedKey { self.point_polynomial[0] = g!(self.point_polynomial[0] + tweak * G).normalize(); - if self.point_polynomial[0].is_zero() { - None - } else { - Some(self) - } + SharedKey::from_inner(self.point_polynomial) } /// Negates the polynomial - pub fn homomorphic_negate(&mut self) { - poly::point::negate(&mut self.point_polynomial) + #[must_use] + pub fn homomorphic_negate(mut self) -> SharedKey { + poly::point::negate(&mut self.point_polynomial); + SharedKey::from_inner(self.point_polynomial) } /// Multiplies the shared key by a scalar. /// /// In order for a [`PairedSecretShare`] to be valid against the new key they will have to apply - /// [the same operation](super::PairedSecretShare::xonly_homomorphic_mul). + /// [the same operation](super::PairedSecretShare::homomorphic_mul). /// /// [`PairedSecretShare`]: super::PairedSecretShare - pub fn homomorphic_mul(&mut self, tweak: Scalar) { + #[must_use] + pub fn homomorphic_mul(mut self, tweak: Scalar) -> SharedKey { for coeff in &mut self.point_polynomial { *coeff = g!(tweak * coeff.deref()).normalize(); } + SharedKey::from_inner(self.point_polynomial) } -} -impl SharedKey { - /// Applies an "XOnly" tweak to the FROST public key. - /// This is how you embed a taproot commitment into a frost public key + /// Create a shared key from a subset of verification shares. /// - /// Tweak the frost public key with a scalar so that the resulting key is equal to the - /// existing key plus `tweak * G` as an [`EvenY`] point. The tweak mutates the public key while still allowing - /// the original set of signers to sign under the new key. + /// If all the verification shares are correct and you have at least a threshold of them then + /// you'll get the right answer. If you put in a wrong share you won't get the right answer! /// - /// ## Return value + /// ## Security /// - /// Returns a new [`SharedKey`] with the same parties but a different frost public key. - /// In the erroneous case that the tweak is exactly equal to the negation of the aggregate - /// secret key it returns `None`. - pub fn xonly_homomorphic_add( - mut self, - tweak: Scalar, - ) -> Option { - self.point_polynomial[0] = g!(self.point_polynomial[0] + tweak * G).normalize(); + /// ⚠ You can't just take any random points you want and pass them in here and hope it's secure. + /// They need to be from a securely generated key. + pub fn from_verification_shares( + shares: &[(PartyIndex, Point)], + ) -> SharedKey { + let poly = poly::point::interpolate(shares); + let poly = poly::point::normalize(poly); + SharedKey::from_inner(poly.collect()) + } +} - let needs_negation = !self.point_polynomial[0].non_zero()?.is_y_even(); +impl SharedKey { + /// Convert the key into a BIP340 "x-only" SharedKey. + /// + /// This is the [BIP340] compatible version of the key which you can put in a segwitv1 output. + /// + /// [BIP340]: https://bips.xyz/340 + pub fn into_xonly(mut self) -> SharedKey { + let needs_negation = !self.key().is_y_even(); if needs_negation { - poly::point::negate(&mut self.point_polynomial); + self = self.homomorphic_negate(); + debug_assert!(self.key().is_y_even()); } - Some(self) + SharedKey::from_inner(self.point_polynomial) } +} - /// Multiplies the shared key by a scalar. +impl SharedKey { + /// The key that was shared with this polynomial defining the sharing. /// - /// In otder for a `PairedSecretShare` to be valid against the new key they will have to apply - /// [the same operation](super::PairedSecretShare::xonly_homomorphic_mul). - pub fn xonly_homomorphic_mul(&mut self, mut tweak: Scalar) { - self.point_polynomial[0] = g!(tweak * self.point_polynomial[0]).normalize(); - let needs_negation = !self.point_polynomial[0] - .non_zero() - .expect("multiplication cannot be zero") - .is_y_even(); - - self.point_polynomial[0] = self.point_polynomial[0].conditional_negate(needs_negation); - tweak.conditional_negate(needs_negation); + /// This is the first coefficient of the polynomial. + pub fn key(&self) -> Point { + Z::cast_point(self.point_polynomial[0]).expect("invariant") + } + /// Constructor to create a from a vector of points where each item represent a polynomial + /// coefficient. + /// + /// Returns `None` if the first coefficient is [`Point::zero`]. + pub fn from_poly(poly: Vec>) -> Option { + if poly.is_empty() { + return None; + } - for coeff in &mut self.point_polynomial[1..] { - *coeff = g!(tweak * coeff.deref()).normalize(); + if poly[0].is_zero() && !Z::is_zero() { + return None; } + + Some(SharedKey::from_inner(poly)) } +} +impl SharedKey { /// The public key that would have signatures verified against for this shared key. pub fn key(&self) -> Point { let (even_y_point, _needs_negation) = self.point_polynomial[0] diff --git a/schnorr_fun/tests/frost_prop.rs b/schnorr_fun/tests/frost_prop.rs index e08f1e1f..d94050fc 100644 --- a/schnorr_fun/tests/frost_prop.rs +++ b/schnorr_fun/tests/frost_prop.rs @@ -30,41 +30,41 @@ proptest! { // // create some scalar polynomial for each party let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); - let (mut frost_poly, mut secret_shares) = proto.simulate_keygen(threshold, n_parties, &mut rng); + let (mut shared_key, mut secret_shares) = proto.simulate_keygen(threshold, n_parties, &mut rng); if let Some(tweak) = add_tweak { for secret_share in &mut secret_shares { - *secret_share = secret_share.homomorphic_add(tweak).unwrap(); + *secret_share = secret_share.homomorphic_add(tweak).non_zero().unwrap(); } - frost_poly = frost_poly.homomorphic_add(tweak).unwrap(); + shared_key = shared_key.homomorphic_add(tweak).non_zero().unwrap(); } if let Some(mul_tweak) = mul_tweak { - frost_poly.homomorphic_mul(mul_tweak); - for secret_share in &mut secret_shares { - secret_share.homomorphic_mul(mul_tweak); + shared_key = shared_key.homomorphic_mul(mul_tweak); + for secret_share in &mut secret_shares { + *secret_share = secret_share.homomorphic_mul(mul_tweak); } } - let mut xonly_poly = frost_poly.into_xonly(); + let mut xonly_shared_key = shared_key.into_xonly(); let mut xonly_secret_shares = secret_shares.into_iter().map(|secret_share| secret_share.into_xonly()).collect::>(); if let Some(tweak) = xonly_add_tweak { - xonly_poly = xonly_poly.xonly_homomorphic_add(tweak).unwrap(); + xonly_shared_key = xonly_shared_key.homomorphic_add(tweak).non_zero().unwrap().into_xonly(); for secret_share in &mut xonly_secret_shares { - *secret_share = secret_share.xonly_homomorphic_add(tweak).unwrap(); + *secret_share = secret_share.homomorphic_add(tweak).non_zero().unwrap().into_xonly(); } } if let Some(xonly_mul_tweak) = xonly_mul_tweak { - xonly_poly.xonly_homomorphic_mul(xonly_mul_tweak); + xonly_shared_key = xonly_shared_key.homomorphic_mul(xonly_mul_tweak).into_xonly(); for secret_share in &mut xonly_secret_shares { - secret_share.xonly_homomorphic_mul(xonly_mul_tweak); + *secret_share = secret_share.homomorphic_mul(xonly_mul_tweak).into_xonly(); } } for secret_share in &xonly_secret_shares { - assert_eq!(secret_share.shared_key(), xonly_poly.key(), "shared key doesn't match"); + assert_eq!(secret_share.shared_key(), xonly_shared_key.key(), "shared key doesn't match"); } // use a boolean mask for which t participants are signers @@ -92,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( - &xonly_poly, + &xonly_shared_key, public_nonces, message ); let party_signing_session = proto.party_sign_session( - xonly_poly.key(), + xonly_shared_key.key(), coord_signing_session.parties(), coord_signing_session.agg_binonce(), message, @@ -112,7 +112,7 @@ proptest! { secret_nonces.remove(&secret_share.index()).unwrap() ); assert!(proto.verify_signature_share( - &xonly_poly, + &xonly_shared_key, &coord_signing_session, secret_share.index(), sig) @@ -125,7 +125,7 @@ proptest! { ); assert!(proto.schnorr.verify( - &xonly_poly.key(), + &xonly_shared_key.key(), message, &combined_sig )); diff --git a/secp256kfun/src/marker/zero_choice.rs b/secp256kfun/src/marker/zero_choice.rs index e001fc15..fceb0580 100644 --- a/secp256kfun/src/marker/zero_choice.rs +++ b/secp256kfun/src/marker/zero_choice.rs @@ -1,3 +1,4 @@ +use crate::Point; /// Something marked with Zero might be `0` i.e. the additive identity #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Ord, PartialOrd)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -30,17 +31,29 @@ pub trait ZeroChoice: { /// Returns whether the type is `Zero` fn is_zero() -> bool; + + /// Casts a point from one zeroness to another. + fn cast_point(point: Point) -> Option>; } impl ZeroChoice for Zero { fn is_zero() -> bool { true } + + fn cast_point(point: Point) -> Option> { + Some(point.mark_zero()) + } } + impl ZeroChoice for NonZero { fn is_zero() -> bool { false } + + fn cast_point(point: Point) -> Option> { + point.non_zero() + } } /// A trait to figure out whether the result of a multiplication should be [`Zero`] or [`NonZero`] at compile time. From dda6cdf79d536d8583186fc16971b838f4e20ba9 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Mon, 29 Jul 2024 17:44:00 +1000 Subject: [PATCH 08/15] [frost] make verifying and combining signature shares better --- schnorr_fun/src/frost/mod.rs | 118 +++++++++++++++++++++++++++---- schnorr_fun/src/frost/session.rs | 10 ++- schnorr_fun/tests/frost_prop.rs | 14 ++-- 3 files changed, 120 insertions(+), 22 deletions(-) diff --git a/schnorr_fun/src/frost/mod.rs b/schnorr_fun/src/frost/mod.rs index 2dda37cd..c9c4ebca 100644 --- a/schnorr_fun/src/frost/mod.rs +++ b/schnorr_fun/src/frost/mod.rs @@ -110,16 +110,20 @@ //! // create a partial signature using our secret share and secret nonce //! let my_sig_share = frost.sign(&sign_session, &xonly_my_secret_share, my_nonce); //! # let sig_share3 = frost.sign(&sign_session, &xonly_secret_share3, nonce3); -//! // 🐙 receive the partial signature(s) from the other participant(s) and verify. -//! assert!(frost.verify_signature_share(&xonly_shared_key, &coord_session, party_index3, sig_share3)); +//! // 🐙 receive the partial signature(s) from the other participant(s). //! // 🐙 combine signature shares into a single signature that is valid under the FROST key -//! // It won't be necessarily be valid unless you verified each share. -//! let combined_sig = frost.combine_signature_shares(&coord_session, vec![my_sig_share, sig_share3]); +//! let combined_sig = frost.verify_and_combine_signature_shares( +//! &xonly_shared_key, +//! &coord_session, +//! [(my_index, my_sig_share), (party_index3, sig_share3)].into() +//! )?; //! assert!(frost.schnorr.verify( //! &xonly_shared_key.key(), //! message, //! &combined_sig //! )); +//! +//! # Ok::<(), schnorr_fun::frost::VerifySignatureSharesError>(()) //! ``` //! //! # Description @@ -717,6 +721,7 @@ impl + Clone, NG> Frost { binding_coeff, challenge, binonce_needs_negation, + final_nonce, } } @@ -814,14 +819,14 @@ impl + Clone, NG> Frost { /// /// ## Return Value /// - /// Returns `bool`, true if partial signature is valid. + /// Returns `true` if signature share is valid. pub fn verify_signature_share( &self, shared_key: &SharedKey, session: &CoordinatorSignSession, index: PartyIndex, signature_share: Scalar, - ) -> bool { + ) -> Result<(), SignatureShareInvalid> { let s = signature_share; let lambda = poly::eval_basis_poly_at_0(index, session.nonces.keys().cloned()); let c = &session.challenge; @@ -832,29 +837,71 @@ impl + Clone, NG> Frost { .get(&index) .expect("verifying party index that is not part of frost signing coalition") .0; - g!(R1 + b * R2 + (c * lambda) * X - s * G).is_zero() + let valid = g!(R1 + b * R2 + (c * lambda) * X - s * G).is_zero(); + if valid { + Ok(()) + } else { + Err(SignatureShareInvalid { index }) + } } - /// Combine a vector of signatures shares into an aggregate signature. + /// Combines signature shares from each party into the final signature. /// - /// This method does not check the validity of the `signature_shares` but if you have verified - /// each signautre share individually the output will be a valid siganture under the `frost_key` - /// and message provided when starting the session. + /// You can use this instead of calling [`verify_signature_share`] on each share. + /// + /// [`verify_signature_share`]: Self::verify_signature_share + pub fn verify_and_combine_signature_shares( + &self, + shared_key: &SharedKey, + session: &CoordinatorSignSession, + signature_shares: BTreeMap>, + ) -> Result { + if signature_shares.len() < shared_key.threshold() { + return Err(VerifySignatureSharesError::NotEnough { + missing: shared_key.threshold() - signature_shares.len(), + }); + } + for (party_index, signature_share) in &signature_shares { + self.verify_signature_share(shared_key, session, *party_index, *signature_share) + .map_err(VerifySignatureSharesError::Invalid)?; + } + + let signature = self + .combine_signature_shares(session.final_nonce(), signature_shares.values().cloned()); + + Ok(signature) + } + + /// Combine a vector of signatures shares into an aggregate signature given the final nonce. + /// + /// You can get `final_nonce` from either of the [`CoordinatorSignSession`] or the [`PartySignSession`]. + /// + /// This method does not check the validity of the `signature_shares` + /// but if you have verified each signature share + /// individually the output will be a valid siganture under the `frost_key` and message provided + /// when starting the session. + /// + /// Alternatively you can use [`verify_and_combine_signature_shares`] which checks and combines + /// the signature shares. /// /// ## Return value /// - /// Returns a combined schnorr [`Signature`] on the message + /// Returns a schnorr [`Signature`] on the message + /// + /// [`CoordinatorSignSession`]: CoordinatorSignSession::final_nonce + /// [`PartySignSession`]: PartySignSession::final_nonce + /// [`verify_and_combine_signature_shares`]: Self::verify_and_combine_signature_shares pub fn combine_signature_shares( &self, - session: &CoordinatorSignSession, - signature_shares: Vec>, + final_nonce: Point, + signature_shares: impl IntoIterator>, ) -> Signature { let sum_s = signature_shares .into_iter() .reduce(|acc, partial_sig| s!(acc + partial_sig).public()) .unwrap_or(Scalar::zero()); Signature { - R: session.final_nonce, + R: final_nonce, s: sum_s, } } @@ -905,6 +952,47 @@ where Frost::default() } +/// Error for a signature share being invalid +#[derive(Clone, Debug, PartialEq)] +pub struct SignatureShareInvalid { + index: PartyIndex, +} + +/// Error returned by [`Frost::verify_and_combine_signature_shares`] +#[derive(Clone, Debug, PartialEq)] +pub enum VerifySignatureSharesError { + /// Not enough signature shares to compelte the signature + NotEnough { + /// How many are missing + missing: usize, + }, + /// One of the signature shars was invalid + Invalid(SignatureShareInvalid), +} + +impl core::fmt::Display for VerifySignatureSharesError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + match self { + VerifySignatureSharesError::NotEnough { missing } => { + write!(f, "not enough signature shares have been collected to finish the signature. You need {missing} more.") + } + VerifySignatureSharesError::Invalid(invalid) => write!(f, "{invalid}"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for VerifySignatureSharesError {} + +impl core::fmt::Display for SignatureShareInvalid { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "signature share from party {} was invalid", self.index) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for SignatureShareInvalid {} + #[cfg(test)] mod test { diff --git a/schnorr_fun/src/frost/session.rs b/schnorr_fun/src/frost/session.rs index d73541ba..835838da 100644 --- a/schnorr_fun/src/frost/session.rs +++ b/schnorr_fun/src/frost/session.rs @@ -40,7 +40,7 @@ impl CoordinatorSignSession { self.agg_binonce } - /// The final nonce that will actually go on the blockchain + /// The final nonce that will actually appear in the signature pub fn final_nonce(&self) -> Point { self.final_nonce } @@ -68,4 +68,12 @@ pub struct PartySignSession { pub(crate) challenge: Scalar, pub(crate) binonce_needs_negation: bool, pub(crate) binding_coeff: Scalar, + pub(crate) final_nonce: Point, +} + +impl PartySignSession { + /// The final nonce that will actually appear in the signature + pub fn final_nonce(&self) -> Point { + self.final_nonce + } } diff --git a/schnorr_fun/tests/frost_prop.rs b/schnorr_fun/tests/frost_prop.rs index d94050fc..27e10b32 100644 --- a/schnorr_fun/tests/frost_prop.rs +++ b/schnorr_fun/tests/frost_prop.rs @@ -104,30 +104,32 @@ proptest! { message, ); - let mut signatures = vec![]; + let mut signatures = BTreeMap::default(); for secret_share in secret_shares_of_signers { let sig = proto.sign( &party_signing_session, &secret_share, secret_nonces.remove(&secret_share.index()).unwrap() ); - assert!(proto.verify_signature_share( + assert_eq!(proto.verify_signature_share( &xonly_shared_key, &coord_signing_session, secret_share.index(), - sig) + sig), Ok(()) ); - signatures.push(sig); + signatures.insert(secret_share.index(), sig); } let combined_sig = proto.combine_signature_shares( - &coord_signing_session, - signatures + coord_signing_session.final_nonce(), + signatures.values().cloned() ); + assert_eq!(proto.verify_and_combine_signature_shares(&xonly_shared_key, &coord_signing_session, signatures), Ok(combined_sig.clone())); assert!(proto.schnorr.verify( &xonly_shared_key.key(), message, &combined_sig )); + } } From 4e222791dad3cde339aa65d9b5f52929b96f74f4 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Tue, 30 Jul 2024 12:52:30 +1000 Subject: [PATCH 09/15] [frost] add Frost::aggregate_binonces So it's clearer how you can avoid a coordinator --- schnorr_fun/src/binonce.rs | 4 ++-- schnorr_fun/src/frost/mod.rs | 31 +++++++++++++++++++++---------- schnorr_fun/src/frost/session.rs | 7 ++++++- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/schnorr_fun/src/binonce.rs b/schnorr_fun/src/binonce.rs index daa1fee8..1d7d27ce 100644 --- a/schnorr_fun/src/binonce.rs +++ b/schnorr_fun/src/binonce.rs @@ -62,8 +62,8 @@ impl HashInto for Nonce { impl Nonce { /// Adds a bunch of binonces together (one for each party signing usually). - pub fn aggregate(nonces: impl Iterator>) -> Self { - let agg = nonces.fold([Point::zero(); 2], |acc, nonce| { + pub fn aggregate(nonces: impl IntoIterator) -> Self { + let agg = nonces.into_iter().fold([Point::zero(); 2], |acc, nonce| { [g!(acc[0] + nonce.0[0]), g!(acc[1] + nonce.0[1])] }); diff --git a/schnorr_fun/src/frost/mod.rs b/schnorr_fun/src/frost/mod.rs index c9c4ebca..0f4d8707 100644 --- a/schnorr_fun/src/frost/mod.rs +++ b/schnorr_fun/src/frost/mod.rs @@ -703,20 +703,31 @@ impl + Clone, NG> Frost { 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. + /// + /// [`party_sign_session`]: Self::party_sign_session + pub fn aggregate_binonces( + &self, + nonces: impl IntoIterator, + ) -> binonce::Nonce { + binonce::Nonce::aggregate(nonces) + } + /// Start party signing session pub fn party_sign_session( &self, - shared_key: Point, + public_key: Point, parties: BTreeSet, - agg_binonce: Nonce, + agg_binonce: binonce::Nonce, message: Message, ) -> PartySignSession { - let binding_coeff = self.binding_coefficient(shared_key, agg_binonce, message); + let binding_coeff = self.binding_coefficient(public_key, agg_binonce, message); let (final_nonce, binonce_needs_negation) = agg_binonce.bind(binding_coeff); - let challenge = self.schnorr.challenge(&final_nonce, &shared_key, message); + let challenge = self.schnorr.challenge(&final_nonce, &public_key, message); PartySignSession { - shared_key, + public_key, parties, binding_coeff, challenge, @@ -736,22 +747,22 @@ impl + Clone, NG> Frost { /// If the number of nonces is less than the threshold. pub fn coordinator_sign_session( &self, - frost_poly: &SharedKey, + shared_key: &SharedKey, mut nonces: BTreeMap, message: Message, ) -> CoordinatorSignSession { - if nonces.len() < frost_poly.threshold() { + if nonces.len() < shared_key.threshold() { panic!("nonces' length was less than the threshold"); } let agg_binonce = binonce::Nonce::aggregate(nonces.values().cloned()); - let binding_coeff = self.binding_coefficient(frost_poly.key(), agg_binonce, message); + let binding_coeff = self.binding_coefficient(shared_key.key(), agg_binonce, message); let (final_nonce, binonce_needs_negation) = agg_binonce.bind(binding_coeff); let challenge = self .schnorr - .challenge(&final_nonce, &frost_poly.key(), message); + .challenge(&final_nonce, &shared_key.key(), message); for nonce in nonces.values_mut() { nonce.conditional_negate(binonce_needs_negation); @@ -799,7 +810,7 @@ impl + Clone, NG> Frost { secret_share: &PairedSecretShare, secret_nonce: NonceKeyPair, ) -> Scalar { - if session.shared_key != secret_share.shared_key() { + if session.public_key != secret_share.shared_key() { panic!("the share's shared key is not the same as the shared key of the session"); } let secret_share = secret_share.secret_share(); diff --git a/schnorr_fun/src/frost/session.rs b/schnorr_fun/src/frost/session.rs index 835838da..9f12e1b0 100644 --- a/schnorr_fun/src/frost/session.rs +++ b/schnorr_fun/src/frost/session.rs @@ -63,7 +63,7 @@ impl CoordinatorSignSession { serde(crate = "crate::fun::serde") )] pub struct PartySignSession { - pub(crate) shared_key: Point, + pub(crate) public_key: Point, pub(crate) parties: BTreeSet>, pub(crate) challenge: Scalar, pub(crate) binonce_needs_negation: bool, @@ -76,4 +76,9 @@ impl PartySignSession { pub fn final_nonce(&self) -> Point { self.final_nonce } + + /// The public key the session was started under + pub fn public_key(&self) -> Point { + self.public_key + } } From cd1847dcafc80a103a35c5bb058ce62e721c66ac Mon Sep 17 00:00:00 2001 From: LLFourn Date: Wed, 31 Jul 2024 10:43:08 +1000 Subject: [PATCH 10/15] =?UTF-8?q?[=E2=9D=84]=20PairedSecretShare=20constru?= =?UTF-8?q?ction=20safety?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pair the secret shares using the shared key so you know it's correct. Also cleaned up the accessing the public key in order to do the implementation. ..and some other misc changes 🤗 --- schnorr_fun/src/frost/mod.rs | 18 ++++--- schnorr_fun/src/frost/share.rs | 43 ++++++++------- schnorr_fun/src/frost/shared_key.rs | 78 +++++++++++++++------------- schnorr_fun/tests/frost_prop.rs | 6 +-- secp256kfun/src/marker/point_type.rs | 67 +++++++++++++++++++++++- 5 files changed, 145 insertions(+), 67 deletions(-) diff --git a/schnorr_fun/src/frost/mod.rs b/schnorr_fun/src/frost/mod.rs index 0f4d8707..958785ab 100644 --- a/schnorr_fun/src/frost/mod.rs +++ b/schnorr_fun/src/frost/mod.rs @@ -99,14 +99,14 @@ //! # let nonce3 = NonceKeyPair::random(&mut rand::thread_rng()); //! // share your public nonce with the other signing participant(s) receive public nonces //! # let received_nonce3 = nonce3.public(); -//! // 🐙 the coordinator has received these +//! // 🐙 the coordinator has received the nonces //! let nonces = BTreeMap::from_iter([(my_index, my_nonce.public()), (party_index3, received_nonce3)]); //! let coord_session = frost.coordinator_sign_session(&xonly_shared_key, nonces, message); //! // Parties receive the agg_nonce from the coordiantor and the list of perties //! let agg_binonce = coord_session.agg_binonce(); //! let parties = coord_session.parties(); //! // start a sign session with these nonces for a message -//! let sign_session = frost.party_sign_session(xonly_my_secret_share.shared_key(),parties, agg_binonce, message); +//! let sign_session = frost.party_sign_session(xonly_my_secret_share.public_key(),parties, agg_binonce, message); //! // create a partial signature using our secret share and secret nonce //! let my_sig_share = frost.sign(&sign_session, &xonly_my_secret_share, my_nonce); //! # let sig_share3 = frost.sign(&sign_session, &xonly_secret_share3, nonce3); @@ -118,7 +118,7 @@ //! [(my_index, my_sig_share), (party_index3, sig_share3)].into() //! )?; //! assert!(frost.schnorr.verify( -//! &xonly_shared_key.key(), +//! &xonly_shared_key.public_key(), //! message, //! &combined_sig //! )); @@ -452,7 +452,7 @@ impl + Clone, NG: NonceGen> Frost { session_id: &[u8], ) -> R { let sid_len = (session_id.len() as u64).to_be_bytes(); - let pk_bytes = paired_secret_share.shared_key().to_xonly_bytes(); + let pk_bytes = paired_secret_share.public_key().to_xonly_bytes(); let rng: R = derive_nonce_rng!( nonce_gen => self.nonce_gen(), @@ -620,6 +620,7 @@ impl + Clone, NG> Frost { .map(|coef| coef.normalize()) .collect(), ) + .non_zero() .ok_or(NewKeyGenError::ZeroFrostKey)?; Ok(KeyGen { @@ -698,7 +699,8 @@ impl + Clone, NG> Frost { share: total_secret_share, }; - let secret_share_with_image = PairedSecretShare::new(secret_share, keygen.frost_poly.key()); + let secret_share_with_image = + PairedSecretShare::new(secret_share, keygen.frost_poly.public_key()); Ok((secret_share_with_image, keygen.frost_poly)) } @@ -757,12 +759,12 @@ impl + Clone, NG> Frost { let agg_binonce = binonce::Nonce::aggregate(nonces.values().cloned()); - let binding_coeff = self.binding_coefficient(shared_key.key(), agg_binonce, message); + let binding_coeff = self.binding_coefficient(shared_key.public_key(), agg_binonce, message); let (final_nonce, binonce_needs_negation) = agg_binonce.bind(binding_coeff); let challenge = self .schnorr - .challenge(&final_nonce, &shared_key.key(), message); + .challenge(&final_nonce, &shared_key.public_key(), message); for nonce in nonces.values_mut() { nonce.conditional_negate(binonce_needs_negation); @@ -810,7 +812,7 @@ impl + Clone, NG> Frost { secret_share: &PairedSecretShare, secret_nonce: NonceKeyPair, ) -> Scalar { - if session.public_key != secret_share.shared_key() { + if session.public_key != secret_share.public_key() { panic!("the share's shared key is not the same as the shared key of the session"); } let secret_share = secret_share.secret_share(); diff --git a/schnorr_fun/src/frost/share.rs b/schnorr_fun/src/frost/share.rs index cd1a0287..9b8b82d3 100644 --- a/schnorr_fun/src/frost/share.rs +++ b/schnorr_fun/src/frost/share.rs @@ -20,8 +20,7 @@ use secp256kfun::{poly, prelude::*}; /// shares that way. Share identification can help for keeping track of them and distinguishing shares /// when there are only a small number of them. /// -/// The backup format is enabled with the `share_backup` feature and accessed with the `FromStr` -/// and `Display`. +/// The backup format is enabled with the `share_backup` feature and accessed with the feature enabled methods. /// /// ### Index in human readable part /// @@ -132,9 +131,9 @@ secp256kfun::impl_display_debug_serialize! { /// /// This is useful so you can keep track of tweaks to the secret value and tweaks to the shared key /// in tandem. -pub struct PairedSecretShare { +pub struct PairedSecretShare { secret_share: SecretShare, - shared_key: Point, + public_key: Point, } impl PairedSecretShare { @@ -148,9 +147,9 @@ impl PairedSecretShare { self.secret_share.share } - /// The shared key this secret is for - pub fn shared_key(&self) -> Point { - self.shared_key + /// The public key that this secert share is a part of + pub fn public_key(&self) -> Point { + self.public_key } /// The inner un-paired secret share. @@ -162,12 +161,16 @@ impl PairedSecretShare { } } -impl PairedSecretShare { - /// Pair a secret share to a shared key - pub fn new(secret_share: SecretShare, shared_key: Point) -> Self { +impl PairedSecretShare { + /// Pair a secret share to a shared key. + /// + /// Users are meant to use [`pair_secret_share`] to create this + /// + /// [`pair_secret_share`]: SharedKey::pair_secret_share + pub(crate) fn new(secret_share: SecretShare, public_key: Point) -> Self { Self { secret_share, - shared_key, + public_key, } } @@ -195,12 +198,12 @@ impl PairedSecretShare { ) -> PairedSecretShare { let PairedSecretShare { mut secret_share, - shared_key, + public_key: shared_key, } = self; let shared_key = g!(shared_key + tweak * G).normalize(); secret_share.share = s!(secret_share.share + tweak); PairedSecretShare { - shared_key, + public_key: shared_key, secret_share, } } @@ -209,7 +212,7 @@ impl PairedSecretShare { #[must_use] pub fn homomorphic_mul(self, tweak: Scalar) -> PairedSecretShare { let PairedSecretShare { - shared_key, + public_key: shared_key, mut secret_share, } = self; @@ -217,7 +220,7 @@ impl PairedSecretShare { secret_share.share = s!(tweak * self.secret_share.share); PairedSecretShare { secret_share, - shared_key, + public_key: shared_key, } } @@ -230,13 +233,13 @@ impl PairedSecretShare { pub fn non_zero(self) -> Option> { Some(PairedSecretShare { secret_share: self.secret_share, - shared_key: self.shared_key.non_zero()?, + public_key: self.public_key.non_zero()?, }) } /// Is the key this is a share of zero pub fn is_zero(&self) -> bool { - self.shared_key.is_zero() + self.public_key.is_zero() } } @@ -244,12 +247,12 @@ impl PairedSecretShare { /// Create an XOnly secert share where the paired image is always an `EvenY` point. #[must_use] pub fn into_xonly(mut self) -> PairedSecretShare { - let (shared_key, needs_negation) = self.shared_key.into_point_with_even_y(); + let (shared_key, needs_negation) = self.public_key.into_point_with_even_y(); self.secret_share.share.conditional_negate(needs_negation); PairedSecretShare { secret_share: self.secret_share, - shared_key, + public_key: shared_key, } } } @@ -488,7 +491,7 @@ mod test { let chosen = shares.choose_multiple(&mut rng, threshold).cloned() .map(|paired_share| paired_share.secret_share).collect::>(); let secret = SecretShare::recover_secret(&chosen); - prop_assert_eq!(g!(secret * G), frost_poly.key()); + prop_assert_eq!(g!(secret * G), frost_poly.public_key()); } } } diff --git a/schnorr_fun/src/frost/shared_key.rs b/schnorr_fun/src/frost/shared_key.rs index 66f7c472..3e44d158 100644 --- a/schnorr_fun/src/frost/shared_key.rs +++ b/schnorr_fun/src/frost/shared_key.rs @@ -3,7 +3,7 @@ use core::{marker::PhantomData, ops::Deref}; use alloc::vec::Vec; use secp256kfun::{poly, prelude::*}; -use super::PartyIndex; +use super::{PairedSecretShare, PartyIndex, SecretShare}; /// A polynomial #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr( @@ -31,20 +31,33 @@ impl SharedKey { poly::point::eval(&self.point_polynomial, index) } + /// "pair" a secret share that belongs to this shared key so you can keep track of tweaks to the + /// public key and the secret share together. + /// + /// Returns `None` if the secret share is not a valid share of this key. + pub fn pair_secret_share(&self, secret_share: SecretShare) -> Option> { + if self.verification_share(secret_share.index) != g!(secret_share.share * G) { + return None; + } + + Some(PairedSecretShare::new(secret_share, self.public_key())) + } + /// The threshold number of participants required in a signing coalition to produce a valid signature. pub fn threshold(&self) -> usize { self.point_polynomial.len() } - /// The public image of the key's polynomial on the elliptic curve. + /// The internal public polynomial coefficients that defines the public key and the share structure. /// - /// Note: the first coefficient (index `0`) is guaranteed to be non-zero but the coefficients - /// may be. + /// To get the first coefficient of the polynomial typed correctly call [`public_key`]. + /// + /// [`public_key`]: Self::public_key pub fn point_polynomial(&self) -> Vec> { self.point_polynomial.clone() } - /// Type unsafe: you have to make sure the polynomial fits the type parameters + /// ☠ Type unsafe: you have to make sure the polynomial fits the type parameters fn from_inner(point_polynomial: Vec>) -> Self { SharedKey { point_polynomial, @@ -134,6 +147,19 @@ impl SharedKey { let poly = poly::point::normalize(poly); SharedKey::from_inner(poly.collect()) } + + /// The public key that has been shared. + /// + /// This is using *public key* in a rather loose sense. Unless it's a `SharedKey` then it + /// won't be usable as an actual Schnorr [BIP340] public key. + /// + /// [BIP340]: https://bips.xyz/340 + pub fn public_key(&self) -> Point { + // SAFETY: we hold the first coefficient to match the type parameters always + let public_key = Z::cast_point(self.point_polynomial[0]).expect("invariant"); + let public_key = T::cast_point(public_key).expect("invariant"); + public_key + } } impl SharedKey { @@ -143,49 +169,31 @@ impl SharedKey { /// /// [BIP340]: https://bips.xyz/340 pub fn into_xonly(mut self) -> SharedKey { - let needs_negation = !self.key().is_y_even(); + let needs_negation = !self.public_key().is_y_even(); if needs_negation { self = self.homomorphic_negate(); - debug_assert!(self.key().is_y_even()); + debug_assert!(self.public_key().is_y_even()); } SharedKey::from_inner(self.point_polynomial) } } -impl SharedKey { - /// The key that was shared with this polynomial defining the sharing. - /// - /// This is the first coefficient of the polynomial. - pub fn key(&self) -> Point { - Z::cast_point(self.point_polynomial[0]).expect("invariant") - } - /// Constructor to create a from a vector of points where each item represent a polynomial +impl SharedKey { + /// Constructor to create a shared key from a vector of points where each item represent a polynomial /// coefficient. /// - /// Returns `None` if the first coefficient is [`Point::zero`]. - pub fn from_poly(poly: Vec>) -> Option { + /// The resulting shared key will be `SharedKey`. It's up to the caller to do the zero check with [`non_zero`] + /// + /// [`non_zero`]: Self::non_zero + pub fn from_poly(poly: Vec>) -> Self { if poly.is_empty() { - return None; + // an empty polynomial is represented as a vector with a single zero item to avoid + // panics + return Self::from_poly(vec![Point::zero()]); } - if poly[0].is_zero() && !Z::is_zero() { - return None; - } - - Some(SharedKey::from_inner(poly)) - } -} - -impl SharedKey { - /// The public key that would have signatures verified against for this shared key. - pub fn key(&self) -> Point { - let (even_y_point, _needs_negation) = self.point_polynomial[0] - .non_zero() - .expect("invariant") - .into_point_with_even_y(); - assert!(!_needs_negation); - even_y_point + SharedKey::from_inner(poly) } } diff --git a/schnorr_fun/tests/frost_prop.rs b/schnorr_fun/tests/frost_prop.rs index 27e10b32..0079f81a 100644 --- a/schnorr_fun/tests/frost_prop.rs +++ b/schnorr_fun/tests/frost_prop.rs @@ -64,7 +64,7 @@ proptest! { } for secret_share in &xonly_secret_shares { - assert_eq!(secret_share.shared_key(), xonly_shared_key.key(), "shared key doesn't match"); + assert_eq!(secret_share.public_key(), xonly_shared_key.public_key(), "shared key doesn't match"); } // use a boolean mask for which t participants are signers @@ -98,7 +98,7 @@ proptest! { ); let party_signing_session = proto.party_sign_session( - xonly_shared_key.key(), + xonly_shared_key.public_key(), coord_signing_session.parties(), coord_signing_session.agg_binonce(), message, @@ -126,7 +126,7 @@ proptest! { assert_eq!(proto.verify_and_combine_signature_shares(&xonly_shared_key, &coord_signing_session, signatures), Ok(combined_sig.clone())); assert!(proto.schnorr.verify( - &xonly_shared_key.key(), + &xonly_shared_key.public_key(), message, &combined_sig )); diff --git a/secp256kfun/src/marker/point_type.rs b/secp256kfun/src/marker/point_type.rs index 894b06af..57267627 100644 --- a/secp256kfun/src/marker/point_type.rs +++ b/secp256kfun/src/marker/point_type.rs @@ -1,3 +1,7 @@ +use crate::Point; + +use super::ZeroChoice; + /// Every `T` of a [`Point`] implements the `PointType` trait. /// /// There are several different point types. @@ -16,6 +20,14 @@ pub trait PointType: /// Whether the point type is normalized or not (i.e. not [`NonNormal`]) fn is_normalized() -> bool; + + /// Cast a point that is not of this type to this type. + /// + /// This is useful internally for doing very generic things and shouldn't be used in + /// applications. + fn cast_point( + point: Point, + ) -> Option>; } /// A Fully Normalized Point. Internally `Normal` points are represented using @@ -72,13 +84,46 @@ impl Normalized for EvenY {} impl Normalized for Normal {} impl Normalized for BasePoint {} -impl PointType for N { +impl PointType for Normal { type NegationType = Normal; #[inline(always)] fn is_normalized() -> bool { true } + + fn cast_point( + point: Point, + ) -> Option> { + Some(point.normalize()) + } +} + +impl PointType for EvenY { + type NegationType = Normal; + + fn is_normalized() -> bool { + true + } + + fn cast_point( + point: Point, + ) -> Option> { + let (point, needs_negation) = point.non_zero()?.into_point_with_even_y(); + if needs_negation { + return None; + } + + // we don't want to allow creating Point + if Z::is_zero() { + return None; + } + + // we already checked it's not zero + let point = Z::cast_point(point).expect("infallible"); + + Some(point) + } } impl PointType for NonNormal { @@ -88,4 +133,24 @@ impl PointType for NonNormal { fn is_normalized() -> bool { false } + + fn cast_point( + point: Point, + ) -> Option> { + Some(point.non_normal()) + } +} + +impl PointType for BasePoint { + type NegationType = Normal; + + fn is_normalized() -> bool { + true + } + + fn cast_point( + _point: Point, + ) -> Option> { + None + } } From 0a6b572ba783c674b8dc54aabf56323b34c41ee2 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Wed, 31 Jul 2024 10:47:31 +1000 Subject: [PATCH 11/15] [schnorr] Make Signature Copy --- schnorr_fun/benches/bench_schnorr.rs | 2 +- schnorr_fun/src/frost/mod.rs | 5 +---- schnorr_fun/src/signature.rs | 2 +- schnorr_fun/tests/frost_prop.rs | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/schnorr_fun/benches/bench_schnorr.rs b/schnorr_fun/benches/bench_schnorr.rs index 38d4989a..926f04f3 100755 --- a/schnorr_fun/benches/bench_schnorr.rs +++ b/schnorr_fun/benches/bench_schnorr.rs @@ -49,7 +49,7 @@ fn verify_schnorr(c: &mut Criterion) { }); { - let sig = sig.clone().set_secrecy::(); + let sig = sig.set_secrecy::(); group.bench_function("fun::schnorr_verify_ct", |b| { b.iter(|| schnorr.verify(verification_key, message, &sig)) }); diff --git a/schnorr_fun/src/frost/mod.rs b/schnorr_fun/src/frost/mod.rs index 958785ab..36f783bc 100644 --- a/schnorr_fun/src/frost/mod.rs +++ b/schnorr_fun/src/frost/mod.rs @@ -504,10 +504,7 @@ impl + Clone, NG: NonceGen> Frost { .map(|(gen_party_index, (party_shares, pop))| { ( *gen_party_index, - ( - party_shares.remove(receiver_party_index).unwrap(), - pop.clone(), - ), + (party_shares.remove(receiver_party_index).unwrap(), *pop), ) }) .collect::>(); diff --git a/schnorr_fun/src/signature.rs b/schnorr_fun/src/signature.rs index 0cfecfeb..fc519d78 100644 --- a/schnorr_fun/src/signature.rs +++ b/schnorr_fun/src/signature.rs @@ -1,7 +1,7 @@ use crate::fun::{marker::*, rand_core::RngCore, Point, Scalar}; /// A Schnorr signature. -#[derive(Clone, Eq)] +#[derive(Clone, Eq, Copy)] pub struct Signature { /// The signature's public nonce /// diff --git a/schnorr_fun/tests/frost_prop.rs b/schnorr_fun/tests/frost_prop.rs index 0079f81a..a27fe369 100644 --- a/schnorr_fun/tests/frost_prop.rs +++ b/schnorr_fun/tests/frost_prop.rs @@ -124,7 +124,7 @@ proptest! { signatures.values().cloned() ); - assert_eq!(proto.verify_and_combine_signature_shares(&xonly_shared_key, &coord_signing_session, signatures), Ok(combined_sig.clone())); + assert_eq!(proto.verify_and_combine_signature_shares(&xonly_shared_key, &coord_signing_session, signatures), Ok(combined_sig)); assert!(proto.schnorr.verify( &xonly_shared_key.public_key(), message, From 6c37a3733c217afd9297800aa9a621a47571e38e Mon Sep 17 00:00:00 2001 From: LLFourn Date: Wed, 31 Jul 2024 10:48:35 +1000 Subject: [PATCH 12/15] =?UTF-8?q?[=E2=9D=84]=20Make=20point=5Fpolynomial?= =?UTF-8?q?=20return=20a=20slice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No need to clone here --- schnorr_fun/src/frost/shared_key.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/schnorr_fun/src/frost/shared_key.rs b/schnorr_fun/src/frost/shared_key.rs index 3e44d158..15d4d6dc 100644 --- a/schnorr_fun/src/frost/shared_key.rs +++ b/schnorr_fun/src/frost/shared_key.rs @@ -53,8 +53,8 @@ impl SharedKey { /// To get the first coefficient of the polynomial typed correctly call [`public_key`]. /// /// [`public_key`]: Self::public_key - pub fn point_polynomial(&self) -> Vec> { - self.point_polynomial.clone() + pub fn point_polynomial(&self) -> &[Point] { + &self.point_polynomial } /// ☠ Type unsafe: you have to make sure the polynomial fits the type parameters @@ -157,8 +157,8 @@ impl SharedKey { pub fn public_key(&self) -> Point { // SAFETY: we hold the first coefficient to match the type parameters always let public_key = Z::cast_point(self.point_polynomial[0]).expect("invariant"); - let public_key = T::cast_point(public_key).expect("invariant"); - public_key + T::cast_point(public_key).expect("invariant") + } } } From 151c6d669b52e217bfeb224b6f6470d1169bba5a Mon Sep 17 00:00:00 2001 From: LLFourn Date: Wed, 31 Jul 2024 12:16:51 +1000 Subject: [PATCH 13/15] =?UTF-8?q?[=E2=9D=84]=20Implement=20SharedKey::to?= =?UTF-8?q?=5Fbytes/from=5Fslice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As well as make more generic the bincode/serde impls --- schnorr_fun/Cargo.toml | 2 +- schnorr_fun/src/frost/shared_key.rs | 198 +++++++++++++++++++++++++-- secp256kfun/src/marker/point_type.rs | 1 + 3 files changed, 185 insertions(+), 16 deletions(-) diff --git a/schnorr_fun/Cargo.toml b/schnorr_fun/Cargo.toml index 939f8ad3..751ca2b5 100644 --- a/schnorr_fun/Cargo.toml +++ b/schnorr_fun/Cargo.toml @@ -18,7 +18,7 @@ secp256kfun = { path = "../secp256kfun", version = "0.10", default-features = f bech32 = { version = "0.11", optional = true, default-features = false, features = ["alloc"] } [dev-dependencies] -secp256kfun = { path = "../secp256kfun", version = "0.10", features = ["proptest"] } +secp256kfun = { path = "../secp256kfun", version = "0.10", features = ["proptest", "bincode", "alloc"] } rand = { version = "0.8" } lazy_static = "1.4" bincode = "1.0" diff --git a/schnorr_fun/src/frost/shared_key.rs b/schnorr_fun/src/frost/shared_key.rs index 15d4d6dc..b0be4e5f 100644 --- a/schnorr_fun/src/frost/shared_key.rs +++ b/schnorr_fun/src/frost/shared_key.rs @@ -159,6 +159,38 @@ impl SharedKey { let public_key = Z::cast_point(self.point_polynomial[0]).expect("invariant"); T::cast_point(public_key).expect("invariant") } + + /// Encodes a `SharedKey` as the compressed encoding of each underlying polynomial coefficient + /// + /// i.e. call [`Point::to_bytes`] on each coefficent starting with the constant term. Note that + /// even if it's a `SharedKey` the first coefficient (A.K.A the public key) will still be + /// encoded as 33 bytes. + /// + /// ⚠ Unlike other secp256kfun things this doesn't exactly match the serde/bincode + /// implemenations which will length prefix the list of points. + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(self.point_polynomial.len() * 33); + for coeff in &self.point_polynomial { + bytes.extend(coeff.to_bytes()) + } + bytes + } + + /// Decodes a `SharedKey` (for any `T` and `Z`) from a slice. + /// + /// Returns `None` if the bytes don't represent points or if the first coefficient doesn't + /// satisfy the constraints of `T` and `Z`. + pub fn from_slice(bytes: &[u8]) -> Option { + let mut poly = vec![]; + for point_bytes in bytes.chunks(33) { + poly.push(Point::from_slice(point_bytes)?); + } + + // check first coefficient satisfies both type parameters + let first_coeff = Z::cast_point(poly[0])?; + let _check = T::cast_point(first_coeff)?; + + Some(Self::from_inner(poly)) } } @@ -196,19 +228,18 @@ impl SharedKey { SharedKey::from_inner(poly) } } - #[cfg(feature = "bincode")] -impl crate::fun::bincode::Decode for SharedKey { +impl crate::fun::bincode::Decode for SharedKey { fn decode( decoder: &mut D, ) -> Result { + use secp256kfun::bincode::error::DecodeError; let poly = Vec::>::decode(decoder)?; - - if poly[0].is_zero() { - return Err(secp256kfun::bincode::error::DecodeError::Other( - "first coefficient of a frost polynomial can't be zero", - )); - } + let first_coeff = Z::cast_point(poly[0]).ok_or(DecodeError::Other( + "zero public key for non-zero shared key", + ))?; + let _check = T::cast_point(first_coeff) + .ok_or(DecodeError::Other("odd-y public key for even-y shared key"))?; Ok(SharedKey { point_polynomial: poly, @@ -218,18 +249,20 @@ impl crate::fun::bincode::Decode for SharedKey { } #[cfg(feature = "serde")] -impl<'de> crate::fun::serde::Deserialize<'de> for SharedKey { +impl<'de, T: PointType, Z: ZeroChoice> crate::fun::serde::Deserialize<'de> for SharedKey { fn deserialize(deserializer: D) -> Result where D: secp256kfun::serde::Deserializer<'de>, { let poly = Vec::>::deserialize(deserializer)?; - if poly[0].is_zero() { - return Err(crate::fun::serde::de::Error::custom( - "first coefficient of a frost polynomial can't be zero", - )); - } + let first_coeff = Z::cast_point(poly[0]).ok_or(crate::fun::serde::de::Error::custom( + "zero public key for non-zero shared key", + ))?; + + let _check = T::cast_point(first_coeff).ok_or(crate::fun::serde::de::Error::custom( + "odd-y public key for even-y shared key", + ))?; Ok(Self { point_polynomial: poly, @@ -239,4 +272,139 @@ impl<'de> crate::fun::serde::Deserialize<'de> for SharedKey { } #[cfg(feature = "bincode")] -crate::fun::bincode::impl_borrow_decode!(SharedKey); +crate::fun::bincode::impl_borrow_decode!(SharedKey); +#[cfg(feature = "bincode")] +crate::fun::bincode::impl_borrow_decode!(SharedKey); +#[cfg(feature = "bincode")] +crate::fun::bincode::impl_borrow_decode!(SharedKey); + +#[cfg(test)] +mod test { + use super::*; + + #[cfg(feature = "bincode")] + #[test] + fn bincode_encoding_decoding_roundtrip() { + use crate::fun::bincode; + let poly_zero = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(0 * G), + g!(1 * G).mark_zero(), + g!(2 * G).mark_zero(), + ]) + .collect(), + ); + let poly_one = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(1 * G).mark_zero(), + g!(2 * G).mark_zero(), + g!(3 * G).mark_zero(), + ]) + .collect(), + ) + .non_zero() + .unwrap() + .into_xonly(); + + let poly_minus_one = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(-1 * G).mark_zero(), + g!(2 * G).mark_zero(), + g!(3 * G).mark_zero(), + ]) + .collect(), + ) + .non_zero() + .unwrap(); + + let bytes_poly_zero = + bincode::encode_to_vec(&poly_zero, bincode::config::standard()).unwrap(); + let bytes_poly_one = + bincode::encode_to_vec(&poly_one, bincode::config::standard()).unwrap(); + let bytes_poly_minus_one = + bincode::encode_to_vec(&poly_minus_one, bincode::config::standard()).unwrap(); + + let (poly_zero_got, _) = bincode::decode_from_slice::, _>( + &bytes_poly_zero, + bincode::config::standard(), + ) + .unwrap(); + let (poly_one_got, _) = bincode::decode_from_slice::, _>( + &bytes_poly_one, + bincode::config::standard(), + ) + .unwrap(); + + let (poly_minus_one_got, _) = bincode::decode_from_slice::, _>( + &bytes_poly_minus_one, + bincode::config::standard(), + ) + .unwrap(); + + assert!(bincode::decode_from_slice::, _>( + &bytes_poly_zero, + bincode::config::standard(), + ) + .is_err()); + + assert!(bincode::decode_from_slice::, _>( + &bytes_poly_minus_one, + bincode::config::standard(), + ) + .is_err()); + + assert_eq!(poly_zero_got, poly_zero); + assert_eq!(poly_one_got, poly_one); + assert_eq!(poly_minus_one_got, poly_minus_one); + } + + #[test] + fn to_bytes_from_slice_roudtrip() { + let poly_zero = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(0 * G), + g!(1 * G).mark_zero(), + g!(2 * G).mark_zero(), + ]) + .collect(), + ); + let poly_one = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(1 * G).mark_zero(), + g!(2 * G).mark_zero(), + g!(3 * G).mark_zero(), + ]) + .collect(), + ) + .non_zero() + .unwrap() + .into_xonly(); + + let poly_minus_one = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(-1 * G).mark_zero(), + g!(2 * G).mark_zero(), + g!(3 * G).mark_zero(), + ]) + .collect(), + ) + .non_zero() + .unwrap(); + + let bytes_poly_zero = poly_zero.to_bytes(); + let bytes_poly_one = poly_one.to_bytes(); + let bytes_poly_minus_one = poly_minus_one.to_bytes(); + + let poly_zero_got = SharedKey::::from_slice(&bytes_poly_zero[..]).unwrap(); + let poly_one_got = SharedKey::::from_slice(&bytes_poly_one).unwrap(); + let poly_minus_one_got = + SharedKey::::from_slice(&bytes_poly_minus_one[..]).unwrap(); + + assert!(SharedKey::::from_slice(&bytes_poly_zero[..]).is_none()); + assert!(SharedKey::::from_slice(&bytes_poly_minus_one[..]).is_none()); + + assert_eq!(poly_zero_got, poly_zero); + assert_eq!(poly_one_got, poly_one); + assert_eq!(poly_minus_one_got, poly_minus_one); + } +} diff --git a/secp256kfun/src/marker/point_type.rs b/secp256kfun/src/marker/point_type.rs index 57267627..c57d1fbe 100644 --- a/secp256kfun/src/marker/point_type.rs +++ b/secp256kfun/src/marker/point_type.rs @@ -106,6 +106,7 @@ impl PointType for EvenY { true } + /// ⚠ This will always return `None` if trying to cast from a `Zero` marked point (even if the actual point is not `Zero`) fn cast_point( point: Point, ) -> Option> { From 87173fac984ede116d102c944de2252963539386 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Thu, 8 Aug 2024 13:29:30 +1000 Subject: [PATCH 14/15] =?UTF-8?q?[=E2=9D=84]=20Make=20verification=20share?= =?UTF-8?q?s=20first=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit signature share verification was unnecessarily slow because you get to regenerate their whole verification share each time. Now you can cache it. Also verification shares are only for EvenY keys. We now have a concept of "share image" which is more general I'm not entirely sure I like but I'll leave it in for now. --- schnorr_fun/src/frost/mod.rs | 60 +++++++++++++++-------- schnorr_fun/src/frost/session.rs | 16 +++++-- schnorr_fun/src/frost/share.rs | 46 +++++++++++++++--- schnorr_fun/src/frost/shared_key.rs | 74 ++++++++++++++++++----------- schnorr_fun/tests/frost_prop.rs | 3 +- secp256kfun/src/poly.rs | 6 +-- secp256kfun/tests/poly.rs | 12 ++--- 7 files changed, 146 insertions(+), 71 deletions(-) diff --git a/schnorr_fun/src/frost/mod.rs b/schnorr_fun/src/frost/mod.rs index 36f783bc..82f426fc 100644 --- a/schnorr_fun/src/frost/mod.rs +++ b/schnorr_fun/src/frost/mod.rs @@ -28,7 +28,7 @@ //! # let public_poly2 = poly::scalar::to_point_poly(&secret_poly2); //! # let public_poly3 = poly::scalar::to_point_poly(&secret_poly3); //! -//! // Party indicies can be any non-zero scalar +//! // 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(); @@ -86,7 +86,7 @@ //! // 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! //! // With signing we'll have at least one party be the "coordinator" (steps marked with 🐙) -//! // In this example we'll be the coordiantor (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 on eof 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(); @@ -169,7 +169,7 @@ //! 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 protectoin +//! // 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"], @@ -221,8 +221,8 @@ use secp256kfun::{ /// It is used in interpolation and computation of the shared secret. /// /// This index can be any non-zero [`Scalar`], but must be unique between parties. -/// In most cases it will make sense to use simple indicies `s!(1), s!(2), ...` for smaller backups. -/// Other applications may desire to use indicies corresponding to pre-existing keys or identifiers. +/// In most cases it will make sense to use simple indices `s!(1), s!(2), ...` for smaller backups. +/// Other applications may desire to use indices corresponding to pre-existing keys or identifiers. pub type PartyIndex = Scalar; /// The FROST context. @@ -269,7 +269,7 @@ impl Frost { &self.nonce_gen } - /// Create our secret shares to be shared with other participants using pre-existing indicies + /// Create our secret shares to be shared with other participants using pre-existing indices /// /// Each secret share needs to be securely communicated to the intended participant. /// @@ -386,9 +386,9 @@ impl core::fmt::Display for FinishKeyGenError { impl std::error::Error for FinishKeyGenError {} impl + Clone, NG: NonceGen> Frost { - /// Convienence method to generate secret shares and proof-of-possession to be shared with other + /// Convenience 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 (shnorr signature) can be publically shared with + /// participant but the proof of possession (schnorr signature) can be publically shared with /// everyone. pub fn create_shares_and_pop( &self, @@ -438,7 +438,7 @@ impl + Clone, NG: NonceGen> Frost { /// must not reuse the session id, the resulting rng or anything derived from that rng again. /// /// 💡 Before using this function with a deterministic rng write a short justification as to why - /// you beleive your session id will be unique per signing attempt. Perhaps include it as a + /// you believe your session id will be unique per signing attempt. Perhaps include it as a /// comment next to the call. Note **it must be unique even across signing attempts for the same /// or different messages**. /// @@ -537,7 +537,7 @@ impl + Clone, NG: NonceGen> Frost { } impl + Clone, NG> Frost { - /// Generate an id for the key generation by hashing the party indicies and their point + /// Generate an id for the key generation by hashing the party indices and their point /// polynomials pub fn keygen_id(&self, keygen: &KeyGen) -> [u8; 32] { let mut keygen_hash = self.keygen_id_hash.clone(); @@ -650,7 +650,7 @@ impl + Clone, NG> Frost { Ok(keygen.frost_poly) } - /// Combine all receieved shares into your long-lived secret share. + /// 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 @@ -773,6 +773,7 @@ impl + Clone, NG> Frost { final_nonce, challenge, nonces, + public_key: shared_key.public_key(), } } @@ -802,7 +803,7 @@ impl + Clone, NG> Frost { /// /// ## Panics /// - /// Panics if the shah + /// Panics if the `secret_share` was not part of the signing session pub fn sign( &self, session: &PartySignSession, @@ -825,27 +826,42 @@ impl + Clone, NG> Frost { s!(r1 + (r2 * b) + lambda * x * c).public() } - /// Verify a partial signature for a participant at `index` (from zero). + /// Verify a partial signature for a participant for a particular session. + /// + /// The `verification_share` is usually derived from either [`SharedKey::verification_share`] or + /// [`PairedSecretShare::verification_share`]. /// /// ## Return Value /// /// Returns `true` if signature share is valid. pub fn verify_signature_share( &self, - shared_key: &SharedKey, + verification_share: VerificationShare, session: &CoordinatorSignSession, - index: PartyIndex, signature_share: Scalar, ) -> Result<(), SignatureShareInvalid> { + let X = verification_share.share_image; + let index = verification_share.index; + + // We need to know the verification share was generated against the session's key for + // further validity to have any meaning. + if verification_share.public_key != session.public_key() { + return Err(SignatureShareInvalid { index }); + } + let s = signature_share; - let lambda = poly::eval_basis_poly_at_0(index, session.nonces.keys().cloned()); + let lambda = + poly::eval_basis_poly_at_0(verification_share.index, session.nonces.keys().cloned()); let c = &session.challenge; let b = &session.binding_coeff; - let X = shared_key.verification_share(index); + debug_assert!( + session.parties().contains(&index), + "the party is not part of the session" + ); let [R1, R2] = session .nonces .get(&index) - .expect("verifying party index that is not part of frost signing coalition") + .ok_or(SignatureShareInvalid { index })? .0; let valid = g!(R1 + b * R2 + (c * lambda) * X - s * G).is_zero(); if valid { @@ -872,8 +888,12 @@ impl + Clone, NG> Frost { }); } for (party_index, signature_share) in &signature_shares { - self.verify_signature_share(shared_key, session, *party_index, *signature_share) - .map_err(VerifySignatureSharesError::Invalid)?; + self.verify_signature_share( + shared_key.verification_share(*party_index), + session, + *signature_share, + ) + .map_err(VerifySignatureSharesError::Invalid)?; } let signature = self diff --git a/schnorr_fun/src/frost/session.rs b/schnorr_fun/src/frost/session.rs index 9f12e1b0..a24d5e28 100644 --- a/schnorr_fun/src/frost/session.rs +++ b/schnorr_fun/src/frost/session.rs @@ -18,10 +18,12 @@ use secp256kfun::prelude::*; serde(crate = "crate::fun::serde") )] pub struct CoordinatorSignSession { + pub(crate) public_key: Point, pub(crate) binding_coeff: Scalar, - pub(crate) agg_binonce: binonce::Nonce, pub(crate) final_nonce: Point, pub(crate) challenge: Scalar, + + pub(crate) agg_binonce: binonce::Nonce, pub(crate) nonces: BTreeMap, } @@ -44,6 +46,11 @@ impl CoordinatorSignSession { pub fn final_nonce(&self) -> Point { self.final_nonce } + + /// The public key this session was started under + pub fn public_key(&self) -> Point { + self.public_key + } } /// The session that is used to sign a message. @@ -64,11 +71,12 @@ impl CoordinatorSignSession { )] pub struct PartySignSession { pub(crate) public_key: Point, - pub(crate) parties: BTreeSet>, - pub(crate) challenge: Scalar, - pub(crate) binonce_needs_negation: bool, pub(crate) binding_coeff: Scalar, pub(crate) final_nonce: Point, + pub(crate) challenge: Scalar, + + pub(crate) parties: BTreeSet>, + pub(crate) binonce_needs_negation: bool, } impl PartySignSession { diff --git a/schnorr_fun/src/frost/share.rs b/schnorr_fun/src/frost/share.rs index 9b8b82d3..b1c0f374 100644 --- a/schnorr_fun/src/frost/share.rs +++ b/schnorr_fun/src/frost/share.rs @@ -52,7 +52,7 @@ use secp256kfun::{poly, prelude::*}; pub struct SecretShare { /// The scalar index for this secret share, usually this is a small number but it can take any /// value (other than 0). - pub index: Scalar, + pub index: PartyIndex, /// The secret scalar which is the output of the polynomial evaluated at `index` pub share: Scalar, } @@ -68,11 +68,6 @@ impl SecretShare { poly::scalar::interpolate_and_eval_poly_at_0(&index_and_secret[..]) } - /// The verification share for this secret share. - pub fn verification_share(&self) -> Point { - g!(self.share * G) - } - /// Encodes the secret share to 64 bytes. The first 32 is the index and the second 32 is the /// secret. pub fn to_bytes(&self) -> [u8; 64] { @@ -90,6 +85,11 @@ impl SecretShare { share: Scalar::from_slice(&bytes[32..])?, }) } + + /// Get the image of the secret share. + pub fn share_image(&self) -> Point { + g!(self.share * G) + } } secp256kfun::impl_fromstr_deserialize! { @@ -243,7 +243,7 @@ impl PairedSecretShare { } } -impl PairedSecretShare { +impl PairedSecretShare { /// Create an XOnly secert share where the paired image is always an `EvenY` point. #[must_use] pub fn into_xonly(mut self) -> PairedSecretShare { @@ -257,6 +257,38 @@ impl PairedSecretShare { } } +impl PairedSecretShare { + /// Get the verification for the inner secret share. + pub fn verification_share(&self) -> VerificationShare { + VerificationShare { + index: self.index(), + share_image: self.secret_share.share_image(), + public_key: self.public_key, + } + } +} + +/// This is the public image of a [`SecretShare`]. You can't sign with it but you can verify +/// signature shares created by the secret share. +/// +/// A `VerificationShare` is the same as a [`share_image`] except it's generated against an `EvenY` +/// key that can actually have signatures verified against it. +/// +/// A `VerificationShare` is not designed to be persisted. The verification share will only be able +/// to verify signatures against the key that it was generated from. Tweaking a key with +/// `homomorphic_add` etc will invalidate the verification share. +/// +/// [`share_image`]: SecretShare::share_image +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct VerificationShare { + /// The index of the share in the secret sharing + pub index: PartyIndex, + /// The image of the secret share + pub share_image: Point, + /// The public key that this is a share of + pub public_key: Point, +} + #[cfg(feature = "share_backup")] mod share_backup { use super::*; diff --git a/schnorr_fun/src/frost/shared_key.rs b/schnorr_fun/src/frost/shared_key.rs index b0be4e5f..c16f368d 100644 --- a/schnorr_fun/src/frost/shared_key.rs +++ b/schnorr_fun/src/frost/shared_key.rs @@ -1,10 +1,13 @@ use core::{marker::PhantomData, ops::Deref}; +use super::{PairedSecretShare, PartyIndex, SecretShare, VerificationShare}; use alloc::vec::Vec; use secp256kfun::{poly, prelude::*}; -use super::{PairedSecretShare, PartyIndex, SecretShare}; -/// A polynomial +/// A polynomial where the first coefficient (constant term) is the image of a secret `Scalar` that +/// has been shared in a [Shamir's secret sharing] structure. +/// +/// [Shamir's secret sharing]: https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr( feature = "serde", @@ -24,19 +27,13 @@ pub struct SharedKey { } impl SharedKey { - /// The verification shares of each party in the key. - /// - /// The verification share is the image of their secret share. - pub fn verification_share(&self, index: PartyIndex) -> Point { - poly::point::eval(&self.point_polynomial, index) - } - /// "pair" a secret share that belongs to this shared key so you can keep track of tweaks to the /// public key and the secret share together. /// /// Returns `None` if the secret share is not a valid share of this key. pub fn pair_secret_share(&self, secret_share: SecretShare) -> Option> { - if self.verification_share(secret_share.index) != g!(secret_share.share * G) { + let share_image = poly::point::eval(&self.point_polynomial, secret_share.index); + if share_image != g!(secret_share.share * G) { return None; } @@ -131,23 +128,6 @@ impl SharedKey { SharedKey::from_inner(self.point_polynomial) } - /// Create a shared key from a subset of verification shares. - /// - /// If all the verification shares are correct and you have at least a threshold of them then - /// you'll get the right answer. If you put in a wrong share you won't get the right answer! - /// - /// ## Security - /// - /// ⚠ You can't just take any random points you want and pass them in here and hope it's secure. - /// They need to be from a securely generated key. - pub fn from_verification_shares( - shares: &[(PartyIndex, Point)], - ) -> SharedKey { - let poly = poly::point::interpolate(shares); - let poly = poly::point::normalize(poly); - SharedKey::from_inner(poly.collect()) - } - /// The public key that has been shared. /// /// This is using *public key* in a rather loose sense. Unless it's a `SharedKey` then it @@ -162,12 +142,12 @@ impl SharedKey { /// Encodes a `SharedKey` as the compressed encoding of each underlying polynomial coefficient /// - /// i.e. call [`Point::to_bytes`] on each coefficent starting with the constant term. Note that + /// i.e. call [`Point::to_bytes`] on each coefficient starting with the constant term. Note that /// even if it's a `SharedKey` the first coefficient (A.K.A the public key) will still be /// encoded as 33 bytes. /// /// ⚠ Unlike other secp256kfun things this doesn't exactly match the serde/bincode - /// implemenations which will length prefix the list of points. + /// implementations which will length prefix the list of points. pub fn to_bytes(&self) -> Vec { let mut bytes = Vec::with_capacity(self.point_polynomial.len() * 33); for coeff in &self.point_polynomial { @@ -227,7 +207,43 @@ impl SharedKey { SharedKey::from_inner(poly) } + + /// Create a shared key from a subset of share images. + /// + /// If all the share images are correct and you have at least a threshold of them then you'll + /// get the original shared key. If you put in a wrong share you won't get the right answer and + /// there will be no error. + /// + /// Note that a "share image" is not a concept that we really use in the core of this library + /// but you can get one from a share with [`SecretShare::share_image`]. + /// + /// ## Security + /// + /// ⚠ You can't just take any points you want and pass them in here and hope it's secure. + /// They need to be from a securely generated key. + pub fn from_share_images( + shares: &[(PartyIndex, Point)], + ) -> Self { + let poly = poly::point::interpolate(shares); + let poly = poly::point::normalize(poly); + SharedKey::from_inner(poly.collect()) + } } + +impl SharedKey { + /// The verification shares of each party in the key. + /// + /// The verification share is the image of their secret share. + pub fn verification_share(&self, index: PartyIndex) -> VerificationShare { + let share_image = poly::point::eval(&self.point_polynomial, index); + VerificationShare { + index, + share_image, + public_key: self.public_key(), + } + } +} + #[cfg(feature = "bincode")] impl crate::fun::bincode::Decode for SharedKey { fn decode( diff --git a/schnorr_fun/tests/frost_prop.rs b/schnorr_fun/tests/frost_prop.rs index a27fe369..42c060c2 100644 --- a/schnorr_fun/tests/frost_prop.rs +++ b/schnorr_fun/tests/frost_prop.rs @@ -112,9 +112,8 @@ proptest! { secret_nonces.remove(&secret_share.index()).unwrap() ); assert_eq!(proto.verify_signature_share( - &xonly_shared_key, + secret_share.verification_share(), &coord_signing_session, - secret_share.index(), sig), Ok(()) ); signatures.insert(secret_share.index(), sig); diff --git a/secp256kfun/src/poly.rs b/secp256kfun/src/poly.rs index fe009aa7..fb99deb5 100644 --- a/secp256kfun/src/poly.rs +++ b/secp256kfun/src/poly.rs @@ -53,11 +53,11 @@ pub mod scalar { pub fn interpolate_and_eval_poly_at_0( x_and_y: &[(Scalar, Scalar)], ) -> Scalar { - let indicies = x_and_y.iter().map(|(index, _)| *index); + let indices = x_and_y.iter().map(|(index, _)| *index); x_and_y .iter() .map(|(index, secret)| { - let lambda = eval_basis_poly_at_0(*index, indicies.clone()); + let lambda = eval_basis_poly_at_0(*index, indices.clone()); s!(secret * lambda) }) .fold(s!(0), |interpolated_poly, scaled_basis_poly| { @@ -189,7 +189,7 @@ pub mod point { /// Find the coefficients of the polynomial that interpolates a set of points (index, point). /// - /// Panics if the indicies are not unique. + /// Panics if the indices are not unique. /// /// A vector with a tail of zero coefficients means the interpolation was overdetermined. #[allow(clippy::type_complexity)] diff --git a/secp256kfun/tests/poly.rs b/secp256kfun/tests/poly.rs index 1284fb1c..f0d341ca 100644 --- a/secp256kfun/tests/poly.rs +++ b/secp256kfun/tests/poly.rs @@ -58,8 +58,8 @@ fn test_add_scalar_poly() { #[test] fn test_recover_public_poly() { let poly = vec![g!(1 * G), g!(2 * G), g!(3 * G)]; - let indicies = vec![s!(1).public(), s!(3).public(), s!(2).public()]; - let points = indicies + let indices = vec![s!(1).public(), s!(3).public(), s!(2).public()]; + let points = indices .clone() .into_iter() .map(|index| { @@ -80,14 +80,14 @@ fn test_recover_public_poly() { #[test] fn test_recover_overdetermined_poly() { let poly = vec![g!(1 * G), g!(2 * G), g!(3 * G)]; - let indicies = vec![ + let indices = vec![ s!(1).public(), s!(2).public(), s!(3).public(), s!(4).public(), s!(5).public(), ]; - let points = indicies + let points = indices .clone() .into_iter() .map(|index| (index, poly::point::eval(&poly, index.public()).normalize())) @@ -114,10 +114,10 @@ fn test_recover_zero_poly() { #[test] fn test_reconstruct_shared_secret() { - let indicies = vec![s!(1).public(), s!(2).public(), s!(3).public()]; + let indices = vec![s!(1).public(), s!(2).public(), s!(3).public()]; let scalar_poly = vec![s!(42), s!(53), s!(64)]; - let secret_shares: Vec<_> = indicies + let secret_shares: Vec<_> = indices .clone() .into_iter() .map(|index| (index, poly::scalar::eval(&scalar_poly, index))) From 84ac38b593e8e6a9dcc9df1cc398d803d6eac554 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Thu, 8 Aug 2024 17:23:36 +1000 Subject: [PATCH 15/15] =?UTF-8?q?[=E2=9D=84]=20Move=20signature=20methods?= =?UTF-8?q?=20onto=20the=20sessions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There's no need for the `Frost` type to do signing/verification. It's needed to start the session but not after. --- schnorr_fun/src/frost/mod.rs | 192 +------------------------------ schnorr_fun/src/frost/session.rs | 187 +++++++++++++++++++++++++++++- schnorr_fun/tests/frost_prop.rs | 10 +- 3 files changed, 191 insertions(+), 198 deletions(-) diff --git a/schnorr_fun/src/frost/mod.rs b/schnorr_fun/src/frost/mod.rs index 82f426fc..7beaae46 100644 --- a/schnorr_fun/src/frost/mod.rs +++ b/schnorr_fun/src/frost/mod.rs @@ -108,13 +108,12 @@ //! // start a sign session with these nonces for a message //! let sign_session = frost.party_sign_session(xonly_my_secret_share.public_key(),parties, agg_binonce, message); //! // create a partial signature using our secret share and secret nonce -//! let my_sig_share = frost.sign(&sign_session, &xonly_my_secret_share, my_nonce); -//! # let sig_share3 = frost.sign(&sign_session, &xonly_secret_share3, nonce3); +//! let my_sig_share = sign_session.sign(&xonly_my_secret_share, my_nonce); +//! # let sig_share3 = sign_session.sign(&xonly_secret_share3, nonce3); //! // 🐙 receive the partial signature(s) from the other participant(s). //! // 🐙 combine signature shares into a single signature that is valid under the FROST key -//! let combined_sig = frost.verify_and_combine_signature_shares( +//! let combined_sig = coord_session.verify_and_combine_signature_shares( //! &xonly_shared_key, -//! &coord_session, //! [(my_index, my_sig_share), (party_index3, sig_share3)].into() //! )?; //! assert!(frost.schnorr.verify( @@ -740,7 +739,6 @@ impl + Clone, NG> Frost { /// The corodinator must have collected nonces from each of the signers and pass them in as `nonces`. /// From there /// - /// /// # Panics /// /// If the number of nonces is less than the threshold. @@ -792,149 +790,6 @@ impl + Clone, NG> Frost { ) .public() } - - /// Generates a signature share under the frost key using a secret share. - /// - /// The `secret_share` is taken as a `PairedSecretShare` to guarantee that the secret is aligned with an `EvenY` point. - /// - /// ## Return value - /// - /// Returns a signature share - /// - /// ## Panics - /// - /// Panics if the `secret_share` was not part of the signing session - pub fn sign( - &self, - session: &PartySignSession, - secret_share: &PairedSecretShare, - secret_nonce: NonceKeyPair, - ) -> Scalar { - if session.public_key != secret_share.public_key() { - panic!("the share's shared key is not the same as the shared key of the session"); - } - let secret_share = secret_share.secret_share(); - let lambda = - poly::eval_basis_poly_at_0(secret_share.index, session.parties.iter().cloned()); - let [mut r1, mut r2] = secret_nonce.secret; - r1.conditional_negate(session.binonce_needs_negation); - r2.conditional_negate(session.binonce_needs_negation); - - let b = &session.binding_coeff; - let x = secret_share.share; - let c = &session.challenge; - s!(r1 + (r2 * b) + lambda * x * c).public() - } - - /// Verify a partial signature for a participant for a particular session. - /// - /// The `verification_share` is usually derived from either [`SharedKey::verification_share`] or - /// [`PairedSecretShare::verification_share`]. - /// - /// ## Return Value - /// - /// Returns `true` if signature share is valid. - pub fn verify_signature_share( - &self, - verification_share: VerificationShare, - session: &CoordinatorSignSession, - signature_share: Scalar, - ) -> Result<(), SignatureShareInvalid> { - let X = verification_share.share_image; - let index = verification_share.index; - - // We need to know the verification share was generated against the session's key for - // further validity to have any meaning. - if verification_share.public_key != session.public_key() { - return Err(SignatureShareInvalid { index }); - } - - let s = signature_share; - let lambda = - poly::eval_basis_poly_at_0(verification_share.index, session.nonces.keys().cloned()); - let c = &session.challenge; - let b = &session.binding_coeff; - debug_assert!( - session.parties().contains(&index), - "the party is not part of the session" - ); - let [R1, R2] = session - .nonces - .get(&index) - .ok_or(SignatureShareInvalid { index })? - .0; - let valid = g!(R1 + b * R2 + (c * lambda) * X - s * G).is_zero(); - if valid { - Ok(()) - } else { - Err(SignatureShareInvalid { index }) - } - } - - /// Combines signature shares from each party into the final signature. - /// - /// You can use this instead of calling [`verify_signature_share`] on each share. - /// - /// [`verify_signature_share`]: Self::verify_signature_share - pub fn verify_and_combine_signature_shares( - &self, - shared_key: &SharedKey, - session: &CoordinatorSignSession, - signature_shares: BTreeMap>, - ) -> Result { - if signature_shares.len() < shared_key.threshold() { - return Err(VerifySignatureSharesError::NotEnough { - missing: shared_key.threshold() - signature_shares.len(), - }); - } - for (party_index, signature_share) in &signature_shares { - self.verify_signature_share( - shared_key.verification_share(*party_index), - session, - *signature_share, - ) - .map_err(VerifySignatureSharesError::Invalid)?; - } - - let signature = self - .combine_signature_shares(session.final_nonce(), signature_shares.values().cloned()); - - Ok(signature) - } - - /// Combine a vector of signatures shares into an aggregate signature given the final nonce. - /// - /// You can get `final_nonce` from either of the [`CoordinatorSignSession`] or the [`PartySignSession`]. - /// - /// This method does not check the validity of the `signature_shares` - /// but if you have verified each signature share - /// individually the output will be a valid siganture under the `frost_key` and message provided - /// when starting the session. - /// - /// Alternatively you can use [`verify_and_combine_signature_shares`] which checks and combines - /// the signature shares. - /// - /// ## Return value - /// - /// Returns a schnorr [`Signature`] on the message - /// - /// [`CoordinatorSignSession`]: CoordinatorSignSession::final_nonce - /// [`PartySignSession`]: PartySignSession::final_nonce - /// [`verify_and_combine_signature_shares`]: Self::verify_and_combine_signature_shares - pub fn combine_signature_shares( - &self, - final_nonce: Point, - signature_shares: impl IntoIterator>, - ) -> Signature { - let sum_s = signature_shares - .into_iter() - .reduce(|acc, partial_sig| s!(acc + partial_sig).public()) - .unwrap_or(Scalar::zero()); - Signature { - R: final_nonce, - s: sum_s, - } - } } /// Constructor for a Frost instance using deterministic nonce generation. @@ -982,47 +837,6 @@ where Frost::default() } -/// Error for a signature share being invalid -#[derive(Clone, Debug, PartialEq)] -pub struct SignatureShareInvalid { - index: PartyIndex, -} - -/// Error returned by [`Frost::verify_and_combine_signature_shares`] -#[derive(Clone, Debug, PartialEq)] -pub enum VerifySignatureSharesError { - /// Not enough signature shares to compelte the signature - NotEnough { - /// How many are missing - missing: usize, - }, - /// One of the signature shars was invalid - Invalid(SignatureShareInvalid), -} - -impl core::fmt::Display for VerifySignatureSharesError { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - match self { - VerifySignatureSharesError::NotEnough { missing } => { - write!(f, "not enough signature shares have been collected to finish the signature. You need {missing} more.") - } - VerifySignatureSharesError::Invalid(invalid) => write!(f, "{invalid}"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for VerifySignatureSharesError {} - -impl core::fmt::Display for SignatureShareInvalid { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "signature share from party {} was invalid", self.index) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for SignatureShareInvalid {} - #[cfg(test)] mod test { diff --git a/schnorr_fun/src/frost/session.rs b/schnorr_fun/src/frost/session.rs index a24d5e28..87cd9b29 100644 --- a/schnorr_fun/src/frost/session.rs +++ b/schnorr_fun/src/frost/session.rs @@ -1,7 +1,9 @@ -use crate::{binonce, frost::PartyIndex}; +use crate::{binonce, frost::PartyIndex, Signature}; use alloc::collections::{BTreeMap, BTreeSet}; -use secp256kfun::prelude::*; -/// A FROST signing session +use secp256kfun::{poly, prelude::*}; + +use super::{NonceKeyPair, PairedSecretShare, SharedKey, VerificationShare}; +/// A FROST signing session used to *verify* signatures. /// /// Created using [`coordinator_sign_session`]. /// @@ -51,6 +53,113 @@ impl CoordinatorSignSession { pub fn public_key(&self) -> Point { self.public_key } + + /// Verify a partial signature for a participant for a particular session. + /// + /// The `verification_share` is usually derived from either [`SharedKey::verification_share`] or + /// [`PairedSecretShare::verification_share`]. + /// + /// ## Return Value + /// + /// Returns `true` if signature share is valid. + pub fn verify_signature_share( + &self, + verification_share: VerificationShare, + signature_share: Scalar, + ) -> Result<(), SignatureShareInvalid> { + let X = verification_share.share_image; + let index = verification_share.index; + + // We need to know the verification share was generated against the session's key for + // further validity to have any meaning. + if verification_share.public_key != self.public_key() { + return Err(SignatureShareInvalid { index }); + } + + let s = signature_share; + let lambda = + poly::eval_basis_poly_at_0(verification_share.index, self.nonces.keys().cloned()); + let c = &self.challenge; + let b = &self.binding_coeff; + debug_assert!( + self.parties().contains(&index), + "the party is not part of the session" + ); + let [R1, R2] = self + .nonces + .get(&index) + .ok_or(SignatureShareInvalid { index })? + .0; + let valid = g!(R1 + b * R2 + (c * lambda) * X - s * G).is_zero(); + if valid { + Ok(()) + } else { + Err(SignatureShareInvalid { index }) + } + } + + /// Combines signature shares from each party into the final signature. + /// + /// You can use this instead of calling [`verify_signature_share`] on each share. + /// + /// [`verify_signature_share`]: Self::verify_signature_share + pub fn verify_and_combine_signature_shares( + &self, + shared_key: &SharedKey, + signature_shares: BTreeMap>, + ) -> Result { + if signature_shares.len() < shared_key.threshold() { + return Err(VerifySignatureSharesError::NotEnough { + missing: shared_key.threshold() - signature_shares.len(), + }); + } + for (party_index, signature_share) in &signature_shares { + self.verify_signature_share( + shared_key.verification_share(*party_index), + *signature_share, + ) + .map_err(VerifySignatureSharesError::Invalid)?; + } + + let signature = + self.combine_signature_shares(self.final_nonce(), signature_shares.values().cloned()); + + Ok(signature) + } + + /// Combine a vector of signatures shares into an aggregate signature given the final nonce. + /// + /// You can get `final_nonce` from either of the [`CoordinatorSignSession`] or the [`PartySignSession`]. + /// + /// This method does not check the validity of the `signature_shares` + /// but if you have verified each signature share + /// individually the output will be a valid siganture under the `frost_key` and message provided + /// when starting the session. + /// + /// Alternatively you can use [`verify_and_combine_signature_shares`] which checks and combines + /// the signature shares. + /// + /// ## Return value + /// + /// Returns a schnorr [`Signature`] on the message + /// + /// [`CoordinatorSignSession`]: CoordinatorSignSession::final_nonce + /// [`PartySignSession`]: PartySignSession::final_nonce + /// [`verify_and_combine_signature_shares`]: Self::verify_and_combine_signature_shares + pub fn combine_signature_shares( + &self, + final_nonce: Point, + signature_shares: impl IntoIterator>, + ) -> Signature { + let sum_s = signature_shares + .into_iter() + .reduce(|acc, partial_sig| s!(acc + partial_sig).public()) + .unwrap_or(Scalar::zero()); + Signature { + R: final_nonce, + s: sum_s, + } + } } /// The session that is used to sign a message. @@ -89,4 +198,76 @@ impl PartySignSession { pub fn public_key(&self) -> Point { self.public_key } + + /// Generates a signature share under the frost key using a secret share. + /// + /// The `secret_share` is taken as a `PairedSecretShare` to guarantee that the secret is aligned with an `EvenY` point. + /// + /// ## Return value + /// + /// Returns a signature share + /// + /// ## Panics + /// + /// Panics if the `secret_share` was not part of the signing session + pub fn sign( + &self, + secret_share: &PairedSecretShare, + secret_nonce: NonceKeyPair, + ) -> Scalar { + if self.public_key != secret_share.public_key() { + panic!("the share's shared key is not the same as the shared key of the session"); + } + let secret_share = secret_share.secret_share(); + let lambda = poly::eval_basis_poly_at_0(secret_share.index, self.parties.iter().cloned()); + let [mut r1, mut r2] = secret_nonce.secret; + r1.conditional_negate(self.binonce_needs_negation); + r2.conditional_negate(self.binonce_needs_negation); + + let b = &self.binding_coeff; + let x = secret_share.share; + let c = &self.challenge; + s!(r1 + (r2 * b) + lambda * x * c).public() + } +} + +/// Error for a signature share being invalid +#[derive(Clone, Debug, PartialEq)] +pub struct SignatureShareInvalid { + index: PartyIndex, } + +impl core::fmt::Display for SignatureShareInvalid { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "signature share from party {} was invalid", self.index) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for SignatureShareInvalid {} + +/// Error returned by [`CoordinatorSignSession::verify_and_combine_signature_shares`] +#[derive(Clone, Debug, PartialEq)] +pub enum VerifySignatureSharesError { + /// Not enough signature shares to compelte the signature + NotEnough { + /// How many are missing + missing: usize, + }, + /// One of the signature shars was invalid + Invalid(SignatureShareInvalid), +} + +impl core::fmt::Display for VerifySignatureSharesError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + match self { + VerifySignatureSharesError::NotEnough { missing } => { + write!(f, "not enough signature shares have been collected to finish the signature. You need {missing} more.") + } + VerifySignatureSharesError::Invalid(invalid) => write!(f, "{invalid}"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for VerifySignatureSharesError {} diff --git a/schnorr_fun/tests/frost_prop.rs b/schnorr_fun/tests/frost_prop.rs index 42c060c2..20e426df 100644 --- a/schnorr_fun/tests/frost_prop.rs +++ b/schnorr_fun/tests/frost_prop.rs @@ -106,24 +106,22 @@ proptest! { let mut signatures = BTreeMap::default(); for secret_share in secret_shares_of_signers { - let sig = proto.sign( - &party_signing_session, + let sig = party_signing_session.sign( &secret_share, secret_nonces.remove(&secret_share.index()).unwrap() ); - assert_eq!(proto.verify_signature_share( + assert_eq!(coord_signing_session.verify_signature_share( secret_share.verification_share(), - &coord_signing_session, sig), Ok(()) ); signatures.insert(secret_share.index(), sig); } - let combined_sig = proto.combine_signature_shares( + let combined_sig = coord_signing_session.combine_signature_shares( coord_signing_session.final_nonce(), signatures.values().cloned() ); - assert_eq!(proto.verify_and_combine_signature_shares(&xonly_shared_key, &coord_signing_session, signatures), Ok(combined_sig)); + assert_eq!(coord_signing_session.verify_and_combine_signature_shares(&xonly_shared_key, signatures), Ok(combined_sig)); assert!(proto.schnorr.verify( &xonly_shared_key.public_key(), message,