diff --git a/2pc-mpc/src/mock.rs b/2pc-mpc/src/mock.rs index e350bfc..063fb6d 100644 --- a/2pc-mpc/src/mock.rs +++ b/2pc-mpc/src/mock.rs @@ -10,13 +10,18 @@ //! Under the `unsafe_mock` feature the exported protocol party/`Protocol` aliases are redirected to //! the mock party types defined here (see `crate::mock::{dkg, ecdsa, schnorr, vss, network_dkg}`); //! the real protocol `advance`/method bodies are left completely untouched. Each mock party -//! finalizes in a single round, deterministically, from the common public inputs alone. +//! simulates its real protocol's round structure — trivial [`MOCK_HONEST_MESSAGE`] rounds with +//! malicious-sender detection and threshold enforcement (see [`mock_advance_result`]) — and +//! finalizes on the last round, deterministically, from the common public inputs alone. //! //! ## What it gives up //! **All security.** Every dWallet uses the SAME constant signing key `x = 42` //! ([`MOCK_SECRET_KEY`]) and a constant nonce, so anyone can forge signatures and every dWallet -//! shares the public key `42·G`. The user (centralized) side is emulated (`SignData::ToBeEmulated`, -//! `x_A = 0`). This must never be enabled in production — only in tests. +//! shares the public key `42·G`. The user (centralized) side does no real cryptography either: +//! under threshold mode it is emulated (`SignData::ToBeEmulated`, `x_A = 0`), and for user-signed +//! flows the centralized sign parties are mocked (`MockSignCentralized*Party`) — they emit a +//! deterministic, structurally-valid partial signature that the mocked decentralized sign ignores. +//! This must never be enabled in production — only in tests. pub(crate) mod dkg; pub(crate) mod ecdsa; @@ -26,13 +31,13 @@ pub(crate) mod schnorr; mod timings; pub(crate) mod vss; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use crypto_bigint::Uint; use group::helpers::DeduplicateAndSort; use group::{GroupElement as _, PartyID, PrimeGroupElement}; -use mpc::AsynchronousRoundResult; +use mpc::{AsynchronousRoundResult, WeightedThresholdAccessStructure}; /// The magic value every honest party's mock protocol message carries. The mock /// parties simulate the real round structure (advancing round-by-round, finalizing @@ -74,12 +79,31 @@ pub(crate) fn mock_round_bookkeeping( /// [`AsynchronousRoundResult::Finalize`] with the outputs from `finalize` (called /// only on the final round; its error is propagated). Threads the detected /// `malicious_parties` through either arm. -pub(crate) fn mock_advance_result( +/// +/// Mimics the real protocols' threshold-not-reached behavior: an advance first +/// filters protocol-invalid (malicious) contributions, then requires the remaining +/// senders of the latest completed round to form an authorized subset — otherwise it +/// aborts with [`mpc::ErrorKind::ThresholdNotReached`], which the +/// guaranteed-output-delivery layer converts into a `ThresholdNotReached` message and +/// a retried advance (attributed to the previous round, see +/// [`mock_round_causing_threshold_not_reached`]). +pub(crate) fn mock_advance_result>( + access_structure: &WeightedThresholdAccessStructure, messages: &[HashMap], total_rounds: usize, finalize: impl FnOnce() -> Result<(PrivateOutput, PublicOutput), Error>, ) -> Result, Error> { let (malicious_parties, is_final_round) = mock_round_bookkeeping(messages, total_rounds); + if let Some(latest_round_messages) = messages.last() { + let honest_senders = latest_round_messages + .keys() + .filter(|party_id| !malicious_parties.contains(party_id)) + .copied() + .collect::>(); + access_structure + .is_authorized_subset(&honest_senders) + .map_err(Error::from)?; + } if is_final_round { let (private_output, public_output) = finalize()?; Ok(AsynchronousRoundResult::Finalize { diff --git a/2pc-mpc/src/mock/dkg.rs b/2pc-mpc/src/mock/dkg.rs index bffe393..7355d85 100644 --- a/2pc-mpc/src/mock/dkg.rs +++ b/2pc-mpc/src/mock/dkg.rs @@ -387,7 +387,7 @@ where fn advance( _session_id: CommitmentSizedNumber, _party_id: PartyID, - _access_structure: &WeightedThresholdAccessStructure, + access_structure: &WeightedThresholdAccessStructure, messages: Vec>, _private_input: Option, public_input: &Self::PublicInput, @@ -397,7 +397,7 @@ where Self::Error, > { // dwallet DKG decentralized party is a 2-round protocol. - crate::mock::mock_advance_result(&messages, 2, || { + crate::mock::mock_advance_result(access_structure, &messages, 2, || { Ok(( (), mock_dkg_output::( diff --git a/2pc-mpc/src/mock/ecdsa.rs b/2pc-mpc/src/mock/ecdsa.rs index 8ade44f..89e2faa 100644 --- a/2pc-mpc/src/mock/ecdsa.rs +++ b/2pc-mpc/src/mock/ecdsa.rs @@ -36,8 +36,18 @@ use ::class_groups::{ RandomnessSpaceGroupElement, RandomnessSpacePublicParameters, SecretKeyShareSizedInteger, }; +use commitment::pedersen; +use proof::GroupsPublicParametersAccessors; + use crate::class_groups::ecdsa::{DKGSignPartyPublicInput, SignPartyPublicInput}; +use crate::class_groups::ProtocolPublicParameters; +use crate::ecdsa::sign::centralized_party::message::class_groups::Message as CentralizedSignMessage; use crate::ecdsa::VerifyingKey; +use crate::languages::{ + self, CommitmentOfDiscreteLogProof, + EqualityBetweenCommitmentsWithDifferentPublicParametersProof, KnowledgeOfDecommitmentProof, + KnowledgeOfDecommitmentUCProof, VectorCommitmentOfDiscreteLogProof, +}; /// Produce an INSECURE but valid ECDSA signature over `message` under the constant mock key `42·G`. /// @@ -348,7 +358,7 @@ where fn advance( session_id: CommitmentSizedNumber, _party_id: PartyID, - _access_structure: &WeightedThresholdAccessStructure, + access_structure: &WeightedThresholdAccessStructure, messages: Vec>, _private_input: Option, public_input: &Self::PublicInput, @@ -358,7 +368,7 @@ where Self::Error, > { // ECDSA presign decentralized party is a 4-round protocol. - crate::mock::mock_advance_result(&messages, 4, || { + crate::mock::mock_advance_result(access_structure, &messages, 4, || { let protocol_public_parameters = &*public_input.protocol_public_parameters; let identity_point = GroupElement::neutral_from_public_parameters( &protocol_public_parameters.group_public_parameters, @@ -392,6 +402,292 @@ where } } +// =================================================================================================== +// ECDSA centralized (user-side) sign +// =================================================================================================== + +/// INSECURE mock of the ECDSA centralized (user-side) sign party — returns a +/// deterministic, structurally-valid sign message (neutral commitment/nonce points, +/// ciphertexts reused from the pp, `new_default` neutral proofs) and performs none of +/// the real work: no class-group homomorphic evaluation, no proof generation, and no +/// input-consistency checks. The real user side is expensive (the homomorphic partial +/// signature evaluation over the presign ciphertexts), which is exactly the cost the +/// mock exists to eliminate. The mocked decentralized sign ignores the sign message (it +/// emits the constant-nonce signature), and the mocked +/// `verify_centralized_party_partial_signature` accepts it without proof verification. +pub struct MockSignCentralizedECDSAParty< + const SCALAR_LIMBS: usize, + const FUNDAMENTAL_DISCRIMINANT_LIMBS: usize, + const NON_FUNDAMENTAL_DISCRIMINANT_LIMBS: usize, + const MESSAGE_LIMBS: usize, + GroupElement, +>(PhantomData); + +impl< + const SCALAR_LIMBS: usize, + const FUNDAMENTAL_DISCRIMINANT_LIMBS: usize, + const NON_FUNDAMENTAL_DISCRIMINANT_LIMBS: usize, + const MESSAGE_LIMBS: usize, + GroupElement: VerifyingKey + Copy, + > mpc::two_party::Round + for MockSignCentralizedECDSAParty< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + MESSAGE_LIMBS, + GroupElement, + > +where + Int: Encoding, + Uint: Encoding, + Int: Encoding, + Uint: Encoding, + Int: Encoding, + Uint: Encoding, + Uint: Encoding, + EquivalenceClass: group::GroupElement< + Value = CompactIbqf, + PublicParameters = equivalence_class::PublicParameters< + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + >, + > + EquivalenceClassOps< + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + MultiFoldNupowAccelerator = MultiFoldNupowAccelerator< + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + >, + >, + EncryptionKey< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + GroupElement, + >: AdditivelyHomomorphicEncryptionKey< + SCALAR_LIMBS, + PublicParameters = encryption_key::PublicParameters< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + group::PublicParameters, + >, + PlaintextSpaceGroupElement = GroupElement::Scalar, + RandomnessSpaceGroupElement = RandomnessSpaceGroupElement, + CiphertextSpaceGroupElement = CiphertextSpaceGroupElement< + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + >, + >, + encryption_key::PublicParameters< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + group::PublicParameters, + >: AsRef< + homomorphic_encryption::GroupsPublicParameters< + group::PublicParameters, + RandomnessSpacePublicParameters, + CiphertextSpacePublicParameters, + >, + >, +{ + type Error = crate::Error; + type PrivateInput = + crate::dkg::centralized_party::SecretKeyShare>; + type PublicInput = + crate::ecdsa::sign::centralized_party::signature_homomorphic_evaluation_round::PublicInput< + crate::dkg::centralized_party::VersionedOutput, + crate::ecdsa::presign::VersionedPresign< + GroupElement::Value, + group::Value>, + >, + ProtocolPublicParameters< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + GroupElement, + >, + >; + type PrivateOutput = (); + type PublicOutputValue = (); + type PublicOutput = (); + type IncomingMessage = (); + type OutgoingMessage = CentralizedSignMessage< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + MESSAGE_LIMBS, + GroupElement, + >; + + fn advance( + _message: Self::IncomingMessage, + _secret_key_share: &Self::PrivateInput, + public_input: &Self::PublicInput, + _rng: &mut impl CsRng, + ) -> std::result::Result< + mpc::two_party::RoundResult, + Self::Error, + > { + let protocol_public_parameters = &public_input.protocol_public_parameters; + + let neutral_point = GroupElement::neutral_from_public_parameters( + &protocol_public_parameters.group_public_parameters, + )? + .value(); + // Reuse a stored ciphertext as the dummy ciphertext (never read — the mocked sign uses + // the constant nonce and ignores the sign message). + let dummy_ciphertext = protocol_public_parameters + .encryption_of_decentralized_party_secret_key_share_first_part; + + // `new_default` proofs need only the languages' witness/statement space public + // parameters; construct each language's public parameters exactly as the real + // centralized party does, with neutral bases and the dummy ciphertexts. + let commitment_scheme_public_parameters = + pedersen::PublicParameters::derive::( + protocol_public_parameters + .scalar_group_public_parameters + .clone(), + protocol_public_parameters.group_public_parameters.clone(), + )?; + + let knowledge_of_decommitment_public_parameters = + languages::construct_knowledge_of_decommitment_public_parameters::< + SCALAR_LIMBS, + GroupElement, + >(commitment_scheme_public_parameters.clone()); + let default_decommitment_proof = + KnowledgeOfDecommitmentProof::::new_default( + knowledge_of_decommitment_public_parameters.witness_space_public_parameters(), + knowledge_of_decommitment_public_parameters.statement_space_public_parameters(), + )?; + + let uc_knowledge_of_decommitment_public_parameters = + languages::construct_uc_knowledge_of_decommitment_public_parameters::< + SCALAR_LIMBS, + GroupElement, + >(commitment_scheme_public_parameters.clone()); + let default_uc_decommitment_proof = + KnowledgeOfDecommitmentUCProof::::new_default( + uc_knowledge_of_decommitment_public_parameters.witness_space_public_parameters(), + uc_knowledge_of_decommitment_public_parameters.statement_space_public_parameters(), + )?; + + let equality_between_commitments_public_parameters = + languages::construct_equality_between_commitments_with_different_public_parameters_public_parameters::< + SCALAR_LIMBS, + GroupElement, + >( + commitment_scheme_public_parameters.clone(), + commitment_scheme_public_parameters.clone(), + ); + let default_equality_between_commitments_proof = + EqualityBetweenCommitmentsWithDifferentPublicParametersProof::< + SCALAR_LIMBS, + GroupElement, + >::new_default( + equality_between_commitments_public_parameters.witness_space_public_parameters(), + equality_between_commitments_public_parameters.statement_space_public_parameters(), + )?; + + let commitment_of_discrete_log_public_parameters = + languages::construct_commitment_of_discrete_log_public_parameters::< + SCALAR_LIMBS, + GroupElement, + >( + neutral_point.clone(), + commitment_scheme_public_parameters.clone(), + protocol_public_parameters + .scalar_group_public_parameters + .clone(), + protocol_public_parameters.group_public_parameters.clone(), + ); + let default_commitment_of_discrete_log_proof = + CommitmentOfDiscreteLogProof::::new_default( + commitment_of_discrete_log_public_parameters.witness_space_public_parameters(), + commitment_of_discrete_log_public_parameters.statement_space_public_parameters(), + )?; + + let vector_commitment_of_discrete_log_public_parameters = + languages::construct_vector_commitment_of_discrete_log_public_parameters::< + SCALAR_LIMBS, + GroupElement, + >( + neutral_point.clone(), + commitment_scheme_public_parameters.clone().into(), + protocol_public_parameters + .scalar_group_public_parameters + .clone(), + protocol_public_parameters.group_public_parameters.clone(), + ); + let default_vector_commitment_of_discrete_log_proof = VectorCommitmentOfDiscreteLogProof::< + SCALAR_LIMBS, + GroupElement, + >::new_default( + vector_commitment_of_discrete_log_public_parameters.witness_space_public_parameters(), + vector_commitment_of_discrete_log_public_parameters.statement_space_public_parameters(), + )?; + + let committed_linear_evaluation_public_parameters = + languages::class_groups::construct_committed_linear_evaluation_public_parameters::< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + MESSAGE_LIMBS, + GroupElement, + >( + dummy_ciphertext, + dummy_ciphertext, + commitment_scheme_public_parameters.clone().into(), + protocol_public_parameters + .scalar_group_public_parameters + .clone(), + protocol_public_parameters.group_public_parameters.clone(), + protocol_public_parameters + .encryption_scheme_public_parameters + .clone(), + true, + )?; + let default_committed_linear_evaluation_proof = + languages::class_groups::CommittedLinearEvaluationProof::< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + MESSAGE_LIMBS, + GroupElement, + >::new_default( + committed_linear_evaluation_public_parameters.witness_space_public_parameters(), + committed_linear_evaluation_public_parameters.statement_space_public_parameters(), + )?; + + let sign_message = CentralizedSignMessage { + public_signature_nonce: neutral_point.clone(), + decentralized_party_nonce_public_share: neutral_point.clone(), + signature_nonce_share_commitment: neutral_point.clone(), + alpha_displacer_commitment: neutral_point.clone(), + beta_displacer_commitment: neutral_point.clone(), + signature_nonce_share_by_secret_share_commitment: neutral_point, + encryption_of_partial_signature: dummy_ciphertext, + encryption_of_displaced_decentralized_party_nonce_share: dummy_ciphertext, + non_zero_commitment_to_signature_nonce_share_proof: default_decommitment_proof.clone(), + non_zero_commitment_to_alpha_displacer_share_proof: default_decommitment_proof, + commitment_to_beta_displacer_share_uc_proof: default_uc_decommitment_proof, + proof_of_equality_between_nonce_share_and_nonce_share_by_secret_key_share_commitments: + default_equality_between_commitments_proof, + public_signature_nonce_proof: default_commitment_of_discrete_log_proof, + decentralized_party_nonce_public_share_displacement_proof: + default_vector_commitment_of_discrete_log_proof, + encryption_of_partial_signature_proof: default_committed_linear_evaluation_proof + .clone(), + encryption_of_displaced_decentralized_party_nonce_share_proof: + default_committed_linear_evaluation_proof, + }; + + Ok(mpc::two_party::RoundResult { + outgoing_message: sign_message, + private_output: (), + public_output: (), + }) + } +} + // =================================================================================================== // ECDSA sign // =================================================================================================== @@ -646,7 +942,7 @@ where fn advance( _session_id: CommitmentSizedNumber, _party_id: PartyID, - _access_structure: &WeightedThresholdAccessStructure, + access_structure: &WeightedThresholdAccessStructure, messages: Vec>, _private_input: Option, public_input: &Self::PublicInput, @@ -656,7 +952,7 @@ where Self::Error, > { // ECDSA sign decentralized party is a 2-round protocol (happy-flow). - crate::mock::mock_advance_result(&messages, 2, || { + crate::mock::mock_advance_result(access_structure, &messages, 2, || { let protocol_public_parameters = &*public_input.protocol_public_parameters; let signature = mock_sign::( &public_input.message, @@ -948,7 +1244,7 @@ where fn advance( _session_id: CommitmentSizedNumber, _party_id: PartyID, - _access_structure: &WeightedThresholdAccessStructure, + access_structure: &WeightedThresholdAccessStructure, messages: Vec>, _private_input: Option, public_input: &Self::PublicInput, @@ -959,7 +1255,7 @@ where > { // The fused ECDSA DKG+sign decentralized party shares the sign protocol's 2-round // (happy-flow) structure. - crate::mock::mock_advance_result(&messages, 2, || { + crate::mock::mock_advance_result(access_structure, &messages, 2, || { let protocol_public_parameters = &*public_input.protocol_public_parameters; let dkg_output = crate::mock::dkg::mock_dkg_output::( protocol_public_parameters, @@ -1364,13 +1660,13 @@ where MESSAGE_LIMBS, GroupElement, > as crate::sign::Protocol>::SignMessage; - type SignCentralizedParty = as crate::sign::Protocol>::SignCentralizedParty; + >; /// INSECURE: skip the centralized partial-signature proof verification and return the sign data. /// The mocked decentralized sign ignores this entirely (and under `ToBeEmulated` it is not called). @@ -1401,6 +1697,95 @@ mod tests { use crate::ecdsa::VerifyingKey as _; + /// The mock centralized sign party must produce a structurally-valid `Message` — its + /// `new_default` proofs and dummy fields must construct without error and the message + /// must round-trip through bcs (the wire format the ika layer uses). + #[test] + fn mock_secp256k1_centralized_sign_message_is_structurally_valid() { + use group::OsCsRng; + use mpc::two_party::Round as _; + + use crate::dkg::centralized_party::SecretKeyShare; + use crate::secp256k1::{ + class_groups::FUNDAMENTAL_DISCRIMINANT_LIMBS, + class_groups::NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, MESSAGE_LIMBS, SCALAR_LIMBS, + }; + + let (protocol_public_parameters, _) = crate::test_helpers::setup_class_groups_secp256k1(); + + type MockParty = super::MockSignCentralizedECDSAParty< + { SCALAR_LIMBS }, + { FUNDAMENTAL_DISCRIMINANT_LIMBS }, + { NON_FUNDAMENTAL_DISCRIMINANT_LIMBS }, + { MESSAGE_LIMBS }, + secp256k1::GroupElement, + >; + + let neutral_point = secp256k1::GroupElement::neutral_from_public_parameters( + &protocol_public_parameters.group_public_parameters, + ) + .unwrap() + .value(); + let neutral_scalar = >::Scalar::neutral_from_public_parameters( + &protocol_public_parameters.scalar_group_public_parameters, + ) + .unwrap() + .value(); + let dummy_ciphertext = protocol_public_parameters + .encryption_of_decentralized_party_secret_key_share_first_part; + + let dkg_output = crate::dkg::centralized_party::VersionedOutput::UniversalPublicDKGOutput { + output: crate::dkg::centralized_party::Output { + public_key_share: neutral_point, + public_key: neutral_point, + decentralized_party_public_key_share: neutral_point, + }, + first_key_public_randomizer: crypto_bigint::Uint::ONE, + second_key_public_randomizer: crypto_bigint::Uint::ONE, + free_coefficient_key_public_randomizer: crypto_bigint::Uint::ONE, + global_decentralized_party_output_commitment: protocol_public_parameters + .global_decentralized_party_output_commitment() + .unwrap(), + }; + let presign = crate::ecdsa::presign::VersionedPresign::TargetedPresign( + crate::ecdsa::presign::Presign { + session_id: commitment::CommitmentSizedNumber::from(42u64), + encryption_of_mask: dummy_ciphertext, + encryption_of_masked_decentralized_party_key_share: dummy_ciphertext, + encryption_of_masked_decentralized_party_nonce_share_first_part: dummy_ciphertext, + encryption_of_masked_decentralized_party_nonce_share_second_part: dummy_ciphertext, + decentralized_party_nonce_public_share_first_part: neutral_point, + decentralized_party_nonce_public_share_second_part: neutral_point, + public_key: neutral_point, + }, + ); + + let public_input = + crate::ecdsa::sign::centralized_party::signature_homomorphic_evaluation_round::PublicInput { + message: b"mock centralized sign".to_vec(), + hash_type: HashScheme::SHA256, + hash_context: HashContext::None, + dkg_output, + presign, + protocol_public_parameters, + }; + + let round_result = MockParty::advance( + (), + &SecretKeyShare::from(neutral_scalar), + &public_input, + &mut OsCsRng, + ) + .expect("mock centralized ECDSA sign advance should succeed"); + + let serialized = bcs::to_bytes(&round_result.outgoing_message).unwrap(); + let deserialized: ::OutgoingMessage = + bcs::from_bytes(&serialized).expect("mock sign message must bcs round-trip"); + assert_eq!(deserialized, round_result.outgoing_message); + } + /// The mock's deterministic ECDSA signature must verify under the constant mock public key `42·G`. #[test] fn mock_secp256k1_ecdsa_signature_verifies() { diff --git a/2pc-mpc/src/mock/network_dkg.rs b/2pc-mpc/src/mock/network_dkg.rs index 01e6dcf..98be4dd 100644 --- a/2pc-mpc/src/mock/network_dkg.rs +++ b/2pc-mpc/src/mock/network_dkg.rs @@ -369,7 +369,7 @@ impl AsynchronouslyAdvanceable for MockNetworkDKGParty { fn advance( _session_id: CommitmentSizedNumber, _tangible_party_id: PartyID, - _access_structure: &WeightedThresholdAccessStructure, + access_structure: &WeightedThresholdAccessStructure, messages: Vec>, _private_input: Option, _public_input: &Self::PublicInput, @@ -379,7 +379,7 @@ impl AsynchronouslyAdvanceable for MockNetworkDKGParty { Self::Error, > { // The network-DKG decentralized party is a 7-round protocol. - crate::mock::mock_advance_result(&messages, 7, || { + crate::mock::mock_advance_result(access_structure, &messages, 7, || { Ok(((), mock_network_dkg_public_output().clone())) }) } @@ -422,7 +422,7 @@ impl AsynchronouslyAdvanceable for MockNetworkReconfigurationParty { Self::Error, > { // The network-reconfiguration decentralized party is a 4-round protocol. - crate::mock::mock_advance_result(&messages, 4, || { + crate::mock::mock_advance_result(current_access_structure, &messages, 4, || { Ok(( (), mock_network_reconfiguration_public_output(current_access_structure), @@ -466,7 +466,7 @@ impl AsynchronouslyAdvanceable for MockNetworkDKGPartyV2 { fn advance( _session_id: CommitmentSizedNumber, _tangible_party_id: PartyID, - _access_structure: &WeightedThresholdAccessStructure, + access_structure: &WeightedThresholdAccessStructure, messages: Vec>, _private_input: Option, _public_input: &Self::PublicInput, @@ -476,7 +476,7 @@ impl AsynchronouslyAdvanceable for MockNetworkDKGPartyV2 { Self::Error, > { // The backward-compatible (V2) network-DKG decentralized party is a 4-round protocol. - crate::mock::mock_advance_result(&messages, 4, || { + crate::mock::mock_advance_result(access_structure, &messages, 4, || { Ok(((), mock_network_dkg_public_output().core.clone())) }) } @@ -521,7 +521,7 @@ impl AsynchronouslyAdvanceable for MockNetworkReconfigurationPartyV2 { > { // The backward-compatible (V2) network-reconfiguration decentralized party is a 4-round // protocol. - crate::mock::mock_advance_result(&messages, 4, || { + crate::mock::mock_advance_result(current_access_structure, &messages, 4, || { Ok(( (), mock_network_reconfiguration_public_output(current_access_structure).core, diff --git a/2pc-mpc/src/mock/schnorr.rs b/2pc-mpc/src/mock/schnorr.rs index a242eae..2f4774c 100644 --- a/2pc-mpc/src/mock/schnorr.rs +++ b/2pc-mpc/src/mock/schnorr.rs @@ -117,9 +117,10 @@ type RealPresignAsyncSchnorrParty< GroupElement, >; -/// INSECURE mock of the Schnorr-AHE presign party — finalizes round 1 with a single dummy presign -/// (identity nonce points, reused ciphertexts, dummy commitment); the mocked constant-nonce sign -/// ignores the presign entirely. +/// INSECURE mock of the Schnorr-AHE presign party — simulates the real 2-round structure and +/// finalizes with a single dummy presign (identity nonce points, reused ciphertexts, and the +/// pp-derived global-output commitment, which the real centralized sign party validates); the +/// mocked constant-nonce sign ignores the presign entirely. pub struct MockPresignAsyncSchnorrParty< const SCALAR_LIMBS: usize, const FUNDAMENTAL_DISCRIMINANT_LIMBS: usize, @@ -343,7 +344,7 @@ where fn advance( session_id: CommitmentSizedNumber, _party_id: PartyID, - _access_structure: &WeightedThresholdAccessStructure, + access_structure: &WeightedThresholdAccessStructure, messages: Vec>, _private_input: Option, public_input: &Self::PublicInput, @@ -353,7 +354,7 @@ where Self::Error, > { // Schnorr-AHE presign decentralized party is a 2-round protocol. - crate::mock::mock_advance_result(&messages, 2, || { + crate::mock::mock_advance_result(access_structure, &messages, 2, || { let protocol_public_parameters = &*public_input.protocol_public_parameters; let identity_point = GroupElement::neutral_from_public_parameters( &protocol_public_parameters.group_public_parameters, @@ -367,6 +368,10 @@ where encryption_of_decentralized_party_nonce_share_second_part: dummy_ciphertext, decentralized_party_nonce_public_share_first_part: identity_point.clone(), decentralized_party_nonce_public_share_second_part: identity_point, + // A dummy is fine: the only consumer that validates this field against the + // pp is the real centralized sign party, and under the mock the + // centralized sign party is mocked too (see + // [`MockSignCentralizedSchnorrParty`]). global_decentralized_party_output_commitment: CommitmentSizedNumber::ZERO, }; Ok(((), vec![presign])) @@ -378,6 +383,143 @@ where } } +// =================================================================================================== +// Schnorr-AHE centralized (user-side) sign +// =================================================================================================== + +/// INSECURE mock of the Schnorr-AHE centralized (user-side) sign party — returns a +/// deterministic, structurally-valid partial signature (neutral nonce point, zero +/// response) and performs no cryptography at all: no AHE evaluation, no proof +/// generation, and no input-consistency checks. The real user side is expensive +/// (class-group homomorphic evaluation over the presign ciphertexts), which is exactly +/// the cost the mock exists to eliminate. The mocked decentralized sign ignores the +/// partial signature (it emits the constant-nonce signature), and the mocked +/// `verify_centralized_party_partial_signature` accepts it unchanged. +pub struct MockSignCentralizedSchnorrParty< + const SCALAR_LIMBS: usize, + const FUNDAMENTAL_DISCRIMINANT_LIMBS: usize, + const NON_FUNDAMENTAL_DISCRIMINANT_LIMBS: usize, + GroupElement, +>(PhantomData); + +impl< + const SCALAR_LIMBS: usize, + const FUNDAMENTAL_DISCRIMINANT_LIMBS: usize, + const NON_FUNDAMENTAL_DISCRIMINANT_LIMBS: usize, + GroupElement: VerifyingKey + Copy, + > mpc::two_party::Round + for MockSignCentralizedSchnorrParty< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + GroupElement, + > +where + Int: Encoding, + Uint: Encoding, + Int: Encoding, + Uint: Encoding, + Int: Encoding, + Uint: Encoding, + EquivalenceClass: group::GroupElement< + Value = CompactIbqf, + PublicParameters = equivalence_class::PublicParameters< + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + >, + > + EquivalenceClassOps< + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + MultiFoldNupowAccelerator = MultiFoldNupowAccelerator< + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + >, + >, + EncryptionKey< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + GroupElement, + >: AdditivelyHomomorphicEncryptionKey< + SCALAR_LIMBS, + PublicParameters = encryption_key::PublicParameters< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + group::PublicParameters, + >, + PlaintextSpaceGroupElement = GroupElement::Scalar, + RandomnessSpaceGroupElement = RandomnessSpaceGroupElement, + CiphertextSpaceGroupElement = CiphertextSpaceGroupElement< + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + >, + >, + encryption_key::PublicParameters< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + group::PublicParameters, + >: AsRef< + homomorphic_encryption::GroupsPublicParameters< + group::PublicParameters, + RandomnessSpacePublicParameters, + CiphertextSpacePublicParameters, + >, + >, +{ + type Error = crate::Error; + type PrivateInput = + crate::dkg::centralized_party::SecretKeyShare>; + type PublicInput = crate::schnorr::ahe::sign::centralized_party::PublicInput< + crate::dkg::centralized_party::VersionedOutput, + crate::schnorr::ahe::presign::Presign< + GroupElement::Value, + group::Value>, + >, + ProtocolPublicParameters< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + GroupElement, + >, + >; + type PrivateOutput = (); + type PublicOutputValue = (); + type PublicOutput = (); + type IncomingMessage = (); + type OutgoingMessage = + PartialSignature>; + + fn advance( + _message: Self::IncomingMessage, + _secret_key_share: &Self::PrivateInput, + public_input: &Self::PublicInput, + _rng: &mut impl CsRng, + ) -> std::result::Result< + mpc::two_party::RoundResult, + Self::Error, + > { + let public_nonce_share_prenormalization = GroupElement::neutral_from_public_parameters( + &public_input + .protocol_public_parameters + .group_public_parameters, + )? + .value(); + let partial_response = GroupElement::Scalar::neutral_from_public_parameters( + &public_input + .protocol_public_parameters + .scalar_group_public_parameters, + )? + .value(); + + Ok(mpc::two_party::RoundResult { + outgoing_message: PartialSignature { + public_nonce_share_prenormalization, + partial_response, + }, + private_output: (), + public_output: (), + }) + } +} + // =================================================================================================== // Schnorr-AHE sign // =================================================================================================== @@ -696,7 +838,7 @@ where fn advance( _session_id: CommitmentSizedNumber, _party_id: PartyID, - _access_structure: &WeightedThresholdAccessStructure, + access_structure: &WeightedThresholdAccessStructure, messages: Vec>, _private_input: Option, public_input: &Self::PublicInput, @@ -706,7 +848,7 @@ where Self::Error, > { // Schnorr-AHE sign decentralized party is a 2-round protocol (happy-flow). - crate::mock::mock_advance_result(&messages, 2, || { + crate::mock::mock_advance_result(access_structure, &messages, 2, || { let protocol_public_parameters = &*public_input.protocol_public_parameters; let signature = mock_sign::( &public_input.message, @@ -1017,7 +1159,7 @@ where fn advance( _session_id: CommitmentSizedNumber, _party_id: PartyID, - _access_structure: &WeightedThresholdAccessStructure, + access_structure: &WeightedThresholdAccessStructure, messages: Vec>, _private_input: Option, public_input: &Self::PublicInput, @@ -1028,7 +1170,7 @@ where > { // The fused Schnorr DKG+sign decentralized party shares the sign protocol's 2-round // (happy-flow) structure. - crate::mock::mock_advance_result(&messages, 2, || { + crate::mock::mock_advance_result(access_structure, &messages, 2, || { let protocol_public_parameters = &*public_input.protocol_public_parameters; let dkg_output = crate::mock::dkg::mock_dkg_output::( protocol_public_parameters, @@ -1431,13 +1573,12 @@ where MESSAGE_LIMBS, GroupElement, > as crate::sign::Protocol>::SignMessage; - type SignCentralizedParty = as crate::sign::Protocol>::SignCentralizedParty; + >; /// INSECURE: skip the centralized partial-signature verification and return it unchanged. The /// mocked decentralized sign ignores this entirely (and under `ToBeEmulated` it is not called). diff --git a/2pc-mpc/src/mock/timings.rs b/2pc-mpc/src/mock/timings.rs index cb06a61..b07128c 100644 --- a/2pc-mpc/src/mock/timings.rs +++ b/2pc-mpc/src/mock/timings.rs @@ -154,9 +154,13 @@ fn mock_protocol_timings_report() { dkg_output: None, protocol_public_parameters: Arc::new(protocol_public_parameters.clone()), }; - // ECDSA presign is a 4-round protocol; drive the final round (three empty prior-round message - // maps) so the timed `advance` performs the presign output construction rather than the - // round-simulation bookkeeping of an intermediate round. + // ECDSA presign is a 4-round protocol; drive the final round (three prior rounds of + // all-honest message maps — the mock advance enforces the threshold over the latest + // round's honest senders) so the timed `advance` performs the presign output + // construction rather than the round-simulation bookkeeping of an intermediate round. + let honest_round: HashMap = (1..=4) + .map(|party_id| (party_id as PartyID, crate::mock::MOCK_HONEST_MESSAGE)) + .collect(); report( "ECDSA presign (secp256k1): full party advance (finalize final round)", 200, @@ -165,7 +169,11 @@ fn mock_protocol_timings_report() { session_id, 1 as PartyID, &access_structure, - vec![HashMap::new(), HashMap::new(), HashMap::new()], + vec![ + honest_round.clone(), + honest_round.clone(), + honest_round.clone(), + ], Some(()), &presign_public_input, &mut OsCsRng, diff --git a/2pc-mpc/src/mock/vss.rs b/2pc-mpc/src/mock/vss.rs index bd7cf32..e8b0ff9 100644 --- a/2pc-mpc/src/mock/vss.rs +++ b/2pc-mpc/src/mock/vss.rs @@ -35,7 +35,7 @@ use ::class_groups::{ use crate::class_groups::ProtocolPublicParameters; use crate::mock::schnorr::mock_sign; use crate::schnorr::vss::presign::{Presign, PrivatePresignOutput}; -use crate::schnorr::VerifyingKey; +use crate::schnorr::{PartialSignature, VerifyingKey}; // =================================================================================================== // Schnorr-VSS presign @@ -271,7 +271,7 @@ where Self::Error, > { // Schnorr-VSS presign decentralized party is a 3-round protocol. - crate::mock::mock_advance_result(&messages, 3, || { + crate::mock::mock_advance_result(access_structure, &messages, 3, || { let protocol_public_parameters = &*public_input.protocol_public_parameters; let identity_point = GroupElement::neutral_from_public_parameters( &protocol_public_parameters.group_public_parameters, @@ -316,6 +316,138 @@ where } } +// =================================================================================================== +// Schnorr-VSS centralized (user-side) sign +// =================================================================================================== + +/// INSECURE mock of the Schnorr-VSS centralized (user-side) sign party — returns a +/// deterministic, structurally-valid partial signature (neutral nonce point, zero +/// response) and performs no cryptography at all: no signing math, no input-consistency +/// checks. The mocked decentralized sign ignores the partial signature (it emits the +/// constant-nonce signature), and the mocked +/// `verify_centralized_party_partial_signature` accepts it unchanged. +pub struct MockSignCentralizedVSSParty< + const SCALAR_LIMBS: usize, + const FUNDAMENTAL_DISCRIMINANT_LIMBS: usize, + const NON_FUNDAMENTAL_DISCRIMINANT_LIMBS: usize, + GroupElement, +>(PhantomData); + +impl< + const SCALAR_LIMBS: usize, + const FUNDAMENTAL_DISCRIMINANT_LIMBS: usize, + const NON_FUNDAMENTAL_DISCRIMINANT_LIMBS: usize, + GroupElement: VerifyingKey + Copy, + > mpc::two_party::Round + for MockSignCentralizedVSSParty< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + GroupElement, + > +where + Int: Encoding, + Uint: Encoding, + Int: Encoding, + Uint: Encoding, + Int: Encoding, + Uint: Encoding, + EquivalenceClass: group::GroupElement< + Value = CompactIbqf, + PublicParameters = equivalence_class::PublicParameters< + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + >, + > + EquivalenceClassOps< + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + MultiFoldNupowAccelerator = MultiFoldNupowAccelerator< + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + >, + >, + EncryptionKey< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + GroupElement, + >: AdditivelyHomomorphicEncryptionKey< + SCALAR_LIMBS, + PublicParameters = encryption_key::PublicParameters< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + group::PublicParameters, + >, + PlaintextSpaceGroupElement = GroupElement::Scalar, + RandomnessSpaceGroupElement = RandomnessSpaceGroupElement, + CiphertextSpaceGroupElement = CiphertextSpaceGroupElement< + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + >, + >, + encryption_key::PublicParameters< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + group::PublicParameters, + >: AsRef< + homomorphic_encryption::GroupsPublicParameters< + group::PublicParameters, + RandomnessSpacePublicParameters, + CiphertextSpacePublicParameters, + >, + >, +{ + type Error = crate::Error; + type PrivateInput = + crate::dkg::centralized_party::SecretKeyShare>; + type PublicInput = crate::schnorr::vss::sign::centralized_party::PublicInput< + crate::dkg::centralized_party::VersionedOutput, + Presign, + ProtocolPublicParameters< + SCALAR_LIMBS, + FUNDAMENTAL_DISCRIMINANT_LIMBS, + NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, + GroupElement, + >, + >; + type PrivateOutput = (); + type PublicOutputValue = (); + type PublicOutput = (); + type IncomingMessage = (); + type OutgoingMessage = + PartialSignature>; + + fn advance( + _message: Self::IncomingMessage, + _secret_key_share: &Self::PrivateInput, + public_input: &Self::PublicInput, + _rng: &mut impl CsRng, + ) -> std::result::Result< + mpc::two_party::RoundResult, + Self::Error, + > { + let public_nonce_share_prenormalization = GroupElement::neutral_from_public_parameters( + &public_input + .protocol_public_parameters + .group_public_parameters, + )? + .value(); + let partial_response = GroupElement::Scalar::neutral_from_public_parameters( + &public_input + .protocol_public_parameters + .scalar_group_public_parameters, + )? + .value(); + + Ok(mpc::two_party::RoundResult { + outgoing_message: PartialSignature { + public_nonce_share_prenormalization, + partial_response, + }, + private_output: (), + public_output: (), + }) + } +} + // =================================================================================================== // Schnorr-VSS sign // =================================================================================================== @@ -569,7 +701,7 @@ where fn advance( _session_id: CommitmentSizedNumber, _party_id: PartyID, - _access_structure: &WeightedThresholdAccessStructure, + access_structure: &WeightedThresholdAccessStructure, messages: Vec>, _private_input: Option, public_input: &Self::PublicInput, @@ -579,7 +711,7 @@ where Self::Error, > { // Schnorr-VSS sign decentralized party is a 2-round protocol (happy-flow). - crate::mock::mock_advance_result(&messages, 2, || { + crate::mock::mock_advance_result(access_structure, &messages, 2, || { let protocol_public_parameters = &*public_input.protocol_public_parameters; let signature = mock_sign::( &public_input.message, @@ -871,7 +1003,7 @@ where fn advance( _session_id: CommitmentSizedNumber, _party_id: PartyID, - _access_structure: &WeightedThresholdAccessStructure, + access_structure: &WeightedThresholdAccessStructure, messages: Vec>, _private_input: Option, public_input: &Self::PublicInput, @@ -882,7 +1014,7 @@ where > { // The fused Schnorr-VSS DKG+sign decentralized party shares the sign protocol's 2-round // (happy-flow) structure. - crate::mock::mock_advance_result(&messages, 2, || { + crate::mock::mock_advance_result(access_structure, &messages, 2, || { let protocol_public_parameters = &*public_input.protocol_public_parameters; let dkg_output = crate::mock::dkg::mock_dkg_output::( protocol_public_parameters, @@ -1231,12 +1363,12 @@ where NON_FUNDAMENTAL_DISCRIMINANT_LIMBS, GroupElement, > as crate::sign::Protocol>::SignMessage; - type SignCentralizedParty = as crate::sign::Protocol>::SignCentralizedParty; + >; /// INSECURE: skip the centralized partial-signature verification and return it unchanged. The /// mocked decentralized sign ignores this entirely (and under `ToBeEmulated` it is not called).