Use a global transcript

This commit is contained in:
Luke Parker
2022-05-06 07:33:08 -04:00
parent cc9c2e0d40
commit 964cb357e6
12 changed files with 165 additions and 182 deletions

View File

@@ -12,6 +12,7 @@ thiserror = "1"
rand_core = "0.6" rand_core = "0.6"
rand_distr = "0.4" rand_distr = "0.4"
rand_chacha = { version = "0.3", optional = true }
tiny-keccak = { version = "2.0", features = ["keccak"] } tiny-keccak = { version = "2.0", features = ["keccak"] }
blake2 = "0.10" blake2 = "0.10"
@@ -34,7 +35,7 @@ monero-epee-bin-serde = "1.0"
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json"] }
[features] [features]
multisig = ["ff", "group", "transcript", "frost", "dalek-ff-group"] multisig = ["ff", "group", "rand_chacha", "transcript", "frost", "dalek-ff-group"]
[dev-dependencies] [dev-dependencies]
rand = "0.8" rand = "0.8"

View File

@@ -1,7 +1,8 @@
use core::fmt::Debug; use core::fmt::Debug;
use std::{rc::Rc, cell::RefCell}; use std::{rc::Rc, cell::RefCell};
use rand_core::{RngCore, CryptoRng}; use rand_core::{RngCore, CryptoRng, SeedableRng};
use rand_chacha::ChaCha12Rng;
use curve25519_dalek::{ use curve25519_dalek::{
constants::ED25519_BASEPOINT_TABLE, constants::ED25519_BASEPOINT_TABLE,
@@ -27,7 +28,7 @@ use crate::{
impl Input { impl Input {
fn transcript<T: TranscriptTrait>(&self, transcript: &mut T) { fn transcript<T: TranscriptTrait>(&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 // Ring index
transcript.append_message(b"ring_index", &[self.i]); transcript.append_message(b"ring_index", &[self.i]);
@@ -61,12 +62,13 @@ struct ClsagSignInterim {
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Multisig { pub struct Multisig {
commitments_H: Vec<u8>, transcript: Transcript,
image: EdwardsPoint,
AH: (dfg::EdwardsPoint, dfg::EdwardsPoint),
input: Input, input: Input,
image: EdwardsPoint,
commitments_H: Vec<u8>,
AH: (dfg::EdwardsPoint, dfg::EdwardsPoint),
msg: Rc<RefCell<[u8; 32]>>, msg: Rc<RefCell<[u8; 32]>>,
mask: Rc<RefCell<Scalar>>, mask: Rc<RefCell<Scalar>>,
@@ -75,18 +77,20 @@ pub struct Multisig {
impl Multisig { impl Multisig {
pub fn new( pub fn new(
transcript: Transcript,
input: Input, input: Input,
msg: Rc<RefCell<[u8; 32]>>, msg: Rc<RefCell<[u8; 32]>>,
mask: Rc<RefCell<Scalar>>, mask: Rc<RefCell<Scalar>>,
) -> Result<Multisig, MultisigError> { ) -> Result<Multisig, MultisigError> {
Ok( Ok(
Multisig { Multisig {
commitments_H: vec![], transcript,
image: EdwardsPoint::identity(),
AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()),
input, input,
image: EdwardsPoint::identity(),
commitments_H: vec![],
AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()),
msg, msg,
mask, mask,
@@ -138,14 +142,28 @@ impl Algorithm<Ed25519> for Multisig {
Err(FrostError::InvalidCommitmentQuantity(l, 9, serialized.len() / 32))?; 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))?; 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; self.image += share;
let alt = &hash_to_point(&self.input.ring[usize::from(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) // 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()); // Given this is guaranteed to match commitments, which FROST commits to, this also technically
self.commitments_H.extend(&serialized[0 .. 64]); // 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)] #[allow(non_snake_case)]
let H = ( let H = (
@@ -171,21 +189,8 @@ impl Algorithm<Ed25519> for Multisig {
Ok(()) Ok(())
} }
fn transcript(&self) -> Option<Self::Transcript> { fn transcript(&mut self) -> &mut Self::Transcript {
let mut transcript = Self::Transcript::new(b"Monero Multisig"); &mut self.transcript
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 sign_share( fn sign_share(
@@ -203,8 +208,8 @@ impl Algorithm<Ed25519> for Multisig {
// The transcript contains private data, preventing passive adversaries from recreating this // 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 // 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 // for, along with the mask which should not only require knowing the shared keys yet also the
// input commitment mask) // input commitment masks)
let mut rng = self.transcript().unwrap().seeded_rng(b"decoy_responses", None); let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"decoy_responses", None));
#[allow(non_snake_case)] #[allow(non_snake_case)]
let (clsag, c, mu_C, z, mu_P, C_out) = sign_core( let (clsag, c, mu_C, z, mu_P, C_out) = sign_core(

View File

@@ -22,7 +22,7 @@ use dalek_ff_group as dfg;
use crate::random_scalar; use crate::random_scalar;
pub(crate) type Transcript = DigestTranscript::<blake2::Blake2b512>; pub type Transcript = DigestTranscript::<blake2::Blake2b512>;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum MultisigError { pub enum MultisigError {

View File

@@ -1,6 +1,7 @@
use std::{rc::Rc, cell::RefCell}; 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}}; use curve25519_dalek::{scalar::Scalar, edwards::{EdwardsPoint, CompressedEdwardsY}};
@@ -24,6 +25,8 @@ use crate::{
pub struct TransactionMachine { pub struct TransactionMachine {
leader: bool, leader: bool,
signable: SignableTransaction, signable: SignableTransaction,
transcript: Transcript,
our_images: Vec<EdwardsPoint>, our_images: Vec<EdwardsPoint>,
mask_sum: Rc<RefCell<Scalar>>, mask_sum: Rc<RefCell<Scalar>>,
msg: Rc<RefCell<[u8; 32]>>, msg: Rc<RefCell<[u8; 32]>>,
@@ -35,6 +38,7 @@ pub struct TransactionMachine {
impl SignableTransaction { impl SignableTransaction {
pub async fn multisig<R: RngCore + CryptoRng>( pub async fn multisig<R: RngCore + CryptoRng>(
mut self, mut self,
label: Vec<u8>,
rng: &mut R, rng: &mut R,
rpc: &Rpc, rpc: &Rpc,
keys: Rc<MultisigKeys<Ed25519>>, keys: Rc<MultisigKeys<Ed25519>>,
@@ -51,25 +55,30 @@ impl SignableTransaction {
// Create a RNG out of the input shared keys, which either requires the view key or being every // 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 // 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 // depending on how these transactions are coordinated
// can withstand the burning attack (and has a static spend key? TODO visit bounds)
// The lack of dedicated entropy here is frustrating. We can probably provide entropy inclusion // 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 // 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(label);
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");
for input in &self.inputs { 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()); transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes());
} }
for payment in &self.payments { for payment in &self.payments {
transcript.append_message(b"payment_address", &payment.0.as_bytes()); transcript.append_message(b"payment_address", &payment.0.as_bytes());
transcript.append_message(b"payment_amount", &payment.1.to_le_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 // Select mixins
let mixins = mixins::select( let mixins = mixins::select(
&mut transcript.seeded_rng(b"mixins", None), &mut ChaCha12Rng::from_seed(transcript.rng_seed(b"mixins", None)),
rpc, rpc,
height, height,
&self.inputs &self.inputs
@@ -86,6 +95,7 @@ impl SignableTransaction {
clsags.push( clsags.push(
AlgorithmMachine::new( AlgorithmMachine::new(
clsag::Multisig::new( clsag::Multisig::new(
transcript.clone(),
clsag::Input::new( clsag::Input::new(
mixins[i].2.clone(), mixins[i].2.clone(),
mixins[i].1, mixins[i].1,
@@ -112,6 +122,7 @@ impl SignableTransaction {
Ok(TransactionMachine { Ok(TransactionMachine {
leader: keys.params().i() == included[0], leader: keys.params().i() == included[0],
signable: self, signable: self,
transcript,
our_images, our_images,
mask_sum, mask_sum,
msg, 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]) -> <Transcript as TranscriptTrait>::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 { impl StateMachine for TransactionMachine {
type Signature = Transaction; type Signature = Transaction;
@@ -157,7 +155,7 @@ impl StateMachine for TransactionMachine {
rng.fill_bytes(&mut entropy); rng.fill_bytes(&mut entropy);
serialized.extend(&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 // Safe to unwrap thanks to the dummy prepare
let (commitments, mask_sum) = self.signable.prepare_outputs(&mut rng).unwrap(); let (commitments, mask_sum) = self.signable.prepare_outputs(&mut rng).unwrap();
self.mask_sum.replace(mask_sum); self.mask_sum.replace(mask_sum);
@@ -196,9 +194,11 @@ impl StateMachine for TransactionMachine {
} }
let prep = prep.as_ref().unwrap(); let prep = prep.as_ref().unwrap();
let mut rng = outputs_rng( let mut rng = ChaCha12Rng::from_seed(
&self.signable, self.transcript.rng_seed(
prep[clsag_lens .. (clsag_lens + 32)].try_into().map_err(|_| FrostError::InvalidShare(l))? 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 // 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))?; let (commitments, mask_sum) = self.signable.prepare_outputs(&mut rng).map_err(|_| FrostError::InvalidShare(l))?;

View File

@@ -7,7 +7,7 @@ use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
use monero_serai::{random_scalar, Commitment, key_image, clsag}; use monero_serai::{random_scalar, Commitment, key_image, clsag};
#[cfg(feature = "multisig")] #[cfg(feature = "multisig")]
use monero_serai::frost::MultisigError; use monero_serai::frost::{MultisigError, Transcript};
#[cfg(feature = "multisig")] #[cfg(feature = "multisig")]
mod frost; mod frost;
@@ -84,6 +84,7 @@ fn test_multisig() -> Result<(), MultisigError> {
machines.push( machines.push(
sign::AlgorithmMachine::new( sign::AlgorithmMachine::new(
clsag::Multisig::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(), clsag::Input::new(ring.clone(), RING_INDEX, Commitment::new(randomness, AMOUNT)).unwrap(),
Rc::new(RefCell::new([1; 32])), Rc::new(RefCell::new([1; 32])),
Rc::new(RefCell::new(Scalar::from(42u64))) Rc::new(RefCell::new(Scalar::from(42u64)))

View File

@@ -32,7 +32,7 @@ pub async fn send_multisig() {
let t = keys[0].params().t(); let t = keys[0].params().t();
// Generate an address // 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 spend = keys[0].group_key().0;
let addr = Address::standard( let addr = Address::standard(
Network::Mainnet, Network::Mainnet,
@@ -57,6 +57,7 @@ pub async fn send_multisig() {
SignableTransaction::new( SignableTransaction::new(
vec![output.clone()], vec![(addr, amount)], addr, fee_per_byte vec![output.clone()], vec![(addr, amount)], addr, fee_per_byte
).unwrap().multisig( ).unwrap().multisig(
b"Monero Serai Test Transaction".to_vec(),
&mut OsRng, &mut OsRng,
&rpc, &rpc,
keys[i - 1].clone(), keys[i - 1].clone(),

View File

@@ -4,7 +4,7 @@ use rand_core::{RngCore, CryptoRng};
use group::Group; use group::Group;
use transcript::{Transcript, DigestTranscript}; use transcript::Transcript;
use crate::{Curve, FrostError, MultisigView}; use crate::{Curve, FrostError, MultisigView};
@@ -14,6 +14,8 @@ pub trait Algorithm<C: Curve>: Clone {
/// The resulting type of the signatures this algorithm will produce /// The resulting type of the signatures this algorithm will produce
type Signature: Clone + Debug; type Signature: Clone + Debug;
fn transcript(&mut self) -> &mut Self::Transcript;
/// Generate an addendum to FROST"s preprocessing stage /// Generate an addendum to FROST"s preprocessing stage
fn preprocess_addendum<R: RngCore + CryptoRng>( fn preprocess_addendum<R: RngCore + CryptoRng>(
rng: &mut R, rng: &mut R,
@@ -30,9 +32,6 @@ pub trait Algorithm<C: Curve>: Clone {
serialized: &[u8], serialized: &[u8],
) -> Result<(), FrostError>; ) -> Result<(), FrostError>;
/// Transcript for this algorithm to be used to create the binding factor
fn transcript(&self) -> Option<Self::Transcript>;
/// Sign a share with the given secret/nonce /// Sign a share with the given secret/nonce
/// The secret will already have been its lagrange coefficient applied so it is the necessary /// The secret will already have been its lagrange coefficient applied so it is the necessary
/// key share /// key share
@@ -41,7 +40,7 @@ pub trait Algorithm<C: Curve>: Clone {
&mut self, &mut self,
params: &MultisigView<C>, params: &MultisigView<C>,
nonce_sum: C::G, nonce_sum: C::G,
b: C::F, binding: C::F,
nonce: C::F, nonce: C::F,
msg: &[u8], msg: &[u8],
) -> C::F; ) -> C::F;
@@ -59,6 +58,26 @@ pub trait Algorithm<C: Curve>: Clone {
) -> bool; ) -> bool;
} }
// Transcript which will create an IETF compliant serialization for the binding factor
#[derive(Clone, Debug)]
pub struct IetfTranscript(Vec<u8>);
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<u8> {
self.0.clone()
}
fn rng_seed(&mut self, _: &'static [u8], _: Option<[u8; 32]>) -> [u8; 32] {
unimplemented!()
}
}
pub trait Hram<C: Curve>: Clone { pub trait Hram<C: Curve>: Clone {
/// HRAM function to generate a challenge /// HRAM function to generate a challenge
/// H2 from the IETF draft despite having a different argument set (not pre-formatted) /// H2 from the IETF draft despite having a different argument set (not pre-formatted)
@@ -68,6 +87,7 @@ pub trait Hram<C: Curve>: Clone {
#[derive(Clone)] #[derive(Clone)]
pub struct Schnorr<C: Curve, H: Hram<C>> { pub struct Schnorr<C: Curve, H: Hram<C>> {
transcript: IetfTranscript,
c: Option<C::F>, c: Option<C::F>,
_hram: PhantomData<H>, _hram: PhantomData<H>,
} }
@@ -75,6 +95,7 @@ pub struct Schnorr<C: Curve, H: Hram<C>> {
impl<C: Curve, H: Hram<C>> Schnorr<C, H> { impl<C: Curve, H: Hram<C>> Schnorr<C, H> {
pub fn new() -> Schnorr<C, H> { pub fn new() -> Schnorr<C, H> {
Schnorr { Schnorr {
transcript: IetfTranscript(vec![]),
c: None, c: None,
_hram: PhantomData _hram: PhantomData
} }
@@ -90,11 +111,13 @@ pub struct SchnorrSignature<C: Curve> {
/// Implementation of Schnorr signatures for use with FROST /// Implementation of Schnorr signatures for use with FROST
impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> { impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> {
// Specify a firm type which either won't matter as it won't be used or will be used (offset) and type Transcript = IetfTranscript;
// is accordingly solid
type Transcript = DigestTranscript::<blake2::Blake2b512>;
type Signature = SchnorrSignature<C>; type Signature = SchnorrSignature<C>;
fn transcript(&mut self) -> &mut Self::Transcript {
&mut self.transcript
}
fn preprocess_addendum<R: RngCore + CryptoRng>( fn preprocess_addendum<R: RngCore + CryptoRng>(
_: &mut R, _: &mut R,
_: &MultisigView<C>, _: &MultisigView<C>,
@@ -113,10 +136,6 @@ impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> {
Ok(()) Ok(())
} }
fn transcript(&self) -> Option<DigestTranscript::<blake2::Blake2b512>> {
None
}
fn sign_share( fn sign_share(
&mut self, &mut self,
params: &MultisigView<C>, params: &MultisigView<C>,

View File

@@ -93,14 +93,14 @@ pub trait Curve: Clone + Copy + PartialEq + Eq + Debug {
#[allow(non_snake_case)] #[allow(non_snake_case)]
fn G_len() -> usize; 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 // 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 // While they do technically exist, their usage of Self::Repr breaks all potential library usage
// without helper functions like this // without helper functions like this
#[allow(non_snake_case)] #[allow(non_snake_case)]
fn F_from_le_slice(slice: &[u8]) -> Result<Self::F, CurveError>; fn F_from_le_slice(slice: &[u8]) -> Result<Self::F, CurveError>;
/// Group element from slice. Should be canonical /// Group element from slice. Must require canonicity or risks differing binding factors
#[allow(non_snake_case)] #[allow(non_snake_case)]
fn G_from_slice(slice: &[u8]) -> Result<Self::G, CurveError>; fn G_from_slice(slice: &[u8]) -> Result<Self::G, CurveError>;

View File

@@ -144,13 +144,21 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
Err(FrostError::NonEmptyParticipantZero)?; 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(&params.keys.offset.unwrap()));
}
}
#[allow(non_snake_case)] #[allow(non_snake_case)]
let mut B = Vec::with_capacity(multisig_params.n + 1); let mut B = Vec::with_capacity(multisig_params.n + 1);
B.push(None); B.push(None);
// Commitments + a presumed 32-byte hash of the message // Commitments + a presumed 32-byte hash of the message
let commitments_len = 2 * C::G_len(); let commitments_len = 2 * C::G_len();
let mut b: Vec<u8> = Vec::with_capacity((multisig_params.t * commitments_len) + 32);
// Parse the commitments and prepare the binding factor // Parse the commitments and prepare the binding factor
for l in 1 ..= multisig_params.n { for l in 1 ..= multisig_params.n {
@@ -160,8 +168,14 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
} }
B.push(Some(our_preprocess.commitments)); 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; continue;
} }
@@ -190,10 +204,20 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
let E = C::G_from_slice(&commitments[C::G_len() .. commitments_len]) let E = C::G_from_slice(&commitments[C::G_len() .. commitments_len])
.map_err(|_| FrostError::InvalidCommitment(l))?; .map_err(|_| FrostError::InvalidCommitment(l))?;
B.push(Some([D, E])); 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 // Process the commitments and addendums
let view = &params.view; let view = &params.view;
for l in &params.view.included { for l in &params.view.included {
@@ -211,45 +235,6 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
)?; )?;
} }
// 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<Transcript>?
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(&params.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)] #[allow(non_snake_case)]
let mut Ris = vec![]; let mut Ris = vec![];
#[allow(non_snake_case)] #[allow(non_snake_case)]
@@ -257,7 +242,7 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
for i in 0 .. params.view.included.len() { for i in 0 .. params.view.included.len() {
let commitments = B[params.view.included[i]].unwrap(); let commitments = B[params.view.included[i]].unwrap();
#[allow(non_snake_case)] #[allow(non_snake_case)]
let this_R = commitments[0] + (commitments[1] * b); let this_R = commitments[0] + (commitments[1] * binding);
Ris.push(this_R); Ris.push(this_R);
R += this_R; R += this_R;
} }
@@ -266,8 +251,8 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
let share = params.algorithm.sign_share( let share = params.algorithm.sign_share(
view, view,
R, R,
b, binding,
our_preprocess.nonces[0] + (our_preprocess.nonces[1] * b), our_preprocess.nonces[0] + (our_preprocess.nonces[1] * binding),
msg msg
); );
Ok((Package { Ris, R, share }, C::F_to_le_bytes(&share))) Ok((Package { Ris, R, share }, C::F_to_le_bytes(&share)))

View File

@@ -7,9 +7,6 @@ authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021" edition = "2021"
[dependencies] [dependencies]
rand_core = "0.6"
rand_chacha = "0.3"
digest = "0.10" digest = "0.10"
merlin = { version = "3", optional = true } merlin = { version = "3", optional = true }

View File

@@ -5,34 +5,30 @@ mod merlin;
#[cfg(features = "merlin")] #[cfg(features = "merlin")]
pub use merlin::MerlinTranscript; pub use merlin::MerlinTranscript;
use rand_core::{RngCore, CryptoRng, SeedableRng};
use rand_chacha::ChaCha12Rng;
use digest::Digest; use digest::Digest;
pub trait Transcript { pub trait Transcript {
type SeededRng: RngCore + CryptoRng; fn domain_separate(&mut self, label: &[u8]);
fn new(label: &'static [u8]) -> Self;
fn append_message(&mut self, label: &'static [u8], message: &[u8]); fn append_message(&mut self, label: &'static [u8], message: &[u8]);
fn challenge(&mut self, label: &'static [u8], len: usize) -> Vec<u8>; fn challenge(&mut self, label: &'static [u8]) -> Vec<u8>;
fn seeded_rng( fn rng_seed(&mut self, label: &'static [u8], additional_entropy: Option<[u8; 32]>) -> [u8; 32];
&self,
label: &'static [u8],
additional_entropy: Option<[u8; 32]>
) -> Self::SeededRng;
// TODO: Consider a domain_separate function
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct DigestTranscript<D: Digest>(Vec<u8>, PhantomData<D>); pub struct DigestTranscript<D: Digest>(Vec<u8>, PhantomData<D>);
impl<D: Digest> Transcript for DigestTranscript<D> {
// Uses ChaCha12 as even ChaCha8 should be secure yet 12 is considered a sane middleground
type SeededRng = ChaCha12Rng;
fn new(label: &'static [u8]) -> Self { impl<D: Digest> DigestTranscript<D> {
DigestTranscript(label.to_vec(), PhantomData) pub fn new(label: Vec<u8>) -> Self {
DigestTranscript(label, PhantomData)
}
}
impl<D: Digest> Transcript for DigestTranscript<D> {
// 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]) { fn append_message(&mut self, label: &'static [u8], message: &[u8]) {
@@ -42,40 +38,18 @@ impl<D: Digest> Transcript for DigestTranscript<D> {
self.0.extend(message); self.0.extend(message);
} }
fn challenge(&mut self, label: &'static [u8], len: usize) -> Vec<u8> { fn challenge(&mut self, label: &'static [u8]) -> Vec<u8> {
self.0.extend(label); self.0.extend(label);
D::new().chain_update(&self.0).finalize().to_vec()
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( fn rng_seed(&mut self, label: &'static [u8], additional_entropy: Option<[u8; 32]>) -> [u8; 32] {
&self,
label: &'static [u8],
additional_entropy: Option<[u8; 32]>
) -> Self::SeededRng {
let mut transcript = DigestTranscript::<D>(self.0.clone(), PhantomData);
if additional_entropy.is_some() { 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]; let mut seed = [0; 32];
seed.copy_from_slice(&D::digest(&transcript.0)[0 .. 32]); seed.copy_from_slice(&self.challenge(label)[0 .. 32]);
ChaCha12Rng::from_seed(seed) seed
} }
} }

View File

@@ -1,42 +1,42 @@
use core::{marker::PhantomData, fmt::{Debug, Formatter}}; use core::{marker::PhantomData, fmt::{Debug, Formatter}};
use rand_core::{RngCore, CryptoRng, SeedableRng};
use rand_chacha::ChaCha12Rng;
use digest::Digest; use digest::Digest;
#[derive(Clone)] #[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 // Merlin doesn't implement Debug so provide a stub which won't panic
impl Debug for MerlinTranscript { impl Debug for MerlinTranscript {
fn fmt(&self, _: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { Ok(()) } fn fmt(&self, _: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { Ok(()) }
} }
impl Transcript for MerlinTranscript { impl Transcript for MerlinTranscript {
type SeededRng = ChaCha12Rng; fn domain_separate(&mut self, label: &[u8]) {
self.append_message(b"dom-sep", label);
fn new(label: &'static [u8]) -> Self {
MerlinTranscript(merlin::Transcript::new(label))
} }
fn append_message(&mut self, label: &'static [u8], message: &[u8]) { fn append_message(&mut self, label: &'static [u8], message: &[u8]) {
self.0.append_message(label, message); self.0.append_message(label, message);
} }
fn challenge(&mut self, label: &'static [u8], len: usize) -> Vec<u8> { fn challenge(&mut self, label: &'static [u8]) -> Vec<u8> {
let mut challenge = 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); self.0.challenge_bytes(label, &mut challenge);
challenge challenge
} }
fn seeded_rng(&self, label: &'static [u8], additional_entropy: Option<[u8; 32]>) -> ChaCha12Rng { fn rng_seed(&mut self, label: &'static [u8], additional_entropy: Option<[u8; 32]>) -> [u8; 32] {
let mut transcript = self.0.clone();
if additional_entropy.is_some() { if additional_entropy.is_some() {
transcript.append_message(b"additional_entropy", &additional_entropy.unwrap()); transcript.append_message(b"additional_entropy", &additional_entropy.unwrap());
} }
let mut seed = [0; 32]; let mut seed = [0; 32];
transcript.challenge_bytes(label, &mut seed); transcript.challenge_bytes(label, &mut seed);
ChaCha12Rng::from_seed(seed) seed
} }
} }