Create dedicated message structures for FROST messages (#140)

* Create message types for FROST key gen

Taking in reader borrows absolutely wasn't feasible. Now, proper types
which can be read (and then passed directly, without a mutable borrow)
exist for key_gen. sign coming next.

* Move FROST signing to messages, not Readers/Writers/Vec<u8>

Also takes the nonce handling code and makes a dedicated file for it, 
aiming to resolve complex types and make the code more legible by 
replacing its previously inlined state.

* clippy

* Update FROST tests

* read_signature_share

* Update the Monero library to the new FROST packages

* Update processor to latest FROST

* Tweaks to terminology and documentation
This commit is contained in:
Luke Parker 2022-10-25 23:17:25 -05:00 committed by GitHub
parent ccdb834e6e
commit cbceaff678
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 874 additions and 591 deletions

4
Cargo.lock generated
View file

@ -1614,7 +1614,7 @@ dependencies = [
[[package]]
name = "dleq"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"blake2",
"dalek-ff-group",
@ -4507,7 +4507,7 @@ dependencies = [
[[package]]
name = "modular-frost"
version = "0.2.4"
version = "0.3.0"
dependencies = [
"dalek-ff-group",
"dleq",

View file

@ -34,7 +34,7 @@ dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.1" }
multiexp = { path = "../../crypto/multiexp", version = "0.2", features = ["batch"] }
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.1", features = ["recommended"], optional = true }
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.2", features = ["ed25519"], optional = true }
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.3", features = ["ed25519"], optional = true }
dleq = { path = "../../crypto/dleq", version = "0.1", features = ["serialize"], optional = true }
monero-generators = { path = "generators", version = "0.1" }
@ -55,7 +55,7 @@ monero-generators = { path = "generators", version = "0.1" }
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.2", features = ["ed25519", "tests"] }
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.3", features = ["ed25519", "tests"] }
[features]
multisig = ["rand_chacha", "blake2", "transcript", "frost", "dleq"]

View file

@ -1,73 +0,0 @@
use std::io::Read;
use thiserror::Error;
use rand_core::{RngCore, CryptoRng};
use zeroize::Zeroize;
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
use group::{Group, GroupEncoding};
use transcript::{Transcript, RecommendedTranscript};
use dalek_ff_group as dfg;
use dleq::DLEqProof;
#[derive(Clone, Error, Debug)]
pub(crate) enum MultisigError {
#[error("invalid discrete log equality proof")]
InvalidDLEqProof(u16),
}
fn transcript() -> RecommendedTranscript {
RecommendedTranscript::new(b"monero_key_image_dleq")
}
#[allow(non_snake_case)]
pub(crate) fn write_dleq<R: RngCore + CryptoRng>(
rng: &mut R,
H: EdwardsPoint,
mut x: Scalar,
) -> Vec<u8> {
let mut res = Vec::with_capacity(64);
DLEqProof::prove(
rng,
// Doesn't take in a larger transcript object due to the usage of this
// Every prover would immediately write their own DLEq proof, when they can only do so in
// the proper order if they want to reach consensus
// It'd be a poor API to have CLSAG define a new transcript solely to pass here, just to try to
// merge later in some form, when it should instead just merge xH (as it does)
&mut transcript(),
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(H)],
dfg::Scalar(x),
)
.serialize(&mut res)
.unwrap();
x.zeroize();
res
}
#[allow(non_snake_case)]
pub(crate) fn read_dleq<Re: Read>(
serialized: &mut Re,
H: EdwardsPoint,
l: u16,
xG: dfg::EdwardsPoint,
) -> Result<dfg::EdwardsPoint, MultisigError> {
let mut bytes = [0; 32];
serialized.read_exact(&mut bytes).map_err(|_| MultisigError::InvalidDLEqProof(l))?;
// dfg ensures the point is torsion free
let xH = Option::<dfg::EdwardsPoint>::from(dfg::EdwardsPoint::from_bytes(&bytes))
.ok_or(MultisigError::InvalidDLEqProof(l))?;
// Ensure this is a canonical point
if xH.to_bytes() != bytes {
Err(MultisigError::InvalidDLEqProof(l))?;
}
DLEqProof::<dfg::EdwardsPoint>::deserialize(serialized)
.map_err(|_| MultisigError::InvalidDLEqProof(l))?
.verify(&mut transcript(), &[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(H)], &[xG, xH])
.map_err(|_| MultisigError::InvalidDLEqProof(l))?;
Ok(xH)
}

View file

@ -33,9 +33,6 @@ use curve25519_dalek::{
pub use monero_generators::H;
#[cfg(feature = "multisig")]
pub(crate) mod frost;
mod serialize;
/// RingCT structs and functionality.

View file

@ -22,7 +22,7 @@ use crate::{
#[cfg(feature = "multisig")]
mod multisig;
#[cfg(feature = "multisig")]
pub use multisig::{ClsagDetails, ClsagMultisig};
pub use multisig::{ClsagDetails, ClsagAddendum, ClsagMultisig};
lazy_static! {
static ref INV_EIGHT: Scalar = Scalar::from(8u8).invert();

View file

@ -1,6 +1,6 @@
use core::fmt::Debug;
use std::{
io::Read,
io::{self, Read, Write},
sync::{Arc, RwLock},
};
@ -16,20 +16,26 @@ use curve25519_dalek::{
edwards::EdwardsPoint,
};
use group::Group;
use group::{Group, GroupEncoding};
use transcript::{Transcript, RecommendedTranscript};
use frost::{curve::Ed25519, FrostError, FrostView, algorithm::Algorithm};
use dalek_ff_group as dfg;
use crate::{
frost::{write_dleq, read_dleq},
ringct::{
hash_to_point,
clsag::{ClsagInput, Clsag},
},
use dleq::DLEqProof;
use frost::{
curve::Ed25519,
FrostError, FrostView,
algorithm::{AddendumSerialize, Algorithm},
};
use crate::ringct::{
hash_to_point,
clsag::{ClsagInput, Clsag},
};
fn dleq_transcript() -> RecommendedTranscript {
RecommendedTranscript::new(b"monero_key_image_dleq")
}
impl ClsagInput {
fn transcript<T: Transcript>(&self, transcript: &mut T) {
// Doesn't domain separate as this is considered part of the larger CLSAG proof
@ -54,7 +60,7 @@ impl ClsagInput {
}
}
/// CLSAG Input and the mask to use for it.
/// CLSAG input and the mask to use for it.
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
pub struct ClsagDetails {
input: ClsagInput,
@ -67,6 +73,20 @@ impl ClsagDetails {
}
}
/// Addendum produced during the FROST signing process with relevant data.
#[derive(Clone, PartialEq, Eq, Zeroize, Debug)]
pub struct ClsagAddendum {
pub(crate) key_image: dfg::EdwardsPoint,
dleq: DLEqProof<dfg::EdwardsPoint>,
}
impl AddendumSerialize for ClsagAddendum {
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(self.key_image.compress().to_bytes().as_ref())?;
self.dleq.serialize(writer)
}
}
#[allow(non_snake_case)]
#[derive(Clone, PartialEq, Eq, Debug)]
struct Interim {
@ -113,10 +133,6 @@ impl ClsagMultisig {
}
}
pub(crate) const fn serialized_len() -> usize {
32 + (2 * 32)
}
fn input(&self) -> ClsagInput {
(*self.details.read().unwrap()).as_ref().unwrap().input.clone()
}
@ -128,6 +144,7 @@ impl ClsagMultisig {
impl Algorithm<Ed25519> for ClsagMultisig {
type Transcript = RecommendedTranscript;
type Addendum = ClsagAddendum;
type Signature = (Clsag, EdwardsPoint);
fn nonces(&self) -> Vec<Vec<dfg::EdwardsPoint>> {
@ -138,18 +155,42 @@ impl Algorithm<Ed25519> for ClsagMultisig {
&mut self,
rng: &mut R,
view: &FrostView<Ed25519>,
) -> Vec<u8> {
let mut serialized = Vec::with_capacity(Self::serialized_len());
serialized.extend((view.secret_share().0 * self.H).compress().to_bytes());
serialized.extend(write_dleq(rng, self.H, view.secret_share().0));
serialized
) -> ClsagAddendum {
ClsagAddendum {
key_image: dfg::EdwardsPoint(self.H * view.secret_share().0),
dleq: DLEqProof::prove(
rng,
// Doesn't take in a larger transcript object due to the usage of this
// Every prover would immediately write their own DLEq proof, when they can only do so in
// the proper order if they want to reach consensus
// It'd be a poor API to have CLSAG define a new transcript solely to pass here, just to
// try to merge later in some form, when it should instead just merge xH (as it does)
&mut dleq_transcript(),
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)],
dfg::Scalar(view.secret_share().0),
),
}
}
fn process_addendum<Re: Read>(
fn read_addendum<R: Read>(&self, reader: &mut R) -> io::Result<ClsagAddendum> {
let mut bytes = [0; 32];
reader.read_exact(&mut bytes)?;
// dfg ensures the point is torsion free
let xH = Option::<dfg::EdwardsPoint>::from(dfg::EdwardsPoint::from_bytes(&bytes))
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid key image"))?;
// Ensure this is a canonical point
if xH.to_bytes() != bytes {
Err(io::Error::new(io::ErrorKind::Other, "non-canonical key image"))?;
}
Ok(ClsagAddendum { key_image: xH, dleq: DLEqProof::<dfg::EdwardsPoint>::deserialize(reader)? })
}
fn process_addendum(
&mut self,
view: &FrostView<Ed25519>,
l: u16,
serialized: &mut Re,
addendum: ClsagAddendum,
) -> Result<(), FrostError> {
if self.image.is_identity() {
self.transcript.domain_separate(b"CLSAG");
@ -158,11 +199,20 @@ impl Algorithm<Ed25519> for ClsagMultisig {
}
self.transcript.append_message(b"participant", &l.to_be_bytes());
let image = read_dleq(serialized, self.H, l, view.verification_share(l))
.map_err(|_| FrostError::InvalidCommitment(l))?
.0;
self.transcript.append_message(b"key_image_share", image.compress().to_bytes().as_ref());
self.image += image;
addendum
.dleq
.verify(
&mut dleq_transcript(),
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)],
&[view.verification_share(l), addendum.key_image],
)
.map_err(|_| FrostError::InvalidPreprocess(l))?;
self
.transcript
.append_message(b"key_image_share", addendum.key_image.compress().to_bytes().as_ref());
self.image += addendum.key_image.0;
Ok(())
}

View file

@ -19,10 +19,7 @@ use crate::{
},
};
#[cfg(feature = "multisig")]
use crate::{
frost::MultisigError,
ringct::clsag::{ClsagDetails, ClsagMultisig},
};
use crate::ringct::clsag::{ClsagDetails, ClsagMultisig};
#[cfg(feature = "multisig")]
use frost::tests::{key_gen, algorithm_machines, sign};
@ -79,7 +76,7 @@ fn clsag() {
#[cfg(feature = "multisig")]
#[test]
fn clsag_multisig() -> Result<(), MultisigError> {
fn clsag_multisig() {
let keys = key_gen::<_, Ed25519>(&mut OsRng);
let randomness = random_scalar(&mut OsRng);
@ -125,6 +122,4 @@ fn clsag_multisig() -> Result<(), MultisigError> {
),
&[1; 32],
);
Ok(())
}

View file

@ -1,5 +1,5 @@
use std::{
io::{Read, Cursor},
io::{self, Read},
sync::{Arc, RwLock},
collections::HashMap,
};
@ -7,26 +7,22 @@ use std::{
use rand_core::{RngCore, CryptoRng, SeedableRng};
use rand_chacha::ChaCha20Rng;
use curve25519_dalek::{
traits::Identity,
scalar::Scalar,
edwards::{EdwardsPoint, CompressedEdwardsY},
};
use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::EdwardsPoint};
use transcript::{Transcript, RecommendedTranscript};
use frost::{
curve::Ed25519,
FrostError, FrostKeys,
sign::{
PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine, AlgorithmSignMachine,
AlgorithmSignatureMachine,
Writable, Preprocess, SignatureShare, PreprocessMachine, SignMachine, SignatureMachine,
AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine,
},
};
use crate::{
random_scalar,
ringct::{
clsag::{ClsagInput, ClsagDetails, ClsagMultisig},
clsag::{ClsagInput, ClsagDetails, ClsagAddendum, ClsagMultisig},
RctPrunable,
},
transaction::{Input, Transaction},
@ -58,7 +54,7 @@ pub struct TransactionSignMachine {
inputs: Vec<Arc<RwLock<Option<ClsagDetails>>>>,
clsags: Vec<AlgorithmSignMachine<Ed25519, ClsagMultisig>>,
our_preprocess: Vec<u8>,
our_preprocess: Vec<Preprocess<Ed25519, ClsagAddendum>>,
}
pub struct TransactionSignatureMachine {
@ -166,28 +162,26 @@ impl SignableTransaction {
}
impl PreprocessMachine for TransactionMachine {
type Preprocess = Vec<Preprocess<Ed25519, ClsagAddendum>>;
type Signature = Transaction;
type SignMachine = TransactionSignMachine;
fn preprocess<R: RngCore + CryptoRng>(
mut self,
rng: &mut R,
) -> (TransactionSignMachine, Vec<u8>) {
) -> (TransactionSignMachine, Self::Preprocess) {
// Iterate over each CLSAG calling preprocess
let mut serialized = Vec::with_capacity(
// D_{G, H}, E_{G, H}, DLEqs, key image addendum
self.clsags.len() * ((2 * (32 + 32)) + (2 * (32 + 32)) + ClsagMultisig::serialized_len()),
);
let mut preprocesses = Vec::with_capacity(self.clsags.len());
let clsags = self
.clsags
.drain(..)
.map(|clsag| {
let (clsag, preprocess) = clsag.preprocess(rng);
serialized.extend(&preprocess);
preprocesses.push(preprocess);
clsag
})
.collect();
let our_preprocess = serialized.clone();
let our_preprocess = preprocesses.clone();
// We could add further entropy here, and previous versions of this library did so
// As of right now, the multisig's key, the inputs being spent, and the FROST data itself
@ -212,33 +206,35 @@ impl PreprocessMachine for TransactionMachine {
our_preprocess,
},
serialized,
preprocesses,
)
}
}
impl SignMachine<Transaction> for TransactionSignMachine {
type Preprocess = Vec<Preprocess<Ed25519, ClsagAddendum>>;
type SignatureShare = Vec<SignatureShare<Ed25519>>;
type SignatureMachine = TransactionSignatureMachine;
fn sign<Re: Read>(
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
self.clsags.iter().map(|clsag| clsag.read_preprocess(reader)).collect()
}
fn sign(
mut self,
mut commitments: HashMap<u16, Re>,
mut commitments: HashMap<u16, Self::Preprocess>,
msg: &[u8],
) -> Result<(TransactionSignatureMachine, Vec<u8>), FrostError> {
) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> {
if !msg.is_empty() {
Err(FrostError::InternalError(
"message was passed to the TransactionMachine when it generates its own",
))?;
}
// FROST commitments and their DLEqs, and the image and its DLEq
const CLSAG_LEN: usize = (2 * (32 + 32)) + (2 * (32 + 32)) + ClsagMultisig::serialized_len();
// Convert the unified commitments to a Vec of the individual commitments
let mut images = vec![EdwardsPoint::identity(); self.clsags.len()];
let mut commitments = (0 .. self.clsags.len())
.map(|c| {
let mut buf = [0; CLSAG_LEN];
self
.included
.iter()
@ -248,31 +244,27 @@ impl SignMachine<Transaction> for TransactionSignMachine {
// transcripts cloned from this TX's initial premise's transcript. For our TX
// transcript to have the CLSAG data for entropy, it'll have to be added ourselves here
self.transcript.append_message(b"participant", &(*l).to_be_bytes());
if *l == self.i {
buf.copy_from_slice(self.our_preprocess.drain(.. CLSAG_LEN).as_slice());
let preprocess = if *l == self.i {
self.our_preprocess[c].clone()
} else {
commitments
.get_mut(l)
.ok_or(FrostError::MissingParticipant(*l))?
.read_exact(&mut buf)
.map_err(|_| FrostError::InvalidCommitment(*l))?;
commitments.get_mut(l).ok_or(FrostError::MissingParticipant(*l))?[c].clone()
};
{
let mut buf = vec![];
preprocess.write(&mut buf).unwrap();
self.transcript.append_message(b"preprocess", &buf);
}
self.transcript.append_message(b"preprocess", &buf);
// While here, calculate the key image
// Clsag will parse/calculate/validate this as needed, yet doing so here as well
// provides the easiest API overall, as this is where the TX is (which needs the key
// images in its message), along with where the outputs are determined (where our
// outputs may need these in order to guarantee uniqueness)
images[c] += CompressedEdwardsY(
buf[(CLSAG_LEN - 96) .. (CLSAG_LEN - 64)]
.try_into()
.map_err(|_| FrostError::InvalidCommitment(*l))?,
)
.decompress()
.ok_or(FrostError::InvalidCommitment(*l))?;
images[c] += preprocess.addendum.key_image.0;
Ok((*l, Cursor::new(buf)))
Ok((*l, preprocess))
})
.collect::<Result<HashMap<_, _>, _>>()
})
@ -346,37 +338,39 @@ impl SignMachine<Transaction> for TransactionSignMachine {
let msg = tx.signature_hash();
// Iterate over each CLSAG calling sign
let mut serialized = Vec::with_capacity(self.clsags.len() * 32);
let mut shares = Vec::with_capacity(self.clsags.len());
let clsags = self
.clsags
.drain(..)
.map(|clsag| {
let (clsag, share) = clsag.sign(commitments.remove(0), &msg)?;
serialized.extend(&share);
shares.push(share);
Ok(clsag)
})
.collect::<Result<_, _>>()?;
Ok((TransactionSignatureMachine { tx, clsags }, serialized))
Ok((TransactionSignatureMachine { tx, clsags }, shares))
}
}
impl SignatureMachine<Transaction> for TransactionSignatureMachine {
fn complete<Re: Read>(self, mut shares: HashMap<u16, Re>) -> Result<Transaction, FrostError> {
type SignatureShare = Vec<SignatureShare<Ed25519>>;
fn read_share<R: Read>(&self, reader: &mut R) -> io::Result<Self::SignatureShare> {
self.clsags.iter().map(|clsag| clsag.read_share(reader)).collect()
}
fn complete(
mut self,
shares: HashMap<u16, Self::SignatureShare>,
) -> Result<Transaction, FrostError> {
let mut tx = self.tx;
match tx.rct_signatures.prunable {
RctPrunable::Null => panic!("Signing for RctPrunable::Null"),
RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => {
for clsag in self.clsags {
for (c, clsag) in self.clsags.drain(..).enumerate() {
let (clsag, pseudo_out) = clsag.complete(
shares
.iter_mut()
.map(|(l, shares)| {
let mut buf = [0; 32];
shares.read_exact(&mut buf).map_err(|_| FrostError::InvalidShare(*l))?;
Ok((*l, Cursor::new(buf)))
})
.collect::<Result<HashMap<_, _>, _>>()?,
shares.iter().map(|(l, shares)| (*l, shares[c].clone())).collect::<HashMap<_, _>>(),
)?;
clsags.push(clsag);
pseudo_outs.push(pseudo_out);

View file

@ -1,6 +1,6 @@
[package]
name = "dleq"
version = "0.1.1"
version = "0.1.2"
description = "Implementation of single and cross-curve Discrete Log Equality proofs"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dleq"

View file

@ -61,7 +61,7 @@ pub enum DLEqError {
InvalidProof,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
pub struct DLEqProof<G: PrimeGroup> {
c: G::Scalar,
s: G::Scalar,

View file

@ -1,6 +1,6 @@
[package]
name = "modular-frost"
version = "0.2.4"
version = "0.3.0"
description = "Modular implementation of FROST over ff/group"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/frost"
@ -40,7 +40,7 @@ transcript = { package = "flexible-transcript", path = "../transcript", features
multiexp = { path = "../multiexp", version = "0.2", features = ["batch"] }
dleq = { path = "../dleq", version = "0.1", features = ["serialize"] }
dleq = { path = "../dleq", version = "^0.1.2", features = ["serialize"] }
[dev-dependencies]
sha2 = "0.10"

View file

@ -1,26 +1,45 @@
use core::{marker::PhantomData, fmt::Debug};
use std::io::Read;
use std::io::{self, Read, Write};
use rand_core::{RngCore, CryptoRng};
use zeroize::Zeroize;
use transcript::Transcript;
use crate::{Curve, FrostError, FrostView, schnorr};
pub use schnorr::SchnorrSignature;
/// Serialize an addendum to a writer.
pub trait AddendumSerialize {
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()>;
}
impl AddendumSerialize for () {
fn write<W: Write>(&self, _: &mut W) -> io::Result<()> {
Ok(())
}
}
/// Trait alias for the requirements to be used as an addendum.
pub trait Addendum: Clone + PartialEq + Debug + Zeroize + AddendumSerialize {}
impl<A: Clone + PartialEq + Debug + Zeroize + AddendumSerialize> Addendum for A {}
/// Algorithm trait usable by the FROST signing machine to produce signatures..
pub trait Algorithm<C: Curve>: Clone {
/// The transcript format this algorithm uses. This likely should NOT be the IETF-compatible
/// transcript included in this crate.
type Transcript: Transcript + Clone + Debug;
type Transcript: Clone + Debug + Transcript;
/// Serializable addendum, used in algorithms requiring more data than just the nonces.
type Addendum: Addendum;
/// The resulting type of the signatures this algorithm will produce.
type Signature: Clone + PartialEq + Debug;
/// Obtain a mutable borrow of the underlying transcript.
fn transcript(&mut self) -> &mut Self::Transcript;
/// Obtain the list of nonces to generate, as specified by the basepoints to create commitments.
/// against per-nonce. These are not committed to by FROST on the underlying transcript.
/// Obtain the list of nonces to generate, as specified by the generators to create commitments
/// against per-nonce
fn nonces(&self) -> Vec<Vec<C::G>>;
/// Generate an addendum to FROST"s preprocessing stage.
@ -28,14 +47,17 @@ pub trait Algorithm<C: Curve>: Clone {
&mut self,
rng: &mut R,
params: &FrostView<C>,
) -> Vec<u8>;
) -> Self::Addendum;
/// Proccess the addendum for the specified participant. Guaranteed to be ordered.
fn process_addendum<Re: Read>(
/// Read an addendum from a reader.
fn read_addendum<R: Read>(&self, reader: &mut R) -> io::Result<Self::Addendum>;
/// Proccess the addendum for the specified participant. Guaranteed to be called in order.
fn process_addendum(
&mut self,
params: &FrostView<C>,
l: u16,
reader: &mut Re,
reader: Self::Addendum,
) -> Result<(), FrostError>;
/// Sign a share with the given secret/nonce.
@ -116,6 +138,7 @@ impl<C: Curve, H: Hram<C>> Schnorr<C, H> {
impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> {
type Transcript = IetfTranscript;
type Addendum = ();
type Signature = SchnorrSignature<C>;
fn transcript(&mut self) -> &mut Self::Transcript {
@ -126,20 +149,13 @@ impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> {
vec![vec![C::generator()]]
}
fn preprocess_addendum<R: RngCore + CryptoRng>(
&mut self,
_: &mut R,
_: &FrostView<C>,
) -> Vec<u8> {
vec![]
fn preprocess_addendum<R: RngCore + CryptoRng>(&mut self, _: &mut R, _: &FrostView<C>) {}
fn read_addendum<R: Read>(&self, _: &mut R) -> io::Result<Self::Addendum> {
Ok(())
}
fn process_addendum<Re: Read>(
&mut self,
_: &FrostView<C>,
_: u16,
_: &mut Re,
) -> Result<(), FrostError> {
fn process_addendum(&mut self, _: &FrostView<C>, _: u16, _: ()) -> Result<(), FrostError> {
Ok(())
}

View file

@ -1,7 +1,5 @@
use core::fmt::Debug;
use std::io::Read;
use thiserror::Error;
use std::io::{self, Read};
use rand_core::{RngCore, CryptoRng};
@ -30,15 +28,6 @@ mod ed448;
#[cfg(feature = "ed448")]
pub use ed448::{Ed448, Ietf8032Ed448Hram, IetfEd448Hram};
/// Set of errors for curve-related operations, namely encoding and decoding.
#[derive(Clone, Error, Debug)]
pub enum CurveError {
#[error("invalid scalar")]
InvalidScalar,
#[error("invalid point")]
InvalidPoint,
}
/// Unified trait to manage an elliptic curve.
// This should be moved into its own crate if the need for generic cryptography over ff/group
// continues, which is the exact reason ff/group exists (to provide a generic interface)
@ -127,13 +116,13 @@ pub trait Curve: Clone + Copy + PartialEq + Eq + Debug + Zeroize {
}
#[allow(non_snake_case)]
fn read_F<R: Read>(r: &mut R) -> Result<Self::F, CurveError> {
fn read_F<R: Read>(r: &mut R) -> io::Result<Self::F> {
let mut encoding = <Self::F as PrimeField>::Repr::default();
r.read_exact(encoding.as_mut()).map_err(|_| CurveError::InvalidScalar)?;
r.read_exact(encoding.as_mut())?;
// ff mandates this is canonical
let res =
Option::<Self::F>::from(Self::F::from_repr(encoding)).ok_or(CurveError::InvalidScalar);
let res = Option::<Self::F>::from(Self::F::from_repr(encoding))
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "non-canonical scalar"));
for b in encoding.as_mut() {
b.zeroize();
}
@ -141,15 +130,15 @@ pub trait Curve: Clone + Copy + PartialEq + Eq + Debug + Zeroize {
}
#[allow(non_snake_case)]
fn read_G<R: Read>(r: &mut R) -> Result<Self::G, CurveError> {
fn read_G<R: Read>(r: &mut R) -> io::Result<Self::G> {
let mut encoding = <Self::G as GroupEncoding>::Repr::default();
r.read_exact(encoding.as_mut()).map_err(|_| CurveError::InvalidPoint)?;
r.read_exact(encoding.as_mut())?;
let point =
Option::<Self::G>::from(Self::G::from_bytes(&encoding)).ok_or(CurveError::InvalidPoint)?;
let point = Option::<Self::G>::from(Self::G::from_bytes(&encoding))
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid point"))?;
// Ban the identity, per the FROST spec, and non-canonical points
if (point.is_identity().into()) || (point.to_bytes().as_ref() != encoding.as_ref()) {
Err(CurveError::InvalidPoint)?;
Err(io::Error::new(io::ErrorKind::Other, "non-canonical or identity point"))?;
}
Ok(point)
}

View file

@ -1,6 +1,6 @@
use std::{
marker::PhantomData,
io::{Read, Cursor},
io::{self, Read, Write},
collections::HashMap,
};
@ -34,101 +34,97 @@ fn challenge<C: Curve>(context: &str, l: u16, R: &[u8], Am: &[u8]) -> C::F {
C::hash_to_F(DST, &transcript)
}
/// Commitments message to be broadcast to all other parties.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Commitments<C: Curve>(Vec<C::G>, Vec<u8>, SchnorrSignature<C>);
impl<C: Curve> Commitments<C> {
pub fn read<R: Read>(reader: &mut R, params: FrostParams) -> io::Result<Self> {
let mut commitments = Vec::with_capacity(params.t().into());
let mut serialized = Vec::with_capacity(usize::from(params.t()) * C::G_len());
for _ in 0 .. params.t() {
let mut buf = <C::G as GroupEncoding>::Repr::default();
reader.read_exact(buf.as_mut())?;
commitments.push(C::read_G(&mut buf.as_ref())?);
serialized.extend(buf.as_ref());
}
Ok(Commitments(commitments, serialized, SchnorrSignature::read(reader)?))
}
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(&self.1)?;
self.2.write(writer)
}
}
// Implements steps 1 through 3 of round 1 of FROST DKG. Returns the coefficients, commitments, and
// the serialized commitments to be broadcasted over an authenticated channel to all parties
// the commitments to be broadcasted over an authenticated channel to all parties
fn generate_key_r1<R: RngCore + CryptoRng, C: Curve>(
rng: &mut R,
params: &FrostParams,
context: &str,
) -> (Vec<C::F>, Vec<C::G>, Vec<u8>) {
) -> (Vec<C::F>, Vec<C::G>, Commitments<C>) {
let t = usize::from(params.t);
let mut coefficients = Vec::with_capacity(t);
let mut commitments = Vec::with_capacity(t);
let mut serialized = Vec::with_capacity((C::G_len() * t) + C::G_len() + C::F_len());
let mut serialized = Vec::with_capacity(t * C::G_len());
for i in 0 .. t {
// Step 1: Generate t random values to form a polynomial with
coefficients.push(C::random_F(&mut *rng));
// Step 3: Generate public commitments
commitments.push(C::generator() * coefficients[i]);
// Serialize them for publication
serialized.extend(commitments[i].to_bytes().as_ref());
}
// Step 2: Provide a proof of knowledge
let mut r = C::random_F(rng);
serialized.extend(
schnorr::sign::<C>(
coefficients[0],
// This could be deterministic as the PoK is a singleton never opened up to cooperative
// discussion
// There's no reason to spend the time and effort to make this deterministic besides a
// general obsession with canonicity and determinism though
r,
challenge::<C>(context, params.i(), (C::generator() * r).to_bytes().as_ref(), &serialized),
)
.serialize(),
let sig = schnorr::sign::<C>(
coefficients[0],
// This could be deterministic as the PoK is a singleton never opened up to cooperative
// discussion
// There's no reason to spend the time and effort to make this deterministic besides a
// general obsession with canonicity and determinism though
r,
challenge::<C>(context, params.i(), (C::generator() * r).to_bytes().as_ref(), &serialized),
);
r.zeroize();
// Step 4: Broadcast
(coefficients, commitments, serialized)
(coefficients, commitments.clone(), Commitments(commitments, serialized, sig))
}
// Verify the received data from the first round of key generation
fn verify_r1<Re: Read, R: RngCore + CryptoRng, C: Curve>(
fn verify_r1<R: RngCore + CryptoRng, C: Curve>(
rng: &mut R,
params: &FrostParams,
context: &str,
our_commitments: Vec<C::G>,
mut serialized: HashMap<u16, Re>,
mut msgs: HashMap<u16, Commitments<C>>,
) -> Result<HashMap<u16, Vec<C::G>>, FrostError> {
validate_map(&serialized, &(1 ..= params.n()).collect::<Vec<_>>(), params.i())?;
let mut commitments = HashMap::new();
commitments.insert(params.i, our_commitments);
validate_map(&msgs, &(1 ..= params.n()).collect::<Vec<_>>(), params.i())?;
let mut signatures = Vec::with_capacity(usize::from(params.n() - 1));
for l in 1 ..= params.n() {
if l == params.i {
continue;
}
let invalid = FrostError::InvalidCommitment(l);
// Read the entire list of commitments as the key we're providing a PoK for (A) and the message
#[allow(non_snake_case)]
let mut Am = vec![0; usize::from(params.t()) * C::G_len()];
serialized.get_mut(&l).unwrap().read_exact(&mut Am).map_err(|_| invalid)?;
let mut these_commitments = vec![];
let mut cursor = Cursor::new(&Am);
for _ in 0 .. usize::from(params.t()) {
these_commitments.push(C::read_G(&mut cursor).map_err(|_| invalid)?);
}
// Don't bother validating our own proof of knowledge
if l != params.i() {
let cursor = serialized.get_mut(&l).unwrap();
#[allow(non_snake_case)]
let R = C::read_G(cursor).map_err(|_| FrostError::InvalidProofOfKnowledge(l))?;
let s = C::read_F(cursor).map_err(|_| FrostError::InvalidProofOfKnowledge(l))?;
let mut commitments = msgs
.drain()
.map(|(l, msg)| {
// Step 5: Validate each proof of knowledge
// This is solely the prep step for the latter batch verification
signatures.push((
l,
these_commitments[0],
challenge::<C>(context, l, R.to_bytes().as_ref(), &Am),
SchnorrSignature::<C> { R, s },
msg.0[0],
challenge::<C>(context, l, msg.2.R.to_bytes().as_ref(), &msg.1),
msg.2,
));
}
commitments.insert(l, these_commitments);
}
(l, msg.0)
})
.collect::<HashMap<_, _>>();
schnorr::batch_verify(rng, &signatures).map_err(FrostError::InvalidProofOfKnowledge)?;
commitments.insert(params.i, our_commitments);
Ok(commitments)
}
@ -144,18 +140,39 @@ fn polynomial<F: PrimeField>(coefficients: &[F], l: u16) -> F {
share
}
// Implements round 1, step 5 and round 2, step 1 of FROST key generation
/// Secret share, to be sent only to the party it's intended for, over an encrypted and
/// authenticated channel.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct SecretShare<C: Curve>(C::F);
impl<C: Curve> SecretShare<C> {
pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> {
Ok(SecretShare(C::read_F(reader)?))
}
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(self.0.to_repr().as_ref())
}
}
impl<C: Curve> Drop for SecretShare<C> {
fn drop(&mut self) {
self.zeroize();
}
}
impl<C: Curve> ZeroizeOnDrop for SecretShare<C> {}
// Calls round 1, step 5 and implements round 2, step 1 of FROST key generation
// Returns our secret share part, commitments for the next step, and a vector for each
// counterparty to receive
fn generate_key_r2<Re: Read, R: RngCore + CryptoRng, C: Curve>(
fn generate_key_r2<R: RngCore + CryptoRng, C: Curve>(
rng: &mut R,
params: &FrostParams,
context: &str,
coefficients: &mut Vec<C::F>,
our_commitments: Vec<C::G>,
commitments: HashMap<u16, Re>,
) -> Result<(C::F, HashMap<u16, Vec<C::G>>, HashMap<u16, Vec<u8>>), FrostError> {
let commitments = verify_r1::<_, _, C>(rng, params, context, our_commitments, commitments)?;
msgs: HashMap<u16, Commitments<C>>,
) -> Result<(C::F, HashMap<u16, Vec<C::G>>, HashMap<u16, SecretShare<C>>), FrostError> {
let commitments = verify_r1::<_, C>(rng, params, context, our_commitments, msgs)?;
// Step 1: Generate secret shares for all other parties
let mut res = HashMap::new();
@ -166,7 +183,7 @@ fn generate_key_r2<Re: Read, R: RngCore + CryptoRng, C: Curve>(
continue;
}
res.insert(l, polynomial(coefficients, l).to_repr().as_ref().to_vec());
res.insert(l, SecretShare(polynomial(coefficients, l)));
}
// Calculate our own share
@ -177,24 +194,17 @@ fn generate_key_r2<Re: Read, R: RngCore + CryptoRng, C: Curve>(
Ok((share, commitments, res))
}
/// Finishes round 2 and returns both the secret share and the serialized public key.
/// This key MUST NOT be considered usable until all parties confirm they have completed the
/// protocol without issue.
fn complete_r2<Re: Read, R: RngCore + CryptoRng, C: Curve>(
// Finishes round 2 and returns the keys.
// This key MUST NOT be considered usable until all parties confirm they have completed the
// protocol without issue.
fn complete_r2<R: RngCore + CryptoRng, C: Curve>(
rng: &mut R,
params: FrostParams,
mut secret_share: C::F,
commitments: &mut HashMap<u16, Vec<C::G>>,
mut serialized: HashMap<u16, Re>,
mut shares: HashMap<u16, SecretShare<C>>,
) -> Result<FrostCore<C>, FrostError> {
validate_map(&serialized, &(1 ..= params.n()).collect::<Vec<_>>(), params.i())?;
// Step 2. Verify each share
let mut shares = HashMap::new();
// TODO: Clear serialized
for (l, share) in serialized.iter_mut() {
shares.insert(*l, C::read_F(share).map_err(|_| FrostError::InvalidShare(*l))?);
}
validate_map(&shares, &(1 ..= params.n()).collect::<Vec<_>>(), params.i())?;
// Calculate the exponent for a given participant and apply it to a series of commitments
// Initially used with the actual commitments to verify the secret share, later used with stripes
@ -210,22 +220,18 @@ fn complete_r2<Re: Read, R: RngCore + CryptoRng, C: Curve>(
};
let mut batch = BatchVerifier::new(shares.len());
for (l, share) in shares.iter_mut() {
if *l == params.i() {
continue;
}
secret_share += *share;
for (l, mut share) in shares.drain() {
secret_share += share.0;
// This can be insecurely linearized from n * t to just n using the below sums for a given
// stripe. Doing so uses naive addition which is subject to malleability. The only way to
// ensure that malleability isn't present is to use this n * t algorithm, which runs
// per sender and not as an aggregate of all senders, which also enables blame
let mut values = exponential(params.i, &commitments[l]);
values.push((-*share, C::generator()));
let mut values = exponential(params.i, &commitments[&l]);
values.push((-share.0, C::generator()));
share.zeroize();
batch.queue(rng, *l, values);
batch.queue(rng, l, values);
}
batch.verify_with_vartime_blame().map_err(FrostError::InvalidCommitment)?;
@ -299,14 +305,14 @@ impl<C: Curve> KeyGenMachine<C> {
}
/// Start generating a key according to the FROST DKG spec.
/// Returns a serialized list of commitments to be sent to all parties over an authenticated
/// Returns a commitments message to be sent to all parties over an authenticated
/// channel. If any party submits multiple sets of commitments, they MUST be treated as
/// malicious.
pub fn generate_coefficients<R: RngCore + CryptoRng>(
self,
rng: &mut R,
) -> (SecretShareMachine<C>, Vec<u8>) {
let (coefficients, our_commitments, serialized) =
) -> (SecretShareMachine<C>, Commitments<C>) {
let (coefficients, our_commitments, commitments) =
generate_key_r1::<_, C>(rng, &self.params, &self.context);
(
@ -316,21 +322,21 @@ impl<C: Curve> KeyGenMachine<C> {
coefficients,
our_commitments,
},
serialized,
commitments,
)
}
}
impl<C: Curve> SecretShareMachine<C> {
/// Continue generating a key.
/// Takes in everyone else's commitments. Returns a HashMap of byte vectors representing secret
/// shares. These MUST be encrypted and only then sent to their respective participants.
pub fn generate_secret_shares<Re: Read, R: RngCore + CryptoRng>(
/// Takes in everyone else's commitments. Returns a HashMap of secret shares.
/// These MUST be encrypted and only then sent to their respective participants.
pub fn generate_secret_shares<R: RngCore + CryptoRng>(
mut self,
rng: &mut R,
commitments: HashMap<u16, Re>,
) -> Result<(KeyMachine<C>, HashMap<u16, Vec<u8>>), FrostError> {
let (secret, commitments, shares) = generate_key_r2::<_, _, C>(
commitments: HashMap<u16, Commitments<C>>,
) -> Result<(KeyMachine<C>, HashMap<u16, SecretShare<C>>), FrostError> {
let (secret, commitments, shares) = generate_key_r2::<_, C>(
rng,
&self.params,
&self.context,
@ -347,10 +353,10 @@ impl<C: Curve> KeyMachine<C> {
/// Takes in everyone elses' shares submitted to us. Returns a FrostCore object representing the
/// generated keys. Successful protocol completion MUST be confirmed by all parties before these
/// keys may be safely used.
pub fn complete<Re: Read, R: RngCore + CryptoRng>(
pub fn complete<R: RngCore + CryptoRng>(
mut self,
rng: &mut R,
shares: HashMap<u16, Re>,
shares: HashMap<u16, SecretShare<C>>,
) -> Result<FrostCore<C>, FrostError> {
complete_r2(rng, self.params, self.secret, &mut self.commitments, shares)
}

View file

@ -38,6 +38,7 @@ pub mod promote;
/// Algorithm for the signing process.
pub mod algorithm;
mod nonce;
/// Threshold signing protocol.
pub mod sign;
@ -45,7 +46,7 @@ pub mod sign;
#[cfg(any(test, feature = "tests"))]
pub mod tests;
// Validate a map of serialized values to have the expected included participants
// Validate a map of values to have the expected included participants
pub(crate) fn validate_map<T>(
map: &HashMap<u16, T>,
included: &[u16],
@ -136,6 +137,8 @@ pub enum FrostError {
InvalidCommitment(u16),
#[error("invalid proof of knowledge (participant {0})")]
InvalidProofOfKnowledge(u16),
#[error("invalid preprocess (participant {0})")]
InvalidPreprocess(u16),
#[error("invalid share (participant {0})")]
InvalidShare(u16),

271
crypto/frost/src/nonce.rs Normal file
View file

@ -0,0 +1,271 @@
// FROST defines its nonce as sum(Di, Ei * bi)
// Monero needs not just the nonce over G however, yet also over H
// Then there is a signature (a modified Chaum Pedersen proof) using multiple nonces at once
//
// Accordingly, in order for this library to be robust, it supports generating an arbitrary amount
// of nonces, each against an arbitrary list of basepoints
//
// Each nonce remains of the form (d, e) and made into a proper nonce with d + (e * b)
// When multiple D, E pairs are provided, a DLEq proof is also provided to confirm their integrity
use std::{
io::{self, Read, Write},
collections::HashMap,
};
use rand_core::{RngCore, CryptoRng};
use zeroize::Zeroize;
use transcript::Transcript;
use group::{ff::PrimeField, Group, GroupEncoding};
use multiexp::multiexp_vartime;
use dleq::DLEqProof;
use crate::curve::Curve;
fn dleq_transcript<T: Transcript>() -> T {
T::new(b"FROST_nonce_dleq")
}
// Each nonce is actually a pair of random scalars, notated as d, e under the FROST paper
// This is considered a single nonce as r = d + be
#[derive(Clone, Zeroize)]
pub(crate) struct Nonce<C: Curve>(pub(crate) [C::F; 2]);
// Commitments to a specific generator for this nonce
#[derive(Copy, Clone, PartialEq, Eq, Zeroize)]
pub(crate) struct GeneratorCommitments<C: Curve>(pub(crate) [C::G; 2]);
impl<C: Curve> GeneratorCommitments<C> {
fn read<R: Read>(reader: &mut R) -> io::Result<GeneratorCommitments<C>> {
Ok(GeneratorCommitments([C::read_G(reader)?, C::read_G(reader)?]))
}
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(self.0[0].to_bytes().as_ref())?;
writer.write_all(self.0[1].to_bytes().as_ref())
}
}
// A single nonce's commitments and relevant proofs
#[derive(Clone, PartialEq, Eq, Zeroize)]
pub(crate) struct NonceCommitments<C: Curve> {
// Called generators as these commitments are indexed by generator
pub(crate) generators: Vec<GeneratorCommitments<C>>,
// DLEq Proofs proving that these commitments are generated using the same scalar pair
// This could be further optimized with a multi-nonce proof, offering just one proof for all
// nonces. See https://github.com/serai-dex/serai/issues/38
// TODO
pub(crate) dleqs: Option<[DLEqProof<C::G>; 2]>,
}
impl<C: Curve> NonceCommitments<C> {
pub(crate) fn new<R: RngCore + CryptoRng, T: Transcript>(
rng: &mut R,
mut secret_share: C::F,
generators: &[C::G],
) -> (Nonce<C>, NonceCommitments<C>) {
let nonce =
Nonce([C::random_nonce(secret_share, &mut *rng), C::random_nonce(secret_share, &mut *rng)]);
secret_share.zeroize();
let mut commitments = Vec::with_capacity(generators.len());
for generator in generators {
commitments.push(GeneratorCommitments([*generator * nonce.0[0], *generator * nonce.0[1]]));
}
let mut dleqs = None;
if generators.len() >= 2 {
let mut dleq = |nonce| {
// Uses an independent transcript as each signer must prove this with their commitments,
// yet they're validated while processing everyone's data sequentially, by the global order
// This avoids needing to clone and fork the transcript around
// TODO: At least include a challenge from the existing transcript
DLEqProof::prove(&mut *rng, &mut dleq_transcript::<T>(), generators, nonce)
};
dleqs = Some([dleq(nonce.0[0]), dleq(nonce.0[1])]);
}
(nonce, NonceCommitments { generators: commitments, dleqs })
}
fn read<R: Read, T: Transcript>(
reader: &mut R,
generators: &[C::G],
) -> io::Result<NonceCommitments<C>> {
let commitments: Vec<GeneratorCommitments<C>> = (0 .. generators.len())
.map(|_| GeneratorCommitments::read(reader))
.collect::<Result<_, _>>()?;
let mut dleqs = None;
if generators.len() >= 2 {
let mut verify = |i| -> io::Result<_> {
let dleq = DLEqProof::deserialize(reader)?;
dleq
.verify(
&mut dleq_transcript::<T>(),
generators,
&commitments.iter().map(|commitments| commitments.0[i]).collect::<Vec<_>>(),
)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid DLEq proof"))?;
Ok(dleq)
};
dleqs = Some([verify(0)?, verify(1)?]);
}
Ok(NonceCommitments { generators: commitments, dleqs })
}
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
for generator in &self.generators {
generator.write(writer)?;
}
if let Some(dleqs) = &self.dleqs {
dleqs[0].serialize(writer)?;
dleqs[1].serialize(writer)?;
}
Ok(())
}
}
#[derive(Clone, PartialEq, Eq, Zeroize)]
pub(crate) struct Commitments<C: Curve> {
// Called nonces as these commitments are indexed by nonce
pub(crate) nonces: Vec<NonceCommitments<C>>,
}
impl<C: Curve> Commitments<C> {
pub(crate) fn new<R: RngCore + CryptoRng, T: Transcript>(
rng: &mut R,
secret_share: C::F,
planned_nonces: &[Vec<C::G>],
) -> (Vec<Nonce<C>>, Commitments<C>) {
let mut nonces = vec![];
let mut commitments = vec![];
for generators in planned_nonces {
let (nonce, these_commitments) =
NonceCommitments::new::<_, T>(&mut *rng, secret_share, generators);
nonces.push(nonce);
commitments.push(these_commitments);
}
(nonces, Commitments { nonces: commitments })
}
pub(crate) fn transcript<T: Transcript>(&self, t: &mut T) {
for nonce in &self.nonces {
for commitments in &nonce.generators {
t.append_message(b"commitment_D", commitments.0[0].to_bytes().as_ref());
t.append_message(b"commitment_E", commitments.0[1].to_bytes().as_ref());
}
// Transcripting the DLEqs implicitly transcripts the exact generators used for this nonce
// This means it shouldn't be possible for variadic generators to cause conflicts as they're
// committed to as their entire series per-nonce, not as isolates
if let Some(dleqs) = &nonce.dleqs {
let mut transcript_dleq = |label, dleq: &DLEqProof<C::G>| {
let mut buf = Vec::with_capacity(C::G_len() + C::F_len());
dleq.serialize(&mut buf).unwrap();
t.append_message(label, &buf);
};
transcript_dleq(b"dleq_D", &dleqs[0]);
transcript_dleq(b"dleq_E", &dleqs[1]);
}
}
}
pub(crate) fn read<R: Read, T: Transcript>(
reader: &mut R,
nonces: &[Vec<C::G>],
) -> io::Result<Self> {
Ok(Commitments {
nonces: (0 .. nonces.len())
.map(|i| NonceCommitments::read::<_, T>(reader, &nonces[i]))
.collect::<Result<_, _>>()?,
})
}
pub(crate) fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
for nonce in &self.nonces {
nonce.write(writer)?;
}
Ok(())
}
}
#[derive(Zeroize)]
pub(crate) struct IndividualBinding<C: Curve> {
commitments: Commitments<C>,
binding_factors: Option<Vec<C::F>>,
}
pub(crate) struct BindingFactor<C: Curve>(pub(crate) HashMap<u16, IndividualBinding<C>>);
impl<C: Curve> Zeroize for BindingFactor<C> {
fn zeroize(&mut self) {
for (mut validator, mut binding) in self.0.drain() {
validator.zeroize();
binding.zeroize();
}
}
}
impl<C: Curve> BindingFactor<C> {
pub(crate) fn insert(&mut self, i: u16, commitments: Commitments<C>) {
self.0.insert(i, IndividualBinding { commitments, binding_factors: None });
}
pub(crate) fn calculate_binding_factors<T: Clone + Transcript>(&mut self, transcript: &mut T) {
for (l, binding) in self.0.iter_mut() {
let mut transcript = transcript.clone();
transcript.append_message(b"participant", C::F::from(u64::from(*l)).to_repr().as_ref());
// It *should* be perfectly fine to reuse a binding factor for multiple nonces
// This generates a binding factor per nonce just to ensure it never comes up as a question
binding.binding_factors = Some(
(0 .. binding.commitments.nonces.len())
.map(|_| C::hash_binding_factor(transcript.challenge(b"rho").as_ref()))
.collect(),
);
}
}
pub(crate) fn binding_factors(&self, i: u16) -> &[C::F] {
self.0[&i].binding_factors.as_ref().unwrap()
}
// Get the bound nonces for a specific party
pub(crate) fn bound(&self, l: u16) -> Vec<Vec<C::G>> {
let mut res = vec![];
for (i, (nonce, rho)) in
self.0[&l].commitments.nonces.iter().zip(self.binding_factors(l).iter()).enumerate()
{
res.push(vec![]);
for generator in &nonce.generators {
res[i].push(generator.0[0] + (generator.0[1] * rho));
}
}
res
}
// Get the nonces for this signing session
pub(crate) fn nonces(&self, planned_nonces: &[Vec<C::G>]) -> Vec<Vec<C::G>> {
let mut nonces = Vec::with_capacity(planned_nonces.len());
for n in 0 .. planned_nonces.len() {
nonces.push(Vec::with_capacity(planned_nonces[n].len()));
for g in 0 .. planned_nonces[n].len() {
#[allow(non_snake_case)]
let mut D = C::G::identity();
let mut statements = Vec::with_capacity(self.0.len());
#[allow(non_snake_case)]
for IndividualBinding { commitments, binding_factors } in self.0.values() {
D += commitments.nonces[n].generators[g].0[0];
statements
.push((binding_factors.as_ref().unwrap()[n], commitments.nonces[n].generators[g].0[1]));
}
nonces[n].push(D + multiexp_vartime(&statements));
}
}
nonces
}
}

View file

@ -12,10 +12,7 @@ use group::GroupEncoding;
use transcript::{Transcript, RecommendedTranscript};
use dleq::DLEqProof;
use crate::{
curve::{CurveError, Curve},
FrostError, FrostCore, FrostKeys, validate_map,
};
use crate::{curve::Curve, FrostError, FrostCore, FrostKeys, validate_map};
/// Promote a set of keys to another Curve definition.
pub trait CurvePromote<C2: Curve> {
@ -73,11 +70,8 @@ impl<C: Curve> GeneratorProof<C> {
self.proof.serialize(writer)
}
pub fn deserialize<R: Read>(reader: &mut R) -> Result<GeneratorProof<C>, CurveError> {
Ok(GeneratorProof {
share: C::read_G(reader)?,
proof: DLEqProof::deserialize(reader).map_err(|_| CurveError::InvalidScalar)?,
})
pub fn deserialize<R: Read>(reader: &mut R) -> io::Result<GeneratorProof<C>> {
Ok(GeneratorProof { share: C::read_G(reader)?, proof: DLEqProof::deserialize(reader)? })
}
}

View file

@ -1,3 +1,5 @@
use std::io::{self, Read, Write};
use rand_core::{RngCore, CryptoRng};
use zeroize::Zeroize;
@ -9,7 +11,7 @@ use group::{
use multiexp::BatchVerifier;
use crate::Curve;
use crate::curve::Curve;
/// A Schnorr signature of the form (R, s) where s = r + cx.
#[allow(non_snake_case)]
@ -20,11 +22,13 @@ pub struct SchnorrSignature<C: Curve> {
}
impl<C: Curve> SchnorrSignature<C> {
pub fn serialize(&self) -> Vec<u8> {
let mut res = Vec::with_capacity(C::G_len() + C::F_len());
res.extend(self.R.to_bytes().as_ref());
res.extend(self.s.to_repr().as_ref());
res
pub(crate) fn read<R: Read>(reader: &mut R) -> io::Result<Self> {
Ok(SchnorrSignature { R: C::read_G(reader)?, s: C::read_F(reader)? })
}
pub(crate) fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(self.R.to_bytes().as_ref())?;
writer.write_all(self.s.to_repr().as_ref())
}
}

View file

@ -1,28 +1,40 @@
use core::fmt;
use std::{
io::{Read, Cursor},
io::{self, Read, Write},
collections::HashMap,
};
use rand_core::{RngCore, CryptoRng};
use zeroize::{Zeroize, ZeroizeOnDrop};
use subtle::ConstantTimeEq;
use transcript::Transcript;
use group::{
ff::{Field, PrimeField},
Group, GroupEncoding,
};
use multiexp::multiexp_vartime;
use dleq::DLEqProof;
use group::{ff::PrimeField, GroupEncoding};
use crate::{
curve::Curve, FrostError, FrostParams, FrostKeys, FrostView, algorithm::Algorithm, validate_map,
curve::Curve,
FrostError, FrostParams, FrostKeys, FrostView,
algorithm::{AddendumSerialize, Addendum, Algorithm},
validate_map,
};
pub(crate) use crate::nonce::*;
/// Trait enabling writing preprocesses and signature shares.
pub trait Writable {
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()>;
}
impl<T: Writable> Writable for Vec<T> {
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
for w in self {
w.write(writer)?;
}
Ok(())
}
}
/// Pairing of an Algorithm with a FrostKeys instance and this specific signing set.
#[derive(Clone)]
pub struct Params<C: Curve, A: Algorithm<C>> {
@ -31,7 +43,6 @@ pub struct Params<C: Curve, A: Algorithm<C>> {
view: FrostView<C>,
}
// Currently public to enable more complex operations as desired, yet solely used in testing
impl<C: Curve, A: Algorithm<C>> Params<C, A> {
pub fn new(
algorithm: A,
@ -79,104 +90,75 @@ impl<C: Curve, A: Algorithm<C>> Params<C, A> {
}
}
fn nonce_transcript<T: Transcript>() -> T {
T::new(b"FROST_nonce_dleq")
/// Preprocess for an instance of the FROST signing protocol.
#[derive(Clone, PartialEq, Eq, Zeroize)]
pub struct Preprocess<C: Curve, A: Addendum> {
pub(crate) commitments: Commitments<C>,
pub addendum: A,
}
impl<C: Curve, A: Addendum> Writable for Preprocess<C, A> {
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
self.commitments.write(writer)?;
self.addendum.write(writer)
}
}
#[derive(Zeroize)]
pub(crate) struct PreprocessPackage<C: Curve> {
pub(crate) nonces: Vec<[C::F; 2]>,
#[zeroize(skip)]
pub(crate) commitments: Vec<Vec<[C::G; 2]>>,
pub(crate) addendum: Vec<u8>,
pub(crate) struct PreprocessData<C: Curve, A: Addendum> {
pub(crate) nonces: Vec<Nonce<C>>,
pub(crate) preprocess: Preprocess<C, A>,
}
impl<C: Curve> Drop for PreprocessPackage<C> {
impl<C: Curve, A: Addendum> Drop for PreprocessData<C, A> {
fn drop(&mut self) {
self.zeroize()
}
}
impl<C: Curve> ZeroizeOnDrop for PreprocessPackage<C> {}
impl<C: Curve, A: Addendum> ZeroizeOnDrop for PreprocessData<C, A> {}
fn preprocess<R: RngCore + CryptoRng, C: Curve, A: Algorithm<C>>(
rng: &mut R,
params: &mut Params<C, A>,
) -> (PreprocessPackage<C>, Vec<u8>) {
let mut serialized = Vec::with_capacity(2 * C::G_len());
let (nonces, commitments) = params
.algorithm
.nonces()
.iter()
.map(|generators| {
let nonces = [
C::random_nonce(params.view().secret_share(), &mut *rng),
C::random_nonce(params.view().secret_share(), &mut *rng),
];
let commit = |generator: C::G, buf: &mut Vec<u8>| {
let commitments = [generator * nonces[0], generator * nonces[1]];
buf.extend(commitments[0].to_bytes().as_ref());
buf.extend(commitments[1].to_bytes().as_ref());
commitments
};
let mut commitments = Vec::with_capacity(generators.len());
for generator in generators.iter() {
commitments.push(commit(*generator, &mut serialized));
}
// Provide a DLEq proof to verify these commitments are for the same nonce
if generators.len() >= 2 {
// Uses an independent transcript as each signer must do this now, yet we validate them
// sequentially by the global order. Avoids needing to clone and fork the transcript around
let mut transcript = nonce_transcript::<A::Transcript>();
// This could be further optimized with a multi-nonce proof.
// See https://github.com/serai-dex/serai/issues/38
for mut nonce in nonces {
DLEqProof::prove(&mut *rng, &mut transcript, generators, nonce)
.serialize(&mut serialized)
.unwrap();
nonce.zeroize();
}
}
(nonces, commitments)
})
.unzip();
) -> (PreprocessData<C, A::Addendum>, Preprocess<C, A::Addendum>) {
let (nonces, commitments) = Commitments::new::<_, A::Transcript>(
&mut *rng,
params.view().secret_share(),
&params.algorithm.nonces(),
);
let addendum = params.algorithm.preprocess_addendum(rng, &params.view);
serialized.extend(&addendum);
(PreprocessPackage { nonces, commitments, addendum }, serialized)
let preprocess = Preprocess { commitments, addendum };
(PreprocessData { nonces, preprocess: preprocess.clone() }, preprocess)
}
#[allow(non_snake_case)]
fn read_D_E<Re: Read, C: Curve>(cursor: &mut Re, l: u16) -> Result<[C::G; 2], FrostError> {
Ok([
C::read_G(cursor).map_err(|_| FrostError::InvalidCommitment(l))?,
C::read_G(cursor).map_err(|_| FrostError::InvalidCommitment(l))?,
])
}
#[allow(non_snake_case)]
struct Package<C: Curve> {
B: HashMap<u16, (Vec<Vec<[C::G; 2]>>, C::F)>,
struct SignData<C: Curve> {
B: BindingFactor<C>,
Rs: Vec<Vec<C::G>>,
share: C::F,
}
/// Share of a signature produced via FROST.
#[derive(Clone, PartialEq, Eq, Zeroize)]
pub struct SignatureShare<C: Curve>(C::F);
impl<C: Curve> Writable for SignatureShare<C> {
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(self.0.to_repr().as_ref())
}
}
// Has every signer perform the role of the signature aggregator
// Step 1 was already deprecated by performing nonce generation as needed
// Step 2 is simply the broadcast round from step 1
fn sign_with_share<Re: Read, C: Curve, A: Algorithm<C>>(
fn sign_with_share<C: Curve, A: Algorithm<C>>(
params: &mut Params<C, A>,
our_preprocess: PreprocessPackage<C>,
mut commitments: HashMap<u16, Re>,
mut our_preprocess: PreprocessData<C, A::Addendum>,
mut preprocesses: HashMap<u16, Preprocess<C, A::Addendum>>,
msg: &[u8],
) -> Result<(Package<C>, Vec<u8>), FrostError> {
) -> Result<(SignData<C>, SignatureShare<C>), FrostError> {
let multisig_params = params.multisig_params();
validate_map(&commitments, &params.view.included, multisig_params.i)?;
validate_map(&preprocesses, &params.view.included, multisig_params.i)?;
{
// Domain separate FROST
@ -185,9 +167,9 @@ fn sign_with_share<Re: Read, C: Curve, A: Algorithm<C>>(
let nonces = params.algorithm.nonces();
#[allow(non_snake_case)]
let mut B = HashMap::<u16, _>::with_capacity(params.view.included.len());
let mut B = BindingFactor(HashMap::<u16, _>::with_capacity(params.view.included.len()));
{
// Parse the commitments
// Parse the preprocesses
for l in &params.view.included {
{
params
@ -196,73 +178,39 @@ fn sign_with_share<Re: Read, C: Curve, A: Algorithm<C>>(
.append_message(b"participant", C::F::from(u64::from(*l)).to_repr().as_ref());
}
// While this doesn't note which nonce/basepoint this is for, those are expected to be
// static. Beyond that, they're committed to in the DLEq proof transcripts, ensuring
// consistency. While this is suboptimal, it maintains IETF compliance, and Algorithm is
// documented accordingly
let transcript = |t: &mut A::Transcript, commitments: [C::G; 2]| {
if commitments[0].ct_eq(&C::G::identity()).into() ||
commitments[1].ct_eq(&C::G::identity()).into()
{
Err(FrostError::InvalidCommitment(*l))?;
}
t.append_message(b"commitment_D", commitments[0].to_bytes().as_ref());
t.append_message(b"commitment_E", commitments[1].to_bytes().as_ref());
Ok(())
};
if *l == params.keys.params().i {
for nonce_commitments in &our_preprocess.commitments {
for commitments in nonce_commitments {
transcript(params.algorithm.transcript(), *commitments).unwrap();
}
let commitments = our_preprocess.preprocess.commitments.clone();
commitments.transcript(params.algorithm.transcript());
let addendum = our_preprocess.preprocess.addendum.clone();
{
let mut buf = vec![];
addendum.write(&mut buf).unwrap();
params.algorithm.transcript().append_message(b"addendum", &buf);
}
B.insert(*l, (our_preprocess.commitments.clone(), C::F::zero()));
params.algorithm.process_addendum(
&params.view,
*l,
&mut Cursor::new(our_preprocess.addendum.clone()),
)?;
B.insert(*l, commitments);
params.algorithm.process_addendum(&params.view, *l, addendum)?;
} else {
let mut cursor = commitments.remove(l).unwrap();
let mut commitments = Vec::with_capacity(nonces.len());
for (n, nonce_generators) in nonces.clone().iter_mut().enumerate() {
commitments.push(Vec::with_capacity(nonce_generators.len()));
for _ in 0 .. nonce_generators.len() {
commitments[n].push(read_D_E::<_, C>(&mut cursor, *l)?);
transcript(params.algorithm.transcript(), commitments[n][commitments[n].len() - 1])?;
}
if nonce_generators.len() >= 2 {
let mut transcript = nonce_transcript::<A::Transcript>();
for de in 0 .. 2 {
DLEqProof::deserialize(&mut cursor)
.map_err(|_| FrostError::InvalidCommitment(*l))?
.verify(
&mut transcript,
nonce_generators,
&commitments[n].iter().map(|commitments| commitments[de]).collect::<Vec<_>>(),
)
.map_err(|_| FrostError::InvalidCommitment(*l))?;
}
}
let preprocess = preprocesses.remove(l).unwrap();
preprocess.commitments.transcript(params.algorithm.transcript());
{
let mut buf = vec![];
preprocess.addendum.write(&mut buf).unwrap();
params.algorithm.transcript().append_message(b"addendum", &buf);
}
B.insert(*l, (commitments, C::F::zero()));
params.algorithm.process_addendum(&params.view, *l, &mut cursor)?;
B.insert(*l, preprocess.commitments);
params.algorithm.process_addendum(&params.view, *l, preprocess.addendum)?;
}
}
// Re-format into the FROST-expected rho transcript
let mut rho_transcript = A::Transcript::new(b"FROST_rho");
rho_transcript.append_message(b"message", &C::hash_msg(msg));
// This won't just be the commitments, yet the full existing transcript if used in an extended
// protocol
rho_transcript.append_message(
b"commitments",
&C::hash_commitments(params.algorithm.transcript().challenge(b"commitments").as_ref()),
b"preprocesses",
&C::hash_commitments(params.algorithm.transcript().challenge(b"preprocesses").as_ref()),
);
// Include the offset, if one exists
@ -280,14 +228,10 @@ fn sign_with_share<Re: Read, C: Curve, A: Algorithm<C>>(
}
// Generate the per-signer binding factors
for (l, commitments) in B.iter_mut() {
let mut rho_transcript = rho_transcript.clone();
rho_transcript.append_message(b"participant", C::F::from(u64::from(*l)).to_repr().as_ref());
commitments.1 = C::hash_binding_factor(rho_transcript.challenge(b"rho").as_ref());
}
B.calculate_binding_factors(&mut rho_transcript);
// Merge the rho transcript back into the global one to ensure its advanced while committing to
// everything
// Merge the rho transcript back into the global one to ensure its advanced, while
// simultaneously committing to everything
params
.algorithm
.transcript()
@ -295,60 +239,44 @@ fn sign_with_share<Re: Read, C: Curve, A: Algorithm<C>>(
}
#[allow(non_snake_case)]
let mut Rs = Vec::with_capacity(nonces.len());
for n in 0 .. nonces.len() {
Rs.push(vec![C::G::identity(); nonces[n].len()]);
for g in 0 .. nonces[n].len() {
#[allow(non_snake_case)]
let mut D = C::G::identity();
let mut statements = Vec::with_capacity(B.len());
#[allow(non_snake_case)]
for (B, binding) in B.values() {
D += B[n][g][0];
statements.push((*binding, B[n][g][1]));
}
Rs[n][g] = D + multiexp_vartime(&statements);
}
}
let Rs = B.nonces(&nonces);
let our_binding_factors = B.binding_factors(multisig_params.i());
let mut nonces = our_preprocess
.nonces
.iter()
.map(|nonces| nonces[0] + (nonces[1] * B[&params.keys.params().i()].1))
.enumerate()
.map(|(n, nonces)| nonces.0[0] + (nonces.0[1] * our_binding_factors[n]))
.collect::<Vec<_>>();
our_preprocess.nonces.zeroize();
let share = params.algorithm.sign_share(&params.view, &Rs, &nonces, msg);
nonces.zeroize();
Ok((Package { B, Rs, share }, share.to_repr().as_ref().to_vec()))
Ok((SignData { B, Rs, share }, SignatureShare(share)))
}
fn complete<Re: Read, C: Curve, A: Algorithm<C>>(
fn complete<C: Curve, A: Algorithm<C>>(
sign_params: &Params<C, A>,
sign: Package<C>,
mut shares: HashMap<u16, Re>,
sign: SignData<C>,
mut shares: HashMap<u16, SignatureShare<C>>,
) -> Result<A::Signature, FrostError> {
let params = sign_params.multisig_params();
validate_map(&shares, &sign_params.view.included, params.i)?;
let mut responses = HashMap::new();
let mut sum = C::F::zero();
for l in &sign_params.view.included {
let part = if *l == params.i {
sign.share
} else {
C::read_F(shares.get_mut(l).unwrap()).map_err(|_| FrostError::InvalidShare(*l))?
};
sum += part;
responses.insert(*l, part);
responses.insert(params.i(), sign.share);
let mut sum = sign.share;
for (l, share) in shares.drain() {
responses.insert(l, share.0);
sum += share.0;
}
// Perform signature validation instead of individual share validation
// For the success route, which should be much more frequent, this should be faster
// It also acts as an integrity check of this library's signing function
let res = sign_params.algorithm.verify(sign_params.view.group_key, &sign.Rs, sum);
if let Some(res) = res {
return Ok(res);
if let Some(sig) = sign_params.algorithm.verify(sign_params.view.group_key, &sign.Rs, sum) {
return Ok(sig);
}
// Find out who misbehaved. It may be beneficial to randomly sort this to have detection be
@ -356,13 +284,7 @@ fn complete<Re: Read, C: Curve, A: Algorithm<C>>(
for l in &sign_params.view.included {
if !sign_params.algorithm.verify_share(
sign_params.view.verification_share(*l),
&sign.B[l]
.0
.iter()
.map(|nonces| {
nonces.iter().map(|commitments| commitments[0] + (commitments[1] * sign.B[l].1)).collect()
})
.collect::<Vec<_>>(),
&sign.B.bound(*l),
responses[l],
) {
Err(FrostError::InvalidShare(*l))?;
@ -375,33 +297,53 @@ fn complete<Re: Read, C: Curve, A: Algorithm<C>>(
/// Trait for the initial state machine of a two-round signing protocol.
pub trait PreprocessMachine {
/// Preprocess message for this machine.
type Preprocess: Clone + PartialEq + Writable;
/// Signature produced by this machine.
type Signature: Clone + PartialEq + fmt::Debug;
type SignMachine: SignMachine<Self::Signature>;
/// SignMachine this PreprocessMachine turns into.
type SignMachine: SignMachine<Self::Signature, Preprocess = Self::Preprocess>;
/// Perform the preprocessing round required in order to sign.
/// Returns a byte vector to be broadcast to all participants, over an authenticated channel.
fn preprocess<R: RngCore + CryptoRng>(self, rng: &mut R) -> (Self::SignMachine, Vec<u8>);
/// Returns a preprocess message to be broadcast to all participants, over an authenticated
/// channel.
fn preprocess<R: RngCore + CryptoRng>(self, rng: &mut R)
-> (Self::SignMachine, Self::Preprocess);
}
/// Trait for the second machine of a two-round signing protocol.
pub trait SignMachine<S> {
type SignatureMachine: SignatureMachine<S>;
/// Preprocess message for this machine.
type Preprocess: Clone + PartialEq + Writable;
/// SignatureShare message for this machine.
type SignatureShare: Clone + PartialEq + Writable;
/// SignatureMachine this SignMachine turns into.
type SignatureMachine: SignatureMachine<S, SignatureShare = Self::SignatureShare>;
/// Read a Preprocess message.
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess>;
/// Sign a message.
/// Takes in the participants' preprocesses. Returns a byte vector representing a signature share
/// to be broadcast to all participants, over an authenticated channel.
fn sign<Re: Read>(
/// Takes in the participants' preprocess messages. Returns the signature share to be broadcast
/// to all participants, over an authenticated channel.
fn sign(
self,
commitments: HashMap<u16, Re>,
commitments: HashMap<u16, Self::Preprocess>,
msg: &[u8],
) -> Result<(Self::SignatureMachine, Vec<u8>), FrostError>;
) -> Result<(Self::SignatureMachine, Self::SignatureShare), FrostError>;
}
/// Trait for the final machine of a two-round signing protocol.
pub trait SignatureMachine<S> {
/// SignatureShare message for this machine.
type SignatureShare: Clone + PartialEq + Writable;
/// Read a Signature Share message.
fn read_share<R: Read>(&self, reader: &mut R) -> io::Result<Self::SignatureShare>;
/// Complete signing.
/// Takes in everyone elses' shares. Returns the signature.
fn complete<Re: Read>(self, shares: HashMap<u16, Re>) -> Result<S, FrostError>;
fn complete(self, shares: HashMap<u16, Self::SignatureShare>) -> Result<S, FrostError>;
}
/// State machine which manages signing for an arbitrary signature algorithm.
@ -412,13 +354,13 @@ pub struct AlgorithmMachine<C: Curve, A: Algorithm<C>> {
/// Next step of the state machine for the signing process.
pub struct AlgorithmSignMachine<C: Curve, A: Algorithm<C>> {
params: Params<C, A>,
preprocess: PreprocessPackage<C>,
preprocess: PreprocessData<C, A::Addendum>,
}
/// Final step of the state machine for the signing process.
pub struct AlgorithmSignatureMachine<C: Curve, A: Algorithm<C>> {
params: Params<C, A>,
sign: Package<C>,
sign: SignData<C>,
}
impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> {
@ -434,39 +376,58 @@ impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> {
#[cfg(any(test, feature = "tests"))]
pub(crate) fn unsafe_override_preprocess(
self,
preprocess: PreprocessPackage<C>,
preprocess: PreprocessData<C, A::Addendum>,
) -> AlgorithmSignMachine<C, A> {
AlgorithmSignMachine { params: self.params, preprocess }
}
}
impl<C: Curve, A: Algorithm<C>> PreprocessMachine for AlgorithmMachine<C, A> {
type Preprocess = Preprocess<C, A::Addendum>;
type Signature = A::Signature;
type SignMachine = AlgorithmSignMachine<C, A>;
fn preprocess<R: RngCore + CryptoRng>(self, rng: &mut R) -> (Self::SignMachine, Vec<u8>) {
fn preprocess<R: RngCore + CryptoRng>(
self,
rng: &mut R,
) -> (Self::SignMachine, Preprocess<C, A::Addendum>) {
let mut params = self.params;
let (preprocess, serialized) = preprocess::<R, C, A>(rng, &mut params);
(AlgorithmSignMachine { params, preprocess }, serialized)
let (preprocess, public) = preprocess::<R, C, A>(rng, &mut params);
(AlgorithmSignMachine { params, preprocess }, public)
}
}
impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachine<C, A> {
type Preprocess = Preprocess<C, A::Addendum>;
type SignatureShare = SignatureShare<C>;
type SignatureMachine = AlgorithmSignatureMachine<C, A>;
fn sign<Re: Read>(
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
Ok(Preprocess {
commitments: Commitments::read::<_, A::Transcript>(reader, &self.params.algorithm.nonces())?,
addendum: self.params.algorithm.read_addendum(reader)?,
})
}
fn sign(
self,
commitments: HashMap<u16, Re>,
commitments: HashMap<u16, Preprocess<C, A::Addendum>>,
msg: &[u8],
) -> Result<(Self::SignatureMachine, Vec<u8>), FrostError> {
) -> Result<(Self::SignatureMachine, SignatureShare<C>), FrostError> {
let mut params = self.params;
let (sign, serialized) = sign_with_share(&mut params, self.preprocess, commitments, msg)?;
Ok((AlgorithmSignatureMachine { params, sign }, serialized))
let (sign, public) = sign_with_share(&mut params, self.preprocess, commitments, msg)?;
Ok((AlgorithmSignatureMachine { params, sign }, public))
}
}
impl<C: Curve, A: Algorithm<C>> SignatureMachine<A::Signature> for AlgorithmSignatureMachine<C, A> {
fn complete<Re: Read>(self, shares: HashMap<u16, Re>) -> Result<A::Signature, FrostError> {
type SignatureShare = SignatureShare<C>;
fn read_share<R: Read>(&self, reader: &mut R) -> io::Result<SignatureShare<C>> {
Ok(SignatureShare(C::read_F(reader)?))
}
fn complete(self, shares: HashMap<u16, SignatureShare<C>>) -> Result<A::Signature, FrostError> {
complete(&self.params, self.sign, shares)
}
}

View file

@ -1,5 +1,3 @@
use std::io::Cursor;
use rand_core::{RngCore, CryptoRng};
use group::Group;
@ -15,7 +13,10 @@ fn key_generation<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
// Test serialization of generated keys
fn keys_serialization<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
for (_, keys) in core_gen::<_, C>(rng) {
assert_eq!(&FrostCore::<C>::deserialize(&mut Cursor::new(keys.serialize())).unwrap(), &keys);
assert_eq!(
&FrostCore::<C>::deserialize::<&[u8]>(&mut keys.serialize().as_ref()).unwrap(),
&keys
);
}
}

View file

@ -1,5 +1,3 @@
use std::io::Cursor;
use rand_core::OsRng;
use crate::{
@ -13,32 +11,31 @@ fn ed448_8032_vector() {
let context = hex::decode("666f6f").unwrap();
#[allow(non_snake_case)]
let A = Ed448::read_G(&mut Cursor::new(
hex::decode(
let A = Ed448::read_G::<&[u8]>(
&mut hex::decode(
"43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c".to_owned() +
"6798c0866aea01eb00742802b8438ea4cb82169c235160627b4c3a94" +
"80",
)
.unwrap(),
))
.unwrap()
.as_ref(),
)
.unwrap();
let msg = hex::decode("03").unwrap();
let mut sig = Cursor::new(
hex::decode(
"d4f8f6131770dd46f40867d6fd5d5055de43541f8c5e35abbcd001b3".to_owned() +
"2a89f7d2151f7647f11d8ca2ae279fb842d607217fce6e042f6815ea" +
"00" +
"0c85741de5c8da1144a6a1aba7f96de42505d7a7298524fda538fccb" +
"bb754f578c1cad10d54d0d5428407e85dcbc98a49155c13764e66c3c" +
"00",
)
.unwrap(),
);
let sig = hex::decode(
"d4f8f6131770dd46f40867d6fd5d5055de43541f8c5e35abbcd001b3".to_owned() +
"2a89f7d2151f7647f11d8ca2ae279fb842d607217fce6e042f6815ea" +
"00" +
"0c85741de5c8da1144a6a1aba7f96de42505d7a7298524fda538fccb" +
"bb754f578c1cad10d54d0d5428407e85dcbc98a49155c13764e66c3c" +
"00",
)
.unwrap();
#[allow(non_snake_case)]
let R = Ed448::read_G(&mut sig).unwrap();
let s = Ed448::read_F(&mut sig).unwrap();
let R = Ed448::read_G::<&[u8]>(&mut sig.as_ref()).unwrap();
let s = Ed448::read_F::<&[u8]>(&mut &sig[57 ..]).unwrap();
assert!(verify(
A,

View file

@ -1,4 +1,4 @@
use std::{io::Cursor, collections::HashMap};
use std::collections::HashMap;
use rand_core::{RngCore, CryptoRng};
@ -6,9 +6,9 @@ use group::ff::Field;
use crate::{
Curve, FrostParams, FrostCore, FrostKeys, lagrange,
key_gen::KeyGenMachine,
key_gen::{SecretShare, Commitments as KGCommitments, KeyGenMachine},
algorithm::Algorithm,
sign::{PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine},
sign::{Writable, PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine},
};
/// Curve tests.
@ -50,15 +50,32 @@ pub fn core_gen<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) -> HashMap<u16, F
);
let (machine, these_commitments) = machine.generate_coefficients(rng);
machines.insert(i, machine);
commitments.insert(i, Cursor::new(these_commitments));
commitments.insert(i, {
let mut buf = vec![];
these_commitments.write(&mut buf).unwrap();
KGCommitments::read::<&[u8]>(
&mut buf.as_ref(),
FrostParams { t: THRESHOLD, n: PARTICIPANTS, i: 1 },
)
.unwrap()
});
}
let mut secret_shares = HashMap::new();
let mut machines = machines
.drain()
.map(|(l, machine)| {
let (machine, shares) =
let (machine, mut shares) =
machine.generate_secret_shares(rng, clone_without(&commitments, &l)).unwrap();
let shares = shares
.drain()
.map(|(l, share)| {
let mut buf = vec![];
share.write(&mut buf).unwrap();
(l, SecretShare::<C>::read::<&[u8]>(&mut buf.as_ref()).unwrap())
})
.collect::<HashMap<_, _>>();
secret_shares.insert(l, shares);
(l, machine)
})
@ -74,7 +91,7 @@ pub fn core_gen<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) -> HashMap<u16, F
if i == *l {
continue;
}
our_secret_shares.insert(*l, Cursor::new(shares[&i].clone()));
our_secret_shares.insert(*l, shares[&i].clone());
}
let these_keys = machine.complete(rng, our_secret_shares).unwrap();
@ -154,7 +171,11 @@ pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
.drain()
.map(|(i, machine)| {
let (machine, preprocess) = machine.preprocess(rng);
commitments.insert(i, Cursor::new(preprocess));
commitments.insert(i, {
let mut buf = vec![];
preprocess.write(&mut buf).unwrap();
machine.read_preprocess::<&[u8]>(&mut buf.as_ref()).unwrap()
});
(i, machine)
})
.collect::<HashMap<_, _>>();
@ -164,7 +185,11 @@ pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
.drain()
.map(|(i, machine)| {
let (machine, share) = machine.sign(clone_without(&commitments, &i), msg).unwrap();
shares.insert(i, Cursor::new(share));
shares.insert(i, {
let mut buf = vec![];
share.write(&mut buf).unwrap();
machine.read_share::<&[u8]>(&mut buf.as_ref()).unwrap()
});
(i, machine)
})
.collect::<HashMap<_, _>>();

View file

@ -1,4 +1,4 @@
use std::{io::Cursor, collections::HashMap};
use std::collections::HashMap;
#[cfg(test)]
use std::str::FromStr;
@ -10,7 +10,10 @@ use crate::{
curve::Curve,
FrostCore, FrostKeys,
algorithm::{Schnorr, Hram},
sign::{PreprocessPackage, SignMachine, SignatureMachine, AlgorithmMachine},
sign::{
Nonce, GeneratorCommitments, NonceCommitments, Commitments, Writable, Preprocess,
PreprocessData, SignMachine, SignatureMachine, AlgorithmMachine,
},
tests::{
clone_without, curve::test_curve, schnorr::test_schnorr, promote::test_promotion, recover,
},
@ -78,12 +81,13 @@ fn vectors_to_multisig_keys<C: Curve>(vectors: &Vectors) -> HashMap<u16, FrostKe
let shares = vectors
.shares
.iter()
.map(|secret| C::read_F(&mut Cursor::new(hex::decode(secret).unwrap())).unwrap())
.map(|secret| C::read_F::<&[u8]>(&mut hex::decode(secret).unwrap().as_ref()).unwrap())
.collect::<Vec<_>>();
let verification_shares = shares.iter().map(|secret| C::generator() * secret).collect::<Vec<_>>();
let mut keys = HashMap::new();
for i in 1 ..= u16::try_from(shares.len()).unwrap() {
// Manually re-implement the serialization for FrostCore to import this data
let mut serialized = vec![];
serialized.extend(u32::try_from(C::ID.len()).unwrap().to_be_bytes());
serialized.extend(C::ID);
@ -95,7 +99,7 @@ fn vectors_to_multisig_keys<C: Curve>(vectors: &Vectors) -> HashMap<u16, FrostKe
serialized.extend(share.to_bytes().as_ref());
}
let these_keys = FrostCore::<C>::deserialize(&mut Cursor::new(serialized)).unwrap();
let these_keys = FrostCore::<C>::deserialize::<&[u8]>(&mut serialized.as_ref()).unwrap();
assert_eq!(these_keys.params().t(), vectors.threshold);
assert_eq!(usize::from(these_keys.params().n()), shares.len());
assert_eq!(these_keys.params().i(), i);
@ -118,8 +122,10 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
// Test against the vectors
let keys = vectors_to_multisig_keys::<C>(&vectors);
let group_key = C::read_G(&mut Cursor::new(hex::decode(&vectors.group_key).unwrap())).unwrap();
let secret = C::read_F(&mut Cursor::new(hex::decode(&vectors.group_secret).unwrap())).unwrap();
let group_key =
C::read_G::<&[u8]>(&mut hex::decode(&vectors.group_key).unwrap().as_ref()).unwrap();
let secret =
C::read_F::<&[u8]>(&mut hex::decode(&vectors.group_secret).unwrap().as_ref()).unwrap();
assert_eq!(C::generator() * secret, group_key);
assert_eq!(recover(&keys), secret);
@ -142,27 +148,36 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
.drain(..)
.map(|(i, machine)| {
let nonces = [
C::read_F(&mut Cursor::new(hex::decode(&vectors.nonces[c][0]).unwrap())).unwrap(),
C::read_F(&mut Cursor::new(hex::decode(&vectors.nonces[c][1]).unwrap())).unwrap(),
C::read_F::<&[u8]>(&mut hex::decode(&vectors.nonces[c][0]).unwrap().as_ref()).unwrap(),
C::read_F::<&[u8]>(&mut hex::decode(&vectors.nonces[c][1]).unwrap().as_ref()).unwrap(),
];
c += 1;
let these_commitments = vec![[C::generator() * nonces[0], C::generator() * nonces[1]]];
let machine = machine.unsafe_override_preprocess(PreprocessPackage {
nonces: vec![nonces],
commitments: vec![these_commitments.clone()],
addendum: vec![],
let these_commitments = [C::generator() * nonces[0], C::generator() * nonces[1]];
let machine = machine.unsafe_override_preprocess(PreprocessData {
nonces: vec![Nonce(nonces)],
preprocess: Preprocess {
commitments: Commitments {
nonces: vec![NonceCommitments {
generators: vec![GeneratorCommitments(these_commitments)],
dleqs: None,
}],
},
addendum: (),
},
});
commitments.insert(
*i,
Cursor::new(
[
these_commitments[0][0].to_bytes().as_ref(),
these_commitments[0][1].to_bytes().as_ref(),
]
.concat()
.to_vec(),
),
machine
.read_preprocess::<&[u8]>(
&mut [
these_commitments[0].to_bytes().as_ref(),
these_commitments[1].to_bytes().as_ref(),
]
.concat()
.as_ref(),
)
.unwrap(),
);
(i, machine)
})
@ -176,10 +191,15 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
let (machine, share) =
machine.sign(clone_without(&commitments, i), &hex::decode(&vectors.msg).unwrap()).unwrap();
let share = {
let mut buf = vec![];
share.write(&mut buf).unwrap();
buf
};
assert_eq!(share, hex::decode(&vectors.sig_shares[c]).unwrap());
c += 1;
shares.insert(*i, Cursor::new(share));
shares.insert(*i, machine.read_share::<&[u8]>(&mut share.as_ref()).unwrap());
(i, machine)
})
.collect::<HashMap<_, _>>();

View file

@ -1,4 +1,4 @@
use std::{marker::Send, io::Cursor, collections::HashMap};
use std::{marker::Send, collections::HashMap};
use async_trait::async_trait;
use thiserror::Error;
@ -18,7 +18,7 @@ pub enum NetworkError {}
#[async_trait]
pub trait Network: Send {
async fn round(&mut self, data: Vec<u8>) -> Result<HashMap<u16, Cursor<Vec<u8>>>, NetworkError>;
async fn round(&mut self, data: Vec<u8>) -> Result<HashMap<u16, Vec<u8>>, NetworkError>;
}
#[derive(Clone, Error, Debug)]

View file

@ -1,5 +1,4 @@
use std::{
io::Cursor,
sync::{Arc, RwLock},
collections::HashMap,
};
@ -19,7 +18,7 @@ struct LocalNetwork {
i: u16,
size: u16,
round: usize,
rounds: Arc<RwLock<Vec<HashMap<u16, Cursor<Vec<u8>>>>>>,
rounds: Arc<RwLock<Vec<HashMap<u16, Vec<u8>>>>>,
}
impl LocalNetwork {
@ -35,13 +34,13 @@ impl LocalNetwork {
#[async_trait]
impl Network for LocalNetwork {
async fn round(&mut self, data: Vec<u8>) -> Result<HashMap<u16, Cursor<Vec<u8>>>, NetworkError> {
async fn round(&mut self, data: Vec<u8>) -> Result<HashMap<u16, Vec<u8>>, NetworkError> {
{
let mut rounds = self.rounds.write().unwrap();
if rounds.len() == self.round {
rounds.push(HashMap::new());
}
rounds[self.round].insert(self.i, Cursor::new(data));
rounds[self.round].insert(self.i, data);
}
while {

View file

@ -7,8 +7,8 @@ use group::GroupEncoding;
use transcript::{Transcript, RecommendedTranscript};
use frost::{
curve::Curve,
FrostKeys,
sign::{PreprocessMachine, SignMachine, SignatureMachine},
FrostError, FrostKeys,
sign::{Writable, PreprocessMachine, SignMachine, SignatureMachine},
};
use crate::{
@ -343,10 +343,44 @@ impl<D: CoinDb, C: Coin> Wallet<D, C> {
self.coin.attempt_send(prepared, &included).await.map_err(SignError::CoinError)?;
let (attempt, commitments) = attempt.preprocess(&mut OsRng);
let commitments = network.round(commitments).await.map_err(SignError::NetworkError)?;
let commitments = network
.round({
let mut buf = vec![];
commitments.write(&mut buf).unwrap();
buf
})
.await
.map_err(SignError::NetworkError)?
.drain()
.map(|(validator, preprocess)| {
Ok((
validator,
attempt
.read_preprocess::<&[u8]>(&mut preprocess.as_ref())
.map_err(|_| SignError::FrostError(FrostError::InvalidPreprocess(validator)))?,
))
})
.collect::<Result<HashMap<_, _>, _>>()?;
let (attempt, share) = attempt.sign(commitments, b"").map_err(SignError::FrostError)?;
let shares = network.round(share).await.map_err(SignError::NetworkError)?;
let shares = network
.round({
let mut buf = vec![];
share.write(&mut buf).unwrap();
buf
})
.await
.map_err(SignError::NetworkError)?
.drain()
.map(|(validator, share)| {
Ok((
validator,
attempt
.read_share::<&[u8]>(&mut share.as_ref())
.map_err(|_| SignError::FrostError(FrostError::InvalidShare(validator)))?,
))
})
.collect::<Result<HashMap<_, _>, _>>()?;
let tx = attempt.complete(shares).map_err(SignError::FrostError)?;