add secp256k1 ciphersuite (#175)
* add secp256k1 ciphersuite * use workaround for hash2field * fix secp256k1 docs caused by gendoc bug * Apply suggestions from code review Co-authored-by: Deirdre Connolly <durumcrustulum@gmail.com> * removed random_nonzero which is no longer needed * typo * cargo fmt * rustdoc url Co-authored-by: Deirdre Connolly <durumcrustulum@gmail.com>
This commit is contained in:
parent
1815280576
commit
3e1fe25dbd
|
@ -3,8 +3,9 @@ resolver = "2"
|
|||
members = [
|
||||
"frost-core",
|
||||
#"frost-redjubjub",
|
||||
"frost-ristretto255",
|
||||
"frost-p256",
|
||||
"frost-ed25519",
|
||||
"frost-p256",
|
||||
"frost-ristretto255",
|
||||
"frost-secp256k1",
|
||||
"gendoc"
|
||||
]
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
[package]
|
||||
name = "frost-secp256k1"
|
||||
edition = "2021"
|
||||
# When releasing to crates.io:
|
||||
# - Update html_root_url
|
||||
# - Update CHANGELOG.md
|
||||
# - Create git tag.
|
||||
version = "0.1.0"
|
||||
authors = ["Deirdre Connolly <durumcrustulum@gmail.com>", "Chelsea Komlo <me@chelseakomlo.com>",
|
||||
"Conrado Gouvea <conradoplg@gmail.com>"]
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/ZcashFoundation/frost"
|
||||
categories = ["cryptography"]
|
||||
keywords = ["cryptography", "crypto", "ristretto", "threshold", "signature"]
|
||||
description = "A Schnorr signature scheme over the prime-order Ristretto group that supports FROST ."
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["nightly"]
|
||||
|
||||
[dependencies]
|
||||
byteorder = "1.4"
|
||||
digest = "0.10"
|
||||
frost-core = { path = "../frost-core", features = ["test-impl"] }
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
# k256 = { version = "0.11.6", features = ["arithmetic", "hash2curve"] }
|
||||
k256 = { git = "https://github.com/RustCrypto/elliptic-curves", rev = "42a18e9e13f3bacba89af00d15ef732dbfd03d01", features = ["arithmetic", "hash2curve"] }
|
||||
rand_core = "0.6"
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
sha2 = "0.10.2"
|
||||
thiserror = "1.0"
|
||||
zeroize = { version = "1", default-features = false, features = ["zeroize_derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
bincode = "1"
|
||||
criterion = "0.4"
|
||||
ed25519-dalek = "1.0.1"
|
||||
ed25519-zebra = "3.0.0"
|
||||
lazy_static = "1.4"
|
||||
proptest = "1.0"
|
||||
proptest-derive = "0.3"
|
||||
rand = "0.8"
|
||||
rand_chacha = "0.3"
|
||||
serde_json = "1.0"
|
||||
|
||||
[features]
|
||||
nightly = []
|
||||
default = ["serde"]
|
|
@ -0,0 +1,98 @@
|
|||
An implementation of Schnorr signatures on the secp256k1 curve for both single and threshold numbers
|
||||
of signers (FROST).
|
||||
|
||||
## Example: key generation with trusted dealer and FROST signing
|
||||
|
||||
Creating a key with a trusted dealer and splitting into shares; then signing a message
|
||||
and aggregating the signature. Note that the example just simulates a distributed
|
||||
scenario in a single thread and it abstracts away any communication between peers.
|
||||
|
||||
|
||||
```rust
|
||||
use frost_secp256k1 as frost;
|
||||
use rand::thread_rng;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut rng = thread_rng();
|
||||
let max_signers = 5;
|
||||
let min_signers = 3;
|
||||
let (shares, pubkeys) = frost::keys::keygen_with_dealer(max_signers, min_signers, &mut rng)?;
|
||||
|
||||
// Verifies the secret shares from the dealer and store them in a HashMap.
|
||||
// In practice, the KeyPackages must be sent to its respective participants
|
||||
// through a confidential and authenticated channel.
|
||||
let key_packages: HashMap<_, _> = shares
|
||||
.into_iter()
|
||||
.map(|share| Ok((share.identifier, frost::keys::KeyPackage::try_from(share)?)))
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
let mut nonces = HashMap::new();
|
||||
let mut commitments = HashMap::new();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Round 1: generating nonces and signing commitments for each participant
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// In practice, each iteration of this loop will be executed by its respective participant.
|
||||
for participant_index in 1..(min_signers as u16 + 1) {
|
||||
let participant_identifier = participant_index.try_into().expect("should be nonzero");
|
||||
// Generate one (1) nonce and one SigningCommitments instance for each
|
||||
// participant, up to _threshold_.
|
||||
let (nonce, commitment) = frost::round1::commit(
|
||||
participant_identifier,
|
||||
key_packages[&participant_identifier].secret_share(),
|
||||
&mut rng,
|
||||
);
|
||||
// In practice, the nonces and commitment must be sent to the coordinator
|
||||
// (or to every other participant if there is no coordinator) using
|
||||
// an authenticated channel.
|
||||
nonces.insert(participant_identifier, nonce);
|
||||
commitments.insert(participant_identifier, commitment);
|
||||
}
|
||||
|
||||
// This is what the signature aggregator / coordinator needs to do:
|
||||
// - decide what message to sign
|
||||
// - take one (unused) commitment per signing participant
|
||||
let mut signature_shares = Vec::new();
|
||||
let message = "message to sign".as_bytes();
|
||||
let comms = commitments.clone().into_values().collect();
|
||||
// In practice, the SigningPackage must be sent to all participants
|
||||
// involved in the current signing (at least min_signers participants),
|
||||
// using an authenticate channel (and confidential if the message is secret).
|
||||
let signing_package = frost::SigningPackage::new(comms, message.to_vec());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Round 2: each participant generates their signature share
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// In practice, each iteration of this loop will be executed by its respective participant.
|
||||
for participant_identifier in nonces.keys() {
|
||||
let key_package = &key_packages[participant_identifier];
|
||||
|
||||
let nonces_to_use = &nonces[participant_identifier];
|
||||
|
||||
// Each participant generates their signature share.
|
||||
let signature_share = frost::round2::sign(&signing_package, nonces_to_use, key_package)?;
|
||||
|
||||
// In practice, the signature share must be sent to the Coordinator
|
||||
// using an authenticated channel.
|
||||
signature_shares.push(signature_share);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Aggregation: collects the signing shares from all participants,
|
||||
// generates the final signature.
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Aggregate (also verifies the signature shares)
|
||||
let group_signature = frost::aggregate(&signing_package, &signature_shares[..], &pubkeys)?;
|
||||
|
||||
// Check that the threshold signature can be verified by the group public
|
||||
// key (the verification key).
|
||||
assert!(pubkeys
|
||||
.group_public
|
||||
.verify(message, &group_signature)
|
||||
.is_ok());
|
||||
|
||||
# Ok::<(), frost::Error>(())
|
||||
```
|
|
@ -0,0 +1,386 @@
|
|||
#![allow(non_snake_case)]
|
||||
#![deny(missing_docs)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
use digest::generic_array::GenericArray;
|
||||
use k256::{
|
||||
elliptic_curve::{
|
||||
bigint::{Encoding, U384},
|
||||
group::prime::PrimeCurveAffine,
|
||||
hash2curve::{ExpandMsg, ExpandMsgXmd, Expander},
|
||||
sec1::{FromEncodedPoint, ToEncodedPoint},
|
||||
Field as FFField, PrimeField,
|
||||
},
|
||||
AffinePoint, ProjectivePoint, Scalar,
|
||||
};
|
||||
use rand_core::{CryptoRng, RngCore};
|
||||
use sha2::{digest::Update, Digest, Sha256};
|
||||
|
||||
use frost_core::{frost, Ciphersuite, Field, Group};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use frost_core::Error;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite scalar field.
|
||||
pub struct Secp256K1ScalarField;
|
||||
|
||||
impl Field for Secp256K1ScalarField {
|
||||
type Scalar = Scalar;
|
||||
|
||||
type Serialization = [u8; 32];
|
||||
|
||||
fn zero() -> Self::Scalar {
|
||||
Scalar::ZERO
|
||||
}
|
||||
|
||||
fn one() -> Self::Scalar {
|
||||
Scalar::ONE
|
||||
}
|
||||
|
||||
fn invert(scalar: &Self::Scalar) -> Result<Self::Scalar, Error> {
|
||||
// [`Scalar`]'s Eq/PartialEq does a constant-time comparison
|
||||
if *scalar == <Self as Field>::zero() {
|
||||
Err(Error::InvalidZeroScalar)
|
||||
} else {
|
||||
Ok(scalar.invert().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
fn random<R: RngCore + CryptoRng>(rng: &mut R) -> Self::Scalar {
|
||||
Scalar::random(rng)
|
||||
}
|
||||
|
||||
fn serialize(scalar: &Self::Scalar) -> Self::Serialization {
|
||||
scalar.to_bytes().into()
|
||||
}
|
||||
|
||||
fn deserialize(buf: &Self::Serialization) -> Result<Self::Scalar, Error> {
|
||||
let field_bytes: &k256::FieldBytes = buf.into();
|
||||
match Scalar::from_repr(*field_bytes).into() {
|
||||
Some(s) => Ok(s),
|
||||
None => Err(Error::MalformedScalar),
|
||||
}
|
||||
}
|
||||
|
||||
fn little_endian_serialize(scalar: &Self::Scalar) -> Self::Serialization {
|
||||
let mut array = Self::serialize(scalar);
|
||||
array.reverse();
|
||||
array
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite group.
|
||||
pub struct Secp256K1Group;
|
||||
|
||||
impl Group for Secp256K1Group {
|
||||
type Field = Secp256K1ScalarField;
|
||||
|
||||
type Element = ProjectivePoint;
|
||||
|
||||
/// [SEC 1][1] serialization of a compressed point in secp256k1 takes 33 bytes
|
||||
/// (1-byte prefix and 32 bytes for the coordinate).
|
||||
///
|
||||
/// Note that, in the SEC 1 spec, the identity is encoded as a single null byte;
|
||||
/// but here we pad with zeroes. This is acceptable as the identity _should_ never
|
||||
/// be serialized in FROST, else we error.
|
||||
///
|
||||
/// [1]: https://secg.org/sec1-v2.pdf
|
||||
type Serialization = [u8; 33];
|
||||
|
||||
fn cofactor() -> <Self::Field as Field>::Scalar {
|
||||
Scalar::one()
|
||||
}
|
||||
|
||||
fn identity() -> Self::Element {
|
||||
ProjectivePoint::IDENTITY
|
||||
}
|
||||
|
||||
fn generator() -> Self::Element {
|
||||
ProjectivePoint::GENERATOR
|
||||
}
|
||||
|
||||
fn serialize(element: &Self::Element) -> Self::Serialization {
|
||||
let mut fixed_serialized = [0; 33];
|
||||
let serialized_point = element.to_affine().to_encoded_point(true);
|
||||
let serialized = serialized_point.as_bytes();
|
||||
// Sanity check; either it takes all bytes or a single byte (identity).
|
||||
assert!(serialized.len() == fixed_serialized.len() || serialized.len() == 1);
|
||||
|
||||
// Copy to the left of the buffer (i.e. pad the identity with zeroes).
|
||||
// TODO: Note that identity elements shouldn't be serialized in FROST. This will likely become
|
||||
// part of the API and when that happens, we should return an error instead of
|
||||
// doing this padding.
|
||||
{
|
||||
let (left, _right) = fixed_serialized.split_at_mut(serialized.len());
|
||||
left.copy_from_slice(serialized);
|
||||
}
|
||||
fixed_serialized
|
||||
}
|
||||
|
||||
fn deserialize(buf: &Self::Serialization) -> Result<Self::Element, Error> {
|
||||
let encoded_point =
|
||||
k256::EncodedPoint::from_bytes(buf).map_err(|_| Error::MalformedElement)?;
|
||||
|
||||
match Option::<AffinePoint>::from(AffinePoint::from_encoded_point(&encoded_point)) {
|
||||
Some(point) => {
|
||||
if point.is_identity().into() {
|
||||
// This is actually impossible since the identity is encoded a a single byte
|
||||
// which will never happen since we receive a 33-byte buffer.
|
||||
// We leave the check for consistency.
|
||||
Err(Error::InvalidIdentityElement)
|
||||
} else {
|
||||
Ok(ProjectivePoint::from(point))
|
||||
}
|
||||
}
|
||||
None => Err(Error::MalformedElement),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// hash2field implementation from <https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-11#section-5.3>
|
||||
///
|
||||
/// From <https://github.com/serai-dex/serai/blob/5df74ac9e28f9299e674e98d08e64c99c34e579c/crypto/ciphersuite/src/kp256.rs#L45-L62>
|
||||
//
|
||||
// After https://github.com/RustCrypto/elliptic-curves/pull/673/ merges this should
|
||||
// be removed, and a similar implementation to p256 should be used.
|
||||
fn hash_to_field(msg: &[u8], dst: &[u8]) -> Scalar {
|
||||
let mut modulus = [0; 48];
|
||||
modulus[16..].copy_from_slice(&(Scalar::ZERO - Scalar::ONE).to_bytes());
|
||||
let modulus = U384::from_be_slice(&modulus).wrapping_add(&U384::ONE);
|
||||
|
||||
let unreduced = U384::from_be_bytes({
|
||||
let mut bytes = [0; 48];
|
||||
ExpandMsgXmd::<Sha256>::expand_message(&[msg], dst, 48)
|
||||
.expect("should never return error according to error cases described in ExpandMsgXmd")
|
||||
.fill_bytes(&mut bytes);
|
||||
bytes
|
||||
})
|
||||
.reduce(&modulus)
|
||||
.unwrap()
|
||||
.to_be_bytes();
|
||||
|
||||
let array = *GenericArray::from_slice(&unreduced[16..]);
|
||||
Scalar::from_repr(array).unwrap()
|
||||
}
|
||||
|
||||
/// Context string from the ciphersuite in the [spec].
|
||||
///
|
||||
/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-11.html#section-6.5-1
|
||||
const CONTEXT_STRING: &str = "FROST-secp256k1-SHA256-v11";
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite.
|
||||
pub struct Secp256K1Sha256;
|
||||
|
||||
impl Ciphersuite for Secp256K1Sha256 {
|
||||
type Group = Secp256K1Group;
|
||||
|
||||
type HashOutput = [u8; 32];
|
||||
|
||||
type SignatureSerialization = [u8; 65];
|
||||
|
||||
/// H1 for FROST(secp256k1, SHA-256)
|
||||
///
|
||||
/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-11.html#section-6.5-2.2.2.1
|
||||
fn H1(m: &[u8]) -> <<Self::Group as Group>::Field as Field>::Scalar {
|
||||
let dst = CONTEXT_STRING.to_owned() + "rho";
|
||||
hash_to_field(m, dst.as_bytes())
|
||||
}
|
||||
|
||||
/// H2 for FROST(secp256k1, SHA-256)
|
||||
///
|
||||
/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-11.html#section-6.5-2.2.2.2
|
||||
fn H2(m: &[u8]) -> <<Self::Group as Group>::Field as Field>::Scalar {
|
||||
let dst = CONTEXT_STRING.to_owned() + "chal";
|
||||
hash_to_field(m, dst.as_bytes())
|
||||
}
|
||||
|
||||
/// H3 for FROST(secp256k1, SHA-256)
|
||||
///
|
||||
/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-11.html#section-6.5-2.2.2.3
|
||||
fn H3(m: &[u8]) -> <<Self::Group as Group>::Field as Field>::Scalar {
|
||||
let dst = CONTEXT_STRING.to_owned() + "nonce";
|
||||
hash_to_field(m, dst.as_bytes())
|
||||
}
|
||||
|
||||
/// H4 for FROST(secp256k1, SHA-256)
|
||||
///
|
||||
/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-11.html#section-6.5-2.2.2.4
|
||||
fn H4(m: &[u8]) -> Self::HashOutput {
|
||||
let h = Sha256::new()
|
||||
.chain(CONTEXT_STRING.as_bytes())
|
||||
.chain("msg")
|
||||
.chain(m);
|
||||
|
||||
let mut output = [0u8; 32];
|
||||
output.copy_from_slice(h.finalize().as_slice());
|
||||
output
|
||||
}
|
||||
|
||||
/// H5 for FROST(secp256k1, SHA-256)
|
||||
///
|
||||
/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-11.html#section-6.5-2.2.2.5
|
||||
fn H5(m: &[u8]) -> Self::HashOutput {
|
||||
let h = Sha256::new()
|
||||
.chain(CONTEXT_STRING.as_bytes())
|
||||
.chain("com")
|
||||
.chain(m);
|
||||
|
||||
let mut output = [0u8; 32];
|
||||
output.copy_from_slice(h.finalize().as_slice());
|
||||
output
|
||||
}
|
||||
|
||||
/// HDKG for FROST(secp256k1, SHA-256)
|
||||
fn HDKG(m: &[u8]) -> Option<<<Self::Group as Group>::Field as Field>::Scalar> {
|
||||
let dst = CONTEXT_STRING.to_owned() + "dkg";
|
||||
Some(hash_to_field(m, dst.as_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
type R = Secp256K1Sha256;
|
||||
|
||||
/// A FROST(secp256k1, SHA-256) participant identifier.
|
||||
pub type Identifier = frost::Identifier<R>;
|
||||
|
||||
/// FROST(secp256k1, SHA-256) keys, key generation, key shares.
|
||||
pub mod keys {
|
||||
use super::*;
|
||||
|
||||
/// Allows all participants' keys to be generated using a central, trusted
|
||||
/// dealer.
|
||||
pub fn keygen_with_dealer<RNG: RngCore + CryptoRng>(
|
||||
max_signers: u16,
|
||||
min_signers: u16,
|
||||
mut rng: RNG,
|
||||
) -> Result<(Vec<SecretShare>, PublicKeyPackage), Error> {
|
||||
frost::keys::keygen_with_dealer(max_signers, min_signers, &mut rng)
|
||||
}
|
||||
|
||||
/// Secret and public key material generated by a dealer performing
|
||||
/// [`keygen_with_dealer`].
|
||||
///
|
||||
/// # Security
|
||||
///
|
||||
/// To derive a FROST(secp256k1, SHA-256) keypair, the receiver of the [`SecretShare`] *must* call
|
||||
/// .into(), which under the hood also performs validation.
|
||||
pub type SecretShare = frost::keys::SecretShare<R>;
|
||||
|
||||
/// A FROST(secp256k1, SHA-256) keypair, which can be generated either by a trusted dealer or using
|
||||
/// a DKG.
|
||||
///
|
||||
/// When using a central dealer, [`SecretShare`]s are distributed to
|
||||
/// participants, who then perform verification, before deriving
|
||||
/// [`KeyPackage`]s, which they store to later use during signing.
|
||||
pub type KeyPackage = frost::keys::KeyPackage<R>;
|
||||
|
||||
/// Public data that contains all the signers' public keys as well as the
|
||||
/// group public key.
|
||||
///
|
||||
/// Used for verification purposes before publishing a signature.
|
||||
pub type PublicKeyPackage = frost::keys::PublicKeyPackage<R>;
|
||||
}
|
||||
|
||||
/// FROST(secp256k1, SHA-256) Round 1 functionality and types.
|
||||
pub mod round1 {
|
||||
use frost_core::frost::keys::SigningShare;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Comprised of FROST(secp256k1, SHA-256) hiding and binding nonces.
|
||||
///
|
||||
/// Note that [`SigningNonces`] must be used *only once* for a signing
|
||||
/// operation; re-using nonces will result in leakage of a signer's long-lived
|
||||
/// signing key.
|
||||
pub type SigningNonces = frost::round1::SigningNonces<R>;
|
||||
|
||||
/// Published by each participant in the first round of the signing protocol.
|
||||
///
|
||||
/// This step can be batched if desired by the implementation. Each
|
||||
/// SigningCommitment can be used for exactly *one* signature.
|
||||
pub type SigningCommitments = frost::round1::SigningCommitments<R>;
|
||||
|
||||
/// Performed once by each participant selected for the signing operation.
|
||||
///
|
||||
/// Generates the signing nonces and commitments to be used in the signing
|
||||
/// operation.
|
||||
pub fn commit<RNG>(
|
||||
participant_identifier: frost::Identifier<R>,
|
||||
secret: &SigningShare<R>,
|
||||
rng: &mut RNG,
|
||||
) -> (SigningNonces, SigningCommitments)
|
||||
where
|
||||
RNG: CryptoRng + RngCore,
|
||||
{
|
||||
frost::round1::commit::<R, RNG>(participant_identifier, secret, rng)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generated by the coordinator of the signing operation and distributed to
|
||||
/// each signing party.
|
||||
pub type SigningPackage = frost::SigningPackage<R>;
|
||||
|
||||
/// FROST(secp256k1, SHA-256) Round 2 functionality and types, for signature share generation.
|
||||
pub mod round2 {
|
||||
use super::*;
|
||||
|
||||
/// A FROST(secp256k1, SHA-256) participant's signature share, which the Coordinator will aggregate with all other signer's
|
||||
/// shares into the joint signature.
|
||||
pub type SignatureShare = frost::round2::SignatureShare<R>;
|
||||
|
||||
/// Generated by the coordinator of the signing operation and distributed to
|
||||
/// each signing party
|
||||
pub type SigningPackage = frost::SigningPackage<R>;
|
||||
|
||||
/// Performed once by each participant selected for the signing operation.
|
||||
///
|
||||
/// Receives the message to be signed and a set of signing commitments and a set
|
||||
/// of randomizing commitments to be used in that signing operation, including
|
||||
/// that for this participant.
|
||||
///
|
||||
/// Assumes the participant has already determined which nonce corresponds with
|
||||
/// the commitment that was assigned by the coordinator in the SigningPackage.
|
||||
pub fn sign(
|
||||
signing_package: &SigningPackage,
|
||||
signer_nonces: &round1::SigningNonces,
|
||||
key_package: &keys::KeyPackage,
|
||||
) -> Result<SignatureShare, Error> {
|
||||
frost::round2::sign(signing_package, signer_nonces, key_package)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Schnorr signature on FROST(secp256k1, SHA-256).
|
||||
pub type Signature = frost_core::Signature<R>;
|
||||
|
||||
/// Verifies each FROST(secp256k1, SHA-256) participant's signature share, and if all are valid,
|
||||
/// aggregates the shares into a signature to publish.
|
||||
///
|
||||
/// Resulting signature is compatible with verification of a plain Schnorr
|
||||
/// signature.
|
||||
///
|
||||
/// 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(
|
||||
signing_package: &round2::SigningPackage,
|
||||
signature_shares: &[round2::SignatureShare],
|
||||
pubkeys: &keys::PublicKeyPackage,
|
||||
) -> Result<Signature, Error> {
|
||||
frost::aggregate(signing_package, signature_shares, pubkeys)
|
||||
}
|
||||
|
||||
/// A signing key for a Schnorr signature on FROST(secp256k1, SHA-256).
|
||||
pub type SigningKey = frost_core::SigningKey<R>;
|
||||
|
||||
/// A valid verifying key for Schnorr signatures on FROST(secp256k1, SHA-256).
|
||||
pub type VerifyingKey = frost_core::VerifyingKey<R>;
|
|
@ -0,0 +1,24 @@
|
|||
use lazy_static::lazy_static;
|
||||
use rand::thread_rng;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::*;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SECP256K1_SHA256: Value =
|
||||
serde_json::from_str(include_str!("tests/vectors.json").trim())
|
||||
.expect("Test vector is valid JSON");
|
||||
}
|
||||
|
||||
/// This is testing that Shamir's secret sharing to compute and arbitrary
|
||||
/// value is working.
|
||||
#[test]
|
||||
fn check_share_generation_secp256k1_sha256() {
|
||||
let rng = thread_rng();
|
||||
frost_core::tests::check_share_generation::<Secp256K1Sha256, _>(rng);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_sign_with_test_vectors() {
|
||||
frost_core::tests::vectors::check_sign_with_test_vectors::<Secp256K1Sha256>(&SECP256K1_SHA256)
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"config": {
|
||||
"MAX_PARTICIPANTS": "3",
|
||||
"NUM_PARTICIPANTS": "2",
|
||||
"MIN_PARTICIPANTS": "2",
|
||||
"name": "FROST(secp256k1, SHA-256)",
|
||||
"group": "secp256k1",
|
||||
"hash": "SHA-256"
|
||||
},
|
||||
"inputs": {
|
||||
"group_secret_key": "0d004150d27c3bf2a42f312683d35fac7394b1e9e318249c1bfe7f0795a83114",
|
||||
"group_public_key": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4f",
|
||||
"message": "74657374",
|
||||
"share_polynomial_coefficients": [
|
||||
"fbf85eadae3058ea14f19148bb72b45e4399c0b16028acaf0395c9b03c823579"
|
||||
],
|
||||
"participants": {
|
||||
"1": {
|
||||
"participant_share": "08f89ffe80ac94dcb920c26f3f46140bfc7f95b493f8310f5fc1ea2b01f4254c"
|
||||
},
|
||||
"2": {
|
||||
"participant_share": "04f0feac2edcedc6ce1253b7fab8c86b856a797f44d83d82a385554e6e401984"
|
||||
},
|
||||
"3": {
|
||||
"participant_share": "00e95d59dd0d46b0e303e500b62b7ccb0e555d49f5b849f5e748c071da8c0dbc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"round_one_outputs": {
|
||||
"participant_list": "1,3",
|
||||
"participants": {
|
||||
"1": {
|
||||
"hiding_nonce_randomness": "80cbea5e405d169999d8c4b30b755fedb26ab07ec8198cda4873ed8ce5e16773",
|
||||
"binding_nonce_randomness": "f6d5b38197843046b68903048c1feba433e3500145281fa8bb1e26fdfeef5e7f",
|
||||
"hiding_nonce": "acc83278035223c1ba464e2d11bfacfc872b2b23e1041cf5f6130da21e4d8068",
|
||||
"binding_nonce": "c3ef169995bc3d2c2d48f30b83d0c63751e67ceb057695bcb2a6aa40ed5d926b",
|
||||
"hiding_nonce_commitment": "036673d68a928793c33ae07776908eae8ea15dd947ed81284e939aaba118573a5e",
|
||||
"binding_nonce_commitment": "03d2a96dd4ec1ee29dc22067109d1290dabd8016cb41856ee8ff9281c3fa1baffd",
|
||||
"binding_factor_input": "a645d8249457bbcac34fa7b740f66bcce08fc39506b8bbf1a1c81092f6272eda82ae39234d714f87a7b91dd67d124a06561a36817c1ecaa255c3527d694fc4f10000000000000000000000000000000000000000000000000000000000000001",
|
||||
"binding_factor": "d7bcbd29408dedc9e138262d99b09d8b5705d76eb5de2369d9103e4423f8ac79"
|
||||
},
|
||||
"3": {
|
||||
"hiding_nonce_randomness": "b9794047604beda0c5c0529ac9dfd83c0a80399a7bdf4c3e23cef2faf69cdcc3",
|
||||
"binding_nonce_randomness": "c28ce6252631620b84c2702b34774fab365e286ebc77030a112ebccccbffa78b",
|
||||
"hiding_nonce": "cb3387defef07fc9010c0564ba6495ed41876626ed86b886ca26cbbd3566ffbc",
|
||||
"binding_nonce": "4559459735eb68e8c16319a9fd9a14016053957cb8cea273a24b7c7bc1ee26f6",
|
||||
"hiding_nonce_commitment": "030278e6e6055fb963b40e0c3c37099f803f3f38930fc89092517f8ce1b47e8d6b",
|
||||
"binding_nonce_commitment": "028eb6d238c6c0fc6216906706ad0ff9943c6c1d6079cdf74f674481ebb2485db3",
|
||||
"binding_factor_input": "a645d8249457bbcac34fa7b740f66bcce08fc39506b8bbf1a1c81092f6272eda82ae39234d714f87a7b91dd67d124a06561a36817c1ecaa255c3527d694fc4f10000000000000000000000000000000000000000000000000000000000000003",
|
||||
"binding_factor": "ecc057259f3c8b195308c9b73aaaf840660a37eb264ebce342412c58102ee437"
|
||||
}
|
||||
}
|
||||
},
|
||||
"round_two_outputs": {
|
||||
"participant_list": "1,3",
|
||||
"participants": {
|
||||
"1": {
|
||||
"sig_share": "1750b2a314a81b66fd81366583617aaafcffa68f14495204795aa0434b907aa3"
|
||||
},
|
||||
"3": {
|
||||
"sig_share": "e4dbbbbbcb035eb3512918b0368c4ab2c836a92dccff3251efa7a4aacc7d3790"
|
||||
}
|
||||
}
|
||||
},
|
||||
"final_output": {
|
||||
"sig": "0259696aac722558e8638485d252bb2556f6241a7adfdf284c8c87a3428d46448dfc2c6e5edfab7a1a4eaa4f15b9edc55dc5364fbce1488456690244ee180db233"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
use frost_core::{Ciphersuite, Group};
|
||||
use frost_secp256k1::*;
|
||||
use rand::thread_rng;
|
||||
|
||||
#[test]
|
||||
fn check_sign_with_dealer() {
|
||||
let rng = thread_rng();
|
||||
|
||||
frost_core::tests::check_sign_with_dealer::<Secp256K1Sha256, _>(rng);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_sign_with_dkg() {
|
||||
let rng = thread_rng();
|
||||
|
||||
frost_core::tests::check_sign_with_dkg::<Secp256K1Sha256, _>(rng);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_batch_verify() {
|
||||
let rng = thread_rng();
|
||||
|
||||
frost_core::tests::batch::batch_verify::<Secp256K1Sha256, _>(rng);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_bad_batch_verify() {
|
||||
let rng = thread_rng();
|
||||
|
||||
frost_core::tests::batch::bad_batch_verify::<Secp256K1Sha256, _>(rng);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_deserialize_identity() {
|
||||
// The identity is actually encoded as a single byte; but the API does not
|
||||
// allow us to change that. Try to send something similar.
|
||||
let encoded_identity = [0u8; 33];
|
||||
|
||||
let r = <<Secp256K1Sha256 as Ciphersuite>::Group as Group>::deserialize(&encoded_identity);
|
||||
assert_eq!(r, Err(Error::MalformedElement));
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
use frost_core::tests::proptests::{tweak_strategy, SignatureCase};
|
||||
use frost_secp256k1::*;
|
||||
use proptest::prelude::*;
|
||||
|
||||
use rand_chacha::ChaChaRng;
|
||||
use rand_core::SeedableRng;
|
||||
|
||||
proptest! {
|
||||
|
||||
#[test]
|
||||
fn tweak_signature(
|
||||
tweaks in prop::collection::vec(tweak_strategy(), (0,5)),
|
||||
rng_seed in prop::array::uniform32(any::<u8>()),
|
||||
) {
|
||||
// Use a deterministic RNG so that test failures can be reproduced.
|
||||
// Seeding with 64 bits of entropy is INSECURE and this code should
|
||||
// not be copied outside of this test!
|
||||
let mut rng = ChaChaRng::from_seed(rng_seed);
|
||||
|
||||
// Create a test case for each signature type.
|
||||
let msg = b"test message for proptests";
|
||||
let mut sig = SignatureCase::<Secp256K1Sha256>::new(&mut rng, msg.to_vec());
|
||||
|
||||
// Apply tweaks to each case.
|
||||
for t in &tweaks {
|
||||
sig.apply_tweak(t);
|
||||
}
|
||||
|
||||
assert!(sig.check());
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -168,4 +168,20 @@ fn main() {
|
|||
&["frost_ed25519", "Ed25519 curve"],
|
||||
);
|
||||
}
|
||||
|
||||
write_docs(
|
||||
&docs,
|
||||
"frost-secp256k1/src/lib.rs",
|
||||
&["Secp256K1Sha556", "Secp256K1", "<E>"],
|
||||
old_suite_names_doc,
|
||||
&["FROST(secp256k1, SHA-256)"],
|
||||
);
|
||||
for filename in ["README.md", "dkg.md"] {
|
||||
copy_and_replace(
|
||||
filename,
|
||||
"frost-secp256k1/README.md",
|
||||
original_strings,
|
||||
&["frost_secp256k1", "secp256k1 curve"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue