From 361a02282efe843ed951dd98519fdc328b7bb28b Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Thu, 8 Dec 2022 17:45:16 -0500 Subject: [PATCH] Support caching preprocesses Closes https://github.com/serai-dex/serai/issues/40. I *could* have added a serialization trait to Algorithm and written a ton of data to disk, while requiring Algorithm implementors also accept such work. Instead, I moved preprocess to a seeded RNG (Chacha20) which should be as secure as the regular RNG. Rebuilding from cache simply loads the previously used Chacha seed, making the Algorithm oblivious to the fact it's being rebuilt from a cache. This removes any requirements for it to be modified while guaranteeing equivalency. This builds on the last commit which delayed determining the signing set till post-preprocess acquisition. Unfortunately, that commit did force preprocess from ThresholdView to ThresholdKeys which had visible effects on Monero. Serai will actually need delayed set determination for #163, and overall, it remains better, hence it's inclusion. --- Cargo.lock | 1 + coins/monero/src/tests/clsag.rs | 40 +++++------ coins/monero/src/wallet/send/multisig.rs | 25 ++++++- coins/monero/tests/runner.rs | 2 +- crypto/frost/Cargo.toml | 1 + crypto/frost/src/sign.rs | 90 +++++++++++++++++++----- crypto/frost/src/tests/mod.rs | 50 ++++++++++++- crypto/frost/src/tests/vectors.rs | 13 +++- 8 files changed, 179 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a4e7f8ab..2ab0a408e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4631,6 +4631,7 @@ dependencies = [ "hkdf", "minimal-ed448", "multiexp", + "rand_chacha 0.3.1", "rand_core 0.6.4", "schnorr-signatures", "serde_json", diff --git a/coins/monero/src/tests/clsag.rs b/coins/monero/src/tests/clsag.rs index 90138d9c8..d0e458cc2 100644 --- a/coins/monero/src/tests/clsag.rs +++ b/coins/monero/src/tests/clsag.rs @@ -101,28 +101,28 @@ fn clsag_multisig() { } let mask_sum = random_scalar(&mut OsRng); + let algorithm = ClsagMultisig::new( + RecommendedTranscript::new(b"Monero Serai CLSAG Test"), + keys[&1].group_key().0, + Arc::new(RwLock::new(Some(ClsagDetails::new( + ClsagInput::new( + Commitment::new(randomness, AMOUNT), + Decoys { + i: RING_INDEX, + offsets: (1 ..= RING_LEN).into_iter().collect(), + ring: ring.clone(), + }, + ) + .unwrap(), + mask_sum, + )))), + ); + sign( &mut OsRng, - algorithm_machines( - &mut OsRng, - ClsagMultisig::new( - RecommendedTranscript::new(b"Monero Serai CLSAG Test"), - keys[&1].group_key().0, - Arc::new(RwLock::new(Some(ClsagDetails::new( - ClsagInput::new( - Commitment::new(randomness, AMOUNT), - Decoys { - i: RING_INDEX, - offsets: (1 ..= RING_LEN).into_iter().collect(), - ring: ring.clone(), - }, - ) - .unwrap(), - mask_sum, - )))), - ), - &keys, - ), + algorithm.clone(), + keys.clone(), + algorithm_machines(&mut OsRng, algorithm, &keys), &[1; 32], ); } diff --git a/coins/monero/src/wallet/send/multisig.rs b/coins/monero/src/wallet/send/multisig.rs index 01bbaafbf..2a0586773 100644 --- a/coins/monero/src/wallet/send/multisig.rs +++ b/coins/monero/src/wallet/send/multisig.rs @@ -4,6 +4,7 @@ use std::{ collections::HashMap, }; +use zeroize::Zeroizing; use rand_core::{RngCore, CryptoRng, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -14,8 +15,8 @@ use frost::{ curve::Ed25519, FrostError, ThresholdKeys, sign::{ - Writable, Preprocess, SignatureShare, PreprocessMachine, SignMachine, SignatureMachine, - AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine, + Writable, Preprocess, CachedPreprocess, SignatureShare, PreprocessMachine, SignMachine, + SignatureMachine, AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine, }, }; @@ -205,10 +206,30 @@ impl PreprocessMachine for TransactionMachine { } impl SignMachine for TransactionSignMachine { + type Params = (); + type Keys = ThresholdKeys; type Preprocess = Vec>; type SignatureShare = Vec>; type SignatureMachine = TransactionSignatureMachine; + fn cache(self) -> Zeroizing { + unimplemented!( + "Monero transactions don't support caching their preprocesses due to {}", + "being already bound to a specific transaction" + ); + } + + fn from_cache( + _: (), + _: ThresholdKeys, + _: Zeroizing, + ) -> Result { + unimplemented!( + "Monero transactions don't support caching their preprocesses due to {}", + "being already bound to a specific transaction" + ); + } + fn read_preprocess(&self, reader: &mut R) -> io::Result { self.clsags.iter().map(|clsag| clsag.read_preprocess(reader)).collect() } diff --git a/coins/monero/tests/runner.rs b/coins/monero/tests/runner.rs index 0ffa459b9..db0970cfe 100644 --- a/coins/monero/tests/runner.rs +++ b/coins/monero/tests/runner.rs @@ -222,7 +222,7 @@ macro_rules! test { ); } - frost::tests::sign(&mut OsRng, machines, &vec![]) + frost::tests::sign_without_caching(&mut OsRng, machines, &vec![]) } } } diff --git a/crypto/frost/Cargo.toml b/crypto/frost/Cargo.toml index e594cc99d..91eaa91dd 100644 --- a/crypto/frost/Cargo.toml +++ b/crypto/frost/Cargo.toml @@ -16,6 +16,7 @@ rustdoc-args = ["--cfg", "docsrs"] thiserror = "1" rand_core = "0.6" +rand_chacha = "0.3" zeroize = { version = "1.5", features = ["zeroize_derive"] } subtle = "2" diff --git a/crypto/frost/src/sign.rs b/crypto/frost/src/sign.rs index 237442530..1641f1d81 100644 --- a/crypto/frost/src/sign.rs +++ b/crypto/frost/src/sign.rs @@ -4,9 +4,10 @@ use std::{ collections::HashMap, }; -use rand_core::{RngCore, CryptoRng}; +use rand_core::{RngCore, CryptoRng, SeedableRng}; +use rand_chacha::ChaCha20Rng; -use zeroize::Zeroize; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; use transcript::Transcript; @@ -73,6 +74,12 @@ impl Writable for Preprocess { } } +/// A cached preprocess. A preprocess MUST only be used once. Reuse will enable third-party +/// recovery of your private key share. Additionally, this MUST be handled with the same security +/// as your private key share, as knowledge of it also enables recovery. +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct CachedPreprocess(pub [u8; 32]); + /// Trait for the initial state machine of a two-round signing protocol. pub trait PreprocessMachine { /// Preprocess message for this machine. @@ -100,13 +107,36 @@ impl> AlgorithmMachine { Ok(AlgorithmMachine { params: Params::new(algorithm, keys)? }) } + fn seeded_preprocess( + self, + seed: Zeroizing, + ) -> (AlgorithmSignMachine, Preprocess) { + let mut params = self.params; + + let mut rng = ChaCha20Rng::from_seed(seed.0); + let (nonces, commitments) = Commitments::new::<_, A::Transcript>( + &mut rng, + params.keys.secret_share(), + ¶ms.algorithm.nonces(), + ); + let addendum = params.algorithm.preprocess_addendum(&mut rng, ¶ms.keys); + + let preprocess = Preprocess { commitments, addendum }; + (AlgorithmSignMachine { params, seed, nonces, preprocess: preprocess.clone() }, preprocess) + } + #[cfg(any(test, feature = "tests"))] pub(crate) fn unsafe_override_preprocess( self, nonces: Vec>, preprocess: Preprocess, ) -> AlgorithmSignMachine { - AlgorithmSignMachine { params: self.params, nonces, preprocess } + AlgorithmSignMachine { + params: self.params, + seed: Zeroizing::new(CachedPreprocess([0; 32])), + nonces, + preprocess, + } } } @@ -119,17 +149,9 @@ impl> PreprocessMachine for AlgorithmMachine { self, rng: &mut R, ) -> (Self::SignMachine, Preprocess) { - let mut params = self.params; - - let (nonces, commitments) = Commitments::new::<_, A::Transcript>( - &mut *rng, - params.keys.secret_share(), - ¶ms.algorithm.nonces(), - ); - let addendum = params.algorithm.preprocess_addendum(rng, ¶ms.keys); - - let preprocess = Preprocess { commitments, addendum }; - (AlgorithmSignMachine { params, nonces, preprocess: preprocess.clone() }, preprocess) + let mut seed = Zeroizing::new(CachedPreprocess([0; 32])); + rng.fill_bytes(seed.0.as_mut()); + self.seeded_preprocess(seed) } } @@ -143,7 +165,11 @@ impl Writable for SignatureShare { } /// Trait for the second machine of a two-round signing protocol. -pub trait SignMachine { +pub trait SignMachine: Sized { + /// Params used to instantiate this machine which can be used to rebuild from a cache. + type Params: Clone; + /// Keys used for signing operations. + type Keys; /// Preprocess message for this machine. type Preprocess: Clone + PartialEq + Writable; /// SignatureShare message for this machine. @@ -151,7 +177,22 @@ pub trait SignMachine { /// SignatureMachine this SignMachine turns into. type SignatureMachine: SignatureMachine; - /// Read a Preprocess message. + /// Cache this preprocess for usage later. This cached preprocess MUST only be used once. Reuse + /// of it enables recovery of your private key share. Third-party recovery of a cached preprocess + /// also enables recovery of your private key share, so this MUST be treated with the same + /// security as your private key share. + fn cache(self) -> Zeroizing; + + /// Create a sign machine from a cached preprocess. After this, the preprocess should be fully + /// deleted, as it must never be reused. It is + fn from_cache( + params: Self::Params, + keys: Self::Keys, + cache: Zeroizing, + ) -> Result; + + /// Read a Preprocess message. Despite taking self, this does not save the preprocess. + /// It must be externally cached and passed into sign. fn read_preprocess(&self, reader: &mut R) -> io::Result; /// Sign a message. @@ -169,16 +210,33 @@ pub trait SignMachine { #[derive(Zeroize)] pub struct AlgorithmSignMachine> { params: Params, + seed: Zeroizing, + pub(crate) nonces: Vec>, #[zeroize(skip)] pub(crate) preprocess: Preprocess, } impl> SignMachine for AlgorithmSignMachine { + type Params = A; + type Keys = ThresholdKeys; type Preprocess = Preprocess; type SignatureShare = SignatureShare; type SignatureMachine = AlgorithmSignatureMachine; + fn cache(self) -> Zeroizing { + self.seed + } + + fn from_cache( + algorithm: A, + keys: ThresholdKeys, + cache: Zeroizing, + ) -> Result { + let (machine, _) = AlgorithmMachine::new(algorithm, keys)?.seeded_preprocess(cache); + Ok(machine) + } + fn read_preprocess(&self, reader: &mut R) -> io::Result { Ok(Preprocess { commitments: Commitments::read::<_, A::Transcript>(reader, &self.params.algorithm.nonces())?, diff --git a/crypto/frost/src/tests/mod.rs b/crypto/frost/src/tests/mod.rs index 9d8ac03b1..927864fad 100644 --- a/crypto/frost/src/tests/mod.rs +++ b/crypto/frost/src/tests/mod.rs @@ -61,10 +61,14 @@ pub fn algorithm_machines>( .collect() } -/// Execute the signing protocol. -pub fn sign( +fn sign_internal< + R: RngCore + CryptoRng, + M: PreprocessMachine, + F: FnMut(&mut R, &mut HashMap), +>( rng: &mut R, mut machines: HashMap, + mut cache: F, msg: &[u8], ) -> M::Signature { let mut commitments = HashMap::new(); @@ -81,6 +85,8 @@ pub fn sign( }) .collect::>(); + cache(rng, &mut machines); + let mut shares = HashMap::new(); let mut machines = machines .drain() @@ -105,3 +111,43 @@ pub fn sign( } signature.unwrap() } + +/// Execute the signing protocol, without caching any machines. This isn't as comprehensive at +/// testing as sign, and accordingly isn't preferred, yet is usable for machines not supporting +/// caching. +pub fn sign_without_caching( + rng: &mut R, + machines: HashMap, + msg: &[u8], +) -> M::Signature { + sign_internal(rng, machines, |_, _| {}, msg) +} + +/// Execute the signing protocol, randomly caching various machines to ensure they can cache +/// successfully. +pub fn sign( + rng: &mut R, + params: >::Params, + mut keys: HashMap>::Keys>, + machines: HashMap, + msg: &[u8], +) -> M::Signature { + sign_internal( + rng, + machines, + |rng, machines| { + // Cache and rebuild half of the machines + let mut included = machines.keys().into_iter().cloned().collect::>(); + for i in included.drain(..) { + if (rng.next_u64() % 2) == 0 { + let cache = machines.remove(&i).unwrap().cache(); + machines.insert( + i, + M::SignMachine::from_cache(params.clone(), keys.remove(&i).unwrap(), cache).unwrap(), + ); + } + } + }, + msg, + ) +} diff --git a/crypto/frost/src/tests/vectors.rs b/crypto/frost/src/tests/vectors.rs index 84eeb361a..f81ec7cc2 100644 --- a/crypto/frost/src/tests/vectors.rs +++ b/crypto/frost/src/tests/vectors.rs @@ -9,7 +9,7 @@ use rand_core::{RngCore, CryptoRng}; use group::{ff::PrimeField, GroupEncoding}; -use dkg::tests::{test_ciphersuite as test_dkg}; +use dkg::tests::{key_gen, test_ciphersuite as test_dkg}; use crate::{ curve::Curve, @@ -19,7 +19,7 @@ use crate::{ Nonce, GeneratorCommitments, NonceCommitments, Commitments, Writable, Preprocess, SignMachine, SignatureMachine, AlgorithmMachine, }, - tests::{clone_without, recover_key, curve::test_curve}, + tests::{clone_without, recover_key, algorithm_machines, sign, curve::test_curve}, }; pub struct Vectors { @@ -124,6 +124,15 @@ pub fn test_with_vectors>( // Test the DKG test_dkg::<_, C>(&mut *rng); + // Test a basic Schnorr signature + { + let keys = key_gen(&mut *rng); + let machines = algorithm_machines(&mut *rng, Schnorr::::new(), &keys); + const MSG: &[u8] = b"Hello, World!"; + let sig = sign(&mut *rng, Schnorr::::new(), keys.clone(), machines, MSG); + assert!(sig.verify(keys[&1].group_key(), H::hram(&sig.R, &keys[&1].group_key(), MSG))); + } + // Test against the vectors let keys = vectors_to_multisig_keys::(&vectors); let group_key =