From b79afccf7e98ee916fb3651d78d74df6ba9a34f7 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"
+ );
+ }
}