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

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