Transcript crate with both a merlin backend and a basic label len value backend

Moves binding factor/seeded RNGs over to the transcripts.
This commit is contained in:
Luke Parker
2022-05-03 07:20:24 -04:00
parent 87f38cafe4
commit bf257b3a1f
19 changed files with 282 additions and 129 deletions

View File

@@ -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"

View File

@@ -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<Input, Error> {
) -> Result<Input, Error> {
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<u8> {
// 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<R: RngCore + CryptoRng>(
&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()

View File

@@ -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<T: TranscriptTrait>(&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<u8>,
commitments_H: Vec<u8>,
image: EdwardsPoint,
AH: (dfg::EdwardsPoint, dfg::EdwardsPoint),
input: Input,
image: EdwardsPoint,
msg: Rc<RefCell<[u8; 32]>>,
mask_sum: Rc<RefCell<Scalar>>,
mask: Rc<RefCell<Scalar>>,
interim: Option<ClsagSignInterim>
}
@@ -56,19 +76,18 @@ impl Multisig {
pub fn new(
input: Input,
msg: Rc<RefCell<[u8; 32]>>,
mask_sum: Rc<RefCell<Scalar>>,
mask: Rc<RefCell<Scalar>>,
) -> Result<Multisig, MultisigError> {
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<Ed25519> 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<R: RngCore + CryptoRng>(
rng: &mut R,
view: &MultisigView<Ed25519>,
@@ -125,15 +137,14 @@ impl Algorithm<Ed25519> 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<Ed25519> for Multisig {
Ok(())
}
fn context(&self) -> Vec<u8> {
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<Self::Transcript> {
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<Ed25519> 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<Ed25519> 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<Ed25519> 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));
}

View File

@@ -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<u8> {

View File

@@ -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}};

View File

@@ -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::<blake2::Blake2b512>;
#[allow(non_snake_case)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Commitment {

View File

@@ -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());

View File

@@ -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::<ChaCha12Rng>(
let (_, mask_sum, tx_inner) = self.signable.prepare_outputs::<<Transcript as TranscriptTrait>::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))?