diff --git a/crypto/dkg/src/lib.rs b/crypto/dkg/src/lib.rs index 266628f0..b4a290d2 100644 --- a/crypto/dkg/src/lib.rs +++ b/crypto/dkg/src/lib.rs @@ -22,6 +22,9 @@ use ciphersuite::{ /// Encryption types and utilities used to secure DKG messages. pub mod encryption; +mod musig; +pub use musig::musig; + /// The distributed key generation protocol described in the /// [FROST paper](https://eprint.iacr.org/2020/852). pub mod frost; diff --git a/crypto/dkg/src/musig.rs b/crypto/dkg/src/musig.rs new file mode 100644 index 00000000..452a3d38 --- /dev/null +++ b/crypto/dkg/src/musig.rs @@ -0,0 +1,92 @@ +use core::ops::Deref; +use std::collections::{HashSet, HashMap}; + +use zeroize::Zeroizing; + +use transcript::{Transcript, RecommendedTranscript}; + +use ciphersuite::{ + group::{Group, GroupEncoding}, + Ciphersuite, +}; + +use crate::{Participant, DkgError, ThresholdParams, ThresholdCore, lagrange}; + +/// A n-of-n non-interactive DKG which does not guarantee the usability of the resulting key. +/// +/// Creating a key with duplicated public keys returns an error. +pub fn musig( + private_key: &Zeroizing, + keys: &[C::G], +) -> Result, DkgError<()>> { + if keys.is_empty() { + Err(DkgError::InvalidSigningSet)?; + } + // Too many signers + let keys_len = u16::try_from(keys.len()).map_err(|_| DkgError::InvalidSigningSet)?; + + // Duplicated public keys + if keys.iter().map(|key| key.to_bytes().as_ref().to_vec()).collect::>().len() != + keys.len() + { + Err(DkgError::InvalidSigningSet)?; + } + + let our_pub_key = C::generator() * private_key.deref(); + let Some(pos) = keys.iter().position(|key| *key == our_pub_key) else { + // Not present in signing set + Err(DkgError::InvalidSigningSet)? + }; + let params = ThresholdParams::new( + keys_len, + keys_len, + // These errors shouldn't be possible, as pos is bounded to len - 1 + // Since len is prior guaranteed to be within u16::MAX, pos + 1 must also be + Participant::new((pos + 1).try_into().map_err(|_| DkgError::InvalidSigningSet)?) + .ok_or(DkgError::InvalidSigningSet)?, + )?; + + // Calculate the binding factor per-key + let mut transcript = RecommendedTranscript::new(b"DKG MuSig v0.5"); + transcript.domain_separate(b"musig_binding_factors"); + for key in keys { + transcript.append_message(b"key", key.to_bytes()); + } + + let mut binding = Vec::with_capacity(keys.len()); + for i in 1 ..= keys_len { + let mut transcript = transcript.clone(); + transcript.append_message(b"participant", i.to_le_bytes()); + binding + .push(C::hash_to_F(b"DKG-MuSig-binding_factor", &transcript.challenge(b"binding_factor"))); + } + + // Multiply our private key by our binding factor + let mut secret_share = private_key.clone(); + *secret_share *= binding[pos]; + + // Calculate verification shares + let mut verification_shares = HashMap::new(); + // When this library generates shares for a specific signing set, it applies the lagrange + // coefficient + // Since this is a n-of-n scheme, there's only one possible signing set, and one possible + // lagrange factor + // Define the group key as the sum of all verification shares, post-lagrange + // While we could invert our lagrange factor and multiply it by our secret share, so the group + // key wasn't post-lagrange, the inversion is ~300 multiplications and we'd have to apply similar + // inversions + multiplications to all verification shares + // Accordingly, it'd never be more performant, though it would simplify group key calculation + let included = (1 ..= keys_len) + // This error also shouldn't be possible, for the same reasons as documented above + .map(|l| Participant::new(l).ok_or(DkgError::InvalidSigningSet)) + .collect::, _>>()?; + let mut group_key = C::G::identity(); + for (l, p) in included.iter().enumerate() { + let verification_share = keys[l] * binding[l]; + group_key += verification_share * lagrange::(*p, &included); + verification_shares.insert(*p, verification_share); + } + debug_assert_eq!(C::generator() * secret_share.deref(), verification_shares[¶ms.i()]); + + Ok(ThresholdCore { params, secret_share, group_key, verification_shares }) +} diff --git a/crypto/dkg/src/tests/mod.rs b/crypto/dkg/src/tests/mod.rs index 915468ff..99386315 100644 --- a/crypto/dkg/src/tests/mod.rs +++ b/crypto/dkg/src/tests/mod.rs @@ -1,11 +1,15 @@ use core::ops::Deref; use std::collections::HashMap; +use zeroize::Zeroizing; use rand_core::{RngCore, CryptoRng}; use ciphersuite::{group::ff::Field, Ciphersuite}; -use crate::{Participant, ThresholdCore, ThresholdKeys, lagrange}; +use crate::{Participant, ThresholdCore, ThresholdKeys, lagrange, musig as musig_fn}; + +mod musig; +pub use musig::test_musig; /// FROST key generation testing utility. pub mod frost; @@ -63,6 +67,28 @@ pub fn key_gen( res } +/// Generate MuSig keys for tests. +pub fn musig_key_gen( + rng: &mut R, +) -> HashMap> { + let mut keys = vec![]; + let mut pub_keys = vec![]; + for _ in 0 .. PARTICIPANTS { + let key = Zeroizing::new(C::F::random(&mut *rng)); + pub_keys.push(C::generator() * *key); + keys.push(key); + } + + let mut res = HashMap::new(); + for key in keys { + let these_keys = musig_fn::(&key, &pub_keys).unwrap(); + res.insert(these_keys.params().i(), ThresholdKeys::new(these_keys)); + } + + assert_eq!(C::generator() * recover_key(&res), res[&Participant(1)].group_key()); + res +} + /// Run the test suite on a ciphersuite. pub fn test_ciphersuite(rng: &mut R) { key_gen::<_, C>(rng); diff --git a/crypto/dkg/src/tests/musig.rs b/crypto/dkg/src/tests/musig.rs new file mode 100644 index 00000000..1b037717 --- /dev/null +++ b/crypto/dkg/src/tests/musig.rs @@ -0,0 +1,61 @@ +use std::collections::HashMap; + +use zeroize::Zeroizing; +use rand_core::{RngCore, CryptoRng}; + +use ciphersuite::{group::ff::Field, Ciphersuite}; + +use crate::{ + ThresholdKeys, musig, + tests::{PARTICIPANTS, recover_key}, +}; + +/// Tests MuSig key generation. +pub fn test_musig(rng: &mut R) { + let mut keys = vec![]; + let mut pub_keys = vec![]; + for _ in 0 .. PARTICIPANTS { + let key = Zeroizing::new(C::F::random(&mut *rng)); + pub_keys.push(C::generator() * *key); + keys.push(key); + } + + // Empty signing set + assert!(musig::(&Zeroizing::new(C::F::ZERO), &[]).is_err()); + // Signing set we're not part of + assert!(musig::(&Zeroizing::new(C::F::ZERO), &[C::generator()]).is_err()); + + // Test with n keys + { + let mut created_keys = HashMap::new(); + let mut verification_shares = HashMap::new(); + let mut group_key = None; + for (i, key) in keys.iter().enumerate() { + let these_keys = musig::(key, &pub_keys).unwrap(); + assert_eq!(these_keys.params().t(), PARTICIPANTS); + assert_eq!(these_keys.params().n(), PARTICIPANTS); + assert_eq!(usize::from(these_keys.params().i().0), i + 1); + + verification_shares + .insert(these_keys.params().i(), C::generator() * **these_keys.secret_share()); + + if group_key.is_none() { + group_key = Some(these_keys.group_key()); + } + assert_eq!(these_keys.group_key(), group_key.unwrap()); + + created_keys.insert(these_keys.params().i(), ThresholdKeys::new(these_keys)); + } + + for keys in created_keys.values() { + assert_eq!(keys.verification_shares(), verification_shares); + } + + assert_eq!(C::generator() * recover_key(&created_keys), group_key.unwrap()); + } +} + +#[test] +fn musig_literal() { + test_musig::<_, ciphersuite::Ristretto>(&mut rand_core::OsRng) +} diff --git a/crypto/frost/src/tests/mod.rs b/crypto/frost/src/tests/mod.rs index fd271409..f20196ce 100644 --- a/crypto/frost/src/tests/mod.rs +++ b/crypto/frost/src/tests/mod.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use rand_core::{RngCore, CryptoRng}; -pub use dkg::tests::{key_gen, recover_key}; +pub use dkg::tests::{key_gen, musig_key_gen, recover_key}; use crate::{ Curve, Participant, ThresholdKeys, FrostError, @@ -24,7 +24,7 @@ mod literal; /// Constant amount of participants to use when testing. pub const PARTICIPANTS: u16 = 5; /// Constant threshold of participants to use when signing. -pub const THRESHOLD: u16 = ((PARTICIPANTS / 3) * 2) + 1; +pub const THRESHOLD: u16 = ((PARTICIPANTS * 2) / 3) + 1; /// Clone a map without a specific value. pub fn clone_without( @@ -192,17 +192,31 @@ pub fn sign( ) } -/// Test a basic Schnorr signature. -pub fn test_schnorr>(rng: &mut R) { +/// Test a basic Schnorr signature with the provided keys. +pub fn test_schnorr_with_keys>( + rng: &mut R, + keys: HashMap>, +) { const MSG: &[u8] = b"Hello, World!"; - let keys = key_gen(&mut *rng); let machines = algorithm_machines(&mut *rng, IetfSchnorr::::ietf(), &keys); let sig = sign(&mut *rng, IetfSchnorr::::ietf(), keys.clone(), machines, MSG); let group_key = keys[&Participant::new(1).unwrap()].group_key(); assert!(sig.verify(group_key, H::hram(&sig.R, &group_key, MSG))); } +/// Test a basic Schnorr signature. +pub fn test_schnorr>(rng: &mut R) { + let keys = key_gen(&mut *rng); + test_schnorr_with_keys::<_, _, H>(&mut *rng, keys) +} + +/// Test a basic Schnorr signature, yet with MuSig. +pub fn test_musig_schnorr>(rng: &mut R) { + let keys = musig_key_gen(&mut *rng); + test_schnorr_with_keys::<_, _, H>(&mut *rng, keys) +} + /// Test an offset Schnorr signature. pub fn test_offset_schnorr>(rng: &mut R) { const MSG: &[u8] = b"Hello, World!"; @@ -248,6 +262,7 @@ pub fn test_schnorr_blame>(rng: &mu /// Run a variety of tests against a ciphersuite. pub fn test_ciphersuite>(rng: &mut R) { test_schnorr::(rng); + test_musig_schnorr::(rng); test_offset_schnorr::(rng); test_schnorr_blame::(rng);