diff --git a/src/protocol/context/mod.rs b/src/protocol/context/mod.rs index 5671eea06..4c1ab8c2f 100644 --- a/src/protocol/context/mod.rs +++ b/src/protocol/context/mod.rs @@ -14,11 +14,14 @@ mod semi_honest; pub use malicious::MaliciousContext; pub use semi_honest::SemiHonestContext; +use super::sort::reshare::Reshare; + /// Context used by each helper to perform secure computation. Provides access to shared randomness /// generator and communication channel. pub trait Context: Clone + SecureMul>::Share> + + Reshare>::Share> + Reveal>::Share> { /// Secret sharing type this context supports. diff --git a/src/protocol/sort/bit_permutation.rs b/src/protocol/sort/bit_permutation.rs index 4c7b1fb2f..ae35ef105 100644 --- a/src/protocol/sort/bit_permutation.rs +++ b/src/protocol/sort/bit_permutation.rs @@ -83,7 +83,7 @@ mod tests { }; #[tokio::test] - pub async fn test_bit_permutation() { + pub async fn semi_honest() { // With this input, for stable sort we expect all 0's to line up before 1's. // The expected sort order is same as expected_sort_output. const INPUT: &[u128] = &[1, 0, 1, 0, 0, 1, 0]; @@ -119,7 +119,7 @@ mod tests { } #[tokio::test] - pub async fn test_bit_permutation_malicious() { + pub async fn malicious() { // With this input, for stable sort we expect all 0's to line up before 1's. // The expected sort order is same as expected_sort_output. const INPUT: &[u128] = &[1, 0, 1, 0, 0, 1, 0]; @@ -157,6 +157,6 @@ mod tests { .try_into() .unwrap(); - validate_list_of_shares_malicious(EXPECTED, &result); + validate_list_of_shares_malicious(r, EXPECTED, &result); } } diff --git a/src/protocol/sort/mod.rs b/src/protocol/sort/mod.rs index 244e282f0..b64680ea8 100644 --- a/src/protocol/sort/mod.rs +++ b/src/protocol/sort/mod.rs @@ -97,3 +97,17 @@ impl AsRef for ShuffleRevealStep { } } } + +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub enum ReshareStep { + ReshareMAC, +} +impl Substep for ReshareStep {} + +impl AsRef for ReshareStep { + fn as_ref(&self) -> &str { + match self { + Self::ReshareMAC => "reshare_mac", + } + } +} diff --git a/src/protocol/sort/reshare.rs b/src/protocol/sort/reshare.rs index 484fdd5dd..28d96ed53 100644 --- a/src/protocol/sort/reshare.rs +++ b/src/protocol/sort/reshare.rs @@ -1,54 +1,63 @@ use crate::ff::Field; -use crate::protocol::context::SemiHonestContext; +use crate::protocol::context::{Context, MaliciousContext}; +use crate::secret_sharing::{MaliciousReplicated, SecretSharing}; use crate::{ error::Error, helpers::{Direction, Role}, - protocol::{context::Context, RecordId}, + protocol::{context::SemiHonestContext, sort::ReshareStep::ReshareMAC, RecordId}, secret_sharing::Replicated, }; +use async_trait::async_trait; use embed_doc_image::embed_doc_image; +use futures::future::try_join; + +/// Trait for reshare protocol to renew shares of a secret value for all 3 helpers. +#[async_trait] +pub trait Reshare { + type Share: SecretSharing; + + async fn reshare( + self, + input: &Self::Share, + record: RecordId, + to_helper: Role, + ) -> Result; +} /// Reshare(i, \[x\]) -// This implements reshare algorithm of "Efficient Secure Three-Party Sorting Protocol with an Honest Majority" at communication cost of 2R. +// This implements semi-honest reshare algorithm of "Efficient Secure Three-Party Sorting Protocol with an Honest Majority" at communication cost of 2R. // Input: Pi-1 and Pi+1 know their secret shares // Output: At the end of the protocol, all 3 helpers receive their shares of a new, random secret sharing of the secret value -#[derive(Debug)] -pub struct Reshare { - input: Replicated, -} - -impl Reshare { - pub fn new(input: Replicated) -> Self { - Self { input } - } - - #[embed_doc_image("reshare", "images/sort/reshare.png")] - /// Steps - /// ![Reshare steps][reshare] - /// 1. While calculating for a helper, we call pseudo random secret sharing (prss) to get random values which match - /// with those generated by other helpers (say `rand_left`, `rand_right`) - /// `to_helper.left` knows `rand_left` (named r1) and `to_helper.right` knows `rand_right` (named r0) - /// 2. `to_helper.left` calculates part1 = (a1 + a2) - r2 = Same as (input.left() + input.right()) - r1 from helper POV - /// `to_helper.right` calculates part2 = (a3 - r3) = Same as (input.left() - r0) from helper POV - /// 3. `to_helper.left` and `to_helper.right` exchange their calculated shares - /// 4. Everyone sets their shares - /// `to_helper.left` = (part1 + part2, `rand_left`) = (part1 + part2, r1) - /// `to_helper` = (`rand_left`, `rand_right`) = (r0, r1) - /// `to_helper.right` = (`rand_right`, part1 + part2) = (r0, part1 + part2) - pub async fn execute( +#[embed_doc_image("reshare", "images/sort/reshare.png")] +/// Steps +/// ![Reshare steps][reshare] +/// 1. While calculating for a helper, we call pseudo random secret sharing (prss) to get random values which match +/// with those generated by other helpers (say `rand_left`, `rand_right`) +/// `to_helper.left` knows `rand_left` (named r1) and `to_helper.right` knows `rand_right` (named r0) +/// 2. `to_helper.left` calculates part1 = (a1 + a2) - r2 = Same as (input.left() + input.right()) - r1 from helper POV +/// `to_helper.right` calculates part2 = (a3 - r3) = Same as (input.left() - r0) from helper POV +/// 3. `to_helper.left` and `to_helper.right` exchange their calculated shares +/// 4. Everyone sets their shares +/// `to_helper.left` = (part1 + part2, `rand_left`) = (part1 + part2, r1) +/// `to_helper` = (`rand_left`, `rand_right`) = (r0, r1) +/// `to_helper.right` = (`rand_right`, part1 + part2) = (r0, part1 + part2) +#[async_trait] +impl Reshare for SemiHonestContext<'_, F> { + type Share = Replicated; + async fn reshare( self, - ctx: &SemiHonestContext<'_, F>, + input: &Self::Share, record_id: RecordId, to_helper: Role, - ) -> Result, Error> { - let channel = ctx.mesh(); - let prss = ctx.prss(); + ) -> Result { + let channel = self.mesh(); + let prss = self.prss(); let (r0, r1) = prss.generate_fields(record_id); // `to_helper.left` calculates part1 = (input.0 + input.1) - r1 and sends part1 to `to_helper.right` // This is same as (a1 + a2) - r2 in the diagram - if ctx.role() == to_helper.peer(Direction::Left) { - let part1 = self.input.left() + self.input.right() - r1; + if self.role() == to_helper.peer(Direction::Left) { + let part1 = input.left() + input.right() - r1; channel .send(to_helper.peer(Direction::Right), record_id, part1) .await?; @@ -59,10 +68,10 @@ impl Reshare { .await?; Ok(Replicated::new(part1 + part2, r1)) - } else if ctx.role() == to_helper.peer(Direction::Right) { + } else if self.role() == to_helper.peer(Direction::Right) { // `to_helper.right` calculates part2 = (input.left() - r0) and sends it to `to_helper.left` // This is same as (a3 - r3) in the diagram - let part2 = self.input.left() - r0; + let part2 = input.left() - r0; channel .send(to_helper.peer(Direction::Left), record_id, part2) .await?; @@ -79,79 +88,147 @@ impl Reshare { } } +/// For malicious reshare, we run semi honest reshare protocol twice, once for x and another for rx and return the results +/// # Errors +/// If either of reshares fails +#[async_trait] +impl Reshare for MaliciousContext<'_, F> { + type Share = MaliciousReplicated; + async fn reshare( + self, + input: &Self::Share, + record_id: RecordId, + to_helper: Role, + ) -> Result { + let rx_ctx = self.narrow(&ReshareMAC); + let (x, rx) = try_join( + self.to_semi_honest() + .reshare(input.x(), record_id, to_helper), + rx_ctx + .to_semi_honest() + .reshare(input.rx(), record_id, to_helper), + ) + .await?; + Ok(MaliciousReplicated::new(x, rx)) + } +} + #[cfg(test)] mod tests { + mod semi_honest { + use proptest::prelude::Rng; + + use rand::thread_rng; + + use crate::ff::Fp32BitPrime; + use crate::protocol::context::Context; + use crate::{ + helpers::Role, + protocol::{sort::reshare::Reshare, QueryId, RecordId}, + test_fixture::{make_world, validate_and_reconstruct}, + }; + + use crate::test_fixture::Runner; + + /// Validates that reshare protocol actually generates new shares using PRSS. + #[tokio::test] + async fn generates_unique_shares() { + let world = make_world(QueryId); + + for &target in Role::all() { + let secret = thread_rng().gen::(); + let shares = world + .semi_honest(secret, |ctx, share| async move { + let record_id = RecordId::from(0); + + // run reshare protocol for all helpers except the one that does not know the input + if ctx.role() == target { + // test follows the reshare protocol + ctx.prss().generate_fields(record_id).into() + } else { + ctx.reshare(&share, record_id, target).await.unwrap() + } + }) + .await; + + let reshared_secret = validate_and_reconstruct(&shares[0], &shares[1], &shares[2]); + + // if reshare cheated and just returned its input without adding randomness, + // this test will catch it with the probability of error (1/|F|)^2. + // Using 32 bit field is sufficient to consider error probability negligible + assert_eq!(secret, reshared_secret); + } + } - use proptest::prelude::Rng; - - use rand::thread_rng; - - use crate::ff::Fp32BitPrime; - use crate::protocol::context::Context; - use crate::{ - helpers::Role, - protocol::{sort::reshare::Reshare, QueryId, RecordId}, - test_fixture::{make_world, validate_and_reconstruct}, - }; - - use crate::test_fixture::Runner; - - /// Validates that reshare protocol actually generates new shares using PRSS. - #[tokio::test] - async fn generates_unique_shares() { - let world = make_world(QueryId); - - for &target in Role::all() { - let secret = thread_rng().gen::(); - let shares = world - .semi_honest(secret, |ctx, share| async move { - let record_id = RecordId::from(0); - - // run reshare protocol for all helpers except the one that does not know the input - if ctx.role() == target { - // test follows the reshare protocol - ctx.prss().generate_fields(record_id).into() - } else { - Reshare::new(share.clone()) - .execute(&ctx, record_id, target) - .await - .unwrap() - } - }) - .await; - - let reshared_secret = validate_and_reconstruct(&shares[0], &shares[1], &shares[2]); - - // if reshare cheated and just returned its input without adding randomness, - // this test will catch it with the probability of error (1/|F|)^2. - // Using 32 bit field is sufficient to consider error probability negligible - assert_eq!(secret, reshared_secret); + /// This test validates the correctness of the protocol, relying on `generates_unique_shares` + /// to ensure security. It does not verify that helpers actually attempt to generate new shares + /// so a naive implementation of reshare that just output shares `[O]` = `[I]` where `[I]` is + /// the input will pass this test. However `generates_unique_shares` will fail this implementation. + #[tokio::test] + async fn correct() { + let world = make_world(QueryId); + + for &role in Role::all() { + let secret = thread_rng().gen::(); + let new_shares = world + .semi_honest(secret, |ctx, share| async move { + ctx.reshare(&share, RecordId::from(0), role).await.unwrap() + }) + .await; + + assert_eq!( + secret, + validate_and_reconstruct(&new_shares[0], &new_shares[1], &new_shares[2]) + ); + } } } - /// This test validates the correctness of the protocol, relying on `generates_unique_shares` - /// to ensure security. It does not verify that helpers actually attempt to generate new shares - /// so a naive implementation of reshare that just output shares `[O]` = `[I]` where `[I]` is - /// the input will pass this test. However `generates_unique_shares` will fail this implementation. - #[tokio::test] - async fn correct() { - let world = make_world(QueryId); - - for role in Role::all() { - let secret = thread_rng().gen::(); - let new_shares = world - .semi_honest(secret, |ctx, share| async move { - Reshare::new(share) - .execute(&ctx, RecordId::from(0), *role) - .await - .unwrap() - }) + mod malicious { + use crate::ff::Fp32BitPrime; + use crate::helpers::Role; + use crate::protocol::sort::reshare::Reshare; + use crate::protocol::{QueryId, RecordId}; + use crate::test_fixture::{ + join3, make_malicious_contexts, make_world, share_malicious, + validate_and_reconstruct_malicious, + }; + use rand::rngs::mock::StepRng; + use rand::Rng; + + /// Relies on semi-honest protocol tests that enforce reshare to communicate and produce + /// new shares. + /// TODO: It would be great to have a test to validate that helpers cannot cheat. In this + /// setting we have 1 helper that does not know the input and if another one is malicious + /// adversary, we are only left with one honest helper that knows the input and can validate + /// it. + #[tokio::test] + pub async fn correct() { + let mut rand = StepRng::new(100, 1); + let mut rng = rand::thread_rng(); + let world = make_world(QueryId); + + for &role in Role::all() { + let r = rng.gen::(); + let secret = rng.gen::(); + + let [ctx0, ctx1, ctx2] = make_malicious_contexts::(&world); + let shares = share_malicious(secret, r, &mut rand); + let record_id = RecordId::from(0); + + let f = join3( + ctx0.ctx.reshare(&shares[0], record_id, role), + ctx1.ctx.reshare(&shares[1], record_id, role), + ctx2.ctx.reshare(&shares[2], record_id, role), + ) .await; - assert_eq!( - secret, - validate_and_reconstruct(&new_shares[0], &new_shares[1], &new_shares[2]) - ); + let (new_secret, new_secret_times_r) = + validate_and_reconstruct_malicious(&f[0], &f[1], &f[2]); + + assert_eq!(secret, new_secret); + assert_eq!(secret * r, new_secret_times_r); + } } } } diff --git a/src/protocol/sort/shuffle.rs b/src/protocol/sort/shuffle.rs index de323f399..701548b2d 100644 --- a/src/protocol/sort/shuffle.rs +++ b/src/protocol/sort/shuffle.rs @@ -1,3 +1,5 @@ +use std::iter::{repeat, zip}; + use embed_doc_image::embed_doc_image; use futures::future::try_join_all; use rand::seq::SliceRandom; @@ -5,6 +7,7 @@ use rand::SeedableRng; use rand_chacha::ChaCha8Rng; use crate::protocol::context::SemiHonestContext; +use crate::secret_sharing::SecretSharing; use crate::{ error::Error, ff::Field, @@ -15,7 +18,6 @@ use crate::{ use super::{ apply::{apply, apply_inv}, - reshare::Reshare, ShuffleStep::{self, Step1, Step2, Step3}, }; @@ -87,19 +89,15 @@ fn shuffle_for_helper(which_step: ShuffleStep) -> Role { } #[allow(clippy::cast_possible_truncation)] -async fn reshare_all_shares( - input: Vec>, - ctx: &SemiHonestContext<'_, F>, +async fn reshare_all_shares, C: Context>( + input: &[S], + ctx: C, to_helper: Role, -) -> Result>, Error> { - let reshares = input - .iter() - .cloned() +) -> Result, Error> { + let reshares = zip(repeat(ctx), input) .enumerate() - .map(|(index, input)| async move { - Reshare::new(input) - .execute(ctx, RecordId::from(index), to_helper) - .await + .map(|(index, (ctx, input))| async move { + ctx.reshare(input, RecordId::from(index), to_helper).await }); try_join_all(reshares).await } @@ -131,7 +129,7 @@ async fn shuffle_or_unshuffle_once( ShuffleOrUnshuffle::Unshuffle => apply(permutation_to_apply, &mut input), } } - reshare_all_shares(input, &ctx, to_helper).await + reshare_all_shares(&input, ctx, to_helper).await } #[embed_doc_image("shuffle", "images/sort/shuffle.png")] diff --git a/src/test_fixture/mod.rs b/src/test_fixture/mod.rs index 35d9ee23c..d4510bb1e 100644 --- a/src/test_fixture/mod.rs +++ b/src/test_fixture/mod.rs @@ -22,8 +22,8 @@ use std::fmt::Debug; use std::sync::atomic::Ordering; pub use sharing::{ - share, share_malicious, shared_bits, validate_and_reconstruct, validate_list_of_shares, - validate_list_of_shares_malicious, + share, share_malicious, shared_bits, validate_and_reconstruct, + validate_and_reconstruct_malicious, validate_list_of_shares, validate_list_of_shares_malicious, }; pub use world::{ make as make_world, make_with_config as make_world_with_config, Runner, TestWorld, diff --git a/src/test_fixture/sharing.rs b/src/test_fixture/sharing.rs index 0a278a4d7..304e943a5 100644 --- a/src/test_fixture/sharing.rs +++ b/src/test_fixture/sharing.rs @@ -115,6 +115,21 @@ pub fn validate_and_reconstruct( s0.left() + s1.left() + s2.left() } +/// Validates correctness of the secret sharing scheme. +/// +/// # Panics +/// Panics if the given input is not a valid replicated secret share. +pub fn validate_and_reconstruct_malicious( + s0: &MaliciousReplicated, + s1: &MaliciousReplicated, + s2: &MaliciousReplicated, +) -> (F, F) { + let result = validate_and_reconstruct(s0.x(), s1.x(), s2.x()); + let result_macs = validate_and_reconstruct(s0.rx(), s1.rx(), s2.rx()); + + (result, result_macs) +} + /// Validates expected result from the secret shares obtained. /// /// # Panics @@ -134,6 +149,7 @@ pub fn validate_list_of_shares(expected_result: &[u128], result: &Repl /// # Panics /// Panics if the expected result is not same as obtained result. Also panics if `validate_and_reconstruct` fails for input or MACs pub fn validate_list_of_shares_malicious( + r: F, expected_result: &[u128], result: &MaliciousShares, ) { @@ -141,9 +157,9 @@ pub fn validate_list_of_shares_malicious( assert_eq!(expected_result.len(), result[1].len()); assert_eq!(expected_result.len(), result[2].len()); for (i, expected) in expected_result.iter().enumerate() { - let revealed = - validate_and_reconstruct(result[0][i].x(), result[1][i].x(), result[2][i].x()); + let (revealed, revealed_times_r) = + validate_and_reconstruct_malicious(&result[0][i], &result[1][i], &result[2][i]); assert_eq!(revealed, F::from(*expected)); - validate_and_reconstruct(result[0][i].rx(), result[1][i].rx(), result[2][i].rx()); + assert_eq!(revealed * r, revealed_times_r); } }