From e6d5afdfb247c43b2f7272a5ed8ad959f294fe2d Mon Sep 17 00:00:00 2001 From: Deirdre Connolly Date: Mon, 16 May 2022 16:53:17 -0400 Subject: [PATCH] Merge pull request #50 from ZcashFoundation/frost-core * frost-core Cargo.toml * Ciphersuite trait * Signature trait * Copy stub ristretto impl for now * First stab at making signing and verifying generic over frost-core::Ciphersuite * Update signing * Nice const generics and stuff for frost-core::Ciphersuite * Have to implement traits for the pre-parameterized types inside the module * Ciphersuite::Group::Field * Make frost/keys generic over Ciphersuite * frost-core genericization mostly done, modulo batch * Move tests around * Remove internal test module * Lots of tidies, including type refinement of Scalar, Challenge * More genericization and tidy'ing * Test vectors working against Ristretto impl in the frost-core integration tests * clippy fix * Fix generic params for full frost example integration test using ristretto * Genericize proptests * clippy --fix * Doc comment identifier module * In-flight batch and multiscalar mul * Stop using Scalar::from_hash() as it expects impl Digest which sha2 0.10+ isn't doing anymore * run cargo udeps * Update frost-core/src/frost/round1.rs * Update frost-core/src/frost.rs * Update frost-core/src/frost/keys.rs --- frost-core/Cargo.toml | 36 +- frost-core/README.md | 3 - frost-core/src/batch.rs | 163 ++++++++ frost-core/src/error.rs | 33 ++ frost-core/src/frost.rs | 343 +++++++++++++++++ frost-core/src/frost/identifier.rs | 198 ++++++++++ frost-core/src/frost/keys.rs | 513 +++++++++++++++++++++++++ frost-core/src/frost/round1.rs | 303 +++++++++++++++ frost-core/src/frost/round2.rs | 164 ++++++++ frost-core/src/lib.rs | 244 +++++++++++- frost-core/src/scalar_mul.rs | 190 +++++++++ frost-core/src/signature.rs | 99 +++++ frost-core/src/signing_key.rs | 73 ++++ frost-core/src/verifying_key.rs | 93 +++++ frost-core/tests/batch.rs.bck | 62 +++ frost-core/tests/common/ciphersuite.rs | 168 ++++++++ frost-core/tests/common/mod.rs | 13 + frost-core/tests/common/vectors.json | 59 +++ frost-core/tests/common/vectors.rs | 147 +++++++ frost-core/tests/frost.rs | 99 +++++ frost-core/tests/proptests.rs | 129 +++++++ frost-core/tests/tests.rs | 152 ++++++++ frost-ristretto255/Cargo.toml | 4 +- frost-ristretto255/src/lib.rs | 4 +- frost-ristretto255/src/signing_key.rs | 17 +- 25 files changed, 3287 insertions(+), 22 deletions(-) create mode 100644 frost-core/src/batch.rs create mode 100644 frost-core/src/error.rs create mode 100644 frost-core/src/frost.rs create mode 100644 frost-core/src/frost/identifier.rs create mode 100644 frost-core/src/frost/keys.rs create mode 100644 frost-core/src/frost/round1.rs create mode 100644 frost-core/src/frost/round2.rs create mode 100644 frost-core/src/scalar_mul.rs create mode 100644 frost-core/src/signature.rs create mode 100644 frost-core/src/signing_key.rs create mode 100644 frost-core/src/verifying_key.rs create mode 100644 frost-core/tests/batch.rs.bck create mode 100644 frost-core/tests/common/ciphersuite.rs create mode 100644 frost-core/tests/common/mod.rs create mode 100644 frost-core/tests/common/vectors.json create mode 100644 frost-core/tests/common/vectors.rs create mode 100644 frost-core/tests/frost.rs create mode 100644 frost-core/tests/proptests.rs create mode 100644 frost-core/tests/tests.rs diff --git a/frost-core/Cargo.toml b/frost-core/Cargo.toml index aa519de..db7cdff 100644 --- a/frost-core/Cargo.toml +++ b/frost-core/Cargo.toml @@ -1,8 +1,40 @@ [package] name = "frost-core" -version = "0.1.0" edition = "2021" +# When releasing to crates.io: +# - Update html_root_url +# - Update CHANGELOG.md +# - Create git tag. +version = "0.1.0-alpha.1" +authors = ["Deirdre Connolly ", "Chelsea Komlo "] +readme = "README.md" +license = "MIT OR Apache-2.0" +repository = "https://github.com/ZcashFoundation/frost" +categories = ["cryptography"] +keywords = ["cryptography", "crypto", "ristretto", "threshold", "signature", "schnorr"] +description = "Types and traits to support implementing Flexible Round-Optimized Schnorr Threshold signature schemes (FROST)." -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[package.metadata.docs.rs] +features = ["nightly"] [dependencies] +byteorder = "1.4" +digest = "0.9" +hex = { version = "0.4.3", features = ["serde"] } +rand_core = "0.6" +serde = { version = "1", optional = true, features = ["derive"] } +thiserror = "1.0" +zeroize = { version = "1.5.4", default-features = false, features = ["derive"] } + +[dev-dependencies] +curve25519-dalek = { version = "4.0.0-pre.1", features = ["serde"] } +lazy_static = "1.4" +proptest = "1.0" +rand = "0.8" +rand_chacha = "0.3" +serde_json = "1.0" +sha2 = "0.10.2" + +[features] +nightly = [] +default = ["serde"] diff --git a/frost-core/README.md b/frost-core/README.md index 2f410b9..3cb9666 100644 --- a/frost-core/README.md +++ b/frost-core/README.md @@ -21,9 +21,6 @@ dependency, such as `frost-ristretto255`. ```rust - ``` - - diff --git a/frost-core/src/batch.rs b/frost-core/src/batch.rs new file mode 100644 index 0000000..15aad60 --- /dev/null +++ b/frost-core/src/batch.rs @@ -0,0 +1,163 @@ +//! Performs batch Schnorr signature verification. +//! +//! Batch verification asks whether *all* signatures in some set are valid, +//! rather than asking whether *each* of them is valid. This allows sharing +//! computations among all signature verifications, performing less work overall +//! at the cost of higher latency (the entire batch must complete), complexity +//! of caller code (which must assemble a batch of signatures across +//! work-items), and loss of the ability to easily pinpoint failing signatures. + +use std::convert::TryFrom; + +use rand_core::{CryptoRng, RngCore}; + +use crate::{ + frost::{self, *}, + *, +}; + +/// A batch verification item. +/// +/// This struct exists to allow batch processing to be decoupled from the +/// lifetime of the message. This is useful when using the batch verification +/// API in an async context. +#[derive(Clone, Debug)] +pub struct Item { + vk: VerifyingKey, + sig: Signature, + c: Challenge, +} + +impl<'msg, C, M> From<(VerifyingKey, Signature, &'msg M)> for Item +where + C: Ciphersuite, + M: AsRef<[u8]>, +{ + fn from((vk, sig, msg): (VerifyingKey, Signature, &'msg M)) -> Self { + // Compute c now to avoid dependency on the msg lifetime. + + let c = crate::challenge(&sig.R, &vk.element, msg.as_ref()); + + Self { vk, sig, c } + } +} + +impl Item +where + C: Ciphersuite, +{ + /// Perform non-batched verification of this `Item`. + /// + /// This is useful (in combination with `Item::clone`) for implementing + /// fallback logic when batch verification fails. In contrast to + /// [`VerifyingKey::verify`](crate::VerifyingKey::verify), which + /// requires borrowing the message data, the `Item` type is unlinked + /// from the lifetime of the message. + pub fn verify_single(self) -> Result<(), Error> { + VerifyingKey::try_from(self.vk_bytes).and_then(|vk| vk.verify_prehashed(&self.sig, self.c)) + } +} + +#[derive(Default)] +/// A batch verification context. +pub struct Verifier { + /// Signature data queued for verification. + signatures: Vec>, +} + +impl Verifier +where + C: Ciphersuite, +{ + /// Constructs a new batch verifier. + pub fn new() -> Verifier { + Verifier::default() + } + + /// Queues an Item for verification. + pub fn queue>>(&mut self, item: I) { + self.signatures.push(item.into()); + } + + /// Performs batch verification, returning `Ok(())` if all signatures were + /// valid and `Err` otherwise. + /// + /// The batch verification equation is: + /// + /// h_G * -[sum(z_i * s_i)]P_G + sum(\[z_i\]R_i + [z_i * c_i]VK_i) = 0_G + /// + /// which we split out into: + /// + /// h_G * -[sum(z_i * s_i)]P_G + sum(\[z_i\]R_i) + sum([z_i * c_i]VK_i) = + /// 0_G + /// + /// so that we can use multiscalar multiplication speedups. + /// + /// where for each signature i, + /// - VK_i is the verification key; + /// - R_i is the signature's R value; + /// - s_i is the signature's s value; + /// - c_i is the hash of the message and other data; + /// - z_i is a random 128-bit Scalar; + /// - h_G is the cofactor of the group; + /// - P_G is the generator of the subgroup; + /// + /// As follows elliptic curve scalar multiplication convention, + /// scalar variables are lowercase and group point variables + /// are uppercase. This does not exactly match the RedDSA + /// notation in the [protocol specification §B.1][ps]. + /// + /// [ps]: https://zips.z.cash/protocol/protocol.pdf#reddsabatchverify + pub fn verify(self, mut rng: R) -> Result<(), Error> { + let n = self.signatures.len(); + + let mut VK_coeffs = Vec::with_capacity(n); + let mut VKs = Vec::with_capacity(n); + let mut R_coeffs = Vec::with_capacity(self.signatures.len()); + let mut Rs = Vec::with_capacity(self.signatures.len()); + let mut P_coeff_acc = Scalar::zero(); + + for item in self.signatures.iter() { + let (z_bytes, R_bytes, c) = (item.sig.z_bytes, item.sig.R_bytes, item.c); + + let s = Scalar::from_bytes_mod_order(z_bytes); + + let R = { + match CompressedRistretto::from_slice(&R_bytes).decompress() { + Some(point) => point, + None => return Err(Error::InvalidSignature), + } + }; + + let VK = VerifyingKey::try_from(item.vk_bytes.bytes)?.point; + + let z = Scalar::random(&mut rng); + + let P_coeff = z * s; + P_coeff_acc -= P_coeff; + + R_coeffs.push(z); + Rs.push(R); + + VK_coeffs.push(Scalar::zero() + (z * c)); + VKs.push(VK); + } + + use std::iter::once; + + let scalars = once(&P_coeff_acc) + .chain(VK_coeffs.iter()) + .chain(R_coeffs.iter()); + + let basepoints = [curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT]; + let points = basepoints.iter().chain(VKs.iter()).chain(Rs.iter()); + + let check = RistrettoPoint::vartime_multiscalar_mul(scalars, points); + + if check == RistrettoPoint::identity() { + Ok(()) + } else { + Err(Error::InvalidSignature) + } + } +} diff --git a/frost-core/src/error.rs b/frost-core/src/error.rs new file mode 100644 index 0000000..54bbbca --- /dev/null +++ b/frost-core/src/error.rs @@ -0,0 +1,33 @@ +//! FROST Error types + +use thiserror::Error; + +/// An error related to FROST. +#[non_exhaustive] +#[derive(Error, Debug, Copy, Clone, Eq, PartialEq)] +pub enum Error { + /// This identifier is unserializable. + #[error("Malformed identifier is unserializable.")] + MalformedIdentifier, + /// The encoding of a group scalar was malformed. + #[error("Malformed scalar encoding.")] + MalformedScalar, + /// The encoding of a group element was malformed. + #[error("Malformed group element encoding.")] + MalformedElement, + /// The encoding of a signing key was malformed. + #[error("Malformed signing key encoding.")] + MalformedSigningKey, + /// The encoding of a verifying key was malformed. + #[error("Malformed verifying key encoding.")] + MalformedVerifyingKey, + /// The encoding of a signature was malformed. + #[error("Malformed signature encoding.")] + MalformedSignature, + /// Signature verification failed. + #[error("Invalid signature.")] + InvalidSignature, + /// This scalar MUST NOT be zero. + #[error("Invalid for this scalar to be zero.")] + InvalidZeroScalar, +} diff --git a/frost-core/src/frost.rs b/frost-core/src/frost.rs new file mode 100644 index 0000000..310b7a6 --- /dev/null +++ b/frost-core/src/frost.rs @@ -0,0 +1,343 @@ +//! An implementation of FROST (Flexible Round-Optimized Schnorr Threshold) +//! signatures. +//! +//! If you are interested in deploying FROST, please do not hesitate to consult the FROST authors. +//! +//! This implementation currently only supports key generation using a central +//! dealer. In the future, we will add support for key generation via a DKG, +//! as specified in the FROST paper. +//! +//! Internally, keygen_with_dealer generates keys using Verifiable Secret +//! Sharing, where shares are generated using Shamir Secret Sharing. + +use std::{ + collections::HashMap, + convert::TryFrom, + fmt::{self, Debug}, +}; + +use hex::FromHex; + +mod identifier; +pub mod keys; +pub mod round1; +pub mod round2; + +use crate::{Ciphersuite, Error, Field, Group, Signature}; + +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. +/// +/// +#[derive(Clone, PartialEq)] +pub struct Rho(<::Field as Field>::Scalar); + +impl Rho +where + C: Ciphersuite, +{ + /// Deserializes [`Rho`] from bytes. + pub fn from_bytes( + bytes: <::Field as Field>::Serialization, + ) -> Result { + <::Field as Field>::deserialize(&bytes).map(|scalar| Self(scalar)) + } + + /// Serializes [`Rho`] to bytes. + pub fn to_bytes(&self) -> <::Field as Field>::Serialization { + <::Field as Field>::serialize(&self.0) + } +} + +impl Debug for Rho +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("Rho") + .field(&hex::encode(self.to_bytes())) + .finish() + } +} + +impl From<&SigningPackage> for Rho +where + C: Ciphersuite, +{ + // [`compute_binding_factor`] in the spec + // + // [`compute_binding_factor`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-03.html#section-4.4 + fn from(signing_package: &SigningPackage) -> Rho { + let preimage = signing_package.rho_preimage(); + + let binding_factor = C::H1(&preimage[..]); + + Rho(binding_factor) + } +} + +impl FromHex for Rho +where + C: Ciphersuite, +{ + type Error = &'static str; + + fn from_hex>(hex: T) -> Result { + match FromHex::from_hex(hex) { + Ok(bytes) => Self::from_bytes(bytes).map_err(|_| "malformed scalar encoding"), + Err(_) => Err("invalid hex"), + } + } +} + +// TODO: pub struct Lagrange(Scalar); + +/// Generates the lagrange coefficient for the i'th participant. +fn derive_lagrange_coeff( + signer_id: u16, + signing_package: &SigningPackage, +) -> Result<<::Field as Field>::Scalar, &'static str> { + // This should fail and panic if signer_id_scalar is 0 in the scalar field. + let signer_id_scalar = Identifier::::try_from(signer_id).unwrap(); + + let zero = <::Field as Field>::zero(); + + // TODO: This is redundant + if signer_id_scalar.0 == zero { + return Err("Invalid parameters"); + } + + if signing_package + .signing_commitments() + .iter() + .any(|commitment| { + let commitment_id_scalar = Identifier::::try_from(commitment.index).unwrap(); + + *commitment_id_scalar == zero + }) + { + return Err("Invalid parameters"); + } + + let mut num = <::Field as Field>::one(); + let mut den = <::Field as Field>::one(); + + // Ala the sorting of B, just always sort by index in ascending order + // + // https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#encoding-operations-dep-encoding + for commitment in signing_package.signing_commitments() { + if commitment.index == signer_id { + continue; + } + + let commitment_id_scalar = Identifier::::try_from(commitment.index).unwrap(); + + num = num * *commitment_id_scalar; + den = den * (*commitment_id_scalar - *signer_id_scalar); + } + + if den == zero { + return Err("Duplicate shares provided"); + } + + // TODO(dconnolly): return this error if the inversion result == zero + let lagrange_coeff = num * <::Field as Field>::invert(&den).unwrap(); + + Ok(lagrange_coeff) +} + +/// Generated by the coordinator of the signing operation and distributed to +/// each signing party +pub struct SigningPackage { + /// The set of commitments participants published in the first round of the + /// protocol. + signing_commitments: HashMap>, + /// Message which each participant will sign. + /// + /// Each signer should perform protocol-specific verification on the + /// message. + message: Vec, +} + +impl SigningPackage +where + C: Ciphersuite, +{ + /// Create a new `SigingPackage` + /// + /// The `signing_commitments` are sorted by participant `index`. + pub fn new( + mut signing_commitments: Vec>, + message: Vec, + ) -> SigningPackage { + signing_commitments.sort_by_key(|a| a.index); + + SigningPackage { + signing_commitments: signing_commitments + .into_iter() + .map(|s| (s.index, s)) + .collect(), + message, + } + } + + /// Get a signing commitment by its participant index. + pub fn signing_commitment(&self, index: &u16) -> round1::SigningCommitments { + self.signing_commitments[index] + } + + /// Get the signing commitments, sorted by the participant indices + pub fn signing_commitments(&self) -> Vec> { + let mut signing_commitments: Vec> = + self.signing_commitments.values().cloned().collect(); + signing_commitments.sort_by_key(|a| a.index); + signing_commitments + } + + /// Get the message to be signed + pub fn message(&self) -> &Vec { + &self.message + } + + /// Compute the preimage to H3 to compute rho + // We separate this out into its own method so it can be tested + pub fn rho_preimage(&self) -> Vec { + let mut preimage = vec![]; + + preimage + .extend_from_slice(&round1::encode_group_commitments(self.signing_commitments())[..]); + preimage.extend_from_slice(C::H3(self.message.as_slice()).as_ref()); + + preimage + } +} + +/// The product of all signers' individual commitments, published as part of the +/// final signature. +#[derive(PartialEq)] +pub struct GroupCommitment(pub(super) ::Element); + +// impl Debug for GroupCommitment where C: Ciphersuite { +// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +// f.debug_tuple("GroupCommitment") +// .field(&hex::encode(self.0.compress().to_bytes())) +// .finish() +// } +// } + +impl TryFrom<&SigningPackage> for GroupCommitment +where + C: Ciphersuite, +{ + type Error = &'static str; + + /// 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-03.html#section-4.4 + fn try_from(signing_package: &SigningPackage) -> Result, &'static str> { + let rho: Rho = signing_package.into(); + + let identity = ::identity(); + + let mut accumulator = ::identity(); + + // Ala the sorting of B, just always sort by index in ascending order + // + // https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#encoding-operations-dep-encoding + for 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("Commitment equals the identity."); + } + + accumulator = accumulator + (commitment.hiding.0 + (commitment.binding.0 * rho.0)) + } + + Ok(GroupCommitment(accumulator)) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Aggregation +//////////////////////////////////////////////////////////////////////////////// + +/// Verifies each 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 SpendAuth +/// 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: &SigningPackage, + signature_shares: &[round2::SignatureShare], + pubkeys: &keys::PublicKeyPackage, +) -> Result, &'static str> +where + C: Ciphersuite, +{ + // Encodes the signing commitment list produced in round one as part of generating [`Rho`], the + // binding factor. + let rho: Rho = signing_package.into(); + + // Compute the group commitment from signing commitments produced in round one. + let group_commitment = GroupCommitment::::try_from(signing_package)?; + + // Compute the per-message challenge. + let challenge = crate::challenge::( + &group_commitment.0, + &pubkeys.group_public.element, + signing_package.message().as_slice(), + ); + + // Verify the signature shares. + for 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.signer_pubkeys.get(&signature_share.index).unwrap(); + + // Compute Lagrange coefficient. + let lambda_i = derive_lagrange_coeff(signature_share.index, signing_package)?; + + // Compute the commitment share. + let R_share = signing_package + .signing_commitment(&signature_share.index) + .to_group_commitment_share(&rho); + + // Compute relation values to verify this signature share. + signature_share.verify(&R_share, signer_pubkey, lambda_i, &challenge)?; + } + + // The aggregation of the signature shares by summing them up, resulting in + // a plain Schnorr signature. + // + // Implements [`frost_aggregate`] from the spec. + // + // [`frost_aggregate`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-03.html#section-5.3-4 + let mut z = <::Field as Field>::zero(); + + for signature_share in signature_shares { + z = z + signature_share.signature.z_share; + } + + Ok(Signature { + R: group_commitment.0, + z, + }) +} diff --git a/frost-core/src/frost/identifier.rs b/frost-core/src/frost/identifier.rs new file mode 100644 index 0000000..ece6a97 --- /dev/null +++ b/frost-core/src/frost/identifier.rs @@ -0,0 +1,198 @@ +//! FROST participant identifiers + +use std::{ + fmt::{self, Debug}, + hash::{Hash, Hasher}, + ops::{Deref, Index}, +}; + +use crate::{Ciphersuite, Error, Field, Group, Scalar}; + +/// A FROST participant identifier. +/// +/// The identifier is a field element in the scalar field that the secret polynomial is defined +/// over, corresponding to some x-coordinate for a polynomial f(x) = y. MUST NOT be zero in the +/// field, as f(0) = the shared secret. +#[derive(Copy, Clone)] +pub struct Identifier(pub(crate) Scalar); + +impl AsRef> for Identifier +where + C: Ciphersuite, +{ + fn as_ref(&self) -> &Scalar { + &self.0 + } +} + +impl Debug for Identifier +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("Identifier") + .field(&usize::from(*self)) + .finish() + } +} + +impl Deref for Identifier +where + C: Ciphersuite, +{ + type Target = Scalar; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// impl Deref for &Identifier +// where +// C: Ciphersuite, +// { +// type Target = Scalar; + +// fn deref(&self) -> &Self::Target { +// &self.0 +// } +// } + +impl Eq for Identifier where C: Ciphersuite {} + +impl From> for usize +where + C: Ciphersuite, +{ + // TODO: this feels janky, are we confident we aren't clamping off the higher byte values? + fn from(id: Identifier) -> usize { + // This is 8 bytes because usize is up to 8 bytes depending on the platform. + // + // https://doc.rust-lang.org/stable/std/primitive.usize.html#method.from_le_bytes + let mut bytes = [0u8; 8]; + + let serialized = <::Field as Field>::serialize(&id.0); + + for i in 0..bytes.len() { + bytes[i] = serialized.as_ref()[i]; + } + + usize::from_le_bytes(bytes) + } +} + +impl Hash for Identifier +where + C: Ciphersuite, +{ + fn hash(&self, state: &mut H) { + <::Field as Field>::serialize(&self.0) + .as_ref() + .hash(state) + } +} + +impl Index> for Vec +where + C: Ciphersuite, +{ + type Output = T; + + fn index(&self, id: Identifier) -> &Self::Output { + &self[usize::from(id)] + } +} + +// impl std::ops::Mul for Identifier +// where +// C: Ciphersuite, +// { +// type Output = Self; + +// fn mul(self, rhs: Identifier) -> Self::Output { +// Self(self.0 * rhs.0) +// } +// } + +// impl std::ops::Mul> for Identifier +// where +// C: Ciphersuite, +// { +// type Output = Scalar; + +// fn mul(self, scalar: Scalar) -> Scalar { +// self.0 * scalar +// } +// } + +// impl<'a, 'b, C> std::ops::Mul<&'b Identifier> for &'a Scalar +// where +// C: Ciphersuite, +// { +// type Output = Scalar; + +// fn mul(self, id: &'b Identifier) -> Scalar { +// self * id.0 +// } +// } + +impl PartialEq for Identifier +where + C: Ciphersuite, +{ + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +// impl std::ops::Sub for Identifier +// where +// C: Ciphersuite, +// { +// type Output = Self; + +// fn sub(self, rhs: Identifier) -> Self::Output { +// Self(self.0 - rhs.0) +// } +// } + +// impl std::ops::Sub> for Identifier +// where +// C: Ciphersuite, +// { +// type Output = Scalar; + +// fn sub(self, scalar: Scalar) -> Scalar { +// self.0 - scalar +// } +// } + +impl TryFrom for Identifier +where + C: Ciphersuite, +{ + type Error = Error; + + // TODO: this feels like a cluster. Improve? + fn try_from(n: u16) -> Result, Self::Error> { + let mut bytes = + Vec::from(<::Field as Field>::Serialization::default().as_ref()); + + for (i, byte) in n.to_le_bytes().iter().enumerate() { + bytes[i] = *byte; + } + + let serialization = bytes + .try_into() + .map_err(|_| Self::Error::MalformedIdentifier)?; + + let scalar = <::Field as Field>::deserialize(&serialization)?; + + // Participant identifiers are public, so this comparison doesn't need to be constant-time. + if scalar == <::Field as Field>::zero() { + Err(Self::Error::InvalidZeroScalar) + } else { + Ok(Self(scalar)) + } + } +} diff --git a/frost-core/src/frost/keys.rs b/frost-core/src/frost/keys.rs new file mode 100644 index 0000000..3597b3e --- /dev/null +++ b/frost-core/src/frost/keys.rs @@ -0,0 +1,513 @@ +//! FROST keys, keygen, key shares + +use std::{ + collections::HashMap, + convert::TryFrom, + default::Default, + fmt::{self, Debug}, +}; + +use hex::FromHex; +use rand_core::{CryptoRng, RngCore}; +use zeroize::{DefaultIsZeroes, Zeroize}; + +use crate::{frost::Identifier, Ciphersuite, Error, Field, Group, Scalar, VerifyingKey}; + +/// A secret scalar value representing a signer's secret key. +#[derive(Clone, Copy, PartialEq)] +pub struct Secret(pub(crate) Scalar); + +impl Secret +where + C: Ciphersuite, +{ + /// Deserialize [`Secret`] from bytes + pub fn from_bytes( + bytes: <::Field as Field>::Serialization, + ) -> Result { + <::Field as Field>::deserialize(&bytes).map(|scalar| Self(scalar)) + } + + /// Serialize [`Secret`] to bytes + pub fn to_bytes(&self) -> <::Field as Field>::Serialization { + <::Field as Field>::serialize(&self.0) + } + + /// Generates a new uniformly random secret value using the provided RNG. + // TODO: should this only be behind test? + pub fn random(mut rng: R) -> Self + where + R: CryptoRng + RngCore, + { + Self(<::Field as Field>::random_nonzero( + &mut rng, + )) + } +} + +impl Debug for Secret +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Secret") + .field(&hex::encode(self.to_bytes())) + .finish() + } +} + +impl Default for Secret +where + C: Ciphersuite, +{ + fn default() -> Self { + Self(<::Field as Field>::zero()) + } +} + +// Implements [`Zeroize`] by overwriting a value with the [`Default::default()`] value +impl DefaultIsZeroes for Secret where C: Ciphersuite {} + +// impl Drop for Secret +// where +// C: Ciphersuite, +// { +// fn drop(&mut self) { +// self.zeroize() +// } +// } + +impl From<&Secret> for VerifyingKey +where + C: Ciphersuite, +{ + fn from(secret: &Secret) -> Self { + let element = ::generator() * secret.0; + + VerifyingKey { element } + } +} + +impl FromHex for Secret +where + C: Ciphersuite, +{ + type Error = &'static str; + + fn from_hex>(hex: T) -> Result { + match FromHex::from_hex(hex) { + Ok(bytes) => Self::from_bytes(bytes).map_err(|_| "malformed secret encoding"), + Err(_) => Err("invalid hex"), + } + } +} + +/// A public group element that represents a single signer's public key. +#[derive(Copy, Clone, PartialEq)] +pub struct Public(pub(super) ::Element) +where + C: Ciphersuite; + +impl Public +where + C: Ciphersuite, +{ + /// Deserialize from bytes + pub fn from_bytes(bytes: ::Serialization) -> Result { + ::deserialize(&bytes).map(|element| Self(element)) + } + + /// Serialize [`Public`] to bytes + pub fn to_bytes(&self) -> ::Serialization { + ::serialize(&self.0) + } +} + +impl Debug for Public +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("Public") + .field(&hex::encode(self.to_bytes())) + .finish() + } +} + +impl From> for Public +where + C: Ciphersuite, +{ + fn from(secret: Secret) -> Public { + Public(::generator() * secret.0 as Scalar) + } +} + +/// A [`Group::Element`] that is a commitment to one coefficient of our secret polynomial. +/// +/// This is a (public) commitment to one coefficient of a secret polynomial used for performing +/// verifiable secret sharing for a Shamir secret share. +#[derive(Clone, Copy, PartialEq)] +pub(super) struct CoefficientCommitment(pub(super) ::Element); + +/// Contains the commitments to the coefficients for our secret polynomial _f_, +/// used to generate participants' key shares. +/// +/// [`VerifiableSecretSharingCommitment`] contains a set of commitments to the coefficients (which +/// themselves are scalars) for a secret polynomial f, where f is used to +/// generate each ith participant's key share f(i). Participants use this set of +/// commitments to perform verifiable secret sharing. +/// +/// Note that participants MUST be assured that they have the *same* +/// [`VerifiableSecretSharingCommitment`], either by performing pairwise comparison, or by using +/// some agreed-upon public location for publication, where each participant can +/// ensure that they received the correct (and same) value. +#[derive(Clone)] +pub struct VerifiableSecretSharingCommitment( + pub(super) Vec>, +); + +/// A secret share generated by performing a (t-out-of-n) secret sharing scheme. +/// +/// `n` is the total number of shares and `t` is the threshold required to reconstruct the secret; +/// in this case we use Shamir's secret sharing. +/// +/// As a solution to the secret polynomial _f_ (a 'point'), the `index` is the x-coordinate, and the +/// `value` is the y-coordinate. +#[derive(Clone, Zeroize)] +pub struct SecretShare { + /// The participant index of this [`SecretShare`]. + pub index: u16, + /// Secret Key. + pub value: Secret, + /// The commitments to be distributed among signers. + pub commitment: VerifiableSecretSharingCommitment, +} + +impl SecretShare +where + C: Ciphersuite, +{ + /// Gets the inner [`Secret`] share value. + pub fn secret(&self) -> &Secret { + &self.value + } + + /// Verifies that a secret share is consistent with a verifiable secret sharing commitment. + /// + /// This ensures that this participant's share has been generated using the same + /// mechanism as all other signing participants. Note that participants *MUST* + /// ensure that they have the same view as all other participants of the + /// commitment! + /// + /// An implementation of `vss_verify()` from the [spec]. + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-04.html#appendix-B.2-4 + pub fn verify(&self) -> Result<(), &'static str> { + let f_result = ::generator() * self.value.0; + + let x = Identifier::::try_from(self.index).unwrap(); + + let (_, result) = self.commitment.0.iter().fold( + ( + <::Field as Field>::one(), + ::identity(), + ), + |(x_to_the_i, sum_so_far), comm_i| { + (*x * x_to_the_i, sum_so_far + comm_i.0 * x_to_the_i) + }, + ); + + if !(f_result == result) { + return Err("SecretShare is invalid."); + } + + Ok(()) + } +} + +/// Secret and public key material generated by a dealer performing +/// [`keygen_with_dealer`]. +/// +/// To derive a FROST keypair, the receiver of the [`SharePackage`] *must* call +/// .into(), which under the hood also performs validation. +#[derive(Clone)] +pub struct SharePackage { + /// Denotes the participant index each share is owned by. + pub index: u16, + /// This participant's secret share. + pub secret_share: SecretShare, + /// This participant's public key. + pub public: Public, + /// The public signing key that represents the entire group. + pub group_public: VerifyingKey, +} + +/// Allows all participants' keys to be generated using a central, trusted +/// dealer. +/// +/// Under the hood, this performs verifiable secret sharing, which itself uses +/// Shamir secret sharing, from which each share becomes a participant's secret +/// key. The output from this function is a set of shares along with one single +/// commitment that participants use to verify the integrity of the share. The +/// number of signers is limited to 255. +/// +/// Implements [`trusted_dealer_keygen`] from the spec. +/// +/// [`trusted_dealer_keygen`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-03.html#appendix-B +pub fn keygen_with_dealer( + num_signers: u8, + threshold: u8, + mut rng: R, +) -> Result<(Vec>, PublicKeyPackage), &'static str> { + let mut bytes = [0; 64]; + rng.fill_bytes(&mut bytes); + + let secret = Secret::random(&mut rng); + let group_public = VerifyingKey::from(&secret); + let secret_shares = generate_secret_shares(&secret, num_signers, threshold, rng)?; + let mut share_packages: Vec> = Vec::with_capacity(num_signers as usize); + let mut signer_pubkeys: HashMap> = HashMap::with_capacity(num_signers as usize); + + for secret_share in secret_shares { + let signer_public = secret_share.value.into(); + + share_packages.push(SharePackage { + index: secret_share.index, + secret_share: secret_share.clone(), + public: signer_public, + group_public, + }); + + signer_pubkeys.insert(secret_share.index, signer_public); + } + + Ok(( + share_packages, + PublicKeyPackage { + signer_pubkeys, + group_public, + }, + )) +} + +/// A FROST keypair, which can be generated either by a trusted dealer or using +/// a DKG. +/// +/// When using a central dealer, [`SharePackage`]s are distributed to +/// participants, who then perform verification, before deriving +/// [`KeyPackage`]s, which they store to later use during signing. +#[derive(Clone)] +pub struct KeyPackage { + /// Denotes the participant index each secret share key package is owned by. + pub index: u16, + /// This participant's secret share. + pub secret_share: Secret, + /// This participant's public key. + pub public: Public, + /// The public signing key that represents the entire group. + pub group_public: VerifyingKey, +} + +impl KeyPackage +where + C: Ciphersuite, +{ + /// Gets the participant index associated with this [`KeyPackage`]. + pub fn index(&self) -> &u16 { + &self.index + } + + /// Gets the participant's [`Secret`] share associated with this [`KeyPackage`]. + pub fn secret_share(&self) -> &Secret { + &self.secret_share + } + + /// Gets the participant's [`Public`] key associated with this [`Secret`] share in this [`KeyPackage`]. + pub fn public(&self) -> &Public { + &self.public + } + + /// Gets the group [`VerifyingKey`] associated with the entire group in this [`KeyPackage`]. + pub fn group_public(&self) -> &VerifyingKey { + &self.group_public + } +} + +impl TryFrom> for KeyPackage +where + C: Ciphersuite, +{ + type Error = &'static str; + + /// Tries to verify a share and construct a [`KeyPackage`] from it. + /// + /// When participants receive a [`SharePackage`] from the dealer, they + /// *MUST* verify the integrity of the share before continuing on to + /// transform it into a signing/verification keypair. Here, we assume that + /// every participant has the same view of the commitment issued by the + /// dealer, but implementations *MUST* make sure that all participants have + /// a consistent view of this commitment in practice. + fn try_from(share_package: SharePackage) -> Result { + share_package.secret_share.verify()?; + + Ok(KeyPackage { + index: share_package.index, + secret_share: share_package.secret_share.value, + public: share_package.public, + group_public: share_package.group_public, + }) + } +} + +/// 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 struct PublicKeyPackage { + /// When performing signing, the coordinator must ensure that they have the + /// correct view of participants' public keys to perform verification before + /// publishing a signature. `signer_pubkeys` represents all signers for a + /// signing operation. + pub signer_pubkeys: HashMap>, + /// The joint public key for the entire group. + pub group_public: VerifyingKey, +} + +/// Creates secret shares for a given secret. +/// +/// This function accepts a secret from which shares are generated. While in +/// FROST this secret should always be generated randomly, we allow this secret +/// to be specified for this internal function for testability. +/// +/// Internally, [`generate_secret_shares`] performs verifiable secret sharing, which +/// generates shares via Shamir Secret Sharing, and then generates public +/// commitments to those shares. +/// +/// More specifically, [`generate_secret_shares`]: +/// - Randomly samples of coefficients [a, b, c], this represents a secret +/// polynomial f +/// - For each participant i, their secret share is f(i) +/// - The commitment to the secret polynomial f is [g^a, g^b, g^c] +/// +/// Implements [`secret_key_shard`] from the spec. +/// +/// [`secret_key_shard`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-03.html#appendix-B.1 +pub fn generate_secret_shares( + secret: &Secret, + numshares: u8, + threshold: u8, + mut rng: R, +) -> Result>, &'static str> { + if threshold < 2 { + return Err("Threshold cannot be less than 2"); + } + + if numshares < 2 { + return Err("Number of shares cannot be less than the minimum threshold 2"); + } + + if threshold > numshares { + return Err("Threshold cannot exceed numshares"); + } + + let numcoeffs = threshold - 1; + + let mut coefficients: Vec> = Vec::with_capacity(threshold as usize); + + let mut secret_shares: Vec> = Vec::with_capacity(numshares as usize); + + let mut commitment: VerifiableSecretSharingCommitment = + VerifiableSecretSharingCommitment(Vec::with_capacity(threshold as usize)); + + for _ in 0..numcoeffs { + coefficients.push(<::Field as Field>::random(&mut rng)); + } + + // Verifiable secret sharing, to make sure that participants can ensure their + // secret is consistent with every other participant's. + commitment.0.push(CoefficientCommitment( + ::generator() * secret.0, + )); + + for c in &coefficients { + commitment + .0 + .push(CoefficientCommitment(::generator() * *c)); + } + + // Evaluate the polynomial with `secret` as the constant term + // and `coeffs` as the other coefficients at the point x=share_index, + // using Horner's method. + for id in (1..=numshares as u16).map_while(|i| Identifier::::try_from(i).ok()) { + let mut value = <::Field as Field>::zero(); + + // Polynomial evaluation, for this index + // + // We rely only on `Add` and `Mul` here so as to not require `AddAssign` and `MulAssign` + // + // Note that this is from the 'last' coefficient to the 'first'. + for i in (0..numcoeffs).rev() { + value = value + coefficients[i as usize]; + value = *id * value; + } + value = value + secret.0; + + secret_shares.push(SecretShare { + index: usize::from(id) as u16, + value: Secret(value), + commitment: commitment.clone(), + }); + } + + Ok(secret_shares) +} + +/// Recompute the secret from t-of-n secret shares using Lagrange interpolation. +pub fn reconstruct_secret( + secret_shares: Vec>, +) -> Result, &'static str> { + if secret_shares.is_empty() { + return Err("No secret_shares provided"); + } + + let secret_share_map: HashMap, SecretShare> = secret_shares + .into_iter() + .map(|share| (Identifier::::try_from(share.index).unwrap(), share)) + .collect(); + + let mut secret = <::Field as Field>::zero(); + + // Compute the Lagrange coefficients + for (i, secret_share) in secret_share_map.clone() { + let mut num = <::Field as Field>::one(); + let mut den = <::Field as Field>::one(); + + for j in secret_share_map.clone().into_keys() { + if j == i { + continue; + } + + // numerator *= j + num = num * *j; + + // denominator *= j - i + den = den * (*j - *i); + } + + // If at this step, the denominator is zero in the scalar field, there must be a duplicate + // secret share. + if den == <::Field as Field>::zero() { + return Err("Duplicate shares provided"); + } + + // Save numerator * 1/denomintor in the scalar field + let lagrange_coefficient = + num * <::Field as Field>::invert(&den).unwrap(); + + // Compute y = f(0) via polynomial interpolation of these t-of-n solutions ('points) of f + secret = secret + (lagrange_coefficient * secret_share.value.0); + } + + Ok(Secret::from_bytes(<::Field as Field>::serialize(&secret)).unwrap()) +} diff --git a/frost-core/src/frost/round1.rs b/frost-core/src/frost/round1.rs new file mode 100644 index 0000000..c60d7af --- /dev/null +++ b/frost-core/src/frost/round1.rs @@ -0,0 +1,303 @@ +//! FROST Round 1 functionality and types + +use std::fmt::{self, Debug}; + +use hex::FromHex; +use rand_core::{CryptoRng, RngCore}; +use zeroize::Zeroize; + +use crate::{frost, Ciphersuite, Error, Field, Group}; + +/// A scalar that is a signing nonce. +#[derive(Clone, PartialEq, Zeroize)] +pub struct Nonce(pub(super) <::Field as Field>::Scalar); + +impl Nonce +where + C: Ciphersuite, +{ + /// Generates a new uniformly random signing nonce. + /// + /// Each participant generates signing nonces before performing a signing + /// operation. + /// + /// An implementation of `RandomNonzeroScalar()` from the [spec]. + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-04.html#section-3.1-3.4 + pub fn random(rng: &mut R) -> Self + where + R: CryptoRng + RngCore, + { + // The values of 'hiding' and 'binding' nonces must be non-zero so that commitments are + // not the identity. + Self(<::Field as Field>::random_nonzero(rng)) + } + + /// Deserialize [`Nonce`] from bytes + pub fn from_bytes( + bytes: <::Field as Field>::Serialization, + ) -> Result { + <::Field as Field>::deserialize(&bytes).map(|scalar| Self(scalar)) + } + + /// Serialize [`Nonce`] to bytes + pub fn to_bytes(&self) -> <::Field as Field>::Serialization { + <::Field as Field>::serialize(&self.0) + } +} + +// impl Drop for Nonce +// where +// C: Ciphersuite, +// { +// fn drop(&mut self) { +// self.zeroize() +// } +// } + +impl FromHex for Nonce +where + C: Ciphersuite, +{ + type Error = &'static str; + + fn from_hex>(hex: T) -> Result { + match FromHex::from_hex(hex) { + Ok(bytes) => Self::from_bytes(bytes).map_err(|_| "malformed nonce encoding"), + Err(_) => Err("invalid hex"), + } + } +} + +/// A Ristretto point that is a commitment to a signing nonce share. +#[derive(Clone, Copy, PartialEq)] +pub struct NonceCommitment(pub(super) ::Element); + +impl NonceCommitment +where + C: Ciphersuite, +{ + /// Deserialize [`NonceCommitment`] from bytes + pub fn from_bytes(bytes: ::Serialization) -> Result { + ::deserialize(&bytes).map(|element| Self(element)) + } + + /// Serialize [`NonceCommitment`] to bytes + pub fn to_bytes(&self) -> ::Serialization { + ::serialize(&self.0) + } +} + +impl Debug for NonceCommitment +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("NonceCommitment") + .field(&hex::encode(self.to_bytes())) + .finish() + } +} + +impl From> for NonceCommitment +where + C: Ciphersuite, +{ + fn from(nonce: Nonce) -> Self { + From::from(&nonce) + } +} + +impl From<&Nonce> for NonceCommitment +where + C: Ciphersuite, +{ + fn from(nonce: &Nonce) -> Self { + Self(::generator() * nonce.0) + } +} + +impl FromHex for NonceCommitment +where + C: Ciphersuite, +{ + type Error = &'static str; + + fn from_hex>(hex: T) -> Result { + match FromHex::from_hex(hex) { + Ok(bytes) => Self::from_bytes(bytes).map_err(|_| "malformed nonce commitment encoding"), + Err(_) => Err("invalid hex"), + } + } +} + +/// Comprised of 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. +#[derive(Clone, Zeroize)] +pub struct SigningNonces { + /// The hiding [`Nonce`]. + pub hiding: Nonce, + /// The binding [`Nonce`]. + pub binding: Nonce, +} + +impl SigningNonces +where + C: Ciphersuite, +{ + /// Generates a new signing nonce. + /// + /// Each participant generates signing nonces before performing a signing + /// operation. + pub fn new(rng: &mut R) -> Self + where + R: CryptoRng + RngCore, + { + // The values of 'hiding' and 'binding' must be non-zero so that commitments are + // not the identity. + let hiding = Nonce::::random(rng); + let binding = Nonce::::random(rng); + + Self { hiding, binding } + } + + /// Gets the hiding [`Nonce`] + pub fn hiding(&self) -> &Nonce { + &self.hiding + } + + /// Gets the binding [`Nonce`] + pub fn binding(&self) -> &Nonce { + &self.binding + } +} + +/// 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. +#[derive(Copy, Clone)] +pub struct SigningCommitments { + /// The participant index. + pub index: u16, + /// Commitment to the hiding [`Nonce`]. + pub hiding: NonceCommitment, + /// Commitment to the binding [`Nonce`]. + pub binding: NonceCommitment, +} + +impl SigningCommitments +where + C: Ciphersuite, +{ + /// Computes the [signature commitment share] from these round one signing commitments. + /// + /// [signature commitment share]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-03.html#name-signature-share-verificatio + pub(super) fn to_group_commitment_share( + self, + binding_factor: &frost::Rho, + ) -> GroupCommitmentShare { + GroupCommitmentShare::(self.hiding.0 + (self.binding.0 * binding_factor.0)) + } + + /// Gets the hiding [`NonceCommitment`]. + pub fn hiding(&self) -> &NonceCommitment { + &self.hiding + } + + /// Gets the binding [`NonceCommitment`]. + pub fn binding(&self) -> &NonceCommitment { + &self.binding + } +} + +impl From<(u16, &SigningNonces)> for SigningCommitments +where + C: Ciphersuite, +{ + fn from((index, nonces): (u16, &SigningNonces)) -> Self { + Self { + index, + hiding: nonces.hiding.clone().into(), + binding: nonces.binding.clone().into(), + } + } +} + +/// One signer's share of the group commitment, derived from their individual signing commitments +/// and the binding factor _rho_. +#[derive(Clone, Copy, PartialEq)] +pub struct GroupCommitmentShare(pub(super) ::Element); + +/// Encode the list of group signing commitments. +/// +/// Implements [`encode_group_commitment_list()`] from the spec. +/// +/// Inputs: +/// - commitment_list = [(j, D_j, E_j), ...], a list of commitments issued by each signer, +/// where each element in the list indicates the signer index and their +/// two commitment Element values. B MUST be sorted in ascending order +/// by signer index. +/// +/// Outputs: +/// - A byte string containing the serialized representation of B. +/// +/// [`encode_group_commitment_list()`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-03.html#section-4.3 +pub(super) fn encode_group_commitments( + signing_commitments: Vec>, +) -> Vec { + // B MUST be sorted in ascending order by signer index. + // + // https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#encoding-operations-dep-encoding + // + // TODO: AtLeastOne or other explicitly Sorted wrapper types? + let mut sorted_signing_commitments = signing_commitments; + sorted_signing_commitments.sort_by_key(|a| a.index); + + let mut bytes = vec![]; + + for item in sorted_signing_commitments { + bytes.extend_from_slice(&item.index.to_be_bytes()); // TODO: 2-bytes until spec moves off u16 + bytes.extend_from_slice(::serialize(&item.hiding.0).as_ref()); + bytes.extend_from_slice(::serialize(&item.binding.0).as_ref()); + } + + bytes +} + +/// Done once by each participant, to generate _their_ nonces and commitments +/// that are then used during signing. +/// +/// When performing signing using two rounds, num_nonces would equal 1, to +/// perform the first round. Batching entails generating more than one +/// nonce/commitment pair at a time. Nonces should be stored in secret storage +/// for later use, whereas the commitments are published. +/// +/// The number of nonces is limited to 255. This limit can be increased if it +/// turns out to be too conservative. +// TODO: Make sure the above is a correct statement, fix if needed in: +// https://github.com/ZcashFoundation/redjubjub/issues/111 +pub fn preprocess( + num_nonces: u8, + participant_index: u16, + rng: &mut R, +) -> (Vec>, Vec>) +where + C: Ciphersuite, + R: CryptoRng + RngCore, +{ + let mut signing_nonces: Vec> = Vec::with_capacity(num_nonces as usize); + let mut signing_commitments: Vec> = + Vec::with_capacity(num_nonces as usize); + + for _ in 0..num_nonces { + let nonces = SigningNonces::new(rng); + signing_commitments.push(SigningCommitments::from((participant_index, &nonces))); + signing_nonces.push(nonces); + } + + (signing_nonces, signing_commitments) +} diff --git a/frost-core/src/frost/round2.rs b/frost-core/src/frost/round2.rs new file mode 100644 index 0000000..1b8414f --- /dev/null +++ b/frost-core/src/frost/round2.rs @@ -0,0 +1,164 @@ +//! FROST Round 2 functionality and types, for signature share generation + +use std::fmt::{self, Debug}; + +use crate::{ + challenge, + frost::{self, round1, *}, + Challenge, Ciphersuite, Error, Field, Group, +}; + +/// A representation of a single signature share used in FROST structures and messages. +#[derive(Clone, Copy)] +pub struct SignatureResponse { + /// The [`Scalar`] contribution to the group signature. + pub z_share: <::Field as Field>::Scalar, +} + +impl SignatureResponse +where + C: Ciphersuite, +{ + /// Deserialize [`SignatureResponse`] from bytes + pub fn from_bytes( + bytes: <::Field as Field>::Serialization, + ) -> Result { + <::Field as Field>::deserialize(&bytes) + .map(|scalar| Self { z_share: scalar }) + } + + /// Serialize [`SignatureResponse`] to bytes + pub fn to_bytes(&self) -> <::Field as Field>::Serialization { + <::Field as Field>::serialize(&self.z_share) + } +} + +impl Debug for SignatureResponse +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("SignatureResponse") + .field("z_share", &hex::encode(self.to_bytes())) + .finish() + } +} + +impl Eq for SignatureResponse where C: Ciphersuite {} + +impl PartialEq for SignatureResponse +where + C: Ciphersuite, +{ + // TODO: should this have any constant-time guarantees? I think signature shares are public. + fn eq(&self, other: &Self) -> bool { + self.z_share == other.z_share + } +} + +/// A participant's signature share, which the coordinator will aggregate with all other signer's +/// shares into the joint signature. +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct SignatureShare { + /// Represents the participant index. + pub index: u16, + /// This participant's signature over the message. + pub signature: SignatureResponse, +} + +impl SignatureShare +where + C: Ciphersuite, +{ + /// Gets the participant index associated with this [`SignatureShare`]. + pub fn index(&self) -> &u16 { + &self.index + } + + /// Tests if a signature share issued by a participant is valid before + /// aggregating it into a final joint signature to publish. + /// + /// This is the final step of [`verify_signature_share`] from the spec. + /// + /// [`verify_signature_share`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-03.html#section-5.3 + pub fn verify( + &self, + group_commitment_share: &round1::GroupCommitmentShare, + public_key: &frost::keys::Public, + lambda_i: <::Field as Field>::Scalar, + challenge: &Challenge, + ) -> Result<(), &'static str> { + if (::generator() * self.signature.z_share) + != (group_commitment_share.0 + (public_key.0 * challenge.0 * lambda_i)) + { + return Err("Invalid signature share"); + } + + Ok(()) + } +} + +impl Debug for SignatureShare +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("SignatureShare") + .field("index", &self.index) + .field("signature", &self.signature) + .finish() + } +} + +// // Zeroizes `SignatureShare` to be the `Default` value on drop (when it goes out +// // of scope). Luckily the derived `Default` includes the `Default` impl of +// // Scalar, which is four 0u64's under the hood, and u16, which is +// // 0u16. +// impl DefaultIsZeroes for SignatureShare {} + +/// Performed once by each participant selected for the signing operation. +/// +/// Implements [`sign`] from the spec. +/// +/// 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. +/// +/// [`sign`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-03.html#section-5.2 +pub fn sign( + signing_package: &SigningPackage, + signer_nonces: &round1::SigningNonces, + key_package: &frost::keys::KeyPackage, +) -> Result, &'static str> { + // Encodes the signing commitment list produced in round one as part of generating [`Rho`], the + // binding factor. + let rho: frost::Rho = signing_package.into(); + + // Compute the group commitment from signing commitments produced in round one. + let group_commitment = GroupCommitment::::try_from(signing_package)?; + + // Compute Lagrange coefficient. + let lambda_i = frost::derive_lagrange_coeff(*key_package.index(), signing_package)?; + + // Compute the per-message challenge. + let challenge = challenge::( + &group_commitment.0, + &key_package.group_public.element, + signing_package.message.as_slice(), + ); + + // Compute the Schnorr signature share. + let z_share: <::Field as Field>::Scalar = signer_nonces.hiding.0 + + (signer_nonces.binding.0 * rho.0) + + (lambda_i * key_package.secret_share.0 * challenge.0); + + let signature_share = SignatureShare:: { + index: *key_package.index(), + signature: SignatureResponse:: { z_share }, + }; + + Ok(signature_share) +} diff --git a/frost-core/src/lib.rs b/frost-core/src/lib.rs index 1b4a90c..1548e8f 100644 --- a/frost-core/src/lib.rs +++ b/frost-core/src/lib.rs @@ -1,8 +1,240 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); +#![allow(non_snake_case)] +#![deny(missing_docs)] +#![doc = include_str!("../README.md")] +#![forbid(unsafe_code)] + +use std::{ + default::Default, + fmt::Debug, + ops::{Add, Mul, Sub}, +}; + +use hex::FromHex; +use rand_core::{CryptoRng, RngCore}; + +// pub mod batch; +mod error; +pub mod frost; +// mod scalar_mul; +mod signature; +mod signing_key; +mod verifying_key; + +pub use error::Error; +pub use signature::Signature; +pub use signing_key::SigningKey; +pub use verifying_key::VerifyingKey; + +/// A prime order finite field GF(q) over which all scalar values for our prime order group can be +/// multiplied are defined. +/// +/// This trait does not have to be implemented for a finite field scalar itself, it can be a +/// pass-through, implemented for a type just for the ciphersuite, and calls through to another +/// implementation underneath, so that this trait does not have to be implemented for types you +/// don't own. +pub trait Field: Copy + Clone { + /// An element of the scalar field GF(p). + type Scalar: Add + + Copy + + Clone + + Eq + + Mul + + PartialEq + + Sub; + + /// A unique byte array buf of fixed length N. + /// + /// Little-endian! + type Serialization: AsRef<[u8]> + AsMut<[u8]> + Debug + Default + FromHex + TryFrom>; + + /// Returns the zero element of the field, the additive identity. + fn zero() -> Self::Scalar; + + /// Returns the one element of the field, the multiplicative identity. + fn one() -> Self::Scalar; + + /// Computes the multiplicative inverse of an element of the scalar field, failing if the + /// element is zero. + fn invert(scalar: &Self::Scalar) -> Result; + + /// Generate a random scalar from the entire space [0, l-1] + /// + /// + fn random(rng: &mut R) -> Self::Scalar; + + /// Generate a random scalar from the entire space [1, l-1] + /// + /// + fn random_nonzero(rng: &mut R) -> Self::Scalar; + + /// A member function of a group _G_ that maps an [`Element`] to a unique byte array buf of + /// fixed length Ne. + /// + /// + fn serialize(scalar: &Self::Scalar) -> Self::Serialization; + + /// A member function of a [`Group`] that attempts to map a byte array `buf` to an [`Element`]. + /// + /// Fails if the input is not a valid byte representation of an [`Element`] of the + /// [`Group`]. This function can raise a [`DeserializeError`] if deserialization fails or if the + /// resulting [`Element`] is the identity element of the group + /// + /// + fn deserialize(buf: &Self::Serialization) -> Result; +} + +/// An element of the [`Ciphersuite`] `C`'s [`Group`]'s scalar [`Field`]. +pub type Scalar = <<::Group as Group>::Field as Field>::Scalar; + +/// A prime-order group (or subgroup) that provides everything we need to create and verify Schnorr +/// signatures. +/// +/// This trait does not have to be implemented for the curve/element/point itself, it can be a +/// pass-through, implemented for a type just for the ciphersuite, and calls through to another +/// implementation underneath, so that this trait does not have to be implemented for types you +/// don't own. +pub trait Group: Copy + Clone { + /// A prime order finite field GF(q) over which all scalar values for our prime order group can + /// be multiplied are defined. + type Field: Field; + + /// An element of our group that we will be computing over. + type Element: Add + + Copy + + Clone + + Eq + + Mul<::Scalar, Output = Self::Element> + + PartialEq + + Sub; + + /// A unique byte array buf of fixed length N. + /// + /// Little-endian! + type Serialization: AsRef<[u8]> + AsMut<[u8]> + Default + FromHex + TryFrom>; + + /// Outputs the order of G (i.e. p) + /// + /// + fn order() -> ::Scalar; + + /// The order of the the quotient group when the prime order subgroup divides the order of the + /// full curve group. + /// + /// If using a prime order elliptic curve, the cofactor should be 1 in the scalar field. + /// + /// + fn cofactor() -> ::Scalar; + + /// Additive [identity] of the prime order group. + /// + /// [identity]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-04.html#section-3.1-3.2 + fn identity() -> Self::Element; + + /// The fixed generator element of the prime order group. + /// + /// The 'base' of [`ScalarBaseMult()`] from the spec. + /// [`ScalarBaseMult()`]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-04.html#section-3.1 + fn generator() -> Self::Element; + + /// A member function of a group _G_ that maps an [`Element`] to a unique byte array buf of + /// fixed length Ne. + /// + /// + fn serialize(element: &Self::Element) -> Self::Serialization; + + /// A member function of a [`Group`] that attempts to map a byte array `buf` to an [`Element`]. + /// + /// Fails if the input is not a valid byte representation of an [`Element`] of the + /// [`Group`]. This function can raise a [`DeserializeError`] if deserialization fails or if the + /// resulting [`Element`] is the identity element of the group + /// + /// + fn deserialize(buf: &Self::Serialization) -> Result; +} + +/// An element of the [`Ciphersuite`] `C`'s [`Group`]. +pub type Element = <::Group as Group>::Element; + +/// A [FROST ciphersuite] specifies the underlying prime-order group details and cryptographic hash +/// function. +/// +/// [FROST ciphersuite]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-04.html#name-ciphersuites +pub trait Ciphersuite: Copy + Clone { + /// The prime order group (or subgroup) that this ciphersuite operates over. + type Group: Group; + + /// A unique byte array of fixed length. + type HashOutput: AsRef<[u8]>; + + /// A unique byte array of fixed length that is the `Group::ElementSerialization` + + /// `Group::ScalarSerialization` + type SignatureSerialization: AsRef<[u8]> + FromHex + TryFrom>; + + /// [H1] for a FROST ciphersuite. + /// + /// Maps arbitrary inputs to non-zero `Self::Scalar` elements of the prime-order group scalar field. + /// + /// [H1]: https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#cryptographic-hash + fn H1(m: &[u8]) -> <::Field as Field>::Scalar; + + /// [H2] for a FROST ciphersuite. + /// + /// Maps arbitrary inputs to non-zero `Self::Scalar` elements of the prime-order group scalar field. + /// + /// [H2]: https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#cryptographic-hash + fn H2(m: &[u8]) -> <::Field as Field>::Scalar; + + /// H3 for a FROST ciphersuite. + /// + /// Usually an an alias for the ciphersuite hash function _H_ with domain separation applied. + /// + /// [spec]: https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#cryptographic-hash + fn H3(m: &[u8]) -> Self::HashOutput; +} + +/// A type refinement for the scalar field element representing the per-message _[challenge]_. +/// +/// [challenge]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-04.html#name-signature-challenge-computa +#[derive(Clone)] +pub struct Challenge( + pub(crate) <<::Group as Group>::Field as Field>::Scalar, +); + +impl Debug for Challenge +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Secret") + .field(&hex::encode( + <<::Group as Group>::Field as Field>::serialize(&self.0), + )) + .finish() } } + +/// Generates the challenge as is required for Schnorr signatures. +/// +/// Deals in bytes, so that [FROST] and singleton signing and verification can use it with different +/// types. +/// +/// This is the only invocation of the H2 hash function from the [RFC]. +/// +/// [FROST]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-04.html#section-4.6 +/// [RFC]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-04.html#section-3.2 +fn challenge( + R: &::Element, + verifying_key: &::Element, + msg: &[u8], +) -> Challenge +where + C: Ciphersuite, +{ + let mut preimage = vec![]; + + preimage.extend_from_slice(::serialize(R).as_ref()); + preimage.extend_from_slice(::serialize(verifying_key).as_ref()); + preimage.extend_from_slice(msg); + + Challenge(C::H2(&preimage[..])) +} diff --git a/frost-core/src/scalar_mul.rs b/frost-core/src/scalar_mul.rs new file mode 100644 index 0000000..2657331 --- /dev/null +++ b/frost-core/src/scalar_mul.rs @@ -0,0 +1,190 @@ +use std::{ + borrow::Borrow, + fmt::{Debug, Result}, +}; + +pub trait NonAdjacentForm { + fn non_adjacent_form(&self, w: usize) -> [i8; 256]; +} + +impl NonAdjacentForm for Scalar +where + C: Ciphersuite, +{ + /// Computes a width-\\(w\\) "Non-Adjacent Form" of this scalar. + /// + /// Thanks to curve25519-dalek for the original implementation that informed this one. + /// + /// The full scalar field MUST fit in 256 bits in this implementation. + fn non_adjacent_form(&self, w: usize) -> [i8; 256] { + // required by the NAF definition + debug_assert!(w >= 2); + // required so that the NAF digits fit in i8 + debug_assert!(w <= 8); + + use byteorder::{ByteOrder, LittleEndian}; + + // NB: Assumes a scalar that fits in 256 bits. + let mut naf = [0i8; 256]; + + let mut x_u64 = [0u64; 5]; + LittleEndian::read_u64_into(&self.to_bytes(), &mut x_u64[0..4]); + + let width = 1 << w; + let window_mask = width - 1; + + let mut pos = 0; + let mut carry = 0; + while pos < 256 { + // Construct a buffer of bits of the scalar, starting at bit `pos` + let u64_idx = pos / 64; + let bit_idx = pos % 64; + let bit_buf: u64; + if bit_idx < 64 - w { + // This window's bits are contained in a single u64 + bit_buf = x_u64[u64_idx] >> bit_idx; + } else { + // Combine the current u64's bits with the bits from the next u64 + bit_buf = (x_u64[u64_idx] >> bit_idx) | (x_u64[1 + u64_idx] << (64 - bit_idx)); + } + + // Add the carry into the current window + let window = carry + (bit_buf & window_mask); + + if window & 1 == 0 { + // If the window value is even, preserve the carry and continue. + // Why is the carry preserved? + // If carry == 0 and window & 1 == 0, then the next carry should be 0 + // If carry == 1 and window & 1 == 0, then bit_buf & 1 == 1 so the next carry should be 1 + pos += 1; + continue; + } + + if window < width / 2 { + carry = 0; + naf[pos] = window as i8; + } else { + carry = 1; + naf[pos] = (window as i8).wrapping_sub(width as i8); + } + + pos += w; + } + + naf + } +} + +/// A trait for variable-time multiscalar multiplication without precomputation. +/// +/// Implement for a group element. +pub trait VartimeMultiscalarMul: Clone { + /// Given an iterator of public scalars and an iterator of + /// `Option`s of group elements, compute either `Some(Q)`, where + /// $$ + /// Q = c\_1 E\_1 + \cdots + c\_n E\_n, + /// $$ + /// if all points were `Some(E_i)`, or else return `None`. + fn optional_multiscalar_mul(scalars: I, elements: J) -> Option + where + I: IntoIterator, + I::Item: Borrow, + J: IntoIterator>; + + /// Given an iterator of public scalars and an iterator of + /// public group elements, compute + /// $$ + /// Q = c\_1 E\_1 + \cdots + c\_n E\_n, + /// $$ + /// using variable-time operations. + /// + /// It is an error to call this function with two iterators of different lengths. + fn vartime_multiscalar_mul(scalars: I, elements: J) -> Self + where + I: IntoIterator, + I::Item: Borrow, + J: IntoIterator, + J::Item: Borrow, + { + Self::optional_multiscalar_mul( + scalars, + elements.into_iter().map(|e| Some(e.borrow().clone())), + ) + .unwrap() + } +} + +impl VartimeMultiscalarMul for Element +where + C: Ciphersuite, +{ + fn optional_multiscalar_mul(scalars: I, elements: J) -> Option> + where + I: IntoIterator, + I::Item: Borrow, + J: IntoIterator>>, + { + let nafs: Vec<_> = scalars + .into_iter() + .map(|c| c.borrow().non_adjacent_form(5)) + .collect(); + + let lookup_tables = elements + .into_iter() + .map(|P_opt| P_opt.map(|P| LookupTable5::>::from(&P))) + .collect::>>()?; + + let mut r = ::identity(); + + for i in (0..256).rev() { + let mut t = r + r.clone(); + + for (naf, lookup_table) in nafs.iter().zip(lookup_tables.iter()) { + if naf[i] > 0 { + t = &t + &lookup_table.select(naf[i] as usize); + } else if naf[i] < 0 { + t = &t - &lookup_table.select(-naf[i] as usize); + } + } + + r = t; + } + + Some(r) + } +} + +/// Holds odd multiples 1A, 3A, ..., 15A of a point A. +#[derive(Copy, Clone)] +pub(crate) struct LookupTable5(pub(crate) [T; 8]); + +impl LookupTable5 { + /// Given public, odd \\( x \\) with \\( 0 < x < 2^4 \\), return \\(xA\\). + pub fn select(&self, x: usize) -> T { + debug_assert_eq!(x & 1, 1); + debug_assert!(x < 16); + + self.0[x / 2] + } +} + +impl Debug for LookupTable5 { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result { + write!(f, "LookupTable5({:?})", self.0) + } +} + +impl<'a, C> From<&'a ::Element> for LookupTable5<::Element> +where + C: Ciphersuite, +{ + fn from(A: &'a ::Element) -> Self { + let mut Ai = [A; 8]; + let A2 = A * A.clone(); + for i in 0..7 { + Ai[i + 1] = (&A2 + &Ai[i]); + } + // Now Ai = [A, 3A, 5A, 7A, 9A, 11A, 13A, 15A] + LookupTable5(Ai) + } +} diff --git a/frost-core/src/signature.rs b/frost-core/src/signature.rs new file mode 100644 index 0000000..52f015d --- /dev/null +++ b/frost-core/src/signature.rs @@ -0,0 +1,99 @@ +//! Schnorr signatures over prime order groups (or subgroups) + +use std::fmt::Debug; + +// use hex::FromHex; + +use crate::{Ciphersuite, Error, Field, Group}; + +/// A Schnorr signature over some prime order group (or subgroup). +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct Signature { + /// The commitment `R` to the signature nonce. + pub(crate) R: ::Element, + /// The response `z` to the challenge computed from the commitment `R`, the verifying key, and + /// the message. + pub(crate) z: <::Field as Field>::Scalar, +} + +impl Signature +where + C: Ciphersuite, + C::Group: Group, + ::Field: Field, +{ + /// Converts bytes as [`C::SignatureSerialization`] into a `Signature`. + pub fn from_bytes(bytes: C::SignatureSerialization) -> Result + where + <::Serialization as TryFrom>>::Error: Debug, + <<::Field as Field>::Serialization as TryFrom>>::Error: Debug, + { + let mut R_bytes = Vec::from(::Serialization::default().as_ref()); + + let R_bytes_len = R_bytes.len(); + + R_bytes[..].copy_from_slice(&bytes.as_ref()[0..R_bytes_len]); + + println!("{:?}", R_bytes); + + let R_serialization = &R_bytes.try_into().map_err(|_| Error::MalformedSignature)?; + + let mut z_bytes = + Vec::from(<::Field as Field>::Serialization::default().as_ref()); + + let z_bytes_len = z_bytes.len(); + + z_bytes[..].copy_from_slice(&bytes.as_ref()[R_bytes_len..z_bytes_len]); + + println!("{:?}", z_bytes); + + let z_serialization = &z_bytes.try_into().map_err(|_| Error::MalformedSignature)?; + + Ok(Self { + R: ::deserialize(R_serialization)?, + z: <::Field as Field>::deserialize(z_serialization)?, + }) + } + + /// Converts this signature to its [`C::SignatureSerialization`] in bytes. + pub fn to_bytes(&self) -> C::SignatureSerialization + where + <::SignatureSerialization as TryFrom>>::Error: Debug, + { + let mut bytes = vec![]; + + bytes.extend(::serialize(&self.R).as_ref()); + bytes.extend(<::Field as Field>::serialize(&self.z).as_ref()); + + bytes.try_into().unwrap() + } +} + +impl std::fmt::Debug for Signature { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct("Signature") + .field( + "R", + &hex::encode(::serialize(&self.R).as_ref()), + ) + .field( + "z", + &hex::encode(<::Field as Field>::serialize(&self.z).as_ref()), + ) + .finish() + } +} + +// impl FromHex for Signature +// where +// C: Ciphersuite, +// { +// type Error = &'static str; + +// fn from_hex>(hex: T) -> Result { +// match FromHex::from_hex(hex) { +// Ok(bytes) => Self::from_bytes(bytes).map_err(|_| "malformed signature encoding"), +// Err(_) => Err("invalid hex"), +// } +// } +// } diff --git a/frost-core/src/signing_key.rs b/frost-core/src/signing_key.rs new file mode 100644 index 0000000..9aeb2b1 --- /dev/null +++ b/frost-core/src/signing_key.rs @@ -0,0 +1,73 @@ +//! Schnorr signature signing keys + +use rand_core::{CryptoRng, RngCore}; + +use crate::{Ciphersuite, Error, Field, Group, Signature, VerifyingKey}; + +/// A signing key for a Schnorr signature on a FROST [`Ciphersuite::Group`]. +#[derive(Copy, Clone)] +pub struct SigningKey +where + C: Ciphersuite, +{ + scalar: <::Field as Field>::Scalar, +} + +impl SigningKey +where + C: Ciphersuite, +{ + /// Generate a new signing key. + pub fn new(mut rng: R) -> SigningKey { + let scalar = <::Field as Field>::random_nonzero(&mut rng); + + SigningKey { scalar } + } + + /// Deserialize from bytes + pub fn from_bytes( + bytes: <::Field as Field>::Serialization, + ) -> Result, Error> { + <::Field as Field>::deserialize(&bytes) + .map(|scalar| SigningKey { scalar }) + } + + /// Serialize `SigningKey` to bytes + pub fn to_bytes(&self) -> <::Field as Field>::Serialization { + <::Field as Field>::serialize(&self.scalar) + } + + /// Create a signature `msg` using this `SigningKey`. + pub fn sign(&self, mut rng: R, msg: &[u8]) -> Signature { + let k = <::Field as Field>::random_nonzero(&mut rng); + + let R = ::generator() * k; + + // Generate Schnorr challenge + let c = crate::challenge::(&R, &VerifyingKey::::from(*self).element, msg); + + let z = k + (c.0 * self.scalar); + + Signature { R, z } + } +} + +impl From<&SigningKey> for VerifyingKey +where + C: Ciphersuite, +{ + fn from(signing_key: &SigningKey) -> Self { + VerifyingKey { + element: C::Group::generator() * signing_key.scalar, + } + } +} + +impl From> for VerifyingKey +where + C: Ciphersuite, +{ + fn from(signing_key: SigningKey) -> Self { + VerifyingKey::::from(&signing_key) + } +} diff --git a/frost-core/src/verifying_key.rs b/frost-core/src/verifying_key.rs new file mode 100644 index 0000000..f21c339 --- /dev/null +++ b/frost-core/src/verifying_key.rs @@ -0,0 +1,93 @@ +use std::fmt::{self, Debug}; + +use hex::FromHex; + +use crate::{Ciphersuite, Error, Group, Signature}; + +/// A valid verifying key for Schnorr signatures over a FROST [`Ciphersuite::Group`]. +#[derive(Copy, Clone, PartialEq)] +pub struct VerifyingKey +where + C: Ciphersuite, +{ + pub(crate) element: ::Element, +} + +impl VerifyingKey +where + C: Ciphersuite, +{ + // pub(crate) fn from(scalar: &<::Field as Field>::Scalar) -> Self { + // let element = ::generator() * *scalar; + + // VerifyingKey { element } + // } + + /// Deserialize from bytes + pub fn from_bytes(bytes: ::Serialization) -> Result, Error> { + ::deserialize(&bytes).map(|element| VerifyingKey { element }) + } + + /// Serialize `VerifyingKey` to bytes + pub fn to_bytes(&self) -> ::Serialization { + ::serialize(&self.element) + } + + /// Verify a purported `signature` over `msg` made by this verification key. + pub fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), Error> { + let c = crate::challenge::(&signature.R, &self.element, msg); + + // Verify check is h * ( - z * B + R + c * A) == 0 + // h * ( z * B - c * A - R) == 0 + // + // where h is the cofactor + let zB = C::Group::generator() * signature.z; + let cA = self.element * c.0; + let check = (zB - cA - signature.R) * C::Group::cofactor(); + + if check == C::Group::identity() { + Ok(()) + } else { + Err(Error::InvalidSignature) + } + } +} + +impl Debug for VerifyingKey +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("VerifyingKey") + .field(&hex::encode(self.to_bytes())) + .finish() + } +} + +impl FromHex for VerifyingKey +where + C: Ciphersuite, +{ + type Error = &'static str; + + fn from_hex>(hex: T) -> Result { + match FromHex::from_hex(hex) { + Ok(bytes) => Self::from_bytes(bytes).map_err(|_| "malformed verifying key encoding"), + Err(_) => Err("invalid hex"), + } + } +} + +// impl From> for ::ElementSerialization { +// fn from(pk: VerifyingKey) -> ::ElementSerialization { +// pk.bytes.bytes +// } +// } + +// impl TryFrom<::ElementSerialization> for VerifyingKey { +// type Error = Error; + +// fn try_from(bytes: [u8; 32]) -> Result { +// VerifyingKeyBytes::from(bytes).try_into() +// } +// } diff --git a/frost-core/tests/batch.rs.bck b/frost-core/tests/batch.rs.bck new file mode 100644 index 0000000..1e79158 --- /dev/null +++ b/frost-core/tests/batch.rs.bck @@ -0,0 +1,62 @@ +use rand::thread_rng; + +use frost_core::*; + +mod common; + +use common::ciphersuite::Ristretto255Sha512 as R; + +#[test] +fn batch_verify() { + let mut rng = thread_rng(); + let mut batch = batch::Verifier::::new(); + for _ in 0..32 { + let sk = SigningKey::new(&mut rng); + let vk = VerifyingKey::::from(&sk); + let msg = b"BatchVerifyTest"; + let sig = sk.sign(&mut rng, &msg[..]); + batch.queue((vk.into(), sig, msg)); + } + assert!(batch.verify(rng).is_ok()); +} + +#[test] +fn bad_batch_verify() { + let mut rng = thread_rng(); + let bad_index = 4; // must be even + let mut batch = batch::Verifier::::new(); + let mut items = Vec::new(); + for i in 0..32 { + let item: batch::Item = match i % 2 { + 0 => { + let sk = SigningKey::new(&mut rng); + let vk = VerifyingKey::::from(&sk); + let msg = b"BatchVerifyTest"; + let sig = if i != bad_index { + sk.sign(&mut rng, &msg[..]) + } else { + sk.sign(&mut rng, b"bad") + }; + (vk.into(), sig, msg).into() + } + 1 => { + let sk = SigningKey::new(&mut rng); + let vk = VerifyingKey::::from(&sk); + let msg = b"BatchVerifyTest"; + let sig = sk.sign(&mut rng, &msg[..]); + (vk.into(), sig, msg).into() + } + _ => unreachable!(), + }; + items.push(item.clone()); + batch.queue(item); + } + assert!(batch.verify(rng).is_err()); + for (i, item) in items.drain(..).enumerate() { + if i != bad_index { + assert!(item.verify_single().is_ok()); + } else { + assert!(item.verify_single().is_err()); + } + } +} diff --git a/frost-core/tests/common/ciphersuite.rs b/frost-core/tests/common/ciphersuite.rs new file mode 100644 index 0000000..d6f9497 --- /dev/null +++ b/frost-core/tests/common/ciphersuite.rs @@ -0,0 +1,168 @@ +#![allow(non_snake_case)] + +use curve25519_dalek::{ + constants::{BASEPOINT_ORDER, RISTRETTO_BASEPOINT_POINT}, + ristretto::{CompressedRistretto, RistrettoPoint}, + scalar::Scalar, + traits::Identity, +}; +use rand_core::{CryptoRng, RngCore}; +use sha2::{digest::Update, Digest, Sha512}; + +use frost_core::{Ciphersuite, Error, Field, Group}; + +#[derive(Clone, Copy)] +pub struct RistrettoScalarField; + +impl Field for RistrettoScalarField { + 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 { + // [`curve25519_dalek::scalar::Scalar`]'s Eq/PartialEq does a constant-time comparison using + // `ConstantTimeEq` + if *scalar == ::zero() { + Err(Error::InvalidZeroScalar) + } else { + Ok(scalar.invert()) + } + } + + fn random(rng: &mut R) -> Self::Scalar { + Scalar::random(rng) + } + + fn random_nonzero(rng: &mut R) -> Self::Scalar { + loop { + let scalar = Scalar::random(rng); + + // This impl of `Eq` calls to `ConstantTimeEq` under the hood + if scalar != Scalar::zero() { + return scalar; + } + } + } + + fn serialize(scalar: &Self::Scalar) -> Self::Serialization { + scalar.to_bytes() + } + + fn deserialize(buf: &Self::Serialization) -> Result { + match Scalar::from_canonical_bytes(*buf) { + Some(s) => Ok(s), + None => Err(Error::MalformedScalar), + } + } +} + +#[derive(Clone, Copy, PartialEq)] +pub struct RistrettoGroup; + +impl Group for RistrettoGroup { + type Field = RistrettoScalarField; + + type Element = RistrettoPoint; + + type Serialization = [u8; 32]; + + fn order() -> ::Scalar { + BASEPOINT_ORDER + } + + fn cofactor() -> ::Scalar { + Scalar::one() + } + + fn identity() -> Self::Element { + RistrettoPoint::identity() + } + + fn generator() -> Self::Element { + RISTRETTO_BASEPOINT_POINT + } + + fn serialize(element: &Self::Element) -> Self::Serialization { + element.compress().to_bytes() + } + + fn deserialize(buf: &Self::Serialization) -> Result { + match CompressedRistretto::from_slice(buf.as_ref()).decompress() { + Some(point) => Ok(point), + None => Err(Error::MalformedElement), + } + } +} + +/// Context string 'FROST-RISTRETTO255-SHA512' from the ciphersuite in the [spec] +/// +/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-04.txt +const CONTEXT_STRING: &str = "FROST-RISTRETTO255-SHA512"; + +#[derive(Clone, Copy, PartialEq)] +pub struct Ristretto255Sha512; + +impl Ciphersuite for Ristretto255Sha512 { + type Group = RistrettoGroup; + + type HashOutput = [u8; 64]; + + type SignatureSerialization = [u8; 64]; + + /// H1 for FROST(ristretto255, SHA-512) + /// + /// [spec]: https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#cryptographic-hash + fn H1(m: &[u8]) -> <::Field as Field>::Scalar { + let h = Sha512::new() + .chain(CONTEXT_STRING.as_bytes()) + .chain("rho") + .chain(m); + + let mut output = [0u8; 64]; + output.copy_from_slice(h.finalize().as_slice()); + <::Field as Field>::Scalar::from_bytes_mod_order_wide(&output) + } + + /// H2 for FROST(ristretto255, SHA-512) + /// + /// [spec]: https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#cryptographic-hash-function-dep-hash + fn H2(m: &[u8]) -> <::Field as Field>::Scalar { + let h = Sha512::new() + .chain(CONTEXT_STRING.as_bytes()) + .chain("chal") + .chain(m); + + let mut output = [0u8; 64]; + output.copy_from_slice(h.finalize().as_slice()); + <::Field as Field>::Scalar::from_bytes_mod_order_wide(&output) + } + + /// H3 for FROST(ristretto255, SHA-512) + /// + /// [spec]: https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#cryptographic-hash-function-dep-hash + fn H3(m: &[u8]) -> Self::HashOutput { + let h = Sha512::new() + .chain(CONTEXT_STRING.as_bytes()) + .chain("digest") + .chain(m); + + let mut output = [0u8; 64]; + output.copy_from_slice(h.finalize().as_slice()); + output + } +} + +#[test] +fn use_parameterized_types() { + let h3_image = Ristretto255Sha512::H3(b"test_message"); + + println!("h3_image: {:?}", h3_image); +} diff --git a/frost-core/tests/common/mod.rs b/frost-core/tests/common/mod.rs new file mode 100644 index 0000000..c311bc7 --- /dev/null +++ b/frost-core/tests/common/mod.rs @@ -0,0 +1,13 @@ +//! Shared code for `frost-ristretto255` integration tests. +//! +//! # Warning +//! +//! Test functions in this file and its submodules will not be run. +//! This file is only for test library code. +//! +//! This module uses the legacy directory structure, +//! to avoid compiling an empty "common" test binary: +//! + +pub mod ciphersuite; +pub mod vectors; diff --git a/frost-core/tests/common/vectors.json b/frost-core/tests/common/vectors.json new file mode 100644 index 0000000..b14b9ba --- /dev/null +++ b/frost-core/tests/common/vectors.json @@ -0,0 +1,59 @@ +{ + "config": { + "MAX_SIGNERS": "3", + "NUM_SIGNERS": "2", + "THRESHOLD_LIMIT": "2", + "name": "FROST(ristretto255, SHA-512)", + "group": "ristretto255", + "hash": "SHA-512" + }, + "inputs": { + "group_secret_key": "b120be204b5e758960458ca9c4675b56b12a8faff2be9c94891d5e1cd75c880e", + "group_public_key": "563b80013f337deaa2a282af7b281bd70d2f501928a89c1aa48b379a5ac4202b", + "message": "74657374", + "signers": { + "1": { + "signer_share": "94ae65bb90030a89507fa00fff08dfed841cf996de5a0c574f1f4693ddcb6705" + }, + "2": { + "signer_share": "641003b3f00bb1e01656ac1818a4419a580e637ecaf67b1915212e0ae43a470c" + }, + "3": { + "signer_share": "479eaa4d36b145e00690c07e5245c5312c00cd65b692ebdbda221681eaa92603" + } + } + }, + "round_one_outputs": { + "participants": "1,2", + "group_binding_factor_input": "0001824e9eddddf02b2a9caf5859825e999d791ca094f65b814a8bca6013d9cc312774c7e1271d2939a84a9a867e3a06579b4d25659b427439ccf0d745b43f75b76600028013834ff4d48e7d6b76c2e732bc611f54720ef8933c4ca4de7eaaa77ff5cd125e056ecc4f7c4657d3a742354430d768f945db229c335d258e9622ad99f3e7582d07b35bd9849ce4af6ad403090d69a7d0eb88bba669a9f985175d70cd15ad5f1ef5b734c98a32b4aab7b43a57e93fc09281f2e7a207076b31e416ba63f53d9d", + "group_binding_factor": "f00ae6007f2d74a1507c962cf30006be77596106db28f2d5443fd66d755e780c", + "signers": { + "1": { + "hiding_nonce": "349b3bb8464a1d87f7d6b56f4559a3f9a6335261a3266089a9b12d9d6f6ce209", + "binding_nonce": "ce7406016a854be4291f03e7d24fe30e77994c3465de031515a4c116f22ca901", + "hiding_nonce_commitment": "824e9eddddf02b2a9caf5859825e999d791ca094f65b814a8bca6013d9cc3127", + "binding_nonce_commitment": "74c7e1271d2939a84a9a867e3a06579b4d25659b427439ccf0d745b43f75b766" + }, + "2": { + "hiding_nonce": "4d66d319f20a728ec3d491cbf260cc6be687bd87cc2b5fdb4d5f528f65fd650d", + "binding_nonce": "278b9b1e04632e6af3f1a3c144d07922ffcf5efd3a341b47abc19c43f48ce306", + "hiding_nonce_commitment": "8013834ff4d48e7d6b76c2e732bc611f54720ef8933c4ca4de7eaaa77ff5cd12", + "binding_nonce_commitment": "5e056ecc4f7c4657d3a742354430d768f945db229c335d258e9622ad99f3e758" + } + } + }, + "round_two_outputs": { + "participants": "1,2", + "signers": { + "1": { + "sig_share": "ec6b075f17c5670e80b1fda8f6de1cfe3c79db06a852f8d5650fb71eaad69501" + }, + "2": { + "sig_share": "87ceccc477069aa9b751b307f25955daaf943a3abc51f214a114781de0f58e03" + } + } + }, + "final_output": { + "sig": "7e92309bf40993141acd5f2c7680a302cc5aa5dd291a833906da8e35bc39b03e733ad4238fcb01b83703b1b0e83872d8ec0d164164a4eaea06242f3c8acc2405" + } + } diff --git a/frost-core/tests/common/vectors.rs b/frost-core/tests/common/vectors.rs new file mode 100644 index 0000000..f85d255 --- /dev/null +++ b/frost-core/tests/common/vectors.rs @@ -0,0 +1,147 @@ +use std::{collections::HashMap, str::FromStr}; + +use curve25519_dalek::scalar::Scalar; +use hex::{self, FromHex}; +use lazy_static::lazy_static; +use serde_json::Value; + +use frost_core::{ + frost::{keys::*, round1::*, round2::*, *}, + VerifyingKey, +}; + +use super::ciphersuite::Ristretto255Sha512; + +lazy_static! { + pub static ref RISTRETTO255_SHA512: Value = + serde_json::from_str(include_str!("vectors.json").trim()) + .expect("Test vector is valid JSON"); +} + +#[allow(clippy::type_complexity)] +#[allow(dead_code)] +pub(crate) fn parse_test_vectors() -> ( + VerifyingKey, + HashMap>, + &'static str, + Vec, + HashMap>, + HashMap>, + Vec, + Rho, + HashMap>, + Vec, // Signature, +) { + type R = Ristretto255Sha512; + + let inputs = &RISTRETTO255_SHA512["inputs"]; + + let message = inputs["message"].as_str().unwrap(); + let message_bytes = hex::decode(message).unwrap(); + + let mut key_packages: HashMap> = HashMap::new(); + + let possible_signers = RISTRETTO255_SHA512["inputs"]["signers"] + .as_object() + .unwrap() + .iter(); + + let group_public = + VerifyingKey::::from_hex(inputs["group_public_key"].as_str().unwrap()).unwrap(); + + for (i, secret_share) in possible_signers { + let secret = Secret::::from_hex(secret_share["signer_share"].as_str().unwrap()).unwrap(); + let signer_public = secret.into(); + + let key_package = KeyPackage:: { + index: u16::from_str(i).unwrap(), + secret_share: secret, + public: signer_public, + group_public, + }; + + key_packages.insert(*key_package.index(), key_package); + } + + // Round one outputs + + let round_one_outputs = &RISTRETTO255_SHA512["round_one_outputs"]; + + let group_binding_factor_input = Vec::::from_hex( + round_one_outputs["group_binding_factor_input"] + .as_str() + .unwrap(), + ) + .unwrap(); + + let group_binding_factor = + Rho::::from_hex(round_one_outputs["group_binding_factor"].as_str().unwrap()).unwrap(); + + let mut signer_nonces: HashMap> = HashMap::new(); + let mut signer_commitments: HashMap> = HashMap::new(); + + for (i, signer) in round_one_outputs["signers"].as_object().unwrap().iter() { + let index = u16::from_str(i).unwrap(); + + let signing_nonces = SigningNonces:: { + hiding: Nonce::::from_hex(signer["hiding_nonce"].as_str().unwrap()).unwrap(), + binding: Nonce::::from_hex(signer["binding_nonce"].as_str().unwrap()).unwrap(), + }; + + signer_nonces.insert(index, signing_nonces); + + let signing_commitments = SigningCommitments:: { + index, + hiding: NonceCommitment::from_hex(signer["hiding_nonce_commitment"].as_str().unwrap()) + .unwrap(), + binding: NonceCommitment::from_hex( + signer["binding_nonce_commitment"].as_str().unwrap(), + ) + .unwrap(), + }; + + signer_commitments.insert(index, signing_commitments); + } + + // Round two outputs + + let round_two_outputs = &RISTRETTO255_SHA512["round_two_outputs"]; + + let mut signature_shares: HashMap> = HashMap::new(); + + for (i, signer) in round_two_outputs["signers"].as_object().unwrap().iter() { + let signature_share = SignatureShare:: { + index: u16::from_str(i).unwrap(), + signature: SignatureResponse { + z_share: Scalar::from_canonical_bytes( + hex::decode(signer["sig_share"].as_str().unwrap()) + .unwrap() + .try_into() + .unwrap(), + ) + .unwrap(), + }, + }; + + signature_shares.insert(u16::from_str(i).unwrap(), signature_share); + } + + // Final output + + let final_output = &RISTRETTO255_SHA512["final_output"]; + + let signature_bytes = FromHex::from_hex(final_output["sig"].as_str().unwrap()).unwrap(); + + ( + group_public, + key_packages, + message, + message_bytes, + signer_nonces, + signer_commitments, + group_binding_factor_input, + group_binding_factor, + signature_shares, + signature_bytes, + ) +} diff --git a/frost-core/tests/frost.rs b/frost-core/tests/frost.rs new file mode 100644 index 0000000..f3c906a --- /dev/null +++ b/frost-core/tests/frost.rs @@ -0,0 +1,99 @@ +use std::{collections::HashMap, convert::TryFrom}; + +use frost_core::frost; +use rand::thread_rng; + +mod common; + +use common::ciphersuite::Ristretto255Sha512 as R; + +#[test] +fn check_sign_with_dealer() { + let mut rng = thread_rng(); + + //////////////////////////////////////////////////////////////////////////// + // Key generation + //////////////////////////////////////////////////////////////////////////// + + let numsigners = 5; + let threshold = 3; + let (shares, pubkeys) = + frost::keys::keygen_with_dealer(numsigners, threshold, &mut rng).unwrap(); + + // Verifies the secret shares from the dealer + let key_packages: Vec> = shares + .into_iter() + .map(|share| frost::keys::KeyPackage::try_from(share).unwrap()) + .collect(); + + let mut nonces: HashMap>> = HashMap::new(); + let mut commitments: HashMap>> = HashMap::new(); + + //////////////////////////////////////////////////////////////////////////// + // Round 1: generating nonces and signing commitments for each participant + //////////////////////////////////////////////////////////////////////////// + + for participant_index in 1..(threshold + 1) { + // Generate one (1) nonce and one SigningCommitments instance for each + // participant, up to _threshold_. + let (nonce, commitment) = frost::round1::preprocess(1, participant_index as u16, &mut rng); + nonces.insert(participant_index as u16, nonce); + commitments.insert(participant_index as u16, 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> = Vec::new(); + let message = "message to sign".as_bytes(); + let comms = commitments.clone().into_values().flatten().collect(); + let signing_package = frost::SigningPackage::new(comms, message.to_vec()); + + //////////////////////////////////////////////////////////////////////////// + // Round 2: each participant generates their signature share + //////////////////////////////////////////////////////////////////////////// + + for participant_index in nonces.keys() { + let key_package = key_packages + .iter() + .find(|key_package| *participant_index == key_package.index) + .unwrap(); + + let nonces_to_use = &nonces.get(participant_index).unwrap()[0]; + + // Each participant generates their signature share. + let signature_share = + frost::round2::sign(&signing_package, nonces_to_use, key_package).unwrap(); + 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_res = frost::aggregate(&signing_package, &signature_shares[..], &pubkeys); + + assert!(group_signature_res.is_ok()); + + let group_signature = group_signature_res.unwrap(); + + // 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()); + + // Check that the threshold signature can be verified by the group public + // key (the verification key) from SharePackage.group_public + for (participant_index, _) in nonces.clone() { + let key_package = key_packages.get(participant_index as usize).unwrap(); + + assert!(key_package + .group_public + .verify(message, &group_signature) + .is_ok()); + } +} diff --git a/frost-core/tests/proptests.rs b/frost-core/tests/proptests.rs new file mode 100644 index 0000000..fa06cd7 --- /dev/null +++ b/frost-core/tests/proptests.rs @@ -0,0 +1,129 @@ +use frost_core::*; +use proptest::prelude::*; +use rand_core::{CryptoRng, RngCore}; + +mod common; + +use common::ciphersuite::Ristretto255Sha512 as R; + +/// A signature test-case, containing signature data and expected validity. +#[derive(Clone, Debug)] +struct SignatureCase { + msg: Vec, + sig: Signature, + vk: VerifyingKey, + invalid_vk: VerifyingKey, + is_valid: bool, +} + +/// A modification to a test-case. +#[derive(Copy, Clone, Debug)] +enum Tweak { + /// No-op, used to check that unchanged cases verify. + None, + /// Change the message the signature is defined for, invalidating the signature. + ChangeMessage, + /// Change the public key the signature is defined for, invalidating the signature. + ChangePubkey, + /* XXX implement this -- needs to regenerate a custom signature because the + nonce commitment is fed into the hash, so it has to have torsion at signing + time. + /// Change the case to have a torsion component in the signature's `r` value. + AddTorsion, + */ + /* XXX implement this -- needs custom handling of field arithmetic. + /// Change the signature's `s` scalar to be unreduced (mod L), invalidating the signature. + UnreducedScalar, + */ +} + +impl SignatureCase +where + C: Ciphersuite, +{ + fn new(mut rng: R, msg: Vec) -> Self { + let sk = SigningKey::::new(&mut rng); + let sig = sk.sign(&mut rng, &msg); + let vk = VerifyingKey::::from(&sk); + let invalid_vk = VerifyingKey::::from(&SigningKey::new(&mut rng)); + Self { + msg, + sig, + vk, + invalid_vk, + is_valid: true, + } + } + + // Check that signature verification succeeds or fails, as expected. + fn check(&self) -> bool { + // // The signature data is stored in (refined) byte types, but do a round trip + // // conversion to raw bytes to exercise those code paths. + // let sig = { + // let bytes: [u8; 64] = self.sig.into(); + // Signature::::from_bytes(bytes) + // }; + + // // Check that the verification key is a valid key. + // let pub_key = VerifyingKey::::from_bytes(pk_bytes) + // .expect("The test verification key to be well-formed."); + + // Check that signature validation has the expected result. + self.is_valid == self.vk.verify(&self.msg, &self.sig).is_ok() + } + + fn apply_tweak(&mut self, tweak: &Tweak) { + match tweak { + Tweak::None => {} + Tweak::ChangeMessage => { + // Changing the message makes the signature invalid. + self.msg.push(90); + self.is_valid = false; + } + Tweak::ChangePubkey => { + // Changing the public key makes the signature invalid. + self.vk = self.invalid_vk; + self.is_valid = false; + } + } + } +} + +fn tweak_strategy() -> impl Strategy { + prop_oneof![ + 10 => Just(Tweak::None), + 1 => Just(Tweak::ChangeMessage), + 1 => Just(Tweak::ChangePubkey), + ] +} + +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::()), + ) { + // 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::::new(&mut rng, msg.to_vec()); + + + // Apply tweaks to each case. + for t in &tweaks { + sig.apply_tweak(t); + } + + assert!(sig.check()); + } + + +} diff --git a/frost-core/tests/tests.rs b/frost-core/tests/tests.rs new file mode 100644 index 0000000..0ac9d54 --- /dev/null +++ b/frost-core/tests/tests.rs @@ -0,0 +1,152 @@ +use frost_core::frost; +use rand::thread_rng; + +mod common; + +use common::{ciphersuite::*, vectors::*}; + +/// This is testing that Shamir's secret sharing to compute and arbitrary +/// value is working. +#[test] +fn check_share_generation_ristretto255_sha512() { + let mut rng = thread_rng(); + + let secret = frost::keys::Secret::::random(&mut rng); + + let secret_shares = frost::keys::generate_secret_shares(&secret, 5, 3, rng).unwrap(); + + for secret_share in secret_shares.iter() { + assert_eq!(secret_share.verify(), Ok(())); + } + + assert_eq!( + frost::keys::reconstruct_secret::(secret_shares).unwrap(), + secret + ) +} + +#[test] +fn check_sign_with_test_vectors() { + let ( + group_public, + key_packages, + _message, + message_bytes, + signer_nonces, + signer_commitments, + group_binding_factor_input, + group_binding_factor, + signature_shares, + signature_bytes, + ) = parse_test_vectors(); + + type R = Ristretto255Sha512; + + //////////////////////////////////////////////////////////////////////////// + // Key generation + //////////////////////////////////////////////////////////////////////////// + + for key_package in key_packages.values() { + assert_eq!( + *key_package.public(), + frost::keys::Public::from(*key_package.secret_share()) + ); + } + + ///////////////////////////////////////////////////////////////////////////// + // Round 1: generating nonces and signing commitments for each participant + ///////////////////////////////////////////////////////////////////////////// + + for (i, _) in signer_commitments.clone() { + // compute nonce commitments from nonces + let nonces = signer_nonces.get(&i).unwrap(); + let nonce_commitments = signer_commitments.get(&i).unwrap(); + + assert_eq!( + &frost::round1::NonceCommitment::from(nonces.hiding()), + nonce_commitments.hiding() + ); + + assert_eq!( + &frost::round1::NonceCommitment::from(nonces.binding()), + nonce_commitments.binding() + ); + } + + ///////////////////////////////////////////////////////////////////////////// + // Round 2: each participant generates their signature share + ///////////////////////////////////////////////////////////////////////////// + + let signer_commitments_vec = signer_commitments + .into_iter() + .map(|(_, signing_commitments)| signing_commitments) + .collect(); + + let signing_package = frost::SigningPackage::new(signer_commitments_vec, message_bytes); + + assert_eq!(signing_package.rho_preimage(), group_binding_factor_input); + + let rho: frost::Rho = (&signing_package).into(); + + assert_eq!(rho, group_binding_factor); + + let mut our_signature_shares: Vec> = Vec::new(); + + // Each participant generates their signature share + for index in signer_nonces.keys() { + let key_package = &key_packages[index]; + let nonces = &signer_nonces[index]; + + // Each participant generates their signature share. + let signature_share = frost::round2::sign(&signing_package, nonces, key_package).unwrap(); + + our_signature_shares.push(signature_share); + } + + for sig_share in our_signature_shares.clone() { + assert_eq!(sig_share, signature_shares[sig_share.index()]); + } + + let signer_pubkeys = key_packages + .into_iter() + .map(|(i, key_package)| (i, *key_package.public())) + .collect(); + + let pubkey_package = frost::keys::PublicKeyPackage { + signer_pubkeys, + group_public, + }; + + //////////////////////////////////////////////////////////////////////////// + // Aggregation: collects the signing shares from all participants, + // generates the final signature. + //////////////////////////////////////////////////////////////////////////// + + // Aggregate the FROST signature from test vector sig shares + let group_signature_result = frost::aggregate( + &signing_package, + &signature_shares + .values() + .cloned() + .collect::>>(), + &pubkey_package, + ); + + // Check that the aggregation passed signature share verification and generation + assert!(group_signature_result.is_ok()); + + // Check that the generated signature matches the test vector signature + let group_signature = group_signature_result.unwrap(); + assert_eq!(group_signature.to_bytes().to_vec(), signature_bytes); + + // Aggregate the FROST signature from our signature shares + let group_signature_result = + frost::aggregate(&signing_package, &our_signature_shares, &pubkey_package); + + // Check that the aggregation passed signature share verification and generation + assert!(group_signature_result.is_ok()); + + // Check that the generated signature matches the test vector signature + let group_signature = group_signature_result.unwrap(); + assert_eq!(group_signature.to_bytes().to_vec(), signature_bytes); +} diff --git a/frost-ristretto255/Cargo.toml b/frost-ristretto255/Cargo.toml index b41fcae..2c41635 100644 --- a/frost-ristretto255/Cargo.toml +++ b/frost-ristretto255/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # - Update CHANGELOG.md # - Create git tag. version = "0.1.0" -authors = ["Henry de Valence ", "Deirdre Connolly ", "Chelsea Komlo "] +authors = [ "Deirdre Connolly ", "Chelsea Komlo ", "Henry de Valence "] readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/ZcashFoundation/frost" @@ -20,7 +20,7 @@ features = ["nightly"] [dependencies] byteorder = "1.4" curve25519-dalek = { version = "4.0.0-pre.1", features = ["serde"] } -digest = "0.9" +# digest = "0.9" hex = { version = "0.4.3", features = ["serde"] } rand_core = "0.6" serde = { version = "1", optional = true, features = ["derive"] } diff --git a/frost-ristretto255/src/lib.rs b/frost-ristretto255/src/lib.rs index 245185c..ebf57c8 100644 --- a/frost-ristretto255/src/lib.rs +++ b/frost-ristretto255/src/lib.rs @@ -12,8 +12,8 @@ #![deny(missing_docs)] #![doc = include_str!("../README.md")] -use curve25519_dalek::{digest::Update, scalar::Scalar}; -use sha2::{Digest, Sha512}; +use curve25519_dalek::scalar::Scalar; +use sha2::{digest::Update, Digest, Sha512}; pub mod batch; mod error; diff --git a/frost-ristretto255/src/signing_key.rs b/frost-ristretto255/src/signing_key.rs index 074f635..dd9f831 100644 --- a/frost-ristretto255/src/signing_key.rs +++ b/frost-ristretto255/src/signing_key.rs @@ -10,7 +10,7 @@ use std::convert::{TryFrom, TryInto}; -use curve25519_dalek::{constants::RISTRETTO_BASEPOINT_POINT, digest::Update, scalar::Scalar}; +use curve25519_dalek::{constants::RISTRETTO_BASEPOINT_POINT, scalar::Scalar}; use rand_core::{CryptoRng, RngCore}; use sha2::{Digest, Sha512}; @@ -92,12 +92,15 @@ impl SigningKey { bytes }; - let nonce = Scalar::from_hash( - Sha512::new() - .chain(&random_bytes[..]) - .chain(&self.pk.bytes.bytes[..]) // XXX ugly - .chain(msg), - ); + let mut hasher = Sha512::new(); + hasher.update(&random_bytes[..]); + hasher.update(&self.pk.bytes.bytes[..]); + hasher.update(msg); + + let mut hash_bytes = [0u8; 64]; + hash_bytes.copy_from_slice(hasher.finalize().as_slice()); + + let nonce = Scalar::from_bytes_mod_order_wide(&hash_bytes); // XXX: does this need `RistrettoPoint::from_uniform_bytes()` ? let R_bytes = (RISTRETTO_BASEPOINT_POINT * nonce).compress().to_bytes();