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)
+ }
+}