Rename sign folder to crypto

Inspired by #3 and #5.
This commit is contained in:
Luke Parker
2022-05-03 00:46:50 -04:00
parent 9ccf683e9d
commit 87f38cafe4
15 changed files with 4 additions and 4 deletions

22
crypto/frost/Cargo.toml Normal file
View File

@@ -0,0 +1,22 @@
[package]
name = "frost"
version = "0.1.0"
description = "Implementation of FROST over ff/group"
license = "MIT"
authors = ["kayabaNerve (Luke Parker) <lukeparker5132@gmail.com>"]
edition = "2021"
[dependencies]
digest = "0.10"
rand_core = "0.6"
ff = "0.11"
group = "0.11"
thiserror = "1"
[dev-dependencies]
rand = "0.8"
sha2 = "0.10"
k256 = { version = "0.10", features = ["arithmetic"] }

21
crypto/frost/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-2022 Luke Parker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
crypto/frost/README.md Normal file
View File

@@ -0,0 +1,3 @@
# FROST
Implementation of FROST for any curve with a ff/group API.

View File

@@ -0,0 +1,151 @@
use core::{marker::PhantomData, fmt::Debug};
use rand_core::{RngCore, CryptoRng};
use group::Group;
use crate::{Curve, FrostError, MultisigView};
/// Algorithm to use FROST with
pub trait Algorithm<C: Curve>: Clone {
/// The resulting type of the signatures this algorithm will produce
type Signature: Clone + Debug;
/// The amount of bytes from each participant's addendum to commit to
fn addendum_commit_len() -> usize;
/// Generate an addendum to FROST"s preprocessing stage
fn preprocess_addendum<R: RngCore + CryptoRng>(
rng: &mut R,
params: &MultisigView<C>,
nonces: &[C::F; 2],
) -> Vec<u8>;
/// Proccess the addendum for the specified participant. Guaranteed to be ordered
fn process_addendum(
&mut self,
params: &MultisigView<C>,
l: usize,
commitments: &[C::G; 2],
serialized: &[u8],
) -> Result<(), FrostError>;
/// Context for this algorithm to be hashed into b, and therefore committed to
fn context(&self) -> Vec<u8>;
/// Sign a share with the given secret/nonce
/// The secret will already have been its lagrange coefficient applied so it is the necessary
/// key share
/// 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,
b: C::F,
nonce: 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>;
/// Verify a specific share given as a response. Used to determine blame if signature
/// verification fails
fn verify_share(
&self,
verification_share: C::G,
nonce: C::G,
share: C::F,
) -> bool;
}
pub trait Hram<C: Curve>: Clone {
/// HRAM function to generate a challenge
/// H2 from the IETF draft despite having a different argument set (not pre-formatted)
#[allow(non_snake_case)]
fn hram(R: &C::G, A: &C::G, m: &[u8]) -> C::F;
}
#[derive(Clone)]
pub struct Schnorr<C: Curve, H: Hram<C>> {
c: Option<C::F>,
_hram: PhantomData<H>,
}
impl<C: Curve, H: Hram<C>> Schnorr<C, H> {
pub fn new() -> Schnorr<C, H> {
Schnorr {
c: None,
_hram: PhantomData
}
}
}
#[allow(non_snake_case)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct SchnorrSignature<C: Curve> {
pub R: C::G,
pub s: C::F,
}
/// Implementation of Schnorr signatures for use with FROST
impl<C: Curve, H: Hram<C>> Algorithm<C> for Schnorr<C, H> {
type Signature = SchnorrSignature<C>;
fn addendum_commit_len() -> usize {
0
}
fn preprocess_addendum<R: RngCore + CryptoRng>(
_: &mut R,
_: &MultisigView<C>,
_: &[C::F; 2],
) -> Vec<u8> {
vec![]
}
fn process_addendum(
&mut self,
_: &MultisigView<C>,
_: usize,
_: &[C::G; 2],
_: &[u8],
) -> Result<(), FrostError> {
Ok(())
}
fn context(&self) -> Vec<u8> {
vec![]
}
fn sign_share(
&mut self,
params: &MultisigView<C>,
nonce_sum: C::G,
_: C::F,
nonce: C::F,
msg: &[u8],
) -> C::F {
let c = H::hram(&nonce_sum, &params.group_key(), msg);
self.c = Some(c);
nonce + (params.secret_share() * c)
}
fn verify(&self, group_key: C::G, nonce: C::G, sum: C::F) -> Option<Self::Signature> {
if (C::generator_table() * sum) + (C::G::identity() - (group_key * self.c.unwrap())) == nonce {
Some(SchnorrSignature { R: nonce, s: sum })
} else {
None
}
}
fn verify_share(
&self,
verification_share: C::G,
nonce: C::G,
share: C::F,
) -> bool {
(C::generator_table() * share) == (nonce + (verification_share * self.c.unwrap()))
}
}

489
crypto/frost/src/key_gen.rs Normal file
View File

@@ -0,0 +1,489 @@
use core::{convert::TryFrom, cmp::min, fmt};
use rand_core::{RngCore, CryptoRng};
use ff::{Field, PrimeField};
use group::Group;
use crate::{Curve, MultisigParams, MultisigKeys, FrostError};
#[allow(non_snake_case)]
fn challenge<C: Curve>(l: usize, context: &str, R: &[u8], Am: &[u8]) -> C::F {
let mut c = Vec::with_capacity(8 + context.len() + R.len() + Am.len());
c.extend(&u64::try_from(l).unwrap().to_le_bytes());
c.extend(context.as_bytes());
c.extend(R); // R
c.extend(Am); // A of the first commitment, which is what we're proving we have the private key
// for
// m of the rest of the commitments, authenticating them
C::hash_to_F(&c)
}
// 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
// TODO: This potentially could return a much more robust serialized message, including a signature
// of its entirety. The issue is it can't use its own key as it has no chain of custody behind it.
// While we could ask for a key to be passed in, explicitly declaring the needed for authenticated
// communications in the API itself, systems will likely already provide a authenticated
// communication method making this redundant. It also doesn't guarantee the system which passed
// the key is correctly using it, meaning we can only minimize risk so much
// One notable improvement would be to include the index in the message. While the system must
// still track this to determine if it's ready for the next step, and to remove duplicates, it
// would ensure no counterparties presume the same index and this system didn't mislabel a
// counterparty
fn generate_key_r1<R: RngCore + CryptoRng, C: Curve>(
rng: &mut R,
params: &MultisigParams,
context: &str,
) -> (Vec<C::F>, Vec<C::G>, Vec<u8>) {
let mut coefficients = Vec::with_capacity(params.t);
let mut commitments = Vec::with_capacity(params.t);
let mut serialized = Vec::with_capacity((C::G_len() * params.t) + C::G_len() + C::F_len());
for j in 0 .. params.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[j]);
// Serialize them for publication
serialized.extend(&C::G_to_bytes(&commitments[j]));
}
// Step 2: Provide a proof of knowledge
// This can be deterministic as the PoK is a singleton never opened up to cooperative discussion
// There's also no reason to spend the time and effort to make this deterministic besides a
// general obsession with canonicity and determinism
let k = C::F::random(rng);
#[allow(non_snake_case)]
let R = C::generator_table() * k;
let c = challenge::<C>(params.i, context, &C::G_to_bytes(&R), &serialized);
let s = k + (coefficients[0] * c);
serialized.extend(&C::G_to_bytes(&R));
serialized.extend(&C::F_to_le_bytes(&s));
// Step 4: Broadcast
(coefficients, commitments, serialized)
}
// Verify the received data from the first round of key generation
fn verify_r1<R: RngCore + CryptoRng, C: Curve>(
rng: &mut R,
params: &MultisigParams,
context: &str,
our_commitments: Vec<C::G>,
serialized: &[Vec<u8>],
) -> Result<Vec<Vec<C::G>>, FrostError> {
// Deserialize all of the commitments, validating the input buffers as needed
if serialized.len() != (params.n + 1) {
Err(
// Prevents a panic if serialized.len() == 0
FrostError::InvalidParticipantQuantity(params.n, serialized.len() - min(1, serialized.len()))
)?;
}
// Expect a null set of commitments for index 0 so the vector is guaranteed to line up with
// actual indexes. Even if we did the offset internally, the system would need to write the vec
// with the same offset in mind. Therefore, this trick which is probably slightly less efficient
// yet keeps everything simple is preferred
if serialized[0] != vec![] {
Err(FrostError::NonEmptyParticipantZero)?;
}
let commitments_len = params.t * C::G_len();
let mut commitments = Vec::with_capacity(params.n + 1);
commitments.push(vec![]);
let signature_len = C::G_len() + C::F_len();
let mut first = true;
let mut scalars = Vec::with_capacity((params.n - 1) * 3);
let mut points = Vec::with_capacity((params.n - 1) * 3);
for l in 1 ..= params.n {
if l == params.i {
if serialized[l].len() != 0 {
Err(FrostError::DuplicatedIndex(l))?;
}
commitments.push(vec![]);
continue;
}
if serialized[l].len() != (commitments_len + signature_len) {
// Return an error with an approximation for how many commitments were included
// Prevents errors if not even the signature was included
if serialized[l].len() < signature_len {
Err(FrostError::InvalidCommitmentQuantity(l, params.t, 0))?;
}
Err(
FrostError::InvalidCommitmentQuantity(
l,
params.t,
// Could technically be x.y despite this returning x, yet any y is negligible
// It could help with debugging to know a partial piece of data was read but this error
// alone should be enough
(serialized[l].len() - signature_len) / C::G_len()
)
)?;
}
commitments.push(Vec::with_capacity(params.t));
for o in 0 .. params.t {
commitments[l].push(
C::G_from_slice(
&serialized[l][(o * C::G_len()) .. ((o + 1) * C::G_len())]
).map_err(|_| FrostError::InvalidCommitment(l))?
);
}
// Step 5: Validate each proof of knowledge (prep)
let mut u = C::F::one();
if !first {
u = C::F::random(&mut *rng);
}
scalars.push(u);
points.push(
C::G_from_slice(
&serialized[l][commitments_len .. commitments_len + C::G_len()]
).map_err(|_| FrostError::InvalidProofOfKnowledge(l))?
);
scalars.push(
-C::F_from_le_slice(
&serialized[l][commitments_len + C::G_len() .. serialized[l].len()]
).map_err(|_| FrostError::InvalidProofOfKnowledge(l))? * u
);
points.push(C::generator());
let c = challenge::<C>(
l,
context,
&serialized[l][commitments_len .. commitments_len + C::G_len()],
&serialized[l][0 .. commitments_len]
);
if first {
scalars.push(c);
first = false;
} else {
scalars.push(c * u);
}
points.push(commitments[l][0]);
}
// Step 5: Implementation
// Uses batch verification to optimize the success case dramatically
// On failure, the cost is now this + blame, yet that should happen infrequently
if C::multiexp_vartime(&scalars, &points) != C::G::identity() {
for l in 1 ..= params.n {
if l == params.i {
continue;
}
#[allow(non_snake_case)]
let R = C::G_from_slice(
&serialized[l][commitments_len .. commitments_len + C::G_len()]
).map_err(|_| FrostError::InvalidProofOfKnowledge(l))?;
let s = C::F_from_le_slice(
&serialized[l][commitments_len + C::G_len() .. serialized[l].len()]
).map_err(|_| FrostError::InvalidProofOfKnowledge(l))?;
let c = challenge::<C>(
l,
context,
&serialized[l][commitments_len .. commitments_len + C::G_len()],
&serialized[l][0 .. commitments_len]
);
if R != ((C::generator_table() * s) + (commitments[l][0] * (C::F::zero() - &c))) {
Err(FrostError::InvalidProofOfKnowledge(l))?;
}
}
Err(FrostError::InternalError("batch validation is broken".to_string()))?;
}
// Write in our own commitments
commitments[params.i] = our_commitments;
Ok(commitments)
}
fn polynomial<F: PrimeField>(
coefficients: &[F],
i: usize
) -> F {
let i = F::from(u64::try_from(i).unwrap());
let mut share = F::zero();
for (idx, coefficient) in coefficients.iter().rev().enumerate() {
share += coefficient;
if idx != (coefficients.len() - 1) {
share *= i;
}
}
share
}
// Implements round 1, step 5 and round 2, step 1 of FROST key generation
// Returns our secret share part, commitments for the next step, and a vector for each
// counterparty to receive
fn generate_key_r2<R: RngCore + CryptoRng, C: Curve>(
rng: &mut R,
params: &MultisigParams,
context: &str,
coefficients: Vec<C::F>,
our_commitments: Vec<C::G>,
commitments: &[Vec<u8>],
) -> Result<(C::F, Vec<Vec<C::G>>, Vec<Vec<u8>>), FrostError> {
let commitments = verify_r1::<R, C>(rng, params, context, our_commitments, commitments)?;
// Step 1: Generate secret shares for all other parties
let mut res = Vec::with_capacity(params.n + 1);
res.push(vec![]);
for i in 1 ..= params.n {
// Don't push our own to the byte buffer which is meant to be sent around
// An app developer could accidentally send it. Best to keep this black boxed
if i == params.i {
res.push(vec![]);
continue
}
res.push(C::F_to_le_bytes(&polynomial(&coefficients, i)));
}
// Calculate our own share
let share = polynomial(&coefficients, params.i);
// The secret shares are discarded here, not cleared. While any system which leaves its memory
// accessible is likely totally lost already, making the distinction meaningless when the key gen
// system acts as the signer system and therefore actively holds the signing key anyways, it
// should be overwritten with /dev/urandom in the name of security (which still doesn't meet
// requirements for secure data deletion yet those requirements expect hardware access which is
// far past what this library can reasonably counter)
// TODO: Zero out the coefficients
Ok((share, commitments, res))
}
/// Finishes round 2 and returns both the secret share and the serialized public key.
/// This key is not usable until all parties confirm they have completed the protocol without
/// issue, yet simply confirming protocol completion without issue is enough to confirm the same
/// key was generated as long as a lack of duplicated commitments was also confirmed when they were
/// broadcasted initially
fn complete_r2<C: Curve>(
params: MultisigParams,
share: C::F,
commitments: &[Vec<C::G>],
// Vec to preserve ownership
serialized: Vec<Vec<u8>>,
) -> Result<MultisigKeys<C>, FrostError> {
// Step 2. Verify each share
if serialized.len() != (params.n + 1) {
Err(
FrostError::InvalidParticipantQuantity(params.n, serialized.len() - min(1, serialized.len()))
)?;
}
if (commitments[0].len() != 0) || (serialized[0].len() != 0) {
Err(FrostError::NonEmptyParticipantZero)?;
}
// Deserialize them
let mut shares: Vec<C::F> = vec![C::F::zero()];
for i in 1 .. serialized.len() {
if i == params.i {
if serialized[i].len() != 0 {
Err(FrostError::DuplicatedIndex(i))?;
}
shares.push(C::F::zero());
continue;
}
shares.push(C::F_from_le_slice(&serialized[i]).map_err(|_| FrostError::InvalidShare(i))?);
}
for l in 1 ..= params.n {
if l == params.i {
continue;
}
let i_scalar = C::F::from(u64::try_from(params.i).unwrap());
let mut exp = C::F::one();
let mut exps = Vec::with_capacity(params.t);
for _ in 0 .. params.t {
exps.push(exp);
exp *= i_scalar;
}
// Doesn't use multiexp_vartime with -shares[l] due to not being able to push to commitments
if C::multiexp_vartime(&exps, &commitments[l]) != (C::generator_table() * shares[l]) {
Err(FrostError::InvalidCommitment(l))?;
}
}
// TODO: Clear the original share
let mut secret_share = share;
for remote_share in shares {
secret_share += remote_share;
}
let mut verification_shares = vec![C::G::identity()];
for i in 1 ..= params.n {
let mut exps = vec![];
let mut cs = vec![];
for j in 1 ..= params.n {
for k in 0 .. params.t {
let mut exp = C::F::one();
for _ in 0 .. k {
exp *= C::F::from(u64::try_from(i).unwrap());
}
exps.push(exp);
cs.push(commitments[j][k]);
}
}
verification_shares.push(C::multiexp_vartime(&exps, &cs));
}
debug_assert_eq!(
C::generator_table() * secret_share,
verification_shares[params.i]
);
let mut group_key = C::G::identity();
for j in 1 ..= params.n {
group_key += commitments[j][0];
}
// TODO: Clear serialized and shares
Ok(MultisigKeys { params, secret_share, group_key, verification_shares, offset: None } )
}
/// 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,
context: String,
state: State,
coefficients: Option<Vec<C::F>>,
our_commitments: Option<Vec<C::G>>,
secret: Option<C::F>,
commitments: Option<Vec<Vec<C::G>>>
}
impl<C: Curve> StateMachine<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
}
}
/// 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,
rng: &mut R
) -> Result<Vec<u8>, FrostError> {
if self.state != State::Fresh {
Err(FrostError::InvalidKeyGenTransition(State::Fresh, self.state))?;
}
let (coefficients, commitments, serialized) = generate_key_r1::<R, C>(
rng,
&self.params,
&self.context,
);
self.coefficients = Some(coefficients);
self.our_commitments = Some(commitments);
self.state = State::GeneratedCoefficients;
Ok(serialized)
}
/// 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,
rng: &mut R,
commitments: Vec<Vec<u8>>,
) -> Result<Vec<Vec<u8>>, FrostError> {
if self.state != State::GeneratedCoefficients {
Err(FrostError::InvalidKeyGenTransition(State::GeneratedCoefficients, self.state))?;
}
let (secret, commitments, shares) = generate_key_r2::<R, C>(
rng,
&self.params,
&self.context,
self.coefficients.take().unwrap(),
self.our_commitments.take().unwrap(),
&commitments,
)?;
self.secret = Some(secret);
self.commitments = Some(commitments);
self.state = State::GeneratedSecretShares;
Ok(shares)
}
/// 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
/// group's public key, while setting a valid secret share inside the machine. > t participants
/// 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(
&mut self,
shares: Vec<Vec<u8>>,
) -> Result<MultisigKeys<C>, FrostError> {
if self.state != State::GeneratedSecretShares {
Err(FrostError::InvalidKeyGenTransition(State::GeneratedSecretShares, self.state))?;
}
let keys = complete_r2(
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
}
}

439
crypto/frost/src/lib.rs Normal file
View File

@@ -0,0 +1,439 @@
use core::{ops::Mul, fmt::Debug};
use ff::{Field, PrimeField};
use group::{Group, GroupOps, ScalarMul};
use thiserror::Error;
pub mod key_gen;
pub mod algorithm;
pub mod sign;
use sign::lagrange;
/// Set of errors for curve-related operations, namely encoding and decoding
#[derive(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 + GroupOps + ScalarMul<Self::F>;
/// 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;
/// Multiexponentation function, presumably Straus or Pippenger
/// This library does provide an implementation of Straus which should increase key generation
/// performance by around 4x, also named multiexp_vartime, with the same API. However, if a more
/// performant implementation is available, that should be used instead
// This could also be written as -> Option<C::G> with None for not implemented
fn multiexp_vartime(scalars: &[Self::F], points: &[Self::G]) -> Self::G;
/// Hash the message as needed to calculate 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, albeit potentially with
// incompatibilities between FROST implementations
// It is kept here as Curve + HRAM is effectively a ciphersuite according to the IETF draft
// and moving it to Schnorr would force all of them into being ciphersuite-specific
fn hash_msg(msg: &[u8]) -> Vec<u8>;
/// Field element from hash, used in key generation and to calculate the binding factor
/// H1 from the IETF draft
/// Key generation uses it as if it's H2 to generate a challenge for a Proof of Knowledge
#[allow(non_snake_case)]
fn hash_to_F(data: &[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
/// 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. Should be canonical
// Required due to the lack of standardized encoding functions provided by ff/group
// While they do technically exist, their usage of Self::Repr breaks all potential library usage
// without helper functions like this
#[allow(non_snake_case)]
fn F_from_le_slice(slice: &[u8]) -> Result<Self::F, CurveError>;
/// Group element from slice. Should be canonical
#[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_le_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 {
/// Participants needed to sign on behalf of the group
t: usize,
/// Amount of participants
n: usize,
/// Index of the participant being acted for
i: usize,
}
impl MultisigParams {
pub fn new(
t: usize,
n: usize,
i: usize
) -> Result<MultisigParams, FrostError> {
if (t == 0) || (n == 0) {
Err(FrostError::ZeroParameter(t, n))?;
}
if u64::try_from(n).is_err() {
Err(FrostError::TooManyParticipants(n, u64::MAX))?;
}
// When t == n, this shouldn't be used (MuSig2 and other variants of MuSig exist for a reason),
// but it's not invalid to do so
if t > n {
Err(FrostError::InvalidRequiredQuantity(t, n))?;
}
if (i == 0) || (i > n) {
Err(FrostError::InvalidParticipantIndex(n, i))?;
}
Ok(MultisigParams{ t, n, i })
}
pub fn t(&self) -> usize { self.t }
pub fn n(&self) -> usize { self.n }
pub fn i(&self) -> usize { self.i }
}
#[derive(Error, Debug)]
pub enum FrostError {
#[error("a parameter was 0 (required {0}, participants {1})")]
ZeroParameter(usize, usize),
#[error("too many participants (max {1}, got {0})")]
TooManyParticipants(usize, u64),
#[error("invalid amount of required participants (max {1}, got {0})")]
InvalidRequiredQuantity(usize, usize),
#[error("invalid participant index (0 < index <= {0}, yet index is {1})")]
InvalidParticipantIndex(usize, usize),
#[error("invalid signing set ({0})")]
InvalidSigningSet(String),
#[error("invalid participant quantity (expected {0}, got {1})")]
InvalidParticipantQuantity(usize, usize),
#[error("duplicated participant index ({0})")]
DuplicatedIndex(usize),
#[error("participant 0 provided data despite not existing")]
NonEmptyParticipantZero,
#[error("invalid commitment quantity (participant {0}, expected {1}, got {2})")]
InvalidCommitmentQuantity(usize, usize, usize),
#[error("invalid commitment (participant {0})")]
InvalidCommitment(usize),
#[error("invalid proof of knowledge (participant {0})")]
InvalidProofOfKnowledge(usize),
#[error("invalid share (participant {0})")]
InvalidShare(usize),
#[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),
}
// View of keys passable to algorithm implementations
#[derive(Clone)]
pub struct MultisigView<C: Curve> {
group_key: C::G,
included: Vec<usize>,
secret_share: C::F,
verification_shares: Vec<C::G>,
}
impl<C: Curve> MultisigView<C> {
pub fn group_key(&self) -> C::G {
self.group_key
}
pub fn included(&self) -> Vec<usize> {
self.included.clone()
}
pub fn secret_share(&self) -> C::F {
self.secret_share
}
pub fn verification_share(&self, l: usize) -> C::G {
self.verification_shares[l]
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct MultisigKeys<C: Curve> {
/// Multisig Parameters
params: MultisigParams,
/// Secret share key
secret_share: C::F,
/// Group key
group_key: C::G,
/// Verification shares
verification_shares: Vec<C::G>,
/// Offset applied to these keys
offset: Option<C::F>,
}
impl<C: Curve> MultisigKeys<C> {
pub fn offset(&self, offset: C::F) -> MultisigKeys<C> {
let mut res = self.clone();
res.offset = Some(offset);
res
}
pub fn params(&self) -> MultisigParams {
self.params
}
pub fn secret_share(&self) -> C::F {
self.secret_share
}
pub fn group_key(&self) -> C::G {
self.group_key
}
pub fn verification_shares(&self) -> Vec<C::G> {
self.verification_shares.clone()
}
pub fn view(&self, included: &[usize]) -> Result<MultisigView<C>, FrostError> {
if (included.len() < self.params.t) || (self.params.n < included.len()) {
Err(FrostError::InvalidSigningSet("invalid amount of participants included".to_string()))?;
}
let secret_share = self.secret_share * lagrange::<C::F>(self.params.i, &included);
let (offset, offset_share) = if self.offset.is_some() {
let offset = self.offset.unwrap();
(offset, offset * C::F::from(included.len().try_into().unwrap()).invert().unwrap())
} else {
(C::F::zero(), C::F::zero())
};
Ok(MultisigView {
group_key: self.group_key + (C::generator_table() * offset),
secret_share: secret_share + offset_share,
verification_shares: self.verification_shares.clone().iter().enumerate().map(
|(l, share)| (*share * lagrange::<C::F>(l, &included)) +
(C::generator_table() * offset_share)
).collect(),
included: included.to_vec(),
})
}
pub fn serialized_len(n: usize) -> usize {
1 + usize::from(C::id_len()) + (3 * 8) + C::F_len() + C::G_len() + (n * C::G_len())
}
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 as u64).to_le_bytes());
serialized.extend(&(self.params.t as u64).to_le_bytes());
serialized.extend(&(self.params.i as u64).to_le_bytes());
serialized.extend(&C::F_to_le_bytes(&self.secret_share));
serialized.extend(&C::G_to_bytes(&self.group_key));
for i in 1 ..= self.params.n {
serialized.extend(&C::G_to_bytes(&self.verification_shares[i]));
}
serialized
}
pub fn deserialize(serialized: &[u8]) -> Result<MultisigKeys<C>, FrostError> {
if serialized.len() < 1 {
Err(FrostError::InternalError("MultisigKeys serialization is empty".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 {
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 = u64::from_le_bytes(serialized[cursor .. (cursor + 8)].try_into().unwrap()).try_into()
.map_err(|_| FrostError::InternalError("parameter doesn't fit into usize".to_string()))?;
cursor += 8;
if serialized.len() != MultisigKeys::<C>::serialized_len(n) {
Err(FrostError::InternalError("incorrect serialization length".to_string()))?;
}
let t = u64::from_le_bytes(serialized[cursor .. (cursor + 8)].try_into().unwrap()).try_into()
.map_err(|_| FrostError::InternalError("parameter doesn't fit into usize".to_string()))?;
cursor += 8;
let i = u64::from_le_bytes(serialized[cursor .. (cursor + 8)].try_into().unwrap()).try_into()
.map_err(|_| FrostError::InternalError("parameter doesn't fit into usize".to_string()))?;
cursor += 8;
let secret_share = C::F_from_le_slice(&serialized[cursor .. (cursor + C::F_len())])
.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())])
.map_err(|_| FrostError::InternalError("invalid group key".to_string()))?;
cursor += C::G_len();
let mut verification_shares = vec![C::G::identity()];
verification_shares.reserve_exact(n + 1);
for _ in 0 .. n {
verification_shares.push(
C::G_from_slice(&serialized[cursor .. (cursor + C::G_len())])
.map_err(|_| FrostError::InternalError("invalid verification share".to_string()))?
);
cursor += C::G_len();
}
Ok(
MultisigKeys {
params: MultisigParams::new(t, n, i)
.map_err(|_| FrostError::InternalError("invalid parameters".to_string()))?,
secret_share,
group_key,
verification_shares,
offset: None
}
)
}
}
/*
An implementation of Straus, which should be more efficient than Pippenger for the expected amount
of points
Completing key generation from the round 2 messages takes:
- Naive
Completed 33-of-50 in 2.66s
Completed 5-of-8 in 11.05ms
- crate Straus
Completed 33-of-50 in 730-833ms (extremely notable effects from taking variable time)
Completed 5-of-8 in 2.8ms
- dalek VartimeMultiscalarMul
Completed 33-of-50 in 266ms
Completed 5-of-8 in 1.6ms
This does show this algorithm isn't appropriately tuned (and potentially isn't even the right
choice), at least with that quantity. Unfortunately, we can't use dalek's multiexp implementation
everywhere, and this does work
*/
pub fn multiexp_vartime<C: Curve>(scalars: &[C::F], points: &[C::G]) -> C::G {
let mut tables = vec![];
// dalek uses 8 in their impl, along with a carry scheme where values are [-8, 8)
// Moving to a similar system here did save a marginal amount, yet not one significant enough for
// its pain (as some fields do have scalars which can have their top bit set, a scenario dalek
// assumes is never true)
tables.resize(points.len(), Vec::with_capacity(15));
for p in 0 .. points.len() {
let mut accum = C::G::identity();
tables[p].push(accum);
for _ in 0 .. 15 {
accum += points[p];
tables[p].push(accum);
}
}
let mut nibbles = vec![];
nibbles.resize(scalars.len(), vec![]);
for s in 0 .. scalars.len() {
let bytes = C::F_to_le_bytes(&scalars[s]);
nibbles[s].resize(C::F_len() * 2, 0);
for i in 0 .. bytes.len() {
nibbles[s][i * 2] = bytes[i] & 0b1111;
nibbles[s][(i * 2) + 1] = (bytes[i] >> 4) & 0b1111;
}
}
let mut res = C::G::identity();
for b in (0 .. (C::F_len() * 2)).rev() {
for _ in 0 .. 4 {
res = res.double();
}
for s in 0 .. scalars.len() {
// This creates a 250% performance increase on key gen, which uses a bunch of very low
// scalars. This is why this function is now committed to being vartime
if nibbles[s][b] != 0 {
res += tables[s][nibbles[s][b] as usize];
}
}
}
res
}

475
crypto/frost/src/sign.rs Normal file
View File

@@ -0,0 +1,475 @@
use core::{convert::TryFrom, cmp::min, fmt};
use std::rc::Rc;
use rand_core::{RngCore, CryptoRng};
use ff::{Field, PrimeField};
use group::Group;
use crate::{Curve, FrostError, MultisigParams, MultisigKeys, MultisigView, algorithm::Algorithm};
/// Calculate the lagrange coefficient
pub fn lagrange<F: PrimeField>(
i: usize,
included: &[usize],
) -> F {
let mut num = F::one();
let mut denom = F::one();
for l in included {
if i == *l {
continue;
}
let share = F::from(u64::try_from(*l).unwrap());
num *= share;
denom *= share - F::from(u64::try_from(i).unwrap());
}
// Safe as this will only be 0 if we're part of the above loop
// (which we have an if case to avoid)
num * denom.invert().unwrap()
}
/// Pairing of an Algorithm with a MultisigKeys 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>,
}
// 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>>,
included: &[usize],
) -> Result<Params<C, A>, FrostError> {
let mut included = included.to_vec();
(&mut included).sort_unstable();
// Included < threshold
if included.len() < keys.params.t {
Err(FrostError::InvalidSigningSet("not enough signers".to_string()))?;
}
// Invalid index
if included[0] == 0 {
Err(FrostError::InvalidParticipantIndex(included[0], keys.params.n))?;
}
// OOB index
if included[included.len() - 1] > keys.params.n {
Err(FrostError::InvalidParticipantIndex(included[included.len() - 1], keys.params.n))?;
}
// Same signer included multiple times
for i in 0 .. included.len() - 1 {
if included[i] == included[i + 1] {
Err(FrostError::DuplicatedIndex(included[i]))?;
}
}
// Not included
if !included.contains(&keys.params.i) {
Err(FrostError::InvalidSigningSet("signing despite not being included".to_string()))?;
}
// Out of order arguments to prevent additional cloning
Ok(Params { algorithm, view: keys.view(&included).unwrap(), keys })
}
pub fn multisig_params(&self) -> MultisigParams {
self.keys.params
}
pub fn view(&self) -> MultisigView<C> {
self.view.clone()
}
}
struct PreprocessPackage<C: Curve> {
nonces: [C::F; 2],
commitments: [C::G; 2],
serialized: Vec<u8>,
}
// This library unifies the preprocessing step with signing due to security concerns and to provide
// a simpler UX
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]));
serialized.extend(
&A::preprocess_addendum(
rng,
&params.view,
&nonces
)
);
PreprocessPackage { nonces, commitments, serialized }
}
#[allow(non_snake_case)]
struct Package<C: Curve> {
Ris: Vec<C::G>,
R: C::G,
share: C::F
}
// Has every signer perform the role of the signature aggregator
// Step 1 was already deprecated by performing nonce generation as needed
// Step 2 is simply the broadcast round from step 1
fn sign_with_share<C: Curve, A: Algorithm<C>>(
params: &mut Params<C, A>,
our_preprocess: PreprocessPackage<C>,
commitments: &[Option<Vec<u8>>],
msg: &[u8],
) -> Result<(Package<C>, Vec<u8>), FrostError> {
let multisig_params = params.multisig_params();
if commitments.len() != (multisig_params.n + 1) {
Err(
FrostError::InvalidParticipantQuantity(
multisig_params.n,
commitments.len() - min(1, commitments.len())
)
)?;
}
if commitments[0].is_some() {
Err(FrostError::NonEmptyParticipantZero)?;
}
let commitments_len = C::G_len() * 2;
// Allow algorithms to commit to more data than just the included nonces
// Not IETF draft compliant yet it doesn't prevent a compliant Schnorr algorithm from being used
// with this library, which does ship one
let commit_len = commitments_len + A::addendum_commit_len();
#[allow(non_snake_case)]
let mut B = Vec::with_capacity(multisig_params.n + 1);
B.push(None);
// Commitments + a presumed 32-byte hash of the message
let mut b: Vec<u8> = Vec::with_capacity((multisig_params.t * 2 * C::G_len()) + 32);
// Parse the commitments and prepare the binding factor
for l in 1 ..= multisig_params.n {
if l == multisig_params.i {
if commitments[l].is_some() {
Err(FrostError::DuplicatedIndex(l))?;
}
B.push(Some(our_preprocess.commitments));
b.extend(&u16::try_from(l).unwrap().to_le_bytes());
b.extend(&our_preprocess.serialized[0 .. commit_len]);
continue;
}
let included = params.view.included.contains(&l);
if commitments[l].is_some() && (!included) {
Err(FrostError::InvalidCommitmentQuantity(l, 0, commitments.len() / C::G_len()))?;
}
if commitments[l].is_none() {
if included {
Err(FrostError::InvalidCommitmentQuantity(l, 2, 0))?;
}
B.push(None);
continue;
}
let commitments = commitments[l].as_ref().unwrap();
if commitments.len() < commitments_len {
Err(FrostError::InvalidCommitmentQuantity(l, 2, commitments.len() / C::G_len()))?;
}
#[allow(non_snake_case)]
let D = C::G_from_slice(&commitments[0 .. C::G_len()])
.map_err(|_| FrostError::InvalidCommitment(l))?;
#[allow(non_snake_case)]
let E = C::G_from_slice(&commitments[C::G_len() .. commitments_len])
.map_err(|_| FrostError::InvalidCommitment(l))?;
B.push(Some([D, E]));
b.extend(&u16::try_from(l).unwrap().to_le_bytes());
b.extend(&commitments[0 .. commit_len]);
}
// Process the commitments and addendums
let view = &params.view;
for l in &params.view.included {
params.algorithm.process_addendum(
view,
*l,
B[*l].as_ref().unwrap(),
if *l == multisig_params.i {
&our_preprocess.serialized[commitments_len .. our_preprocess.serialized.len()]
} else {
&commitments[*l].as_ref().unwrap()[
commitments_len .. commitments[*l].as_ref().unwrap().len()
]
}
)?;
}
// Finish the binding factor
b.extend(&C::hash_msg(&msg));
// If the following are used with certain lengths, it is possible to craft distinct
// commitments/messages/contexts with the same binding factor. While we can't length prefix the
// commitments, unfortunately, we can tag and length prefix the following
// If the offset functionality provided by this library is in use, include it in the binding
// factor. Not compliant with the IETF spec which doesn't have a concept of offsets
if params.keys.offset.is_some() {
b.extend(b"offset");
b.extend(u64::try_from(C::F_len()).unwrap().to_le_bytes());
b.extend(&C::F_to_le_bytes(&params.keys.offset.unwrap()));
}
// Also include any context the algorithm may want to specify. Again not compliant with the IETF
// spec which doesn't considered there may be signatures other than Schnorr being generated with
// FROST
let context = params.algorithm.context();
if context.len() != 0 {
b.extend(b"context");
b.extend(u64::try_from(context.len()).unwrap().to_le_bytes());
b.extend(&context);
}
let b = C::hash_to_F(&b);
#[allow(non_snake_case)]
let mut Ris = vec![];
#[allow(non_snake_case)]
let mut R = C::G::identity();
for i in 0 .. params.view.included.len() {
let commitments = B[params.view.included[i]].unwrap();
#[allow(non_snake_case)]
let this_R = commitments[0] + (commitments[1] * b);
Ris.push(this_R);
R += this_R;
}
let view = &params.view;
let share = params.algorithm.sign_share(
view,
R,
b,
our_preprocess.nonces[0] + (our_preprocess.nonces[1] * b),
msg
);
Ok((Package { Ris, R, share }, C::F_to_le_bytes(&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>,
serialized: &[Option<Vec<u8>>],
) -> Result<A::Signature, FrostError> {
let params = sign_params.multisig_params();
if serialized.len() != (params.n + 1) {
Err(
FrostError::InvalidParticipantQuantity(params.n, serialized.len() - min(1, serialized.len()))
)?;
}
if serialized[0].is_some() {
Err(FrostError::NonEmptyParticipantZero)?;
}
let mut responses = Vec::with_capacity(params.t);
let mut sum = sign.share;
for i in 0 .. sign_params.view.included.len() {
let l = sign_params.view.included[i];
if l == params.i {
responses.push(None);
continue;
}
// Make sure they actually provided a share
if serialized[l].is_none() {
Err(FrostError::InvalidShare(l))?;
}
let part = C::F_from_le_slice(serialized[l].as_ref().unwrap())
.map_err(|_| FrostError::InvalidShare(l))?;
sum += part;
responses.push(Some(part));
}
// 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);
if res.is_some() {
return Ok(res.unwrap());
}
// Find out who misbehaved
for i in 0 .. sign_params.view.included.len() {
match responses[i] {
Some(part) => {
let l = sign_params.view.included[i];
if !sign_params.algorithm.verify_share(
sign_params.view.verification_share(l),
sign.Ris[i],
part
) {
Err(FrostError::InvalidShare(l))?;
}
},
// Happens when l == i
None => {}
}
}
// If everyone has a valid share and there were enough participants, this should've worked
Err(
FrostError::InternalError(
"everyone had a valid share yet the signature was still invalid".to_string()
)
)
}
/// 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 {
type 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,
rng: &mut R
) -> Result<Vec<u8>, FrostError>;
/// Sign a message
/// Takes in the participant's commitments, which are expected to be in a Vec where participant
/// index = Vec index. None is expected at index 0 to allow for this. None is also expected at
/// 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,
commitments: &[Option<Vec<u8>>],
msg: &[u8],
) -> Result<Vec<u8>, FrostError>;
/// 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: &[Option<Vec<u8>>]) -> Result<Self::Signature, FrostError>;
fn multisig_params(&self) -> MultisigParams;
fn state(&self) -> State;
}
/// 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>,
state: State,
preprocess: Option<PreprocessPackage<C>>,
sign: Option<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>>,
included: &[usize],
) -> Result<AlgorithmMachine<C, A>, FrostError> {
Ok(
AlgorithmMachine {
params: Params::new(algorithm, keys, included)?,
state: State::Fresh,
preprocess: None,
sign: None,
}
)
}
}
impl<C: Curve, A: Algorithm<C>> StateMachine for AlgorithmMachine<C, A> {
type Signature = A::Signature;
fn preprocess<R: RngCore + CryptoRng>(
&mut 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);
let serialized = preprocess.serialized.clone();
self.preprocess = Some(preprocess);
self.state = State::Preprocessed;
Ok(serialized)
}
fn sign(
&mut self,
commitments: &[Option<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: &[Option<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
}
}

View File

@@ -0,0 +1,112 @@
use core::convert::TryInto;
use digest::Digest;
use ff::PrimeField;
use group::GroupEncoding;
use sha2::{Sha256, Sha512};
use k256::{
elliptic_curve::{generic_array::GenericArray, bigint::{ArrayEncoding, U512}, ops::Reduce},
Scalar,
ProjectivePoint
};
use frost::{CurveError, Curve, multiexp_vartime, algorithm::Hram};
#[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 {
Self::id().len() as u8
}
fn generator() -> Self::G {
Self::G::GENERATOR
}
fn generator_table() -> Self::T {
Self::G::GENERATOR
}
fn multiexp_vartime(scalars: &[Self::F], points: &[Self::G]) -> Self::G {
multiexp_vartime::<Secp256k1>(scalars, points)
}
// The IETF draft doesn't specify a secp256k1 ciphersuite
// This test just uses the simplest ciphersuite which would still be viable to deploy
fn hash_msg(msg: &[u8]) -> Vec<u8> {
(&Sha256::digest(msg)).to_vec()
}
// Use wide reduction for security
fn hash_to_F(data: &[u8]) -> Self::F {
Scalar::from_uint_reduced(
U512::from_be_byte_array(Sha512::new().chain_update("rho").chain_update(data).finalize())
)
}
fn F_len() -> usize {
32
}
fn G_len() -> usize {
33
}
fn F_from_le_slice(slice: &[u8]) -> Result<Self::F, CurveError> {
let mut bytes: [u8; 32] = slice.try_into().map_err(
|_| CurveError::InvalidLength(32, slice.len())
)?;
bytes.reverse();
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_le_bytes(f: &Self::F) -> Vec<u8> {
let mut res: [u8; 32] = f.to_bytes().into();
res.reverse();
res.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()
)
)
}
}

View File

@@ -0,0 +1,144 @@
use std::rc::Rc;
use rand::{RngCore, rngs::OsRng};
use digest::Digest;
use sha2::Sha256;
use frost::{
Curve,
MultisigParams, MultisigKeys,
key_gen,
algorithm::{Algorithm, Schnorr, SchnorrSignature},
sign::{StateMachine, AlgorithmMachine}
};
mod common;
use common::{Secp256k1, TestHram};
const PARTICIPANTS: usize = 8;
fn sign<C: Curve, A: Algorithm<C, Signature = SchnorrSignature<C>>>(
algorithm: A,
keys: Vec<Rc<MultisigKeys<C>>>
) {
let t = keys[0].params().t();
let mut machines = vec![];
let mut commitments = Vec::with_capacity(PARTICIPANTS + 1);
commitments.resize(PARTICIPANTS + 1, None);
for i in 1 ..= t {
machines.push(
AlgorithmMachine::new(
algorithm.clone(),
keys[i - 1].clone(),
&(1 ..= t).collect::<Vec<usize>>()
).unwrap()
);
commitments[i] = Some(machines[i - 1].preprocess(&mut OsRng).unwrap());
}
let mut shares = Vec::with_capacity(PARTICIPANTS + 1);
shares.resize(PARTICIPANTS + 1, None);
for i in 1 ..= t {
shares[i] = Some(
machines[i - 1].sign(
&commitments
.iter()
.enumerate()
.map(|(idx, value)| if idx == i { None } else { value.to_owned() })
.collect::<Vec<Option<Vec<u8>>>>(),
b"Hello World"
).unwrap()
);
}
let mut signature = None;
for i in 1 ..= t {
let sig = machines[i - 1].complete(
&shares
.iter()
.enumerate()
.map(|(idx, value)| if idx == i { None } else { value.to_owned() })
.collect::<Vec<Option<Vec<u8>>>>()
).unwrap();
if signature.is_none() {
signature = Some(sig);
}
assert_eq!(sig, signature.unwrap());
}
}
#[test]
fn key_gen_and_sign() {
let mut params = vec![];
let mut machines = vec![];
let mut commitments = vec![vec![]];
for i in 1 ..= PARTICIPANTS {
params.push(
MultisigParams::new(
((PARTICIPANTS / 3) * 2) + 1,
PARTICIPANTS,
i
).unwrap()
);
machines.push(
key_gen::StateMachine::<Secp256k1>::new(
params[i - 1],
"FF/Group Rust key_gen test".to_string()
)
);
commitments.push(machines[i - 1].generate_coefficients(&mut OsRng).unwrap());
}
let mut secret_shares = vec![];
for i in 1 ..= PARTICIPANTS {
secret_shares.push(
machines[i - 1].generate_secret_shares(
&mut OsRng,
commitments
.iter()
.enumerate()
.map(|(idx, commitments)| if idx == i { vec![] } else { commitments.to_vec() })
.collect()
).unwrap()
);
}
let mut verification_shares = vec![];
let mut group_key = None;
let mut keys = vec![];
for i in 1 ..= PARTICIPANTS {
let mut our_secret_shares = vec![vec![]];
our_secret_shares.extend(
secret_shares.iter().map(|shares| shares[i].clone()).collect::<Vec<Vec<u8>>>()
);
let these_keys = machines[i - 1].complete(our_secret_shares).unwrap();
assert_eq!(
MultisigKeys::<Secp256k1>::deserialize(&these_keys.serialize()).unwrap(),
these_keys
);
keys.push(Rc::new(these_keys.clone()));
if verification_shares.len() == 0 {
verification_shares = these_keys.verification_shares();
}
assert_eq!(verification_shares, these_keys.verification_shares());
if group_key.is_none() {
group_key = Some(these_keys.group_key());
}
assert_eq!(group_key.unwrap(), these_keys.group_key());
}
sign(Schnorr::<Secp256k1, TestHram>::new(), keys.clone());
let mut randomization = [0; 64];
(&mut OsRng).fill_bytes(&mut randomization);
sign(
Schnorr::<Secp256k1, TestHram>::new(),
keys.iter().map(
|keys| Rc::new(keys.offset(Secp256k1::hash_to_F(&Sha256::digest(&randomization))))
).collect()
);
}