Use a global transcript

This commit is contained in:
Luke Parker 2022-05-06 07:33:08 -04:00
parent cc9c2e0d40
commit 964cb357e6
No known key found for this signature in database
GPG key ID: F9F1386DB1E119B6
12 changed files with 165 additions and 182 deletions

View file

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

View file

@ -1,7 +1,8 @@
use core::fmt::Debug;
use std::{rc::Rc, cell::RefCell};
use rand_core::{RngCore, CryptoRng};
use rand_core::{RngCore, CryptoRng, SeedableRng};
use rand_chacha::ChaCha12Rng;
use curve25519_dalek::{
constants::ED25519_BASEPOINT_TABLE,
@ -27,7 +28,7 @@ use crate::{
impl Input {
fn transcript<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
transcript.append_message(b"ring_index", &[self.i]);
@ -61,12 +62,13 @@ struct ClsagSignInterim {
#[allow(non_snake_case)]
#[derive(Clone, Debug)]
pub struct Multisig {
commitments_H: Vec<u8>,
image: EdwardsPoint,
AH: (dfg::EdwardsPoint, dfg::EdwardsPoint),
transcript: Transcript,
input: Input,
image: EdwardsPoint,
commitments_H: Vec<u8>,
AH: (dfg::EdwardsPoint, dfg::EdwardsPoint),
msg: Rc<RefCell<[u8; 32]>>,
mask: Rc<RefCell<Scalar>>,
@ -75,18 +77,20 @@ pub struct Multisig {
impl Multisig {
pub fn new(
transcript: Transcript,
input: Input,
msg: Rc<RefCell<[u8; 32]>>,
mask: Rc<RefCell<Scalar>>,
) -> Result<Multisig, MultisigError> {
Ok(
Multisig {
commitments_H: vec![],
image: EdwardsPoint::identity(),
AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()),
transcript,
input,
image: EdwardsPoint::identity(),
commitments_H: vec![],
AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()),
msg,
mask,
@ -138,14 +142,28 @@ impl Algorithm<Ed25519> for Multisig {
Err(FrostError::InvalidCommitmentQuantity(l, 9, serialized.len() / 32))?;
}
if self.commitments_H.len() == 0 {
self.transcript.domain_separate(b"CLSAG");
self.input.transcript(&mut self.transcript);
self.transcript.append_message(b"message", &*self.msg.borrow());
self.transcript.append_message(b"mask", &self.mask.borrow().to_bytes());
}
let (share, serialized) = key_image::verify_share(view, l, serialized).map_err(|_| FrostError::InvalidShare(l))?;
// Given the fact there's only ever one possible value for this, this may technically not need
// to be committed to. If signing a TX, it'll be double committed to thanks to the message
// It doesn't hurt to have though and ensures security boundaries are well formed
self.transcript.append_message(b"image_share", &share.compress().to_bytes());
self.image += share;
let alt = &hash_to_point(&self.input.ring[usize::from(self.input.i)][0]);
// Uses the same format FROST does for the expected commitments (nonce * G where this is nonce * H)
self.commitments_H.extend(&u64::try_from(l).unwrap().to_le_bytes());
self.commitments_H.extend(&serialized[0 .. 64]);
// Given this is guaranteed to match commitments, which FROST commits to, this also technically
// doesn't need to be committed to if a canonical serialization is guaranteed
// It, again, doesn't hurt to include and ensures security boundaries are well formed
self.transcript.append_message(b"participant", &u64::try_from(l).unwrap().to_le_bytes());
self.transcript.append_message(b"commitments_H", &serialized[0 .. 64]);
#[allow(non_snake_case)]
let H = (
@ -171,21 +189,8 @@ impl Algorithm<Ed25519> for Multisig {
Ok(())
}
fn transcript(&self) -> Option<Self::Transcript> {
let mut transcript = Self::Transcript::new(b"Monero Multisig");
self.input.transcript(&mut transcript);
transcript.append_message(b"dom-sep", b"CLSAG");
// Given the fact there's only ever one possible value for this, this may technically not need
// to be committed to. If signing a TX, it's be double committed to thanks to the message
// It doesn't hurt to have though and ensures security boundaries are well formed
transcript.append_message(b"image", &self.image.compress().to_bytes());
// Given this is guaranteed to match commitments, which FROST commits to, this also technically
// doesn't need to be committed to if a canonical serialization is guaranteed
// It, again, doesn't hurt to include and ensures security boundaries are well formed
transcript.append_message(b"commitments_H", &self.commitments_H);
transcript.append_message(b"message", &*self.msg.borrow());
transcript.append_message(b"mask", &self.mask.borrow().to_bytes());
Some(transcript)
fn transcript(&mut self) -> &mut Self::Transcript {
&mut self.transcript
}
fn sign_share(
@ -203,8 +208,8 @@ impl Algorithm<Ed25519> for Multisig {
// The transcript contains private data, preventing passive adversaries from recreating this
// process even if they have access to commitments (specifically, the ring index being signed
// for, along with the mask which should not only require knowing the shared keys yet also the
// input commitment mask)
let mut rng = self.transcript().unwrap().seeded_rng(b"decoy_responses", None);
// input commitment masks)
let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"decoy_responses", None));
#[allow(non_snake_case)]
let (clsag, c, mu_C, z, mu_P, C_out) = sign_core(

View file

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

View file

@ -1,6 +1,7 @@
use std::{rc::Rc, cell::RefCell};
use rand_core::{RngCore, CryptoRng};
use rand_core::{RngCore, CryptoRng, SeedableRng};
use rand_chacha::ChaCha12Rng;
use curve25519_dalek::{scalar::Scalar, edwards::{EdwardsPoint, CompressedEdwardsY}};
@ -24,6 +25,8 @@ use crate::{
pub struct TransactionMachine {
leader: bool,
signable: SignableTransaction,
transcript: Transcript,
our_images: Vec<EdwardsPoint>,
mask_sum: Rc<RefCell<Scalar>>,
msg: Rc<RefCell<[u8; 32]>>,
@ -35,6 +38,7 @@ pub struct TransactionMachine {
impl SignableTransaction {
pub async fn multisig<R: RngCore + CryptoRng>(
mut self,
label: Vec<u8>,
rng: &mut R,
rpc: &Rpc,
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
// sender, and the payments (address and amount), which a passive adversary may be able to know
// The use of input shared keys technically makes this one time given a competent wallet which
// can withstand the burning attack (and has a static spend key? TODO visit bounds)
// depending on how these transactions are coordinated
// The lack of dedicated entropy here is frustrating. We can probably provide entropy inclusion
// if we move CLSAG ring to a Rc RefCell like msg and mask? TODO
// For the above TODO, also consider FROST's TODO of a global transcript instance
let mut transcript = Transcript::new(b"Input Mixins");
// Does dom-sep despite not being a proof because it's a unique section (and we have no dom-sep yet)
transcript.append_message("dom-sep", "inputs_outputs");
let mut transcript = Transcript::new(label);
for input in &self.inputs {
// These outputs can only be spent once. Therefore, it forces all RNGs derived from this
// transcript (such as the one used to create one time keys) to be unique
transcript.append_message(b"input_hash", &input.tx.0);
transcript.append_message(b"input_output_index", &u64::try_from(input.o).unwrap().to_le_bytes());
// Not including this, with a doxxed list of payments, would allow brute forcing the inputs
// to determine RNG seeds and therefore the true spends
transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes());
}
for payment in &self.payments {
transcript.append_message(b"payment_address", &payment.0.as_bytes());
transcript.append_message(b"payment_amount", &payment.1.to_le_bytes());
}
// Not only is this an output, but this locks to the base keys to be complete with the above key offsets
transcript.append_message(b"change", &self.change.as_bytes());
// Select mixins
let mixins = mixins::select(
&mut transcript.seeded_rng(b"mixins", None),
&mut ChaCha12Rng::from_seed(transcript.rng_seed(b"mixins", None)),
rpc,
height,
&self.inputs
@ -86,6 +95,7 @@ impl SignableTransaction {
clsags.push(
AlgorithmMachine::new(
clsag::Multisig::new(
transcript.clone(),
clsag::Input::new(
mixins[i].2.clone(),
mixins[i].1,
@ -112,6 +122,7 @@ impl SignableTransaction {
Ok(TransactionMachine {
leader: keys.params().i() == included[0],
signable: self,
transcript,
our_images,
mask_sum,
msg,
@ -122,19 +133,6 @@ impl SignableTransaction {
}
}
// Seeded RNG so multisig participants agree on one time keys to use, preventing burning attacks
fn outputs_rng(tx: &SignableTransaction, entropy: [u8; 32]) -> <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 {
type Signature = Transaction;
@ -157,7 +155,7 @@ impl StateMachine for TransactionMachine {
rng.fill_bytes(&mut entropy);
serialized.extend(&entropy);
let mut rng = outputs_rng(&self.signable, entropy);
let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"tx_keys", Some(entropy)));
// Safe to unwrap thanks to the dummy prepare
let (commitments, mask_sum) = self.signable.prepare_outputs(&mut rng).unwrap();
self.mask_sum.replace(mask_sum);
@ -196,9 +194,11 @@ impl StateMachine for TransactionMachine {
}
let prep = prep.as_ref().unwrap();
let mut rng = outputs_rng(
&self.signable,
prep[clsag_lens .. (clsag_lens + 32)].try_into().map_err(|_| FrostError::InvalidShare(l))?
let mut rng = ChaCha12Rng::from_seed(
self.transcript.rng_seed(
b"tx_keys",
Some(prep[clsag_lens .. (clsag_lens + 32)].try_into().map_err(|_| FrostError::InvalidShare(l))?)
)
);
// Not invalid outputs due to doing a dummy prep as leader
let (commitments, mask_sum) = self.signable.prepare_outputs(&mut rng).map_err(|_| FrostError::InvalidShare(l))?;

View file

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

View file

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

View file

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

View file

@ -93,14 +93,14 @@ pub trait Curve: Clone + Copy + PartialEq + Eq + Debug {
#[allow(non_snake_case)]
fn G_len() -> usize;
/// Field element from slice. Should be canonical
/// Field element from slice. Preferred to be canonical yet does not have to be
// Required due to the lack of standardized encoding functions provided by ff/group
// While they do technically exist, their usage of Self::Repr breaks all potential library usage
// without helper functions like this
#[allow(non_snake_case)]
fn F_from_le_slice(slice: &[u8]) -> Result<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)]
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)?;
}
// 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)]
let mut B = Vec::with_capacity(multisig_params.n + 1);
B.push(None);
// Commitments + a presumed 32-byte hash of the message
let commitments_len = 2 * C::G_len();
let mut b: Vec<u8> = Vec::with_capacity((multisig_params.t * commitments_len) + 32);
// Parse the commitments and prepare the binding factor
for l in 1 ..= multisig_params.n {
@ -160,8 +168,14 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
}
B.push(Some(our_preprocess.commitments));
b.extend(&u16::try_from(l).unwrap().to_le_bytes());
b.extend(&our_preprocess.serialized[0 .. (C::G_len() * 2)]);
{
let transcript = params.algorithm.transcript();
transcript.append_message(b"participant", &u16::try_from(l).unwrap().to_le_bytes());
transcript.append_message(
b"commitments",
&our_preprocess.serialized[0 .. (C::G_len() * 2)]
);
}
continue;
}
@ -190,10 +204,20 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
let E = C::G_from_slice(&commitments[C::G_len() .. commitments_len])
.map_err(|_| FrostError::InvalidCommitment(l))?;
B.push(Some([D, E]));
b.extend(&u16::try_from(l).unwrap().to_le_bytes());
b.extend(&commitments[0 .. commitments_len]);
{
let transcript = params.algorithm.transcript();
transcript.append_message(b"participant", &u16::try_from(l).unwrap().to_le_bytes());
transcript.append_message(b"commitments", &commitments[0 .. commitments_len]);
}
}
// Add the message to the binding factor
let binding = {
let transcript = params.algorithm.transcript();
transcript.append_message(b"message", &C::hash_msg(&msg));
C::hash_to_F(&transcript.challenge(b"binding"))
};
// Process the commitments and addendums
let view = &params.view;
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)]
let mut Ris = vec![];
#[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() {
let commitments = B[params.view.included[i]].unwrap();
#[allow(non_snake_case)]
let this_R = commitments[0] + (commitments[1] * b);
let this_R = commitments[0] + (commitments[1] * binding);
Ris.push(this_R);
R += this_R;
}
@ -266,8 +251,8 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
let share = params.algorithm.sign_share(
view,
R,
b,
our_preprocess.nonces[0] + (our_preprocess.nonces[1] * b),
binding,
our_preprocess.nonces[0] + (our_preprocess.nonces[1] * binding),
msg
);
Ok((Package { Ris, R, share }, C::F_to_le_bytes(&share)))

View file

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

View file

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

View file

@ -1,42 +1,42 @@
use core::{marker::PhantomData, fmt::{Debug, Formatter}};
use rand_core::{RngCore, CryptoRng, SeedableRng};
use rand_chacha::ChaCha12Rng;
use digest::Digest;
#[derive(Clone)]
pub struct MerlinTranscript(merlin::Transcript);
pub struct MerlinTranscript(pub merlin::Transcript);
// Merlin doesn't implement Debug so provide a stub which won't panic
impl Debug for MerlinTranscript {
fn fmt(&self, _: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { Ok(()) }
}
impl Transcript for MerlinTranscript {
type SeededRng = ChaCha12Rng;
fn new(label: &'static [u8]) -> Self {
MerlinTranscript(merlin::Transcript::new(label))
fn domain_separate(&mut self, label: &[u8]) {
self.append_message(b"dom-sep", label);
}
fn append_message(&mut self, label: &'static [u8], message: &[u8]) {
self.0.append_message(label, message);
}
fn challenge(&mut self, label: &'static [u8], len: usize) -> Vec<u8> {
fn challenge(&mut self, label: &'static [u8]) -> Vec<u8> {
let mut challenge = vec![];
challenge.resize(len, 0);
// Uses a challenge length of 64 bytes to support wide reduction on generated scalars
// From a security level standpoint, this should just be 32 bytes
// From a Merlin standpoint, this should be variable per call
// From a practical standpoint, this is a demo file not planned to be used and anything using
// this wrapper is fine without any settings it uses
challenge.resize(64, 0);
self.0.challenge_bytes(label, &mut challenge);
challenge
}
fn seeded_rng(&self, label: &'static [u8], additional_entropy: Option<[u8; 32]>) -> ChaCha12Rng {
let mut transcript = self.0.clone();
fn rng_seed(&mut self, label: &'static [u8], additional_entropy: Option<[u8; 32]>) -> [u8; 32] {
if additional_entropy.is_some() {
transcript.append_message(b"additional_entropy", &additional_entropy.unwrap());
}
let mut seed = [0; 32];
transcript.challenge_bytes(label, &mut seed);
ChaCha12Rng::from_seed(seed)
seed
}
}