From 2ebc08f91078617e5f7f34c37cd244beb850fe9e Mon Sep 17 00:00:00 2001 From: Deirdre Connolly Date: Thu, 25 Feb 2021 11:06:54 -0500 Subject: [PATCH] Frost keygen with dealer (#47) Implements FROST (Flexible Round Optimized Schnorr Threshold Signatures, https://eprint.iacr.org/2020/852) where key generation is performed by a trusted dealer. Future work will include implementing distributed key generation and re-randomizability. Co-authored-by: Chelsea Komlo Co-authored-by: Isis Lovecruft --- Cargo.toml | 3 +- LICENCE | 43 +++ LICENCE.MIT | 18 ++ LICENSE.Apache-2.0 | 13 + src/batch.rs | 16 +- src/constants.rs | 9 + src/error.rs | 10 + src/frost.rs | 680 ++++++++++++++++++++++++++++++++++++++++ src/hash.rs | 10 + src/lib.rs | 11 + src/scalar_mul.rs | 12 + src/signature.rs | 9 + src/signing_key.rs | 10 + src/verification_key.rs | 10 + tests/frost.rs | 62 ++++ 15 files changed, 912 insertions(+), 4 deletions(-) create mode 100644 LICENCE create mode 100644 LICENCE.MIT create mode 100644 LICENSE.Apache-2.0 create mode 100644 src/frost.rs create mode 100644 tests/frost.rs diff --git a/Cargo.toml b/Cargo.toml index c216003..77bc3fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" # - Update CHANGELOG.md # - Create git tag. version = "0.2.2" -authors = ["Henry de Valence ", "Deirdre Connolly "] +authors = ["Henry de Valence ", "Deirdre Connolly ", "Chelsea Komlo "] readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/ZcashFoundation/redjubjub" @@ -25,6 +25,7 @@ jubjub = "0.3" rand_core = "0.5" serde = { version = "1", optional = true, features = ["derive"] } thiserror = "1.0" +zeroize = { version = "1", default-features = false, features = ["zeroize_derive"] } [dev-dependencies] bincode = "1" diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..7a3a691 --- /dev/null +++ b/LICENCE @@ -0,0 +1,43 @@ +This software is licensed optionally under either the MIT license or Apache 2.0 +license, the full text of which may be found respectively in the LICENCE.MIT and +LICENCE.Apache-2.0 files contained within this software distribution. + +============================================================================== + +Portions of redjubjub are taken from curve25519-dalek, which can be found at +, under the following +license. This implementation does NOT use the portions of curve25519-dalek +which were originally derived from Adam Langley's Go edwards25519 +implementation, and, as such, that portion of the curve25519-dalek license is +omitted here. + +============================================================================== + +Copyright (c) 2016-2021 Isis Agora Lovecruft, Henry de Valence. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENCE.MIT b/LICENCE.MIT new file mode 100644 index 0000000..f8a592e --- /dev/null +++ b/LICENCE.MIT @@ -0,0 +1,18 @@ +Copyright 2019-2021 Zcash Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSE.Apache-2.0 b/LICENSE.Apache-2.0 new file mode 100644 index 0000000..3e13069 --- /dev/null +++ b/LICENSE.Apache-2.0 @@ -0,0 +1,13 @@ +Copyright 2019-2021 Zcash Foundation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/batch.rs b/src/batch.rs index 9979cfa..0bc3838 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -1,3 +1,13 @@ +// -*- mode: rust; -*- +// +// This file is part of redjubjub. +// Copyright (c) 2019-2021 Zcash Foundation +// See LICENSE for licensing information. +// +// Authors: +// - Deirdre Connolly +// - Henry de Valence + //! Performs batch RedJubjub signature verification. //! //! Batch verification asks whether *all* signatures in some set are valid, @@ -136,11 +146,11 @@ impl Verifier { /// /// 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 + /// 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 + /// 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. /// @@ -159,7 +169,7 @@ impl Verifier { /// signatures of each type in our batch, but we can still /// amortize computation nicely in one multiscalar multiplication: /// - /// h_G * ( [-sum(z_i * s_i): i_type == SpendAuth]P_SpendAuth + [-sum(z_i * s_i): i_type == Binding]P_Binding + sum([z_i]R_i) + sum([z_i * c_i]VK_i) ) = 0_G + /// h_G * ( [-sum(z_i * s_i): i_type == SpendAuth]P_SpendAuth + [-sum(z_i * s_i): i_type == Binding]P_Binding + sum(\[z_i\]R_i) + sum([z_i * c_i]VK_i) ) = 0_G /// /// As follows elliptic curve scalar multiplication convention, /// scalar variables are lowercase and group point variables diff --git a/src/constants.rs b/src/constants.rs index f6bda44..d534680 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,3 +1,12 @@ +// -*- mode: rust; -*- +// +// This file is part of redjubjub. +// Copyright (c) 2019-2021 Zcash Foundation +// See LICENSE for licensing information. +// +// Authors: +// - Henry de Valence + /// The byte-encoding of the basepoint for `SpendAuthSig`. // Extracted ad-hoc from librustzcash // XXX add tests for this value. diff --git a/src/error.rs b/src/error.rs index e36f064..e130f45 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,13 @@ +// -*- mode: rust; -*- +// +// This file is part of redjubjub. +// Copyright (c) 2019-2021 Zcash Foundation +// See LICENSE for licensing information. +// +// Authors: +// - Deirdre Connolly +// - Henry de Valence + use thiserror::Error; /// An error related to RedJubJub signatures. diff --git a/src/frost.rs b/src/frost.rs new file mode 100644 index 0000000..fa7e0de --- /dev/null +++ b/src/frost.rs @@ -0,0 +1,680 @@ +// -*- mode: rust; -*- +// +// This file is part of redjubjub. +// Copyright (c) 2020-2021 Zcash Foundation +// See LICENSE for licensing information. +// +// Authors: +// - Chelsea H. Komlo +// - Deirdre Connolly +// - isis agora lovecruft + +//! An implementation of FROST (Flexible Round-Optimized Schnorr Threshold) +//! signatures. +//! +//! > **WARNING**: This implementation is unstable and subject to +//! > revision. It is not covered by the crate's semver guarantees and should not +//! > be deployed without consultation from 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 rand_core::{CryptoRng, RngCore}; +use std::convert::TryFrom; + +use std::{collections::HashMap, marker::PhantomData}; + +use zeroize::Zeroize; + +use crate::private::Sealed; +use crate::{HStar, Scalar, Signature, SpendAuth, VerificationKey}; + +/// A secret scalar value representing a single signer's secret key. +#[derive(Clone, Default)] +pub struct Secret(Scalar); + +impl Zeroize for Secret { + fn zeroize(&mut self) { + self.0 = Scalar::zero(); + } +} + +impl Drop for Secret { + fn drop(&mut self) { + self.zeroize(); + } +} + +impl From for Secret { + fn from(source: Scalar) -> Secret { + Secret(source) + } +} + +/// A public group element that represents a single signer's public key. +#[derive(Copy, Clone)] +pub struct Public(jubjub::ExtendedPoint); + +impl From for Public { + fn from(source: jubjub::ExtendedPoint) -> Public { + Public(source) + } +} + +/// A share generated by performing a (t-out-of-n) secret sharing scheme where +/// 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. +#[derive(Clone)] +pub struct Share { + receiver_index: u32, + value: Secret, + commitment: ShareCommitment, +} + +/// A Jubjub point 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)] +struct Commitment(jubjub::ExtendedPoint); + +/// Contains the commitments to the coefficients for our secret polynomial _f_, +/// used to generate participants' key shares. +/// +/// [`ShareCommitment`] 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* +/// [`ShareCommitment`], 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 ShareCommitment(Vec); + +/// The product of all signers' individual commitments, published as part of the +/// final signature. +pub struct GroupCommitment(jubjub::ExtendedPoint); + +/// 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. +pub struct SharePackage { + /// Denotes the participant index each share is owned by. + pub index: u32, + /// This participant's share. + pub(crate) share: Share, + /// This participant's public key. + pub(crate) public: Public, + /// The public signing key that represents the entire group. + pub(crate) group_public: VerificationKey, +} + +impl TryFrom for KeyPackage { + 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(sharepackage: SharePackage) -> Result { + verify_share(&sharepackage.share)?; + + Ok(KeyPackage { + index: sharepackage.index, + secret_share: sharepackage.share.value, + public: sharepackage.public, + group_public: sharepackage.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. +pub struct KeyPackage { + index: u32, + secret_share: Secret, + public: Public, + group_public: VerificationKey, +} + +/// Public data that contains all the signer's 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 participant's public keys to perform verification before + /// publishing a signature. signer_pubkeys represents all signers for a + /// signing operation. + pub(crate) signer_pubkeys: HashMap, + /// group_public represents the joint public key for the entire group. + pub group_public: VerificationKey, +} + +/// 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. +pub fn keygen_with_dealer( + num_signers: u32, + threshold: u32, + mut rng: R, +) -> Result<(Vec, PublicKeyPackage), &'static str> { + let mut bytes = [0; 64]; + rng.fill_bytes(&mut bytes); + + let secret = Secret(Scalar::from_bytes_wide(&bytes)); + let group_public = VerificationKey::from(&secret.0); + let shares = generate_shares(&secret, num_signers, threshold, rng)?; + let mut sharepackages: Vec = Vec::with_capacity(num_signers as usize); + let mut signer_pubkeys: HashMap = HashMap::with_capacity(num_signers as usize); + + for share in shares { + let signer_public = Public(SpendAuth::basepoint() * share.value.0); + sharepackages.push(SharePackage { + index: share.receiver_index, + share: share.clone(), + public: signer_public, + group_public, + }); + + signer_pubkeys.insert(share.receiver_index, signer_public); + } + + Ok(( + sharepackages, + PublicKeyPackage { + signer_pubkeys, + group_public, + }, + )) +} + +/// Verifies that a share is consistent with a 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! +fn verify_share(share: &Share) -> Result<(), &'static str> { + let f_result = SpendAuth::basepoint() * share.value.0; + + let x = Scalar::from(share.receiver_index as u64); + + let (_, result) = share.commitment.0.iter().fold( + (Scalar::one(), jubjub::ExtendedPoint::identity()), + |(x_to_the_i, sum_so_far), comm_i| (x_to_the_i * x, sum_so_far + comm_i.0 * x_to_the_i), + ); + + if !(f_result == result) { + return Err("Share is invalid."); + } + + Ok(()) +} + +/// 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_shares`] performs verifiable secret sharing, which +/// generates shares via Shamir Secret Sharing, and then generates public +/// commitments to those shares. +/// +/// More specifically, [`generate_shares`]: +/// - Randomly samples of coefficents [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] +fn generate_shares( + secret: &Secret, + numshares: u32, + threshold: u32, + mut rng: R, +) -> Result, &'static str> { + if threshold < 1 { + return Err("Threshold cannot be 0"); + } + + if numshares < 1 { + return Err("Number of shares cannot be 0"); + } + + 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 shares: Vec = Vec::with_capacity(numshares as usize); + + let mut commitment: ShareCommitment = ShareCommitment(Vec::with_capacity(threshold as usize)); + + for _ in 0..numcoeffs { + let mut bytes = [0; 64]; + rng.fill_bytes(&mut bytes); + coefficients.push(Scalar::from_bytes_wide(&bytes)); + } + + // Verifiable secret sharing, to make sure that participants can ensure their secret is consistent + // with every other participant's. + commitment + .0 + .push(Commitment(SpendAuth::basepoint() * secret.0)); + + for c in &coefficients { + commitment.0.push(Commitment(SpendAuth::basepoint() * 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 index in 1..numshares + 1 { + let scalar_index = Scalar::from(index as u64); + let mut value = Scalar::zero(); + + // Polynomial evaluation, for this index + for i in (0..numcoeffs).rev() { + value += &coefficients[i as usize]; + value *= scalar_index; + } + value += secret.0; + + shares.push(Share { + receiver_index: index, + value: Secret(value), + commitment: commitment.clone(), + }); + } + + Ok(shares) +} + +/// 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)] +pub struct SigningNonces { + hiding: Scalar, + binding: Scalar, +} + +// TODO finish drop +impl Drop for SigningNonces { + fn drop(&mut self) {} +} + +impl SigningNonces { + /// 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, + { + let mut bytes = [0; 64]; + rng.fill_bytes(&mut bytes); + let hiding = Scalar::from_bytes_wide(&bytes); + + let mut bytes = [0; 64]; + rng.fill_bytes(&mut bytes); + let binding = Scalar::from_bytes_wide(&bytes); + + Self { hiding, 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 { + index: u32, + hiding: jubjub::ExtendedPoint, + binding: jubjub::ExtendedPoint, +} + +impl From<(u32, &SigningNonces)> for SigningCommitments { + /// For SpendAuth signatures only, not Binding signatures, in RedJubjub/Zcash. + fn from((index, nonces): (u32, &SigningNonces)) -> Self { + Self { + index, + hiding: SpendAuth::basepoint() * nonces.hiding, + binding: SpendAuth::basepoint() * nonces.binding, + } + } +} + +/// Generated by the coordinator of the signing operation and distributed to +/// each signing party. +pub struct SigningPackage { + /// Message which each participant will sign + pub message: &'static [u8], + /// The set of commitments participants published in the first round of the + /// protocol. + pub signing_commitments: Vec, +} + +/// A participant's signature share, which the coordinator will use to aggregate +/// with all other signer's shares into the joint signature. +pub struct SignatureShare { + /// Represents the participant index. + pub(crate) index: u32, + /// This participant's signature over the message. + pub(crate) signature: Scalar, +} +impl SignatureShare { + /// Tests if a signature share issued by a participant is valid before + /// aggregating it into a final joint signature to publish. + pub fn check_is_valid( + &self, + pubkey: &Public, + lambda_i: Scalar, + commitment: jubjub::ExtendedPoint, + challenge: Scalar, + ) -> Result<(), &'static str> { + if (SpendAuth::basepoint() * self.signature) + != (commitment + pubkey.0 * challenge * lambda_i) + { + return Err("Invalid signature share"); + } + Ok(()) + } +} + +/// 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. +pub fn preprocess( + num_nonces: u32, + participant_index: u32, + rng: &mut R, +) -> (Vec, Vec) +where + 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) +} + +/// Generates the binding factor that ensures each signature share is strongly +/// bound to a signing set, specific set of commitments, and a specific message. +fn gen_rho_i(index: u32, signing_package: &SigningPackage) -> Scalar { + let mut hasher = HStar::default(); + hasher + .update("FROST_rho".as_bytes()) + .update(index.to_be_bytes()) + .update(signing_package.message); + + for item in signing_package.signing_commitments.iter() { + hasher.update(item.index.to_be_bytes()); + let hiding_bytes = jubjub::AffinePoint::from(item.hiding).to_bytes(); + hasher.update(hiding_bytes); + let binding_bytes = jubjub::AffinePoint::from(item.binding).to_bytes(); + hasher.update(binding_bytes); + } + + hasher.finalize() +} + +/// Generates the group commitment which is published as part of the joint +/// Schnorr signature. +fn gen_group_commitment( + signing_package: &SigningPackage, + bindings: &HashMap, +) -> Result { + let mut accumulator = jubjub::ExtendedPoint::identity(); + + for commitment in signing_package.signing_commitments.iter() { + let rho_i = bindings + .get(&commitment.index) + .ok_or("No matching commitment index")?; + accumulator += commitment.hiding + (commitment.binding * rho_i) + } + + Ok(GroupCommitment(accumulator)) +} + +/// Generates the challenge as is required for Schnorr signatures. +fn gen_challenge( + signing_package: &SigningPackage, + group_commitment: &GroupCommitment, + group_public: &VerificationKey, +) -> Scalar { + let group_commitment_bytes = jubjub::AffinePoint::from(group_commitment.0).to_bytes(); + + HStar::default() + .update(group_commitment_bytes) + .update(group_public.bytes.bytes) + .update(signing_package.message) + .finalize() +} + +/// Generates the langrange coefficient for the i'th participant. +fn gen_lagrange_coeff( + signer_index: u32, + signing_package: &SigningPackage, +) -> Result { + let mut num = Scalar::one(); + let mut den = Scalar::one(); + for commitment in signing_package.signing_commitments.iter() { + if commitment.index == signer_index { + continue; + } + num *= Scalar::from(commitment.index as u64); + den *= Scalar::from(commitment.index as u64) - Scalar::from(signer_index as u64); + } + + if den == Scalar::zero() { + return Err("Duplicate shares provided"); + } + + // TODO: handle this unwrap better like other CtOption's + let lagrange_coeff = num * den.invert().unwrap(); + + Ok(lagrange_coeff) +} + +/// Performed once by each participant selected for the signing operation. +/// +/// Receives the message to be signed and a set of signing commitments and a set +/// of randomizing commitments to be used in that signing operation, including +/// that for this participant. +/// +/// Assumes the participant has already determined which nonce corresponds with +/// the commitment that was assigned by the coordinator in the SigningPackage. +pub fn sign( + signing_package: &SigningPackage, + participant_nonces: &SigningNonces, // TODO this should probably consume the nonce + share_package: &SharePackage, +) -> Result { + let mut bindings: HashMap = + HashMap::with_capacity(signing_package.signing_commitments.len()); + + for comm in signing_package.signing_commitments.iter() { + let rho_i = gen_rho_i(comm.index, &signing_package); + bindings.insert(comm.index, rho_i); + } + + let lambda_i = gen_lagrange_coeff(share_package.index, &signing_package)?; + + let group_commitment = gen_group_commitment(&signing_package, &bindings)?; + + let challenge = gen_challenge( + &signing_package, + &group_commitment, + &share_package.group_public, + ); + + let participant_rho_i = bindings + .get(&share_package.index) + .ok_or("No matching binding!")?; + + // The Schnorr signature share + let signature: Scalar = participant_nonces.hiding + + (participant_nonces.binding * participant_rho_i) + + (lambda_i * share_package.share.value.0 * challenge); + + Ok(SignatureShare { + index: share_package.index, + signature, + }) +} + +/// 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). +pub fn aggregate( + signing_package: &SigningPackage, + signing_shares: &[SignatureShare], + pubkeys: &PublicKeyPackage, +) -> Result, &'static str> { + let mut bindings: HashMap = + HashMap::with_capacity(signing_package.signing_commitments.len()); + + for comm in signing_package.signing_commitments.iter() { + let rho_i = gen_rho_i(comm.index, &signing_package); + bindings.insert(comm.index, rho_i); + } + + let group_commitment = gen_group_commitment(&signing_package, &bindings)?; + + let challenge = gen_challenge(&signing_package, &group_commitment, &pubkeys.group_public); + + for signing_share in signing_shares { + let signer_pubkey = pubkeys.signer_pubkeys[&signing_share.index]; + let lambda_i = gen_lagrange_coeff(signing_share.index, &signing_package)?; + let signer_commitment = signing_package + .signing_commitments + .iter() + .find(|comm| comm.index == signing_share.index) + .ok_or("No matching signing commitment for signer")?; + + let commitment_i = + signer_commitment.hiding + (signer_commitment.binding * bindings[&signing_share.index]); + + signing_share.check_is_valid(&signer_pubkey, lambda_i, commitment_i, challenge)?; + } + + // The aggregation of the signature shares by summing them up, resulting in + // a plain Schnorr signature. + let mut z = Scalar::zero(); + for signature_share in signing_shares { + z += signature_share.signature; + } + + Ok(Signature { + r_bytes: jubjub::AffinePoint::from(group_commitment.0).to_bytes(), + s_bytes: z.to_bytes(), + _marker: PhantomData, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::thread_rng; + + fn reconstruct_secret(shares: Vec) -> Result { + let numshares = shares.len(); + + if numshares < 1 { + return Err("No shares provided"); + } + + let mut lagrange_coeffs: Vec = Vec::with_capacity(numshares as usize); + + for i in 0..numshares { + let mut num = Scalar::one(); + let mut den = Scalar::one(); + for j in 0..numshares { + if j == i { + continue; + } + num *= Scalar::from(shares[j].receiver_index as u64); + den *= Scalar::from(shares[j].receiver_index as u64) + - Scalar::from(shares[i].receiver_index as u64); + } + if den == Scalar::zero() { + return Err("Duplicate shares provided"); + } + lagrange_coeffs.push(num * den.invert().unwrap()); + } + + let mut secret = Scalar::zero(); + + for i in 0..numshares { + secret += lagrange_coeffs[i] * shares[i].value.0; + } + + Ok(secret) + } + + /// This is testing that Shamir's secret sharing to compute and arbitrary + /// value is working. + #[test] + fn check_share_generation() { + let mut rng = thread_rng(); + + let mut bytes = [0; 64]; + rng.fill_bytes(&mut bytes); + let secret = Secret(Scalar::from_bytes_wide(&bytes)); + + let _ = SpendAuth::basepoint() * secret.0; + + let shares = generate_shares(&secret, 5, 3, rng).unwrap(); + + for share in shares.iter() { + assert_eq!(verify_share(&share), Ok(())); + } + + assert_eq!(reconstruct_secret(shares).unwrap(), secret.0) + } +} diff --git a/src/hash.rs b/src/hash.rs index 7c52eb1..684b872 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -1,3 +1,13 @@ +// -*- mode: rust; -*- +// +// This file is part of redjubjub. +// Copyright (c) 2019-2021 Zcash Foundation +// See LICENSE for licensing information. +// +// Authors: +// - Deirdre Connolly +// - Henry de Valence + use crate::Scalar; use blake2b_simd::{Params, State}; diff --git a/src/lib.rs b/src/lib.rs index a3bbbc1..01c7626 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,13 @@ +// -*- mode: rust; -*- +// +// This file is part of redjubjub. +// Copyright (c) 2019-2021 Zcash Foundation +// See LICENSE for licensing information. +// +// Authors: +// - Deirdre Connolly +// - Henry de Valence + #![doc(html_root_url = "https://docs.rs/redjubjub/0.2.2")] #![cfg_attr(feature = "nightly", feature(external_doc))] #![cfg_attr(feature = "nightly", doc(include = "../README.md"))] @@ -8,6 +18,7 @@ pub mod batch; mod constants; mod error; +pub mod frost; mod hash; mod scalar_mul; mod signature; diff --git a/src/scalar_mul.rs b/src/scalar_mul.rs index 6522c3e..d8f3d81 100644 --- a/src/scalar_mul.rs +++ b/src/scalar_mul.rs @@ -1,3 +1,15 @@ +// -*- mode: rust; -*- +// +// This file is part of redjubjub. +// Copyright (c) 2019-2021 Zcash Foundation +// Copyright (c) 2017-2021 isis agora lovecruft, Henry de Valence +// See LICENSE for licensing information. +// +// Authors: +// - isis agora lovecruft +// - Henry de Valence +// - Deirdre Connolly + use std::{borrow::Borrow, fmt::Debug}; use jubjub::*; diff --git a/src/signature.rs b/src/signature.rs index acb9bd1..8ad09f5 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -1,3 +1,12 @@ +// -*- mode: rust; -*- +// +// This file is part of redjubjub. +// Copyright (c) 2019-2021 Zcash Foundation +// See LICENSE for licensing information. +// +// Authors: +// - Henry de Valence + use std::marker::PhantomData; use crate::SigType; diff --git a/src/signing_key.rs b/src/signing_key.rs index 9659e84..3f3183c 100644 --- a/src/signing_key.rs +++ b/src/signing_key.rs @@ -1,3 +1,13 @@ +// -*- mode: rust; -*- +// +// This file is part of redjubjub. +// Copyright (c) 2019-2021 Zcash Foundation +// See LICENSE for licensing information. +// +// Authors: +// - Deirdre Connolly +// - Henry de Valence + use std::{ convert::{TryFrom, TryInto}, marker::PhantomData, diff --git a/src/verification_key.rs b/src/verification_key.rs index 1d898f9..be67405 100644 --- a/src/verification_key.rs +++ b/src/verification_key.rs @@ -1,3 +1,13 @@ +// -*- mode: rust; -*- +// +// This file is part of redjubjub. +// Copyright (c) 2019-2021 Zcash Foundation +// See LICENSE for licensing information. +// +// Authors: +// - Deirdre Connolly +// - Henry de Valence + use std::{ convert::TryFrom, hash::{Hash, Hasher}, diff --git a/tests/frost.rs b/tests/frost.rs new file mode 100644 index 0000000..32ec121 --- /dev/null +++ b/tests/frost.rs @@ -0,0 +1,62 @@ +use rand::thread_rng; +use std::collections::HashMap; + +use redjubjub::frost; + +#[test] +fn check_sign_with_dealer() { + let mut rng = thread_rng(); + let numsigners = 5; + let threshold = 3; + let (shares, pubkeys) = frost::keygen_with_dealer(numsigners, threshold, &mut rng).unwrap(); + + let mut nonces: HashMap> = + HashMap::with_capacity(threshold as usize); + let mut commitments: Vec = Vec::with_capacity(threshold as usize); + + // 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::preprocess(1, participant_index, &mut rng); + nonces.insert(participant_index, nonce); + commitments.push(commitment[0]); + } + + // 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::with_capacity(threshold as usize); + let message = "message to sign".as_bytes(); + let signing_package = frost::SigningPackage { + message, + signing_commitments: commitments, + }; + + // Round 2: each participant generates their signature share + for (participant_index, nonce) in nonces { + let share_package = shares + .iter() + .find(|share| participant_index == share.index) + .unwrap(); + let nonce_to_use = &nonce[0]; + // Each participant generates their signature share. + let signature_share = frost::sign(&signing_package, &nonce_to_use, share_package).unwrap(); + signature_shares.push(signature_share); + } + + // The aggregator collects the signing shares from all participants and + // generates the final signature. + 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 (aka verification key). + assert!(pubkeys + .group_public + .verify(&message, &group_signature) + .is_ok()); + + // TODO: also check that the SharePackage.group_public also verifies the group signature. +}