diff --git a/coins/monero/Cargo.toml b/coins/monero/Cargo.toml index 75bf68ab..9c2b7aa5 100644 --- a/coins/monero/Cargo.toml +++ b/coins/monero/Cargo.toml @@ -12,6 +12,7 @@ thiserror = "1" rand_core = "0.6" rand_distr = "0.4" +rand_chacha = { version = "0.3", optional = true } tiny-keccak = { version = "2.0", features = ["keccak"] } blake2 = "0.10" @@ -34,7 +35,7 @@ monero-epee-bin-serde = "1.0" reqwest = { version = "0.11", features = ["json"] } [features] -multisig = ["ff", "group", "transcript", "frost", "dalek-ff-group"] +multisig = ["ff", "group", "rand_chacha", "transcript", "frost", "dalek-ff-group"] [dev-dependencies] rand = "0.8" diff --git a/coins/monero/src/clsag/multisig.rs b/coins/monero/src/clsag/multisig.rs index 0b127539..211c7653 100644 --- a/coins/monero/src/clsag/multisig.rs +++ b/coins/monero/src/clsag/multisig.rs @@ -1,7 +1,8 @@ use core::fmt::Debug; use std::{rc::Rc, cell::RefCell}; -use rand_core::{RngCore, CryptoRng}; +use rand_core::{RngCore, CryptoRng, SeedableRng}; +use rand_chacha::ChaCha12Rng; use curve25519_dalek::{ constants::ED25519_BASEPOINT_TABLE, @@ -27,7 +28,7 @@ use crate::{ impl Input { fn transcript(&self, transcript: &mut T) { - // Doesn't dom-sep as this is considered part of the larger input signing proof + // Doesn't domain separate as this is considered part of the larger CLSAG proof // Ring index transcript.append_message(b"ring_index", &[self.i]); @@ -61,12 +62,13 @@ struct ClsagSignInterim { #[allow(non_snake_case)] #[derive(Clone, Debug)] pub struct Multisig { - commitments_H: Vec, - image: EdwardsPoint, - AH: (dfg::EdwardsPoint, dfg::EdwardsPoint), - + transcript: Transcript, input: Input, + image: EdwardsPoint, + commitments_H: Vec, + AH: (dfg::EdwardsPoint, dfg::EdwardsPoint), + msg: Rc>, mask: Rc>, @@ -75,18 +77,20 @@ pub struct Multisig { impl Multisig { pub fn new( + transcript: Transcript, input: Input, msg: Rc>, mask: Rc>, ) -> Result { Ok( Multisig { - commitments_H: vec![], - image: EdwardsPoint::identity(), - AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()), - + transcript, input, + image: EdwardsPoint::identity(), + commitments_H: vec![], + AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()), + msg, mask, @@ -138,14 +142,28 @@ impl Algorithm for Multisig { Err(FrostError::InvalidCommitmentQuantity(l, 9, serialized.len() / 32))?; } + if self.commitments_H.len() == 0 { + self.transcript.domain_separate(b"CLSAG"); + self.input.transcript(&mut self.transcript); + self.transcript.append_message(b"message", &*self.msg.borrow()); + self.transcript.append_message(b"mask", &self.mask.borrow().to_bytes()); + } + let (share, serialized) = key_image::verify_share(view, l, serialized).map_err(|_| FrostError::InvalidShare(l))?; + // Given the fact there's only ever one possible value for this, this may technically not need + // to be committed to. If signing a TX, it'll be double committed to thanks to the message + // It doesn't hurt to have though and ensures security boundaries are well formed + self.transcript.append_message(b"image_share", &share.compress().to_bytes()); self.image += share; let alt = &hash_to_point(&self.input.ring[usize::from(self.input.i)][0]); // Uses the same format FROST does for the expected commitments (nonce * G where this is nonce * H) - self.commitments_H.extend(&u64::try_from(l).unwrap().to_le_bytes()); - self.commitments_H.extend(&serialized[0 .. 64]); + // Given this is guaranteed to match commitments, which FROST commits to, this also technically + // doesn't need to be committed to if a canonical serialization is guaranteed + // It, again, doesn't hurt to include and ensures security boundaries are well formed + self.transcript.append_message(b"participant", &u64::try_from(l).unwrap().to_le_bytes()); + self.transcript.append_message(b"commitments_H", &serialized[0 .. 64]); #[allow(non_snake_case)] let H = ( @@ -171,21 +189,8 @@ impl Algorithm for Multisig { Ok(()) } - fn transcript(&self) -> Option { - let mut transcript = Self::Transcript::new(b"Monero Multisig"); - self.input.transcript(&mut transcript); - transcript.append_message(b"dom-sep", b"CLSAG"); - // Given the fact there's only ever one possible value for this, this may technically not need - // to be committed to. If signing a TX, it's be double committed to thanks to the message - // It doesn't hurt to have though and ensures security boundaries are well formed - transcript.append_message(b"image", &self.image.compress().to_bytes()); - // Given this is guaranteed to match commitments, which FROST commits to, this also technically - // doesn't need to be committed to if a canonical serialization is guaranteed - // It, again, doesn't hurt to include and ensures security boundaries are well formed - transcript.append_message(b"commitments_H", &self.commitments_H); - transcript.append_message(b"message", &*self.msg.borrow()); - transcript.append_message(b"mask", &self.mask.borrow().to_bytes()); - Some(transcript) + fn transcript(&mut self) -> &mut Self::Transcript { + &mut self.transcript } fn sign_share( @@ -203,8 +208,8 @@ impl Algorithm for Multisig { // The transcript contains private data, preventing passive adversaries from recreating this // process even if they have access to commitments (specifically, the ring index being signed // for, along with the mask which should not only require knowing the shared keys yet also the - // input commitment mask) - let mut rng = self.transcript().unwrap().seeded_rng(b"decoy_responses", None); + // input commitment masks) + let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"decoy_responses", None)); #[allow(non_snake_case)] let (clsag, c, mu_C, z, mu_P, C_out) = sign_core( diff --git a/coins/monero/src/frost.rs b/coins/monero/src/frost.rs index addbfee2..c10641c7 100644 --- a/coins/monero/src/frost.rs +++ b/coins/monero/src/frost.rs @@ -22,7 +22,7 @@ use dalek_ff_group as dfg; use crate::random_scalar; -pub(crate) type Transcript = DigestTranscript::; +pub type Transcript = DigestTranscript::; #[derive(Error, Debug)] pub enum MultisigError { diff --git a/coins/monero/src/transaction/multisig.rs b/coins/monero/src/transaction/multisig.rs index 723c40c4..d9f2685f 100644 --- a/coins/monero/src/transaction/multisig.rs +++ b/coins/monero/src/transaction/multisig.rs @@ -1,6 +1,7 @@ use std::{rc::Rc, cell::RefCell}; -use rand_core::{RngCore, CryptoRng}; +use rand_core::{RngCore, CryptoRng, SeedableRng}; +use rand_chacha::ChaCha12Rng; use curve25519_dalek::{scalar::Scalar, edwards::{EdwardsPoint, CompressedEdwardsY}}; @@ -24,6 +25,8 @@ use crate::{ pub struct TransactionMachine { leader: bool, signable: SignableTransaction, + transcript: Transcript, + our_images: Vec, mask_sum: Rc>, msg: Rc>, @@ -35,6 +38,7 @@ pub struct TransactionMachine { impl SignableTransaction { pub async fn multisig( mut self, + label: Vec, rng: &mut R, rpc: &Rpc, keys: Rc>, @@ -51,25 +55,30 @@ impl SignableTransaction { // Create a RNG out of the input shared keys, which either requires the view key or being every // sender, and the payments (address and amount), which a passive adversary may be able to know - // The use of input shared keys technically makes this one time given a competent wallet which - // can withstand the burning attack (and has a static spend key? TODO visit bounds) + // depending on how these transactions are coordinated + // The lack of dedicated entropy here is frustrating. We can probably provide entropy inclusion // if we move CLSAG ring to a Rc RefCell like msg and mask? TODO - // For the above TODO, also consider FROST's TODO of a global transcript instance - let mut transcript = Transcript::new(b"Input Mixins"); - // Does dom-sep despite not being a proof because it's a unique section (and we have no dom-sep yet) - transcript.append_message("dom-sep", "inputs_outputs"); + let mut transcript = Transcript::new(label); for input in &self.inputs { + // These outputs can only be spent once. Therefore, it forces all RNGs derived from this + // transcript (such as the one used to create one time keys) to be unique + transcript.append_message(b"input_hash", &input.tx.0); + transcript.append_message(b"input_output_index", &u64::try_from(input.o).unwrap().to_le_bytes()); + // Not including this, with a doxxed list of payments, would allow brute forcing the inputs + // to determine RNG seeds and therefore the true spends transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes()); } for payment in &self.payments { transcript.append_message(b"payment_address", &payment.0.as_bytes()); transcript.append_message(b"payment_amount", &payment.1.to_le_bytes()); } + // Not only is this an output, but this locks to the base keys to be complete with the above key offsets + transcript.append_message(b"change", &self.change.as_bytes()); // Select mixins let mixins = mixins::select( - &mut transcript.seeded_rng(b"mixins", None), + &mut ChaCha12Rng::from_seed(transcript.rng_seed(b"mixins", None)), rpc, height, &self.inputs @@ -86,6 +95,7 @@ impl SignableTransaction { clsags.push( AlgorithmMachine::new( clsag::Multisig::new( + transcript.clone(), clsag::Input::new( mixins[i].2.clone(), mixins[i].1, @@ -112,6 +122,7 @@ impl SignableTransaction { Ok(TransactionMachine { leader: keys.params().i() == included[0], signable: self, + transcript, our_images, mask_sum, msg, @@ -122,19 +133,6 @@ impl SignableTransaction { } } -// Seeded RNG so multisig participants agree on one time keys to use, preventing burning attacks -fn outputs_rng(tx: &SignableTransaction, entropy: [u8; 32]) -> ::SeededRng { - let mut transcript = Transcript::new(b"Stealth Addresses"); - // This output can only be spent once. Therefore, it forces all one time keys used here to be - // unique, even if the entropy is reused. While another transaction could use a different input - // ordering to swap which 0 is, that input set can't contain this input without being a double - // spend - transcript.append_message(b"dom-sep", b"input_0"); - transcript.append_message(b"hash", &tx.inputs[0].tx.0); - transcript.append_message(b"index", &u64::try_from(tx.inputs[0].o).unwrap().to_le_bytes()); - transcript.seeded_rng(b"tx_keys", Some(entropy)) -} - impl StateMachine for TransactionMachine { type Signature = Transaction; @@ -157,7 +155,7 @@ impl StateMachine for TransactionMachine { rng.fill_bytes(&mut entropy); serialized.extend(&entropy); - let mut rng = outputs_rng(&self.signable, entropy); + let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"tx_keys", Some(entropy))); // Safe to unwrap thanks to the dummy prepare let (commitments, mask_sum) = self.signable.prepare_outputs(&mut rng).unwrap(); self.mask_sum.replace(mask_sum); @@ -196,9 +194,11 @@ impl StateMachine for TransactionMachine { } let prep = prep.as_ref().unwrap(); - let mut rng = outputs_rng( - &self.signable, - prep[clsag_lens .. (clsag_lens + 32)].try_into().map_err(|_| FrostError::InvalidShare(l))? + let mut rng = ChaCha12Rng::from_seed( + self.transcript.rng_seed( + b"tx_keys", + Some(prep[clsag_lens .. (clsag_lens + 32)].try_into().map_err(|_| FrostError::InvalidShare(l))?) + ) ); // Not invalid outputs due to doing a dummy prep as leader let (commitments, mask_sum) = self.signable.prepare_outputs(&mut rng).map_err(|_| FrostError::InvalidShare(l))?; diff --git a/coins/monero/tests/clsag.rs b/coins/monero/tests/clsag.rs index 8028c44a..80256a2e 100644 --- a/coins/monero/tests/clsag.rs +++ b/coins/monero/tests/clsag.rs @@ -7,7 +7,7 @@ use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; use monero_serai::{random_scalar, Commitment, key_image, clsag}; #[cfg(feature = "multisig")] -use monero_serai::frost::MultisigError; +use monero_serai::frost::{MultisigError, Transcript}; #[cfg(feature = "multisig")] mod frost; @@ -84,6 +84,7 @@ fn test_multisig() -> Result<(), MultisigError> { machines.push( sign::AlgorithmMachine::new( clsag::Multisig::new( + Transcript::new(b"Monero Serai CLSAG Test".to_vec()), clsag::Input::new(ring.clone(), RING_INDEX, Commitment::new(randomness, AMOUNT)).unwrap(), Rc::new(RefCell::new([1; 32])), Rc::new(RefCell::new(Scalar::from(42u64))) diff --git a/coins/monero/tests/send_multisig.rs b/coins/monero/tests/send_multisig.rs index d60244b5..6f2fcef6 100644 --- a/coins/monero/tests/send_multisig.rs +++ b/coins/monero/tests/send_multisig.rs @@ -32,7 +32,7 @@ pub async fn send_multisig() { let t = keys[0].params().t(); // Generate an address - let view = Scalar::from_hash(Blake2b512::new().chain("Serai DEX")).0; + let view = Scalar::from_hash(Blake2b512::new().chain("Monero Serai Transaction Test")).0; let spend = keys[0].group_key().0; let addr = Address::standard( Network::Mainnet, @@ -57,6 +57,7 @@ pub async fn send_multisig() { SignableTransaction::new( vec![output.clone()], vec![(addr, amount)], addr, fee_per_byte ).unwrap().multisig( + b"Monero Serai Test Transaction".to_vec(), &mut OsRng, &rpc, keys[i - 1].clone(), diff --git a/crypto/frost/src/algorithm.rs b/crypto/frost/src/algorithm.rs index 93ed33d0..f64e075c 100644 --- a/crypto/frost/src/algorithm.rs +++ b/crypto/frost/src/algorithm.rs @@ -4,7 +4,7 @@ use rand_core::{RngCore, CryptoRng}; use group::Group; -use transcript::{Transcript, DigestTranscript}; +use transcript::Transcript; use crate::{Curve, FrostError, MultisigView}; @@ -14,6 +14,8 @@ pub trait Algorithm: Clone { /// The resulting type of the signatures this algorithm will produce type Signature: Clone + Debug; + fn transcript(&mut self) -> &mut Self::Transcript; + /// Generate an addendum to FROST"s preprocessing stage fn preprocess_addendum( rng: &mut R, @@ -30,9 +32,6 @@ pub trait Algorithm: Clone { serialized: &[u8], ) -> Result<(), FrostError>; - /// Transcript for this algorithm to be used to create the binding factor - fn transcript(&self) -> Option; - /// Sign a share with the given secret/nonce /// The secret will already have been its lagrange coefficient applied so it is the necessary /// key share @@ -41,7 +40,7 @@ pub trait Algorithm: Clone { &mut self, params: &MultisigView, nonce_sum: C::G, - b: C::F, + binding: C::F, nonce: C::F, msg: &[u8], ) -> C::F; @@ -59,6 +58,26 @@ pub trait Algorithm: Clone { ) -> bool; } +// Transcript which will create an IETF compliant serialization for the binding factor +#[derive(Clone, Debug)] +pub struct IetfTranscript(Vec); +impl Transcript for IetfTranscript { + fn domain_separate(&mut self, _: &[u8]) {} + + fn append_message(&mut self, _: &'static [u8], message: &[u8]) { + self.0.extend(message); + } + + fn challenge(&mut self, _: &'static [u8]) -> Vec { + self.0.clone() + } + + fn rng_seed(&mut self, _: &'static [u8], _: Option<[u8; 32]>) -> [u8; 32] { + unimplemented!() + } +} + + pub trait Hram: Clone { /// HRAM function to generate a challenge /// H2 from the IETF draft despite having a different argument set (not pre-formatted) @@ -68,6 +87,7 @@ pub trait Hram: Clone { #[derive(Clone)] pub struct Schnorr> { + transcript: IetfTranscript, c: Option, _hram: PhantomData, } @@ -75,6 +95,7 @@ pub struct Schnorr> { impl> Schnorr { pub fn new() -> Schnorr { Schnorr { + transcript: IetfTranscript(vec![]), c: None, _hram: PhantomData } @@ -90,11 +111,13 @@ pub struct SchnorrSignature { /// Implementation of Schnorr signatures for use with FROST impl> Algorithm for Schnorr { - // Specify a firm type which either won't matter as it won't be used or will be used (offset) and - // is accordingly solid - type Transcript = DigestTranscript::; + type Transcript = IetfTranscript; type Signature = SchnorrSignature; + fn transcript(&mut self) -> &mut Self::Transcript { + &mut self.transcript + } + fn preprocess_addendum( _: &mut R, _: &MultisigView, @@ -113,10 +136,6 @@ impl> Algorithm for Schnorr { Ok(()) } - fn transcript(&self) -> Option> { - None - } - fn sign_share( &mut self, params: &MultisigView, diff --git a/crypto/frost/src/lib.rs b/crypto/frost/src/lib.rs index 2818afa9..275c5276 100644 --- a/crypto/frost/src/lib.rs +++ b/crypto/frost/src/lib.rs @@ -93,14 +93,14 @@ pub trait Curve: Clone + Copy + PartialEq + Eq + Debug { #[allow(non_snake_case)] fn G_len() -> usize; - /// Field element from slice. Should be canonical + /// Field element from slice. Preferred to be canonical yet does not have to be // Required due to the lack of standardized encoding functions provided by ff/group // While they do technically exist, their usage of Self::Repr breaks all potential library usage // without helper functions like this #[allow(non_snake_case)] fn F_from_le_slice(slice: &[u8]) -> Result; - /// Group element from slice. Should be canonical + /// Group element from slice. Must require canonicity or risks differing binding factors #[allow(non_snake_case)] fn G_from_slice(slice: &[u8]) -> Result; diff --git a/crypto/frost/src/sign.rs b/crypto/frost/src/sign.rs index 864079be..bfbe8ff5 100644 --- a/crypto/frost/src/sign.rs +++ b/crypto/frost/src/sign.rs @@ -144,13 +144,21 @@ fn sign_with_share>( Err(FrostError::NonEmptyParticipantZero)?; } + // Domain separate FROST + { + let transcript = params.algorithm.transcript(); + transcript.domain_separate(b"FROST"); + if params.keys.offset.is_some() { + transcript.append_message(b"offset", &C::F_to_le_bytes(¶ms.keys.offset.unwrap())); + } + } + #[allow(non_snake_case)] let mut B = Vec::with_capacity(multisig_params.n + 1); B.push(None); // Commitments + a presumed 32-byte hash of the message let commitments_len = 2 * C::G_len(); - let mut b: Vec = Vec::with_capacity((multisig_params.t * commitments_len) + 32); // Parse the commitments and prepare the binding factor for l in 1 ..= multisig_params.n { @@ -160,8 +168,14 @@ fn sign_with_share>( } B.push(Some(our_preprocess.commitments)); - b.extend(&u16::try_from(l).unwrap().to_le_bytes()); - b.extend(&our_preprocess.serialized[0 .. (C::G_len() * 2)]); + { + let transcript = params.algorithm.transcript(); + transcript.append_message(b"participant", &u16::try_from(l).unwrap().to_le_bytes()); + transcript.append_message( + b"commitments", + &our_preprocess.serialized[0 .. (C::G_len() * 2)] + ); + } continue; } @@ -190,10 +204,20 @@ fn sign_with_share>( let E = C::G_from_slice(&commitments[C::G_len() .. commitments_len]) .map_err(|_| FrostError::InvalidCommitment(l))?; B.push(Some([D, E])); - b.extend(&u16::try_from(l).unwrap().to_le_bytes()); - b.extend(&commitments[0 .. commitments_len]); + { + let transcript = params.algorithm.transcript(); + transcript.append_message(b"participant", &u16::try_from(l).unwrap().to_le_bytes()); + transcript.append_message(b"commitments", &commitments[0 .. commitments_len]); + } } + // Add the message to the binding factor + let binding = { + let transcript = params.algorithm.transcript(); + transcript.append_message(b"message", &C::hash_msg(&msg)); + C::hash_to_F(&transcript.challenge(b"binding")) + }; + // Process the commitments and addendums let view = ¶ms.view; for l in ¶ms.view.included { @@ -211,45 +235,6 @@ fn sign_with_share>( )?; } - // Finish the binding factor - b.extend(&C::hash_msg(&msg)); - - // Let the algorithm provide a transcript of its variables - // While Merlin, which may or may not be the transcript used here, wants application level - // transcripts passed around to proof systems, this maintains a desired level of abstraction and - // works without issue - // Not to mention, mandating a global transcript would conflict with the IETF draft UNLESS an - // IetfTranscript was declared which ignores field names and solely does their values, with a - // fresh instantiation per sign round. That could likely be made to align without issue - // TODO: Consider Option? - let mut transcript = params.algorithm.transcript(); - if params.keys.offset.is_some() && transcript.is_none() { - transcript = Some(A::Transcript::new(b"FROST Offset")); - } - if transcript.is_some() { - // https://github.com/rust-lang/rust/issues/91345 - transcript = transcript.map(|mut t| { t.append_message(b"dom-sep", b"FROST"); t }); - } - - // If the offset functionality provided by this library is in use, include it in the transcript. - // Not compliant with the IETF spec which doesn't have a concept of offsets, nor does it use - // transcripts - if params.keys.offset.is_some() { - transcript = transcript.map( - |mut t| { t.append_message(b"offset", &C::F_to_le_bytes(¶ms.keys.offset.unwrap())); t } - ); - } - - // If a transcript was defined, move the commitments used for the binding factor into it - // Then, obtain its sum and use that as the binding factor - if transcript.is_some() { - let mut transcript = transcript.unwrap(); - transcript.append_message(b"commitments", &b); - b = transcript.challenge(b"binding", 64); - } - - let b = C::hash_to_F(&b); - #[allow(non_snake_case)] let mut Ris = vec![]; #[allow(non_snake_case)] @@ -257,7 +242,7 @@ fn sign_with_share>( for i in 0 .. params.view.included.len() { let commitments = B[params.view.included[i]].unwrap(); #[allow(non_snake_case)] - let this_R = commitments[0] + (commitments[1] * b); + let this_R = commitments[0] + (commitments[1] * binding); Ris.push(this_R); R += this_R; } @@ -266,8 +251,8 @@ fn sign_with_share>( let share = params.algorithm.sign_share( view, R, - b, - our_preprocess.nonces[0] + (our_preprocess.nonces[1] * b), + binding, + our_preprocess.nonces[0] + (our_preprocess.nonces[1] * binding), msg ); Ok((Package { Ris, R, share }, C::F_to_le_bytes(&share))) diff --git a/crypto/transcript/Cargo.toml b/crypto/transcript/Cargo.toml index 62b195bd..3c9bc9f8 100644 --- a/crypto/transcript/Cargo.toml +++ b/crypto/transcript/Cargo.toml @@ -7,9 +7,6 @@ authors = ["Luke Parker "] edition = "2021" [dependencies] -rand_core = "0.6" -rand_chacha = "0.3" - digest = "0.10" merlin = { version = "3", optional = true } diff --git a/crypto/transcript/src/lib.rs b/crypto/transcript/src/lib.rs index 6b372509..be534aae 100644 --- a/crypto/transcript/src/lib.rs +++ b/crypto/transcript/src/lib.rs @@ -5,34 +5,30 @@ mod merlin; #[cfg(features = "merlin")] pub use merlin::MerlinTranscript; -use rand_core::{RngCore, CryptoRng, SeedableRng}; -use rand_chacha::ChaCha12Rng; - use digest::Digest; pub trait Transcript { - type SeededRng: RngCore + CryptoRng; - - fn new(label: &'static [u8]) -> Self; + fn domain_separate(&mut self, label: &[u8]); fn append_message(&mut self, label: &'static [u8], message: &[u8]); - fn challenge(&mut self, label: &'static [u8], len: usize) -> Vec; - fn seeded_rng( - &self, - label: &'static [u8], - additional_entropy: Option<[u8; 32]> - ) -> Self::SeededRng; - - // TODO: Consider a domain_separate function + fn challenge(&mut self, label: &'static [u8]) -> Vec; + fn rng_seed(&mut self, label: &'static [u8], additional_entropy: Option<[u8; 32]>) -> [u8; 32]; } #[derive(Clone, Debug)] pub struct DigestTranscript(Vec, PhantomData); -impl Transcript for DigestTranscript { - // Uses ChaCha12 as even ChaCha8 should be secure yet 12 is considered a sane middleground - type SeededRng = ChaCha12Rng; - fn new(label: &'static [u8]) -> Self { - DigestTranscript(label.to_vec(), PhantomData) +impl DigestTranscript { + pub fn new(label: Vec) -> Self { + DigestTranscript(label, PhantomData) + } +} + +impl Transcript for DigestTranscript { + // It may be beneficial for each domain to be a nested transcript which is itself length prefixed + // This would go further than Merlin though and require an accurate end_domain function which has + // frustrations not worth bothering with when this shouldn't actually be meaningful + fn domain_separate(&mut self, label: &[u8]) { + self.append_message(b"domain", label); } fn append_message(&mut self, label: &'static [u8], message: &[u8]) { @@ -42,40 +38,18 @@ impl Transcript for DigestTranscript { self.0.extend(message); } - fn challenge(&mut self, label: &'static [u8], len: usize) -> Vec { + fn challenge(&mut self, label: &'static [u8]) -> Vec { self.0.extend(label); - - let mut challenge = Vec::with_capacity(len); - challenge.extend( - &D::new() - .chain_update(&self.0) - .chain_update(&0u64.to_le_bytes()).finalize() - ); - for i in 0 .. (len / challenge.len()) { - challenge.extend( - &D::new() - .chain_update(&self.0) - .chain_update(&u64::try_from(i).unwrap().to_le_bytes()) - .finalize() - ); - } - challenge.truncate(len); - challenge + D::new().chain_update(&self.0).finalize().to_vec() } - fn seeded_rng( - &self, - label: &'static [u8], - additional_entropy: Option<[u8; 32]> - ) -> Self::SeededRng { - let mut transcript = DigestTranscript::(self.0.clone(), PhantomData); + fn rng_seed(&mut self, label: &'static [u8], additional_entropy: Option<[u8; 32]>) -> [u8; 32] { if additional_entropy.is_some() { - transcript.append_message(b"additional_entropy", &additional_entropy.unwrap()); + self.append_message(b"additional_entropy", &additional_entropy.unwrap()); } - transcript.0.extend(label); let mut seed = [0; 32]; - seed.copy_from_slice(&D::digest(&transcript.0)[0 .. 32]); - ChaCha12Rng::from_seed(seed) + seed.copy_from_slice(&self.challenge(label)[0 .. 32]); + seed } } diff --git a/crypto/transcript/src/merlin.rs b/crypto/transcript/src/merlin.rs index e11b4673..cba838fe 100644 --- a/crypto/transcript/src/merlin.rs +++ b/crypto/transcript/src/merlin.rs @@ -1,42 +1,42 @@ use core::{marker::PhantomData, fmt::{Debug, Formatter}}; -use rand_core::{RngCore, CryptoRng, SeedableRng}; -use rand_chacha::ChaCha12Rng; - use digest::Digest; #[derive(Clone)] -pub struct MerlinTranscript(merlin::Transcript); +pub struct MerlinTranscript(pub merlin::Transcript); // Merlin doesn't implement Debug so provide a stub which won't panic impl Debug for MerlinTranscript { fn fmt(&self, _: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { Ok(()) } } impl Transcript for MerlinTranscript { - type SeededRng = ChaCha12Rng; - - fn new(label: &'static [u8]) -> Self { - MerlinTranscript(merlin::Transcript::new(label)) + fn domain_separate(&mut self, label: &[u8]) { + self.append_message(b"dom-sep", label); } fn append_message(&mut self, label: &'static [u8], message: &[u8]) { self.0.append_message(label, message); } - fn challenge(&mut self, label: &'static [u8], len: usize) -> Vec { + fn challenge(&mut self, label: &'static [u8]) -> Vec { let mut challenge = vec![]; - challenge.resize(len, 0); + // Uses a challenge length of 64 bytes to support wide reduction on generated scalars + // From a security level standpoint, this should just be 32 bytes + // From a Merlin standpoint, this should be variable per call + // From a practical standpoint, this is a demo file not planned to be used and anything using + // this wrapper is fine without any settings it uses + challenge.resize(64, 0); self.0.challenge_bytes(label, &mut challenge); challenge } - fn seeded_rng(&self, label: &'static [u8], additional_entropy: Option<[u8; 32]>) -> ChaCha12Rng { - let mut transcript = self.0.clone(); + fn rng_seed(&mut self, label: &'static [u8], additional_entropy: Option<[u8; 32]>) -> [u8; 32] { if additional_entropy.is_some() { transcript.append_message(b"additional_entropy", &additional_entropy.unwrap()); } + let mut seed = [0; 32]; transcript.challenge_bytes(label, &mut seed); - ChaCha12Rng::from_seed(seed) + seed } }