remove frost module (#568)
This commit is contained in:
parent
71c092532c
commit
a0df08e30a
|
@ -4,8 +4,11 @@ Entries are listed in reverse chronological order.
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
## 0.8.0
|
## 1.0.0-rc.0
|
||||||
|
|
||||||
|
* The `frost-core::frost` module contents were merged into `frost-core`, thus
|
||||||
|
eliminating the `frost` module. You can adapt any calling code with e.g.
|
||||||
|
changing `use frost_core::frost::*` to `use frost-core::*`.
|
||||||
* Both serde serialization and the default byte-oriented serialization now
|
* Both serde serialization and the default byte-oriented serialization now
|
||||||
include a version field (a u8) at the beginning which is always 0 for now. The
|
include a version field (a u8) at the beginning which is always 0 for now. The
|
||||||
ciphersuite ID field was moved from the last field to the second field, after
|
ciphersuite ID field was moved from the last field to the second field, after
|
||||||
|
|
|
@ -2,12 +2,20 @@
|
||||||
|
|
||||||
Base traits and types in Rust that implement ['Two-Round Threshold Schnorr Signatures with
|
Base traits and types in Rust that implement ['Two-Round Threshold Schnorr Signatures with
|
||||||
FROST'](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/) generically for
|
FROST'](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost/) generically for
|
||||||
`frost-core::Ciphersuite` implementations.
|
[`Ciphersuite`] implementations.
|
||||||
|
|
||||||
|
For key generation, refer to the [`keys`] module. For round-specific
|
||||||
|
types and functions, refer to the [`round1`] and [`round2`] modules. This module
|
||||||
|
contains types and functions not directly related to key generation and the
|
||||||
|
FROST rounds.
|
||||||
|
|
||||||
|
|
||||||
## Status ⚠
|
## Status ⚠
|
||||||
|
|
||||||
The FROST specification is not yet finalized, and this codebase has not yet been audited or
|
The FROST specification is not yet finalized, though no significant changes are
|
||||||
released. The APIs and types in `frost-core` are subject to change.
|
expected at this point. This code base has been audited by NCC. The APIs and
|
||||||
|
types in `frost-core` are subject to change during the release candidate phase,
|
||||||
|
and will follow SemVer guarantees after 1.0.0.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,8 @@ use std::collections::BTreeMap;
|
||||||
use criterion::{BenchmarkId, Criterion, Throughput};
|
use criterion::{BenchmarkId, Criterion, Throughput};
|
||||||
use rand_core::{CryptoRng, RngCore};
|
use rand_core::{CryptoRng, RngCore};
|
||||||
|
|
||||||
use crate::{batch, frost, Ciphersuite, Signature, SigningKey, VerifyingKey};
|
use crate as frost;
|
||||||
|
use crate::{batch, Ciphersuite, Signature, SigningKey, VerifyingKey};
|
||||||
|
|
||||||
struct Item<C: Ciphersuite> {
|
struct Item<C: Ciphersuite> {
|
||||||
vk: VerifyingKey<C>,
|
vk: VerifyingKey<C>,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{frost::Identifier, Ciphersuite};
|
use crate::{Ciphersuite, Identifier};
|
||||||
|
|
||||||
#[derive(Error, Debug, Clone, Copy, Eq, PartialEq)]
|
#[derive(Error, Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
pub struct ParticipantError<C: Ciphersuite>(Identifier<C>);
|
pub struct ParticipantError<C: Ciphersuite>(Identifier<C>);
|
||||||
|
|
|
@ -1,514 +1 @@
|
||||||
//! An implementation of FROST (Flexible Round-Optimized Schnorr Threshold)
|
|
||||||
//! signatures.
|
|
||||||
//!
|
|
||||||
//! For key generation, refer to the [`keys`] module.
|
|
||||||
//! For round-specific types and functions, refer to the [`round1`] and
|
|
||||||
//! [`round2`] modules.
|
|
||||||
//!
|
|
||||||
//! This module contains types and functions not directly related to key
|
|
||||||
//! generation and the FROST rounds.
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
collections::{BTreeMap, BTreeSet},
|
|
||||||
fmt::{self, Debug},
|
|
||||||
};
|
|
||||||
|
|
||||||
use derive_getters::Getters;
|
|
||||||
#[cfg(any(test, feature = "test-impl"))]
|
|
||||||
use hex::FromHex;
|
|
||||||
|
|
||||||
mod identifier;
|
|
||||||
pub mod keys;
|
|
||||||
pub mod round1;
|
|
||||||
pub mod round2;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
scalar_mul::VartimeMultiscalarMul, Ciphersuite, Deserialize, Element, Error, Field, Group,
|
|
||||||
Header, Scalar, Serialize, Signature, VerifyingKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use self::identifier::Identifier;
|
|
||||||
|
|
||||||
/// The binding factor, also known as _rho_ (ρ)
|
|
||||||
///
|
|
||||||
/// Ensures each signature share is strongly bound to a signing set, specific set
|
|
||||||
/// of commitments, and a specific message.
|
|
||||||
///
|
|
||||||
/// <https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md>
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
|
||||||
pub struct BindingFactor<C: Ciphersuite>(Scalar<C>);
|
|
||||||
|
|
||||||
impl<C> BindingFactor<C>
|
|
||||||
where
|
|
||||||
C: Ciphersuite,
|
|
||||||
{
|
|
||||||
/// Deserializes [`BindingFactor`] from bytes.
|
|
||||||
pub fn deserialize(
|
|
||||||
bytes: <<C::Group as Group>::Field as Field>::Serialization,
|
|
||||||
) -> Result<Self, Error<C>> {
|
|
||||||
<<C::Group as Group>::Field>::deserialize(&bytes)
|
|
||||||
.map(|scalar| Self(scalar))
|
|
||||||
.map_err(|e| e.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serializes [`BindingFactor`] to bytes.
|
|
||||||
pub fn serialize(&self) -> <<C::Group as Group>::Field as Field>::Serialization {
|
|
||||||
<<C::Group as Group>::Field>::serialize(&self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C> Debug for BindingFactor<C>
|
|
||||||
where
|
|
||||||
C: Ciphersuite,
|
|
||||||
{
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
f.debug_tuple("BindingFactor")
|
|
||||||
.field(&hex::encode(self.serialize()))
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A list of binding factors and their associated identifiers.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct BindingFactorList<C: Ciphersuite>(BTreeMap<Identifier<C>, BindingFactor<C>>);
|
|
||||||
|
|
||||||
impl<C> BindingFactorList<C>
|
|
||||||
where
|
|
||||||
C: Ciphersuite,
|
|
||||||
{
|
|
||||||
/// Create a new [`BindingFactorList`] from a map of identifiers to binding factors.
|
|
||||||
#[cfg(feature = "internals")]
|
|
||||||
pub fn new(binding_factors: BTreeMap<Identifier<C>, BindingFactor<C>>) -> Self {
|
|
||||||
Self(binding_factors)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return iterator through all factors.
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = (&Identifier<C>, &BindingFactor<C>)> {
|
|
||||||
self.0.iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the [`BindingFactor`] for the given identifier, or None if not found.
|
|
||||||
pub fn get(&self, key: &Identifier<C>) -> Option<&BindingFactor<C>> {
|
|
||||||
self.0.get(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [`compute_binding_factors`] in the spec
|
|
||||||
///
|
|
||||||
/// [`compute_binding_factors`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-4.4
|
|
||||||
#[cfg_attr(feature = "internals", visibility::make(pub))]
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "internals")))]
|
|
||||||
pub(crate) fn compute_binding_factor_list<C>(
|
|
||||||
signing_package: &SigningPackage<C>,
|
|
||||||
verifying_key: &VerifyingKey<C>,
|
|
||||||
additional_prefix: &[u8],
|
|
||||||
) -> BindingFactorList<C>
|
|
||||||
where
|
|
||||||
C: Ciphersuite,
|
|
||||||
{
|
|
||||||
let preimages = signing_package.binding_factor_preimages(verifying_key, additional_prefix);
|
|
||||||
|
|
||||||
BindingFactorList(
|
|
||||||
preimages
|
|
||||||
.iter()
|
|
||||||
.map(|(identifier, preimage)| {
|
|
||||||
let binding_factor = C::H1(preimage);
|
|
||||||
(*identifier, BindingFactor(binding_factor))
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-impl"))]
|
|
||||||
impl<C> FromHex for BindingFactor<C>
|
|
||||||
where
|
|
||||||
C: Ciphersuite,
|
|
||||||
{
|
|
||||||
type Error = &'static str;
|
|
||||||
|
|
||||||
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
|
|
||||||
let v: Vec<u8> = FromHex::from_hex(hex).map_err(|_| "invalid hex")?;
|
|
||||||
match v.try_into() {
|
|
||||||
Ok(bytes) => Self::deserialize(bytes).map_err(|_| "malformed scalar encoding"),
|
|
||||||
Err(_) => Err("malformed scalar encoding"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a lagrange coefficient.
|
|
||||||
///
|
|
||||||
/// The Lagrange polynomial for a set of points (x_j, y_j) for 0 <= j <= k
|
|
||||||
/// is ∑_{i=0}^k y_i.ℓ_i(x), where ℓ_i(x) is the Lagrange basis polynomial:
|
|
||||||
///
|
|
||||||
/// ℓ_i(x) = ∏_{0≤j≤k; j≠i} (x - x_j) / (x_i - x_j).
|
|
||||||
///
|
|
||||||
/// This computes ℓ_j(x) for the set of points `xs` and for the j corresponding
|
|
||||||
/// to the given xj.
|
|
||||||
///
|
|
||||||
/// If `x` is None, it uses 0 for it (since Identifiers can't be 0)
|
|
||||||
#[cfg_attr(feature = "internals", visibility::make(pub))]
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "internals")))]
|
|
||||||
fn compute_lagrange_coefficient<C: Ciphersuite>(
|
|
||||||
x_set: &BTreeSet<Identifier<C>>,
|
|
||||||
x: Option<Identifier<C>>,
|
|
||||||
x_i: Identifier<C>,
|
|
||||||
) -> Result<Scalar<C>, Error<C>> {
|
|
||||||
if x_set.is_empty() {
|
|
||||||
return Err(Error::IncorrectNumberOfIdentifiers);
|
|
||||||
}
|
|
||||||
let mut num = <<C::Group as Group>::Field>::one();
|
|
||||||
let mut den = <<C::Group as Group>::Field>::one();
|
|
||||||
|
|
||||||
let mut x_i_found = false;
|
|
||||||
|
|
||||||
for x_j in x_set.iter() {
|
|
||||||
if x_i == *x_j {
|
|
||||||
x_i_found = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(x) = x {
|
|
||||||
num *= x - *x_j;
|
|
||||||
den *= x_i - *x_j;
|
|
||||||
} else {
|
|
||||||
// Both signs inverted just to avoid requiring Neg (-*xj)
|
|
||||||
num *= *x_j;
|
|
||||||
den *= *x_j - x_i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !x_i_found {
|
|
||||||
return Err(Error::UnknownIdentifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(
|
|
||||||
num * <<C::Group as Group>::Field>::invert(&den)
|
|
||||||
.map_err(|_| Error::DuplicatedIdentifier)?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates the lagrange coefficient for the i'th participant (for `signer_id`).
|
|
||||||
///
|
|
||||||
/// Implements [`derive_interpolating_value()`] from the spec.
|
|
||||||
///
|
|
||||||
/// [`derive_interpolating_value()`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-polynomials
|
|
||||||
#[cfg_attr(feature = "internals", visibility::make(pub))]
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "internals")))]
|
|
||||||
fn derive_interpolating_value<C: Ciphersuite>(
|
|
||||||
signer_id: &Identifier<C>,
|
|
||||||
signing_package: &SigningPackage<C>,
|
|
||||||
) -> Result<Scalar<C>, Error<C>> {
|
|
||||||
compute_lagrange_coefficient(
|
|
||||||
&signing_package
|
|
||||||
.signing_commitments()
|
|
||||||
.keys()
|
|
||||||
.cloned()
|
|
||||||
.collect(),
|
|
||||||
None,
|
|
||||||
*signer_id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generated by the coordinator of the signing operation and distributed to
|
|
||||||
/// each signing party
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Getters)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
||||||
#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))]
|
|
||||||
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
|
|
||||||
pub struct SigningPackage<C: Ciphersuite> {
|
|
||||||
/// Serialization header
|
|
||||||
#[getter(skip)]
|
|
||||||
pub(crate) header: Header<C>,
|
|
||||||
/// The set of commitments participants published in the first round of the
|
|
||||||
/// protocol.
|
|
||||||
signing_commitments: BTreeMap<Identifier<C>, round1::SigningCommitments<C>>,
|
|
||||||
/// Message which each participant will sign.
|
|
||||||
///
|
|
||||||
/// Each signer should perform protocol-specific verification on the
|
|
||||||
/// message.
|
|
||||||
#[cfg_attr(
|
|
||||||
feature = "serde",
|
|
||||||
serde(
|
|
||||||
serialize_with = "serdect::slice::serialize_hex_lower_or_bin",
|
|
||||||
deserialize_with = "serdect::slice::deserialize_hex_or_bin_vec"
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
message: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C> SigningPackage<C>
|
|
||||||
where
|
|
||||||
C: Ciphersuite,
|
|
||||||
{
|
|
||||||
/// Create a new `SigningPackage`
|
|
||||||
///
|
|
||||||
/// The `signing_commitments` are sorted by participant `identifier`.
|
|
||||||
pub fn new(
|
|
||||||
signing_commitments: BTreeMap<Identifier<C>, round1::SigningCommitments<C>>,
|
|
||||||
message: &[u8],
|
|
||||||
) -> SigningPackage<C> {
|
|
||||||
SigningPackage {
|
|
||||||
header: Header::default(),
|
|
||||||
signing_commitments,
|
|
||||||
message: message.to_vec(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a signing commitment by its participant identifier, or None if not found.
|
|
||||||
pub fn signing_commitment(
|
|
||||||
&self,
|
|
||||||
identifier: &Identifier<C>,
|
|
||||||
) -> Option<round1::SigningCommitments<C>> {
|
|
||||||
self.signing_commitments.get(identifier).copied()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute the preimages to H1 to compute the per-signer binding factors
|
|
||||||
// We separate this out into its own method so it can be tested
|
|
||||||
#[cfg_attr(feature = "internals", visibility::make(pub))]
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "internals")))]
|
|
||||||
pub fn binding_factor_preimages(
|
|
||||||
&self,
|
|
||||||
verifying_key: &VerifyingKey<C>,
|
|
||||||
additional_prefix: &[u8],
|
|
||||||
) -> Vec<(Identifier<C>, Vec<u8>)> {
|
|
||||||
let mut binding_factor_input_prefix = vec![];
|
|
||||||
|
|
||||||
// The length of a serialized verifying key of the same cipersuite does
|
|
||||||
// not change between runs of the protocol, so we don't need to hash to
|
|
||||||
// get a fixed length.
|
|
||||||
binding_factor_input_prefix.extend_from_slice(verifying_key.serialize().as_ref());
|
|
||||||
|
|
||||||
// The message is hashed with H4 to force the variable-length message
|
|
||||||
// into a fixed-length byte string, same for hashing the variable-sized
|
|
||||||
// (between runs of the protocol) set of group commitments, but with H5.
|
|
||||||
binding_factor_input_prefix.extend_from_slice(C::H4(self.message.as_slice()).as_ref());
|
|
||||||
binding_factor_input_prefix.extend_from_slice(
|
|
||||||
C::H5(&round1::encode_group_commitments(self.signing_commitments())[..]).as_ref(),
|
|
||||||
);
|
|
||||||
binding_factor_input_prefix.extend_from_slice(additional_prefix);
|
|
||||||
|
|
||||||
self.signing_commitments()
|
|
||||||
.keys()
|
|
||||||
.map(|identifier| {
|
|
||||||
let mut binding_factor_input = vec![];
|
|
||||||
|
|
||||||
binding_factor_input.extend_from_slice(&binding_factor_input_prefix);
|
|
||||||
binding_factor_input.extend_from_slice(identifier.serialize().as_ref());
|
|
||||||
(*identifier, binding_factor_input)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "serialization")]
|
|
||||||
impl<C> SigningPackage<C>
|
|
||||||
where
|
|
||||||
C: Ciphersuite,
|
|
||||||
{
|
|
||||||
/// Serialize the struct into a Vec.
|
|
||||||
pub fn serialize(&self) -> Result<Vec<u8>, Error<C>> {
|
|
||||||
Serialize::serialize(&self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deserialize the struct from a slice of bytes.
|
|
||||||
pub fn deserialize(bytes: &[u8]) -> Result<Self, Error<C>> {
|
|
||||||
Deserialize::deserialize(bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The product of all signers' individual commitments, published as part of the
|
|
||||||
/// final signature.
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
|
||||||
pub struct GroupCommitment<C: Ciphersuite>(pub(super) Element<C>);
|
|
||||||
|
|
||||||
impl<C> GroupCommitment<C>
|
|
||||||
where
|
|
||||||
C: Ciphersuite,
|
|
||||||
{
|
|
||||||
/// Return the underlying element.
|
|
||||||
#[cfg(feature = "internals")]
|
|
||||||
pub fn to_element(self) -> <C::Group as Group>::Element {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates the group commitment which is published as part of the joint
|
|
||||||
/// Schnorr signature.
|
|
||||||
///
|
|
||||||
/// Implements [`compute_group_commitment`] from the spec.
|
|
||||||
///
|
|
||||||
/// [`compute_group_commitment`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-4.5
|
|
||||||
#[cfg_attr(feature = "internals", visibility::make(pub))]
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "internals")))]
|
|
||||||
fn compute_group_commitment<C>(
|
|
||||||
signing_package: &SigningPackage<C>,
|
|
||||||
binding_factor_list: &BindingFactorList<C>,
|
|
||||||
) -> Result<GroupCommitment<C>, Error<C>>
|
|
||||||
where
|
|
||||||
C: Ciphersuite,
|
|
||||||
{
|
|
||||||
let identity = <C::Group as Group>::identity();
|
|
||||||
|
|
||||||
let mut group_commitment = <C::Group as Group>::identity();
|
|
||||||
|
|
||||||
// Number of signing participants we are iterating over.
|
|
||||||
let n = signing_package.signing_commitments().len();
|
|
||||||
|
|
||||||
let mut binding_scalars = Vec::with_capacity(n);
|
|
||||||
|
|
||||||
let mut binding_elements = Vec::with_capacity(n);
|
|
||||||
|
|
||||||
for (commitment_identifier, commitment) in signing_package.signing_commitments() {
|
|
||||||
// The following check prevents a party from accidentally revealing their share.
|
|
||||||
// Note that the '&&' operator would be sufficient.
|
|
||||||
if identity == commitment.binding.0 || identity == commitment.hiding.0 {
|
|
||||||
return Err(Error::IdentityCommitment);
|
|
||||||
}
|
|
||||||
|
|
||||||
let binding_factor = binding_factor_list
|
|
||||||
.get(commitment_identifier)
|
|
||||||
.ok_or(Error::UnknownIdentifier)?;
|
|
||||||
|
|
||||||
// Collect the binding commitments and their binding factors for one big
|
|
||||||
// multiscalar multiplication at the end.
|
|
||||||
binding_elements.push(commitment.binding.0);
|
|
||||||
binding_scalars.push(binding_factor.0);
|
|
||||||
|
|
||||||
group_commitment = group_commitment + commitment.hiding.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let accumulated_binding_commitment: Element<C> =
|
|
||||||
VartimeMultiscalarMul::<C>::vartime_multiscalar_mul(binding_scalars, binding_elements);
|
|
||||||
|
|
||||||
group_commitment = group_commitment + accumulated_binding_commitment;
|
|
||||||
|
|
||||||
Ok(GroupCommitment(group_commitment))
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Aggregation
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
/// Aggregates the signature shares to produce a final signature that
|
|
||||||
/// can be verified with the group public key.
|
|
||||||
///
|
|
||||||
/// `signature_shares` maps the identifier of each participant to the
|
|
||||||
/// [`round2::SignatureShare`] they sent. These identifiers must come from whatever mapping
|
|
||||||
/// the coordinator has between communication channels and participants, i.e.
|
|
||||||
/// they must have assurance that the [`round2::SignatureShare`] came from
|
|
||||||
/// the participant with that identifier.
|
|
||||||
///
|
|
||||||
/// This operation is performed by a coordinator that can communicate with all
|
|
||||||
/// the signing participants before publishing the final signature. The
|
|
||||||
/// coordinator can be one of the participants or a semi-trusted third party
|
|
||||||
/// (who is trusted to not perform denial of service attacks, but does not learn
|
|
||||||
/// any secret information). Note that because the coordinator is trusted to
|
|
||||||
/// report misbehaving parties in order to avoid publishing an invalid
|
|
||||||
/// signature, if the coordinator themselves is a signer and misbehaves, they
|
|
||||||
/// can avoid that step. However, at worst, this results in a denial of
|
|
||||||
/// service attack due to publishing an invalid signature.
|
|
||||||
|
|
||||||
pub fn aggregate<C>(
|
|
||||||
signing_package: &SigningPackage<C>,
|
|
||||||
signature_shares: &BTreeMap<Identifier<C>, round2::SignatureShare<C>>,
|
|
||||||
pubkeys: &keys::PublicKeyPackage<C>,
|
|
||||||
) -> Result<Signature<C>, Error<C>>
|
|
||||||
where
|
|
||||||
C: Ciphersuite,
|
|
||||||
{
|
|
||||||
// Check if signing_package.signing_commitments and signature_shares have
|
|
||||||
// the same set of identifiers, and if they are all in pubkeys.verifying_shares.
|
|
||||||
if signing_package.signing_commitments().len() != signature_shares.len() {
|
|
||||||
return Err(Error::UnknownIdentifier);
|
|
||||||
}
|
|
||||||
if !signing_package.signing_commitments().keys().all(|id| {
|
|
||||||
#[cfg(feature = "cheater-detection")]
|
|
||||||
return signature_shares.contains_key(id) && pubkeys.verifying_shares().contains_key(id);
|
|
||||||
#[cfg(not(feature = "cheater-detection"))]
|
|
||||||
return signature_shares.contains_key(id);
|
|
||||||
}) {
|
|
||||||
return Err(Error::UnknownIdentifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the
|
|
||||||
// binding factor.
|
|
||||||
let binding_factor_list: BindingFactorList<C> =
|
|
||||||
compute_binding_factor_list(signing_package, &pubkeys.verifying_key, &[]);
|
|
||||||
|
|
||||||
// Compute the group commitment from signing commitments produced in round one.
|
|
||||||
let group_commitment = compute_group_commitment(signing_package, &binding_factor_list)?;
|
|
||||||
|
|
||||||
// The aggregation of the signature shares by summing them up, resulting in
|
|
||||||
// a plain Schnorr signature.
|
|
||||||
//
|
|
||||||
// Implements [`aggregate`] from the spec.
|
|
||||||
//
|
|
||||||
// [`aggregate`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-5.3
|
|
||||||
let mut z = <<C::Group as Group>::Field>::zero();
|
|
||||||
|
|
||||||
for signature_share in signature_shares.values() {
|
|
||||||
z = z + signature_share.share;
|
|
||||||
}
|
|
||||||
|
|
||||||
let signature = Signature {
|
|
||||||
R: group_commitment.0,
|
|
||||||
z,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Verify the aggregate signature
|
|
||||||
let verification_result = pubkeys
|
|
||||||
.verifying_key
|
|
||||||
.verify(signing_package.message(), &signature);
|
|
||||||
|
|
||||||
// Only if the verification of the aggregate signature failed; verify each share to find the cheater.
|
|
||||||
// This approach is more efficient since we don't need to verify all shares
|
|
||||||
// if the aggregate signature is valid (which should be the common case).
|
|
||||||
#[cfg(feature = "cheater-detection")]
|
|
||||||
if let Err(err) = verification_result {
|
|
||||||
// Compute the per-message challenge.
|
|
||||||
let challenge = crate::challenge::<C>(
|
|
||||||
&group_commitment.0,
|
|
||||||
&pubkeys.verifying_key,
|
|
||||||
signing_package.message().as_slice(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify the signature shares.
|
|
||||||
for (signature_share_identifier, signature_share) in signature_shares {
|
|
||||||
// Look up the public key for this signer, where `signer_pubkey` = _G.ScalarBaseMult(s[i])_,
|
|
||||||
// and where s[i] is a secret share of the constant term of _f_, the secret polynomial.
|
|
||||||
let signer_pubkey = pubkeys
|
|
||||||
.verifying_shares
|
|
||||||
.get(signature_share_identifier)
|
|
||||||
.ok_or(Error::UnknownIdentifier)?;
|
|
||||||
|
|
||||||
// Compute Lagrange coefficient.
|
|
||||||
let lambda_i = derive_interpolating_value(signature_share_identifier, signing_package)?;
|
|
||||||
|
|
||||||
let binding_factor = binding_factor_list
|
|
||||||
.get(signature_share_identifier)
|
|
||||||
.ok_or(Error::UnknownIdentifier)?;
|
|
||||||
|
|
||||||
// Compute the commitment share.
|
|
||||||
let R_share = signing_package
|
|
||||||
.signing_commitment(signature_share_identifier)
|
|
||||||
.ok_or(Error::UnknownIdentifier)?
|
|
||||||
.to_group_commitment_share(binding_factor);
|
|
||||||
|
|
||||||
// Compute relation values to verify this signature share.
|
|
||||||
signature_share.verify(
|
|
||||||
*signature_share_identifier,
|
|
||||||
&R_share,
|
|
||||||
signer_pubkey,
|
|
||||||
lambda_i,
|
|
||||||
&challenge,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should never reach here; but we return the verification error to be safe.
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "cheater-detection"))]
|
|
||||||
verification_result?;
|
|
||||||
|
|
||||||
Ok(signature)
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,8 +17,8 @@ use rand_core::{CryptoRng, RngCore};
|
||||||
use zeroize::{DefaultIsZeroes, Zeroize};
|
use zeroize::{DefaultIsZeroes, Zeroize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
frost::Identifier, Ciphersuite, Deserialize, Element, Error, Field, Group, Header, Scalar,
|
Ciphersuite, Deserialize, Element, Error, Field, Group, Header, Identifier, Scalar, Serialize,
|
||||||
Serialize, SigningKey, VerifyingKey,
|
SigningKey, VerifyingKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
|
@ -35,8 +35,8 @@ use std::{collections::BTreeMap, iter};
|
||||||
use rand_core::{CryptoRng, RngCore};
|
use rand_core::{CryptoRng, RngCore};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
frost::Identifier, Challenge, Ciphersuite, Element, Error, Field, Group, Header, Scalar,
|
Challenge, Ciphersuite, Element, Error, Field, Group, Header, Identifier, Scalar, Signature,
|
||||||
Signature, SigningKey, VerifyingKey,
|
SigningKey, VerifyingKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
|
@ -7,8 +7,8 @@
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
frost::{compute_lagrange_coefficient, Identifier},
|
compute_lagrange_coefficient, Ciphersuite, CryptoRng, Error, Field, Group, Header, Identifier,
|
||||||
Ciphersuite, CryptoRng, Error, Field, Group, Header, RngCore, Scalar,
|
RngCore, Scalar,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{generate_coefficients, SecretShare, SigningShare, VerifiableSecretSharingCommitment};
|
use super::{generate_coefficients, SecretShare, SigningShare, VerifiableSecretSharingCommitment};
|
|
@ -11,13 +11,18 @@
|
||||||
#![doc = document_features::document_features!()]
|
#![doc = document_features::document_features!()]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::{BTreeMap, BTreeSet},
|
||||||
default::Default,
|
default::Default,
|
||||||
fmt::Debug,
|
fmt::{self, Debug},
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
ops::{Add, Mul, Sub},
|
ops::{Add, Mul, Sub},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use derive_getters::Getters;
|
||||||
|
#[cfg(any(test, feature = "test-impl"))]
|
||||||
|
use hex::FromHex;
|
||||||
use rand_core::{CryptoRng, RngCore};
|
use rand_core::{CryptoRng, RngCore};
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
// Re-export serde
|
// Re-export serde
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
|
@ -27,7 +32,10 @@ pub mod batch;
|
||||||
#[cfg(any(test, feature = "test-impl"))]
|
#[cfg(any(test, feature = "test-impl"))]
|
||||||
pub mod benches;
|
pub mod benches;
|
||||||
mod error;
|
mod error;
|
||||||
pub mod frost;
|
mod identifier;
|
||||||
|
pub mod keys;
|
||||||
|
pub mod round1;
|
||||||
|
pub mod round2;
|
||||||
mod scalar_mul;
|
mod scalar_mul;
|
||||||
mod signature;
|
mod signature;
|
||||||
mod signing_key;
|
mod signing_key;
|
||||||
|
@ -35,11 +43,12 @@ mod signing_key;
|
||||||
pub mod tests;
|
pub mod tests;
|
||||||
mod verifying_key;
|
mod verifying_key;
|
||||||
|
|
||||||
|
pub use self::identifier::Identifier;
|
||||||
|
use crate::scalar_mul::VartimeMultiscalarMul;
|
||||||
pub use error::{Error, FieldError, GroupError};
|
pub use error::{Error, FieldError, GroupError};
|
||||||
pub use signature::Signature;
|
pub use signature::Signature;
|
||||||
pub use signing_key::SigningKey;
|
pub use signing_key::SigningKey;
|
||||||
pub use verifying_key::VerifyingKey;
|
pub use verifying_key::VerifyingKey;
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
/// A prime order finite field GF(q) over which all scalar values for our prime order group can be
|
/// A prime order finite field GF(q) over which all scalar values for our prime order group can be
|
||||||
/// multiplied are defined.
|
/// multiplied are defined.
|
||||||
|
@ -560,3 +569,487 @@ impl<T: for<'de> serde::Deserialize<'de>, C: Ciphersuite> Deserialize<C> for T {
|
||||||
postcard::from_bytes(bytes).map_err(|_| Error::DeserializationError)
|
postcard::from_bytes(bytes).map_err(|_| Error::DeserializationError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The binding factor, also known as _rho_ (ρ)
|
||||||
|
///
|
||||||
|
/// Ensures each signature share is strongly bound to a signing set, specific set
|
||||||
|
/// of commitments, and a specific message.
|
||||||
|
///
|
||||||
|
/// <https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md>
|
||||||
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
|
pub struct BindingFactor<C: Ciphersuite>(Scalar<C>);
|
||||||
|
|
||||||
|
impl<C> BindingFactor<C>
|
||||||
|
where
|
||||||
|
C: Ciphersuite,
|
||||||
|
{
|
||||||
|
/// Deserializes [`BindingFactor`] from bytes.
|
||||||
|
pub fn deserialize(
|
||||||
|
bytes: <<C::Group as Group>::Field as Field>::Serialization,
|
||||||
|
) -> Result<Self, Error<C>> {
|
||||||
|
<<C::Group as Group>::Field>::deserialize(&bytes)
|
||||||
|
.map(|scalar| Self(scalar))
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializes [`BindingFactor`] to bytes.
|
||||||
|
pub fn serialize(&self) -> <<C::Group as Group>::Field as Field>::Serialization {
|
||||||
|
<<C::Group as Group>::Field>::serialize(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> Debug for BindingFactor<C>
|
||||||
|
where
|
||||||
|
C: Ciphersuite,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.debug_tuple("BindingFactor")
|
||||||
|
.field(&hex::encode(self.serialize()))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list of binding factors and their associated identifiers.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct BindingFactorList<C: Ciphersuite>(BTreeMap<Identifier<C>, BindingFactor<C>>);
|
||||||
|
|
||||||
|
impl<C> BindingFactorList<C>
|
||||||
|
where
|
||||||
|
C: Ciphersuite,
|
||||||
|
{
|
||||||
|
/// Create a new [`BindingFactorList`] from a map of identifiers to binding factors.
|
||||||
|
#[cfg(feature = "internals")]
|
||||||
|
pub fn new(binding_factors: BTreeMap<Identifier<C>, BindingFactor<C>>) -> Self {
|
||||||
|
Self(binding_factors)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return iterator through all factors.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = (&Identifier<C>, &BindingFactor<C>)> {
|
||||||
|
self.0.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the [`BindingFactor`] for the given identifier, or None if not found.
|
||||||
|
pub fn get(&self, key: &Identifier<C>) -> Option<&BindingFactor<C>> {
|
||||||
|
self.0.get(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`compute_binding_factors`] in the spec
|
||||||
|
///
|
||||||
|
/// [`compute_binding_factors`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-4.4
|
||||||
|
#[cfg_attr(feature = "internals", visibility::make(pub))]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "internals")))]
|
||||||
|
pub(crate) fn compute_binding_factor_list<C>(
|
||||||
|
signing_package: &SigningPackage<C>,
|
||||||
|
verifying_key: &VerifyingKey<C>,
|
||||||
|
additional_prefix: &[u8],
|
||||||
|
) -> BindingFactorList<C>
|
||||||
|
where
|
||||||
|
C: Ciphersuite,
|
||||||
|
{
|
||||||
|
let preimages = signing_package.binding_factor_preimages(verifying_key, additional_prefix);
|
||||||
|
|
||||||
|
BindingFactorList(
|
||||||
|
preimages
|
||||||
|
.iter()
|
||||||
|
.map(|(identifier, preimage)| {
|
||||||
|
let binding_factor = C::H1(preimage);
|
||||||
|
(*identifier, BindingFactor(binding_factor))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-impl"))]
|
||||||
|
impl<C> FromHex for BindingFactor<C>
|
||||||
|
where
|
||||||
|
C: Ciphersuite,
|
||||||
|
{
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
|
||||||
|
let v: Vec<u8> = FromHex::from_hex(hex).map_err(|_| "invalid hex")?;
|
||||||
|
match v.try_into() {
|
||||||
|
Ok(bytes) => Self::deserialize(bytes).map_err(|_| "malformed scalar encoding"),
|
||||||
|
Err(_) => Err("malformed scalar encoding"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a lagrange coefficient.
|
||||||
|
///
|
||||||
|
/// The Lagrange polynomial for a set of points (x_j, y_j) for 0 <= j <= k
|
||||||
|
/// is ∑_{i=0}^k y_i.ℓ_i(x), where ℓ_i(x) is the Lagrange basis polynomial:
|
||||||
|
///
|
||||||
|
/// ℓ_i(x) = ∏_{0≤j≤k; j≠i} (x - x_j) / (x_i - x_j).
|
||||||
|
///
|
||||||
|
/// This computes ℓ_j(x) for the set of points `xs` and for the j corresponding
|
||||||
|
/// to the given xj.
|
||||||
|
///
|
||||||
|
/// If `x` is None, it uses 0 for it (since Identifiers can't be 0)
|
||||||
|
#[cfg_attr(feature = "internals", visibility::make(pub))]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "internals")))]
|
||||||
|
fn compute_lagrange_coefficient<C: Ciphersuite>(
|
||||||
|
x_set: &BTreeSet<Identifier<C>>,
|
||||||
|
x: Option<Identifier<C>>,
|
||||||
|
x_i: Identifier<C>,
|
||||||
|
) -> Result<Scalar<C>, Error<C>> {
|
||||||
|
if x_set.is_empty() {
|
||||||
|
return Err(Error::IncorrectNumberOfIdentifiers);
|
||||||
|
}
|
||||||
|
let mut num = <<C::Group as Group>::Field>::one();
|
||||||
|
let mut den = <<C::Group as Group>::Field>::one();
|
||||||
|
|
||||||
|
let mut x_i_found = false;
|
||||||
|
|
||||||
|
for x_j in x_set.iter() {
|
||||||
|
if x_i == *x_j {
|
||||||
|
x_i_found = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = x {
|
||||||
|
num *= x - *x_j;
|
||||||
|
den *= x_i - *x_j;
|
||||||
|
} else {
|
||||||
|
// Both signs inverted just to avoid requiring Neg (-*xj)
|
||||||
|
num *= *x_j;
|
||||||
|
den *= *x_j - x_i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !x_i_found {
|
||||||
|
return Err(Error::UnknownIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
num * <<C::Group as Group>::Field>::invert(&den)
|
||||||
|
.map_err(|_| Error::DuplicatedIdentifier)?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates the lagrange coefficient for the i'th participant (for `signer_id`).
|
||||||
|
///
|
||||||
|
/// Implements [`derive_interpolating_value()`] from the spec.
|
||||||
|
///
|
||||||
|
/// [`derive_interpolating_value()`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#name-polynomials
|
||||||
|
#[cfg_attr(feature = "internals", visibility::make(pub))]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "internals")))]
|
||||||
|
fn derive_interpolating_value<C: Ciphersuite>(
|
||||||
|
signer_id: &Identifier<C>,
|
||||||
|
signing_package: &SigningPackage<C>,
|
||||||
|
) -> Result<Scalar<C>, Error<C>> {
|
||||||
|
compute_lagrange_coefficient(
|
||||||
|
&signing_package
|
||||||
|
.signing_commitments()
|
||||||
|
.keys()
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
None,
|
||||||
|
*signer_id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated by the coordinator of the signing operation and distributed to
|
||||||
|
/// each signing party
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Getters)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
|
||||||
|
pub struct SigningPackage<C: Ciphersuite> {
|
||||||
|
/// Serialization header
|
||||||
|
#[getter(skip)]
|
||||||
|
pub(crate) header: Header<C>,
|
||||||
|
/// The set of commitments participants published in the first round of the
|
||||||
|
/// protocol.
|
||||||
|
signing_commitments: BTreeMap<Identifier<C>, round1::SigningCommitments<C>>,
|
||||||
|
/// Message which each participant will sign.
|
||||||
|
///
|
||||||
|
/// Each signer should perform protocol-specific verification on the
|
||||||
|
/// message.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "serde",
|
||||||
|
serde(
|
||||||
|
serialize_with = "serdect::slice::serialize_hex_lower_or_bin",
|
||||||
|
deserialize_with = "serdect::slice::deserialize_hex_or_bin_vec"
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
message: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> SigningPackage<C>
|
||||||
|
where
|
||||||
|
C: Ciphersuite,
|
||||||
|
{
|
||||||
|
/// Create a new `SigningPackage`
|
||||||
|
///
|
||||||
|
/// The `signing_commitments` are sorted by participant `identifier`.
|
||||||
|
pub fn new(
|
||||||
|
signing_commitments: BTreeMap<Identifier<C>, round1::SigningCommitments<C>>,
|
||||||
|
message: &[u8],
|
||||||
|
) -> SigningPackage<C> {
|
||||||
|
SigningPackage {
|
||||||
|
header: Header::default(),
|
||||||
|
signing_commitments,
|
||||||
|
message: message.to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a signing commitment by its participant identifier, or None if not found.
|
||||||
|
pub fn signing_commitment(
|
||||||
|
&self,
|
||||||
|
identifier: &Identifier<C>,
|
||||||
|
) -> Option<round1::SigningCommitments<C>> {
|
||||||
|
self.signing_commitments.get(identifier).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the preimages to H1 to compute the per-signer binding factors
|
||||||
|
// We separate this out into its own method so it can be tested
|
||||||
|
#[cfg_attr(feature = "internals", visibility::make(pub))]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "internals")))]
|
||||||
|
pub fn binding_factor_preimages(
|
||||||
|
&self,
|
||||||
|
verifying_key: &VerifyingKey<C>,
|
||||||
|
additional_prefix: &[u8],
|
||||||
|
) -> Vec<(Identifier<C>, Vec<u8>)> {
|
||||||
|
let mut binding_factor_input_prefix = vec![];
|
||||||
|
|
||||||
|
// The length of a serialized verifying key of the same cipersuite does
|
||||||
|
// not change between runs of the protocol, so we don't need to hash to
|
||||||
|
// get a fixed length.
|
||||||
|
binding_factor_input_prefix.extend_from_slice(verifying_key.serialize().as_ref());
|
||||||
|
|
||||||
|
// The message is hashed with H4 to force the variable-length message
|
||||||
|
// into a fixed-length byte string, same for hashing the variable-sized
|
||||||
|
// (between runs of the protocol) set of group commitments, but with H5.
|
||||||
|
binding_factor_input_prefix.extend_from_slice(C::H4(self.message.as_slice()).as_ref());
|
||||||
|
binding_factor_input_prefix.extend_from_slice(
|
||||||
|
C::H5(&round1::encode_group_commitments(self.signing_commitments())[..]).as_ref(),
|
||||||
|
);
|
||||||
|
binding_factor_input_prefix.extend_from_slice(additional_prefix);
|
||||||
|
|
||||||
|
self.signing_commitments()
|
||||||
|
.keys()
|
||||||
|
.map(|identifier| {
|
||||||
|
let mut binding_factor_input = vec![];
|
||||||
|
|
||||||
|
binding_factor_input.extend_from_slice(&binding_factor_input_prefix);
|
||||||
|
binding_factor_input.extend_from_slice(identifier.serialize().as_ref());
|
||||||
|
(*identifier, binding_factor_input)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serialization")]
|
||||||
|
impl<C> SigningPackage<C>
|
||||||
|
where
|
||||||
|
C: Ciphersuite,
|
||||||
|
{
|
||||||
|
/// Serialize the struct into a Vec.
|
||||||
|
pub fn serialize(&self) -> Result<Vec<u8>, Error<C>> {
|
||||||
|
Serialize::serialize(&self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize the struct from a slice of bytes.
|
||||||
|
pub fn deserialize(bytes: &[u8]) -> Result<Self, Error<C>> {
|
||||||
|
Deserialize::deserialize(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The product of all signers' individual commitments, published as part of the
|
||||||
|
/// final signature.
|
||||||
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
|
pub struct GroupCommitment<C: Ciphersuite>(pub(crate) Element<C>);
|
||||||
|
|
||||||
|
impl<C> GroupCommitment<C>
|
||||||
|
where
|
||||||
|
C: Ciphersuite,
|
||||||
|
{
|
||||||
|
/// Return the underlying element.
|
||||||
|
#[cfg(feature = "internals")]
|
||||||
|
pub fn to_element(self) -> <C::Group as Group>::Element {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates the group commitment which is published as part of the joint
|
||||||
|
/// Schnorr signature.
|
||||||
|
///
|
||||||
|
/// Implements [`compute_group_commitment`] from the spec.
|
||||||
|
///
|
||||||
|
/// [`compute_group_commitment`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-4.5
|
||||||
|
#[cfg_attr(feature = "internals", visibility::make(pub))]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "internals")))]
|
||||||
|
fn compute_group_commitment<C>(
|
||||||
|
signing_package: &SigningPackage<C>,
|
||||||
|
binding_factor_list: &BindingFactorList<C>,
|
||||||
|
) -> Result<GroupCommitment<C>, Error<C>>
|
||||||
|
where
|
||||||
|
C: Ciphersuite,
|
||||||
|
{
|
||||||
|
let identity = <C::Group as Group>::identity();
|
||||||
|
|
||||||
|
let mut group_commitment = <C::Group as Group>::identity();
|
||||||
|
|
||||||
|
// Number of signing participants we are iterating over.
|
||||||
|
let n = signing_package.signing_commitments().len();
|
||||||
|
|
||||||
|
let mut binding_scalars = Vec::with_capacity(n);
|
||||||
|
|
||||||
|
let mut binding_elements = Vec::with_capacity(n);
|
||||||
|
|
||||||
|
for (commitment_identifier, commitment) in signing_package.signing_commitments() {
|
||||||
|
// The following check prevents a party from accidentally revealing their share.
|
||||||
|
// Note that the '&&' operator would be sufficient.
|
||||||
|
if identity == commitment.binding.0 || identity == commitment.hiding.0 {
|
||||||
|
return Err(Error::IdentityCommitment);
|
||||||
|
}
|
||||||
|
|
||||||
|
let binding_factor = binding_factor_list
|
||||||
|
.get(commitment_identifier)
|
||||||
|
.ok_or(Error::UnknownIdentifier)?;
|
||||||
|
|
||||||
|
// Collect the binding commitments and their binding factors for one big
|
||||||
|
// multiscalar multiplication at the end.
|
||||||
|
binding_elements.push(commitment.binding.0);
|
||||||
|
binding_scalars.push(binding_factor.0);
|
||||||
|
|
||||||
|
group_commitment = group_commitment + commitment.hiding.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let accumulated_binding_commitment: Element<C> =
|
||||||
|
VartimeMultiscalarMul::<C>::vartime_multiscalar_mul(binding_scalars, binding_elements);
|
||||||
|
|
||||||
|
group_commitment = group_commitment + accumulated_binding_commitment;
|
||||||
|
|
||||||
|
Ok(GroupCommitment(group_commitment))
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Aggregation
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/// Aggregates the signature shares to produce a final signature that
|
||||||
|
/// can be verified with the group public key.
|
||||||
|
///
|
||||||
|
/// `signature_shares` maps the identifier of each participant to the
|
||||||
|
/// [`round2::SignatureShare`] they sent. These identifiers must come from whatever mapping
|
||||||
|
/// the coordinator has between communication channels and participants, i.e.
|
||||||
|
/// they must have assurance that the [`round2::SignatureShare`] came from
|
||||||
|
/// the participant with that identifier.
|
||||||
|
///
|
||||||
|
/// This operation is performed by a coordinator that can communicate with all
|
||||||
|
/// the signing participants before publishing the final signature. The
|
||||||
|
/// coordinator can be one of the participants or a semi-trusted third party
|
||||||
|
/// (who is trusted to not perform denial of service attacks, but does not learn
|
||||||
|
/// any secret information). Note that because the coordinator is trusted to
|
||||||
|
/// report misbehaving parties in order to avoid publishing an invalid
|
||||||
|
/// signature, if the coordinator themselves is a signer and misbehaves, they
|
||||||
|
/// can avoid that step. However, at worst, this results in a denial of
|
||||||
|
/// service attack due to publishing an invalid signature.
|
||||||
|
|
||||||
|
pub fn aggregate<C>(
|
||||||
|
signing_package: &SigningPackage<C>,
|
||||||
|
signature_shares: &BTreeMap<Identifier<C>, round2::SignatureShare<C>>,
|
||||||
|
pubkeys: &keys::PublicKeyPackage<C>,
|
||||||
|
) -> Result<Signature<C>, Error<C>>
|
||||||
|
where
|
||||||
|
C: Ciphersuite,
|
||||||
|
{
|
||||||
|
// Check if signing_package.signing_commitments and signature_shares have
|
||||||
|
// the same set of identifiers, and if they are all in pubkeys.verifying_shares.
|
||||||
|
if signing_package.signing_commitments().len() != signature_shares.len() {
|
||||||
|
return Err(Error::UnknownIdentifier);
|
||||||
|
}
|
||||||
|
if !signing_package.signing_commitments().keys().all(|id| {
|
||||||
|
#[cfg(feature = "cheater-detection")]
|
||||||
|
return signature_shares.contains_key(id) && pubkeys.verifying_shares().contains_key(id);
|
||||||
|
#[cfg(not(feature = "cheater-detection"))]
|
||||||
|
return signature_shares.contains_key(id);
|
||||||
|
}) {
|
||||||
|
return Err(Error::UnknownIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the
|
||||||
|
// binding factor.
|
||||||
|
let binding_factor_list: BindingFactorList<C> =
|
||||||
|
compute_binding_factor_list(signing_package, &pubkeys.verifying_key, &[]);
|
||||||
|
|
||||||
|
// Compute the group commitment from signing commitments produced in round one.
|
||||||
|
let group_commitment = compute_group_commitment(signing_package, &binding_factor_list)?;
|
||||||
|
|
||||||
|
// The aggregation of the signature shares by summing them up, resulting in
|
||||||
|
// a plain Schnorr signature.
|
||||||
|
//
|
||||||
|
// Implements [`aggregate`] from the spec.
|
||||||
|
//
|
||||||
|
// [`aggregate`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-5.3
|
||||||
|
let mut z = <<C::Group as Group>::Field>::zero();
|
||||||
|
|
||||||
|
for signature_share in signature_shares.values() {
|
||||||
|
z = z + signature_share.share;
|
||||||
|
}
|
||||||
|
|
||||||
|
let signature = Signature {
|
||||||
|
R: group_commitment.0,
|
||||||
|
z,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify the aggregate signature
|
||||||
|
let verification_result = pubkeys
|
||||||
|
.verifying_key
|
||||||
|
.verify(signing_package.message(), &signature);
|
||||||
|
|
||||||
|
// Only if the verification of the aggregate signature failed; verify each share to find the cheater.
|
||||||
|
// This approach is more efficient since we don't need to verify all shares
|
||||||
|
// if the aggregate signature is valid (which should be the common case).
|
||||||
|
#[cfg(feature = "cheater-detection")]
|
||||||
|
if let Err(err) = verification_result {
|
||||||
|
// Compute the per-message challenge.
|
||||||
|
let challenge = crate::challenge::<C>(
|
||||||
|
&group_commitment.0,
|
||||||
|
&pubkeys.verifying_key,
|
||||||
|
signing_package.message().as_slice(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the signature shares.
|
||||||
|
for (signature_share_identifier, signature_share) in signature_shares {
|
||||||
|
// Look up the public key for this signer, where `signer_pubkey` = _G.ScalarBaseMult(s[i])_,
|
||||||
|
// and where s[i] is a secret share of the constant term of _f_, the secret polynomial.
|
||||||
|
let signer_pubkey = pubkeys
|
||||||
|
.verifying_shares
|
||||||
|
.get(signature_share_identifier)
|
||||||
|
.ok_or(Error::UnknownIdentifier)?;
|
||||||
|
|
||||||
|
// Compute Lagrange coefficient.
|
||||||
|
let lambda_i = derive_interpolating_value(signature_share_identifier, signing_package)?;
|
||||||
|
|
||||||
|
let binding_factor = binding_factor_list
|
||||||
|
.get(signature_share_identifier)
|
||||||
|
.ok_or(Error::UnknownIdentifier)?;
|
||||||
|
|
||||||
|
// Compute the commitment share.
|
||||||
|
let R_share = signing_package
|
||||||
|
.signing_commitment(signature_share_identifier)
|
||||||
|
.ok_or(Error::UnknownIdentifier)?
|
||||||
|
.to_group_commitment_share(binding_factor);
|
||||||
|
|
||||||
|
// Compute relation values to verify this signature share.
|
||||||
|
signature_share.verify(
|
||||||
|
*signature_share_identifier,
|
||||||
|
&R_share,
|
||||||
|
signer_pubkey,
|
||||||
|
lambda_i,
|
||||||
|
&challenge,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should never reach here; but we return the verification error to be safe.
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "cheater-detection"))]
|
||||||
|
verification_result?;
|
||||||
|
|
||||||
|
Ok(signature)
|
||||||
|
}
|
||||||
|
|
|
@ -12,9 +12,8 @@ use hex::FromHex;
|
||||||
use rand_core::{CryptoRng, RngCore};
|
use rand_core::{CryptoRng, RngCore};
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
use crate::{
|
use crate as frost;
|
||||||
frost, Ciphersuite, Deserialize, Element, Error, Field, Group, Header, Scalar, Serialize,
|
use crate::{Ciphersuite, Deserialize, Element, Error, Field, Group, Header, Scalar, Serialize};
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use crate::ElementSerialization;
|
use crate::ElementSerialization;
|
|
@ -2,10 +2,9 @@
|
||||||
|
|
||||||
use std::fmt::{self, Debug};
|
use std::fmt::{self, Debug};
|
||||||
|
|
||||||
|
use crate as frost;
|
||||||
use crate::{
|
use crate::{
|
||||||
challenge,
|
challenge, Challenge, Ciphersuite, Error, Field, Group, {round1, *},
|
||||||
frost::{self, round1, *},
|
|
||||||
Challenge, Ciphersuite, Error, Field, Group,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
|
@ -3,9 +3,9 @@
|
||||||
|
|
||||||
use std::{collections::BTreeMap, convert::TryFrom};
|
use std::{collections::BTreeMap, convert::TryFrom};
|
||||||
|
|
||||||
|
use crate as frost;
|
||||||
use crate::{
|
use crate::{
|
||||||
frost::{self, keys::PublicKeyPackage, Identifier},
|
keys::PublicKeyPackage, Error, Field, Group, Identifier, Signature, SigningKey, VerifyingKey,
|
||||||
Error, Field, Group, Signature, SigningKey, VerifyingKey,
|
|
||||||
};
|
};
|
||||||
use rand_core::{CryptoRng, RngCore};
|
use rand_core::{CryptoRng, RngCore};
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,8 @@
|
||||||
|
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use crate::{
|
use crate as frost;
|
||||||
frost::{self, keys::CoefficientCommitment},
|
use crate::{keys::CoefficientCommitment, tests::helpers::generate_element, Group};
|
||||||
tests::helpers::generate_element,
|
|
||||||
Group,
|
|
||||||
};
|
|
||||||
use debugless_unwrap::DebuglessUnwrap;
|
use debugless_unwrap::DebuglessUnwrap;
|
||||||
use rand_core::{CryptoRng, RngCore};
|
use rand_core::{CryptoRng, RngCore};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
|
@ -6,16 +6,14 @@ use debugless_unwrap::DebuglessUnwrap;
|
||||||
use rand_core::{CryptoRng, RngCore};
|
use rand_core::{CryptoRng, RngCore};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate as frost;
|
||||||
use crate::{
|
use crate::{
|
||||||
frost::{
|
compute_lagrange_coefficient,
|
||||||
self, compute_lagrange_coefficient,
|
keys::{
|
||||||
keys::{
|
repairable::{repair_share_step_1, repair_share_step_2, repair_share_step_3},
|
||||||
repairable::{repair_share_step_1, repair_share_step_2, repair_share_step_3},
|
PublicKeyPackage, SecretShare, SigningShare,
|
||||||
PublicKeyPackage, SecretShare, SigningShare,
|
|
||||||
},
|
|
||||||
Identifier,
|
|
||||||
},
|
},
|
||||||
Ciphersuite, Error, Field, Group, Scalar,
|
Ciphersuite, Error, Field, Group, Identifier, Scalar,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// We want to test that recover share matches the original share
|
/// We want to test that recover share matches the original share
|
||||||
|
|
|
@ -5,9 +5,9 @@ use debugless_unwrap::DebuglessUnwrap;
|
||||||
use hex::{self, FromHex};
|
use hex::{self, FromHex};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate as frost;
|
||||||
use crate::{
|
use crate::{
|
||||||
frost::{self, keys::*, round1::*, round2::*, *},
|
keys::*, round1::*, round2::*, Ciphersuite, Field, Group, Scalar, SigningKey, VerifyingKey, *,
|
||||||
Ciphersuite, Field, Group, Scalar, SigningKey, VerifyingKey,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Test vectors for a ciphersuite.
|
/// Test vectors for a ciphersuite.
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
frost::keys::{CoefficientCommitment, VerifiableSecretSharingCommitment},
|
keys::{CoefficientCommitment, VerifiableSecretSharingCommitment},
|
||||||
tests::helpers::generate_element,
|
tests::helpers::generate_element,
|
||||||
Group,
|
Group,
|
||||||
};
|
};
|
||||||
|
@ -11,7 +11,7 @@ use debugless_unwrap::DebuglessUnwrap;
|
||||||
use rand_core::{CryptoRng, RngCore};
|
use rand_core::{CryptoRng, RngCore};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::frost::keys::{generate_with_dealer, IdentifierList, PublicKeyPackage};
|
use crate::keys::{generate_with_dealer, IdentifierList, PublicKeyPackage};
|
||||||
use crate::Ciphersuite;
|
use crate::Ciphersuite;
|
||||||
|
|
||||||
/// Test serialize VerifiableSecretSharingCommitment
|
/// Test serialize VerifiableSecretSharingCommitment
|
||||||
|
|
|
@ -82,7 +82,7 @@ where
|
||||||
/// Computes the group public key given the group commitment.
|
/// Computes the group public key given the group commitment.
|
||||||
#[cfg_attr(feature = "internals", visibility::make(pub))]
|
#[cfg_attr(feature = "internals", visibility::make(pub))]
|
||||||
pub(crate) fn from_commitment(
|
pub(crate) fn from_commitment(
|
||||||
commitment: &crate::frost::keys::VerifiableSecretSharingCommitment<C>,
|
commitment: &crate::keys::VerifiableSecretSharingCommitment<C>,
|
||||||
) -> Result<VerifyingKey<C>, Error<C>> {
|
) -> Result<VerifyingKey<C>, Error<C>> {
|
||||||
Ok(VerifyingKey {
|
Ok(VerifyingKey {
|
||||||
element: commitment
|
element: commitment
|
||||||
|
|
|
@ -17,7 +17,7 @@ use frost_rerandomized::RandomizedCiphersuite;
|
||||||
use rand_core::{CryptoRng, RngCore};
|
use rand_core::{CryptoRng, RngCore};
|
||||||
use sha2::{Digest, Sha512};
|
use sha2::{Digest, Sha512};
|
||||||
|
|
||||||
use frost_core::frost;
|
use frost_core as frost;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
|
@ -18,7 +18,7 @@ use sha3::{
|
||||||
Shake256,
|
Shake256,
|
||||||
};
|
};
|
||||||
|
|
||||||
use frost_core::frost;
|
use frost_core as frost;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
|
@ -19,7 +19,7 @@ use p256::{
|
||||||
use rand_core::{CryptoRng, RngCore};
|
use rand_core::{CryptoRng, RngCore};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
use frost_core::frost;
|
use frost_core as frost;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
|
@ -20,12 +20,9 @@ use derive_getters::Getters;
|
||||||
pub use frost_core;
|
pub use frost_core;
|
||||||
|
|
||||||
use frost_core::{
|
use frost_core::{
|
||||||
frost::{
|
self as frost,
|
||||||
self,
|
keys::{KeyPackage, PublicKeyPackage, SigningShare, VerifyingShare},
|
||||||
keys::{KeyPackage, PublicKeyPackage, SigningShare, VerifyingShare},
|
Ciphersuite, Error, Field, Group, Scalar, SigningPackage, VerifyingKey,
|
||||||
SigningPackage,
|
|
||||||
},
|
|
||||||
Ciphersuite, Error, Field, Group, Scalar, VerifyingKey,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use crate::{frost_core::frost, RandomizedCiphersuite, RandomizedParams, Randomizer};
|
use crate::{frost_core as frost, RandomizedCiphersuite, RandomizedParams, Randomizer};
|
||||||
use frost_core::{frost::SigningPackage, Field, Group, Signature, VerifyingKey};
|
use frost_core::{Field, Group, Signature, SigningPackage, VerifyingKey};
|
||||||
use rand_core::{CryptoRng, RngCore};
|
use rand_core::{CryptoRng, RngCore};
|
||||||
|
|
||||||
/// Test re-randomized FROST signing with trusted dealer with a Ciphersuite.
|
/// Test re-randomized FROST signing with trusted dealer with a Ciphersuite.
|
||||||
|
|
|
@ -14,7 +14,7 @@ use frost_rerandomized::RandomizedCiphersuite;
|
||||||
use rand_core::{CryptoRng, RngCore};
|
use rand_core::{CryptoRng, RngCore};
|
||||||
use sha2::{Digest, Sha512};
|
use sha2::{Digest, Sha512};
|
||||||
|
|
||||||
use frost_core::frost;
|
use frost_core as frost;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
|
@ -20,7 +20,7 @@ use k256::{
|
||||||
use rand_core::{CryptoRng, RngCore};
|
use rand_core::{CryptoRng, RngCore};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
use frost_core::frost;
|
use frost_core as frost;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
|
@ -194,7 +194,7 @@ fn main() -> ExitCode {
|
||||||
|
|
||||||
// Copy the frost-core repairable docs into ristretto255.
|
// Copy the frost-core repairable docs into ristretto255.
|
||||||
// This will then be copied later down into the other ciphersuites.
|
// This will then be copied later down into the other ciphersuites.
|
||||||
let repairable_docs = read_docs("frost-core/src/frost/keys/repairable.rs", &[]);
|
let repairable_docs = read_docs("frost-core/src/keys/repairable.rs", &[]);
|
||||||
replaced |= write_docs(
|
replaced |= write_docs(
|
||||||
&repairable_docs,
|
&repairable_docs,
|
||||||
"frost-ristretto255/src/keys/repairable.rs",
|
"frost-ristretto255/src/keys/repairable.rs",
|
||||||
|
|
Loading…
Reference in New Issue