From 454b73aec321a91502d19b85235aa4904a2922df Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sat, 13 Aug 2022 08:49:38 -0400 Subject: [PATCH] Add FROST key promotion Closes https://github.com/serai-dex/serai/issues/72. Adds a trait, with a commented impl for a semi-unsafe niche feature, which will be used in https://github.com/serai-dex/serai/issues/73. --- crypto/dleq/src/lib.rs | 2 +- crypto/frost/Cargo.toml | 2 +- crypto/frost/src/lib.rs | 3 + crypto/frost/src/promote.rs | 142 ++++++++++++++++++++++++++++++ crypto/frost/src/tests/mod.rs | 1 + crypto/frost/src/tests/promote.rs | 125 ++++++++++++++++++++++++++ crypto/frost/src/tests/schnorr.rs | 10 ++- crypto/frost/src/tests/vectors.rs | 5 +- 8 files changed, 283 insertions(+), 7 deletions(-) create mode 100644 crypto/frost/src/promote.rs create mode 100644 crypto/frost/src/tests/promote.rs diff --git a/crypto/dleq/src/lib.rs b/crypto/dleq/src/lib.rs index ddc09bd5..3492bba5 100644 --- a/crypto/dleq/src/lib.rs +++ b/crypto/dleq/src/lib.rs @@ -60,7 +60,7 @@ pub enum DLEqError { InvalidProof, } -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct DLEqProof { c: G::Scalar, s: G::Scalar, diff --git a/crypto/frost/Cargo.toml b/crypto/frost/Cargo.toml index dfed181a..ae6706f7 100644 --- a/crypto/frost/Cargo.toml +++ b/crypto/frost/Cargo.toml @@ -27,7 +27,7 @@ p256 = { version = "0.11", features = ["arithmetic", "bits", "hash2curve"], opti k256 = { version = "0.11", features = ["arithmetic", "bits", "hash2curve"], optional = true } dalek-ff-group = { path = "../dalek-ff-group", version = "0.1", optional = true } -transcript = { package = "flexible-transcript", path = "../transcript", version = "0.1" } +transcript = { package = "flexible-transcript", path = "../transcript", features = ["recommended"], version = "0.1" } multiexp = { path = "../multiexp", version = "0.2", features = ["batch"] } diff --git a/crypto/frost/src/lib.rs b/crypto/frost/src/lib.rs index 57a567ca..d41600ce 100644 --- a/crypto/frost/src/lib.rs +++ b/crypto/frost/src/lib.rs @@ -14,7 +14,10 @@ mod schnorr; pub mod curve; use curve::Curve; + pub mod key_gen; +pub mod promote; + pub mod algorithm; pub mod sign; diff --git a/crypto/frost/src/promote.rs b/crypto/frost/src/promote.rs new file mode 100644 index 00000000..eba79ca3 --- /dev/null +++ b/crypto/frost/src/promote.rs @@ -0,0 +1,142 @@ +use std::{ + marker::PhantomData, + io::{self, Read, Write}, + sync::Arc, + collections::HashMap, +}; + +use rand_core::{RngCore, CryptoRng}; + +use group::GroupEncoding; + +use transcript::{Transcript, RecommendedTranscript}; +use dleq::DLEqProof; + +use crate::{ + curve::{CurveError, Curve}, + FrostError, FrostCore, FrostKeys, validate_map, +}; + +/// Promote a set of keys to another Curve definition +pub trait CurvePromote { + #[doc(hidden)] + #[allow(non_snake_case)] + fn _bound_C2(_c2: C2) { + panic!() + } + + fn promote(self) -> FrostKeys; +} + +// Implement promotion to different ciphersuites, panicking if the generators are different +// Commented due to lack of practical benefit. While it'd have interoperability benefits, those +// would have their own DKG process which isn't compatible anyways. This becomes unsafe code +// that'll never be used but we're bound to support +/* +impl CurvePromote for FrostKeys +where + C2: Curve, +{ + fn promote(self) -> FrostKeys { + assert_eq!(C::GENERATOR, C2::GENERATOR); + + FrostKeys { + core: Arc::new(FrostCore { + params: self.core.params, + secret_share: self.core.secret_share, + group_key: self.core.group_key, + verification_shares: self.core.verification_shares(), + }), + offset: None, + } + } +} +*/ + +fn transcript(key: G, i: u16) -> RecommendedTranscript { + let mut transcript = RecommendedTranscript::new(b"FROST Generator Update"); + transcript.append_message(b"group_key", key.to_bytes().as_ref()); + transcript.append_message(b"participant", &i.to_be_bytes()); + transcript +} + +#[derive(Clone, Copy)] +pub struct GeneratorProof { + share: C::G, + proof: DLEqProof, +} + +impl GeneratorProof { + pub fn serialize(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(self.share.to_bytes().as_ref())?; + self.proof.serialize(writer) + } + + pub fn deserialize(reader: &mut R) -> Result, CurveError> { + Ok(GeneratorProof { + share: C::read_G(reader)?, + proof: DLEqProof::deserialize(reader).map_err(|_| CurveError::InvalidScalar)?, + }) + } +} + +pub struct GeneratorPromotion { + base: FrostKeys, + proof: GeneratorProof, + _c2: PhantomData, +} + +/// Promote a set of keys from one generator to another +// The linear DLEq proofs are much more efficient than an exponential key gen +impl GeneratorPromotion +where + C2: Curve, +{ + pub fn promote( + rng: &mut R, + base: FrostKeys, + ) -> (GeneratorPromotion, GeneratorProof) { + // Do a DLEqProof for the new generator + let proof = GeneratorProof { + share: C2::generator() * base.secret_share(), + proof: DLEqProof::prove( + rng, + &mut transcript(base.core.group_key(), base.params().i), + &[C1::generator(), C2::generator()], + base.secret_share(), + ), + }; + + (GeneratorPromotion { base, proof, _c2: PhantomData:: }, proof) + } + + pub fn complete( + self, + proofs: &HashMap>, + ) -> Result, FrostError> { + let params = self.base.params(); + validate_map(proofs, &(1 ..= params.n).collect::>(), params.i)?; + + let original_shares = self.base.verification_shares(); + + let mut verification_shares = HashMap::new(); + verification_shares.insert(params.i, self.proof.share); + for (i, proof) in proofs { + let i = *i; + proof + .proof + .verify( + &mut transcript(self.base.core.group_key(), i), + &[C1::generator(), C2::generator()], + &[original_shares[&i], proof.share], + ) + .map_err(|_| FrostError::InvalidProofOfKnowledge(i))?; + verification_shares.insert(i, proof.share); + } + + Ok(FrostKeys { + core: Arc::new(FrostCore::new(params, self.base.secret_share(), verification_shares)), + offset: None, + }) + } +} diff --git a/crypto/frost/src/tests/mod.rs b/crypto/frost/src/tests/mod.rs index fcbc2ccf..8f25aab9 100644 --- a/crypto/frost/src/tests/mod.rs +++ b/crypto/frost/src/tests/mod.rs @@ -14,6 +14,7 @@ use crate::{ // Test suites for public usage pub mod curve; pub mod schnorr; +pub mod promote; pub mod vectors; // Literal test definitions to run during `cargo test` diff --git a/crypto/frost/src/tests/promote.rs b/crypto/frost/src/tests/promote.rs new file mode 100644 index 00000000..5d184892 --- /dev/null +++ b/crypto/frost/src/tests/promote.rs @@ -0,0 +1,125 @@ +use std::{marker::PhantomData, collections::HashMap}; + +use rand_core::{RngCore, CryptoRng}; + +use zeroize::Zeroize; + +use group::Group; + +use crate::{ + Curve, // FrostKeys, + promote::{GeneratorPromotion /* CurvePromote */}, + tests::{clone_without, key_gen, schnorr::sign_core}, +}; + +/* +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +struct AltFunctions { + _curve: PhantomData, +} + +impl Curve for AltFunctions { + type F = C::F; + type G = C::G; + + const ID: &'static [u8] = b"alt_functions"; + + fn generator() -> Self::G { + C::generator() + } + + fn hash_msg(msg: &[u8]) -> Vec { + C::hash_msg(&[msg, b"alt"].concat()) + } + + fn hash_binding_factor(binding: &[u8]) -> Self::F { + C::hash_to_F(b"rho_alt", binding) + } + + fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F { + C::hash_to_F(&[dst, b"alt"].concat(), msg) + } +} + +// Test promotion of FROST keys to another set of functions for interoperability +fn test_ciphersuite_promotion(rng: &mut R) { + let keys = key_gen::<_, C>(&mut *rng); + for keys in keys.values() { + let promoted: FrostKeys> = keys.clone().promote(); + // Verify equivalence via their serializations, minus the ID's length and ID itself + assert_eq!( + keys.serialize()[(4 + C::ID.len()) ..], + promoted.serialize()[(4 + AltFunctions::::ID.len()) ..] + ); + } +} +*/ + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +struct AltGenerator { + _curve: PhantomData, +} + +impl Curve for AltGenerator { + type F = C::F; + type G = C::G; + + const ID: &'static [u8] = b"alt_generator"; + + fn generator() -> Self::G { + C::G::generator() * C::hash_to_F(b"FROST_tests", b"generator") + } + + fn hash_msg(msg: &[u8]) -> Vec { + C::hash_msg(msg) + } + + fn hash_binding_factor(binding: &[u8]) -> Self::F { + C::hash_binding_factor(binding) + } + + fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F { + C::hash_to_F(dst, msg) + } +} + +// Test promotion of FROST keys to another generator +fn test_generator_promotion(rng: &mut R) { + // A seeded RNG can theoretically generate for C1 and C2, verifying promotion that way? + // TODO + let keys = key_gen::<_, C>(&mut *rng); + + let mut promotions = HashMap::new(); + let mut proofs = HashMap::new(); + for (i, keys) in &keys { + let promotion = GeneratorPromotion::<_, AltGenerator>::promote(&mut *rng, keys.clone()); + promotions.insert(*i, promotion.0); + proofs.insert(*i, promotion.1); + } + + let mut new_keys = HashMap::new(); + let mut group_key = None; + let mut verification_shares = None; + for (i, promoting) in promotions.drain() { + let promoted = promoting.complete(&clone_without(&proofs, &i)).unwrap(); + assert_eq!(keys[&i].params(), promoted.params()); + assert_eq!(keys[&i].secret_share(), promoted.secret_share()); + + if group_key.is_none() { + group_key = Some(keys[&i].group_key()); + verification_shares = Some(keys[&i].verification_shares()); + } + assert_eq!(keys[&i].group_key(), group_key.unwrap()); + assert_eq!(&keys[&i].verification_shares(), verification_shares.as_ref().unwrap()); + + new_keys.insert(i, promoted); + } + + // Sign with the keys to ensure their integrity + sign_core(rng, &new_keys); +} + +pub fn test_promotion(rng: &mut R) { + // test_ciphersuite_promotion::<_, C>(rng); + test_generator_promotion::<_, C>(rng); +} diff --git a/crypto/frost/src/tests/schnorr.rs b/crypto/frost/src/tests/schnorr.rs index 460261a5..80a478c9 100644 --- a/crypto/frost/src/tests/schnorr.rs +++ b/crypto/frost/src/tests/schnorr.rs @@ -75,15 +75,16 @@ pub(crate) fn core_batch_verify(rng: &mut R) { } } -fn sign_core( +pub(crate) fn sign_core( rng: &mut R, - group_key: C::G, keys: &HashMap>, ) { const MESSAGE: &[u8] = b"Hello, World!"; let machines = algorithm_machines(rng, Schnorr::>::new(), keys); let sig = sign_test(&mut *rng, machines, MESSAGE); + + let group_key = keys[&1].group_key(); assert!(schnorr::verify(group_key, TestHram::::hram(&sig.R, &group_key, MESSAGE), &sig)); } @@ -100,7 +101,7 @@ impl Hram for TestHram { fn sign(rng: &mut R) { let keys = key_gen::<_, C>(&mut *rng); - sign_core(rng, keys[&1].group_key(), &keys); + sign_core(rng, &keys); } fn sign_with_offset(rng: &mut R) { @@ -112,8 +113,9 @@ fn sign_with_offset(rng: &mut R) { keys.insert(i, keys[&i].offset(offset)); } let offset_key = group_key + (C::generator() * offset); + assert_eq!(keys[&1].group_key(), offset_key); - sign_core(rng, offset_key, &keys); + sign_core(rng, &keys); } pub fn test_schnorr(rng: &mut R) { diff --git a/crypto/frost/src/tests/vectors.rs b/crypto/frost/src/tests/vectors.rs index d286ec76..ca2cf278 100644 --- a/crypto/frost/src/tests/vectors.rs +++ b/crypto/frost/src/tests/vectors.rs @@ -9,7 +9,9 @@ use crate::{ FrostCore, FrostKeys, algorithm::{Schnorr, Hram}, sign::{PreprocessPackage, SignMachine, SignatureMachine, AlgorithmMachine}, - tests::{clone_without, curve::test_curve, schnorr::test_schnorr, recover}, + tests::{ + clone_without, curve::test_curve, schnorr::test_schnorr, promote::test_promotion, recover, + }, }; pub struct Vectors { @@ -66,6 +68,7 @@ pub fn test_with_vectors>( // Do basic tests before trying the vectors test_curve::<_, C>(&mut *rng); test_schnorr::<_, C>(rng); + test_promotion::<_, C>(rng); // Test against the vectors let keys = vectors_to_multisig_keys::(&vectors);