From cbceaff67861720a3a4793af751652948b608dd0 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Tue, 25 Oct 2022 23:17:25 -0500 Subject: [PATCH] Create dedicated message structures for FROST messages (#140) * Create message types for FROST key gen Taking in reader borrows absolutely wasn't feasible. Now, proper types which can be read (and then passed directly, without a mutable borrow) exist for key_gen. sign coming next. * Move FROST signing to messages, not Readers/Writers/Vec Also takes the nonce handling code and makes a dedicated file for it, aiming to resolve complex types and make the code more legible by replacing its previously inlined state. * clippy * Update FROST tests * read_signature_share * Update the Monero library to the new FROST packages * Update processor to latest FROST * Tweaks to terminology and documentation --- Cargo.lock | 4 +- coins/monero/Cargo.toml | 4 +- coins/monero/src/frost.rs | 73 ----- coins/monero/src/lib.rs | 3 - coins/monero/src/ringct/clsag/mod.rs | 2 +- coins/monero/src/ringct/clsag/multisig.rs | 104 ++++-- coins/monero/src/tests/clsag.rs | 9 +- coins/monero/src/wallet/send/multisig.rs | 102 +++--- crypto/dleq/Cargo.toml | 2 +- crypto/dleq/src/lib.rs | 2 +- crypto/frost/Cargo.toml | 4 +- crypto/frost/src/algorithm.rs | 56 ++-- crypto/frost/src/curve/mod.rs | 31 +- crypto/frost/src/key_gen.rs | 194 +++++------ crypto/frost/src/lib.rs | 5 +- crypto/frost/src/nonce.rs | 271 ++++++++++++++++ crypto/frost/src/promote.rs | 12 +- crypto/frost/src/schnorr.rs | 16 +- crypto/frost/src/sign.rs | 371 ++++++++++------------ crypto/frost/src/tests/curve.rs | 7 +- crypto/frost/src/tests/literal/ed448.rs | 35 +- crypto/frost/src/tests/mod.rs | 41 ++- crypto/frost/src/tests/vectors.rs | 64 ++-- processor/src/lib.rs | 4 +- processor/src/tests/mod.rs | 7 +- processor/src/wallet.rs | 42 ++- 26 files changed, 874 insertions(+), 591 deletions(-) delete mode 100644 coins/monero/src/frost.rs create mode 100644 crypto/frost/src/nonce.rs diff --git a/Cargo.lock b/Cargo.lock index 05e6bdac..b4438176 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1614,7 +1614,7 @@ dependencies = [ [[package]] name = "dleq" -version = "0.1.1" +version = "0.1.2" dependencies = [ "blake2", "dalek-ff-group", @@ -4507,7 +4507,7 @@ dependencies = [ [[package]] name = "modular-frost" -version = "0.2.4" +version = "0.3.0" dependencies = [ "dalek-ff-group", "dleq", diff --git a/coins/monero/Cargo.toml b/coins/monero/Cargo.toml index ccc1d5de..00308153 100644 --- a/coins/monero/Cargo.toml +++ b/coins/monero/Cargo.toml @@ -34,7 +34,7 @@ dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.1" } multiexp = { path = "../../crypto/multiexp", version = "0.2", features = ["batch"] } transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.1", features = ["recommended"], optional = true } -frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.2", features = ["ed25519"], optional = true } +frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.3", features = ["ed25519"], optional = true } dleq = { path = "../../crypto/dleq", version = "0.1", features = ["serialize"], optional = true } monero-generators = { path = "generators", version = "0.1" } @@ -55,7 +55,7 @@ monero-generators = { path = "generators", version = "0.1" } [dev-dependencies] tokio = { version = "1", features = ["full"] } -frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.2", features = ["ed25519", "tests"] } +frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.3", features = ["ed25519", "tests"] } [features] multisig = ["rand_chacha", "blake2", "transcript", "frost", "dleq"] diff --git a/coins/monero/src/frost.rs b/coins/monero/src/frost.rs deleted file mode 100644 index bd27ffdc..00000000 --- a/coins/monero/src/frost.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::io::Read; - -use thiserror::Error; -use rand_core::{RngCore, CryptoRng}; - -use zeroize::Zeroize; - -use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint}; - -use group::{Group, GroupEncoding}; - -use transcript::{Transcript, RecommendedTranscript}; -use dalek_ff_group as dfg; -use dleq::DLEqProof; - -#[derive(Clone, Error, Debug)] -pub(crate) enum MultisigError { - #[error("invalid discrete log equality proof")] - InvalidDLEqProof(u16), -} - -fn transcript() -> RecommendedTranscript { - RecommendedTranscript::new(b"monero_key_image_dleq") -} - -#[allow(non_snake_case)] -pub(crate) fn write_dleq( - rng: &mut R, - H: EdwardsPoint, - mut x: Scalar, -) -> Vec { - let mut res = Vec::with_capacity(64); - DLEqProof::prove( - rng, - // Doesn't take in a larger transcript object due to the usage of this - // Every prover would immediately write their own DLEq proof, when they can only do so in - // the proper order if they want to reach consensus - // It'd be a poor API to have CLSAG define a new transcript solely to pass here, just to try to - // merge later in some form, when it should instead just merge xH (as it does) - &mut transcript(), - &[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(H)], - dfg::Scalar(x), - ) - .serialize(&mut res) - .unwrap(); - x.zeroize(); - res -} - -#[allow(non_snake_case)] -pub(crate) fn read_dleq( - serialized: &mut Re, - H: EdwardsPoint, - l: u16, - xG: dfg::EdwardsPoint, -) -> Result { - let mut bytes = [0; 32]; - serialized.read_exact(&mut bytes).map_err(|_| MultisigError::InvalidDLEqProof(l))?; - // dfg ensures the point is torsion free - let xH = Option::::from(dfg::EdwardsPoint::from_bytes(&bytes)) - .ok_or(MultisigError::InvalidDLEqProof(l))?; - // Ensure this is a canonical point - if xH.to_bytes() != bytes { - Err(MultisigError::InvalidDLEqProof(l))?; - } - - DLEqProof::::deserialize(serialized) - .map_err(|_| MultisigError::InvalidDLEqProof(l))? - .verify(&mut transcript(), &[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(H)], &[xG, xH]) - .map_err(|_| MultisigError::InvalidDLEqProof(l))?; - - Ok(xH) -} diff --git a/coins/monero/src/lib.rs b/coins/monero/src/lib.rs index 52e1a758..b8344bf9 100644 --- a/coins/monero/src/lib.rs +++ b/coins/monero/src/lib.rs @@ -33,9 +33,6 @@ use curve25519_dalek::{ pub use monero_generators::H; -#[cfg(feature = "multisig")] -pub(crate) mod frost; - mod serialize; /// RingCT structs and functionality. diff --git a/coins/monero/src/ringct/clsag/mod.rs b/coins/monero/src/ringct/clsag/mod.rs index 71fa2072..80a3e9b3 100644 --- a/coins/monero/src/ringct/clsag/mod.rs +++ b/coins/monero/src/ringct/clsag/mod.rs @@ -22,7 +22,7 @@ use crate::{ #[cfg(feature = "multisig")] mod multisig; #[cfg(feature = "multisig")] -pub use multisig::{ClsagDetails, ClsagMultisig}; +pub use multisig::{ClsagDetails, ClsagAddendum, ClsagMultisig}; lazy_static! { static ref INV_EIGHT: Scalar = Scalar::from(8u8).invert(); diff --git a/coins/monero/src/ringct/clsag/multisig.rs b/coins/monero/src/ringct/clsag/multisig.rs index f9816b88..89b7d3b8 100644 --- a/coins/monero/src/ringct/clsag/multisig.rs +++ b/coins/monero/src/ringct/clsag/multisig.rs @@ -1,6 +1,6 @@ use core::fmt::Debug; use std::{ - io::Read, + io::{self, Read, Write}, sync::{Arc, RwLock}, }; @@ -16,20 +16,26 @@ use curve25519_dalek::{ edwards::EdwardsPoint, }; -use group::Group; +use group::{Group, GroupEncoding}; use transcript::{Transcript, RecommendedTranscript}; -use frost::{curve::Ed25519, FrostError, FrostView, algorithm::Algorithm}; use dalek_ff_group as dfg; - -use crate::{ - frost::{write_dleq, read_dleq}, - ringct::{ - hash_to_point, - clsag::{ClsagInput, Clsag}, - }, +use dleq::DLEqProof; +use frost::{ + curve::Ed25519, + FrostError, FrostView, + algorithm::{AddendumSerialize, Algorithm}, }; +use crate::ringct::{ + hash_to_point, + clsag::{ClsagInput, Clsag}, +}; + +fn dleq_transcript() -> RecommendedTranscript { + RecommendedTranscript::new(b"monero_key_image_dleq") +} + impl ClsagInput { fn transcript(&self, transcript: &mut T) { // Doesn't domain separate as this is considered part of the larger CLSAG proof @@ -54,7 +60,7 @@ impl ClsagInput { } } -/// CLSAG Input and the mask to use for it. +/// CLSAG input and the mask to use for it. #[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)] pub struct ClsagDetails { input: ClsagInput, @@ -67,6 +73,20 @@ impl ClsagDetails { } } +/// Addendum produced during the FROST signing process with relevant data. +#[derive(Clone, PartialEq, Eq, Zeroize, Debug)] +pub struct ClsagAddendum { + pub(crate) key_image: dfg::EdwardsPoint, + dleq: DLEqProof, +} + +impl AddendumSerialize for ClsagAddendum { + fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(self.key_image.compress().to_bytes().as_ref())?; + self.dleq.serialize(writer) + } +} + #[allow(non_snake_case)] #[derive(Clone, PartialEq, Eq, Debug)] struct Interim { @@ -113,10 +133,6 @@ impl ClsagMultisig { } } - pub(crate) const fn serialized_len() -> usize { - 32 + (2 * 32) - } - fn input(&self) -> ClsagInput { (*self.details.read().unwrap()).as_ref().unwrap().input.clone() } @@ -128,6 +144,7 @@ impl ClsagMultisig { impl Algorithm for ClsagMultisig { type Transcript = RecommendedTranscript; + type Addendum = ClsagAddendum; type Signature = (Clsag, EdwardsPoint); fn nonces(&self) -> Vec> { @@ -138,18 +155,42 @@ impl Algorithm for ClsagMultisig { &mut self, rng: &mut R, view: &FrostView, - ) -> Vec { - let mut serialized = Vec::with_capacity(Self::serialized_len()); - serialized.extend((view.secret_share().0 * self.H).compress().to_bytes()); - serialized.extend(write_dleq(rng, self.H, view.secret_share().0)); - serialized + ) -> ClsagAddendum { + ClsagAddendum { + key_image: dfg::EdwardsPoint(self.H * view.secret_share().0), + dleq: DLEqProof::prove( + rng, + // Doesn't take in a larger transcript object due to the usage of this + // Every prover would immediately write their own DLEq proof, when they can only do so in + // the proper order if they want to reach consensus + // It'd be a poor API to have CLSAG define a new transcript solely to pass here, just to + // try to merge later in some form, when it should instead just merge xH (as it does) + &mut dleq_transcript(), + &[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)], + dfg::Scalar(view.secret_share().0), + ), + } } - fn process_addendum( + fn read_addendum(&self, reader: &mut R) -> io::Result { + let mut bytes = [0; 32]; + reader.read_exact(&mut bytes)?; + // dfg ensures the point is torsion free + let xH = Option::::from(dfg::EdwardsPoint::from_bytes(&bytes)) + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid key image"))?; + // Ensure this is a canonical point + if xH.to_bytes() != bytes { + Err(io::Error::new(io::ErrorKind::Other, "non-canonical key image"))?; + } + + Ok(ClsagAddendum { key_image: xH, dleq: DLEqProof::::deserialize(reader)? }) + } + + fn process_addendum( &mut self, view: &FrostView, l: u16, - serialized: &mut Re, + addendum: ClsagAddendum, ) -> Result<(), FrostError> { if self.image.is_identity() { self.transcript.domain_separate(b"CLSAG"); @@ -158,11 +199,20 @@ impl Algorithm for ClsagMultisig { } self.transcript.append_message(b"participant", &l.to_be_bytes()); - let image = read_dleq(serialized, self.H, l, view.verification_share(l)) - .map_err(|_| FrostError::InvalidCommitment(l))? - .0; - self.transcript.append_message(b"key_image_share", image.compress().to_bytes().as_ref()); - self.image += image; + + addendum + .dleq + .verify( + &mut dleq_transcript(), + &[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)], + &[view.verification_share(l), addendum.key_image], + ) + .map_err(|_| FrostError::InvalidPreprocess(l))?; + + self + .transcript + .append_message(b"key_image_share", addendum.key_image.compress().to_bytes().as_ref()); + self.image += addendum.key_image.0; Ok(()) } diff --git a/coins/monero/src/tests/clsag.rs b/coins/monero/src/tests/clsag.rs index 0068073b..a89f6d3a 100644 --- a/coins/monero/src/tests/clsag.rs +++ b/coins/monero/src/tests/clsag.rs @@ -19,10 +19,7 @@ use crate::{ }, }; #[cfg(feature = "multisig")] -use crate::{ - frost::MultisigError, - ringct::clsag::{ClsagDetails, ClsagMultisig}, -}; +use crate::ringct::clsag::{ClsagDetails, ClsagMultisig}; #[cfg(feature = "multisig")] use frost::tests::{key_gen, algorithm_machines, sign}; @@ -79,7 +76,7 @@ fn clsag() { #[cfg(feature = "multisig")] #[test] -fn clsag_multisig() -> Result<(), MultisigError> { +fn clsag_multisig() { let keys = key_gen::<_, Ed25519>(&mut OsRng); let randomness = random_scalar(&mut OsRng); @@ -125,6 +122,4 @@ fn clsag_multisig() -> Result<(), MultisigError> { ), &[1; 32], ); - - Ok(()) } diff --git a/coins/monero/src/wallet/send/multisig.rs b/coins/monero/src/wallet/send/multisig.rs index e3205bed..55782880 100644 --- a/coins/monero/src/wallet/send/multisig.rs +++ b/coins/monero/src/wallet/send/multisig.rs @@ -1,5 +1,5 @@ use std::{ - io::{Read, Cursor}, + io::{self, Read}, sync::{Arc, RwLock}, collections::HashMap, }; @@ -7,26 +7,22 @@ use std::{ use rand_core::{RngCore, CryptoRng, SeedableRng}; use rand_chacha::ChaCha20Rng; -use curve25519_dalek::{ - traits::Identity, - scalar::Scalar, - edwards::{EdwardsPoint, CompressedEdwardsY}, -}; +use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::EdwardsPoint}; use transcript::{Transcript, RecommendedTranscript}; use frost::{ curve::Ed25519, FrostError, FrostKeys, sign::{ - PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine, AlgorithmSignMachine, - AlgorithmSignatureMachine, + Writable, Preprocess, SignatureShare, PreprocessMachine, SignMachine, SignatureMachine, + AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine, }, }; use crate::{ random_scalar, ringct::{ - clsag::{ClsagInput, ClsagDetails, ClsagMultisig}, + clsag::{ClsagInput, ClsagDetails, ClsagAddendum, ClsagMultisig}, RctPrunable, }, transaction::{Input, Transaction}, @@ -58,7 +54,7 @@ pub struct TransactionSignMachine { inputs: Vec>>>, clsags: Vec>, - our_preprocess: Vec, + our_preprocess: Vec>, } pub struct TransactionSignatureMachine { @@ -166,28 +162,26 @@ impl SignableTransaction { } impl PreprocessMachine for TransactionMachine { + type Preprocess = Vec>; type Signature = Transaction; type SignMachine = TransactionSignMachine; fn preprocess( mut self, rng: &mut R, - ) -> (TransactionSignMachine, Vec) { + ) -> (TransactionSignMachine, Self::Preprocess) { // Iterate over each CLSAG calling preprocess - let mut serialized = Vec::with_capacity( - // D_{G, H}, E_{G, H}, DLEqs, key image addendum - self.clsags.len() * ((2 * (32 + 32)) + (2 * (32 + 32)) + ClsagMultisig::serialized_len()), - ); + let mut preprocesses = Vec::with_capacity(self.clsags.len()); let clsags = self .clsags .drain(..) .map(|clsag| { let (clsag, preprocess) = clsag.preprocess(rng); - serialized.extend(&preprocess); + preprocesses.push(preprocess); clsag }) .collect(); - let our_preprocess = serialized.clone(); + let our_preprocess = preprocesses.clone(); // We could add further entropy here, and previous versions of this library did so // As of right now, the multisig's key, the inputs being spent, and the FROST data itself @@ -212,33 +206,35 @@ impl PreprocessMachine for TransactionMachine { our_preprocess, }, - serialized, + preprocesses, ) } } impl SignMachine for TransactionSignMachine { + type Preprocess = Vec>; + type SignatureShare = Vec>; type SignatureMachine = TransactionSignatureMachine; - fn sign( + fn read_preprocess(&self, reader: &mut R) -> io::Result { + self.clsags.iter().map(|clsag| clsag.read_preprocess(reader)).collect() + } + + fn sign( mut self, - mut commitments: HashMap, + mut commitments: HashMap, msg: &[u8], - ) -> Result<(TransactionSignatureMachine, Vec), FrostError> { + ) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> { if !msg.is_empty() { Err(FrostError::InternalError( "message was passed to the TransactionMachine when it generates its own", ))?; } - // FROST commitments and their DLEqs, and the image and its DLEq - const CLSAG_LEN: usize = (2 * (32 + 32)) + (2 * (32 + 32)) + ClsagMultisig::serialized_len(); - // Convert the unified commitments to a Vec of the individual commitments let mut images = vec![EdwardsPoint::identity(); self.clsags.len()]; let mut commitments = (0 .. self.clsags.len()) .map(|c| { - let mut buf = [0; CLSAG_LEN]; self .included .iter() @@ -248,31 +244,27 @@ impl SignMachine for TransactionSignMachine { // transcripts cloned from this TX's initial premise's transcript. For our TX // transcript to have the CLSAG data for entropy, it'll have to be added ourselves here self.transcript.append_message(b"participant", &(*l).to_be_bytes()); - if *l == self.i { - buf.copy_from_slice(self.our_preprocess.drain(.. CLSAG_LEN).as_slice()); + + let preprocess = if *l == self.i { + self.our_preprocess[c].clone() } else { - commitments - .get_mut(l) - .ok_or(FrostError::MissingParticipant(*l))? - .read_exact(&mut buf) - .map_err(|_| FrostError::InvalidCommitment(*l))?; + commitments.get_mut(l).ok_or(FrostError::MissingParticipant(*l))?[c].clone() + }; + + { + let mut buf = vec![]; + preprocess.write(&mut buf).unwrap(); + self.transcript.append_message(b"preprocess", &buf); } - self.transcript.append_message(b"preprocess", &buf); // While here, calculate the key image // Clsag will parse/calculate/validate this as needed, yet doing so here as well // provides the easiest API overall, as this is where the TX is (which needs the key // images in its message), along with where the outputs are determined (where our // outputs may need these in order to guarantee uniqueness) - images[c] += CompressedEdwardsY( - buf[(CLSAG_LEN - 96) .. (CLSAG_LEN - 64)] - .try_into() - .map_err(|_| FrostError::InvalidCommitment(*l))?, - ) - .decompress() - .ok_or(FrostError::InvalidCommitment(*l))?; + images[c] += preprocess.addendum.key_image.0; - Ok((*l, Cursor::new(buf))) + Ok((*l, preprocess)) }) .collect::, _>>() }) @@ -346,37 +338,39 @@ impl SignMachine for TransactionSignMachine { let msg = tx.signature_hash(); // Iterate over each CLSAG calling sign - let mut serialized = Vec::with_capacity(self.clsags.len() * 32); + let mut shares = Vec::with_capacity(self.clsags.len()); let clsags = self .clsags .drain(..) .map(|clsag| { let (clsag, share) = clsag.sign(commitments.remove(0), &msg)?; - serialized.extend(&share); + shares.push(share); Ok(clsag) }) .collect::>()?; - Ok((TransactionSignatureMachine { tx, clsags }, serialized)) + Ok((TransactionSignatureMachine { tx, clsags }, shares)) } } impl SignatureMachine for TransactionSignatureMachine { - fn complete(self, mut shares: HashMap) -> Result { + type SignatureShare = Vec>; + + fn read_share(&self, reader: &mut R) -> io::Result { + self.clsags.iter().map(|clsag| clsag.read_share(reader)).collect() + } + + fn complete( + mut self, + shares: HashMap, + ) -> Result { let mut tx = self.tx; match tx.rct_signatures.prunable { RctPrunable::Null => panic!("Signing for RctPrunable::Null"), RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => { - for clsag in self.clsags { + for (c, clsag) in self.clsags.drain(..).enumerate() { let (clsag, pseudo_out) = clsag.complete( - shares - .iter_mut() - .map(|(l, shares)| { - let mut buf = [0; 32]; - shares.read_exact(&mut buf).map_err(|_| FrostError::InvalidShare(*l))?; - Ok((*l, Cursor::new(buf))) - }) - .collect::, _>>()?, + shares.iter().map(|(l, shares)| (*l, shares[c].clone())).collect::>(), )?; clsags.push(clsag); pseudo_outs.push(pseudo_out); diff --git a/crypto/dleq/Cargo.toml b/crypto/dleq/Cargo.toml index 99a0fa47..485c97e4 100644 --- a/crypto/dleq/Cargo.toml +++ b/crypto/dleq/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dleq" -version = "0.1.1" +version = "0.1.2" description = "Implementation of single and cross-curve Discrete Log Equality proofs" license = "MIT" repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dleq" diff --git a/crypto/dleq/src/lib.rs b/crypto/dleq/src/lib.rs index 642cfd2d..f170a370 100644 --- a/crypto/dleq/src/lib.rs +++ b/crypto/dleq/src/lib.rs @@ -61,7 +61,7 @@ pub enum DLEqError { InvalidProof, } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] pub struct DLEqProof { c: G::Scalar, s: G::Scalar, diff --git a/crypto/frost/Cargo.toml b/crypto/frost/Cargo.toml index 452ea429..4300868c 100644 --- a/crypto/frost/Cargo.toml +++ b/crypto/frost/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "modular-frost" -version = "0.2.4" +version = "0.3.0" description = "Modular implementation of FROST over ff/group" license = "MIT" repository = "https://github.com/serai-dex/serai/tree/develop/crypto/frost" @@ -40,7 +40,7 @@ transcript = { package = "flexible-transcript", path = "../transcript", features multiexp = { path = "../multiexp", version = "0.2", features = ["batch"] } -dleq = { path = "../dleq", version = "0.1", features = ["serialize"] } +dleq = { path = "../dleq", version = "^0.1.2", features = ["serialize"] } [dev-dependencies] sha2 = "0.10" diff --git a/crypto/frost/src/algorithm.rs b/crypto/frost/src/algorithm.rs index 28adaa1d..b2241f13 100644 --- a/crypto/frost/src/algorithm.rs +++ b/crypto/frost/src/algorithm.rs @@ -1,26 +1,45 @@ use core::{marker::PhantomData, fmt::Debug}; -use std::io::Read; +use std::io::{self, Read, Write}; use rand_core::{RngCore, CryptoRng}; +use zeroize::Zeroize; + use transcript::Transcript; use crate::{Curve, FrostError, FrostView, schnorr}; pub use schnorr::SchnorrSignature; +/// Serialize an addendum to a writer. +pub trait AddendumSerialize { + fn write(&self, writer: &mut W) -> io::Result<()>; +} + +impl AddendumSerialize for () { + fn write(&self, _: &mut W) -> io::Result<()> { + Ok(()) + } +} + +/// Trait alias for the requirements to be used as an addendum. +pub trait Addendum: Clone + PartialEq + Debug + Zeroize + AddendumSerialize {} +impl Addendum for A {} + /// Algorithm trait usable by the FROST signing machine to produce signatures.. pub trait Algorithm: Clone { /// The transcript format this algorithm uses. This likely should NOT be the IETF-compatible /// transcript included in this crate. - type Transcript: Transcript + Clone + Debug; + type Transcript: Clone + Debug + Transcript; + /// Serializable addendum, used in algorithms requiring more data than just the nonces. + type Addendum: Addendum; /// The resulting type of the signatures this algorithm will produce. type Signature: Clone + PartialEq + Debug; /// Obtain a mutable borrow of the underlying transcript. fn transcript(&mut self) -> &mut Self::Transcript; - /// Obtain the list of nonces to generate, as specified by the basepoints to create commitments. - /// against per-nonce. These are not committed to by FROST on the underlying transcript. + /// Obtain the list of nonces to generate, as specified by the generators to create commitments + /// against per-nonce fn nonces(&self) -> Vec>; /// Generate an addendum to FROST"s preprocessing stage. @@ -28,14 +47,17 @@ pub trait Algorithm: Clone { &mut self, rng: &mut R, params: &FrostView, - ) -> Vec; + ) -> Self::Addendum; - /// Proccess the addendum for the specified participant. Guaranteed to be ordered. - fn process_addendum( + /// Read an addendum from a reader. + fn read_addendum(&self, reader: &mut R) -> io::Result; + + /// Proccess the addendum for the specified participant. Guaranteed to be called in order. + fn process_addendum( &mut self, params: &FrostView, l: u16, - reader: &mut Re, + reader: Self::Addendum, ) -> Result<(), FrostError>; /// Sign a share with the given secret/nonce. @@ -116,6 +138,7 @@ impl> Schnorr { impl> Algorithm for Schnorr { type Transcript = IetfTranscript; + type Addendum = (); type Signature = SchnorrSignature; fn transcript(&mut self) -> &mut Self::Transcript { @@ -126,20 +149,13 @@ impl> Algorithm for Schnorr { vec![vec![C::generator()]] } - fn preprocess_addendum( - &mut self, - _: &mut R, - _: &FrostView, - ) -> Vec { - vec![] + fn preprocess_addendum(&mut self, _: &mut R, _: &FrostView) {} + + fn read_addendum(&self, _: &mut R) -> io::Result { + Ok(()) } - fn process_addendum( - &mut self, - _: &FrostView, - _: u16, - _: &mut Re, - ) -> Result<(), FrostError> { + fn process_addendum(&mut self, _: &FrostView, _: u16, _: ()) -> Result<(), FrostError> { Ok(()) } diff --git a/crypto/frost/src/curve/mod.rs b/crypto/frost/src/curve/mod.rs index b8365d30..5600f444 100644 --- a/crypto/frost/src/curve/mod.rs +++ b/crypto/frost/src/curve/mod.rs @@ -1,7 +1,5 @@ use core::fmt::Debug; -use std::io::Read; - -use thiserror::Error; +use std::io::{self, Read}; use rand_core::{RngCore, CryptoRng}; @@ -30,15 +28,6 @@ mod ed448; #[cfg(feature = "ed448")] pub use ed448::{Ed448, Ietf8032Ed448Hram, IetfEd448Hram}; -/// Set of errors for curve-related operations, namely encoding and decoding. -#[derive(Clone, Error, Debug)] -pub enum CurveError { - #[error("invalid scalar")] - InvalidScalar, - #[error("invalid point")] - InvalidPoint, -} - /// Unified trait to manage an elliptic curve. // This should be moved into its own crate if the need for generic cryptography over ff/group // continues, which is the exact reason ff/group exists (to provide a generic interface) @@ -127,13 +116,13 @@ pub trait Curve: Clone + Copy + PartialEq + Eq + Debug + Zeroize { } #[allow(non_snake_case)] - fn read_F(r: &mut R) -> Result { + fn read_F(r: &mut R) -> io::Result { let mut encoding = ::Repr::default(); - r.read_exact(encoding.as_mut()).map_err(|_| CurveError::InvalidScalar)?; + r.read_exact(encoding.as_mut())?; // ff mandates this is canonical - let res = - Option::::from(Self::F::from_repr(encoding)).ok_or(CurveError::InvalidScalar); + let res = Option::::from(Self::F::from_repr(encoding)) + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "non-canonical scalar")); for b in encoding.as_mut() { b.zeroize(); } @@ -141,15 +130,15 @@ pub trait Curve: Clone + Copy + PartialEq + Eq + Debug + Zeroize { } #[allow(non_snake_case)] - fn read_G(r: &mut R) -> Result { + fn read_G(r: &mut R) -> io::Result { let mut encoding = ::Repr::default(); - r.read_exact(encoding.as_mut()).map_err(|_| CurveError::InvalidPoint)?; + r.read_exact(encoding.as_mut())?; - let point = - Option::::from(Self::G::from_bytes(&encoding)).ok_or(CurveError::InvalidPoint)?; + let point = Option::::from(Self::G::from_bytes(&encoding)) + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid point"))?; // Ban the identity, per the FROST spec, and non-canonical points if (point.is_identity().into()) || (point.to_bytes().as_ref() != encoding.as_ref()) { - Err(CurveError::InvalidPoint)?; + Err(io::Error::new(io::ErrorKind::Other, "non-canonical or identity point"))?; } Ok(point) } diff --git a/crypto/frost/src/key_gen.rs b/crypto/frost/src/key_gen.rs index 4a9571d7..e752e773 100644 --- a/crypto/frost/src/key_gen.rs +++ b/crypto/frost/src/key_gen.rs @@ -1,6 +1,6 @@ use std::{ marker::PhantomData, - io::{Read, Cursor}, + io::{self, Read, Write}, collections::HashMap, }; @@ -34,101 +34,97 @@ fn challenge(context: &str, l: u16, R: &[u8], Am: &[u8]) -> C::F { C::hash_to_F(DST, &transcript) } +/// Commitments message to be broadcast to all other parties. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Commitments(Vec, Vec, SchnorrSignature); +impl Commitments { + pub fn read(reader: &mut R, params: FrostParams) -> io::Result { + let mut commitments = Vec::with_capacity(params.t().into()); + let mut serialized = Vec::with_capacity(usize::from(params.t()) * C::G_len()); + for _ in 0 .. params.t() { + let mut buf = ::Repr::default(); + reader.read_exact(buf.as_mut())?; + + commitments.push(C::read_G(&mut buf.as_ref())?); + serialized.extend(buf.as_ref()); + } + + Ok(Commitments(commitments, serialized, SchnorrSignature::read(reader)?)) + } + + pub fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(&self.1)?; + self.2.write(writer) + } +} + // Implements steps 1 through 3 of round 1 of FROST DKG. Returns the coefficients, commitments, and -// the serialized commitments to be broadcasted over an authenticated channel to all parties +// the commitments to be broadcasted over an authenticated channel to all parties fn generate_key_r1( rng: &mut R, params: &FrostParams, context: &str, -) -> (Vec, Vec, Vec) { +) -> (Vec, Vec, Commitments) { let t = usize::from(params.t); let mut coefficients = Vec::with_capacity(t); let mut commitments = Vec::with_capacity(t); - let mut serialized = Vec::with_capacity((C::G_len() * t) + C::G_len() + C::F_len()); + let mut serialized = Vec::with_capacity(t * C::G_len()); for i in 0 .. t { // Step 1: Generate t random values to form a polynomial with coefficients.push(C::random_F(&mut *rng)); // Step 3: Generate public commitments commitments.push(C::generator() * coefficients[i]); - // Serialize them for publication serialized.extend(commitments[i].to_bytes().as_ref()); } // Step 2: Provide a proof of knowledge let mut r = C::random_F(rng); - serialized.extend( - schnorr::sign::( - coefficients[0], - // This could be deterministic as the PoK is a singleton never opened up to cooperative - // discussion - // There's no reason to spend the time and effort to make this deterministic besides a - // general obsession with canonicity and determinism though - r, - challenge::(context, params.i(), (C::generator() * r).to_bytes().as_ref(), &serialized), - ) - .serialize(), + let sig = schnorr::sign::( + coefficients[0], + // This could be deterministic as the PoK is a singleton never opened up to cooperative + // discussion + // There's no reason to spend the time and effort to make this deterministic besides a + // general obsession with canonicity and determinism though + r, + challenge::(context, params.i(), (C::generator() * r).to_bytes().as_ref(), &serialized), ); r.zeroize(); // Step 4: Broadcast - (coefficients, commitments, serialized) + (coefficients, commitments.clone(), Commitments(commitments, serialized, sig)) } // Verify the received data from the first round of key generation -fn verify_r1( +fn verify_r1( rng: &mut R, params: &FrostParams, context: &str, our_commitments: Vec, - mut serialized: HashMap, + mut msgs: HashMap>, ) -> Result>, FrostError> { - validate_map(&serialized, &(1 ..= params.n()).collect::>(), params.i())?; - - let mut commitments = HashMap::new(); - commitments.insert(params.i, our_commitments); + validate_map(&msgs, &(1 ..= params.n()).collect::>(), params.i())?; let mut signatures = Vec::with_capacity(usize::from(params.n() - 1)); - for l in 1 ..= params.n() { - if l == params.i { - continue; - } - - let invalid = FrostError::InvalidCommitment(l); - - // Read the entire list of commitments as the key we're providing a PoK for (A) and the message - #[allow(non_snake_case)] - let mut Am = vec![0; usize::from(params.t()) * C::G_len()]; - serialized.get_mut(&l).unwrap().read_exact(&mut Am).map_err(|_| invalid)?; - - let mut these_commitments = vec![]; - let mut cursor = Cursor::new(&Am); - for _ in 0 .. usize::from(params.t()) { - these_commitments.push(C::read_G(&mut cursor).map_err(|_| invalid)?); - } - - // Don't bother validating our own proof of knowledge - if l != params.i() { - let cursor = serialized.get_mut(&l).unwrap(); - #[allow(non_snake_case)] - let R = C::read_G(cursor).map_err(|_| FrostError::InvalidProofOfKnowledge(l))?; - let s = C::read_F(cursor).map_err(|_| FrostError::InvalidProofOfKnowledge(l))?; - + let mut commitments = msgs + .drain() + .map(|(l, msg)| { // Step 5: Validate each proof of knowledge // This is solely the prep step for the latter batch verification signatures.push(( l, - these_commitments[0], - challenge::(context, l, R.to_bytes().as_ref(), &Am), - SchnorrSignature:: { R, s }, + msg.0[0], + challenge::(context, l, msg.2.R.to_bytes().as_ref(), &msg.1), + msg.2, )); - } - commitments.insert(l, these_commitments); - } + (l, msg.0) + }) + .collect::>(); schnorr::batch_verify(rng, &signatures).map_err(FrostError::InvalidProofOfKnowledge)?; + commitments.insert(params.i, our_commitments); Ok(commitments) } @@ -144,18 +140,39 @@ fn polynomial(coefficients: &[F], l: u16) -> F { share } -// Implements round 1, step 5 and round 2, step 1 of FROST key generation +/// Secret share, to be sent only to the party it's intended for, over an encrypted and +/// authenticated channel. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub struct SecretShare(C::F); +impl SecretShare { + pub fn read(reader: &mut R) -> io::Result { + Ok(SecretShare(C::read_F(reader)?)) + } + + pub fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(self.0.to_repr().as_ref()) + } +} + +impl Drop for SecretShare { + fn drop(&mut self) { + self.zeroize(); + } +} +impl ZeroizeOnDrop for SecretShare {} + +// Calls round 1, step 5 and implements round 2, step 1 of FROST key generation // Returns our secret share part, commitments for the next step, and a vector for each // counterparty to receive -fn generate_key_r2( +fn generate_key_r2( rng: &mut R, params: &FrostParams, context: &str, coefficients: &mut Vec, our_commitments: Vec, - commitments: HashMap, -) -> Result<(C::F, HashMap>, HashMap>), FrostError> { - let commitments = verify_r1::<_, _, C>(rng, params, context, our_commitments, commitments)?; + msgs: HashMap>, +) -> Result<(C::F, HashMap>, HashMap>), FrostError> { + let commitments = verify_r1::<_, C>(rng, params, context, our_commitments, msgs)?; // Step 1: Generate secret shares for all other parties let mut res = HashMap::new(); @@ -166,7 +183,7 @@ fn generate_key_r2( continue; } - res.insert(l, polynomial(coefficients, l).to_repr().as_ref().to_vec()); + res.insert(l, SecretShare(polynomial(coefficients, l))); } // Calculate our own share @@ -177,24 +194,17 @@ fn generate_key_r2( Ok((share, commitments, res)) } -/// Finishes round 2 and returns both the secret share and the serialized public key. -/// This key MUST NOT be considered usable until all parties confirm they have completed the -/// protocol without issue. -fn complete_r2( +// Finishes round 2 and returns the keys. +// This key MUST NOT be considered usable until all parties confirm they have completed the +// protocol without issue. +fn complete_r2( rng: &mut R, params: FrostParams, mut secret_share: C::F, commitments: &mut HashMap>, - mut serialized: HashMap, + mut shares: HashMap>, ) -> Result, FrostError> { - validate_map(&serialized, &(1 ..= params.n()).collect::>(), params.i())?; - - // Step 2. Verify each share - let mut shares = HashMap::new(); - // TODO: Clear serialized - for (l, share) in serialized.iter_mut() { - shares.insert(*l, C::read_F(share).map_err(|_| FrostError::InvalidShare(*l))?); - } + validate_map(&shares, &(1 ..= params.n()).collect::>(), params.i())?; // Calculate the exponent for a given participant and apply it to a series of commitments // Initially used with the actual commitments to verify the secret share, later used with stripes @@ -210,22 +220,18 @@ fn complete_r2( }; let mut batch = BatchVerifier::new(shares.len()); - for (l, share) in shares.iter_mut() { - if *l == params.i() { - continue; - } - - secret_share += *share; + for (l, mut share) in shares.drain() { + secret_share += share.0; // This can be insecurely linearized from n * t to just n using the below sums for a given // stripe. Doing so uses naive addition which is subject to malleability. The only way to // ensure that malleability isn't present is to use this n * t algorithm, which runs // per sender and not as an aggregate of all senders, which also enables blame - let mut values = exponential(params.i, &commitments[l]); - values.push((-*share, C::generator())); + let mut values = exponential(params.i, &commitments[&l]); + values.push((-share.0, C::generator())); share.zeroize(); - batch.queue(rng, *l, values); + batch.queue(rng, l, values); } batch.verify_with_vartime_blame().map_err(FrostError::InvalidCommitment)?; @@ -299,14 +305,14 @@ impl KeyGenMachine { } /// Start generating a key according to the FROST DKG spec. - /// Returns a serialized list of commitments to be sent to all parties over an authenticated + /// Returns a commitments message to be sent to all parties over an authenticated /// channel. If any party submits multiple sets of commitments, they MUST be treated as /// malicious. pub fn generate_coefficients( self, rng: &mut R, - ) -> (SecretShareMachine, Vec) { - let (coefficients, our_commitments, serialized) = + ) -> (SecretShareMachine, Commitments) { + let (coefficients, our_commitments, commitments) = generate_key_r1::<_, C>(rng, &self.params, &self.context); ( @@ -316,21 +322,21 @@ impl KeyGenMachine { coefficients, our_commitments, }, - serialized, + commitments, ) } } impl SecretShareMachine { /// Continue generating a key. - /// Takes in everyone else's commitments. Returns a HashMap of byte vectors representing secret - /// shares. These MUST be encrypted and only then sent to their respective participants. - pub fn generate_secret_shares( + /// Takes in everyone else's commitments. Returns a HashMap of secret shares. + /// These MUST be encrypted and only then sent to their respective participants. + pub fn generate_secret_shares( mut self, rng: &mut R, - commitments: HashMap, - ) -> Result<(KeyMachine, HashMap>), FrostError> { - let (secret, commitments, shares) = generate_key_r2::<_, _, C>( + commitments: HashMap>, + ) -> Result<(KeyMachine, HashMap>), FrostError> { + let (secret, commitments, shares) = generate_key_r2::<_, C>( rng, &self.params, &self.context, @@ -347,10 +353,10 @@ impl KeyMachine { /// Takes in everyone elses' shares submitted to us. Returns a FrostCore object representing the /// generated keys. Successful protocol completion MUST be confirmed by all parties before these /// keys may be safely used. - pub fn complete( + pub fn complete( mut self, rng: &mut R, - shares: HashMap, + shares: HashMap>, ) -> Result, FrostError> { complete_r2(rng, self.params, self.secret, &mut self.commitments, shares) } diff --git a/crypto/frost/src/lib.rs b/crypto/frost/src/lib.rs index 7c6e3753..cf109369 100644 --- a/crypto/frost/src/lib.rs +++ b/crypto/frost/src/lib.rs @@ -38,6 +38,7 @@ pub mod promote; /// Algorithm for the signing process. pub mod algorithm; +mod nonce; /// Threshold signing protocol. pub mod sign; @@ -45,7 +46,7 @@ pub mod sign; #[cfg(any(test, feature = "tests"))] pub mod tests; -// Validate a map of serialized values to have the expected included participants +// Validate a map of values to have the expected included participants pub(crate) fn validate_map( map: &HashMap, included: &[u16], @@ -136,6 +137,8 @@ pub enum FrostError { InvalidCommitment(u16), #[error("invalid proof of knowledge (participant {0})")] InvalidProofOfKnowledge(u16), + #[error("invalid preprocess (participant {0})")] + InvalidPreprocess(u16), #[error("invalid share (participant {0})")] InvalidShare(u16), diff --git a/crypto/frost/src/nonce.rs b/crypto/frost/src/nonce.rs new file mode 100644 index 00000000..21200349 --- /dev/null +++ b/crypto/frost/src/nonce.rs @@ -0,0 +1,271 @@ +// FROST defines its nonce as sum(Di, Ei * bi) +// Monero needs not just the nonce over G however, yet also over H +// Then there is a signature (a modified Chaum Pedersen proof) using multiple nonces at once +// +// Accordingly, in order for this library to be robust, it supports generating an arbitrary amount +// of nonces, each against an arbitrary list of basepoints +// +// Each nonce remains of the form (d, e) and made into a proper nonce with d + (e * b) +// When multiple D, E pairs are provided, a DLEq proof is also provided to confirm their integrity + +use std::{ + io::{self, Read, Write}, + collections::HashMap, +}; + +use rand_core::{RngCore, CryptoRng}; + +use zeroize::Zeroize; + +use transcript::Transcript; + +use group::{ff::PrimeField, Group, GroupEncoding}; +use multiexp::multiexp_vartime; + +use dleq::DLEqProof; + +use crate::curve::Curve; + +fn dleq_transcript() -> T { + T::new(b"FROST_nonce_dleq") +} + +// Each nonce is actually a pair of random scalars, notated as d, e under the FROST paper +// This is considered a single nonce as r = d + be +#[derive(Clone, Zeroize)] +pub(crate) struct Nonce(pub(crate) [C::F; 2]); + +// Commitments to a specific generator for this nonce +#[derive(Copy, Clone, PartialEq, Eq, Zeroize)] +pub(crate) struct GeneratorCommitments(pub(crate) [C::G; 2]); +impl GeneratorCommitments { + fn read(reader: &mut R) -> io::Result> { + Ok(GeneratorCommitments([C::read_G(reader)?, C::read_G(reader)?])) + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(self.0[0].to_bytes().as_ref())?; + writer.write_all(self.0[1].to_bytes().as_ref()) + } +} + +// A single nonce's commitments and relevant proofs +#[derive(Clone, PartialEq, Eq, Zeroize)] +pub(crate) struct NonceCommitments { + // Called generators as these commitments are indexed by generator + pub(crate) generators: Vec>, + // DLEq Proofs proving that these commitments are generated using the same scalar pair + // This could be further optimized with a multi-nonce proof, offering just one proof for all + // nonces. See https://github.com/serai-dex/serai/issues/38 + // TODO + pub(crate) dleqs: Option<[DLEqProof; 2]>, +} + +impl NonceCommitments { + pub(crate) fn new( + rng: &mut R, + mut secret_share: C::F, + generators: &[C::G], + ) -> (Nonce, NonceCommitments) { + let nonce = + Nonce([C::random_nonce(secret_share, &mut *rng), C::random_nonce(secret_share, &mut *rng)]); + secret_share.zeroize(); + + let mut commitments = Vec::with_capacity(generators.len()); + for generator in generators { + commitments.push(GeneratorCommitments([*generator * nonce.0[0], *generator * nonce.0[1]])); + } + + let mut dleqs = None; + if generators.len() >= 2 { + let mut dleq = |nonce| { + // Uses an independent transcript as each signer must prove this with their commitments, + // yet they're validated while processing everyone's data sequentially, by the global order + // This avoids needing to clone and fork the transcript around + // TODO: At least include a challenge from the existing transcript + DLEqProof::prove(&mut *rng, &mut dleq_transcript::(), generators, nonce) + }; + dleqs = Some([dleq(nonce.0[0]), dleq(nonce.0[1])]); + } + + (nonce, NonceCommitments { generators: commitments, dleqs }) + } + + fn read( + reader: &mut R, + generators: &[C::G], + ) -> io::Result> { + let commitments: Vec> = (0 .. generators.len()) + .map(|_| GeneratorCommitments::read(reader)) + .collect::>()?; + + let mut dleqs = None; + if generators.len() >= 2 { + let mut verify = |i| -> io::Result<_> { + let dleq = DLEqProof::deserialize(reader)?; + dleq + .verify( + &mut dleq_transcript::(), + generators, + &commitments.iter().map(|commitments| commitments.0[i]).collect::>(), + ) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid DLEq proof"))?; + Ok(dleq) + }; + dleqs = Some([verify(0)?, verify(1)?]); + } + + Ok(NonceCommitments { generators: commitments, dleqs }) + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + for generator in &self.generators { + generator.write(writer)?; + } + if let Some(dleqs) = &self.dleqs { + dleqs[0].serialize(writer)?; + dleqs[1].serialize(writer)?; + } + Ok(()) + } +} + +#[derive(Clone, PartialEq, Eq, Zeroize)] +pub(crate) struct Commitments { + // Called nonces as these commitments are indexed by nonce + pub(crate) nonces: Vec>, +} + +impl Commitments { + pub(crate) fn new( + rng: &mut R, + secret_share: C::F, + planned_nonces: &[Vec], + ) -> (Vec>, Commitments) { + let mut nonces = vec![]; + let mut commitments = vec![]; + for generators in planned_nonces { + let (nonce, these_commitments) = + NonceCommitments::new::<_, T>(&mut *rng, secret_share, generators); + nonces.push(nonce); + commitments.push(these_commitments); + } + (nonces, Commitments { nonces: commitments }) + } + + pub(crate) fn transcript(&self, t: &mut T) { + for nonce in &self.nonces { + for commitments in &nonce.generators { + t.append_message(b"commitment_D", commitments.0[0].to_bytes().as_ref()); + t.append_message(b"commitment_E", commitments.0[1].to_bytes().as_ref()); + } + + // Transcripting the DLEqs implicitly transcripts the exact generators used for this nonce + // This means it shouldn't be possible for variadic generators to cause conflicts as they're + // committed to as their entire series per-nonce, not as isolates + if let Some(dleqs) = &nonce.dleqs { + let mut transcript_dleq = |label, dleq: &DLEqProof| { + let mut buf = Vec::with_capacity(C::G_len() + C::F_len()); + dleq.serialize(&mut buf).unwrap(); + t.append_message(label, &buf); + }; + transcript_dleq(b"dleq_D", &dleqs[0]); + transcript_dleq(b"dleq_E", &dleqs[1]); + } + } + } + + pub(crate) fn read( + reader: &mut R, + nonces: &[Vec], + ) -> io::Result { + Ok(Commitments { + nonces: (0 .. nonces.len()) + .map(|i| NonceCommitments::read::<_, T>(reader, &nonces[i])) + .collect::>()?, + }) + } + + pub(crate) fn write(&self, writer: &mut W) -> io::Result<()> { + for nonce in &self.nonces { + nonce.write(writer)?; + } + Ok(()) + } +} + +#[derive(Zeroize)] +pub(crate) struct IndividualBinding { + commitments: Commitments, + binding_factors: Option>, +} + +pub(crate) struct BindingFactor(pub(crate) HashMap>); + +impl Zeroize for BindingFactor { + fn zeroize(&mut self) { + for (mut validator, mut binding) in self.0.drain() { + validator.zeroize(); + binding.zeroize(); + } + } +} + +impl BindingFactor { + pub(crate) fn insert(&mut self, i: u16, commitments: Commitments) { + self.0.insert(i, IndividualBinding { commitments, binding_factors: None }); + } + + pub(crate) fn calculate_binding_factors(&mut self, transcript: &mut T) { + for (l, binding) in self.0.iter_mut() { + let mut transcript = transcript.clone(); + transcript.append_message(b"participant", C::F::from(u64::from(*l)).to_repr().as_ref()); + // It *should* be perfectly fine to reuse a binding factor for multiple nonces + // This generates a binding factor per nonce just to ensure it never comes up as a question + binding.binding_factors = Some( + (0 .. binding.commitments.nonces.len()) + .map(|_| C::hash_binding_factor(transcript.challenge(b"rho").as_ref())) + .collect(), + ); + } + } + + pub(crate) fn binding_factors(&self, i: u16) -> &[C::F] { + self.0[&i].binding_factors.as_ref().unwrap() + } + + // Get the bound nonces for a specific party + pub(crate) fn bound(&self, l: u16) -> Vec> { + let mut res = vec![]; + for (i, (nonce, rho)) in + self.0[&l].commitments.nonces.iter().zip(self.binding_factors(l).iter()).enumerate() + { + res.push(vec![]); + for generator in &nonce.generators { + res[i].push(generator.0[0] + (generator.0[1] * rho)); + } + } + res + } + + // Get the nonces for this signing session + pub(crate) fn nonces(&self, planned_nonces: &[Vec]) -> Vec> { + let mut nonces = Vec::with_capacity(planned_nonces.len()); + for n in 0 .. planned_nonces.len() { + nonces.push(Vec::with_capacity(planned_nonces[n].len())); + for g in 0 .. planned_nonces[n].len() { + #[allow(non_snake_case)] + let mut D = C::G::identity(); + let mut statements = Vec::with_capacity(self.0.len()); + #[allow(non_snake_case)] + for IndividualBinding { commitments, binding_factors } in self.0.values() { + D += commitments.nonces[n].generators[g].0[0]; + statements + .push((binding_factors.as_ref().unwrap()[n], commitments.nonces[n].generators[g].0[1])); + } + nonces[n].push(D + multiexp_vartime(&statements)); + } + } + nonces + } +} diff --git a/crypto/frost/src/promote.rs b/crypto/frost/src/promote.rs index c879f056..96b852ba 100644 --- a/crypto/frost/src/promote.rs +++ b/crypto/frost/src/promote.rs @@ -12,10 +12,7 @@ use group::GroupEncoding; use transcript::{Transcript, RecommendedTranscript}; use dleq::DLEqProof; -use crate::{ - curve::{CurveError, Curve}, - FrostError, FrostCore, FrostKeys, validate_map, -}; +use crate::{curve::Curve, FrostError, FrostCore, FrostKeys, validate_map}; /// Promote a set of keys to another Curve definition. pub trait CurvePromote { @@ -73,11 +70,8 @@ impl GeneratorProof { 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 fn deserialize(reader: &mut R) -> io::Result> { + Ok(GeneratorProof { share: C::read_G(reader)?, proof: DLEqProof::deserialize(reader)? }) } } diff --git a/crypto/frost/src/schnorr.rs b/crypto/frost/src/schnorr.rs index da229145..5e49e08c 100644 --- a/crypto/frost/src/schnorr.rs +++ b/crypto/frost/src/schnorr.rs @@ -1,3 +1,5 @@ +use std::io::{self, Read, Write}; + use rand_core::{RngCore, CryptoRng}; use zeroize::Zeroize; @@ -9,7 +11,7 @@ use group::{ use multiexp::BatchVerifier; -use crate::Curve; +use crate::curve::Curve; /// A Schnorr signature of the form (R, s) where s = r + cx. #[allow(non_snake_case)] @@ -20,11 +22,13 @@ pub struct SchnorrSignature { } impl SchnorrSignature { - pub fn serialize(&self) -> Vec { - let mut res = Vec::with_capacity(C::G_len() + C::F_len()); - res.extend(self.R.to_bytes().as_ref()); - res.extend(self.s.to_repr().as_ref()); - res + pub(crate) fn read(reader: &mut R) -> io::Result { + Ok(SchnorrSignature { R: C::read_G(reader)?, s: C::read_F(reader)? }) + } + + pub(crate) fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(self.R.to_bytes().as_ref())?; + writer.write_all(self.s.to_repr().as_ref()) } } diff --git a/crypto/frost/src/sign.rs b/crypto/frost/src/sign.rs index a863a76a..67fb5bff 100644 --- a/crypto/frost/src/sign.rs +++ b/crypto/frost/src/sign.rs @@ -1,28 +1,40 @@ use core::fmt; use std::{ - io::{Read, Cursor}, + io::{self, Read, Write}, collections::HashMap, }; use rand_core::{RngCore, CryptoRng}; use zeroize::{Zeroize, ZeroizeOnDrop}; -use subtle::ConstantTimeEq; use transcript::Transcript; -use group::{ - ff::{Field, PrimeField}, - Group, GroupEncoding, -}; -use multiexp::multiexp_vartime; - -use dleq::DLEqProof; +use group::{ff::PrimeField, GroupEncoding}; use crate::{ - curve::Curve, FrostError, FrostParams, FrostKeys, FrostView, algorithm::Algorithm, validate_map, + curve::Curve, + FrostError, FrostParams, FrostKeys, FrostView, + algorithm::{AddendumSerialize, Addendum, Algorithm}, + validate_map, }; +pub(crate) use crate::nonce::*; + +/// Trait enabling writing preprocesses and signature shares. +pub trait Writable { + fn write(&self, writer: &mut W) -> io::Result<()>; +} + +impl Writable for Vec { + fn write(&self, writer: &mut W) -> io::Result<()> { + for w in self { + w.write(writer)?; + } + Ok(()) + } +} + /// Pairing of an Algorithm with a FrostKeys instance and this specific signing set. #[derive(Clone)] pub struct Params> { @@ -31,7 +43,6 @@ pub struct Params> { view: FrostView, } -// Currently public to enable more complex operations as desired, yet solely used in testing impl> Params { pub fn new( algorithm: A, @@ -79,104 +90,75 @@ impl> Params { } } -fn nonce_transcript() -> T { - T::new(b"FROST_nonce_dleq") +/// Preprocess for an instance of the FROST signing protocol. +#[derive(Clone, PartialEq, Eq, Zeroize)] +pub struct Preprocess { + pub(crate) commitments: Commitments, + pub addendum: A, +} + +impl Writable for Preprocess { + fn write(&self, writer: &mut W) -> io::Result<()> { + self.commitments.write(writer)?; + self.addendum.write(writer) + } } #[derive(Zeroize)] -pub(crate) struct PreprocessPackage { - pub(crate) nonces: Vec<[C::F; 2]>, - #[zeroize(skip)] - pub(crate) commitments: Vec>, - pub(crate) addendum: Vec, +pub(crate) struct PreprocessData { + pub(crate) nonces: Vec>, + pub(crate) preprocess: Preprocess, } -impl Drop for PreprocessPackage { +impl Drop for PreprocessData { fn drop(&mut self) { self.zeroize() } } -impl ZeroizeOnDrop for PreprocessPackage {} +impl ZeroizeOnDrop for PreprocessData {} fn preprocess>( rng: &mut R, params: &mut Params, -) -> (PreprocessPackage, Vec) { - let mut serialized = Vec::with_capacity(2 * C::G_len()); - let (nonces, commitments) = params - .algorithm - .nonces() - .iter() - .map(|generators| { - let nonces = [ - C::random_nonce(params.view().secret_share(), &mut *rng), - C::random_nonce(params.view().secret_share(), &mut *rng), - ]; - - let commit = |generator: C::G, buf: &mut Vec| { - let commitments = [generator * nonces[0], generator * nonces[1]]; - buf.extend(commitments[0].to_bytes().as_ref()); - buf.extend(commitments[1].to_bytes().as_ref()); - commitments - }; - - let mut commitments = Vec::with_capacity(generators.len()); - for generator in generators.iter() { - commitments.push(commit(*generator, &mut serialized)); - } - - // Provide a DLEq proof to verify these commitments are for the same nonce - if generators.len() >= 2 { - // Uses an independent transcript as each signer must do this now, yet we validate them - // sequentially by the global order. Avoids needing to clone and fork the transcript around - let mut transcript = nonce_transcript::(); - - // This could be further optimized with a multi-nonce proof. - // See https://github.com/serai-dex/serai/issues/38 - for mut nonce in nonces { - DLEqProof::prove(&mut *rng, &mut transcript, generators, nonce) - .serialize(&mut serialized) - .unwrap(); - nonce.zeroize(); - } - } - - (nonces, commitments) - }) - .unzip(); - +) -> (PreprocessData, Preprocess) { + let (nonces, commitments) = Commitments::new::<_, A::Transcript>( + &mut *rng, + params.view().secret_share(), + ¶ms.algorithm.nonces(), + ); let addendum = params.algorithm.preprocess_addendum(rng, ¶ms.view); - serialized.extend(&addendum); - (PreprocessPackage { nonces, commitments, addendum }, serialized) + let preprocess = Preprocess { commitments, addendum }; + (PreprocessData { nonces, preprocess: preprocess.clone() }, preprocess) } #[allow(non_snake_case)] -fn read_D_E(cursor: &mut Re, l: u16) -> Result<[C::G; 2], FrostError> { - Ok([ - C::read_G(cursor).map_err(|_| FrostError::InvalidCommitment(l))?, - C::read_G(cursor).map_err(|_| FrostError::InvalidCommitment(l))?, - ]) -} - -#[allow(non_snake_case)] -struct Package { - B: HashMap>, C::F)>, +struct SignData { + B: BindingFactor, Rs: Vec>, share: C::F, } +/// Share of a signature produced via FROST. +#[derive(Clone, PartialEq, Eq, Zeroize)] +pub struct SignatureShare(C::F); +impl Writable for SignatureShare { + fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(self.0.to_repr().as_ref()) + } +} + // Has every signer perform the role of the signature aggregator // Step 1 was already deprecated by performing nonce generation as needed // Step 2 is simply the broadcast round from step 1 -fn sign_with_share>( +fn sign_with_share>( params: &mut Params, - our_preprocess: PreprocessPackage, - mut commitments: HashMap, + mut our_preprocess: PreprocessData, + mut preprocesses: HashMap>, msg: &[u8], -) -> Result<(Package, Vec), FrostError> { +) -> Result<(SignData, SignatureShare), FrostError> { let multisig_params = params.multisig_params(); - validate_map(&commitments, ¶ms.view.included, multisig_params.i)?; + validate_map(&preprocesses, ¶ms.view.included, multisig_params.i)?; { // Domain separate FROST @@ -185,9 +167,9 @@ fn sign_with_share>( let nonces = params.algorithm.nonces(); #[allow(non_snake_case)] - let mut B = HashMap::::with_capacity(params.view.included.len()); + let mut B = BindingFactor(HashMap::::with_capacity(params.view.included.len())); { - // Parse the commitments + // Parse the preprocesses for l in ¶ms.view.included { { params @@ -196,73 +178,39 @@ fn sign_with_share>( .append_message(b"participant", C::F::from(u64::from(*l)).to_repr().as_ref()); } - // While this doesn't note which nonce/basepoint this is for, those are expected to be - // static. Beyond that, they're committed to in the DLEq proof transcripts, ensuring - // consistency. While this is suboptimal, it maintains IETF compliance, and Algorithm is - // documented accordingly - let transcript = |t: &mut A::Transcript, commitments: [C::G; 2]| { - if commitments[0].ct_eq(&C::G::identity()).into() || - commitments[1].ct_eq(&C::G::identity()).into() - { - Err(FrostError::InvalidCommitment(*l))?; - } - t.append_message(b"commitment_D", commitments[0].to_bytes().as_ref()); - t.append_message(b"commitment_E", commitments[1].to_bytes().as_ref()); - Ok(()) - }; - if *l == params.keys.params().i { - for nonce_commitments in &our_preprocess.commitments { - for commitments in nonce_commitments { - transcript(params.algorithm.transcript(), *commitments).unwrap(); - } + let commitments = our_preprocess.preprocess.commitments.clone(); + commitments.transcript(params.algorithm.transcript()); + + let addendum = our_preprocess.preprocess.addendum.clone(); + { + let mut buf = vec![]; + addendum.write(&mut buf).unwrap(); + params.algorithm.transcript().append_message(b"addendum", &buf); } - B.insert(*l, (our_preprocess.commitments.clone(), C::F::zero())); - params.algorithm.process_addendum( - ¶ms.view, - *l, - &mut Cursor::new(our_preprocess.addendum.clone()), - )?; + B.insert(*l, commitments); + params.algorithm.process_addendum(¶ms.view, *l, addendum)?; } else { - let mut cursor = commitments.remove(l).unwrap(); - - let mut commitments = Vec::with_capacity(nonces.len()); - for (n, nonce_generators) in nonces.clone().iter_mut().enumerate() { - commitments.push(Vec::with_capacity(nonce_generators.len())); - for _ in 0 .. nonce_generators.len() { - commitments[n].push(read_D_E::<_, C>(&mut cursor, *l)?); - transcript(params.algorithm.transcript(), commitments[n][commitments[n].len() - 1])?; - } - - if nonce_generators.len() >= 2 { - let mut transcript = nonce_transcript::(); - for de in 0 .. 2 { - DLEqProof::deserialize(&mut cursor) - .map_err(|_| FrostError::InvalidCommitment(*l))? - .verify( - &mut transcript, - nonce_generators, - &commitments[n].iter().map(|commitments| commitments[de]).collect::>(), - ) - .map_err(|_| FrostError::InvalidCommitment(*l))?; - } - } + let preprocess = preprocesses.remove(l).unwrap(); + preprocess.commitments.transcript(params.algorithm.transcript()); + { + let mut buf = vec![]; + preprocess.addendum.write(&mut buf).unwrap(); + params.algorithm.transcript().append_message(b"addendum", &buf); } - B.insert(*l, (commitments, C::F::zero())); - params.algorithm.process_addendum(¶ms.view, *l, &mut cursor)?; + B.insert(*l, preprocess.commitments); + params.algorithm.process_addendum(¶ms.view, *l, preprocess.addendum)?; } } // Re-format into the FROST-expected rho transcript let mut rho_transcript = A::Transcript::new(b"FROST_rho"); rho_transcript.append_message(b"message", &C::hash_msg(msg)); - // This won't just be the commitments, yet the full existing transcript if used in an extended - // protocol rho_transcript.append_message( - b"commitments", - &C::hash_commitments(params.algorithm.transcript().challenge(b"commitments").as_ref()), + b"preprocesses", + &C::hash_commitments(params.algorithm.transcript().challenge(b"preprocesses").as_ref()), ); // Include the offset, if one exists @@ -280,14 +228,10 @@ fn sign_with_share>( } // Generate the per-signer binding factors - for (l, commitments) in B.iter_mut() { - let mut rho_transcript = rho_transcript.clone(); - rho_transcript.append_message(b"participant", C::F::from(u64::from(*l)).to_repr().as_ref()); - commitments.1 = C::hash_binding_factor(rho_transcript.challenge(b"rho").as_ref()); - } + B.calculate_binding_factors(&mut rho_transcript); - // Merge the rho transcript back into the global one to ensure its advanced while committing to - // everything + // Merge the rho transcript back into the global one to ensure its advanced, while + // simultaneously committing to everything params .algorithm .transcript() @@ -295,60 +239,44 @@ fn sign_with_share>( } #[allow(non_snake_case)] - let mut Rs = Vec::with_capacity(nonces.len()); - for n in 0 .. nonces.len() { - Rs.push(vec![C::G::identity(); nonces[n].len()]); - for g in 0 .. nonces[n].len() { - #[allow(non_snake_case)] - let mut D = C::G::identity(); - let mut statements = Vec::with_capacity(B.len()); - #[allow(non_snake_case)] - for (B, binding) in B.values() { - D += B[n][g][0]; - statements.push((*binding, B[n][g][1])); - } - Rs[n][g] = D + multiexp_vartime(&statements); - } - } + let Rs = B.nonces(&nonces); + let our_binding_factors = B.binding_factors(multisig_params.i()); let mut nonces = our_preprocess .nonces .iter() - .map(|nonces| nonces[0] + (nonces[1] * B[¶ms.keys.params().i()].1)) + .enumerate() + .map(|(n, nonces)| nonces.0[0] + (nonces.0[1] * our_binding_factors[n])) .collect::>(); + our_preprocess.nonces.zeroize(); let share = params.algorithm.sign_share(¶ms.view, &Rs, &nonces, msg); nonces.zeroize(); - Ok((Package { B, Rs, share }, share.to_repr().as_ref().to_vec())) + Ok((SignData { B, Rs, share }, SignatureShare(share))) } -fn complete>( +fn complete>( sign_params: &Params, - sign: Package, - mut shares: HashMap, + sign: SignData, + mut shares: HashMap>, ) -> Result { let params = sign_params.multisig_params(); validate_map(&shares, &sign_params.view.included, params.i)?; let mut responses = HashMap::new(); - let mut sum = C::F::zero(); - for l in &sign_params.view.included { - let part = if *l == params.i { - sign.share - } else { - C::read_F(shares.get_mut(l).unwrap()).map_err(|_| FrostError::InvalidShare(*l))? - }; - sum += part; - responses.insert(*l, part); + responses.insert(params.i(), sign.share); + let mut sum = sign.share; + for (l, share) in shares.drain() { + responses.insert(l, share.0); + sum += share.0; } // Perform signature validation instead of individual share validation // For the success route, which should be much more frequent, this should be faster // It also acts as an integrity check of this library's signing function - let res = sign_params.algorithm.verify(sign_params.view.group_key, &sign.Rs, sum); - if let Some(res) = res { - return Ok(res); + if let Some(sig) = sign_params.algorithm.verify(sign_params.view.group_key, &sign.Rs, sum) { + return Ok(sig); } // Find out who misbehaved. It may be beneficial to randomly sort this to have detection be @@ -356,13 +284,7 @@ fn complete>( for l in &sign_params.view.included { if !sign_params.algorithm.verify_share( sign_params.view.verification_share(*l), - &sign.B[l] - .0 - .iter() - .map(|nonces| { - nonces.iter().map(|commitments| commitments[0] + (commitments[1] * sign.B[l].1)).collect() - }) - .collect::>(), + &sign.B.bound(*l), responses[l], ) { Err(FrostError::InvalidShare(*l))?; @@ -375,33 +297,53 @@ fn complete>( /// Trait for the initial state machine of a two-round signing protocol. pub trait PreprocessMachine { + /// Preprocess message for this machine. + type Preprocess: Clone + PartialEq + Writable; + /// Signature produced by this machine. type Signature: Clone + PartialEq + fmt::Debug; - type SignMachine: SignMachine; + /// SignMachine this PreprocessMachine turns into. + type SignMachine: SignMachine; /// Perform the preprocessing round required in order to sign. - /// Returns a byte vector to be broadcast to all participants, over an authenticated channel. - fn preprocess(self, rng: &mut R) -> (Self::SignMachine, Vec); + /// Returns a preprocess message to be broadcast to all participants, over an authenticated + /// channel. + fn preprocess(self, rng: &mut R) + -> (Self::SignMachine, Self::Preprocess); } /// Trait for the second machine of a two-round signing protocol. pub trait SignMachine { - type SignatureMachine: SignatureMachine; + /// Preprocess message for this machine. + type Preprocess: Clone + PartialEq + Writable; + /// SignatureShare message for this machine. + type SignatureShare: Clone + PartialEq + Writable; + /// SignatureMachine this SignMachine turns into. + type SignatureMachine: SignatureMachine; + + /// Read a Preprocess message. + fn read_preprocess(&self, reader: &mut R) -> io::Result; /// Sign a message. - /// Takes in the participants' preprocesses. Returns a byte vector representing a signature share - /// to be broadcast to all participants, over an authenticated channel. - fn sign( + /// Takes in the participants' preprocess messages. Returns the signature share to be broadcast + /// to all participants, over an authenticated channel. + fn sign( self, - commitments: HashMap, + commitments: HashMap, msg: &[u8], - ) -> Result<(Self::SignatureMachine, Vec), FrostError>; + ) -> Result<(Self::SignatureMachine, Self::SignatureShare), FrostError>; } /// Trait for the final machine of a two-round signing protocol. pub trait SignatureMachine { + /// SignatureShare message for this machine. + type SignatureShare: Clone + PartialEq + Writable; + + /// Read a Signature Share message. + fn read_share(&self, reader: &mut R) -> io::Result; + /// Complete signing. /// Takes in everyone elses' shares. Returns the signature. - fn complete(self, shares: HashMap) -> Result; + fn complete(self, shares: HashMap) -> Result; } /// State machine which manages signing for an arbitrary signature algorithm. @@ -412,13 +354,13 @@ pub struct AlgorithmMachine> { /// Next step of the state machine for the signing process. pub struct AlgorithmSignMachine> { params: Params, - preprocess: PreprocessPackage, + preprocess: PreprocessData, } /// Final step of the state machine for the signing process. pub struct AlgorithmSignatureMachine> { params: Params, - sign: Package, + sign: SignData, } impl> AlgorithmMachine { @@ -434,39 +376,58 @@ impl> AlgorithmMachine { #[cfg(any(test, feature = "tests"))] pub(crate) fn unsafe_override_preprocess( self, - preprocess: PreprocessPackage, + preprocess: PreprocessData, ) -> AlgorithmSignMachine { AlgorithmSignMachine { params: self.params, preprocess } } } impl> PreprocessMachine for AlgorithmMachine { + type Preprocess = Preprocess; type Signature = A::Signature; type SignMachine = AlgorithmSignMachine; - fn preprocess(self, rng: &mut R) -> (Self::SignMachine, Vec) { + fn preprocess( + self, + rng: &mut R, + ) -> (Self::SignMachine, Preprocess) { let mut params = self.params; - let (preprocess, serialized) = preprocess::(rng, &mut params); - (AlgorithmSignMachine { params, preprocess }, serialized) + let (preprocess, public) = preprocess::(rng, &mut params); + (AlgorithmSignMachine { params, preprocess }, public) } } impl> SignMachine for AlgorithmSignMachine { + type Preprocess = Preprocess; + type SignatureShare = SignatureShare; type SignatureMachine = AlgorithmSignatureMachine; - fn sign( + fn read_preprocess(&self, reader: &mut R) -> io::Result { + Ok(Preprocess { + commitments: Commitments::read::<_, A::Transcript>(reader, &self.params.algorithm.nonces())?, + addendum: self.params.algorithm.read_addendum(reader)?, + }) + } + + fn sign( self, - commitments: HashMap, + commitments: HashMap>, msg: &[u8], - ) -> Result<(Self::SignatureMachine, Vec), FrostError> { + ) -> Result<(Self::SignatureMachine, SignatureShare), FrostError> { let mut params = self.params; - let (sign, serialized) = sign_with_share(&mut params, self.preprocess, commitments, msg)?; - Ok((AlgorithmSignatureMachine { params, sign }, serialized)) + let (sign, public) = sign_with_share(&mut params, self.preprocess, commitments, msg)?; + Ok((AlgorithmSignatureMachine { params, sign }, public)) } } impl> SignatureMachine for AlgorithmSignatureMachine { - fn complete(self, shares: HashMap) -> Result { + type SignatureShare = SignatureShare; + + fn read_share(&self, reader: &mut R) -> io::Result> { + Ok(SignatureShare(C::read_F(reader)?)) + } + + fn complete(self, shares: HashMap>) -> Result { complete(&self.params, self.sign, shares) } } diff --git a/crypto/frost/src/tests/curve.rs b/crypto/frost/src/tests/curve.rs index 56265f85..17ad71f2 100644 --- a/crypto/frost/src/tests/curve.rs +++ b/crypto/frost/src/tests/curve.rs @@ -1,5 +1,3 @@ -use std::io::Cursor; - use rand_core::{RngCore, CryptoRng}; use group::Group; @@ -15,7 +13,10 @@ fn key_generation(rng: &mut R) { // Test serialization of generated keys fn keys_serialization(rng: &mut R) { for (_, keys) in core_gen::<_, C>(rng) { - assert_eq!(&FrostCore::::deserialize(&mut Cursor::new(keys.serialize())).unwrap(), &keys); + assert_eq!( + &FrostCore::::deserialize::<&[u8]>(&mut keys.serialize().as_ref()).unwrap(), + &keys + ); } } diff --git a/crypto/frost/src/tests/literal/ed448.rs b/crypto/frost/src/tests/literal/ed448.rs index df0c9ee7..3550651f 100644 --- a/crypto/frost/src/tests/literal/ed448.rs +++ b/crypto/frost/src/tests/literal/ed448.rs @@ -1,5 +1,3 @@ -use std::io::Cursor; - use rand_core::OsRng; use crate::{ @@ -13,32 +11,31 @@ fn ed448_8032_vector() { let context = hex::decode("666f6f").unwrap(); #[allow(non_snake_case)] - let A = Ed448::read_G(&mut Cursor::new( - hex::decode( + let A = Ed448::read_G::<&[u8]>( + &mut hex::decode( "43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c".to_owned() + "6798c0866aea01eb00742802b8438ea4cb82169c235160627b4c3a94" + "80", ) - .unwrap(), - )) + .unwrap() + .as_ref(), + ) .unwrap(); let msg = hex::decode("03").unwrap(); - let mut sig = Cursor::new( - hex::decode( - "d4f8f6131770dd46f40867d6fd5d5055de43541f8c5e35abbcd001b3".to_owned() + - "2a89f7d2151f7647f11d8ca2ae279fb842d607217fce6e042f6815ea" + - "00" + - "0c85741de5c8da1144a6a1aba7f96de42505d7a7298524fda538fccb" + - "bb754f578c1cad10d54d0d5428407e85dcbc98a49155c13764e66c3c" + - "00", - ) - .unwrap(), - ); + let sig = hex::decode( + "d4f8f6131770dd46f40867d6fd5d5055de43541f8c5e35abbcd001b3".to_owned() + + "2a89f7d2151f7647f11d8ca2ae279fb842d607217fce6e042f6815ea" + + "00" + + "0c85741de5c8da1144a6a1aba7f96de42505d7a7298524fda538fccb" + + "bb754f578c1cad10d54d0d5428407e85dcbc98a49155c13764e66c3c" + + "00", + ) + .unwrap(); #[allow(non_snake_case)] - let R = Ed448::read_G(&mut sig).unwrap(); - let s = Ed448::read_F(&mut sig).unwrap(); + let R = Ed448::read_G::<&[u8]>(&mut sig.as_ref()).unwrap(); + let s = Ed448::read_F::<&[u8]>(&mut &sig[57 ..]).unwrap(); assert!(verify( A, diff --git a/crypto/frost/src/tests/mod.rs b/crypto/frost/src/tests/mod.rs index 72f6ccc5..74176da9 100644 --- a/crypto/frost/src/tests/mod.rs +++ b/crypto/frost/src/tests/mod.rs @@ -1,4 +1,4 @@ -use std::{io::Cursor, collections::HashMap}; +use std::collections::HashMap; use rand_core::{RngCore, CryptoRng}; @@ -6,9 +6,9 @@ use group::ff::Field; use crate::{ Curve, FrostParams, FrostCore, FrostKeys, lagrange, - key_gen::KeyGenMachine, + key_gen::{SecretShare, Commitments as KGCommitments, KeyGenMachine}, algorithm::Algorithm, - sign::{PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine}, + sign::{Writable, PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine}, }; /// Curve tests. @@ -50,15 +50,32 @@ pub fn core_gen(rng: &mut R) -> HashMap( + &mut buf.as_ref(), + FrostParams { t: THRESHOLD, n: PARTICIPANTS, i: 1 }, + ) + .unwrap() + }); } let mut secret_shares = HashMap::new(); let mut machines = machines .drain() .map(|(l, machine)| { - let (machine, shares) = + let (machine, mut shares) = machine.generate_secret_shares(rng, clone_without(&commitments, &l)).unwrap(); + let shares = shares + .drain() + .map(|(l, share)| { + let mut buf = vec![]; + share.write(&mut buf).unwrap(); + (l, SecretShare::::read::<&[u8]>(&mut buf.as_ref()).unwrap()) + }) + .collect::>(); secret_shares.insert(l, shares); (l, machine) }) @@ -74,7 +91,7 @@ pub fn core_gen(rng: &mut R) -> HashMap( .drain() .map(|(i, machine)| { let (machine, preprocess) = machine.preprocess(rng); - commitments.insert(i, Cursor::new(preprocess)); + commitments.insert(i, { + let mut buf = vec![]; + preprocess.write(&mut buf).unwrap(); + machine.read_preprocess::<&[u8]>(&mut buf.as_ref()).unwrap() + }); (i, machine) }) .collect::>(); @@ -164,7 +185,11 @@ pub fn sign( .drain() .map(|(i, machine)| { let (machine, share) = machine.sign(clone_without(&commitments, &i), msg).unwrap(); - shares.insert(i, Cursor::new(share)); + shares.insert(i, { + let mut buf = vec![]; + share.write(&mut buf).unwrap(); + machine.read_share::<&[u8]>(&mut buf.as_ref()).unwrap() + }); (i, machine) }) .collect::>(); diff --git a/crypto/frost/src/tests/vectors.rs b/crypto/frost/src/tests/vectors.rs index 747df6a1..5c48c39f 100644 --- a/crypto/frost/src/tests/vectors.rs +++ b/crypto/frost/src/tests/vectors.rs @@ -1,4 +1,4 @@ -use std::{io::Cursor, collections::HashMap}; +use std::collections::HashMap; #[cfg(test)] use std::str::FromStr; @@ -10,7 +10,10 @@ use crate::{ curve::Curve, FrostCore, FrostKeys, algorithm::{Schnorr, Hram}, - sign::{PreprocessPackage, SignMachine, SignatureMachine, AlgorithmMachine}, + sign::{ + Nonce, GeneratorCommitments, NonceCommitments, Commitments, Writable, Preprocess, + PreprocessData, SignMachine, SignatureMachine, AlgorithmMachine, + }, tests::{ clone_without, curve::test_curve, schnorr::test_schnorr, promote::test_promotion, recover, }, @@ -78,12 +81,13 @@ fn vectors_to_multisig_keys(vectors: &Vectors) -> HashMap(&mut hex::decode(secret).unwrap().as_ref()).unwrap()) .collect::>(); let verification_shares = shares.iter().map(|secret| C::generator() * secret).collect::>(); let mut keys = HashMap::new(); for i in 1 ..= u16::try_from(shares.len()).unwrap() { + // Manually re-implement the serialization for FrostCore to import this data let mut serialized = vec![]; serialized.extend(u32::try_from(C::ID.len()).unwrap().to_be_bytes()); serialized.extend(C::ID); @@ -95,7 +99,7 @@ fn vectors_to_multisig_keys(vectors: &Vectors) -> HashMap::deserialize(&mut Cursor::new(serialized)).unwrap(); + let these_keys = FrostCore::::deserialize::<&[u8]>(&mut serialized.as_ref()).unwrap(); assert_eq!(these_keys.params().t(), vectors.threshold); assert_eq!(usize::from(these_keys.params().n()), shares.len()); assert_eq!(these_keys.params().i(), i); @@ -118,8 +122,10 @@ pub fn test_with_vectors>( // Test against the vectors let keys = vectors_to_multisig_keys::(&vectors); - let group_key = C::read_G(&mut Cursor::new(hex::decode(&vectors.group_key).unwrap())).unwrap(); - let secret = C::read_F(&mut Cursor::new(hex::decode(&vectors.group_secret).unwrap())).unwrap(); + let group_key = + C::read_G::<&[u8]>(&mut hex::decode(&vectors.group_key).unwrap().as_ref()).unwrap(); + let secret = + C::read_F::<&[u8]>(&mut hex::decode(&vectors.group_secret).unwrap().as_ref()).unwrap(); assert_eq!(C::generator() * secret, group_key); assert_eq!(recover(&keys), secret); @@ -142,27 +148,36 @@ pub fn test_with_vectors>( .drain(..) .map(|(i, machine)| { let nonces = [ - C::read_F(&mut Cursor::new(hex::decode(&vectors.nonces[c][0]).unwrap())).unwrap(), - C::read_F(&mut Cursor::new(hex::decode(&vectors.nonces[c][1]).unwrap())).unwrap(), + C::read_F::<&[u8]>(&mut hex::decode(&vectors.nonces[c][0]).unwrap().as_ref()).unwrap(), + C::read_F::<&[u8]>(&mut hex::decode(&vectors.nonces[c][1]).unwrap().as_ref()).unwrap(), ]; c += 1; - let these_commitments = vec![[C::generator() * nonces[0], C::generator() * nonces[1]]]; - let machine = machine.unsafe_override_preprocess(PreprocessPackage { - nonces: vec![nonces], - commitments: vec![these_commitments.clone()], - addendum: vec![], + let these_commitments = [C::generator() * nonces[0], C::generator() * nonces[1]]; + let machine = machine.unsafe_override_preprocess(PreprocessData { + nonces: vec![Nonce(nonces)], + preprocess: Preprocess { + commitments: Commitments { + nonces: vec![NonceCommitments { + generators: vec![GeneratorCommitments(these_commitments)], + dleqs: None, + }], + }, + addendum: (), + }, }); commitments.insert( *i, - Cursor::new( - [ - these_commitments[0][0].to_bytes().as_ref(), - these_commitments[0][1].to_bytes().as_ref(), - ] - .concat() - .to_vec(), - ), + machine + .read_preprocess::<&[u8]>( + &mut [ + these_commitments[0].to_bytes().as_ref(), + these_commitments[1].to_bytes().as_ref(), + ] + .concat() + .as_ref(), + ) + .unwrap(), ); (i, machine) }) @@ -176,10 +191,15 @@ pub fn test_with_vectors>( let (machine, share) = machine.sign(clone_without(&commitments, i), &hex::decode(&vectors.msg).unwrap()).unwrap(); + let share = { + let mut buf = vec![]; + share.write(&mut buf).unwrap(); + buf + }; assert_eq!(share, hex::decode(&vectors.sig_shares[c]).unwrap()); c += 1; - shares.insert(*i, Cursor::new(share)); + shares.insert(*i, machine.read_share::<&[u8]>(&mut share.as_ref()).unwrap()); (i, machine) }) .collect::>(); diff --git a/processor/src/lib.rs b/processor/src/lib.rs index 45583dc2..79e68e35 100644 --- a/processor/src/lib.rs +++ b/processor/src/lib.rs @@ -1,4 +1,4 @@ -use std::{marker::Send, io::Cursor, collections::HashMap}; +use std::{marker::Send, collections::HashMap}; use async_trait::async_trait; use thiserror::Error; @@ -18,7 +18,7 @@ pub enum NetworkError {} #[async_trait] pub trait Network: Send { - async fn round(&mut self, data: Vec) -> Result>>, NetworkError>; + async fn round(&mut self, data: Vec) -> Result>, NetworkError>; } #[derive(Clone, Error, Debug)] diff --git a/processor/src/tests/mod.rs b/processor/src/tests/mod.rs index 27cd8f31..11520a63 100644 --- a/processor/src/tests/mod.rs +++ b/processor/src/tests/mod.rs @@ -1,5 +1,4 @@ use std::{ - io::Cursor, sync::{Arc, RwLock}, collections::HashMap, }; @@ -19,7 +18,7 @@ struct LocalNetwork { i: u16, size: u16, round: usize, - rounds: Arc>>>>>, + rounds: Arc>>>>, } impl LocalNetwork { @@ -35,13 +34,13 @@ impl LocalNetwork { #[async_trait] impl Network for LocalNetwork { - async fn round(&mut self, data: Vec) -> Result>>, NetworkError> { + async fn round(&mut self, data: Vec) -> Result>, NetworkError> { { let mut rounds = self.rounds.write().unwrap(); if rounds.len() == self.round { rounds.push(HashMap::new()); } - rounds[self.round].insert(self.i, Cursor::new(data)); + rounds[self.round].insert(self.i, data); } while { diff --git a/processor/src/wallet.rs b/processor/src/wallet.rs index b2496e8a..0448dfc4 100644 --- a/processor/src/wallet.rs +++ b/processor/src/wallet.rs @@ -7,8 +7,8 @@ use group::GroupEncoding; use transcript::{Transcript, RecommendedTranscript}; use frost::{ curve::Curve, - FrostKeys, - sign::{PreprocessMachine, SignMachine, SignatureMachine}, + FrostError, FrostKeys, + sign::{Writable, PreprocessMachine, SignMachine, SignatureMachine}, }; use crate::{ @@ -343,10 +343,44 @@ impl Wallet { self.coin.attempt_send(prepared, &included).await.map_err(SignError::CoinError)?; let (attempt, commitments) = attempt.preprocess(&mut OsRng); - let commitments = network.round(commitments).await.map_err(SignError::NetworkError)?; + let commitments = network + .round({ + let mut buf = vec![]; + commitments.write(&mut buf).unwrap(); + buf + }) + .await + .map_err(SignError::NetworkError)? + .drain() + .map(|(validator, preprocess)| { + Ok(( + validator, + attempt + .read_preprocess::<&[u8]>(&mut preprocess.as_ref()) + .map_err(|_| SignError::FrostError(FrostError::InvalidPreprocess(validator)))?, + )) + }) + .collect::, _>>()?; let (attempt, share) = attempt.sign(commitments, b"").map_err(SignError::FrostError)?; - let shares = network.round(share).await.map_err(SignError::NetworkError)?; + let shares = network + .round({ + let mut buf = vec![]; + share.write(&mut buf).unwrap(); + buf + }) + .await + .map_err(SignError::NetworkError)? + .drain() + .map(|(validator, share)| { + Ok(( + validator, + attempt + .read_share::<&[u8]>(&mut share.as_ref()) + .map_err(|_| SignError::FrostError(FrostError::InvalidShare(validator)))?, + )) + }) + .collect::, _>>()?; let tx = attempt.complete(shares).map_err(SignError::FrostError)?;