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)))