From bf257b3a1f100cecfd338b3fbd858bf9fbac4310 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Tue, 3 May 2022 07:20:24 -0400 Subject: [PATCH] Transcript crate with both a merlin backend and a basic label len value backend Moves binding factor/seeded RNGs over to the transcripts. --- Cargo.toml | 1 + coins/monero/Cargo.toml | 5 +- coins/monero/src/clsag/mod.rs | 27 ++---- coins/monero/src/clsag/multisig.rs | 110 +++++++++++++---------- coins/monero/src/frost.rs | 5 +- coins/monero/src/key_image/multisig.rs | 1 + coins/monero/src/lib.rs | 4 + coins/monero/src/transaction/mod.rs | 21 +++-- coins/monero/src/transaction/multisig.rs | 9 +- crypto/frost/Cargo.toml | 7 +- crypto/frost/src/algorithm.rs | 21 +++-- crypto/frost/src/lib.rs | 4 +- crypto/frost/src/sign.rs | 47 +++++----- crypto/frost/tests/common.rs | 3 +- crypto/frost/tests/key_gen_and_sign.rs | 3 +- crypto/transcript/Cargo.toml | 18 ++++ crypto/transcript/LICENSE | 21 +++++ crypto/transcript/src/lib.rs | 62 +++++++++++++ crypto/transcript/src/merlin.rs | 42 +++++++++ 19 files changed, 282 insertions(+), 129 deletions(-) create mode 100644 crypto/transcript/Cargo.toml create mode 100644 crypto/transcript/LICENSE create mode 100644 crypto/transcript/src/lib.rs create mode 100644 crypto/transcript/src/merlin.rs diff --git a/Cargo.toml b/Cargo.toml index aca21a6d..012a580f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ + "crypto/transcript", "crypto/frost", "crypto/dalek-ff-group", "coins/monero", diff --git a/coins/monero/Cargo.toml b/coins/monero/Cargo.toml index 8ad9dd14..dd695541 100644 --- a/coins/monero/Cargo.toml +++ b/coins/monero/Cargo.toml @@ -11,13 +11,14 @@ lazy_static = "1" thiserror = "1" rand_core = "0.6" -rand_chacha = { version = "0.3", optional = true } tiny-keccak = { version = "2.0", features = ["keccak"] } blake2 = "0.10" curve25519-dalek = { version = "3.2", features = ["std", "simd_backend"] } +transcript = { path = "../../crypto/transcript" } + ff = { version = "0.11", optional = true } group = { version = "0.11", optional = true } dalek-ff-group = { path = "../../crypto/dalek-ff-group", optional = true } @@ -33,7 +34,7 @@ monero-epee-bin-serde = "1.0" reqwest = { version = "0.11", features = ["json"] } [features] -multisig = ["ff", "group", "dalek-ff-group", "frost", "rand_chacha"] +multisig = ["ff", "group", "dalek-ff-group", "frost"] [dev-dependencies] rand = "0.8" diff --git a/coins/monero/src/clsag/mod.rs b/coins/monero/src/clsag/mod.rs index e156354c..a19bd574 100644 --- a/coins/monero/src/clsag/mod.rs +++ b/coins/monero/src/clsag/mod.rs @@ -1,5 +1,5 @@ -use rand_core::{RngCore, CryptoRng}; use thiserror::Error; +use rand_core::{RngCore, CryptoRng}; use curve25519_dalek::{ constants::ED25519_BASEPOINT_TABLE, @@ -40,8 +40,8 @@ pub enum Error { pub struct Input { // Ring, the index we're signing for, and the actual commitment behind it pub ring: Vec<[EdwardsPoint; 2]>, - pub i: usize, - pub commitment: Commitment, + pub i: u8, + pub commitment: Commitment } impl Input { @@ -49,7 +49,7 @@ impl Input { ring: Vec<[EdwardsPoint; 2]>, i: u8, commitment: Commitment -) -> Result { + ) -> Result { let n = ring.len(); if n > u8::MAX.into() { Err(Error::InternalError("max ring size in this library is u8 max".to_string()))?; @@ -57,29 +57,14 @@ impl Input { if i >= (n as u8) { Err(Error::InvalidRingMember(i, n as u8))?; } - let i: usize = i.into(); // Validate the commitment matches - if ring[i][1] != commitment.calculate() { + if ring[usize::from(i)][1] != commitment.calculate() { Err(Error::InvalidCommitment)?; } Ok(Input { ring, i, commitment }) } - - #[cfg(feature = "multisig")] - pub fn context(&self) -> Vec { - // Ring index - let mut context = u8::try_from(self.i).unwrap().to_le_bytes().to_vec(); - // Ring - for pair in &self.ring { - // Doesn't include key offsets as CLSAG doesn't care and won't be affected by it - context.extend(&pair[0].compress().to_bytes()); - context.extend(&pair[1].compress().to_bytes()); - } - // Doesn't include commitment as the above ring + index includes the commitment - context - } } #[allow(non_snake_case)] @@ -233,7 +218,7 @@ pub fn sign( &inputs[i].1, &inputs[i].2, mask, - &nonce * &ED25519_BASEPOINT_TABLE, nonce * hash_to_point(&inputs[i].1.ring[inputs[i].1.i][0]) + &nonce * &ED25519_BASEPOINT_TABLE, nonce * hash_to_point(&inputs[i].1.ring[usize::from(inputs[i].1.i)][0]) ); clsag.s[inputs[i].1.i as usize] = Key { key: (nonce - (c * ((mu_C * z) + (mu_P * inputs[i].0)))).to_bytes() diff --git a/coins/monero/src/clsag/multisig.rs b/coins/monero/src/clsag/multisig.rs index db9fd91d..d75a8bac 100644 --- a/coins/monero/src/clsag/multisig.rs +++ b/coins/monero/src/clsag/multisig.rs @@ -1,10 +1,7 @@ use core::fmt::Debug; use std::{rc::Rc, cell::RefCell}; -use rand_core::{RngCore, CryptoRng, SeedableRng}; -use rand_chacha::ChaCha12Rng; - -use blake2::{Digest, Blake2b512}; +use rand_core::{RngCore, CryptoRng}; use curve25519_dalek::{ constants::ED25519_BASEPOINT_TABLE, @@ -13,19 +10,43 @@ use curve25519_dalek::{ edwards::EdwardsPoint }; -use group::Group; -use dalek_ff_group as dfg; -use frost::{Curve, FrostError, algorithm::Algorithm, MultisigView}; - use monero::util::ringct::{Key, Clsag}; +use group::Group; + +use dalek_ff_group as dfg; +use transcript::Transcript as TranscriptTrait; +use frost::{Curve, FrostError, algorithm::Algorithm, MultisigView}; + use crate::{ + Transcript, hash_to_point, frost::{MultisigError, Ed25519, DLEqProof}, key_image, clsag::{Input, sign_core, verify} }; +impl Input { + pub fn transcript(&self, transcript: &mut T) { + // Ring index + transcript.append_message(b"ring_index", &[self.i]); + + // Ring + let mut ring = vec![]; + for pair in &self.ring { + // Doesn't include global output indexes as CLSAG doesn't care and won't be affected by it + // They're just a mutable reference to this data + ring.extend(&pair[0].compress().to_bytes()); + ring.extend(&pair[1].compress().to_bytes()); + } + transcript.append_message(b"ring", &ring); + + // Doesn't include the commitment's parts as the above ring + index includes the commitment + // The only potential malleability would be if the G/H relationship is known breaking the + // discrete log problem, which breaks everything already + } +} + #[allow(non_snake_case)] #[derive(Clone, Debug)] struct ClsagSignInterim { @@ -39,15 +60,14 @@ struct ClsagSignInterim { #[allow(non_snake_case)] #[derive(Clone, Debug)] pub struct Multisig { - entropy: Vec, + commitments_H: Vec, + image: EdwardsPoint, AH: (dfg::EdwardsPoint, dfg::EdwardsPoint), input: Input, - image: EdwardsPoint, - msg: Rc>, - mask_sum: Rc>, + mask: Rc>, interim: Option } @@ -56,19 +76,18 @@ impl Multisig { pub fn new( input: Input, msg: Rc>, - mask_sum: Rc>, + mask: Rc>, ) -> Result { Ok( Multisig { - entropy: vec![], + commitments_H: vec![], + image: EdwardsPoint::identity(), AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()), input, - image: EdwardsPoint::identity(), - msg, - mask_sum, + mask, interim: None } @@ -81,16 +100,9 @@ impl Multisig { } impl Algorithm for Multisig { + type Transcript = Transcript; type Signature = (Clsag, EdwardsPoint); - // We arguably don't have to commit to the nonces at all thanks to xG and yG being committed to, - // both of those being proven to have the same scalar as xH and yH, yet it doesn't hurt - // As for the image, that should be committed to by the msg, yet putting it here as well ensures - // the security bounds of this - fn addendum_commit_len() -> usize { - 3 * 32 - } - fn preprocess_addendum( rng: &mut R, view: &MultisigView, @@ -125,15 +137,14 @@ impl Algorithm for Multisig { Err(FrostError::InvalidCommitmentQuantity(l, 9, serialized.len() / 32))?; } - // Use everyone's commitments to derive a random source all signers can agree upon - // Cannot be manipulated to effect and all signers must, and will, know this - self.entropy.extend(&l.to_le_bytes()); - self.entropy.extend(&serialized[0 .. Multisig::addendum_commit_len()]); - let (share, serialized) = key_image::verify_share(view, l, serialized).map_err(|_| FrostError::InvalidShare(l))?; self.image += share; - let alt = &hash_to_point(&self.input.ring[self.input.i][0]); + 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]); #[allow(non_snake_case)] let H = ( @@ -159,12 +170,20 @@ impl Algorithm for Multisig { Ok(()) } - fn context(&self) -> Vec { - let mut context = Vec::with_capacity(32 + 32 + 1 + (2 * 11 * 32)); - context.extend(&*self.msg.borrow()); - context.extend(&self.mask_sum.borrow().to_bytes()); - context.extend(&self.input.context()); - context + fn transcript(&self) -> Option { + let mut transcript = Self::Transcript::new(b"CLSAG"); + self.input.transcript(&mut transcript); + // 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 sign_share( @@ -178,13 +197,12 @@ impl Algorithm for Multisig { // Apply the binding factor to the H variant of the nonce self.AH.0 += self.AH.1 * b; - // Use the context with the entropy to prevent passive observers of messages from being able to - // break privacy, as the context includes the index of the output in the ring, which can only - // be known if you have the view key and know which of the wallet's TXOs is being spent - let mut seed = b"CLSAG_randomness".to_vec(); - seed.extend(&self.context()); - seed.extend(&self.entropy); - let mut rng = ChaCha12Rng::from_seed(Blake2b512::digest(seed)[0 .. 32].try_into().unwrap()); + // Use the transcript to get a seeded random number generator + // 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); #[allow(non_snake_case)] let (clsag, c, mu_C, z, mu_P, C_out) = sign_core( @@ -192,7 +210,7 @@ impl Algorithm for Multisig { &self.msg.borrow(), &self.input, &self.image, - *self.mask_sum.borrow(), + *self.mask.borrow(), nonce_sum.0, self.AH.0.0 ); @@ -212,7 +230,7 @@ impl Algorithm for Multisig { let interim = self.interim.as_ref().unwrap(); let mut clsag = interim.clsag.clone(); - clsag.s[self.input.i] = Key { key: (sum.0 - interim.s).to_bytes() }; + clsag.s[usize::from(self.input.i)] = Key { key: (sum.0 - interim.s).to_bytes() }; if verify(&clsag, &self.msg.borrow(), self.image, &self.input.ring, interim.C_out) { return Some((clsag, interim.C_out)); } diff --git a/coins/monero/src/frost.rs b/coins/monero/src/frost.rs index 4523665d..413ac95a 100644 --- a/coins/monero/src/frost.rs +++ b/coins/monero/src/frost.rs @@ -1,7 +1,7 @@ use core::convert::TryInto; -use rand_core::{RngCore, CryptoRng}; use thiserror::Error; +use rand_core::{RngCore, CryptoRng}; use blake2::{digest::Update, Digest, Blake2b512}; @@ -12,7 +12,6 @@ use curve25519_dalek::{ edwards::EdwardsPoint as DPoint }; -use dalek_ff_group::EdwardsPoint; use ff::PrimeField; use group::Group; @@ -56,7 +55,7 @@ impl Curve for Ed25519 { } fn multiexp_vartime(scalars: &[Self::F], points: &[Self::G]) -> Self::G { - EdwardsPoint(DPoint::vartime_multiscalar_mul(scalars, points)) + dfg::EdwardsPoint(DPoint::vartime_multiscalar_mul(scalars, points)) } fn hash_msg(msg: &[u8]) -> Vec { diff --git a/coins/monero/src/key_image/multisig.rs b/coins/monero/src/key_image/multisig.rs index 978bed63..abb1d77e 100644 --- a/coins/monero/src/key_image/multisig.rs +++ b/coins/monero/src/key_image/multisig.rs @@ -1,6 +1,7 @@ use rand_core::{RngCore, CryptoRng}; use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY}; + use frost::MultisigView; use crate::{hash_to_point, frost::{MultisigError, Ed25519, DLEqProof}}; diff --git a/coins/monero/src/lib.rs b/coins/monero/src/lib.rs index fdda8a9f..d7cefbd8 100644 --- a/coins/monero/src/lib.rs +++ b/coins/monero/src/lib.rs @@ -12,6 +12,8 @@ use curve25519_dalek::{ use monero::util::key::H; +use transcript::DigestTranscript; + #[cfg(feature = "multisig")] pub mod frost; @@ -48,6 +50,8 @@ lazy_static! { static ref H_TABLE: EdwardsBasepointTable = EdwardsBasepointTable::create(&H.point.decompress().unwrap()); } +pub(crate) type Transcript = DigestTranscript::; + #[allow(non_snake_case)] #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub struct Commitment { diff --git a/coins/monero/src/transaction/mod.rs b/coins/monero/src/transaction/mod.rs index f27ed4dc..298d37d2 100644 --- a/coins/monero/src/transaction/mod.rs +++ b/coins/monero/src/transaction/mod.rs @@ -1,8 +1,6 @@ -use rand_core::{RngCore, CryptoRng, SeedableRng}; -use rand_chacha::ChaCha12Rng; use thiserror::Error; -use blake2::{Digest, Blake2b512}; +use rand_core::{RngCore, CryptoRng}; use curve25519_dalek::{ constants::ED25519_BASEPOINT_TABLE, @@ -26,10 +24,13 @@ use monero::{ } }; +use transcript::Transcript as TranscriptTrait; + #[cfg(feature = "multisig")] use frost::FrostError; use crate::{ + Transcript, Commitment, random_scalar, hash, hash_to_scalar, @@ -264,6 +265,9 @@ impl SignableTransaction { ) } + // This could be refactored so prep, a multisig-required variable, is used only by multisig + // Not shimmed by the single signer API as well + // This would enable moving Transcript as a whole to the multisig feature fn prepare_outputs<'a, R: RngCore + CryptoRng>( &self, prep: &mut Preparation<'a, R> @@ -289,6 +293,7 @@ impl SignableTransaction { match prep { Preparation::Leader(ref mut rng) => { // The Leader generates the entropy for the one time keys and the bulletproof + // This prevents de-anonymization via recalculation of the randomness which is deterministic rng.fill_bytes(&mut entropy); }, Preparation::Follower(e, b) => { @@ -297,16 +302,14 @@ impl SignableTransaction { } } - let mut seed = b"StealthAddress_randomness".to_vec(); - // Leader selected entropy to prevent de-anonymization via recalculation of randomness - seed.extend(&entropy); + let mut transcript = Transcript::new(b"StealthAddress"); // This output can only be spent once. Therefore, it forces all one time keys used here to be // unique, even if the leader reuses entropy. 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 - seed.extend(&self.inputs[0].tx.0); - seed.extend(&self.inputs[0].o.to_le_bytes()); - let mut rng = ChaCha12Rng::from_seed(Blake2b512::digest(seed)[0 .. 32].try_into().unwrap()); + transcript.append_message(b"hash", &self.inputs[0].tx.0); + transcript.append_message(b"index", &u64::try_from(self.inputs[0].o).unwrap().to_le_bytes()); + let mut rng = transcript.seeded_rng(b"tx_keys", Some(entropy)); let mut outputs = Vec::with_capacity(payments.len()); let mut commitments = Vec::with_capacity(payments.len()); diff --git a/coins/monero/src/transaction/multisig.rs b/coins/monero/src/transaction/multisig.rs index e740b5b4..d1350568 100644 --- a/coins/monero/src/transaction/multisig.rs +++ b/coins/monero/src/transaction/multisig.rs @@ -1,12 +1,9 @@ use std::{rc::Rc, cell::RefCell}; use rand_core::{RngCore, CryptoRng}; -use rand_chacha::ChaCha12Rng; use curve25519_dalek::{scalar::Scalar, edwards::{EdwardsPoint, CompressedEdwardsY}}; -use frost::{FrostError, MultisigKeys, MultisigParams, sign::{State, StateMachine, AlgorithmMachine}}; - use monero::{ Hash, VarInt, consensus::deserialize, @@ -14,7 +11,11 @@ use monero::{ blockdata::transaction::{KeyImage, TxIn, Transaction} }; +use transcript::Transcript as TranscriptTrait; +use frost::{FrostError, MultisigKeys, MultisigParams, sign::{State, StateMachine, AlgorithmMachine}}; + use crate::{ + Transcript, frost::Ed25519, key_image, clsag, @@ -150,7 +151,7 @@ impl StateMachine for TransactionMachine { let prep = prep.as_ref().unwrap(); // Handle the prep with a seeded RNG type to make rustc happy - let (_, mask_sum, tx_inner) = self.signable.prepare_outputs::( + let (_, mask_sum, tx_inner) = self.signable.prepare_outputs::<::SeededRng>( &mut Preparation::Follower( prep[clsag_lens .. (clsag_lens + 32)].try_into().map_err(|_| FrostError::InvalidCommitment(l))?, deserialize(&prep[(clsag_lens + 32) .. prep.len()]).map_err(|_| FrostError::InvalidCommitment(l))? diff --git a/crypto/frost/Cargo.toml b/crypto/frost/Cargo.toml index 92205db8..4f6fc5ad 100644 --- a/crypto/frost/Cargo.toml +++ b/crypto/frost/Cargo.toml @@ -3,18 +3,19 @@ name = "frost" version = "0.1.0" description = "Implementation of FROST over ff/group" license = "MIT" -authors = ["kayabaNerve (Luke Parker) "] +authors = ["Luke Parker "] edition = "2021" [dependencies] -digest = "0.10" +thiserror = "1" rand_core = "0.6" ff = "0.11" group = "0.11" -thiserror = "1" +blake2 = "0.10" +transcript = { path = "../transcript" } [dev-dependencies] rand = "0.8" diff --git a/crypto/frost/src/algorithm.rs b/crypto/frost/src/algorithm.rs index 054b46eb..93ed33d0 100644 --- a/crypto/frost/src/algorithm.rs +++ b/crypto/frost/src/algorithm.rs @@ -4,16 +4,16 @@ use rand_core::{RngCore, CryptoRng}; use group::Group; +use transcript::{Transcript, DigestTranscript}; + use crate::{Curve, FrostError, MultisigView}; /// Algorithm to use FROST with pub trait Algorithm: Clone { + type Transcript: Transcript + Clone + Debug; /// The resulting type of the signatures this algorithm will produce type Signature: Clone + Debug; - /// The amount of bytes from each participant's addendum to commit to - fn addendum_commit_len() -> usize; - /// Generate an addendum to FROST"s preprocessing stage fn preprocess_addendum( rng: &mut R, @@ -30,8 +30,8 @@ pub trait Algorithm: Clone { serialized: &[u8], ) -> Result<(), FrostError>; - /// Context for this algorithm to be hashed into b, and therefore committed to - fn context(&self) -> Vec; + /// 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 @@ -90,12 +90,11 @@ 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 Signature = SchnorrSignature; - fn addendum_commit_len() -> usize { - 0 - } - fn preprocess_addendum( _: &mut R, _: &MultisigView, @@ -114,8 +113,8 @@ impl> Algorithm for Schnorr { Ok(()) } - fn context(&self) -> Vec { - vec![] + fn transcript(&self) -> Option> { + None } fn sign_share( diff --git a/crypto/frost/src/lib.rs b/crypto/frost/src/lib.rs index 4da270e8..3d55d4e4 100644 --- a/crypto/frost/src/lib.rs +++ b/crypto/frost/src/lib.rs @@ -1,10 +1,10 @@ use core::{ops::Mul, fmt::Debug}; +use thiserror::Error; + use ff::{Field, PrimeField}; use group::{Group, GroupOps, ScalarMul}; -use thiserror::Error; - pub mod key_gen; pub mod algorithm; pub mod sign; diff --git a/crypto/frost/src/sign.rs b/crypto/frost/src/sign.rs index eddc614a..83d6b676 100644 --- a/crypto/frost/src/sign.rs +++ b/crypto/frost/src/sign.rs @@ -6,6 +6,8 @@ use rand_core::{RngCore, CryptoRng}; use ff::{Field, PrimeField}; use group::Group; +use transcript::Transcript; + use crate::{Curve, FrostError, MultisigParams, MultisigKeys, MultisigView, algorithm::Algorithm}; /// Calculate the lagrange coefficient @@ -142,17 +144,13 @@ fn sign_with_share>( Err(FrostError::NonEmptyParticipantZero)?; } - let commitments_len = C::G_len() * 2; - // Allow algorithms to commit to more data than just the included nonces - // Not IETF draft compliant yet it doesn't prevent a compliant Schnorr algorithm from being used - // with this library, which does ship one - let commit_len = commitments_len + A::addendum_commit_len(); #[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 mut b: Vec = Vec::with_capacity((multisig_params.t * 2 * C::G_len()) + 32); + 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 { @@ -163,7 +161,7 @@ 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 .. commit_len]); + b.extend(&our_preprocess.serialized[0 .. (C::G_len() * 2)]); continue; } @@ -193,7 +191,7 @@ fn sign_with_share>( .map_err(|_| FrostError::InvalidCommitment(l))?; B.push(Some([D, E])); b.extend(&u16::try_from(l).unwrap().to_le_bytes()); - b.extend(&commitments[0 .. commit_len]); + b.extend(&commitments[0 .. commitments_len]); } // Process the commitments and addendums @@ -216,26 +214,27 @@ fn sign_with_share>( // Finish the binding factor b.extend(&C::hash_msg(&msg)); - // If the following are used with certain lengths, it is possible to craft distinct - // commitments/messages/contexts with the same binding factor. While we can't length prefix the - // commitments, unfortunately, we can tag and length prefix the following + // 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 + let mut transcript = params.algorithm.transcript(); - // If the offset functionality provided by this library is in use, include it in the binding - // factor. Not compliant with the IETF spec which doesn't have a concept of offsets + // 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() { - b.extend(b"offset"); - b.extend(u64::try_from(C::F_len()).unwrap().to_le_bytes()); - b.extend(&C::F_to_le_bytes(¶ms.keys.offset.unwrap())); + let mut offset_transcript = transcript.unwrap_or(A::Transcript::new(b"FROST_offset")); + offset_transcript.append_message(b"offset", &C::F_to_le_bytes(¶ms.keys.offset.unwrap())); + transcript = Some(offset_transcript); } - // Also include any context the algorithm may want to specify. Again not compliant with the IETF - // spec which doesn't considered there may be signatures other than Schnorr being generated with - // FROST - let context = params.algorithm.context(); - if context.len() != 0 { - b.extend(b"context"); - b.extend(u64::try_from(context.len()).unwrap().to_le_bytes()); - b.extend(&context); + // 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); diff --git a/crypto/frost/tests/common.rs b/crypto/frost/tests/common.rs index 0ef2902b..286372c1 100644 --- a/crypto/frost/tests/common.rs +++ b/crypto/frost/tests/common.rs @@ -1,10 +1,9 @@ use core::convert::TryInto; -use digest::Digest; use ff::PrimeField; use group::GroupEncoding; -use sha2::{Sha256, Sha512}; +use sha2::{Digest, Sha256, Sha512}; use k256::{ elliptic_curve::{generic_array::GenericArray, bigint::{ArrayEncoding, U512}, ops::Reduce}, diff --git a/crypto/frost/tests/key_gen_and_sign.rs b/crypto/frost/tests/key_gen_and_sign.rs index 08f08bff..740ea811 100644 --- a/crypto/frost/tests/key_gen_and_sign.rs +++ b/crypto/frost/tests/key_gen_and_sign.rs @@ -2,8 +2,7 @@ use std::rc::Rc; use rand::{RngCore, rngs::OsRng}; -use digest::Digest; -use sha2::Sha256; +use sha2::{Digest, Sha256}; use frost::{ Curve, diff --git a/crypto/transcript/Cargo.toml b/crypto/transcript/Cargo.toml new file mode 100644 index 00000000..62b195bd --- /dev/null +++ b/crypto/transcript/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "transcript" +version = "0.1.0" +description = "A simple transcript definition" +license = "MIT" +authors = ["Luke Parker "] +edition = "2021" + +[dependencies] +rand_core = "0.6" +rand_chacha = "0.3" + +digest = "0.10" + +merlin = { version = "3", optional = true } + +[features] +merlin = ["dep:merlin"] diff --git a/crypto/transcript/LICENSE b/crypto/transcript/LICENSE new file mode 100644 index 00000000..f05b748b --- /dev/null +++ b/crypto/transcript/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crypto/transcript/src/lib.rs b/crypto/transcript/src/lib.rs new file mode 100644 index 00000000..a3e0bd31 --- /dev/null +++ b/crypto/transcript/src/lib.rs @@ -0,0 +1,62 @@ +use core::{marker::PhantomData, fmt::Debug}; + +#[cfg(features = "merlin")] +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 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; +} + +#[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) + } + + fn append_message(&mut self, label: &'static [u8], message: &[u8]) { + self.0.extend(label); + // Assumes messages don't exceed 16 exabytes + self.0.extend(u64::try_from(message.len()).unwrap().to_le_bytes()); + self.0.extend(message); + } + + fn challenge(&mut self, label: &'static [u8], len: usize) -> 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 + } + + fn seeded_rng(&self, label: &'static [u8], additional_entropy: Option<[u8; 32]>) -> Self::SeededRng { + let mut transcript = DigestTranscript::(self.0.clone(), PhantomData); + if additional_entropy.is_some() { + transcript.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) + } +} diff --git a/crypto/transcript/src/merlin.rs b/crypto/transcript/src/merlin.rs new file mode 100644 index 00000000..e11b4673 --- /dev/null +++ b/crypto/transcript/src/merlin.rs @@ -0,0 +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); +// 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 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 { + let mut challenge = vec![]; + challenge.resize(len, 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(); + 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) + } +}