mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-10 05:09:22 +00:00
Merge branch 'develop' into firo
This commit is contained in:
@@ -1,24 +1,46 @@
|
||||
[package]
|
||||
name = "frost"
|
||||
name = "modular-frost"
|
||||
version = "0.1.0"
|
||||
description = "Implementation of FROST over ff/group"
|
||||
description = "Modular implementation of FROST over ff/group"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["frost", "multisig", "threshold"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1"
|
||||
|
||||
rand_core = "0.6"
|
||||
hex = "0.4"
|
||||
|
||||
ff = "0.11"
|
||||
group = "0.11"
|
||||
sha2 = { version = "0.10", optional = true }
|
||||
|
||||
transcript = { path = "../transcript" }
|
||||
ff = "0.12"
|
||||
group = "0.12"
|
||||
|
||||
multiexp = { path = "../multiexp", features = ["batch"] }
|
||||
elliptic-curve = { version = "0.12", features = ["hash2curve"], optional = true }
|
||||
p256 = { version = "0.11", features = ["arithmetic", "bits", "hash2curve"], optional = true }
|
||||
k256 = { version = "0.11", features = ["arithmetic", "bits", "hash2curve"], optional = true }
|
||||
dalek-ff-group = { path = "../dalek-ff-group", version = "0.1", optional = true }
|
||||
|
||||
transcript = { package = "flexible-transcript", path = "../transcript", version = "0.1" }
|
||||
|
||||
multiexp = { path = "../multiexp", version = "0.1", features = ["batch"] }
|
||||
|
||||
dleq = { package = "dleq", path = "../dleq", version = "0.1", features = ["serialize"] }
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
|
||||
sha2 = "0.10"
|
||||
k256 = { version = "0.10", features = ["arithmetic"] }
|
||||
dalek-ff-group = { path = "../dalek-ff-group" }
|
||||
|
||||
[features]
|
||||
curves = ["sha2"] # All officially denoted curves use the SHA2 family of hashes
|
||||
kp256 = ["elliptic-curve", "curves"]
|
||||
p256 = ["kp256", "dep:p256"]
|
||||
secp256k1 = ["kp256", "k256"]
|
||||
dalek = ["curves", "dalek-ff-group"]
|
||||
ed25519 = ["dalek"]
|
||||
ristretto = ["dalek"]
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# FROST
|
||||
# Modular FROST
|
||||
|
||||
Implementation of FROST for any curve with a ff/group API.
|
||||
A modular implementation of FROST for any curve with a ff/group API. Notably,
|
||||
beyond curve modularity, custom algorithms may be specified, providing support
|
||||
for privacy coins. The provided Schnorr algorithm also has a modular HRAM due
|
||||
to the variety in existence, enabling integration with existing systems.
|
||||
|
||||
@@ -4,7 +4,7 @@ use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use transcript::Transcript;
|
||||
|
||||
use crate::{Curve, FrostError, MultisigView, schnorr};
|
||||
use crate::{Curve, FrostError, FrostView, schnorr};
|
||||
pub use schnorr::SchnorrSignature;
|
||||
|
||||
/// Algorithm to use FROST with
|
||||
@@ -13,22 +13,25 @@ pub trait Algorithm<C: Curve>: Clone {
|
||||
/// 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
|
||||
fn nonces(&self) -> Vec<Vec<C::G>>;
|
||||
|
||||
/// Generate an addendum to FROST"s preprocessing stage
|
||||
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
params: &MultisigView<C>,
|
||||
nonces: &[C::F; 2],
|
||||
params: &FrostView<C>,
|
||||
) -> Vec<u8>;
|
||||
|
||||
/// Proccess the addendum for the specified participant. Guaranteed to be ordered
|
||||
fn process_addendum(
|
||||
&mut self,
|
||||
params: &MultisigView<C>,
|
||||
params: &FrostView<C>,
|
||||
l: u16,
|
||||
commitments: &[C::G; 2],
|
||||
serialized: &[u8],
|
||||
) -> Result<(), FrostError>;
|
||||
|
||||
@@ -38,23 +41,24 @@ pub trait Algorithm<C: Curve>: Clone {
|
||||
/// The nonce will already have been processed into the combined form d + (e * p)
|
||||
fn sign_share(
|
||||
&mut self,
|
||||
params: &MultisigView<C>,
|
||||
nonce_sum: C::G,
|
||||
binding: C::F,
|
||||
nonce: C::F,
|
||||
params: &FrostView<C>,
|
||||
nonce_sums: &[Vec<C::G>],
|
||||
nonces: &[C::F],
|
||||
msg: &[u8],
|
||||
) -> C::F;
|
||||
|
||||
/// Verify a signature
|
||||
fn verify(&self, group_key: C::G, nonce: C::G, sum: C::F) -> Option<Self::Signature>;
|
||||
#[must_use]
|
||||
fn verify(&self, group_key: C::G, nonces: &[Vec<C::G>], sum: C::F) -> Option<Self::Signature>;
|
||||
|
||||
/// Verify a specific share given as a response. Used to determine blame if signature
|
||||
/// verification fails
|
||||
#[must_use]
|
||||
fn verify_share(
|
||||
&self,
|
||||
l: u16,
|
||||
verification_share: C::G,
|
||||
nonce: C::G,
|
||||
nonces: &[Vec<C::G>],
|
||||
share: C::F,
|
||||
) -> bool;
|
||||
}
|
||||
@@ -63,6 +67,12 @@ pub trait Algorithm<C: Curve>: Clone {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IetfTranscript(Vec<u8>);
|
||||
impl Transcript for IetfTranscript {
|
||||
type Challenge = Vec<u8>;
|
||||
|
||||
fn new(_: &'static [u8]) -> IetfTranscript {
|
||||
unimplemented!("IetfTranscript should not be used with multiple nonce protocols");
|
||||
}
|
||||
|
||||
fn domain_separate(&mut self, _: &[u8]) {}
|
||||
|
||||
fn append_message(&mut self, _: &'static [u8], message: &[u8]) {
|
||||
@@ -112,20 +122,22 @@ impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> {
|
||||
&mut self.transcript
|
||||
}
|
||||
|
||||
fn nonces(&self) -> Vec<Vec<C::G>> {
|
||||
vec![vec![C::GENERATOR]]
|
||||
}
|
||||
|
||||
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
_: &mut R,
|
||||
_: &MultisigView<C>,
|
||||
_: &[C::F; 2],
|
||||
_: &FrostView<C>,
|
||||
) -> Vec<u8> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn process_addendum(
|
||||
&mut self,
|
||||
_: &MultisigView<C>,
|
||||
_: &FrostView<C>,
|
||||
_: u16,
|
||||
_: &[C::G; 2],
|
||||
_: &[u8],
|
||||
) -> Result<(), FrostError> {
|
||||
Ok(())
|
||||
@@ -133,19 +145,19 @@ impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> {
|
||||
|
||||
fn sign_share(
|
||||
&mut self,
|
||||
params: &MultisigView<C>,
|
||||
nonce_sum: C::G,
|
||||
_: C::F,
|
||||
nonce: C::F,
|
||||
params: &FrostView<C>,
|
||||
nonce_sums: &[Vec<C::G>],
|
||||
nonces: &[C::F],
|
||||
msg: &[u8],
|
||||
) -> C::F {
|
||||
let c = H::hram(&nonce_sum, ¶ms.group_key(), msg);
|
||||
let c = H::hram(&nonce_sums[0][0], ¶ms.group_key(), msg);
|
||||
self.c = Some(c);
|
||||
schnorr::sign::<C>(params.secret_share(), nonce, c).s
|
||||
schnorr::sign::<C>(params.secret_share(), nonces[0], c).s
|
||||
}
|
||||
|
||||
fn verify(&self, group_key: C::G, nonce: C::G, sum: C::F) -> Option<Self::Signature> {
|
||||
let sig = SchnorrSignature { R: nonce, s: sum };
|
||||
#[must_use]
|
||||
fn verify(&self, group_key: C::G, nonces: &[Vec<C::G>], sum: C::F) -> Option<Self::Signature> {
|
||||
let sig = SchnorrSignature { R: nonces[0][0], s: sum };
|
||||
if schnorr::verify::<C>(group_key, self.c.unwrap(), &sig) {
|
||||
Some(sig)
|
||||
} else {
|
||||
@@ -153,17 +165,18 @@ impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn verify_share(
|
||||
&self,
|
||||
_: u16,
|
||||
verification_share: C::G,
|
||||
nonce: C::G,
|
||||
nonces: &[Vec<C::G>],
|
||||
share: C::F,
|
||||
) -> bool {
|
||||
schnorr::verify::<C>(
|
||||
verification_share,
|
||||
self.c.unwrap(),
|
||||
&SchnorrSignature { R: nonce, s: share}
|
||||
&SchnorrSignature { R: nonces[0][0], s: share}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
91
crypto/frost/src/curve/dalek.rs
Normal file
91
crypto/frost/src/curve/dalek.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use sha2::{Digest, Sha512};
|
||||
|
||||
use dalek_ff_group::Scalar;
|
||||
|
||||
use crate::{curve::Curve, algorithm::Hram};
|
||||
|
||||
macro_rules! dalek_curve {
|
||||
(
|
||||
$Curve: ident,
|
||||
$Hram: ident,
|
||||
$Point: ident,
|
||||
|
||||
$POINT: ident,
|
||||
|
||||
$ID: literal,
|
||||
$CONTEXT: literal,
|
||||
$chal: literal,
|
||||
$digest: literal,
|
||||
) => {
|
||||
use dalek_ff_group::{$Point, $POINT};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct $Curve;
|
||||
impl Curve for $Curve {
|
||||
type F = Scalar;
|
||||
type G = $Point;
|
||||
|
||||
const ID: &'static [u8] = $ID;
|
||||
const GENERATOR: Self::G = $POINT;
|
||||
|
||||
fn random_nonce<R: RngCore + CryptoRng>(secret: Self::F, rng: &mut R) -> Self::F {
|
||||
let mut seed = vec![0; 32];
|
||||
rng.fill_bytes(&mut seed);
|
||||
seed.extend(&secret.to_bytes());
|
||||
Self::hash_to_F(b"nonce", &seed)
|
||||
}
|
||||
|
||||
fn hash_msg(msg: &[u8]) -> Vec<u8> {
|
||||
Sha512::new()
|
||||
.chain_update($CONTEXT)
|
||||
.chain_update($digest)
|
||||
.chain_update(msg)
|
||||
.finalize()
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
fn hash_binding_factor(binding: &[u8]) -> Self::F {
|
||||
Self::hash_to_F(b"rho", binding)
|
||||
}
|
||||
|
||||
fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F {
|
||||
Scalar::from_hash(Sha512::new().chain_update($CONTEXT).chain_update(dst).chain_update(msg))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct $Hram;
|
||||
impl Hram<$Curve> for $Hram {
|
||||
#[allow(non_snake_case)]
|
||||
fn hram(R: &$Point, A: &$Point, m: &[u8]) -> Scalar {
|
||||
$Curve::hash_to_F($chal, &[&R.compress().to_bytes(), &A.compress().to_bytes(), m].concat())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "ristretto"))]
|
||||
dalek_curve!(
|
||||
Ristretto,
|
||||
IetfRistrettoHram,
|
||||
RistrettoPoint,
|
||||
RISTRETTO_BASEPOINT_POINT,
|
||||
b"ristretto",
|
||||
b"FROST-RISTRETTO255-SHA512-v5",
|
||||
b"chal",
|
||||
b"digest",
|
||||
);
|
||||
|
||||
#[cfg(feature = "ed25519")]
|
||||
dalek_curve!(
|
||||
Ed25519,
|
||||
IetfEd25519Hram,
|
||||
EdwardsPoint,
|
||||
ED25519_BASEPOINT_POINT,
|
||||
b"edwards25519",
|
||||
b"",
|
||||
b"",
|
||||
b"",
|
||||
);
|
||||
105
crypto/frost/src/curve/kp256.rs
Normal file
105
crypto/frost/src/curve/kp256.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use sha2::{digest::Update, Digest, Sha256};
|
||||
|
||||
use group::{ff::Field, GroupEncoding};
|
||||
|
||||
use elliptic_curve::{bigint::{Encoding, U384}, hash2curve::{Expander, ExpandMsg, ExpandMsgXmd}};
|
||||
|
||||
use crate::{curve::{Curve, F_from_slice}, algorithm::Hram};
|
||||
|
||||
macro_rules! kp_curve {
|
||||
(
|
||||
$lib: ident,
|
||||
$Curve: ident,
|
||||
$Hram: ident,
|
||||
|
||||
$ID: literal,
|
||||
$CONTEXT: literal
|
||||
) => {
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct $Curve;
|
||||
impl Curve for $Curve {
|
||||
type F = $lib::Scalar;
|
||||
type G = $lib::ProjectivePoint;
|
||||
|
||||
const ID: &'static [u8] = $ID;
|
||||
const GENERATOR: Self::G = $lib::ProjectivePoint::GENERATOR;
|
||||
|
||||
fn random_nonce<R: RngCore + CryptoRng>(secret: Self::F, rng: &mut R) -> Self::F {
|
||||
let mut seed = vec![0; 32];
|
||||
rng.fill_bytes(&mut seed);
|
||||
seed.extend(secret.to_bytes());
|
||||
Self::hash_to_F(&[$CONTEXT as &[u8], b"nonce"].concat(), &seed)
|
||||
}
|
||||
|
||||
fn hash_msg(msg: &[u8]) -> Vec<u8> {
|
||||
(&Sha256::new()
|
||||
.chain($CONTEXT)
|
||||
.chain(b"digest")
|
||||
.chain(msg)
|
||||
.finalize()
|
||||
).to_vec()
|
||||
}
|
||||
|
||||
fn hash_binding_factor(binding: &[u8]) -> Self::F {
|
||||
Self::hash_to_F(&[$CONTEXT as &[u8], b"rho"].concat(), binding)
|
||||
}
|
||||
|
||||
fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F {
|
||||
let mut dst = dst;
|
||||
let oversize = Sha256::digest([b"H2C-OVERSIZE-DST-", dst].concat());
|
||||
if dst.len() > 255 {
|
||||
dst = &oversize;
|
||||
}
|
||||
|
||||
// While one of these two libraries does support directly hashing to the Scalar field, the
|
||||
// other doesn't. While that's probably an oversight, this is a universally working method
|
||||
let mut modulus = vec![0; 16];
|
||||
modulus.extend((Self::F::zero() - Self::F::one()).to_bytes());
|
||||
let modulus = U384::from_be_slice(&modulus).wrapping_add(&U384::ONE);
|
||||
F_from_slice::<Self::F>(
|
||||
&U384::from_be_slice(&{
|
||||
let mut bytes = [0; 48];
|
||||
ExpandMsgXmd::<Sha256>::expand_message(
|
||||
&[msg],
|
||||
dst,
|
||||
48
|
||||
).unwrap().fill_bytes(&mut bytes);
|
||||
bytes
|
||||
}).reduce(&modulus).unwrap().to_be_bytes()[16 ..]
|
||||
).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct $Hram;
|
||||
impl Hram<$Curve> for $Hram {
|
||||
#[allow(non_snake_case)]
|
||||
fn hram(R: &$lib::ProjectivePoint, A: &$lib::ProjectivePoint, m: &[u8]) -> $lib::Scalar {
|
||||
$Curve::hash_to_F(
|
||||
&[$CONTEXT as &[u8], b"chal"].concat(),
|
||||
&[R.to_bytes().as_ref(), A.to_bytes().as_ref(), m].concat()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "p256")]
|
||||
kp_curve!(
|
||||
p256,
|
||||
P256,
|
||||
IetfP256Hram,
|
||||
b"P-256",
|
||||
b"FROST-P256-SHA256-v5"
|
||||
);
|
||||
|
||||
#[cfg(feature = "secp256k1")]
|
||||
kp_curve!(
|
||||
k256,
|
||||
Secp256k1,
|
||||
NonIetfSecp256k1Hram,
|
||||
b"secp256k1",
|
||||
b"FROST-secp256k1-SHA256-v5"
|
||||
);
|
||||
117
crypto/frost/src/curve/mod.rs
Normal file
117
crypto/frost/src/curve/mod.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use core::fmt::Debug;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ff::{PrimeField, PrimeFieldBits};
|
||||
use group::{Group, GroupOps, GroupEncoding, prime::PrimeGroup};
|
||||
|
||||
#[cfg(any(test, feature = "dalek"))]
|
||||
mod dalek;
|
||||
#[cfg(any(test, feature = "ristretto"))]
|
||||
pub use dalek::{Ristretto, IetfRistrettoHram};
|
||||
#[cfg(feature = "ed25519")]
|
||||
pub use dalek::{Ed25519, IetfEd25519Hram};
|
||||
|
||||
#[cfg(feature = "kp256")]
|
||||
mod kp256;
|
||||
#[cfg(feature = "secp256k1")]
|
||||
pub use kp256::{Secp256k1, NonIetfSecp256k1Hram};
|
||||
#[cfg(feature = "p256")]
|
||||
pub use kp256::{P256, IetfP256Hram};
|
||||
|
||||
/// Set of errors for curve-related operations, namely encoding and decoding
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum CurveError {
|
||||
#[error("invalid length for data (expected {0}, got {0})")]
|
||||
InvalidLength(usize, usize),
|
||||
#[error("invalid scalar")]
|
||||
InvalidScalar,
|
||||
#[error("invalid point")]
|
||||
InvalidPoint,
|
||||
}
|
||||
|
||||
/// Unified trait to manage a field/group
|
||||
// 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)
|
||||
// elliptic-curve exists, yet it doesn't really serve the same role, nor does it use &[u8]/Vec<u8>
|
||||
// It uses GenericArray which will hopefully be deprecated as Rust evolves and doesn't offer enough
|
||||
// advantages in the modern day to be worth the hassle -- Kayaba
|
||||
pub trait Curve: Clone + Copy + PartialEq + Eq + Debug {
|
||||
/// Scalar field element type
|
||||
// This is available via G::Scalar yet `C::G::Scalar` is ambiguous, forcing horrific accesses
|
||||
type F: PrimeField + PrimeFieldBits;
|
||||
/// Group element type
|
||||
type G: Group<Scalar = Self::F> + GroupOps + PrimeGroup;
|
||||
|
||||
/// ID for this curve
|
||||
const ID: &'static [u8];
|
||||
|
||||
/// Generator for the group
|
||||
// While group does provide this in its API, privacy coins may want to use a custom basepoint
|
||||
const GENERATOR: Self::G;
|
||||
|
||||
/// Securely generate a random nonce. H4 from the IETF draft
|
||||
fn random_nonce<R: RngCore + CryptoRng>(secret: Self::F, rng: &mut R) -> Self::F;
|
||||
|
||||
/// Hash the message for the binding factor. H3 from the IETF draft
|
||||
// This doesn't actually need to be part of Curve as it does nothing with the curve
|
||||
// This also solely relates to FROST and with a proper Algorithm/HRAM, all projects using
|
||||
// aggregatable signatures over this curve will work without issue
|
||||
// It is kept here as Curve + H{1, 2, 3} is effectively a ciphersuite according to the IETF draft
|
||||
// and moving it to Schnorr would force all of them into being ciphersuite-specific
|
||||
// H2 is left to the Schnorr Algorithm as H2 is the H used in HRAM, which Schnorr further
|
||||
// modularizes
|
||||
fn hash_msg(msg: &[u8]) -> Vec<u8>;
|
||||
|
||||
/// Hash the commitments and message to calculate the binding factor. H1 from the IETF draft
|
||||
fn hash_binding_factor(binding: &[u8]) -> Self::F;
|
||||
|
||||
// The following methods would optimally be F:: and G:: yet developers can't control F/G
|
||||
// They can control a trait they pass into this library
|
||||
|
||||
/// Field element from hash. Used during key gen and by other crates under Serai as a general
|
||||
/// utility
|
||||
// Not parameterized by Digest as it's fine for it to use its own hash function as relevant to
|
||||
// hash_msg and hash_binding_factor
|
||||
#[allow(non_snake_case)]
|
||||
fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn F_len<C: Curve>() -> usize {
|
||||
<C::F as PrimeField>::Repr::default().as_ref().len()
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn G_len<C: Curve>() -> usize {
|
||||
<C::G as GroupEncoding>::Repr::default().as_ref().len()
|
||||
}
|
||||
|
||||
/// Field element from slice
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn F_from_slice<F: PrimeField>(slice: &[u8]) -> Result<F, CurveError> {
|
||||
let mut encoding = F::Repr::default();
|
||||
encoding.as_mut().copy_from_slice(slice);
|
||||
|
||||
let point = Option::<F>::from(F::from_repr(encoding)).ok_or(CurveError::InvalidScalar)?;
|
||||
if point.to_repr().as_ref() != slice {
|
||||
Err(CurveError::InvalidScalar)?;
|
||||
}
|
||||
Ok(point)
|
||||
}
|
||||
|
||||
/// Group element from slice
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn G_from_slice<G: PrimeGroup>(slice: &[u8]) -> Result<G, CurveError> {
|
||||
let mut encoding = G::Repr::default();
|
||||
encoding.as_mut().copy_from_slice(slice);
|
||||
|
||||
let point = Option::<G>::from(G::from_bytes(&encoding)).ok_or(CurveError::InvalidPoint)?;
|
||||
// Ban the identity, per the FROST spec, and non-canonical points
|
||||
if (point.is_identity().into()) || (point.to_bytes().as_ref() != slice) {
|
||||
Err(CurveError::InvalidPoint)?;
|
||||
}
|
||||
Ok(point)
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
use core::fmt;
|
||||
use std::collections::HashMap;
|
||||
use std::{marker::PhantomData, collections::HashMap};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ff::{Field, PrimeField};
|
||||
use group::{ff::{Field, PrimeField}, GroupEncoding};
|
||||
|
||||
use multiexp::{multiexp_vartime, BatchVerifier};
|
||||
|
||||
use crate::{
|
||||
Curve, MultisigParams, MultisigKeys, FrostError,
|
||||
curve::{Curve, F_len, G_len, F_from_slice, G_from_slice},
|
||||
FrostError, FrostParams, FrostKeys,
|
||||
schnorr::{self, SchnorrSignature},
|
||||
validate_map
|
||||
};
|
||||
@@ -16,29 +16,34 @@ use crate::{
|
||||
#[allow(non_snake_case)]
|
||||
fn challenge<C: Curve>(context: &str, l: u16, R: &[u8], Am: &[u8]) -> C::F {
|
||||
const DST: &'static [u8] = b"FROST Schnorr Proof of Knowledge";
|
||||
|
||||
// Uses hash_msg to get a fixed size value out of the context string
|
||||
C::hash_to_F(&[DST, &C::hash_msg(context.as_bytes()), &l.to_be_bytes(), R, Am].concat())
|
||||
let mut transcript = C::hash_msg(context.as_bytes());
|
||||
transcript.extend(l.to_be_bytes());
|
||||
transcript.extend(R);
|
||||
transcript.extend(Am);
|
||||
C::hash_to_F(DST, &transcript)
|
||||
}
|
||||
|
||||
// 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
|
||||
fn generate_key_r1<R: RngCore + CryptoRng, C: Curve>(
|
||||
rng: &mut R,
|
||||
params: &MultisigParams,
|
||||
params: &FrostParams,
|
||||
context: &str,
|
||||
) -> (Vec<C::F>, Vec<u8>) {
|
||||
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((G_len::<C>() * t) + G_len::<C>() + F_len::<C>());
|
||||
|
||||
for i in 0 .. t {
|
||||
// Step 1: Generate t random values to form a polynomial with
|
||||
coefficients.push(C::F::random(&mut *rng));
|
||||
// Step 3: Generate public commitments
|
||||
commitments.push(C::generator_table() * coefficients[i]);
|
||||
commitments.push(C::GENERATOR * coefficients[i]);
|
||||
// Serialize them for publication
|
||||
serialized.extend(&C::G_to_bytes(&commitments[i]));
|
||||
serialized.extend(commitments[i].to_bytes().as_ref());
|
||||
}
|
||||
|
||||
// Step 2: Provide a proof of knowledge
|
||||
@@ -54,7 +59,7 @@ fn generate_key_r1<R: RngCore + CryptoRng, C: Curve>(
|
||||
challenge::<C>(
|
||||
context,
|
||||
params.i(),
|
||||
&C::G_to_bytes(&(C::generator_table() * r)),
|
||||
(C::GENERATOR * r).to_bytes().as_ref(),
|
||||
&serialized
|
||||
)
|
||||
).serialize()
|
||||
@@ -67,7 +72,7 @@ fn generate_key_r1<R: RngCore + CryptoRng, C: Curve>(
|
||||
// Verify the received data from the first round of key generation
|
||||
fn verify_r1<R: RngCore + CryptoRng, C: Curve>(
|
||||
rng: &mut R,
|
||||
params: &MultisigParams,
|
||||
params: &FrostParams,
|
||||
context: &str,
|
||||
our_commitments: Vec<u8>,
|
||||
mut serialized: HashMap<u16, Vec<u8>>,
|
||||
@@ -78,19 +83,19 @@ fn verify_r1<R: RngCore + CryptoRng, C: Curve>(
|
||||
(params.i(), our_commitments)
|
||||
)?;
|
||||
|
||||
let commitments_len = usize::from(params.t()) * C::G_len();
|
||||
let commitments_len = usize::from(params.t()) * G_len::<C>();
|
||||
|
||||
let mut commitments = HashMap::new();
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let R_bytes = |l| &serialized[&l][commitments_len .. commitments_len + C::G_len()];
|
||||
let R_bytes = |l| &serialized[&l][commitments_len .. commitments_len + G_len::<C>()];
|
||||
#[allow(non_snake_case)]
|
||||
let R = |l| C::G_from_slice(R_bytes(l)).map_err(|_| FrostError::InvalidProofOfKnowledge(l));
|
||||
let R = |l| G_from_slice::<C::G>(R_bytes(l)).map_err(|_| FrostError::InvalidProofOfKnowledge(l));
|
||||
#[allow(non_snake_case)]
|
||||
let Am = |l| &serialized[&l][0 .. commitments_len];
|
||||
|
||||
let s = |l| C::F_from_slice(
|
||||
&serialized[&l][commitments_len + C::G_len() ..]
|
||||
let s = |l| F_from_slice::<C::F>(
|
||||
&serialized[&l][commitments_len + G_len::<C>() ..]
|
||||
).map_err(|_| FrostError::InvalidProofOfKnowledge(l));
|
||||
|
||||
let mut signatures = Vec::with_capacity(usize::from(params.n() - 1));
|
||||
@@ -98,8 +103,8 @@ fn verify_r1<R: RngCore + CryptoRng, C: Curve>(
|
||||
let mut these_commitments = vec![];
|
||||
for c in 0 .. usize::from(params.t()) {
|
||||
these_commitments.push(
|
||||
C::G_from_slice(
|
||||
&serialized[&l][(c * C::G_len()) .. ((c + 1) * C::G_len())]
|
||||
G_from_slice::<C::G>(
|
||||
&serialized[&l][(c * G_len::<C>()) .. ((c + 1) * G_len::<C>())]
|
||||
).map_err(|_| FrostError::InvalidCommitment(l.try_into().unwrap()))?
|
||||
);
|
||||
}
|
||||
@@ -144,7 +149,7 @@ fn polynomial<F: PrimeField>(
|
||||
// counterparty to receive
|
||||
fn generate_key_r2<R: RngCore + CryptoRng, C: Curve>(
|
||||
rng: &mut R,
|
||||
params: &MultisigParams,
|
||||
params: &FrostParams,
|
||||
context: &str,
|
||||
coefficients: Vec<C::F>,
|
||||
our_commitments: Vec<u8>,
|
||||
@@ -161,7 +166,7 @@ fn generate_key_r2<R: RngCore + CryptoRng, C: Curve>(
|
||||
continue;
|
||||
}
|
||||
|
||||
res.insert(l, C::F_to_bytes(&polynomial(&coefficients, l)));
|
||||
res.insert(l, polynomial(&coefficients, l).to_repr().as_ref().to_vec());
|
||||
}
|
||||
|
||||
// Calculate our own share
|
||||
@@ -185,22 +190,22 @@ fn generate_key_r2<R: RngCore + CryptoRng, C: Curve>(
|
||||
/// broadcasted initially
|
||||
fn complete_r2<R: RngCore + CryptoRng, C: Curve>(
|
||||
rng: &mut R,
|
||||
params: MultisigParams,
|
||||
params: FrostParams,
|
||||
mut secret_share: C::F,
|
||||
commitments: HashMap<u16, Vec<C::G>>,
|
||||
// Vec to preserve ownership
|
||||
mut serialized: HashMap<u16, Vec<u8>>,
|
||||
) -> Result<MultisigKeys<C>, FrostError> {
|
||||
) -> Result<FrostKeys<C>, FrostError> {
|
||||
validate_map(
|
||||
&mut serialized,
|
||||
&(1 ..= params.n()).into_iter().collect::<Vec<_>>(),
|
||||
(params.i(), C::F_to_bytes(&secret_share))
|
||||
(params.i(), secret_share.to_repr().as_ref().to_vec())
|
||||
)?;
|
||||
|
||||
// Step 2. Verify each share
|
||||
let mut shares = HashMap::new();
|
||||
for (l, share) in serialized {
|
||||
shares.insert(l, C::F_from_slice(&share).map_err(|_| FrostError::InvalidShare(l))?);
|
||||
shares.insert(l, F_from_slice::<C::F>(&share).map_err(|_| FrostError::InvalidShare(l))?);
|
||||
}
|
||||
|
||||
// Calculate the exponent for a given participant and apply it to a series of commitments
|
||||
@@ -219,7 +224,7 @@ fn complete_r2<R: RngCore + CryptoRng, C: Curve>(
|
||||
res
|
||||
};
|
||||
|
||||
let mut batch = BatchVerifier::new(shares.len(), C::little_endian());
|
||||
let mut batch = BatchVerifier::new(shares.len());
|
||||
for (l, share) in &shares {
|
||||
if *l == params.i() {
|
||||
continue;
|
||||
@@ -232,7 +237,7 @@ fn complete_r2<R: RngCore + CryptoRng, C: Curve>(
|
||||
// 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()));
|
||||
values.push((-*share, C::GENERATOR));
|
||||
batch.queue(rng, *l, values);
|
||||
}
|
||||
batch.verify_with_vartime_blame().map_err(|l| FrostError::InvalidCommitment(l))?;
|
||||
@@ -249,14 +254,15 @@ fn complete_r2<R: RngCore + CryptoRng, C: Curve>(
|
||||
// Calculate each user's verification share
|
||||
let mut verification_shares = HashMap::new();
|
||||
for i in 1 ..= params.n() {
|
||||
verification_shares.insert(i, multiexp_vartime(exponential(i, &stripes), C::little_endian()));
|
||||
verification_shares.insert(i, multiexp_vartime(&exponential(i, &stripes)));
|
||||
}
|
||||
debug_assert_eq!(C::generator_table() * secret_share, verification_shares[¶ms.i()]);
|
||||
// Removing this check would enable optimizing the above from t + (n * t) to t + ((n - 1) * t)
|
||||
debug_assert_eq!(C::GENERATOR * secret_share, verification_shares[¶ms.i()]);
|
||||
|
||||
// TODO: Clear serialized and shares
|
||||
|
||||
Ok(
|
||||
MultisigKeys {
|
||||
FrostKeys {
|
||||
params,
|
||||
secret_share,
|
||||
group_key: stripes[0],
|
||||
@@ -266,100 +272,76 @@ fn complete_r2<R: RngCore + CryptoRng, C: Curve>(
|
||||
)
|
||||
}
|
||||
|
||||
/// State of a Key Generation machine
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum State {
|
||||
Fresh,
|
||||
GeneratedCoefficients,
|
||||
GeneratedSecretShares,
|
||||
Complete,
|
||||
}
|
||||
|
||||
impl fmt::Display for State {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// State machine which manages key generation
|
||||
#[allow(non_snake_case)]
|
||||
pub struct StateMachine<C: Curve> {
|
||||
params: MultisigParams,
|
||||
pub struct KeyGenMachine<C: Curve> {
|
||||
params: FrostParams,
|
||||
context: String,
|
||||
state: State,
|
||||
coefficients: Option<Vec<C::F>>,
|
||||
our_commitments: Option<Vec<u8>>,
|
||||
secret: Option<C::F>,
|
||||
commitments: Option<HashMap<u16, Vec<C::G>>>
|
||||
_curve: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<C: Curve> StateMachine<C> {
|
||||
pub struct SecretShareMachine<C: Curve> {
|
||||
params: FrostParams,
|
||||
context: String,
|
||||
coefficients: Vec<C::F>,
|
||||
our_commitments: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct KeyMachine<C: Curve> {
|
||||
params: FrostParams,
|
||||
secret: C::F,
|
||||
commitments: HashMap<u16, Vec<C::G>>,
|
||||
}
|
||||
|
||||
impl<C: Curve> KeyGenMachine<C> {
|
||||
/// Creates a new machine to generate a key for the specified curve in the specified multisig
|
||||
// The context string must be unique among multisigs
|
||||
pub fn new(params: MultisigParams, context: String) -> StateMachine<C> {
|
||||
StateMachine {
|
||||
params,
|
||||
context,
|
||||
state: State::Fresh,
|
||||
coefficients: None,
|
||||
our_commitments: None,
|
||||
secret: None,
|
||||
commitments: None
|
||||
}
|
||||
pub fn new(params: FrostParams, context: String) -> KeyGenMachine<C> {
|
||||
KeyGenMachine { params, context, _curve: PhantomData }
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// channel. If any party submits multiple sets of commitments, they MUST be treated as malicious
|
||||
pub fn generate_coefficients<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
self,
|
||||
rng: &mut R
|
||||
) -> Result<Vec<u8>, FrostError> {
|
||||
if self.state != State::Fresh {
|
||||
Err(FrostError::InvalidKeyGenTransition(State::Fresh, self.state))?;
|
||||
}
|
||||
|
||||
let (coefficients, serialized) = generate_key_r1::<R, C>(
|
||||
rng,
|
||||
&self.params,
|
||||
&self.context,
|
||||
);
|
||||
|
||||
self.coefficients = Some(coefficients);
|
||||
self.our_commitments = Some(serialized.clone());
|
||||
self.state = State::GeneratedCoefficients;
|
||||
Ok(serialized)
|
||||
) -> (SecretShareMachine<C>, Vec<u8>) {
|
||||
let (coefficients, serialized) = generate_key_r1::<R, C>(rng, &self.params, &self.context);
|
||||
(
|
||||
SecretShareMachine {
|
||||
params: self.params,
|
||||
context: self.context,
|
||||
coefficients,
|
||||
our_commitments: serialized.clone()
|
||||
},
|
||||
serialized,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Curve> SecretShareMachine<C> {
|
||||
/// Continue generating a key
|
||||
/// Takes in everyone else's commitments, which are expected to be in a Vec where participant
|
||||
/// index = Vec index. An empty vector is expected at index 0 to allow for this. An empty vector
|
||||
/// is also expected at index i which is locally handled. Returns a byte vector representing a
|
||||
/// secret share for each other participant which should be encrypted before sending
|
||||
pub fn generate_secret_shares<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
self,
|
||||
rng: &mut R,
|
||||
commitments: HashMap<u16, Vec<u8>>,
|
||||
) -> Result<HashMap<u16, Vec<u8>>, FrostError> {
|
||||
if self.state != State::GeneratedCoefficients {
|
||||
Err(FrostError::InvalidKeyGenTransition(State::GeneratedCoefficients, self.state))?;
|
||||
}
|
||||
|
||||
) -> Result<(KeyMachine<C>, HashMap<u16, Vec<u8>>), FrostError> {
|
||||
let (secret, commitments, shares) = generate_key_r2::<R, C>(
|
||||
rng,
|
||||
&self.params,
|
||||
&self.context,
|
||||
self.coefficients.take().unwrap(),
|
||||
self.our_commitments.take().unwrap(),
|
||||
self.coefficients,
|
||||
self.our_commitments,
|
||||
commitments,
|
||||
)?;
|
||||
|
||||
self.secret = Some(secret);
|
||||
self.commitments = Some(commitments);
|
||||
self.state = State::GeneratedSecretShares;
|
||||
Ok(shares)
|
||||
Ok((KeyMachine { params: self.params, secret, commitments }, shares))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Curve> KeyMachine<C> {
|
||||
/// Complete key generation
|
||||
/// Takes in everyone elses' shares submitted to us as a Vec, expecting participant index =
|
||||
/// Vec index with an empty vector at index 0 and index i. Returns a byte vector representing the
|
||||
@@ -367,31 +349,10 @@ impl<C: Curve> StateMachine<C> {
|
||||
/// must report completion without issue before this key can be considered usable, yet you should
|
||||
/// wait for all participants to report as such
|
||||
pub fn complete<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
self,
|
||||
rng: &mut R,
|
||||
shares: HashMap<u16, Vec<u8>>,
|
||||
) -> Result<MultisigKeys<C>, FrostError> {
|
||||
if self.state != State::GeneratedSecretShares {
|
||||
Err(FrostError::InvalidKeyGenTransition(State::GeneratedSecretShares, self.state))?;
|
||||
}
|
||||
|
||||
let keys = complete_r2(
|
||||
rng,
|
||||
self.params,
|
||||
self.secret.take().unwrap(),
|
||||
self.commitments.take().unwrap(),
|
||||
shares,
|
||||
)?;
|
||||
|
||||
self.state = State::Complete;
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
pub fn params(&self) -> MultisigParams {
|
||||
self.params.clone()
|
||||
}
|
||||
|
||||
pub fn state(&self) -> State {
|
||||
self.state
|
||||
) -> Result<FrostKeys<C>, FrostError> {
|
||||
complete_r2(rng, self.params, self.secret, self.commitments, shares)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,124 +1,24 @@
|
||||
use core::{ops::Mul, fmt::Debug};
|
||||
use core::fmt::Debug;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use ff::{Field, PrimeField};
|
||||
use group::{Group, GroupOps};
|
||||
use group::{ff::{Field, PrimeField}, GroupEncoding};
|
||||
|
||||
mod schnorr;
|
||||
|
||||
pub mod curve;
|
||||
use curve::{Curve, F_len, G_len, F_from_slice, G_from_slice};
|
||||
pub mod key_gen;
|
||||
pub mod algorithm;
|
||||
pub mod sign;
|
||||
|
||||
pub mod tests;
|
||||
|
||||
/// Set of errors for curve-related operations, namely encoding and decoding
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum CurveError {
|
||||
#[error("invalid length for data (expected {0}, got {0})")]
|
||||
InvalidLength(usize, usize),
|
||||
#[error("invalid scalar")]
|
||||
InvalidScalar,
|
||||
#[error("invalid point")]
|
||||
InvalidPoint,
|
||||
}
|
||||
|
||||
/// Unified trait to manage a field/group
|
||||
// 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)
|
||||
// elliptic-curve exists, yet it doesn't really serve the same role, nor does it use &[u8]/Vec<u8>
|
||||
// It uses GenericArray which will hopefully be deprecated as Rust evolves and doesn't offer enough
|
||||
// advantages in the modern day to be worth the hassle -- Kayaba
|
||||
pub trait Curve: Clone + Copy + PartialEq + Eq + Debug {
|
||||
/// Field element type
|
||||
// This is available via G::Scalar yet `C::G::Scalar` is ambiguous, forcing horrific accesses
|
||||
type F: PrimeField;
|
||||
/// Group element type
|
||||
type G: Group<Scalar = Self::F> + GroupOps;
|
||||
/// Precomputed table type
|
||||
type T: Mul<Self::F, Output = Self::G>;
|
||||
|
||||
/// ID for this curve
|
||||
fn id() -> String;
|
||||
/// Byte length of the curve ID
|
||||
// While curve.id().len() is trivial, this bounds it to u8 and lets us ignore the possibility it
|
||||
// contains Unicode, therefore having a String length which is different from its byte length
|
||||
fn id_len() -> u8;
|
||||
|
||||
/// Generator for the group
|
||||
// While group does provide this in its API, Jubjub users will want to use a custom basepoint
|
||||
fn generator() -> Self::G;
|
||||
|
||||
/// Table for the generator for the group
|
||||
/// If there isn't a precomputed table available, the generator itself should be used
|
||||
fn generator_table() -> Self::T;
|
||||
|
||||
/// If little endian is used for the scalar field's Repr
|
||||
fn little_endian() -> bool;
|
||||
|
||||
/// Hash the message for the binding factor. H3 from the IETF draft
|
||||
// This doesn't actually need to be part of Curve as it does nothing with the curve
|
||||
// This also solely relates to FROST and with a proper Algorithm/HRAM, all projects using
|
||||
// aggregatable signatures over this curve will work without issue
|
||||
// It is kept here as Curve + H{1, 2, 3} is effectively a ciphersuite according to the IETF draft
|
||||
// and moving it to Schnorr would force all of them into being ciphersuite-specific
|
||||
// H2 is left to the Schnorr Algorithm as H2 is the H used in HRAM, which Schnorr further
|
||||
// modularizes
|
||||
fn hash_msg(msg: &[u8]) -> Vec<u8>;
|
||||
|
||||
/// Hash the commitments and message to calculate the binding factor. H1 from the IETF draft
|
||||
fn hash_binding_factor(binding: &[u8]) -> Self::F;
|
||||
|
||||
// The following methods would optimally be F:: and G:: yet developers can't control F/G
|
||||
// They can control a trait they pass into this library
|
||||
|
||||
/// Field element from hash. Used during key gen and by other crates under Serai as a general
|
||||
/// utility
|
||||
// Not parameterized by Digest as it's fine for it to use its own hash function as relevant to
|
||||
// hash_msg and hash_binding_factor
|
||||
#[allow(non_snake_case)]
|
||||
fn hash_to_F(data: &[u8]) -> Self::F;
|
||||
|
||||
/// Constant size of a serialized field element
|
||||
// The alternative way to grab this would be either serializing a junk element and getting its
|
||||
// length or doing a naive division of its BITS property by 8 and assuming a lack of padding
|
||||
#[allow(non_snake_case)]
|
||||
fn F_len() -> usize;
|
||||
|
||||
/// Constant size of a serialized group element
|
||||
// We could grab the serialization as described above yet a naive developer may use a
|
||||
// non-constant size encoding, proving yet another reason to force this to be a provided constant
|
||||
// A naive developer could still provide a constant for a variable length encoding, yet at least
|
||||
// that is on them
|
||||
#[allow(non_snake_case)]
|
||||
fn G_len() -> usize;
|
||||
|
||||
/// 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_slice(slice: &[u8]) -> Result<Self::F, CurveError>;
|
||||
|
||||
/// 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>;
|
||||
|
||||
/// Obtain a vector of the byte encoding of F
|
||||
#[allow(non_snake_case)]
|
||||
fn F_to_bytes(f: &Self::F) -> Vec<u8>;
|
||||
|
||||
/// Obtain a vector of the byte encoding of G
|
||||
#[allow(non_snake_case)]
|
||||
fn G_to_bytes(g: &Self::G) -> Vec<u8>;
|
||||
}
|
||||
|
||||
/// Parameters for a multisig
|
||||
// These fields can not be made public as they should be static
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct MultisigParams {
|
||||
pub struct FrostParams {
|
||||
/// Participants needed to sign on behalf of the group
|
||||
t: u16,
|
||||
/// Amount of participants
|
||||
@@ -127,12 +27,12 @@ pub struct MultisigParams {
|
||||
i: u16,
|
||||
}
|
||||
|
||||
impl MultisigParams {
|
||||
impl FrostParams {
|
||||
pub fn new(
|
||||
t: u16,
|
||||
n: u16,
|
||||
i: u16
|
||||
) -> Result<MultisigParams, FrostError> {
|
||||
) -> Result<FrostParams, FrostError> {
|
||||
if (t == 0) || (n == 0) {
|
||||
Err(FrostError::ZeroParameter(t, n))?;
|
||||
}
|
||||
@@ -146,7 +46,7 @@ impl MultisigParams {
|
||||
Err(FrostError::InvalidParticipantIndex(n, i))?;
|
||||
}
|
||||
|
||||
Ok(MultisigParams{ t, n, i })
|
||||
Ok(FrostParams{ t, n, i })
|
||||
}
|
||||
|
||||
pub fn t(&self) -> u16 { self.t }
|
||||
@@ -179,11 +79,6 @@ pub enum FrostError {
|
||||
InvalidProofOfKnowledge(u16),
|
||||
#[error("invalid share (participant {0})")]
|
||||
InvalidShare(u16),
|
||||
#[error("invalid key generation state machine transition (expected {0}, was {1})")]
|
||||
InvalidKeyGenTransition(key_gen::State, key_gen::State),
|
||||
|
||||
#[error("invalid sign state machine transition (expected {0}, was {1})")]
|
||||
InvalidSignTransition(sign::State, sign::State),
|
||||
|
||||
#[error("internal error ({0})")]
|
||||
InternalError(String),
|
||||
@@ -191,14 +86,14 @@ pub enum FrostError {
|
||||
|
||||
// View of keys passable to algorithm implementations
|
||||
#[derive(Clone)]
|
||||
pub struct MultisigView<C: Curve> {
|
||||
pub struct FrostView<C: Curve> {
|
||||
group_key: C::G,
|
||||
included: Vec<u16>,
|
||||
secret_share: C::F,
|
||||
verification_shares: HashMap<u16, C::G>,
|
||||
}
|
||||
|
||||
impl<C: Curve> MultisigView<C> {
|
||||
impl<C: Curve> FrostView<C> {
|
||||
pub fn group_key(&self) -> C::G {
|
||||
self.group_key
|
||||
}
|
||||
@@ -239,9 +134,9 @@ pub fn lagrange<F: PrimeField>(
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct MultisigKeys<C: Curve> {
|
||||
/// Multisig Parameters
|
||||
params: MultisigParams,
|
||||
pub struct FrostKeys<C: Curve> {
|
||||
/// FROST Parameters
|
||||
params: FrostParams,
|
||||
|
||||
/// Secret share key
|
||||
secret_share: C::F,
|
||||
@@ -254,21 +149,26 @@ pub struct MultisigKeys<C: Curve> {
|
||||
offset: Option<C::F>,
|
||||
}
|
||||
|
||||
impl<C: Curve> MultisigKeys<C> {
|
||||
pub fn offset(&self, offset: C::F) -> MultisigKeys<C> {
|
||||
impl<C: Curve> FrostKeys<C> {
|
||||
/// Offset the keys by a given scalar to allow for account and privacy schemes
|
||||
/// This offset is ephemeral and will not be included when these keys are serialized
|
||||
/// Keys offset multiple times will form a new offset of their sum
|
||||
/// Not IETF compliant
|
||||
pub fn offset(&self, offset: C::F) -> FrostKeys<C> {
|
||||
let mut res = self.clone();
|
||||
// Carry any existing offset
|
||||
// Enables schemes like Monero's subaddresses which have a per-subaddress offset and then a
|
||||
// one-time-key offset
|
||||
res.offset = Some(offset + res.offset.unwrap_or(C::F::zero()));
|
||||
res.group_key += C::GENERATOR * offset;
|
||||
res
|
||||
}
|
||||
|
||||
pub fn params(&self) -> MultisigParams {
|
||||
pub fn params(&self) -> FrostParams {
|
||||
self.params
|
||||
}
|
||||
|
||||
pub fn secret_share(&self) -> C::F {
|
||||
fn secret_share(&self) -> C::F {
|
||||
self.secret_share
|
||||
}
|
||||
|
||||
@@ -276,11 +176,11 @@ impl<C: Curve> MultisigKeys<C> {
|
||||
self.group_key
|
||||
}
|
||||
|
||||
pub fn verification_shares(&self) -> HashMap<u16, C::G> {
|
||||
fn verification_shares(&self) -> HashMap<u16, C::G> {
|
||||
self.verification_shares.clone()
|
||||
}
|
||||
|
||||
pub fn view(&self, included: &[u16]) -> Result<MultisigView<C>, FrostError> {
|
||||
pub fn view(&self, included: &[u16]) -> Result<FrostView<C>, FrostError> {
|
||||
if (included.len() < self.params.t.into()) || (usize::from(self.params.n) < included.len()) {
|
||||
Err(FrostError::InvalidSigningSet("invalid amount of participants included".to_string()))?;
|
||||
}
|
||||
@@ -289,13 +189,13 @@ impl<C: Curve> MultisigKeys<C> {
|
||||
let offset = self.offset.unwrap_or(C::F::zero());
|
||||
let offset_share = offset * C::F::from(included.len().try_into().unwrap()).invert().unwrap();
|
||||
|
||||
Ok(MultisigView {
|
||||
group_key: self.group_key + (C::generator_table() * offset),
|
||||
Ok(FrostView {
|
||||
group_key: self.group_key,
|
||||
secret_share: secret_share + offset_share,
|
||||
verification_shares: self.verification_shares.iter().map(
|
||||
|(l, share)| (
|
||||
*l,
|
||||
(*share * lagrange::<C::F>(*l, &included)) + (C::generator_table() * offset_share)
|
||||
(*share * lagrange::<C::F>(*l, &included)) + (C::GENERATOR * offset_share)
|
||||
)
|
||||
).collect(),
|
||||
included: included.to_vec(),
|
||||
@@ -303,84 +203,76 @@ impl<C: Curve> MultisigKeys<C> {
|
||||
}
|
||||
|
||||
pub fn serialized_len(n: u16) -> usize {
|
||||
1 + usize::from(C::id_len()) + (3 * 2) + C::F_len() + C::G_len() + (usize::from(n) * C::G_len())
|
||||
8 + C::ID.len() + (3 * 2) + F_len::<C>() + G_len::<C>() + (usize::from(n) * G_len::<C>())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut serialized = Vec::with_capacity(
|
||||
1 + usize::from(C::id_len()) + MultisigKeys::<C>::serialized_len(self.params.n)
|
||||
);
|
||||
serialized.push(C::id_len());
|
||||
serialized.extend(C::id().as_bytes());
|
||||
serialized.extend(&self.params.n.to_le_bytes());
|
||||
serialized.extend(&self.params.t.to_le_bytes());
|
||||
serialized.extend(&self.params.i.to_le_bytes());
|
||||
serialized.extend(&C::F_to_bytes(&self.secret_share));
|
||||
serialized.extend(&C::G_to_bytes(&self.group_key));
|
||||
let mut serialized = Vec::with_capacity(FrostKeys::<C>::serialized_len(self.params.n));
|
||||
serialized.extend(u64::try_from(C::ID.len()).unwrap().to_be_bytes());
|
||||
serialized.extend(C::ID);
|
||||
serialized.extend(&self.params.t.to_be_bytes());
|
||||
serialized.extend(&self.params.n.to_be_bytes());
|
||||
serialized.extend(&self.params.i.to_be_bytes());
|
||||
serialized.extend(self.secret_share.to_repr().as_ref());
|
||||
serialized.extend(self.group_key.to_bytes().as_ref());
|
||||
for l in 1 ..= self.params.n.into() {
|
||||
serialized.extend(&C::G_to_bytes(&self.verification_shares[&l]));
|
||||
serialized.extend(self.verification_shares[&l].to_bytes().as_ref());
|
||||
}
|
||||
|
||||
serialized
|
||||
}
|
||||
|
||||
pub fn deserialize(serialized: &[u8]) -> Result<MultisigKeys<C>, FrostError> {
|
||||
if serialized.len() < 1 {
|
||||
Err(FrostError::InternalError("MultisigKeys serialization is empty".to_string()))?;
|
||||
pub fn deserialize(serialized: &[u8]) -> Result<FrostKeys<C>, FrostError> {
|
||||
let mut start = u64::try_from(C::ID.len()).unwrap().to_be_bytes().to_vec();
|
||||
start.extend(C::ID);
|
||||
let mut cursor = start.len();
|
||||
|
||||
if serialized.len() < (cursor + 4) {
|
||||
Err(
|
||||
FrostError::InternalError(
|
||||
"FrostKeys serialization is missing its curve/participant quantities".to_string()
|
||||
)
|
||||
)?;
|
||||
}
|
||||
|
||||
let id_len: usize = serialized[0].into();
|
||||
let mut cursor = 1;
|
||||
|
||||
if serialized.len() < (cursor + id_len) {
|
||||
Err(FrostError::InternalError("ID wasn't included".to_string()))?;
|
||||
}
|
||||
|
||||
let id = &serialized[cursor .. (cursor + id_len)];
|
||||
if C::id().as_bytes() != id {
|
||||
if &start != &serialized[.. cursor] {
|
||||
Err(
|
||||
FrostError::InternalError(
|
||||
"curve is distinct between serialization and deserialization".to_string()
|
||||
)
|
||||
)?;
|
||||
}
|
||||
cursor += id_len;
|
||||
|
||||
if serialized.len() < (cursor + 8) {
|
||||
Err(FrostError::InternalError("participant quantity wasn't included".to_string()))?;
|
||||
}
|
||||
|
||||
let n = u16::from_le_bytes(serialized[cursor .. (cursor + 2)].try_into().unwrap());
|
||||
let t = u16::from_be_bytes(serialized[cursor .. (cursor + 2)].try_into().unwrap());
|
||||
cursor += 2;
|
||||
if serialized.len() != MultisigKeys::<C>::serialized_len(n) {
|
||||
|
||||
let n = u16::from_be_bytes(serialized[cursor .. (cursor + 2)].try_into().unwrap());
|
||||
cursor += 2;
|
||||
if serialized.len() != FrostKeys::<C>::serialized_len(n) {
|
||||
Err(FrostError::InternalError("incorrect serialization length".to_string()))?;
|
||||
}
|
||||
|
||||
let t = u16::from_le_bytes(serialized[cursor .. (cursor + 2)].try_into().unwrap());
|
||||
cursor += 2;
|
||||
let i = u16::from_le_bytes(serialized[cursor .. (cursor + 2)].try_into().unwrap());
|
||||
let i = u16::from_be_bytes(serialized[cursor .. (cursor + 2)].try_into().unwrap());
|
||||
cursor += 2;
|
||||
|
||||
let secret_share = C::F_from_slice(&serialized[cursor .. (cursor + C::F_len())])
|
||||
let secret_share = F_from_slice::<C::F>(&serialized[cursor .. (cursor + F_len::<C>())])
|
||||
.map_err(|_| FrostError::InternalError("invalid secret share".to_string()))?;
|
||||
cursor += C::F_len();
|
||||
let group_key = C::G_from_slice(&serialized[cursor .. (cursor + C::G_len())])
|
||||
cursor += F_len::<C>();
|
||||
let group_key = G_from_slice::<C::G>(&serialized[cursor .. (cursor + G_len::<C>())])
|
||||
.map_err(|_| FrostError::InternalError("invalid group key".to_string()))?;
|
||||
cursor += C::G_len();
|
||||
cursor += G_len::<C>();
|
||||
|
||||
let mut verification_shares = HashMap::new();
|
||||
for l in 1 ..= n {
|
||||
verification_shares.insert(
|
||||
l,
|
||||
C::G_from_slice(&serialized[cursor .. (cursor + C::G_len())])
|
||||
G_from_slice::<C::G>(&serialized[cursor .. (cursor + G_len::<C>())])
|
||||
.map_err(|_| FrostError::InternalError("invalid verification share".to_string()))?
|
||||
);
|
||||
cursor += C::G_len();
|
||||
cursor += G_len::<C>();
|
||||
}
|
||||
|
||||
Ok(
|
||||
MultisigKeys {
|
||||
params: MultisigParams::new(t, n, i)
|
||||
FrostKeys {
|
||||
params: FrostParams::new(t, n, i)
|
||||
.map_err(|_| FrostError::InternalError("invalid parameters".to_string()))?,
|
||||
secret_share,
|
||||
group_key,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ff::Field;
|
||||
use group::{ff::{Field, PrimeField}, GroupEncoding};
|
||||
|
||||
use multiexp::BatchVerifier;
|
||||
|
||||
use crate::Curve;
|
||||
use crate::{Curve, F_len, G_len};
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
@@ -15,9 +15,9 @@ 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(C::G_to_bytes(&self.R));
|
||||
res.extend(C::F_to_bytes(&self.s));
|
||||
let mut res = Vec::with_capacity(G_len::<C>() + F_len::<C>());
|
||||
res.extend(self.R.to_bytes().as_ref());
|
||||
res.extend(self.s.to_repr().as_ref());
|
||||
res
|
||||
}
|
||||
}
|
||||
@@ -28,25 +28,26 @@ pub(crate) fn sign<C: Curve>(
|
||||
challenge: C::F
|
||||
) -> SchnorrSignature<C> {
|
||||
SchnorrSignature {
|
||||
R: C::generator_table() * nonce,
|
||||
R: C::GENERATOR * nonce,
|
||||
s: nonce + (private_key * challenge)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn verify<C: Curve>(
|
||||
public_key: C::G,
|
||||
challenge: C::F,
|
||||
signature: &SchnorrSignature<C>
|
||||
) -> bool {
|
||||
(C::generator_table() * signature.s) == (signature.R + (public_key * challenge))
|
||||
(C::GENERATOR * signature.s) == (signature.R + (public_key * challenge))
|
||||
}
|
||||
|
||||
pub(crate) fn batch_verify<C: Curve, R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
triplets: &[(u16, C::G, C::F, SchnorrSignature<C>)]
|
||||
) -> Result<(), u16> {
|
||||
let mut values = [(C::F::one(), C::generator()); 3];
|
||||
let mut batch = BatchVerifier::new(triplets.len(), C::little_endian());
|
||||
let mut values = [(C::F::one(), C::GENERATOR); 3];
|
||||
let mut batch = BatchVerifier::new(triplets.len());
|
||||
for triple in triplets {
|
||||
// s = r + ca
|
||||
// sG == R + cA
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
use core::fmt;
|
||||
use std::{rc::Rc, collections::HashMap};
|
||||
use std::{sync::Arc, collections::HashMap};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ff::Field;
|
||||
use group::{ff::{Field, PrimeField}, Group, GroupEncoding};
|
||||
|
||||
use transcript::Transcript;
|
||||
|
||||
use dleq::{Generators, DLEqProof};
|
||||
|
||||
use crate::{
|
||||
Curve,
|
||||
curve::{Curve, F_len, G_len, F_from_slice, G_from_slice},
|
||||
FrostError,
|
||||
MultisigParams, MultisigKeys, MultisigView,
|
||||
FrostParams, FrostKeys, FrostView,
|
||||
algorithm::Algorithm,
|
||||
validate_map
|
||||
};
|
||||
|
||||
/// Pairing of an Algorithm with a MultisigKeys instance and this specific signing set
|
||||
/// Pairing of an Algorithm with a FrostKeys instance and this specific signing set
|
||||
#[derive(Clone)]
|
||||
pub struct Params<C: Curve, A: Algorithm<C>> {
|
||||
algorithm: A,
|
||||
keys: Rc<MultisigKeys<C>>,
|
||||
view: MultisigView<C>,
|
||||
keys: Arc<FrostKeys<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,
|
||||
keys: Rc<MultisigKeys<C>>,
|
||||
keys: Arc<FrostKeys<C>>,
|
||||
included: &[u16],
|
||||
) -> Result<Params<C, A>, FrostError> {
|
||||
let mut included = included.to_vec();
|
||||
@@ -60,18 +62,22 @@ impl<C: Curve, A: Algorithm<C>> Params<C, A> {
|
||||
Ok(Params { algorithm, view: keys.view(&included).unwrap(), keys })
|
||||
}
|
||||
|
||||
pub fn multisig_params(&self) -> MultisigParams {
|
||||
pub fn multisig_params(&self) -> FrostParams {
|
||||
self.keys.params
|
||||
}
|
||||
|
||||
pub fn view(&self) -> MultisigView<C> {
|
||||
pub fn view(&self) -> FrostView<C> {
|
||||
self.view.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct PreprocessPackage<C: Curve> {
|
||||
nonces: [C::F; 2],
|
||||
serialized: Vec<u8>,
|
||||
fn nonce_transcript<T: Transcript>() -> T {
|
||||
T::new(b"FROST_nonce_dleq")
|
||||
}
|
||||
|
||||
pub(crate) struct PreprocessPackage<C: Curve> {
|
||||
pub(crate) nonces: Vec<[C::F; 2]>,
|
||||
pub(crate) serialized: Vec<u8>,
|
||||
}
|
||||
|
||||
// This library unifies the preprocessing step with signing due to security concerns and to provide
|
||||
@@ -80,27 +86,53 @@ fn preprocess<R: RngCore + CryptoRng, C: Curve, A: Algorithm<C>>(
|
||||
rng: &mut R,
|
||||
params: &mut Params<C, A>,
|
||||
) -> PreprocessPackage<C> {
|
||||
let nonces = [C::F::random(&mut *rng), C::F::random(&mut *rng)];
|
||||
let commitments = [C::generator_table() * nonces[0], C::generator_table() * nonces[1]];
|
||||
let mut serialized = C::G_to_bytes(&commitments[0]);
|
||||
serialized.extend(&C::G_to_bytes(&commitments[1]));
|
||||
let mut serialized = Vec::with_capacity(2 * G_len::<C>());
|
||||
let nonces = params.algorithm.nonces().iter().cloned().map(
|
||||
|mut generators| {
|
||||
let nonces = [
|
||||
C::random_nonce(params.view().secret_share(), &mut *rng),
|
||||
C::random_nonce(params.view().secret_share(), &mut *rng)
|
||||
];
|
||||
|
||||
serialized.extend(
|
||||
¶ms.algorithm.preprocess_addendum(
|
||||
rng,
|
||||
¶ms.view,
|
||||
&nonces
|
||||
)
|
||||
);
|
||||
let commit = |generator: C::G| {
|
||||
let commitments = [generator * nonces[0], generator * nonces[1]];
|
||||
[commitments[0].to_bytes().as_ref(), commitments[1].to_bytes().as_ref()].concat().to_vec()
|
||||
};
|
||||
|
||||
let first = generators.remove(0);
|
||||
serialized.extend(commit(first));
|
||||
|
||||
// Iterate over the rest
|
||||
for generator in generators.iter() {
|
||||
serialized.extend(commit(*generator));
|
||||
// Provide a DLEq to verify these commitments are for the same nonce
|
||||
// TODO: Provide a single DLEq. See https://github.com/serai-dex/serai/issues/34
|
||||
for nonce in nonces {
|
||||
DLEqProof::prove(
|
||||
&mut *rng,
|
||||
// Uses an independent transcript as each signer must do this now, yet we validate them
|
||||
// sequentially by the global order. Avoids needing to clone the transcript around
|
||||
&mut nonce_transcript::<A::Transcript>(),
|
||||
Generators::new(first, *generator),
|
||||
nonce
|
||||
).serialize(&mut serialized).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
nonces
|
||||
}
|
||||
).collect::<Vec<_>>();
|
||||
|
||||
serialized.extend(¶ms.algorithm.preprocess_addendum(rng, ¶ms.view));
|
||||
|
||||
PreprocessPackage { nonces, serialized }
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
struct Package<C: Curve> {
|
||||
B: HashMap<u16, [C::G; 2]>,
|
||||
B: HashMap<u16, Vec<Vec<[C::G; 2]>>>,
|
||||
binding: C::F,
|
||||
R: C::G,
|
||||
Rs: Vec<Vec<C::G>>,
|
||||
share: Vec<u8>
|
||||
}
|
||||
|
||||
@@ -126,7 +158,7 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
|
||||
transcript.domain_separate(b"FROST");
|
||||
// Include the offset, if one exists
|
||||
if let Some(offset) = params.keys.offset {
|
||||
transcript.append_message(b"offset", &C::F_to_bytes(&offset));
|
||||
transcript.append_message(b"offset", offset.to_repr().as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,61 +166,98 @@ fn sign_with_share<C: Curve, A: Algorithm<C>>(
|
||||
let mut B = HashMap::<u16, _>::with_capacity(params.view.included.len());
|
||||
|
||||
// Get the binding factor
|
||||
let nonces = params.algorithm.nonces();
|
||||
let mut addendums = HashMap::new();
|
||||
let binding = {
|
||||
let transcript = params.algorithm.transcript();
|
||||
// Parse the commitments
|
||||
for l in ¶ms.view.included {
|
||||
transcript.append_message(b"participant", &l.to_be_bytes());
|
||||
let serialized = commitments.remove(l).unwrap();
|
||||
|
||||
let commitments = commitments.remove(l).unwrap();
|
||||
let mut read_commitment = |c, label| {
|
||||
let commitment = &commitments[c .. (c + C::G_len())];
|
||||
let commitment = &serialized[c .. (c + G_len::<C>())];
|
||||
transcript.append_message(label, commitment);
|
||||
C::G_from_slice(commitment).map_err(|_| FrostError::InvalidCommitment(*l))
|
||||
G_from_slice::<C::G>(commitment).map_err(|_| FrostError::InvalidCommitment(*l))
|
||||
};
|
||||
|
||||
// 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
|
||||
#[allow(non_snake_case)]
|
||||
let mut read_D_E = || Ok(
|
||||
[read_commitment(0, b"commitment_D")?, read_commitment(C::G_len(), b"commitment_E")?]
|
||||
);
|
||||
let mut read_D_E = |c| Ok([
|
||||
read_commitment(c, b"commitment_D")?,
|
||||
read_commitment(c + G_len::<C>(), b"commitment_E")?
|
||||
]);
|
||||
|
||||
B.insert(*l, read_D_E()?);
|
||||
addendums.insert(*l, commitments[(C::G_len() * 2) ..].to_vec());
|
||||
let mut c = 0;
|
||||
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()));
|
||||
|
||||
let first = nonce_generators.remove(0);
|
||||
commitments[n].push(read_D_E(c)?);
|
||||
c += 2 * G_len::<C>();
|
||||
|
||||
let mut c = 2 * G_len::<C>();
|
||||
for generator in nonce_generators {
|
||||
commitments[n].push(read_D_E(c)?);
|
||||
c += 2 * G_len::<C>();
|
||||
for de in 0 .. 2 {
|
||||
DLEqProof::deserialize(
|
||||
&mut std::io::Cursor::new(&serialized[c .. (c + (2 * F_len::<C>()))])
|
||||
).map_err(|_| FrostError::InvalidCommitment(*l))?.verify(
|
||||
&mut nonce_transcript::<A::Transcript>(),
|
||||
Generators::new(first, *generator),
|
||||
(commitments[n][0][de], commitments[n][commitments[n].len() - 1][de])
|
||||
).map_err(|_| FrostError::InvalidCommitment(*l))?;
|
||||
c += 2 * F_len::<C>();
|
||||
}
|
||||
}
|
||||
|
||||
addendums.insert(*l, serialized[c ..].to_vec());
|
||||
}
|
||||
B.insert(*l, commitments);
|
||||
}
|
||||
|
||||
// Append the message to the transcript
|
||||
transcript.append_message(b"message", &C::hash_msg(&msg));
|
||||
|
||||
// Calculate the binding factor
|
||||
C::hash_binding_factor(&transcript.challenge(b"binding"))
|
||||
C::hash_binding_factor(transcript.challenge(b"binding").as_ref())
|
||||
};
|
||||
|
||||
// Process the addendums
|
||||
for l in ¶ms.view.included {
|
||||
params.algorithm.process_addendum(¶ms.view, *l, &B[l], &addendums[l])?;
|
||||
params.algorithm.process_addendum(¶ms.view, *l, &addendums[l])?;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let R = {
|
||||
B.values().map(|B| B[0]).sum::<C::G>() + (B.values().map(|B| B[1]).sum::<C::G>() * binding)
|
||||
};
|
||||
let share = C::F_to_bytes(
|
||||
¶ms.algorithm.sign_share(
|
||||
¶ms.view,
|
||||
R,
|
||||
binding,
|
||||
our_preprocess.nonces[0] + (our_preprocess.nonces[1] * binding),
|
||||
msg
|
||||
)
|
||||
);
|
||||
let mut Rs = Vec::with_capacity(nonces.len());
|
||||
for n in 0 .. nonces.len() {
|
||||
Rs.push(vec![C::G::identity(); nonces[n].len()]);
|
||||
#[allow(non_snake_case)]
|
||||
for g in 0 .. nonces[n].len() {
|
||||
Rs[n][g] = {
|
||||
B.values().map(|B| B[n][g][0]).sum::<C::G>() +
|
||||
(B.values().map(|B| B[n][g][1]).sum::<C::G>() * binding)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok((Package { B, binding, R, share: share.clone() }, share))
|
||||
let share = params.algorithm.sign_share(
|
||||
¶ms.view,
|
||||
&Rs,
|
||||
&our_preprocess.nonces.iter().map(
|
||||
|nonces| nonces[0] + (nonces[1] * binding)
|
||||
).collect::<Vec<_>>(),
|
||||
msg
|
||||
).to_repr().as_ref().to_vec();
|
||||
|
||||
Ok((Package { B, binding, Rs, share: share.clone() }, share))
|
||||
}
|
||||
|
||||
// This doesn't check the signing set is as expected and unexpected changes can cause false blames
|
||||
// if legitimate participants are still using the original, expected, signing set. This library
|
||||
// could be made more robust in that regard
|
||||
fn complete<C: Curve, A: Algorithm<C>>(
|
||||
sign_params: &Params<C, A>,
|
||||
sign: Package<C>,
|
||||
@@ -200,7 +269,7 @@ fn complete<C: Curve, A: Algorithm<C>>(
|
||||
let mut responses = HashMap::new();
|
||||
let mut sum = C::F::zero();
|
||||
for l in &sign_params.view.included {
|
||||
let part = C::F_from_slice(&shares[l]).map_err(|_| FrostError::InvalidShare(*l))?;
|
||||
let part = F_from_slice::<C::F>(&shares[l]).map_err(|_| FrostError::InvalidShare(*l))?;
|
||||
sum += part;
|
||||
responses.insert(*l, part);
|
||||
}
|
||||
@@ -208,7 +277,7 @@ fn complete<C: Curve, A: Algorithm<C>>(
|
||||
// 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.R, sum);
|
||||
let res = sign_params.algorithm.verify(sign_params.view.group_key, &sign.Rs, sum);
|
||||
if let Some(res) = res {
|
||||
return Ok(res);
|
||||
}
|
||||
@@ -219,7 +288,11 @@ fn complete<C: Curve, A: Algorithm<C>>(
|
||||
if !sign_params.algorithm.verify_share(
|
||||
*l,
|
||||
sign_params.view.verification_share(*l),
|
||||
sign.B[l][0] + (sign.B[l][1] * sign.binding),
|
||||
&sign.B[l].iter().map(
|
||||
|nonces| nonces.iter().map(
|
||||
|commitments| commitments[0] + (commitments[1] * sign.binding)
|
||||
).collect()
|
||||
).collect::<Vec<_>>(),
|
||||
responses[l]
|
||||
) {
|
||||
Err(FrostError::InvalidShare(*l))?;
|
||||
@@ -234,31 +307,21 @@ fn complete<C: Curve, A: Algorithm<C>>(
|
||||
)
|
||||
}
|
||||
|
||||
/// State of a Sign machine
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum State {
|
||||
Fresh,
|
||||
Preprocessed,
|
||||
Signed,
|
||||
Complete,
|
||||
}
|
||||
|
||||
impl fmt::Display for State {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait StateMachine {
|
||||
pub trait PreprocessMachine {
|
||||
type Signature: Clone + PartialEq + fmt::Debug;
|
||||
type SignMachine: SignMachine<Self::Signature>;
|
||||
|
||||
/// Perform the preprocessing round required in order to sign
|
||||
/// Returns a byte vector which must be transmitted to all parties selected for this signing
|
||||
/// process, over an authenticated channel
|
||||
fn preprocess<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
self,
|
||||
rng: &mut R
|
||||
) -> Result<Vec<u8>, FrostError>;
|
||||
) -> (Self::SignMachine, Vec<u8>);
|
||||
}
|
||||
|
||||
pub trait SignMachine<S> {
|
||||
type SignatureMachine: SignatureMachine<S>;
|
||||
|
||||
/// Sign a message
|
||||
/// Takes in the participant's commitments, which are expected to be in a Vec where participant
|
||||
@@ -266,107 +329,88 @@ pub trait StateMachine {
|
||||
/// index i which is locally handled. Returns a byte vector representing a share of the signature
|
||||
/// for every other participant to receive, over an authenticated channel
|
||||
fn sign(
|
||||
&mut self,
|
||||
self,
|
||||
commitments: HashMap<u16, Vec<u8>>,
|
||||
msg: &[u8],
|
||||
) -> Result<Vec<u8>, FrostError>;
|
||||
) -> Result<(Self::SignatureMachine, Vec<u8>), FrostError>;
|
||||
}
|
||||
|
||||
pub trait SignatureMachine<S> {
|
||||
/// Complete signing
|
||||
/// Takes in everyone elses' shares submitted to us as a Vec, expecting participant index =
|
||||
/// Vec index with None at index 0 and index i. Returns a byte vector representing the serialized
|
||||
/// signature
|
||||
fn complete(&mut self, shares: HashMap<u16, Vec<u8>>) -> Result<Self::Signature, FrostError>;
|
||||
|
||||
fn multisig_params(&self) -> MultisigParams;
|
||||
|
||||
fn state(&self) -> State;
|
||||
fn complete(self, shares: HashMap<u16, Vec<u8>>) -> Result<S, FrostError>;
|
||||
}
|
||||
|
||||
/// State machine which manages signing for an arbitrary signature algorithm
|
||||
#[allow(non_snake_case)]
|
||||
pub struct AlgorithmMachine<C: Curve, A: Algorithm<C>> {
|
||||
params: Params<C, A>
|
||||
}
|
||||
|
||||
pub struct AlgorithmSignMachine<C: Curve, A: Algorithm<C>> {
|
||||
params: Params<C, A>,
|
||||
state: State,
|
||||
preprocess: Option<PreprocessPackage<C>>,
|
||||
sign: Option<Package<C>>,
|
||||
preprocess: PreprocessPackage<C>,
|
||||
}
|
||||
|
||||
pub struct AlgorithmSignatureMachine<C: Curve, A: Algorithm<C>> {
|
||||
params: Params<C, A>,
|
||||
sign: Package<C>,
|
||||
}
|
||||
|
||||
impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> {
|
||||
/// Creates a new machine to generate a key for the specified curve in the specified multisig
|
||||
pub fn new(
|
||||
algorithm: A,
|
||||
keys: Rc<MultisigKeys<C>>,
|
||||
keys: Arc<FrostKeys<C>>,
|
||||
included: &[u16],
|
||||
) -> Result<AlgorithmMachine<C, A>, FrostError> {
|
||||
Ok(
|
||||
AlgorithmMachine {
|
||||
params: Params::new(algorithm, keys, included)?,
|
||||
state: State::Fresh,
|
||||
preprocess: None,
|
||||
sign: None,
|
||||
}
|
||||
)
|
||||
Ok(AlgorithmMachine { params: Params::new(algorithm, keys, included)? })
|
||||
}
|
||||
|
||||
pub(crate) fn unsafe_override_preprocess(
|
||||
self,
|
||||
preprocess: PreprocessPackage<C>
|
||||
) -> (AlgorithmSignMachine<C, A>, Vec<u8>) {
|
||||
let serialized = preprocess.serialized.clone();
|
||||
(AlgorithmSignMachine { params: self.params, preprocess }, serialized)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Curve, A: Algorithm<C>> StateMachine for AlgorithmMachine<C, A> {
|
||||
impl<C: Curve, A: Algorithm<C>> PreprocessMachine for AlgorithmMachine<C, A> {
|
||||
type Signature = A::Signature;
|
||||
type SignMachine = AlgorithmSignMachine<C, A>;
|
||||
|
||||
fn preprocess<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
self,
|
||||
rng: &mut R
|
||||
) -> Result<Vec<u8>, FrostError> {
|
||||
if self.state != State::Fresh {
|
||||
Err(FrostError::InvalidSignTransition(State::Fresh, self.state))?;
|
||||
}
|
||||
let preprocess = preprocess::<R, C, A>(rng, &mut self.params);
|
||||
) -> (Self::SignMachine, Vec<u8>) {
|
||||
let mut params = self.params;
|
||||
let preprocess = preprocess::<R, C, A>(rng, &mut params);
|
||||
let serialized = preprocess.serialized.clone();
|
||||
self.preprocess = Some(preprocess);
|
||||
self.state = State::Preprocessed;
|
||||
Ok(serialized)
|
||||
}
|
||||
|
||||
fn sign(
|
||||
&mut self,
|
||||
commitments: HashMap<u16, Vec<u8>>,
|
||||
msg: &[u8],
|
||||
) -> Result<Vec<u8>, FrostError> {
|
||||
if self.state != State::Preprocessed {
|
||||
Err(FrostError::InvalidSignTransition(State::Preprocessed, self.state))?;
|
||||
}
|
||||
|
||||
let (sign, serialized) = sign_with_share(
|
||||
&mut self.params,
|
||||
self.preprocess.take().unwrap(),
|
||||
commitments,
|
||||
msg,
|
||||
)?;
|
||||
|
||||
self.sign = Some(sign);
|
||||
self.state = State::Signed;
|
||||
Ok(serialized)
|
||||
}
|
||||
|
||||
fn complete(&mut self, shares: HashMap<u16, Vec<u8>>) -> Result<A::Signature, FrostError> {
|
||||
if self.state != State::Signed {
|
||||
Err(FrostError::InvalidSignTransition(State::Signed, self.state))?;
|
||||
}
|
||||
|
||||
let signature = complete(
|
||||
&self.params,
|
||||
self.sign.take().unwrap(),
|
||||
shares,
|
||||
)?;
|
||||
|
||||
self.state = State::Complete;
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
fn multisig_params(&self) -> MultisigParams {
|
||||
self.params.multisig_params().clone()
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
self.state
|
||||
(AlgorithmSignMachine { params, preprocess }, serialized)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachine<C, A> {
|
||||
type SignatureMachine = AlgorithmSignatureMachine<C, A>;
|
||||
|
||||
fn sign(
|
||||
self,
|
||||
commitments: HashMap<u16, Vec<u8>>,
|
||||
msg: &[u8]
|
||||
) -> Result<(Self::SignatureMachine, Vec<u8>), FrostError> {
|
||||
let mut params = self.params;
|
||||
let (sign, serialized) = sign_with_share(&mut params, self.preprocess, commitments, msg)?;
|
||||
Ok((AlgorithmSignatureMachine { params, sign }, serialized))
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
C: Curve,
|
||||
A: Algorithm<C>
|
||||
> SignatureMachine<A::Signature> for AlgorithmSignatureMachine<C, A> {
|
||||
fn complete(self, shares: HashMap<u16, Vec<u8>>) -> Result<A::Signature, FrostError> {
|
||||
complete(&self.params, self.sign, shares)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use crate::{
|
||||
Curve, MultisigKeys,
|
||||
tests::{schnorr::{sign, verify, batch_verify}, key_gen}
|
||||
};
|
||||
use group::{ff::Field, Group};
|
||||
|
||||
use crate::{Curve, FrostKeys, tests::key_gen};
|
||||
|
||||
// Test generation of FROST keys
|
||||
fn key_generation<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
@@ -14,21 +13,30 @@ 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 key_gen::<_, C>(rng) {
|
||||
assert_eq!(&MultisigKeys::<C>::deserialize(&keys.serialize()).unwrap(), &*keys);
|
||||
assert_eq!(&FrostKeys::<C>::deserialize(&keys.serialize()).unwrap(), &*keys);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_curve<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
// TODO: Test the Curve functions themselves
|
||||
|
||||
// Test Schnorr signatures work as expected
|
||||
// This is a bit unnecessary, as they should for any valid curve, yet this provides tests with
|
||||
// meaning, which the above tests won't have
|
||||
sign::<_, C>(rng);
|
||||
verify::<_, C>(rng);
|
||||
batch_verify::<_, C>(rng);
|
||||
// Test successful multiexp, with enough pairs to trigger its variety of algorithms
|
||||
// Multiexp has its own tests, yet only against k256 and Ed25519 (which should be sufficient
|
||||
// as-is to prove multiexp), and this doesn't hurt
|
||||
{
|
||||
let mut pairs = Vec::with_capacity(1000);
|
||||
let mut sum = C::G::identity();
|
||||
for _ in 0 .. 10 {
|
||||
for _ in 0 .. 100 {
|
||||
pairs.push((C::F::random(&mut *rng), C::GENERATOR * C::F::random(&mut *rng)));
|
||||
sum += pairs[pairs.len() - 1].1 * pairs[pairs.len() - 1].0;
|
||||
}
|
||||
assert_eq!(multiexp::multiexp(&pairs), sum);
|
||||
assert_eq!(multiexp::multiexp_vartime(&pairs), sum);
|
||||
}
|
||||
}
|
||||
|
||||
// Test FROST key generation and serialization of MultisigKeys works as expected
|
||||
// Test FROST key generation and serialization of FrostKeys works as expected
|
||||
key_generation::<_, C>(rng);
|
||||
keys_serialization::<_, C>(rng);
|
||||
}
|
||||
|
||||
77
crypto/frost/src/tests/literal/dalek.rs
Normal file
77
crypto/frost/src/tests/literal/dalek.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
use crate::{curve, tests::vectors::{Vectors, test_with_vectors}};
|
||||
|
||||
#[cfg(any(test, feature = "ristretto"))]
|
||||
#[test]
|
||||
fn ristretto_vectors() {
|
||||
test_with_vectors::<_, curve::Ristretto, curve::IetfRistrettoHram>(
|
||||
&mut OsRng,
|
||||
Vectors {
|
||||
threshold: 2,
|
||||
shares: &[
|
||||
"5c3430d391552f6e60ecdc093ff9f6f4488756aa6cebdbad75a768010b8f830e",
|
||||
"b06fc5eac20b4f6e1b271d9df2343d843e1e1fb03c4cbb673f2872d459ce6f01",
|
||||
"f17e505f0e2581c6acfe54d3846a622834b5e7b50cad9a2109a97ba7a80d5c04"
|
||||
],
|
||||
group_secret: "1b25a55e463cfd15cf14a5d3acc3d15053f08da49c8afcf3ab265f2ebc4f970b",
|
||||
group_key: "e2a62f39eede11269e3bd5a7d97554f5ca384f9f6d3dd9c3c0d05083c7254f57",
|
||||
|
||||
msg: "74657374",
|
||||
included: &[1, 3],
|
||||
nonces: &[
|
||||
[
|
||||
"b358743151e33d84bf00c12f71808f4103957c3e2cabab7b895c436b5e70f90c",
|
||||
"7bd112153b9ae1ab9b31f5e78f61f5c4ca9ee67b7ea6d1181799c409d14c350c"
|
||||
],
|
||||
[
|
||||
"22acad88478e0d0373a991092a322ebd1b9a2dad90451a976d0db3215426af0e",
|
||||
"9155e3d7bcf7cd468b980c7e20b2c77cbdfbe33a1dcae031fd8bc6b1403f4b04"
|
||||
]
|
||||
],
|
||||
sig_shares: &[
|
||||
"ff801b4e0839faa67f16dee4127b9f7fbcf5fd007900257b0e2bbc02cbe5e709",
|
||||
"afdf5481023c855bf3411a5c8a5fafa92357296a078c3b80dc168f294cb4f504"
|
||||
],
|
||||
sig: "deae61af10e8ee48ba492573592fba547f5debeff6bd6e2024e8673584746f5e".to_owned() +
|
||||
"ae6070cf0a757f027358f8409dda4e29e04c276b808c60fbea414b2c179add0e"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ed25519")]
|
||||
#[test]
|
||||
fn ed25519_vectors() {
|
||||
test_with_vectors::<_, curve::Ed25519, curve::IetfEd25519Hram>(
|
||||
&mut OsRng,
|
||||
Vectors {
|
||||
threshold: 2,
|
||||
shares: &[
|
||||
"929dcc590407aae7d388761cddb0c0db6f5627aea8e217f4a033f2ec83d93509",
|
||||
"a91e66e012e4364ac9aaa405fcafd370402d9859f7b6685c07eed76bf409e80d",
|
||||
"d3cb090a075eb154e82fdb4b3cb507f110040905468bb9c46da8bdea643a9a02"
|
||||
],
|
||||
group_secret: "7b1c33d3f5291d85de664833beb1ad469f7fb6025a0ec78b3a790c6e13a98304",
|
||||
group_key: "15d21ccd7ee42959562fc8aa63224c8851fb3ec85a3faf66040d380fb9738673",
|
||||
|
||||
msg: "74657374",
|
||||
included: &[1, 3],
|
||||
nonces: &[
|
||||
[
|
||||
"8c76af04340e83bb5fc427c117d38347fc8ef86d5397feea9aa6412d96c05b0a",
|
||||
"14a37ddbeae8d9e9687369e5eb3c6d54f03dc19d76bb54fb5425131bc37a600b"
|
||||
],
|
||||
[
|
||||
"5ca39ebab6874f5e7b5089f3521819a2aa1e2cf738bae6974ee80555de2ef70e",
|
||||
"0afe3650c4815ff37becd3c6948066e906e929ea9b8f546c74e10002dbcc150c"
|
||||
]
|
||||
],
|
||||
sig_shares: &[
|
||||
"4369474a398aa10357b60d683da91ea6a767dcf53fd541a8ed6b4d780827ea0a",
|
||||
"32fcc690d926075e45d2dfb746bab71447943cddbefe80d122c39174aa2e1004"
|
||||
],
|
||||
sig: "2b8d9c6995333c5990e3a3dd6568785539d3322f7f0376452487ea35cfda587b".to_owned() +
|
||||
"75650edb12b1a8619c88ed1f8463d6baeefb18d3fed3c279102fdfecb255fa0e"
|
||||
}
|
||||
);
|
||||
}
|
||||
55
crypto/frost/src/tests/literal/kp256.rs
Normal file
55
crypto/frost/src/tests/literal/kp256.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
#[cfg(feature = "secp256k1")]
|
||||
use crate::tests::{curve::test_curve, schnorr::test_schnorr};
|
||||
#[cfg(feature = "secp256k1")]
|
||||
use crate::curve::Secp256k1;
|
||||
|
||||
#[cfg(feature = "p256")]
|
||||
use crate::tests::vectors::{Vectors, test_with_vectors};
|
||||
#[cfg(feature = "p256")]
|
||||
use crate::curve::{P256, IetfP256Hram};
|
||||
|
||||
#[cfg(feature = "secp256k1")]
|
||||
#[test]
|
||||
fn secp256k1_non_ietf() {
|
||||
test_curve::<_, Secp256k1>(&mut OsRng);
|
||||
test_schnorr::<_, Secp256k1>(&mut OsRng);
|
||||
}
|
||||
|
||||
#[cfg(feature = "p256")]
|
||||
#[test]
|
||||
fn p256_vectors() {
|
||||
test_with_vectors::<_, P256, IetfP256Hram>(
|
||||
&mut OsRng,
|
||||
Vectors {
|
||||
threshold: 2,
|
||||
shares: &[
|
||||
"0c9c1a0fe806c184add50bbdcac913dda73e482daf95dcb9f35dbb0d8a9f7731",
|
||||
"8d8e787bef0ff6c2f494ca45f4dad198c6bee01212d6c84067159c52e1863ad5",
|
||||
"0e80d6e8f6192c003b5488ce1eec8f5429587d48cf001541e713b2d53c09d928"
|
||||
],
|
||||
group_secret: "8ba9bba2e0fd8c4767154d35a0b7562244a4aaf6f36c8fb8735fa48b301bd8de",
|
||||
group_key: "023a309ad94e9fe8a7ba45dfc58f38bf091959d3c99cfbd02b4dc00585ec45ab70",
|
||||
|
||||
msg: "74657374",
|
||||
included: &[1, 3],
|
||||
nonces: &[
|
||||
[
|
||||
"081617b24375e069b39f649d4c4ce2fba6e38b73e7c16759de0b6079a22c4c7e",
|
||||
"4de5fb77d99f03a2491a83a6a4cb91ca3c82a3f34ce94cec939174f47c9f95dd"
|
||||
],
|
||||
[
|
||||
"d186ea92593f83ea83181b184d41aa93493301ac2bc5b4b1767e94d2db943e38",
|
||||
"486e2ee25a3fbc8e6399d748b077a2755fde99fa85cc24fa647ea4ebf5811a15"
|
||||
]
|
||||
],
|
||||
sig_shares: &[
|
||||
"9e4d8865faf8c7b3193a3b35eda3d9e12118447114b1e7d5b4809ea28067f8a9",
|
||||
"b7d094eab6305ae74daeed1acd31abba9ab81f638d38b72c132cb25a5dfae1fc"
|
||||
],
|
||||
sig: "0342c14c77f9d4ef9b8bd64fb0d7bbfdb9f8216a44e5f7bbe6ac0f3ed5e1a57367".to_owned() +
|
||||
"561e1d51b129229966e92850bad5859bfee96926fad3007cd3f38639e1ffb554"
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
mod secp256k1;
|
||||
mod schnorr;
|
||||
#[cfg(any(test, feature = "dalek"))]
|
||||
mod dalek;
|
||||
#[cfg(feature = "kp256")]
|
||||
mod kp256;
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
use crate::{
|
||||
Curve, schnorr, algorithm::{Hram, Schnorr},
|
||||
tests::{key_gen, algorithm_machines, sign as sign_test, literal::secp256k1::{Secp256k1, TestHram}}
|
||||
};
|
||||
|
||||
const MESSAGE: &[u8] = b"Hello World";
|
||||
|
||||
#[test]
|
||||
fn sign() {
|
||||
sign_test(
|
||||
&mut OsRng,
|
||||
algorithm_machines(
|
||||
&mut OsRng,
|
||||
Schnorr::<Secp256k1, TestHram>::new(),
|
||||
&key_gen::<_, Secp256k1>(&mut OsRng)
|
||||
),
|
||||
MESSAGE
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_with_offset() {
|
||||
let mut keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
||||
let group_key = keys[&1].group_key();
|
||||
|
||||
let offset = Secp256k1::hash_to_F(b"offset");
|
||||
for i in 1 ..= u16::try_from(keys.len()).unwrap() {
|
||||
keys.insert(i, Rc::new(keys[&i].offset(offset)));
|
||||
}
|
||||
let offset_key = group_key + (Secp256k1::generator_table() * offset);
|
||||
|
||||
let sig = sign_test(
|
||||
&mut OsRng,
|
||||
algorithm_machines(&mut OsRng, Schnorr::<Secp256k1, TestHram>::new(), &keys),
|
||||
MESSAGE
|
||||
);
|
||||
assert!(schnorr::verify(offset_key, TestHram::hram(&sig.R, &offset_key, MESSAGE), &sig));
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
use core::convert::TryInto;
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
use ff::PrimeField;
|
||||
use group::GroupEncoding;
|
||||
|
||||
use sha2::{Digest, Sha256, Sha512};
|
||||
|
||||
use k256::{
|
||||
elliptic_curve::{generic_array::GenericArray, bigint::{ArrayEncoding, U512}, ops::Reduce},
|
||||
Scalar,
|
||||
ProjectivePoint
|
||||
};
|
||||
|
||||
use crate::{CurveError, Curve, algorithm::Hram, tests::curve::test_curve};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct Secp256k1;
|
||||
impl Curve for Secp256k1 {
|
||||
type F = Scalar;
|
||||
type G = ProjectivePoint;
|
||||
type T = ProjectivePoint;
|
||||
|
||||
fn id() -> String {
|
||||
"secp256k1".to_string()
|
||||
}
|
||||
|
||||
fn id_len() -> u8 {
|
||||
u8::try_from(Self::id().len()).unwrap()
|
||||
}
|
||||
|
||||
fn generator() -> Self::G {
|
||||
Self::G::GENERATOR
|
||||
}
|
||||
|
||||
fn generator_table() -> Self::T {
|
||||
Self::G::GENERATOR
|
||||
}
|
||||
|
||||
fn little_endian() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// The IETF draft doesn't specify a secp256k1 ciphersuite
|
||||
// This test just uses the simplest ciphersuite which would still be viable to deploy
|
||||
// The comparable P-256 curve uses hash_to_field from the Hash To Curve IETF draft with a context
|
||||
// string and further DST for H1 ("rho") and H3 ("digest"). It's not currently worth it to add
|
||||
// that weight, yet if secp256k1 is ever officially acknowledged (not just a testing curve), it
|
||||
// must be properly implemented.
|
||||
fn hash_msg(msg: &[u8]) -> Vec<u8> {
|
||||
(&Sha256::digest(msg)).to_vec()
|
||||
}
|
||||
|
||||
fn hash_binding_factor(binding: &[u8]) -> Self::F {
|
||||
Self::hash_to_F(&[b"rho", binding].concat())
|
||||
}
|
||||
|
||||
// Use wide reduction for security
|
||||
fn hash_to_F(data: &[u8]) -> Self::F {
|
||||
Scalar::from_uint_reduced(U512::from_be_byte_array(Sha512::digest(data)))
|
||||
}
|
||||
|
||||
fn F_len() -> usize {
|
||||
32
|
||||
}
|
||||
|
||||
fn G_len() -> usize {
|
||||
33
|
||||
}
|
||||
|
||||
fn F_from_slice(slice: &[u8]) -> Result<Self::F, CurveError> {
|
||||
let bytes: [u8; 32] = slice.try_into()
|
||||
.map_err(|_| CurveError::InvalidLength(32, slice.len()))?;
|
||||
let scalar = Scalar::from_repr(bytes.into());
|
||||
if scalar.is_none().unwrap_u8() == 1 {
|
||||
Err(CurveError::InvalidScalar)?;
|
||||
}
|
||||
Ok(scalar.unwrap())
|
||||
}
|
||||
|
||||
fn G_from_slice(slice: &[u8]) -> Result<Self::G, CurveError> {
|
||||
let point = ProjectivePoint::from_bytes(GenericArray::from_slice(slice));
|
||||
if point.is_none().unwrap_u8() == 1 {
|
||||
Err(CurveError::InvalidScalar)?;
|
||||
}
|
||||
Ok(point.unwrap())
|
||||
}
|
||||
|
||||
fn F_to_bytes(f: &Self::F) -> Vec<u8> {
|
||||
(&f.to_bytes()).to_vec()
|
||||
}
|
||||
|
||||
fn G_to_bytes(g: &Self::G) -> Vec<u8> {
|
||||
(&g.to_bytes()).to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone)]
|
||||
pub struct TestHram {}
|
||||
impl Hram<Secp256k1> for TestHram {
|
||||
#[allow(non_snake_case)]
|
||||
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
|
||||
Scalar::from_uint_reduced(
|
||||
U512::from_be_byte_array(
|
||||
Sha512::new()
|
||||
.chain_update(Secp256k1::G_to_bytes(R))
|
||||
.chain_update(Secp256k1::G_to_bytes(A))
|
||||
.chain_update(m)
|
||||
.finalize()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secp256k1_curve() {
|
||||
test_curve::<_, Secp256k1>(&mut OsRng);
|
||||
}
|
||||
@@ -1,23 +1,22 @@
|
||||
use std::{rc::Rc, collections::HashMap};
|
||||
use std::{sync::Arc, collections::HashMap};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ff::Field;
|
||||
use group::ff::Field;
|
||||
|
||||
use crate::{
|
||||
Curve,
|
||||
MultisigParams, MultisigKeys,
|
||||
FrostParams, FrostKeys,
|
||||
lagrange,
|
||||
key_gen,
|
||||
key_gen::KeyGenMachine,
|
||||
algorithm::Algorithm,
|
||||
sign::{StateMachine, AlgorithmMachine}
|
||||
sign::{PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine}
|
||||
};
|
||||
|
||||
// Internal tests
|
||||
mod schnorr;
|
||||
|
||||
// Test suites for public usage
|
||||
pub mod curve;
|
||||
pub mod schnorr;
|
||||
pub mod vectors;
|
||||
|
||||
// Literal test definitions to run during `cargo test`
|
||||
#[cfg(test)]
|
||||
@@ -37,50 +36,37 @@ pub fn clone_without<K: Clone + std::cmp::Eq + std::hash::Hash, V: Clone>(
|
||||
|
||||
pub fn key_gen<R: RngCore + CryptoRng, C: Curve>(
|
||||
rng: &mut R
|
||||
) -> HashMap<u16, Rc<MultisigKeys<C>>> {
|
||||
let mut params = HashMap::new();
|
||||
) -> HashMap<u16, Arc<FrostKeys<C>>> {
|
||||
let mut machines = HashMap::new();
|
||||
|
||||
let mut commitments = HashMap::new();
|
||||
for i in 1 ..= PARTICIPANTS {
|
||||
params.insert(
|
||||
i,
|
||||
MultisigParams::new(
|
||||
THRESHOLD,
|
||||
PARTICIPANTS,
|
||||
i
|
||||
).unwrap()
|
||||
);
|
||||
machines.insert(
|
||||
i,
|
||||
key_gen::StateMachine::<C>::new(
|
||||
params[&i],
|
||||
"FROST test key_gen".to_string()
|
||||
)
|
||||
);
|
||||
commitments.insert(
|
||||
i,
|
||||
machines.get_mut(&i).unwrap().generate_coefficients(rng).unwrap()
|
||||
let machine = KeyGenMachine::<C>::new(
|
||||
FrostParams::new(THRESHOLD, PARTICIPANTS, i).unwrap(),
|
||||
"FROST Test key_gen".to_string()
|
||||
);
|
||||
let (machine, these_commitments) = machine.generate_coefficients(rng);
|
||||
machines.insert(i, machine);
|
||||
commitments.insert(i, these_commitments);
|
||||
}
|
||||
|
||||
let mut secret_shares = HashMap::new();
|
||||
for (l, machine) in machines.iter_mut() {
|
||||
secret_shares.insert(
|
||||
*l,
|
||||
let mut machines = machines.drain().map(|(l, machine)| {
|
||||
let (machine, shares) = machine.generate_secret_shares(
|
||||
rng,
|
||||
// clone_without isn't necessary, as this machine's own data will be inserted without
|
||||
// conflict, yet using it ensures the machine's own data is actually inserted as expected
|
||||
machine.generate_secret_shares(rng, clone_without(&commitments, l)).unwrap()
|
||||
);
|
||||
}
|
||||
clone_without(&commitments, &l)
|
||||
).unwrap();
|
||||
secret_shares.insert(l, shares);
|
||||
(l, machine)
|
||||
}).collect::<HashMap<_, _>>();
|
||||
|
||||
let mut verification_shares = None;
|
||||
let mut group_key = None;
|
||||
let mut keys = HashMap::new();
|
||||
for (i, machine) in machines.iter_mut() {
|
||||
machines.drain().map(|(i, machine)| {
|
||||
let mut our_secret_shares = HashMap::new();
|
||||
for (l, shares) in &secret_shares {
|
||||
if i == l {
|
||||
if i == *l {
|
||||
continue;
|
||||
}
|
||||
our_secret_shares.insert(*l, shares[&i].clone());
|
||||
@@ -99,13 +85,11 @@ pub fn key_gen<R: RngCore + CryptoRng, C: Curve>(
|
||||
}
|
||||
assert_eq!(group_key.unwrap(), these_keys.group_key());
|
||||
|
||||
keys.insert(*i, Rc::new(these_keys));
|
||||
}
|
||||
|
||||
keys
|
||||
(i, Arc::new(these_keys))
|
||||
}).collect::<HashMap<_, _>>()
|
||||
}
|
||||
|
||||
pub fn recover<C: Curve>(keys: &HashMap<u16, MultisigKeys<C>>) -> C::F {
|
||||
pub fn recover<C: Curve>(keys: &HashMap<u16, FrostKeys<C>>) -> C::F {
|
||||
let first = keys.values().next().expect("no keys provided");
|
||||
assert!(keys.len() >= first.params().t().into(), "not enough keys provided");
|
||||
let included = keys.keys().cloned().collect::<Vec<_>>();
|
||||
@@ -114,14 +98,14 @@ pub fn recover<C: Curve>(keys: &HashMap<u16, MultisigKeys<C>>) -> C::F {
|
||||
C::F::zero(),
|
||||
|accum, (i, keys)| accum + (keys.secret_share() * lagrange::<C::F>(*i, &included))
|
||||
);
|
||||
assert_eq!(C::generator_table() * group_private, first.group_key(), "failed to recover keys");
|
||||
assert_eq!(C::GENERATOR * group_private, first.group_key(), "failed to recover keys");
|
||||
group_private
|
||||
}
|
||||
|
||||
pub fn algorithm_machines<R: RngCore, C: Curve, A: Algorithm<C>>(
|
||||
rng: &mut R,
|
||||
algorithm: A,
|
||||
keys: &HashMap<u16, Rc<MultisigKeys<C>>>,
|
||||
keys: &HashMap<u16, Arc<FrostKeys<C>>>,
|
||||
) -> HashMap<u16, AlgorithmMachine<C, A>> {
|
||||
let mut included = vec![];
|
||||
while included.len() < usize::from(keys[&1].params().t()) {
|
||||
@@ -148,27 +132,28 @@ pub fn algorithm_machines<R: RngCore, C: Curve, A: Algorithm<C>>(
|
||||
).collect()
|
||||
}
|
||||
|
||||
pub fn sign<R: RngCore + CryptoRng, M: StateMachine>(
|
||||
pub fn sign<R: RngCore + CryptoRng, M: PreprocessMachine>(
|
||||
rng: &mut R,
|
||||
mut machines: HashMap<u16, M>,
|
||||
msg: &[u8]
|
||||
) -> M::Signature {
|
||||
let mut commitments = HashMap::new();
|
||||
for (i, machine) in machines.iter_mut() {
|
||||
commitments.insert(*i, machine.preprocess(rng).unwrap());
|
||||
}
|
||||
let mut machines = machines.drain().map(|(i, machine)| {
|
||||
let (machine, preprocess) = machine.preprocess(rng);
|
||||
commitments.insert(i, preprocess);
|
||||
(i, machine)
|
||||
}).collect::<HashMap<_, _>>();
|
||||
|
||||
let mut shares = HashMap::new();
|
||||
for (i, machine) in machines.iter_mut() {
|
||||
shares.insert(
|
||||
*i,
|
||||
machine.sign(clone_without(&commitments, i), msg).unwrap()
|
||||
);
|
||||
}
|
||||
let mut machines = machines.drain().map(|(i, machine)| {
|
||||
let (machine, share) = machine.sign(clone_without(&commitments, &i), msg).unwrap();
|
||||
shares.insert(i, share);
|
||||
(i, machine)
|
||||
}).collect::<HashMap<_, _>>();
|
||||
|
||||
let mut signature = None;
|
||||
for (i, machine) in machines.iter_mut() {
|
||||
let sig = machine.complete(clone_without(&shares, i)).unwrap();
|
||||
for (i, machine) in machines.drain() {
|
||||
let sig = machine.complete(clone_without(&shares, &i)).unwrap();
|
||||
if signature.is_none() {
|
||||
signature = Some(sig.clone());
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
use std::{marker::PhantomData, sync::Arc, collections::HashMap};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use ff::Field;
|
||||
use group::{ff::Field, GroupEncoding};
|
||||
|
||||
use crate::{Curve, schnorr, algorithm::SchnorrSignature};
|
||||
use crate::{
|
||||
Curve, FrostKeys, schnorr::{self, SchnorrSignature}, algorithm::{Hram, Schnorr},
|
||||
tests::{key_gen, algorithm_machines, sign as sign_test}
|
||||
};
|
||||
|
||||
pub(crate) fn sign<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
pub(crate) fn core_sign<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
let private_key = C::F::random(&mut *rng);
|
||||
let nonce = C::F::random(&mut *rng);
|
||||
let challenge = C::F::random(rng); // Doesn't bother to craft an HRAM
|
||||
assert!(
|
||||
schnorr::verify::<C>(
|
||||
C::generator_table() * private_key,
|
||||
C::GENERATOR * private_key,
|
||||
challenge,
|
||||
&schnorr::sign(private_key, nonce, challenge)
|
||||
)
|
||||
@@ -20,17 +25,17 @@ pub(crate) fn sign<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
// The above sign function verifies signing works
|
||||
// This verifies invalid signatures don't pass, using zero signatures, which should effectively be
|
||||
// random
|
||||
pub(crate) fn verify<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
pub(crate) fn core_verify<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
assert!(
|
||||
!schnorr::verify::<C>(
|
||||
C::generator_table() * C::F::random(&mut *rng),
|
||||
C::GENERATOR * C::F::random(&mut *rng),
|
||||
C::F::random(rng),
|
||||
&SchnorrSignature { R: C::generator_table() * C::F::zero(), s: C::F::zero() }
|
||||
&SchnorrSignature { R: C::GENERATOR * C::F::zero(), s: C::F::zero() }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn batch_verify<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
pub(crate) fn core_batch_verify<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
// Create 5 signatures
|
||||
let mut keys = vec![];
|
||||
let mut challenges = vec![];
|
||||
@@ -43,7 +48,7 @@ pub(crate) fn batch_verify<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
|
||||
// Batch verify
|
||||
let triplets = (0 .. 5).map(
|
||||
|i| (u16::try_from(i + 1).unwrap(), C::generator_table() * keys[i], challenges[i], sigs[i])
|
||||
|i| (u16::try_from(i + 1).unwrap(), C::GENERATOR * keys[i], challenges[i], sigs[i])
|
||||
).collect::<Vec<_>>();
|
||||
schnorr::batch_verify(rng, &triplets).unwrap();
|
||||
|
||||
@@ -71,3 +76,56 @@ pub(crate) fn batch_verify<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sign_core<R: RngCore + CryptoRng, C: Curve>(
|
||||
rng: &mut R,
|
||||
group_key: C::G,
|
||||
keys: &HashMap<u16, Arc<FrostKeys<C>>>
|
||||
) {
|
||||
const MESSAGE: &'static [u8] = b"Hello, World!";
|
||||
|
||||
let machines = algorithm_machines(rng, Schnorr::<C, TestHram<C>>::new(), keys);
|
||||
let sig = sign_test(&mut *rng, machines, MESSAGE);
|
||||
assert!(schnorr::verify(group_key, TestHram::<C>::hram(&sig.R, &group_key, MESSAGE), &sig));
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestHram<C: Curve> {
|
||||
_curve: PhantomData<C>
|
||||
}
|
||||
impl<C: Curve> Hram<C> for TestHram<C> {
|
||||
#[allow(non_snake_case)]
|
||||
fn hram(R: &C::G, A: &C::G, m: &[u8]) -> C::F {
|
||||
C::hash_to_F(b"challenge", &[R.to_bytes().as_ref(), A.to_bytes().as_ref(), m].concat())
|
||||
}
|
||||
}
|
||||
|
||||
fn sign<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
let keys = key_gen::<_, C>(&mut *rng);
|
||||
sign_core(rng, keys[&1].group_key(), &keys);
|
||||
}
|
||||
|
||||
fn sign_with_offset<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
let mut keys = key_gen::<_, C>(&mut *rng);
|
||||
let group_key = keys[&1].group_key();
|
||||
|
||||
let offset = C::hash_to_F(b"FROST Test sign_with_offset", b"offset");
|
||||
for i in 1 ..= u16::try_from(keys.len()).unwrap() {
|
||||
keys.insert(i, Arc::new(keys[&i].offset(offset)));
|
||||
}
|
||||
let offset_key = group_key + (C::GENERATOR * offset);
|
||||
|
||||
sign_core(rng, offset_key, &keys);
|
||||
}
|
||||
|
||||
pub fn test_schnorr<R: RngCore + CryptoRng, C: Curve>(rng: &mut R) {
|
||||
// Test Schnorr signatures work as expected
|
||||
// This is a bit unnecessary, as they should for any valid curve, yet this establishes sanity
|
||||
core_sign::<_, C>(rng);
|
||||
core_verify::<_, C>(rng);
|
||||
core_batch_verify::<_, C>(rng);
|
||||
|
||||
// Test Schnorr signatures under FROST
|
||||
sign::<_, C>(rng);
|
||||
sign_with_offset::<_, C>(rng);
|
||||
}
|
||||
|
||||
136
crypto/frost/src/tests/vectors.rs
Normal file
136
crypto/frost/src/tests/vectors.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use std::{sync::Arc, collections::HashMap};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use group::{ff::PrimeField, GroupEncoding};
|
||||
|
||||
use crate::{
|
||||
curve::{Curve, F_from_slice, G_from_slice}, FrostKeys,
|
||||
algorithm::{Schnorr, Hram},
|
||||
sign::{PreprocessPackage, SignMachine, SignatureMachine, AlgorithmMachine},
|
||||
tests::{curve::test_curve, schnorr::test_schnorr, recover}
|
||||
};
|
||||
|
||||
pub struct Vectors {
|
||||
pub threshold: u16,
|
||||
pub shares: &'static [&'static str],
|
||||
pub group_secret: &'static str,
|
||||
pub group_key: &'static str,
|
||||
|
||||
pub msg: &'static str,
|
||||
pub included: &'static [u16],
|
||||
pub nonces: &'static [[&'static str; 2]],
|
||||
pub sig_shares: &'static [&'static str],
|
||||
pub sig: String
|
||||
}
|
||||
|
||||
// Load these vectors into FrostKeys using a custom serialization it'll deserialize
|
||||
fn vectors_to_multisig_keys<C: Curve>(vectors: &Vectors) -> HashMap<u16, FrostKeys<C>> {
|
||||
let shares = vectors.shares.iter().map(
|
||||
|secret| F_from_slice::<C::F>(&hex::decode(secret).unwrap()).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() {
|
||||
let mut serialized = vec![];
|
||||
serialized.extend(u64::try_from(C::ID.len()).unwrap().to_be_bytes());
|
||||
serialized.extend(C::ID);
|
||||
serialized.extend(vectors.threshold.to_be_bytes());
|
||||
serialized.extend(u16::try_from(shares.len()).unwrap().to_be_bytes());
|
||||
serialized.extend(i.to_be_bytes());
|
||||
serialized.extend(shares[usize::from(i) - 1].to_repr().as_ref());
|
||||
serialized.extend(&hex::decode(vectors.group_key).unwrap());
|
||||
for share in &verification_shares {
|
||||
serialized.extend(share.to_bytes().as_ref());
|
||||
}
|
||||
|
||||
let these_keys = FrostKeys::<C>::deserialize(&serialized).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);
|
||||
assert_eq!(these_keys.secret_share(), shares[usize::from(i - 1)]);
|
||||
assert_eq!(&hex::encode(these_keys.group_key().to_bytes().as_ref()), vectors.group_key);
|
||||
keys.insert(i, these_keys);
|
||||
}
|
||||
|
||||
keys
|
||||
}
|
||||
|
||||
pub fn test_with_vectors<
|
||||
R: RngCore + CryptoRng,
|
||||
C: Curve,
|
||||
H: Hram<C>
|
||||
>(rng: &mut R, vectors: Vectors) {
|
||||
// Do basic tests before trying the vectors
|
||||
test_curve::<_, C>(&mut *rng);
|
||||
test_schnorr::<_, C>(rng);
|
||||
|
||||
// Test against the vectors
|
||||
let keys = vectors_to_multisig_keys::<C>(&vectors);
|
||||
let group_key = G_from_slice::<C::G>(&hex::decode(vectors.group_key).unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
C::GENERATOR * F_from_slice::<C::F>(&hex::decode(vectors.group_secret).unwrap()).unwrap(),
|
||||
group_key
|
||||
);
|
||||
assert_eq!(
|
||||
recover(&keys),
|
||||
F_from_slice::<C::F>(&hex::decode(vectors.group_secret).unwrap()).unwrap()
|
||||
);
|
||||
|
||||
let mut machines = vec![];
|
||||
for i in vectors.included {
|
||||
machines.push((
|
||||
*i,
|
||||
AlgorithmMachine::new(
|
||||
Schnorr::<C, H>::new(),
|
||||
Arc::new(keys[i].clone()),
|
||||
vectors.included.clone()
|
||||
).unwrap()
|
||||
));
|
||||
}
|
||||
|
||||
let mut commitments = HashMap::new();
|
||||
let mut c = 0;
|
||||
let mut machines = machines.drain(..).map(|(i, machine)| {
|
||||
let nonces = [
|
||||
F_from_slice::<C::F>(&hex::decode(vectors.nonces[c][0]).unwrap()).unwrap(),
|
||||
F_from_slice::<C::F>(&hex::decode(vectors.nonces[c][1]).unwrap()).unwrap()
|
||||
];
|
||||
c += 1;
|
||||
|
||||
let mut serialized = (C::GENERATOR * nonces[0]).to_bytes().as_ref().to_vec();
|
||||
serialized.extend((C::GENERATOR * nonces[1]).to_bytes().as_ref());
|
||||
|
||||
let (machine, serialized) = machine.unsafe_override_preprocess(
|
||||
PreprocessPackage { nonces: vec![nonces], serialized: serialized.clone() }
|
||||
);
|
||||
|
||||
commitments.insert(i, serialized);
|
||||
(i, machine)
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
let mut shares = HashMap::new();
|
||||
c = 0;
|
||||
let mut machines = machines.drain(..).map(|(i, machine)| {
|
||||
let (machine, share) = machine.sign(
|
||||
commitments.clone(),
|
||||
&hex::decode(vectors.msg).unwrap()
|
||||
).unwrap();
|
||||
|
||||
assert_eq!(share, hex::decode(vectors.sig_shares[c]).unwrap());
|
||||
c += 1;
|
||||
|
||||
shares.insert(i, share);
|
||||
(i, machine)
|
||||
}).collect::<HashMap<_, _>>();
|
||||
|
||||
for (_, machine) in machines.drain() {
|
||||
let sig = machine.complete(shares.clone()).unwrap();
|
||||
let mut serialized = sig.R.to_bytes().as_ref().to_vec();
|
||||
serialized.extend(sig.s.to_repr().as_ref());
|
||||
assert_eq!(hex::encode(serialized), vectors.sig);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user