diff --git a/ferveo-wasm/tests/node.rs b/ferveo-wasm/tests/node.rs index bae5750b..7b35efa5 100644 --- a/ferveo-wasm/tests/node.rs +++ b/ferveo-wasm/tests/node.rs @@ -18,8 +18,12 @@ type TestSetup = ( const TAU: u32 = 0; -fn setup_dkg(shares_num: u32, security_threshold: u32) -> TestSetup { - let validator_keypairs = (0..shares_num as usize) +fn setup_dkg( + shares_num: u32, + validators_num: u32, + security_threshold: u32, +) -> TestSetup { + let validator_keypairs = (0..validators_num as usize) .map(gen_keypair) .collect::>(); let validators = validator_keypairs @@ -32,7 +36,7 @@ fn setup_dkg(shares_num: u32, security_threshold: u32) -> TestSetup { // Each validator holds their own DKG instance and generates a transcript every // validator, including themselves let messages = validators.iter().map(|sender| { - let mut dkg = Dkg::new( + let mut validator_dkg = Dkg::new( TAU, shares_num, security_threshold, @@ -40,7 +44,7 @@ fn setup_dkg(shares_num: u32, security_threshold: u32) -> TestSetup { sender, ) .unwrap(); - let transcript = dkg.generate_transcript().unwrap(); + let transcript = validator_dkg.generate_transcript().unwrap(); ValidatorMessage::new(sender, &transcript).unwrap() }); @@ -61,12 +65,16 @@ fn setup_dkg(shares_num: u32, security_threshold: u32) -> TestSetup { // Server can aggregate the transcripts and verify them let server_aggregate = dkg.aggregate_transcripts(&messages_js).unwrap(); - let is_valid = server_aggregate.verify(shares_num, &messages_js).unwrap(); + let is_valid = server_aggregate + .verify(validators_num, &messages_js) + .unwrap(); assert!(is_valid); // Client can also aggregate the transcripts and verify them let client_aggregate = AggregatedTranscript::new(&messages_js).unwrap(); - let is_valid = client_aggregate.verify(shares_num, &messages_js).unwrap(); + let is_valid = client_aggregate + .verify(validators_num, &messages_js) + .unwrap(); assert!(is_valid); // In the meantime, the client creates a ciphertext and decryption request @@ -88,105 +96,116 @@ fn setup_dkg(shares_num: u32, security_threshold: u32) -> TestSetup { #[wasm_bindgen_test] fn tdec_simple() { let shares_num = 16; - let security_threshold = 10; - let ( - validator_keypairs, - validators, - validators_js, - messages_js, - msg, - aad, - ciphertext, - ) = setup_dkg(shares_num, security_threshold); - - // Having aggregated the transcripts, the validators can now create decryption shares - let decryption_shares = zip_eq(validators, validator_keypairs) - .map(|(validator, keypair)| { - let mut dkg = Dkg::new( - TAU, - shares_num, - security_threshold, - &validators_js, - &validator, - ) - .unwrap(); - let aggregate = dkg.aggregate_transcripts(&messages_js).unwrap(); - let is_valid = aggregate.verify(shares_num, &messages_js).unwrap(); - assert!(is_valid); - - aggregate - .create_decryption_share_simple( - &dkg, - &ciphertext.header().unwrap(), - &aad, - &keypair, + let security_threshold = shares_num / 2; + for validators_num in [shares_num, shares_num + 2] { + let ( + validator_keypairs, + validators, + validators_js, + messages_js, + msg, + aad, + ciphertext, + ) = setup_dkg(shares_num, validators_num, security_threshold); + + // Having aggregated the transcripts, the validators can now create decryption shares + let decryption_shares = zip_eq(validators, validator_keypairs) + .map(|(validator, keypair)| { + let mut dkg = Dkg::new( + TAU, + shares_num, + security_threshold, + &validators_js, + &validator, ) - .unwrap() - }) - .collect::>(); - let decryption_shares_js = into_js_array(decryption_shares); - - // Now, the decryption share can be used to decrypt the ciphertext - // This part is in the client API - - let shared_secret = - combine_decryption_shares_simple(&decryption_shares_js).unwrap(); - - // The client should have access to the public parameters of the DKG - let plaintext = - decrypt_with_shared_secret(&ciphertext, &aad, &shared_secret).unwrap(); - assert_eq!(msg, plaintext); + .unwrap(); + let aggregate = + dkg.aggregate_transcripts(&messages_js).unwrap(); + let is_valid = + aggregate.verify(validators_num, &messages_js).unwrap(); + assert!(is_valid); + + aggregate + .create_decryption_share_simple( + &dkg, + &ciphertext.header().unwrap(), + &aad, + &keypair, + ) + .unwrap() + }) + .collect::>(); + let decryption_shares_js = into_js_array(decryption_shares); + + // Now, the decryption share can be used to decrypt the ciphertext + // This part is in the client API + + let shared_secret = + combine_decryption_shares_simple(&decryption_shares_js).unwrap(); + + // The client should have access to the public parameters of the DKG + let plaintext = + decrypt_with_shared_secret(&ciphertext, &aad, &shared_secret) + .unwrap(); + assert_eq!(msg, plaintext); + } } #[wasm_bindgen_test] fn tdec_precomputed() { let shares_num = 16; let security_threshold = shares_num; // Must be equal to shares_num in precomputed variant - let ( - validator_keypairs, - validators, - validators_js, - messages_js, - msg, - aad, - ciphertext, - ) = setup_dkg(shares_num, security_threshold); - - // Having aggregated the transcripts, the validators can now create decryption shares - let decryption_shares = zip_eq(validators, validator_keypairs) - .map(|(validator, keypair)| { - let mut dkg = Dkg::new( - TAU, - shares_num, - security_threshold, - &validators_js, - &validator, - ) - .unwrap(); - let aggregate = dkg.aggregate_transcripts(&messages_js).unwrap(); - let is_valid = aggregate.verify(shares_num, &messages_js).unwrap(); - assert!(is_valid); - - aggregate - .create_decryption_share_precomputed( - &dkg, - &ciphertext.header().unwrap(), - &aad, - &keypair, + for validators_num in [shares_num, shares_num + 2] { + let ( + validator_keypairs, + validators, + validators_js, + messages_js, + msg, + aad, + ciphertext, + ) = setup_dkg(shares_num, validators_num, security_threshold); + + // Having aggregated the transcripts, the validators can now create decryption shares + let decryption_shares = zip_eq(validators, validator_keypairs) + .map(|(validator, keypair)| { + let mut dkg = Dkg::new( + TAU, + shares_num, + security_threshold, + &validators_js, + &validator, ) - .unwrap() - }) - .collect::>(); - let decryption_shares_js = into_js_array(decryption_shares); - - // Now, the decryption share can be used to decrypt the ciphertext - // This part is in the client API - - let shared_secret = - combine_decryption_shares_precomputed(&decryption_shares_js).unwrap(); - - // The client should have access to the public parameters of the DKG - let plaintext = - decrypt_with_shared_secret(&ciphertext, &aad, &shared_secret).unwrap(); - assert_eq!(msg, plaintext); + .unwrap(); + let aggregate = + dkg.aggregate_transcripts(&messages_js).unwrap(); + let is_valid = + aggregate.verify(validators_num, &messages_js).unwrap(); + assert!(is_valid); + + aggregate + .create_decryption_share_precomputed( + &dkg, + &ciphertext.header().unwrap(), + &aad, + &keypair, + ) + .unwrap() + }) + .collect::>(); + let decryption_shares_js = into_js_array(decryption_shares); + + // Now, the decryption share can be used to decrypt the ciphertext + // This part is in the client API + + let shared_secret = + combine_decryption_shares_precomputed(&decryption_shares_js) + .unwrap(); + + // The client should have access to the public parameters of the DKG + let plaintext = + decrypt_with_shared_secret(&ciphertext, &aad, &shared_secret) + .unwrap(); + assert_eq!(msg, plaintext); + } } diff --git a/ferveo/src/api.rs b/ferveo/src/api.rs index 80ab58d6..caa0d9b4 100644 --- a/ferveo/src/api.rs +++ b/ferveo/src/api.rs @@ -286,12 +286,20 @@ impl AggregatedTranscript { pub fn verify( &self, - shares_num: u32, + validators_num: u32, messages: &[ValidatorMessage], ) -> Result { + if validators_num < messages.len() as u32 { + return Err(Error::InvalidAggregateVerificationParameters( + validators_num, + messages.len() as u32, + )); + } + let pvss_params = PubliclyVerifiableParams::::default(); - let domain = GeneralEvaluationDomain::::new(shares_num as usize) - .expect("Unable to construct an evaluation domain"); + let domain = + GeneralEvaluationDomain::::new(validators_num as usize) + .expect("Unable to construct an evaluation domain"); let is_valid_optimistic = self.0.verify_optimistic(); if !is_valid_optimistic { @@ -428,8 +436,9 @@ mod test_ferveo_api { tau: u32, security_threshold: u32, shares_num: u32, + validators_num: u32, ) -> TestInputs { - let validator_keypairs = gen_keypairs(shares_num); + let validator_keypairs = gen_keypairs(validators_num); let validators = validator_keypairs .iter() .enumerate() @@ -469,16 +478,22 @@ mod test_ferveo_api { assert_eq!(dkg_pk, deserialized); } - #[test_case(4; "number of shares (validators) is a power of 2")] - #[test_case(7; "number of shares (validators) is not a power of 2")] - fn test_server_api_tdec_precomputed(shares_num: u32) { + #[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_server_api_tdec_precomputed(shares_num: u32, validators_num: u32) { let rng = &mut StdRng::seed_from_u64(0); // In precomputed variant, the security threshold is equal to the number of shares let security_threshold = shares_num; - let (messages, validators, validator_keypairs) = - make_test_inputs(rng, TAU, security_threshold, shares_num); + let (messages, validators, validator_keypairs) = make_test_inputs( + rng, + TAU, + security_threshold, + shares_num, + validators_num, + ); // Now that every validator holds a dkg instance and a transcript for every other validator, // every validator can aggregate the transcripts @@ -488,7 +503,7 @@ mod test_ferveo_api { .unwrap(); let pvss_aggregated = dkg.aggregate_transcripts(&messages).unwrap(); - assert!(pvss_aggregated.verify(shares_num, &messages).unwrap()); + assert!(pvss_aggregated.verify(validators_num, &messages).unwrap()); // At this point, any given validator should be able to provide a DKG public key let dkg_public_key = dkg.public_key(); @@ -511,7 +526,9 @@ mod test_ferveo_api { ) .unwrap(); let aggregate = dkg.aggregate_transcripts(&messages).unwrap(); - assert!(pvss_aggregated.verify(shares_num, &messages).unwrap()); + assert!(pvss_aggregated + .verify(validators_num, &messages) + .unwrap()); // And then each validator creates their own decryption share aggregate @@ -551,15 +568,21 @@ mod test_ferveo_api { assert!(result.is_err()); } - #[test_case(4; "number of shares (validators) is a power of 2")] - #[test_case(7; "number of shares (validators) is not a power of 2")] - fn test_server_api_tdec_simple(shares_num: u32) { + #[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_server_api_tdec_simple(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) = - make_test_inputs(rng, TAU, security_threshold, shares_num); + let (messages, validators, validator_keypairs) = make_test_inputs( + rng, + TAU, + security_threshold, + shares_num, + validators_num, + ); // Now that every validator holds a dkg instance and a transcript for every other validator, // every validator can aggregate the transcripts @@ -573,7 +596,7 @@ mod test_ferveo_api { .unwrap(); let pvss_aggregated = dkg.aggregate_transcripts(&messages).unwrap(); - assert!(pvss_aggregated.verify(shares_num, &messages).unwrap()); + assert!(pvss_aggregated.verify(validators_num, &messages).unwrap()); // At this point, any given validator should be able to provide a DKG public key let public_key = dkg.public_key(); @@ -595,7 +618,7 @@ mod test_ferveo_api { ) .unwrap(); let aggregate = dkg.aggregate_transcripts(&messages).unwrap(); - assert!(aggregate.verify(shares_num, &messages).unwrap()); + assert!(aggregate.verify(validators_num, &messages).unwrap()); aggregate .create_decryption_share_simple( &dkg, @@ -635,104 +658,137 @@ mod test_ferveo_api { // implementation for aggregation and aggregate verification. // Here, we focus on testing user-facing APIs for server and client users. - #[test] - fn server_side_local_verification() { + #[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 server_side_local_verification(shares_num: u32, validators_num: u32) { let rng = &mut StdRng::seed_from_u64(0); + let security_threshold = shares_num / 2 + 1; - let (messages, validators, _) = - make_test_inputs(rng, TAU, SECURITY_THRESHOLD, SHARES_NUM); + let (messages, validators, _) = make_test_inputs( + rng, + TAU, + security_threshold, + shares_num, + validators_num, + ); // Now that every validator holds a dkg instance and a transcript for every other validator, // every validator can aggregate the transcripts let me = validators[0].clone(); let mut dkg = - Dkg::new(TAU, SHARES_NUM, SECURITY_THRESHOLD, &validators, &me) + Dkg::new(TAU, shares_num, security_threshold, &validators, &me) .unwrap(); - let local_aggregate = dkg.aggregate_transcripts(&messages).unwrap(); - assert!(local_aggregate - .verify(dkg.0.dkg_params.shares_num(), &messages) - .is_ok()); + let good_aggregate = dkg.aggregate_transcripts(&messages).unwrap(); + assert!(good_aggregate.verify(validators_num, &messages).is_ok()); // Test negative cases // Notice that the dkg instance is mutable, so we need to get a fresh one // for every test case + // Should fail if the number of validators is less than the number of messages + assert!(good_aggregate + .verify(messages.len() as u32 - 1, &messages) + .is_err()); + // Should fail if no transcripts are provided let mut dkg = - Dkg::new(TAU, SHARES_NUM, SECURITY_THRESHOLD, &validators, &me) + Dkg::new(TAU, shares_num, security_threshold, &validators, &me) .unwrap(); let result = dkg.aggregate_transcripts(&[]); assert!(result.is_err()); // Not enough transcripts let mut dkg = - Dkg::new(TAU, SHARES_NUM, SECURITY_THRESHOLD, &validators, &me) + Dkg::new(TAU, shares_num, security_threshold, &validators, &me) .unwrap(); - let not_enough_messages = &messages[..SECURITY_THRESHOLD as usize - 1]; - assert!(not_enough_messages.len() < SECURITY_THRESHOLD as usize); + let not_enough_messages = &messages[..security_threshold as usize - 1]; + assert!(not_enough_messages.len() < security_threshold as usize); let insufficient_aggregate = dkg.aggregate_transcripts(not_enough_messages).unwrap(); - let result = insufficient_aggregate.verify(SHARES_NUM, &messages); + let result = insufficient_aggregate.verify(validators_num, &messages); assert!(result.is_err()); // Unexpected transcripts in the aggregate or transcripts from a different ritual // Using same DKG parameters, but different DKG instances and validators let mut dkg = - Dkg::new(TAU, SHARES_NUM, SECURITY_THRESHOLD, &validators, &me) + Dkg::new(TAU, shares_num, security_threshold, &validators, &me) .unwrap(); - let (bad_messages, _, _) = - make_test_inputs(rng, TAU, SECURITY_THRESHOLD, SHARES_NUM); + let (bad_messages, _, _) = make_test_inputs( + rng, + TAU, + security_threshold, + shares_num, + validators_num, + ); let mixed_messages = [&messages[..2], &bad_messages[..1]].concat(); let bad_aggregate = dkg.aggregate_transcripts(&mixed_messages).unwrap(); - let result = bad_aggregate.verify(SHARES_NUM, &messages); + let result = bad_aggregate.verify(validators_num, &messages); assert!(result.is_err()); } - #[test] - fn client_side_local_verification() { + #[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 client_side_local_verification(shares_num: u32, validators_num: u32) { let rng = &mut StdRng::seed_from_u64(0); + let security_threshold = shares_num / 2 + 1; - let (messages, _, _) = - make_test_inputs(rng, TAU, SECURITY_THRESHOLD, SHARES_NUM); + let (messages, _, _) = make_test_inputs( + rng, + TAU, + security_threshold, + shares_num, + validators_num, + ); // We only need `security_threshold` transcripts to aggregate - let messages = &messages[..SECURITY_THRESHOLD as usize]; + let messages = &messages[..security_threshold as usize]; // Create an aggregated transcript on the client side - let aggregated_transcript = - AggregatedTranscript::new(messages).unwrap(); + let good_aggregate = AggregatedTranscript::new(messages).unwrap(); // We are separating the verification from the aggregation since the client may fetch // the aggregate from a side-channel or decide to persist it and verify it later // Now, the client can verify the aggregated transcript - let result = aggregated_transcript.verify(SHARES_NUM, messages); + let result = good_aggregate.verify(validators_num, messages); assert!(result.is_ok()); assert!(result.unwrap()); // Test negative cases + // Should fail if the number of validators is less than the number of messages + assert!(good_aggregate + .verify(messages.len() as u32 - 1, messages) + .is_err()); + // Should fail if no transcripts are provided let result = AggregatedTranscript::new(&[]); assert!(result.is_err()); // Not enough transcripts - let not_enough_messages = &messages[..SECURITY_THRESHOLD as usize - 1]; - assert!(not_enough_messages.len() < SECURITY_THRESHOLD as usize); + let not_enough_messages = &messages[..security_threshold as usize - 1]; + assert!(not_enough_messages.len() < security_threshold as usize); let insufficient_aggregate = AggregatedTranscript::new(not_enough_messages).unwrap(); - let result = insufficient_aggregate.verify(SHARES_NUM, messages); + let result = insufficient_aggregate.verify(validators_num, messages); assert!(result.is_err()); // Unexpected transcripts in the aggregate or transcripts from a different ritual // Using same DKG parameters, but different DKG instances and validators - let (bad_messages, _, _) = - make_test_inputs(rng, TAU, SECURITY_THRESHOLD, SHARES_NUM); + let (bad_messages, _, _) = make_test_inputs( + rng, + TAU, + security_threshold, + shares_num, + validators_num, + ); let mixed_messages = [&messages[..2], &bad_messages[..1]].concat(); let bad_aggregate = AggregatedTranscript::new(&mixed_messages).unwrap(); - let result = bad_aggregate.verify(SHARES_NUM, messages); + let result = bad_aggregate.verify(validators_num, messages); assert!(result.is_err()); } } diff --git a/ferveo/src/bindings_python.rs b/ferveo/src/bindings_python.rs index bd29dc0d..137bc718 100644 --- a/ferveo/src/bindings_python.rs +++ b/ferveo/src/bindings_python.rs @@ -116,6 +116,11 @@ impl From for PyErr { Error::NoTranscriptsToAggregate => { NoTranscriptsToAggregate::new_err("") } + Error::InvalidAggregateVerificationParameters(validators_num, messages_num) => { + InvalidDkgParameters::new_err(format!( + "validators_num: {validators_num}, messages_num: {messages_num}" + )) + } }, _ => default(), } diff --git a/ferveo/src/lib.rs b/ferveo/src/lib.rs index d5f1fb11..47926b3f 100644 --- a/ferveo/src/lib.rs +++ b/ferveo/src/lib.rs @@ -117,6 +117,10 @@ pub enum Error { /// Creating a transcript aggregate requires at least one transcript #[error("No transcripts to aggregate")] NoTranscriptsToAggregate, + + /// The number of messages may not be greater than the number of validators + #[error("Invalid aggregate verification parameters: number of validators {0}, number of messages: {1}")] + InvalidAggregateVerificationParameters(u32, u32), } pub type Result = std::result::Result;