Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test manipulated bit multiplication fails verification #1181

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 126 additions & 3 deletions ipa-core/src/protocol/basics/mul/dzkp_malicious.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,143 @@

#[cfg(all(test, unit_test))]
mod test {
use ipa_step_derive::CompactStep;

use crate::{
error::Error,
ff::boolean::Boolean,
helpers::{Role, Role::H1},
protocol::{
basics::SecureMul,
context::{dzkp_validator::DZKPValidator, Context, DZKPContext, UpgradableContext},
basics::{
mul::{dzkp_malicious::Field, semi_honest::multiplication_protocol, Replicated},
SecureMul,
},
context::{
dzkp_field::DZKPCompatibleField,
dzkp_validator::{DZKPValidator, Segment},
Context, DZKPContext, DZKPUpgradedMaliciousContext, UpgradableContext,
},
RecordId,
},
rand::{thread_rng, Rng},
secret_sharing::{replicated::semi_honest::AdditiveShare, SharedValueArray, Vectorizable},
test_fixture::{Reconstruct, Runner, TestWorld},
};

/// This function mirrors `zkp_multiply` except that on party cheats.
///
/// The cheating party flips `prss_left`
/// which causes a flip in `z_left` computed by the cheating party.
/// This manipulated `z_left` is then sent to a different helper
/// and included in the DZKP batch.
pub async fn multiply_with_cheater<'a, F, const N: usize>(
ctx: DZKPUpgradedMaliciousContext<'a>,
record_id: RecordId,
a: &Replicated<F, N>,
b: &Replicated<F, N>,
prss: &Replicated<F, N>,
cheater: Role,
) -> Result<Replicated<F, N>, Error>
where
F: Field + DZKPCompatibleField<N>,
{
let mut prss_left = prss.left_arr().clone();
if ctx.role() == cheater {
prss_left += <<F as Vectorizable<N>>::Array>::from_fn(|_| F::ONE);
};

let z =
multiplication_protocol(&ctx, record_id, a, b, &prss_left, prss.right_arr()).await?;
Comment on lines +123 to +129
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it just adds an extra level of confusion that we achieve this bit flip specifically through manipulating the prss... I would rather just mutate z after computing it.

// create segment
let segment = Segment::from_entries(
F::as_segment_entry(a.left_arr()),
F::as_segment_entry(a.right_arr()),
F::as_segment_entry(b.left_arr()),
F::as_segment_entry(b.right_arr()),
F::as_segment_entry(prss.left_arr()),
F::as_segment_entry(prss.right_arr()),
F::as_segment_entry(z.right_arr()),
);

// add segment to the batch that needs to be verified by the dzkp prover and verifiers
ctx.push(record_id, segment);

Ok(z)
}
Comment on lines +131 to +145
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach leads to a lot of code duplication. Why not just invoke the normal protocol (which already does all of this segment entry stuff), and just mutate the result (apply an additive attack)?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. These tests are running against reference implementation and not intended to be formal verification methods that explore all possible states the system can be it. There are better tools than our poor in-memory infra for that.

Modelling the state of malicious adversaries does not seem very useful - there are infinite combinatorial states they can be it. The threat model allows them to delay response infinitely, respond with errors, run Byzantine attacks etc. Instead of focusing on what malicious adversaries can do, it may be easier to shift our attention to what honest helpers can see from such entities. This greatly simplifies testing imo, because there are only a few things a malicious actor can do:

  1. It may never respond to multiplication requests
  2. It can send correct values and tamper the proof
  3. It can send corrupted values and proof for correct values
  4. It can send corrupted values and proof that matches them

For the first 3 we don't need to duplicate protocols (#1202 covers 2 and 3 and 1 will be covered after #1205). I could be wrong, but test here seems to be checking 3) so we could leverage our new infrastructure capabilities and avoid copy-pasting the segment construction logic.

Both @danielmasny and @andyleiserson expressed interest in covering 4) which is an interesting exercise. I think code duplication is probably wrong for it in the long term as well - we need to figure out how to do it without cloning our MPC circuits.

fn generate_share_from_three_bits(role: Role, i: usize) -> AdditiveShare<Boolean> {
let (first_bit, second_bit) = match role {
Role::H1 => (i % 2 == 0, (i >> 1) % 2 == 0),
Role::H2 => ((i >> 1) % 2 == 0, (i >> 2) % 2 == 0),
Role::H3 => ((i >> 2) % 2 == 0, i % 2 == 0),
};
<AdditiveShare<Boolean>>::from((first_bit.into(), second_bit.into()))
}

fn all_combination_of_inputs(role: Role, i: usize) -> [AdditiveShare<Boolean>; 3] {
// first three bits
let a = generate_share_from_three_bits(role, i);
// middle three bits
let b = generate_share_from_three_bits(role, i >> 3);
// last three bits
let prss = generate_share_from_three_bits(role, i >> 6);

[a, b, prss]
}

#[derive(CompactStep)]

Check warning on line 166 in ipa-core/src/protocol/basics/mul/dzkp_malicious.rs

View check run for this annotation

Codecov / codecov/patch

ipa-core/src/protocol/basics/mul/dzkp_malicious.rs#L166

Added line #L166 was not covered by tests
enum TestStep {
#[step(count = 512)]
Counter(usize),
}

#[tokio::test]
async fn detect_cheating() {
let world = TestWorld::default();

for i in 0..512 {
let [(_, s_1), (_, s_2), (v_3, s_3)] = world
.malicious((), |ctx, ()| async move {
let [a, b, prss] = all_combination_of_inputs(ctx.role(), i);
let validator = ctx.narrow(&TestStep::Counter(i)).dzkp_validator(10);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what this "10" is / does.

let mctx = validator.context();
let product = multiply_with_cheater(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would rather have the other two parties call the normal protocol, and only one call multiply_with_cheater. I realize that only the person with the correct role is going to actually cheat, but this seems riskier than just testing the verifiers running the normal code.

mctx.set_total_records(1),
RecordId::FIRST,
&a,
&b,
&prss,
H1,
)
.await
.unwrap();

(
validator.validate().await,
[
bool::from(*a.left_arr().first()),
bool::from(*a.right_arr().first()),
bool::from(*b.left_arr().first()),
bool::from(*b.right_arr().first()),
bool::from(*prss.left_arr().first()),
bool::from(*prss.right_arr().first()),
bool::from(*product.left_arr().first()),
bool::from(*product.right_arr().first()),
],
)
})
.await;

// H1 cheats means H3 fails
// since always verifier on the left of the cheating prover fails
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes zero sense to me. I cannot think of any reason why just one verifier should fail.

Both verifiers should be checking that all of the b shares sum to zero. If it fails for one, it should fail for both.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets sync on this once you are back next week.

match v_3 {
Ok(()) => panic!("Got a result H1: {s_1:?}, H2: {s_2:?}, H3: {s_3:?}"),
Err(ref err) => assert!(matches!(err, Error::DZKPValidationFailed)),
}
}
}

#[tokio::test]
pub async fn simple() {
async fn simple() {
let world = TestWorld::default();

let mut rng = thread_rng();
Expand Down