From 039f32947b8619516e7c5b26a144ce56def8dbad Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Tue, 6 Feb 2024 12:47:40 +0100 Subject: [PATCH] refactor: avoid using crypto primitives directly, part 1 --- ferveo-tdec/benches/tpke.rs | 6 +- ferveo-tdec/src/ciphertext.rs | 14 +- ferveo-tdec/src/decryption.rs | 34 ++-- ferveo-tdec/src/key_share.rs | 13 +- ferveo-tdec/src/lib.rs | 34 +++- ferveo/src/api.rs | 33 ++-- ferveo/src/dkg.rs | 50 +++-- ferveo/src/lib.rs | 64 ++++--- ferveo/src/pvss.rs | 47 +++-- ferveo/src/refresh.rs | 332 +++++++++++++++++++++------------- 10 files changed, 378 insertions(+), 249 deletions(-) diff --git a/ferveo-tdec/benches/tpke.rs b/ferveo-tdec/benches/tpke.rs index 420bf869..0bbba434 100644 --- a/ferveo-tdec/benches/tpke.rs +++ b/ferveo-tdec/benches/tpke.rs @@ -1,6 +1,6 @@ #![allow(clippy::redundant_closure)] -use ark_bls12_381::{Bls12_381, Fr, G1Affine as G1, G2Affine as G2}; +use ark_bls12_381::{Bls12_381, Fr}; use ark_ec::pairing::Pairing; use criterion::{ black_box, criterion_group, criterion_main, BenchmarkId, Criterion, @@ -25,8 +25,8 @@ struct SetupShared { shares_num: usize, msg: Vec, aad: Vec, - pubkey: G1, - privkey: G2, + pubkey: PublicKeyShare, + privkey: PrivateKeyShare, ciphertext: Ciphertext, shared_secret: SharedSecret, } diff --git a/ferveo-tdec/src/ciphertext.rs b/ferveo-tdec/src/ciphertext.rs index 81f79389..cdaf956c 100644 --- a/ferveo-tdec/src/ciphertext.rs +++ b/ferveo-tdec/src/ciphertext.rs @@ -13,7 +13,10 @@ use serde_with::serde_as; use sha2::{digest::Digest, Sha256}; use zeroize::ZeroizeOnDrop; -use crate::{htp_bls12381_g2, Error, Result, SecretBox, SharedSecret}; +use crate::{ + htp_bls12381_g2, Error, PrivateKeyShare, PublicKeyShare, Result, SecretBox, + SharedSecret, +}; #[serde_as] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -95,7 +98,7 @@ impl CiphertextHeader { pub fn encrypt( message: SecretBox>, aad: &[u8], - pubkey: &E::G1Affine, + pubkey: &PublicKeyShare, rng: &mut impl rand::Rng, ) -> Result> { // r @@ -105,7 +108,8 @@ pub fn encrypt( // h let h_gen = E::G2Affine::generator(); - let ry_prep = E::G1Prepared::from(pubkey.mul(rand_element).into()); + let ry_prep = + E::G1Prepared::from(pubkey.public_key_share.mul(rand_element).into()); // s let product = E::pairing(ry_prep, h_gen).0; // u @@ -140,13 +144,13 @@ pub fn encrypt( pub fn decrypt_symmetric( ciphertext: &Ciphertext, aad: &[u8], - private_key: &E::G2Affine, + private_key: &PrivateKeyShare, g_inv: &E::G1Prepared, ) -> Result> { ciphertext.check(aad, g_inv)?; let shared_secret = E::pairing( E::G1Prepared::from(ciphertext.commitment), - E::G2Prepared::from(*private_key), + E::G2Prepared::from(private_key.private_key_share), ) .0; let shared_secret = SharedSecret(shared_secret); diff --git a/ferveo-tdec/src/decryption.rs b/ferveo-tdec/src/decryption.rs index 0622e6a8..eb9068bd 100644 --- a/ferveo-tdec/src/decryption.rs +++ b/ferveo-tdec/src/decryption.rs @@ -2,6 +2,7 @@ use std::ops::Mul; use ark_ec::{pairing::Pairing, CurveGroup}; use ark_ff::{Field, One, Zero}; +use ark_std::UniformRand; use ferveo_common::serialization; use itertools::{izip, zip_eq}; use rand_core::RngCore; @@ -9,8 +10,8 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_with::serde_as; use crate::{ - generate_random, Ciphertext, CiphertextHeader, PrivateKeyShare, - PublicDecryptionContextFast, PublicDecryptionContextSimple, Result, + Ciphertext, CiphertextHeader, PrivateKeyShare, PublicDecryptionContextFast, + PublicDecryptionContextSimple, Result, }; #[serde_as] @@ -226,6 +227,15 @@ impl DecryptionSharePrecomputed { } } +pub fn generate_random_scalars( + n: usize, + rng: &mut R, +) -> Vec { + (0..n) + .map(|_| E::ScalarField::rand(rng)) + .collect::>() +} + // TODO: Remove this code? Currently only used in benchmarks. Move to benchmark suite? pub fn batch_verify_decryption_shares( pub_contexts: &[PublicDecryptionContextFast], @@ -240,16 +250,17 @@ pub fn batch_verify_decryption_shares( let blinding_keys = decryption_shares[0] .iter() .map(|d| { - pub_contexts[d.decrypter_index] - .blinded_key_share - .blinding_key_prepared - .clone() + E::G2Prepared::from( + pub_contexts[d.decrypter_index] + .blinded_key_share + .blinding_key, + ) }) .collect::>(); // For each ciphertext, generate num_shares random scalars let alpha_ij = (0..num_ciphertexts) - .map(|_| generate_random::<_, E>(num_shares, rng)) + .map(|_| generate_random_scalars::<_, E>(num_shares, rng)) .collect::>(); let mut pairings_a = Vec::with_capacity(num_shares + 1); @@ -302,10 +313,11 @@ pub fn verify_decryption_shares_fast( let blinding_keys = decryption_shares .iter() .map(|d| { - pub_contexts[d.decrypter_index] - .blinded_key_share - .blinding_key_prepared - .clone() + E::G2Prepared::from( + pub_contexts[d.decrypter_index] + .blinded_key_share + .blinding_key, + ) }) .collect::>(); diff --git a/ferveo-tdec/src/key_share.rs b/ferveo-tdec/src/key_share.rs index 2daaae56..64a9e3e4 100644 --- a/ferveo-tdec/src/key_share.rs +++ b/ferveo-tdec/src/key_share.rs @@ -7,6 +7,7 @@ use rand_core::RngCore; use zeroize::ZeroizeOnDrop; #[derive(Debug, Clone)] +// TODO: Should we rename it to PublicKey or SharedPublicKey? pub struct PublicKeyShare { pub public_key_share: E::G1Affine, // A_{i, \omega_i} } @@ -15,16 +16,6 @@ pub struct PublicKeyShare { pub struct BlindedKeyShare { pub blinding_key: E::G2Affine, // [b] H pub blinded_key_share: E::G2Affine, // [b] Z_{i, \omega_i} - pub blinding_key_prepared: E::G2Prepared, -} - -pub fn generate_random( - n: usize, - rng: &mut R, -) -> Vec { - (0..n) - .map(|_| E::ScalarField::rand(rng)) - .collect::>() } impl BlindedKeyShare { @@ -60,6 +51,7 @@ impl BlindedKeyShare { #[derive(Debug, Clone, PartialEq, Eq, ZeroizeOnDrop)] pub struct PrivateKeyShare { + // TODO: Replace with a tuple? pub private_key_share: E::G2Affine, } @@ -68,7 +60,6 @@ impl PrivateKeyShare { let blinding_key = E::G2Affine::generator().mul(b).into_affine(); BlindedKeyShare:: { blinding_key, - blinding_key_prepared: E::G2Prepared::from(blinding_key), blinded_key_share: self.private_key_share.mul(b).into_affine(), } } diff --git a/ferveo-tdec/src/lib.rs b/ferveo-tdec/src/lib.rs index 297b066c..b5ffed22 100644 --- a/ferveo-tdec/src/lib.rs +++ b/ferveo-tdec/src/lib.rs @@ -77,8 +77,8 @@ pub mod test_common { shares_num: usize, rng: &mut impl RngCore, ) -> ( - E::G1Affine, - E::G2Affine, + PublicKeyShare, + PrivateKeyShare, Vec>, ) { assert!(shares_num >= threshold); @@ -138,7 +138,7 @@ pub mod test_common { ) .enumerate() { - let private_key_share = PrivateKeyShare:: { + let private_key_share = PrivateKeyShare { private_key_share: *private, }; let b = E::ScalarField::rand(rng); @@ -171,7 +171,15 @@ pub mod test_common { private.public_decryption_contexts = public_contexts.clone(); } - (pubkey.into(), privkey.into(), private_contexts) + ( + PublicKeyShare { + public_key_share: pubkey.into(), + }, + PrivateKeyShare { + private_key_share: privkey.into(), + }, + private_contexts, + ) } pub fn setup_simple( @@ -179,8 +187,8 @@ pub mod test_common { shares_num: usize, rng: &mut impl rand::Rng, ) -> ( - E::G1Affine, - E::G2Affine, + PublicKeyShare, + PrivateKeyShare, Vec>, ) { assert!(shares_num >= threshold); @@ -259,15 +267,23 @@ pub mod test_common { private.public_decryption_contexts = public_contexts.clone(); } - (pubkey.into(), privkey.into(), private_contexts) + ( + PublicKeyShare { + public_key_share: pubkey.into(), + }, + PrivateKeyShare { + private_key_share: privkey.into(), + }, + private_contexts, + ) } pub fn setup_precomputed( shares_num: usize, rng: &mut impl rand::Rng, ) -> ( - E::G1Affine, - E::G2Affine, + PublicKeyShare, + PrivateKeyShare, Vec>, ) { // In precomputed variant, the security threshold is equal to the number of shares diff --git a/ferveo/src/api.rs b/ferveo/src/api.rs index caa0d9b4..2371f552 100644 --- a/ferveo/src/api.rs +++ b/ferveo/src/api.rs @@ -7,8 +7,10 @@ use bincode; use ferveo_common::serialization; pub use ferveo_tdec::api::{ prepare_combine_simple, share_combine_precomputed, share_combine_simple, - Fr, G1Affine, G1Prepared, G2Affine, SecretBox, E, + DecryptionSharePrecomputed, Fr, G1Affine, G1Prepared, G2Affine, SecretBox, + E, }; +use ferveo_tdec::PublicKeyShare; use generic_array::{ typenum::{Unsigned, U48}, GenericArray, @@ -17,13 +19,6 @@ use rand::RngCore; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -pub type PublicKey = ferveo_common::PublicKey; -pub type Keypair = ferveo_common::Keypair; -pub type Validator = crate::Validator; -pub type Transcript = PubliclyVerifiableSS; - -pub type ValidatorMessage = (Validator, Transcript); - #[cfg(feature = "bindings-python")] use crate::bindings_python; #[cfg(feature = "bindings-wasm")] @@ -34,8 +29,11 @@ use crate::{ PubliclyVerifiableSS, Result, }; -pub type DecryptionSharePrecomputed = - ferveo_tdec::api::DecryptionSharePrecomputed; +pub type PublicKey = ferveo_common::PublicKey; +pub type Keypair = ferveo_common::Keypair; +pub type Validator = crate::Validator; +pub type Transcript = PubliclyVerifiableSS; +pub type ValidatorMessage = (Validator, Transcript); // Normally, we would use a custom trait for this, but we can't because // the arkworks will not let us create a blanket implementation for G1Affine @@ -58,8 +56,14 @@ pub fn encrypt( pubkey: &DkgPublicKey, ) -> Result { let mut rng = rand::thread_rng(); - let ciphertext = - ferveo_tdec::api::encrypt(message, aad, &pubkey.0, &mut rng)?; + let ciphertext = ferveo_tdec::api::encrypt( + message, + aad, + &PublicKeyShare { + public_key_share: pubkey.0, + }, + &mut rng, + )?; Ok(Ciphertext(ciphertext)) } @@ -91,7 +95,7 @@ impl Ciphertext { } } -#[serde_as] +#[serde_as] // TODO: Redundant serde_as? #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct CiphertextHeader(ferveo_tdec::api::CiphertextHeader); @@ -146,6 +150,7 @@ impl From for FerveoVariant { #[serde_as] #[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct DkgPublicKey( + // TODO: Consider not using G1Affine directly here #[serde_as(as = "serialization::SerdeAs")] pub(crate) G1Affine, ); @@ -218,7 +223,7 @@ impl Dkg { } pub fn public_key(&self) -> DkgPublicKey { - DkgPublicKey(self.0.public_key()) + DkgPublicKey(self.0.public_key().public_key_share) } pub fn generate_transcript( diff --git a/ferveo/src/dkg.rs b/ferveo/src/dkg.rs index e8afbe30..b309449e 100644 --- a/ferveo/src/dkg.rs +++ b/ferveo/src/dkg.rs @@ -69,9 +69,14 @@ pub type PVSSMap = BTreeMap>; #[derive(Debug, Clone)] pub enum DkgState { // TODO: Do we need to keep track of the block number? - Sharing { accumulated_shares: u32, block: u32 }, + Sharing { + accumulated_shares: u32, + block: u32, + }, Dealt, - Success { public_key: E::G1Affine }, + Success { + public_key: ferveo_tdec::PublicKeyShare, + }, Invalid, } @@ -178,7 +183,7 @@ impl PubliclyVerifiableDkg { let pvss_list = self.vss.values().cloned().collect::>(); Ok(Message::Aggregate(Aggregation { vss: aggregate(&pvss_list)?, - public_key, + public_key: public_key.public_key_share, })) } _ => Err(Error::InvalidDkgStateToAggregate), @@ -186,12 +191,15 @@ impl PubliclyVerifiableDkg { } /// Returns the public key generated by the DKG - pub fn public_key(&self) -> E::G1Affine { - self.vss - .values() - .map(|vss| vss.coeffs[0].into_group()) - .sum::() - .into_affine() + pub fn public_key(&self) -> ferveo_tdec::PublicKeyShare { + ferveo_tdec::PublicKeyShare { + public_key_share: self + .vss + .values() + .map(|vss| vss.coeffs[0].into_group()) + .sum::() + .into_affine(), + } } /// Return a domain point for the share_index @@ -245,7 +253,7 @@ impl PubliclyVerifiableDkg { )) } else if vss.verify_aggregation(self).is_err() { Err(Error::InvalidTranscriptAggregate) - } else if &self.public_key() == public_key { + } else if &self.public_key().public_key_share == public_key { Ok(()) } else { Err(Error::InvalidDkgPublicKey) @@ -382,6 +390,7 @@ mod test_dkg_init { #[cfg(test)] mod test_dealing { use ark_ec::AffineRepr; + use ferveo_tdec::PublicKeyShare; use crate::{ test_common::*, DkgParams, DkgState, DkgState::Dealt, Error, @@ -587,7 +596,9 @@ mod test_dealing { )); dkg.state = DkgState::Success { - public_key: G1::zero(), + public_key: PublicKeyShare { + public_key_share: G1::zero(), + }, }; assert!(dkg.share(rng).is_err()); @@ -614,7 +625,9 @@ mod test_dealing { let sender = dkg.me.clone(); dkg.state = DkgState::Success { - public_key: G1::zero(), + public_key: PublicKeyShare { + public_key_share: G1::zero(), + }, }; assert!(dkg.verify_message(&sender, &pvss).is_err()); assert!(dkg.apply_message(&sender, &pvss).is_err()); @@ -631,6 +644,7 @@ mod test_dealing { #[cfg(test)] mod test_aggregation { use ark_ec::AffineRepr; + use ferveo_tdec::PublicKeyShare; use test_case::test_case; use crate::{dkg::*, test_common::*, DkgState, Message}; @@ -649,7 +663,7 @@ mod test_aggregation { if let Message::Aggregate(Aggregation { public_key, .. }) = &aggregate_msg { - assert_eq!(public_key, &dkg.public_key()); + assert_eq!(public_key, &dkg.public_key().public_key_share); } else { panic!("Expected aggregate message") } @@ -669,7 +683,9 @@ mod test_aggregation { }; assert!(dkg.aggregate().is_err()); dkg.state = DkgState::Success { - public_key: G1::zero(), + public_key: PublicKeyShare { + public_key_share: G1::zero(), + }, }; assert!(dkg.aggregate().is_err()); } @@ -690,7 +706,9 @@ mod test_aggregation { assert!(dkg.apply_message(&sender, &aggregate).is_err()); dkg.state = DkgState::Success { - public_key: G1::zero(), + public_key: PublicKeyShare { + public_key_share: G1::zero(), + }, }; assert!(dkg.verify_message(&sender, &aggregate).is_err()); assert!(dkg.apply_message(&sender, &aggregate).is_err()) @@ -713,7 +731,7 @@ mod test_aggregation { fn test_aggregate_wont_verify_if_wrong_key() { let (dkg, _) = setup_dealt_dkg(); let mut aggregate = dkg.aggregate().unwrap(); - while dkg.public_key() == G1::zero() { + while dkg.public_key().public_key_share == G1::zero() { let (_dkg, _) = setup_dealt_dkg(); } if let Message::Aggregate(Aggregation { public_key, .. }) = diff --git a/ferveo/src/lib.rs b/ferveo/src/lib.rs index f9d6c1a5..0e9cc3ca 100644 --- a/ferveo/src/lib.rs +++ b/ferveo/src/lib.rs @@ -422,13 +422,14 @@ mod test_dkg_full { let share_updates = remaining_validators .keys() .map(|v_addr| { - let deltas_i = prepare_share_updates_for_recovery::( - &domain_points, - &dkg.pvss_params.h.into_affine(), - &x_r, - dkg.dkg_params.security_threshold() as usize, - rng, - ); + let deltas_i = + ShareRecoveryUpdate::make_share_updates_for_recovery( + &domain_points, + &dkg.pvss_params.h.into_affine(), + &x_r, + dkg.dkg_params.security_threshold() as usize, + rng, + ); (v_addr.clone(), deltas_i) }) .collect::>(); @@ -444,8 +445,9 @@ mod test_dkg_full { let updates_for_participant: Vec<_> = share_updates .values() .map(|updates| { - *updates.get(validator.share_index as usize).unwrap() + updates.get(validator.share_index as usize).unwrap() }) + .cloned() .collect(); // Each validator uses their decryption key to update their share @@ -459,7 +461,7 @@ mod test_dkg_full { let pvss_list = dkg.vss.values().cloned().collect::>(); let pvss_aggregated = aggregate(&pvss_list).unwrap(); pvss_aggregated - .update_private_key_share_for_recovery( + .make_updated_private_key_share( &decryption_key, validator.share_index as usize, updates_for_participant.as_slice(), @@ -468,14 +470,13 @@ mod test_dkg_full { }) .collect(); - // TODO: Rename updated_private_shares to something that doesn't imply mutation (see #162, #163) - // Now, we have to combine new share fragments into a new share - let new_private_key_share = recover_share_from_updated_private_shares( - &x_r, - &domain_points, - &updated_shares, - ); + let recovered_key_share = + PrivateKeyShare::recover_share_from_updated_private_shares( + &x_r, + &domain_points, + &updated_shares, + ); // Get decryption shares from remaining participants let mut remaining_validator_keypairs = validator_keypairs; @@ -508,7 +509,7 @@ mod test_dkg_full { decryption_shares.push( DecryptionShareSimple::create( &new_validator_decryption_key, - &new_private_key_share, + &recovered_key_share, &ciphertext.header().unwrap(), AAD, &dkg.pvss_params.g_inv(), @@ -566,12 +567,13 @@ mod test_dkg_full { .validators .keys() .map(|v_addr| { - let deltas_i = prepare_share_updates_for_refresh::( - &dkg.domain_points(), - &dkg.pvss_params.h.into_affine(), - dkg.dkg_params.security_threshold() as usize, - rng, - ); + let deltas_i = + ShareRefreshUpdate::make_share_updates_for_refresh( + &dkg.domain_points(), + &dkg.pvss_params.h.into_affine(), + dkg.dkg_params.security_threshold() as usize, + rng, + ); (v_addr.clone(), deltas_i) }) .collect::>(); @@ -580,7 +582,7 @@ mod test_dkg_full { // Now, every participant separately: // TODO: Move this logic outside tests (see #162, #163) - let updated_shares: Vec<_> = dkg + let updated_private_key_shares: Vec<_> = dkg .validators .values() .map(|validator| { @@ -588,7 +590,10 @@ mod test_dkg_full { let updates_for_participant: Vec<_> = share_updates .values() .map(|updates| { - *updates.get(validator.share_index as usize).unwrap() + updates + .get(validator.share_index as usize) + .cloned() + .unwrap() }) .collect(); @@ -603,7 +608,7 @@ mod test_dkg_full { let pvss_list = dkg.vss.values().cloned().collect::>(); let pvss_aggregated = aggregate(&pvss_list).unwrap(); pvss_aggregated - .update_private_key_share_for_recovery( + .make_updated_private_key_share( &decryption_key, validator.share_index as usize, updates_for_participant.as_slice(), @@ -620,7 +625,12 @@ mod test_dkg_full { .map(|(share_index, validator_keypair)| { DecryptionShareSimple::create( &validator_keypair.decryption_key, - updated_shares.get(share_index).unwrap(), + // In order to proceed with the decryption, we need to convert the updated private key shares + &updated_private_key_shares + .get(share_index) + .unwrap() + .inner() + .0, &ciphertext.header().unwrap(), AAD, &dkg.pvss_params.g_inv(), diff --git a/ferveo/src/pvss.rs b/ferveo/src/pvss.rs index 34695bd3..910c7cbd 100644 --- a/ferveo/src/pvss.rs +++ b/ferveo/src/pvss.rs @@ -8,7 +8,7 @@ use ark_poly::{ }; use ferveo_tdec::{ prepare_combine_simple, CiphertextHeader, DecryptionSharePrecomputed, - DecryptionShareSimple, PrivateKeyShare, + DecryptionShareSimple, }; use itertools::Itertools; use rand::RngCore; @@ -18,9 +18,9 @@ use subproductdomain::fast_multiexp; use zeroize::{self, Zeroize, ZeroizeOnDrop}; use crate::{ - apply_updates_to_private_share, assert_no_share_duplicates, - batch_to_projective_g1, batch_to_projective_g2, Error, PVSSMap, - PubliclyVerifiableDkg, Result, Validator, + assert_no_share_duplicates, batch_to_projective_g1, batch_to_projective_g2, + Error, PVSSMap, PrivateKeyShare, PrivateKeyShareUpdate, + PubliclyVerifiableDkg, Result, UpdatedPrivateKeyShare, Validator, }; /// These are the blinded evaluations of shares of a single random polynomial @@ -304,7 +304,7 @@ impl PubliclyVerifiableSS { validator_decryption_key: &E::ScalarField, share_index: usize, ) -> Result> { - // Decrypt private key shares https://nikkolasg.github.io/ferveo/pvss.html#validator-decryption-of-private-key-shares + // Decrypt private key share https://nikkolasg.github.io/ferveo/pvss.html#validator-decryption-of-private-key-shares let private_key_share = self .shares .get(share_index) @@ -315,7 +315,10 @@ impl PubliclyVerifiableSS { .expect("Validator decryption key must have an inverse"), ) .into_affine(); - Ok(PrivateKeyShare { private_key_share }) + // TODO: Consider adding a from trait to simplify this conversion + let private_key_share = + ferveo_tdec::PrivateKeyShare { private_key_share }; + Ok(PrivateKeyShare(private_key_share)) } pub fn make_decryption_share_simple( @@ -330,7 +333,7 @@ impl PubliclyVerifiableSS { .decrypt_private_key_share(validator_decryption_key, share_index)?; DecryptionShareSimple::create( validator_decryption_key, - &private_key_share, + &private_key_share.0, ciphertext, aad, g_inv, @@ -356,7 +359,7 @@ impl PubliclyVerifiableSS { DecryptionSharePrecomputed::new( share_index, validator_decryption_key, - &private_key_share, + &private_key_share.0, ciphertext_header, aad, &lagrange_coeffs[share_index], @@ -365,22 +368,16 @@ impl PubliclyVerifiableSS { .map_err(|e| e.into()) } - // TODO: Consider relocate to different place, maybe PrivateKeyShare? (see #162, #163) - pub fn update_private_key_share_for_recovery( + pub fn make_updated_private_key_share( &self, validator_decryption_key: &E::ScalarField, share_index: usize, - share_updates: &[E::G2], - ) -> Result> { - // Retrieves their private key share - let private_key_share = self - .decrypt_private_key_share(validator_decryption_key, share_index)?; - - // And updates their share - Ok(apply_updates_to_private_share::( - &private_key_share, - share_updates, - )) + share_updates: &[impl PrivateKeyShareUpdate], + ) -> Result> { + // Retrieve the private key share and apply the updates + Ok(self + .decrypt_private_key_share(validator_decryption_key, share_index)? + .make_updated_key_share(share_updates)) } } @@ -435,8 +432,8 @@ mod test_pvss { /// Test the happy flow such that the PVSS with the correct form is created /// and that appropriate validations pass - #[test_case(4,4; "number of validators is equal to the number of shares")] - #[test_case(4,6; "number of validators is greater than the number of shares")] + #[test_case(4, 4; "number of validators is equal to the number of shares")] + #[test_case(4, 6; "number of validators is greater than the number of shares")] fn test_new_pvss(shares_num: u32, validators_num: u32) { let rng = &mut ark_std::test_rng(); let security_threshold = shares_num - 1; @@ -510,8 +507,8 @@ mod test_pvss { /// Check that happy flow of aggregating PVSS transcripts /// has the correct form and it's validations passes - #[test_case(4,4; "number of validators is equal to the number of shares")] - #[test_case(4,6; "number of validators is greater than the number of shares")] + #[test_case(4, 4; "number of validators is equal to the number of shares")] + #[test_case(4, 6; "number of validators is greater than the number of shares")] fn test_aggregate_pvss(shares_num: u32, validators_num: u32) { let security_threshold = shares_num - 1; let (dkg, _) = setup_dealt_dkg_with_n_validators( diff --git a/ferveo/src/refresh.rs b/ferveo/src/refresh.rs index b02eba3b..761481fa 100644 --- a/ferveo/src/refresh.rs +++ b/ferveo/src/refresh.rs @@ -1,86 +1,155 @@ use std::{ops::Mul, usize}; -use ark_ec::{pairing::Pairing, AffineRepr, CurveGroup}; +use ark_ec::{pairing::Pairing, CurveGroup}; use ark_ff::Zero; use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial, Polynomial}; -use ferveo_tdec::{lagrange_basis_at, PrivateKeyShare}; +use ferveo_tdec::lagrange_basis_at; use itertools::zip_eq; use rand_core::RngCore; +use zeroize::ZeroizeOnDrop; -// SHARE UPDATE FUNCTIONS: +type InnerPrivateKeyShare = ferveo_tdec::PrivateKeyShare; -/// From PSS paper, section 4.2.1, (https://link.springer.com/content/pdf/10.1007/3-540-44750-4_27.pdf) -pub fn prepare_share_updates_for_recovery( - domain_points: &[E::ScalarField], - h: &E::G2Affine, - x_r: &E::ScalarField, - threshold: usize, - rng: &mut impl RngCore, -) -> Vec { - // Update polynomial has root at x_r - prepare_share_updates_with_root::(domain_points, h, x_r, threshold, rng) +/// Private key share held by a participant in the DKG protocol. +#[derive(Debug, Clone, PartialEq, Eq, ZeroizeOnDrop)] +pub struct PrivateKeyShare(pub InnerPrivateKeyShare); + +impl PrivateKeyShare { + /// From PSS paper, section 4.2.3, (https://link.springer.com/content/pdf/10.1007/3-540-44750-4_27.pdf) + pub fn make_updated_key_share( + &self, + share_updates: &[impl PrivateKeyShareUpdate], + ) -> UpdatedPrivateKeyShare { + let updated_key_share = share_updates + .iter() + .fold(self.0.private_key_share, |acc, delta| { + (acc + delta.inner().private_key_share).into() + }); + let updated_key_share = InnerPrivateKeyShare { + private_key_share: updated_key_share, + }; + UpdatedPrivateKeyShare(updated_key_share) + } + + /// From the PSS paper, section 4.2.4, (https://link.springer.com/content/pdf/10.1007/3-540-44750-4_27.pdf) + pub fn recover_share_from_updated_private_shares( + x_r: &E::ScalarField, + domain_points: &[E::ScalarField], + updated_private_shares: &[UpdatedPrivateKeyShare], + ) -> ferveo_tdec::PrivateKeyShare { + // Interpolate new shares to recover y_r + let lagrange = lagrange_basis_at::(domain_points, x_r); + let prods = zip_eq(updated_private_shares, lagrange) + .map(|(y_j, l)| y_j.0.private_key_share.mul(l)); + let y_r = prods.fold(E::G2::zero(), |acc, y_j| acc + y_j); + ferveo_tdec::PrivateKeyShare { + private_key_share: y_r.into_affine(), + } + } } -// TODO: Consider relocating to PrivateKeyShare (see #162, #163) -/// From PSS paper, section 4.2.3, (https://link.springer.com/content/pdf/10.1007/3-540-44750-4_27.pdf) -pub fn apply_updates_to_private_share( - private_key_share: &PrivateKeyShare, - share_updates: &[E::G2], -) -> PrivateKeyShare { - let private_key_share = share_updates - .iter() - .fold( - private_key_share.private_key_share.into_group(), - |acc, delta| acc + delta, - ) - .into_affine(); - PrivateKeyShare { private_key_share } +/// An updated private key share, resulting from an intermediate step in a share recovery or refresh operation. +#[derive(Debug, Clone, PartialEq, Eq, ZeroizeOnDrop)] +pub struct UpdatedPrivateKeyShare(InnerPrivateKeyShare); + +impl UpdatedPrivateKeyShare { + /// One-way conversion from `UpdatedPrivateKeyShare` to `PrivateKeyShare`. + /// Use this method to eject from the `UpdatedPrivateKeyShare` type and use the resulting `PrivateKeyShare` in further operations. + pub fn inner(&self) -> PrivateKeyShare { + PrivateKeyShare(self.0.clone()) + } } -/// From the PSS paper, section 4.2.4, (https://link.springer.com/content/pdf/10.1007/3-540-44750-4_27.pdf) -pub fn recover_share_from_updated_private_shares( - x_r: &E::ScalarField, - domain_points: &[E::ScalarField], - updated_private_shares: &[PrivateKeyShare], -) -> PrivateKeyShare { - // Interpolate new shares to recover y_r - let lagrange = lagrange_basis_at::(domain_points, x_r); - let prods = zip_eq(updated_private_shares, lagrange) - .map(|(y_j, l)| y_j.private_key_share.mul(l)); - let y_r = prods.fold(E::G2::zero(), |acc, y_j| acc + y_j); - - PrivateKeyShare { - private_key_share: y_r.into_affine(), +impl UpdatedPrivateKeyShare { + pub fn new(private_key_share: InnerPrivateKeyShare) -> Self { + Self(private_key_share) } } -// SHARE REFRESH FUNCTIONS: +// TODO: Replace with an into trait? +/// Trait for types that can be used to update a private key share. +pub trait PrivateKeyShareUpdate { + fn inner(&self) -> &InnerPrivateKeyShare; // TODO: Should we return g2affine instead? +} + +/// An update to a private key share generated by a participant in a share recovery operation. +#[derive(Debug, Clone, PartialEq, Eq, ZeroizeOnDrop)] +pub struct ShareRecoveryUpdate(InnerPrivateKeyShare); -pub fn prepare_share_updates_for_refresh( - domain_points: &[E::ScalarField], - h: &E::G2Affine, - threshold: usize, - rng: &mut impl RngCore, -) -> Vec { - // Update polynomial has root at 0 - prepare_share_updates_with_root::( - domain_points, - h, - &E::ScalarField::zero(), - threshold, - rng, - ) +impl PrivateKeyShareUpdate for ShareRecoveryUpdate { + fn inner(&self) -> &InnerPrivateKeyShare { + &self.0 + } +} + +impl ShareRecoveryUpdate { + /// From PSS paper, section 4.2.1, (https://link.springer.com/content/pdf/10.1007/3-540-44750-4_27.pdf) + pub fn make_share_updates_for_recovery( + domain_points: &[E::ScalarField], + h: &E::G2Affine, + x_r: &E::ScalarField, + threshold: usize, + rng: &mut impl RngCore, + ) -> Vec> { + // Update polynomial has root at x_r + prepare_share_updates_with_root::( + domain_points, + h, + x_r, + threshold, + rng, + ) + .iter() + .map(|p| Self(p.clone())) + .collect() + } } -// UTILS: +/// An update to a private key share generated by a participant in a share refresh operation. +#[derive(Debug, Clone, PartialEq, Eq, ZeroizeOnDrop)] +pub struct ShareRefreshUpdate(InnerPrivateKeyShare); +impl PrivateKeyShareUpdate for ShareRefreshUpdate { + fn inner(&self) -> &InnerPrivateKeyShare { + &self.0 + } +} + +impl ShareRefreshUpdate { + /// From PSS paper, section 4.2.1, (https://link.springer.com/content/pdf/10.1007/3-540-44750-4_27.pdf) + pub fn make_share_updates_for_refresh( + domain_points: &[E::ScalarField], + h: &E::G2Affine, + threshold: usize, + rng: &mut impl RngCore, + ) -> Vec> { + // Update polynomial has root at 0 + prepare_share_updates_with_root::( + domain_points, + h, + &E::ScalarField::zero(), + threshold, + rng, + ) + .iter() + .cloned() + .map(|p| ShareRefreshUpdate(p)) + .collect() + } +} + +/// Prepare share updates with a given root +/// This is a helper function for `ShareRecoveryUpdate::make_share_updates_for_recovery` and `ShareRefreshUpdate::make_share_updates_for_refresh` +/// It generates a new random polynomial with a defined root and evaluates it at each of the participants' indices. +/// The result is a list of share updates. +/// We represent the share updates as `InnerPrivateKeyShare` to avoid dependency on the concrete implementation of `PrivateKeyShareUpdate`. fn prepare_share_updates_with_root( domain_points: &[E::ScalarField], h: &E::G2Affine, root: &E::ScalarField, threshold: usize, rng: &mut impl RngCore, -) -> Vec { +) -> Vec> { // Generate a new random polynomial with defined root let d_i = make_random_polynomial_with_root::(threshold - 1, root, rng); @@ -89,12 +158,16 @@ fn prepare_share_updates_with_root( .iter() .map(|x_i| { let eval = d_i.evaluate(x_i); - h.mul(eval) + h.mul(eval).into_affine() + }) + .map(|p| InnerPrivateKeyShare { + private_key_share: p, }) .collect() } -pub fn make_random_polynomial_with_root( +/// Generate a random polynomial with a given root +fn make_random_polynomial_with_root( degree: usize, root: &E::ScalarField, rng: &mut impl RngCore, @@ -124,23 +197,21 @@ mod tests_refresh { use ark_std::{test_rng, UniformRand, Zero}; use ferveo_tdec::{ test_common::setup_simple, PrivateDecryptionContextSimple, - PrivateKeyShare, }; use rand_core::RngCore; use test_case::test_matrix; use crate::{ - apply_updates_to_private_share, prepare_share_updates_for_recovery, - prepare_share_updates_for_refresh, - recover_share_from_updated_private_shares, test_common::*, + test_common::*, PrivateKeyShare, ShareRecoveryUpdate, + ShareRefreshUpdate, UpdatedPrivateKeyShare, }; - fn make_new_share_fragments_for_recovery( + fn make_updated_private_key_shares( rng: &mut R, threshold: usize, x_r: &Fr, remaining_participants: &[PrivateDecryptionContextSimple], - ) -> Vec> { + ) -> Vec> { // Each participant prepares an update for each other participant // TODO: Extract as parameter let domain_points = remaining_participants[0] @@ -152,36 +223,36 @@ mod tests_refresh { let share_updates = remaining_participants .iter() .map(|p| { - let deltas_i = prepare_share_updates_for_recovery::( - &domain_points, - &h, - x_r, - threshold, - rng, - ); + let deltas_i = + ShareRecoveryUpdate::make_share_updates_for_recovery( + &domain_points, + &h, + x_r, + threshold, + rng, + ); (p.index, deltas_i) }) .collect::>(); // Participants share updates and update their shares - let new_share_fragments: Vec<_> = remaining_participants + let updated_private_key_shares: Vec<_> = remaining_participants .iter() .map(|p| { // Current participant receives updates from other participants let updates_for_participant: Vec<_> = share_updates .values() - .map(|updates| *updates.get(p.index).unwrap()) + .map(|updates| updates.get(p.index).cloned().unwrap()) .collect(); // And updates their share - apply_updates_to_private_share::( - &p.private_key_share, - &updates_for_participant, - ) + // TODO: Remove wrapping after migrating to dkg based tests + PrivateKeyShare(p.private_key_share.clone()) + .make_updated_key_share(&updates_for_participant) }) .collect(); - new_share_fragments + updated_private_key_shares } /// Ñ parties (where t <= Ñ <= N) jointly execute a "share recovery" algorithm, and the output is 1 new share. @@ -205,14 +276,14 @@ mod tests_refresh { .domain; let original_private_key_share = selected_participant.private_key_share; - // Remove one participant from the contexts and all nested structures + // Remove the selected participant from the contexts and all nested structures let mut remaining_participants = contexts; for p in &mut remaining_participants { p.public_decryption_contexts.pop().unwrap(); } // Each participant prepares an update for each other participant, and uses it to create a new share fragment - let new_share_fragments = make_new_share_fragments_for_recovery( + let updated_private_key_shares = make_updated_private_key_shares( rng, security_threshold, &x_r, @@ -225,21 +296,22 @@ mod tests_refresh { .iter() .map(|ctxt| ctxt.domain) .collect::>(); - let new_private_key_share = recover_share_from_updated_private_shares( - &x_r, - &domain_points[..security_threshold], - &new_share_fragments[..security_threshold], - ); + let new_private_key_share = + PrivateKeyShare::recover_share_from_updated_private_shares( + &x_r, + &domain_points[..security_threshold], + &updated_private_key_shares[..security_threshold], + ); assert_eq!(new_private_key_share, original_private_key_share); // If we don't have enough private share updates, the resulting private share will be incorrect - assert_eq!(domain_points.len(), new_share_fragments.len()); + assert_eq!(domain_points.len(), updated_private_key_shares.len()); let incorrect_private_key_share = - recover_share_from_updated_private_shares( + PrivateKeyShare::recover_share_from_updated_private_shares( &x_r, &domain_points[..(security_threshold - 1)], - &new_share_fragments[..(security_threshold - 1)], + &updated_private_key_shares[..(security_threshold - 1)], ); assert_ne!(incorrect_private_key_share, original_private_key_share); @@ -270,7 +342,7 @@ mod tests_refresh { let x_r = ScalarField::rand(rng); // Each participant prepares an update for each other participant, and uses it to create a new share fragment - let new_share_fragments = make_new_share_fragments_for_recovery( + let share_recovery_fragmetns = make_updated_private_key_shares( rng, threshold, &x_r, @@ -283,11 +355,12 @@ mod tests_refresh { .iter() .map(|ctxt| ctxt.domain) .collect::>(); - let new_private_key_share = recover_share_from_updated_private_shares( - &x_r, - &domain_points[..threshold], - &new_share_fragments[..threshold], - ); + let recovered_private_key_share = + PrivateKeyShare::recover_share_from_updated_private_shares( + &x_r, + &domain_points[..threshold], + &share_recovery_fragmetns[..threshold], + ); let mut private_shares = contexts .iter() @@ -297,18 +370,23 @@ mod tests_refresh { // Finally, let's recreate the shared private key from some original shares and the recovered one domain_points.push(x_r); - private_shares.push(new_private_key_share); + private_shares.push(recovered_private_key_share); + // This is a workaround for a type mismatch - We need to convert the private shares to updated private shares + // This is just to test that we are able to recover the shared private key from the updated private shares + let updated_private_key_shares = private_shares + .iter() + .cloned() + .map(UpdatedPrivateKeyShare::new) + .collect::>(); let start_from = shares_num - threshold; - let new_shared_private_key = recover_share_from_updated_private_shares( - &ScalarField::zero(), - &domain_points[start_from..], - &private_shares[start_from..], - ); + let new_shared_private_key = + PrivateKeyShare::recover_share_from_updated_private_shares( + &ScalarField::zero(), + &domain_points[start_from..], + &updated_private_key_shares[start_from..], + ); - assert_eq!( - shared_private_key, - new_shared_private_key.private_key_share - ); + assert_eq!(shared_private_key, new_shared_private_key); } /// Ñ parties (where t <= Ñ <= N) jointly execute a "share refresh" algorithm. @@ -319,7 +397,7 @@ mod tests_refresh { let rng = &mut test_rng(); let threshold = shares_num * 2 / 3; - let (_, shared_private_key, contexts) = + let (_, private_key_share, contexts) = setup_simple::(threshold, shares_num, rng); let domain_points = &contexts[0] @@ -333,12 +411,13 @@ mod tests_refresh { let share_updates = contexts .iter() .map(|p| { - let deltas_i = prepare_share_updates_for_refresh::( - domain_points, - &h, - threshold, - rng, - ); + let deltas_i = + ShareRefreshUpdate::::make_share_updates_for_refresh( + domain_points, + &h, + threshold, + rng, + ); (p.index, deltas_i) }) .collect::>(); @@ -350,27 +429,24 @@ mod tests_refresh { // Current participant receives updates from other participants let updates_for_participant: Vec<_> = share_updates .values() - .map(|updates| *updates.get(p.index).unwrap()) + .map(|updates| updates.get(p.index).cloned().unwrap()) .collect(); - // And updates their share - apply_updates_to_private_share::( - &p.private_key_share, - &updates_for_participant, - ) + // And creates a new, refreshed share + // TODO: Remove wrapping after migrating to dkg based tests + PrivateKeyShare(p.private_key_share.clone()) + .make_updated_key_share(&updates_for_participant) }) .collect(); // Finally, let's recreate the shared private key from the refreshed shares - let new_shared_private_key = recover_share_from_updated_private_shares( - &ScalarField::zero(), - &domain_points[..threshold], - &refreshed_shares[..threshold], - ); + let new_shared_private_key = + PrivateKeyShare::recover_share_from_updated_private_shares( + &ScalarField::zero(), + &domain_points[..threshold], + &refreshed_shares[..threshold], + ); - assert_eq!( - shared_private_key, - new_shared_private_key.private_key_share - ); + assert_eq!(private_key_share, new_shared_private_key); } }