From f03d5e2d487555d4e912fd5118b3cff4d0464a85 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Mon, 12 Feb 2024 15:17:32 +0100 Subject: [PATCH] feature: introduce refreshing api in ferveo --- ferveo/src/api.rs | 358 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) diff --git a/ferveo/src/api.rs b/ferveo/src/api.rs index dd8e40bd..396c8aff 100644 --- a/ferveo/src/api.rs +++ b/ferveo/src/api.rs @@ -1395,4 +1395,362 @@ mod test_ferveo_api { "Shared secret reconstruction failed" ); } + + fn make_share_update_test_inputs( + shares_num: u32, + validators_num: u32, + rng: &mut StdRng, + security_threshold: u32, + ) -> ( + Vec, + Vec, + Vec, + Vec, + CiphertextHeader, + SharedSecret, + ) { + let (messages, validators, validator_keypairs) = make_test_inputs( + rng, + TAU, + security_threshold, + shares_num, + validators_num, + ); + let mut dkgs = validators + .iter() + .map(|validator| { + Dkg::new( + TAU, + shares_num, + security_threshold, + &validators, + validator, + ) + .unwrap() + }) + .collect::>(); + let pvss_aggregated = dkgs[0].aggregate_transcripts(&messages).unwrap(); + assert!(pvss_aggregated.verify(validators_num, &messages).unwrap()); + + // Create an initial shared secret for testing purposes + let public_key = &dkgs[0].public_key(); + let ciphertext = + encrypt(SecretBox::new(MSG.to_vec()), AAD, public_key).unwrap(); + let ciphertext_header = ciphertext.header().unwrap(); + let (_, _, old_shared_secret) = + crate::test_dkg_full::create_shared_secret_simple_tdec( + &dkgs[0].0, + AAD, + &ciphertext_header.0, + validator_keypairs.as_slice(), + ); + + ( + messages, + validators, + validator_keypairs, + dkgs, + ciphertext_header, + SharedSecret(old_shared_secret), + ) + } + + #[test_case(4, 4, true; "number of shares (validators) is a power of 2")] + #[test_case(7, 7, true; "number of shares (validators) is not a power of 2")] + #[test_case(4, 6, true; "number of validators greater than the number of shares")] + #[test_case(4, 6, false; "recovery at a specific point")] + fn test_dkg_simple_tdec_share_recovery( + shares_num: u32, + validators_num: u32, + recover_at_random_point: bool, + ) { + let rng = &mut StdRng::seed_from_u64(0); + let security_threshold = shares_num / 2 + 1; + + let ( + mut messages, + mut validators, + mut validator_keypairs, + mut dkgs, + ciphertext_header, + old_shared_secret, + ) = make_share_update_test_inputs( + shares_num, + validators_num, + rng, + security_threshold, + ); + + // We assume that all participants have the same aggregate, and that participants created + // their own aggregates before the off-boarding of the validator + // If we didn't create this aggregate here, we risk having a "dangling validator message" + // later when we off-board the validator + let aggregated_transcript = + dkgs[0].clone().aggregate_transcripts(&messages).unwrap(); + assert!(aggregated_transcript + .verify(validators_num, &messages) + .unwrap()); + + // We need to save this domain point to be user in the recovery testing scenario + let mut domain_points = dkgs[0].domain_points(); + let removed_domain_point = domain_points.pop().unwrap(); + + // Remove one participant from the contexts and all nested structure + // to simulate off-boarding a validator + messages.pop().unwrap(); + dkgs.pop(); + validator_keypairs.pop().unwrap(); + + let removed_validator = validators.pop().unwrap(); + for dkg in dkgs.iter_mut() { + dkg.0 + .offboard_validator(&removed_validator.address) + .expect("Unable to off-board a validator from the DKG context"); + } + + // Now, we're going to recover a new share at a random point or at a specific point + // and check that the shared secret is still the same. + let x_r = if recover_at_random_point { + // Onboarding a validator with a completely new private key share + DomainPoint::::rand(rng) + } else { + // Onboarding a validator with a private key share recovered from the removed validator + removed_domain_point + }; + + // Each participant prepares an update for each other participant + let share_updates = dkgs + .iter() + .map(|validator_dkg| { + let share_update = ShareRecoveryUpdate::create_share_updates( + validator_dkg, + &x_r, + ) + .unwrap(); + (validator_dkg.me().address.clone(), share_update) + }) + .collect::>(); + + // Participants share updates and update their shares + + // Now, every participant separately: + let updated_shares: Vec<_> = dkgs + .iter() + .map(|validator_dkg| { + // Current participant receives updates from other participants + let updates_for_participant: Vec<_> = share_updates + .values() + .map(|updates| { + updates + .get(validator_dkg.me().share_index as usize) + .unwrap() + }) + .cloned() + .collect(); + + // Each validator uses their decryption key to update their share + let validator_keypair = validator_keypairs + .get(validator_dkg.me().share_index as usize) + .unwrap(); + + // And creates updated private key shares + aggregated_transcript + .get_private_key_share( + validator_keypair, + validator_dkg.me().share_index, + ) + .unwrap() + .create_updated_private_key_share_for_recovery( + &updates_for_participant, + ) + .unwrap() + }) + .collect(); + + // Now, we have to combine new share fragments into a new share + let recovered_key_share = + PrivateKeyShare::recover_share_from_updated_private_shares( + &x_r, + &domain_points, + &updated_shares, + ) + .unwrap(); + + // Get decryption shares from remaining participants + let mut decryption_shares: Vec = + validator_keypairs + .iter() + .zip_eq(dkgs.iter()) + .map(|(validator_keypair, validator_dkg)| { + aggregated_transcript + .create_decryption_share_simple( + validator_dkg, + &ciphertext_header, + AAD, + validator_keypair, + ) + .unwrap() + }) + .collect(); + decryption_shares.shuffle(rng); + + // In order to test the recovery, we need to create a new decryption share from the recovered + // private key share. To do that, we need a new validator + + // Let's create and onboard a new validator + // TODO: Add test scenarios for onboarding and offboarding validators + let new_validator_keypair = Keypair::random(); + // Normally, we would get these from the Coordinator: + let new_validator_share_index = removed_validator.share_index; + let new_validator = Validator { + address: gen_address(new_validator_share_index as usize), + public_key: new_validator_keypair.public_key(), + share_index: new_validator_share_index, + }; + validators.push(new_validator.clone()); + let new_validator_dkg = Dkg::new( + TAU, + shares_num, + security_threshold, + &validators, + &new_validator, + ) + .unwrap(); + + let new_decryption_share = recovered_key_share + .create_decryption_share_simple( + &new_validator_dkg, + &ciphertext_header, + &new_validator_keypair, + AAD, + ) + .unwrap(); + decryption_shares.push(new_decryption_share); + domain_points.push(x_r); + assert_eq!(domain_points.len(), validators_num as usize); + assert_eq!(decryption_shares.len(), validators_num as usize); + + let domain_points = &domain_points[..security_threshold as usize]; + let decryption_shares = + &decryption_shares[..security_threshold as usize]; + assert_eq!(domain_points.len(), security_threshold as usize); + assert_eq!(decryption_shares.len(), security_threshold as usize); + + let new_shared_secret = combine_shares_simple(decryption_shares); + assert_eq!( + old_shared_secret, new_shared_secret, + "Shared secret reconstruction failed" + ); + } + + #[test_case(4, 4; "number of shares (validators) is a power of 2")] + #[test_case(7, 7; "number of shares (validators) is not a power of 2")] + #[test_case(4, 6; "number of validators greater than the number of shares")] + fn test_dkg_simple_tdec_share_refresh( + shares_num: u32, + validators_num: u32, + ) { + let rng = &mut StdRng::seed_from_u64(0); + let security_threshold = shares_num / 2 + 1; + + let ( + messages, + _validators, + validator_keypairs, + dkgs, + ciphertext_header, + old_shared_secret, + ) = make_share_update_test_inputs( + shares_num, + validators_num, + rng, + security_threshold, + ); + + // Each participant prepares an update for each other participant + let share_updates = dkgs + .iter() + .map(|validator_dkg| { + let share_update = + ShareRefreshUpdate::create_share_updates(validator_dkg) + .unwrap(); + (validator_dkg.me().address.clone(), share_update) + }) + .collect::>(); + + // Participants share updates and update their shares + + // Now, every participant separately: + let updated_shares: Vec<_> = dkgs + .iter() + .map(|validator_dkg| { + // Current participant receives updates from other participants + let updates_for_participant: Vec<_> = share_updates + .values() + .map(|updates| { + updates + .get(validator_dkg.me().share_index as usize) + .unwrap() + }) + .cloned() + .collect(); + + // Each validator uses their decryption key to update their share + let validator_keypair = validator_keypairs + .get(validator_dkg.me().share_index as usize) + .unwrap(); + + // And creates updated private key shares + // We need an aggregate for that + let aggregate = validator_dkg + .clone() + .aggregate_transcripts(&messages) + .unwrap(); + assert!(aggregate.verify(validators_num, &messages).unwrap()); + + aggregate + .get_private_key_share( + validator_keypair, + validator_dkg.me().share_index, + ) + .unwrap() + .create_updated_private_key_share_for_refresh( + &updates_for_participant, + ) + .unwrap() + }) + .collect(); + + // Participants create decryption shares + let mut decryption_shares: Vec = + validator_keypairs + .iter() + .zip_eq(dkgs.iter()) + .map(|(validator_keypair, validator_dkg)| { + let pks = updated_shares + .get(validator_dkg.me().share_index as usize) + .unwrap() + .clone() + .into_private_key_share(); + pks.create_decryption_share_simple( + validator_dkg, + &ciphertext_header, + validator_keypair, + AAD, + ) + .unwrap() + }) + .collect(); + decryption_shares.shuffle(rng); + + let decryption_shares = + &decryption_shares[..security_threshold as usize]; + assert_eq!(decryption_shares.len(), security_threshold as usize); + + let new_shared_secret = combine_shares_simple(decryption_shares); + assert_eq!( + old_shared_secret, new_shared_secret, + "Shared secret reconstruction failed" + ); + } }