def test_reset(self): # Arrange mediator = KeyCeremonyMediator("mediator_reset", CEREMONY_DETAILS) new_ceremony_details = CeremonyDetails(3, 3) mediator.reset(new_ceremony_details) self.assertEqual(mediator.ceremony_details, new_ceremony_details)
def __init__(self, manifest_file: str, manifest_path: str, guardians: int, quorom: int): self.NUMBER_OF_GUARDIANS = guardians self.QUORUM = quorom self.manifest_path = manifest_path self.manifest_file = manifest_file self.guardians: List[Guardian] = [] self.key_ceremony_mediator = KeyCeremonyMediator( CeremonyDetails(self.NUMBER_OF_GUARDIANS, self.QUORUM))
def get_hamilton_manifest_with_encryption_context( self, ) -> Tuple[AllPublicElectionData, AllPrivateElectionData]: guardians: List[Guardian] = [] guardian_records: List[GuardianRecord] = [] # Configure the election builder manifest = self.get_hamilton_manifest_from_file() builder = ElectionBuilder(NUMBER_OF_GUARDIANS, QUORUM, manifest) # Run the Key Ceremony ceremony_details = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM) guardians = KeyCeremonyHelper.create_guardians(ceremony_details) mediator = KeyCeremonyMediator("key-ceremony-mediator", ceremony_details) KeyCeremonyHelper.perform_full_ceremony(guardians, mediator) # Final: Joint Key joint_key = mediator.publish_joint_key() # Publish Guardian Records guardian_records = [guardian.publish() for guardian in guardians] builder.set_public_key(get_optional(joint_key).joint_public_key) builder.set_commitment_hash(get_optional(joint_key).commitment_hash) internal_manifest, context = get_optional(builder.build()) constants = ElectionConstants() return ( AllPublicElectionData( manifest, internal_manifest, context, constants, guardian_records, ), AllPrivateElectionData(guardians), )
class TestDecryptionMediator(TestCase): NUMBER_OF_GUARDIANS = 3 QUORUM = 2 CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM) def setUp(self): self.key_ceremony = KeyCeremonyMediator(self.CEREMONY_DETAILS) self.guardians: List[Guardian] = [] # Setup Guardians for i in range(self.NUMBER_OF_GUARDIANS): sequence = i + 2 self.guardians.append( Guardian( "guardian_" + str(sequence), sequence, self.NUMBER_OF_GUARDIANS, self.QUORUM, )) # Attendance (Public Key Share) for guardian in self.guardians: self.key_ceremony.announce(guardian) self.key_ceremony.orchestrate(identity_auxiliary_encrypt) self.key_ceremony.verify(identity_auxiliary_decrypt) self.joint_public_key = self.key_ceremony.publish_joint_key() self.assertIsNotNone(self.joint_public_key) # setup the election self.election = election_factory.get_fake_election() builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, self.election) self.assertIsNone( builder.build()) # Can't build without the public key builder.set_public_key(self.joint_public_key) self.metadata, self.context = get_optional(builder.build()) self.encryption_device = EncryptionDevice("location") self.ballot_marking_device = EncryptionMediator( self.metadata, self.context, self.encryption_device) # get some fake ballots self.fake_cast_ballot = ballot_factory.get_fake_ballot( self.metadata, "some-unique-ballot-id-cast") self.more_fake_ballots = [] for i in range(10): self.more_fake_ballots.append( ballot_factory.get_fake_ballot( self.metadata, f"some-unique-ballot-id-cast{i}")) self.fake_spoiled_ballot = ballot_factory.get_fake_ballot( self.metadata, "some-unique-ballot-id-spoiled") self.assertTrue( self.fake_cast_ballot.is_valid( self.metadata.ballot_styles[0].object_id)) self.assertTrue( self.fake_spoiled_ballot.is_valid( self.metadata.ballot_styles[0].object_id)) self.expected_plaintext_tally = accumulate_plaintext_ballots( [self.fake_cast_ballot] + self.more_fake_ballots) # Fill in the expected values with any missing selections # that were not made on any ballots selection_ids = set([ selection.object_id for contest in self.metadata.contests for selection in contest.ballot_selections ]) missing_selection_ids = selection_ids.difference( set(self.expected_plaintext_tally)) for id in missing_selection_ids: self.expected_plaintext_tally[id] = 0 # Encrypt encrypted_fake_cast_ballot = self.ballot_marking_device.encrypt( self.fake_cast_ballot) encrypted_fake_spoiled_ballot = self.ballot_marking_device.encrypt( self.fake_spoiled_ballot) self.assertIsNotNone(encrypted_fake_cast_ballot) self.assertIsNotNone(encrypted_fake_spoiled_ballot) self.assertTrue( encrypted_fake_cast_ballot.is_valid_encryption( self.context.crypto_extended_base_hash, self.joint_public_key)) # encrypt some more fake ballots self.more_fake_encrypted_ballots = [] for fake_ballot in self.more_fake_ballots: self.more_fake_encrypted_ballots.append( self.ballot_marking_device.encrypt(fake_ballot)) # configure the ballot box ballot_store = BallotStore() ballot_box = BallotBox(self.metadata, self.context, ballot_store) ballot_box.cast(encrypted_fake_cast_ballot) ballot_box.spoil(encrypted_fake_spoiled_ballot) # Cast some more fake ballots for fake_ballot in self.more_fake_encrypted_ballots: ballot_box.cast(fake_ballot) # generate encrypted tally self.ciphertext_tally = tally_ballots(ballot_store, self.metadata, self.context) def tearDown(self): self.key_ceremony.reset( CeremonyDetails(self.NUMBER_OF_GUARDIANS, self.QUORUM)) def test_announce(self): # Arrange subject = DecryptionMediator(self.metadata, self.context, self.ciphertext_tally) # act result = subject.announce(self.guardians[0]) # assert self.assertIsNotNone(result) # Can only announce once self.assertIsNotNone(subject.announce(self.guardians[0])) # Cannot submit another share internally self.assertFalse( subject._submit_decryption_share( TallyDecryptionShare(self.guardians[0].object_id, ZERO_MOD_P, {}, {}))) # Cannot get plaintext tally without a quorum self.assertIsNone(subject.get_plaintext_tally()) def test_compute_selection(self): # Arrange first_selection = [ selection for contest in self.ciphertext_tally.cast.values() for selection in contest.tally_selections.values() ][0] # act result = compute_decryption_share_for_selection( self.guardians[0], first_selection, self.context) # assert self.assertIsNotNone(result) def test_compute_compensated_selection_failure(self): # Arrange first_selection = [ selection for contest in self.ciphertext_tally.cast.values() for selection in contest.tally_selections.values() ][0] # Act self.guardians[0]._guardian_election_partial_key_backups.pop( self.guardians[2].object_id) self.assertIsNone(self.guardians[0].recovery_public_key_for( self.guardians[2].object_id)) result = compute_compensated_decryption_share_for_selection( self.guardians[0], self.guardians[2].object_id, first_selection, self.context, identity_auxiliary_decrypt, ) # Assert self.assertIsNone(result) def test_compute_compensated_selection(self): """ demonstrates the complete workflow for computing a comepnsated decryption share For one selection. It is useful for verifying that the workflow is correct """ # Arrange first_selection = [ selection for contest in self.ciphertext_tally.cast.values() for selection in contest.tally_selections.values() ][0] # Compute lagrange coefficients for the guardians that are present lagrange_0 = compute_lagrange_coefficient( self.guardians[0].sequence_order, *[self.guardians[1].sequence_order], ) lagrange_1 = compute_lagrange_coefficient( self.guardians[1].sequence_order, *[self.guardians[0].sequence_order], ) print( f"lagrange: sequence_orders: ({self.guardians[0].sequence_order}, {self.guardians[1].sequence_order}, {self.guardians[2].sequence_order})\n" ) print(lagrange_0) print(lagrange_1) # compute their shares share_0 = compute_decryption_share_for_selection( self.guardians[0], first_selection, self.context) share_1 = compute_decryption_share_for_selection( self.guardians[1], first_selection, self.context) self.assertIsNotNone(share_0) self.assertIsNotNone(share_1) # compute compensations shares for the missing guardian compensation_0 = compute_compensated_decryption_share_for_selection( self.guardians[0], self.guardians[2].object_id, first_selection, self.context, identity_auxiliary_decrypt, ) compensation_1 = compute_compensated_decryption_share_for_selection( self.guardians[1], self.guardians[2].object_id, first_selection, self.context, identity_auxiliary_decrypt, ) self.assertIsNotNone(compensation_0) self.assertIsNotNone(compensation_1) print("\nSHARES:") print(compensation_0) print(compensation_1) # Check the share proofs self.assertTrue( compensation_0.proof.is_valid( first_selection.message, get_optional(self.guardians[0].recovery_public_key_for( self.guardians[2].object_id)), compensation_0.share, self.context.crypto_extended_base_hash, )) self.assertTrue( compensation_1.proof.is_valid( first_selection.message, get_optional(self.guardians[1].recovery_public_key_for( self.guardians[2].object_id)), compensation_1.share, self.context.crypto_extended_base_hash, )) share_pow_p = [ pow_p(compensation_0.share, lagrange_0), pow_p(compensation_1.share, lagrange_1), ] print("\nSHARE_POW_P") print(share_pow_p) # reconstruct the missing share from the compensation shares reconstructed_share = mult_p(*[ pow_p(compensation_0.share, lagrange_0), pow_p(compensation_1.share, lagrange_1), ]) print("\nRECONSTRUCTED SHARE\n") print(reconstructed_share) share_2 = CiphertextDecryptionSelection( first_selection.object_id, self.guardians[2].object_id, first_selection.description_hash, reconstructed_share, { self.guardians[0].object_id: compensation_0, self.guardians[1].object_id: compensation_1, }, ) # Decrypt the result result = decrypt_selection_with_decryption_shares( first_selection, { self.guardians[0].object_id: ( self.guardians[0].share_election_public_key().key, share_0, ), self.guardians[1].object_id: ( self.guardians[1].share_election_public_key().key, share_1, ), self.guardians[2].object_id: ( self.guardians[2].share_election_public_key().key, share_2, ), }, self.context.crypto_extended_base_hash, ) print(result) self.assertIsNotNone(result) self.assertEqual( result.plaintext, self.expected_plaintext_tally[first_selection.object_id]) def test_decrypt_selection_all_present(self): # Arrange # find the first selection first_contest = [ contest for contest in self.ciphertext_tally.cast.values() ][0] first_selection = list(first_contest.tally_selections.values())[0] # precompute decryption shares for the guardians first_share = compute_decryption_share(self.guardians[0], self.ciphertext_tally, self.context) second_share = compute_decryption_share(self.guardians[1], self.ciphertext_tally, self.context) third_share = compute_decryption_share(self.guardians[2], self.ciphertext_tally, self.context) # build type: Dict[GUARDIAN_ID, Tuple[ELECTION_PUBLIC_KEY, TallyDecryptionShare]] shares = { self.guardians[0].object_id: ( self.guardians[0].share_election_public_key().key, first_share.contests[first_contest.object_id].selections[ first_selection.object_id], ), self.guardians[1].object_id: ( self.guardians[1].share_election_public_key().key, second_share.contests[first_contest.object_id].selections[ first_selection.object_id], ), self.guardians[2].object_id: ( self.guardians[2].share_election_public_key().key, third_share.contests[first_contest.object_id].selections[ first_selection.object_id], ), } # act result = decrypt_selection_with_decryption_shares( first_selection, shares, self.context.crypto_extended_base_hash) # assert self.assertIsNotNone(result) self.assertEqual( self.expected_plaintext_tally[first_selection.object_id], result.plaintext) def test_decrypt_spoiled_ballots_all_guardians_present(self): # Arrange # precompute decryption shares for the guardians first_share = compute_decryption_share(self.guardians[0], self.ciphertext_tally, self.context) second_share = compute_decryption_share(self.guardians[1], self.ciphertext_tally, self.context) third_share = compute_decryption_share(self.guardians[2], self.ciphertext_tally, self.context) shares = { self.guardians[0].object_id: first_share, self.guardians[1].object_id: second_share, self.guardians[2].object_id: third_share, } subject = DecryptionMediator(self.metadata, self.context, self.ciphertext_tally) # act result = decrypt_spoiled_ballots( self.ciphertext_tally.spoiled_ballots, shares, self.context.crypto_extended_base_hash, ) # assert self.assertIsNotNone(result) self.assertTrue(self.fake_spoiled_ballot.object_id in result) spoiled_ballot = result[self.fake_spoiled_ballot.object_id] for contest in self.fake_spoiled_ballot.contests: for selection in contest.ballot_selections: self.assertEqual( spoiled_ballot[contest.object_id].selections[ selection.object_id].plaintext, result[self.fake_spoiled_ballot.object_id][ contest.object_id].selections[ selection.object_id].plaintext, ) def test_get_plaintext_tally_all_guardians_present_simple(self): # Arrange subject = DecryptionMediator(self.metadata, self.context, self.ciphertext_tally) # act for guardian in self.guardians: self.assertIsNotNone(subject.announce(guardian)) decrypted_tallies = subject.get_plaintext_tally() result = self._convert_to_selections(decrypted_tallies) # assert self.assertIsNotNone(result) self.assertEqual(self.expected_plaintext_tally, result) # Verify we get the same tally back if we call again another_decrypted_tally = subject.get_plaintext_tally() self.assertEqual(decrypted_tallies, another_decrypted_tally) def test_get_plaintext_tally_compensate_missing_guardian_simple(self): # Arrange subject = DecryptionMediator(self.metadata, self.context, self.ciphertext_tally) # Act self.assertIsNotNone(subject.announce(self.guardians[0])) self.assertIsNotNone(subject.announce(self.guardians[1])) # explicitly compensate to demonstrate that this is possible, but not required self.assertIsNotNone( subject.compensate(self.guardians[2].object_id, identity_auxiliary_decrypt)) decrypted_tallies = subject.get_plaintext_tally() self.assertIsNotNone(decrypted_tallies) result = self._convert_to_selections(decrypted_tallies) # assert self.assertIsNotNone(result) print(result) self.assertEqual(self.expected_plaintext_tally, result) @settings( deadline=timedelta(milliseconds=15000), suppress_health_check=[HealthCheck.too_slow], max_examples=8, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given(data(), integers(1, 3), integers(2, 5)) def test_get_plaintext_tally_all_guardians_present(self, data, parties: int, contests: int): # Arrange description = data.draw(election_descriptions(parties, contests)) builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, description) metadata, context = builder.set_public_key( self.joint_public_key).build() plaintext_ballots: List[PlaintextBallot] = data.draw( plaintext_voted_ballots(metadata, randrange(3, 6))) plaintext_tallies = accumulate_plaintext_ballots(plaintext_ballots) encrypted_tally = self._generate_encrypted_tally( metadata, context, plaintext_ballots) subject = DecryptionMediator(metadata, context, encrypted_tally) # act for guardian in self.guardians: self.assertIsNotNone(subject.announce(guardian)) decrypted_tallies = subject.get_plaintext_tally() result = self._convert_to_selections(decrypted_tallies) # assert self.assertIsNotNone(result) self.assertEqual(plaintext_tallies, result) def _generate_encrypted_tally( self, metadata: InternalElectionDescription, context: CiphertextElectionContext, ballots: List[PlaintextBallot], ) -> CiphertextTally: # encrypt each ballot store = BallotStore() for ballot in ballots: encrypted_ballot = encrypt_ballot(ballot, metadata, context, int_to_q_unchecked(1)) self.assertIsNotNone(encrypted_ballot) # add to the ballot store store.set( encrypted_ballot.object_id, from_ciphertext_ballot(encrypted_ballot, BallotBoxState.CAST), ) tally = tally_ballots(store, metadata, context) self.assertIsNotNone(tally) return get_optional(tally) def _convert_to_selections(self, tally: PlaintextTally) -> Dict[str, int]: plaintext_selections: Dict[str, int] = {} for _, contest in tally.contests.items(): for selection_id, selection in contest.selections.items(): plaintext_selections[selection_id] = selection.plaintext return plaintext_selections
def tearDown(self): self.key_ceremony.reset( CeremonyDetails(self.NUMBER_OF_GUARDIANS, self.QUORUM))
class TestDecryptWithShares(TestCase): """Test decrypt with shares methods""" NUMBER_OF_GUARDIANS = 3 QUORUM = 2 CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM) def setUp(self): # Key Ceremony self.key_ceremony_mediator = KeyCeremonyMediator( "key_ceremony_mediator_mediator", self.CEREMONY_DETAILS) self.guardians: List[Guardian] = KeyCeremonyHelper.create_guardians( self.CEREMONY_DETAILS) KeyCeremonyHelper.perform_full_ceremony(self.guardians, self.key_ceremony_mediator) self.joint_public_key = self.key_ceremony_mediator.publish_joint_key() # Setup the election manifest = election_factory.get_fake_manifest() builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, manifest) builder.set_public_key(self.joint_public_key.joint_public_key) builder.set_commitment_hash(self.joint_public_key.commitment_hash) self.internal_manifest, self.context = get_optional(builder.build()) self.encryption_device = election_factory.get_encryption_device() self.ballot_marking_device = EncryptionMediator( self.internal_manifest, self.context, self.encryption_device) # get some fake ballots self.fake_cast_ballot = ballot_factory.get_fake_ballot( self.internal_manifest, "some-unique-ballot-id-cast") self.more_fake_ballots = [] for i in range(10): self.more_fake_ballots.append( ballot_factory.get_fake_ballot( self.internal_manifest, f"some-unique-ballot-id-cast{i}")) self.fake_spoiled_ballot = ballot_factory.get_fake_ballot( self.internal_manifest, "some-unique-ballot-id-spoiled") self.more_fake_spoiled_ballots = [] for i in range(2): self.more_fake_spoiled_ballots.append( ballot_factory.get_fake_ballot( self.internal_manifest, f"some-unique-ballot-id-spoiled{i}")) self.assertTrue( self.fake_cast_ballot.is_valid( self.internal_manifest.ballot_styles[0].object_id)) self.assertTrue( self.fake_spoiled_ballot.is_valid( self.internal_manifest.ballot_styles[0].object_id)) self.expected_plaintext_tally = accumulate_plaintext_ballots( [self.fake_cast_ballot] + self.more_fake_ballots) # Fill in the expected values with any missing selections # that were not made on any ballots selection_ids = { selection.object_id for contest in self.internal_manifest.contests for selection in contest.ballot_selections } missing_selection_ids = selection_ids.difference( set(self.expected_plaintext_tally)) for id in missing_selection_ids: self.expected_plaintext_tally[id] = 0 # Encrypt self.encrypted_fake_cast_ballot = self.ballot_marking_device.encrypt( self.fake_cast_ballot) self.encrypted_fake_spoiled_ballot = self.ballot_marking_device.encrypt( self.fake_spoiled_ballot) # encrypt some more fake ballots self.more_fake_encrypted_ballots = [] for fake_ballot in self.more_fake_ballots: self.more_fake_encrypted_ballots.append( self.ballot_marking_device.encrypt(fake_ballot)) # encrypt some more fake ballots self.more_fake_encrypted_spoiled_ballots = [] for fake_ballot in self.more_fake_spoiled_ballots: self.more_fake_encrypted_spoiled_ballots.append( self.ballot_marking_device.encrypt(fake_ballot)) # configure the ballot box ballot_store = DataStore() ballot_box = BallotBox(self.internal_manifest, self.context, ballot_store) ballot_box.cast(self.encrypted_fake_cast_ballot) ballot_box.spoil(self.encrypted_fake_spoiled_ballot) # Cast some more fake ballots for fake_ballot in self.more_fake_encrypted_ballots: ballot_box.cast(fake_ballot) # Spoil some more fake ballots for fake_ballot in self.more_fake_encrypted_spoiled_ballots: ballot_box.spoil(fake_ballot) # generate encrypted tally self.ciphertext_tally = tally_ballots(ballot_store, self.internal_manifest, self.context) self.ciphertext_ballots = get_ballots(ballot_store, BallotBoxState.SPOILED) def tearDown(self): self.key_ceremony_mediator.reset( CeremonyDetails(self.NUMBER_OF_GUARDIANS, self.QUORUM)) def test_decrypt_selection_with_all_guardians_present(self): # Arrange available_guardians = self.guardians # find the first selection first_contest = list(self.ciphertext_tally.contests.values())[0] first_selection = list(first_contest.selections.values())[0] print(first_contest.object_id) print(first_selection.object_id) # precompute decryption shares for specific selection for the guardians shares: Dict[GUARDIAN_ID, Tuple[ELECTION_PUBLIC_KEY, DecryptionShare]] = { guardian.id: ( guardian.share_election_public_key().key, compute_decryption_share( guardian._election_keys, self.ciphertext_tally, self.context, ).contests[first_contest.object_id].selections[ first_selection.object_id], ) for guardian in available_guardians } # Act result = decrypt_selection_with_decryption_shares( first_selection, shares, self.context.crypto_extended_base_hash) # Assert self.assertIsNotNone(result) self.assertEqual( self.expected_plaintext_tally[first_selection.object_id], result.tally) def test_decrypt_ballot_with_all_guardians_present(self): # Arrange # precompute decryption shares for the guardians available_guardians = self.guardians plaintext_ballot = self.fake_cast_ballot encrypted_ballot = self.encrypted_fake_cast_ballot shares = { available_guardian.id: compute_decryption_share_for_ballot( available_guardian._election_keys, encrypted_ballot, self.context, ) for available_guardian in available_guardians } # act result = decrypt_ballot( encrypted_ballot, shares, self.context.crypto_extended_base_hash, ) # assert self.assertIsNotNone(result) for contest in plaintext_ballot.contests: for selection in contest.ballot_selections: expected_tally = selection.vote actual_tally = (result.contests[contest.object_id].selections[ selection.object_id].tally) self.assertEqual(expected_tally, actual_tally) def test_decrypt_ballot_with_missing_guardians(self): # Arrange # precompute decryption shares for the guardians plaintext_ballot = self.fake_cast_ballot encrypted_ballot = self.encrypted_fake_cast_ballot available_guardians = self.guardians[0:2] missing_guardian = self.guardians[2] available_shares = { available_guardian.id: compute_decryption_share_for_ballot( available_guardian._election_keys, encrypted_ballot, self.context, ) for available_guardian in available_guardians } compensated_shares = { available_guardian.id: compute_compensated_decryption_share_for_ballot( available_guardian.share_election_public_key(), available_guardian._auxiliary_keys, missing_guardian.share_election_public_key(), get_optional( available_guardian._guardian_election_partial_key_backups. get(missing_guardian.id)), encrypted_ballot, self.context, identity_auxiliary_decrypt, ) for available_guardian in available_guardians } lagrange_coefficients = compute_lagrange_coefficients_for_guardians([ guardian.share_election_public_key() for guardian in available_guardians ]) reconstructed_share = reconstruct_decryption_share_for_ballot( missing_guardian.share_election_public_key(), encrypted_ballot, compensated_shares, lagrange_coefficients, ) all_shares = { **available_shares, missing_guardian.id: reconstructed_share } # act result = decrypt_ballot( encrypted_ballot, all_shares, self.context.crypto_extended_base_hash, ) # assert self.assertIsNotNone(result) for contest in plaintext_ballot.contests: for selection in contest.ballot_selections: expected_tally = selection.vote actual_tally = (result.contests[contest.object_id].selections[ selection.object_id].tally) self.assertEqual(expected_tally, actual_tally)
class TestDecryption(TestCase): """Test decryption methods""" NUMBER_OF_GUARDIANS = 3 QUORUM = 2 CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM) def setUp(self): # Key Ceremony self.key_ceremony_mediator = KeyCeremonyMediator( "key_ceremony_mediator_mediator", self.CEREMONY_DETAILS) self.guardians: List[Guardian] = KeyCeremonyHelper.create_guardians( self.CEREMONY_DETAILS) KeyCeremonyHelper.perform_full_ceremony(self.guardians, self.key_ceremony_mediator) self.joint_public_key = self.key_ceremony_mediator.publish_joint_key() # Setup the election manifest = election_factory.get_fake_manifest() builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, manifest) builder.set_public_key(self.joint_public_key.joint_public_key) builder.set_commitment_hash(self.joint_public_key.commitment_hash) self.internal_manifest, self.context = get_optional(builder.build()) self.encryption_device = election_factory.get_encryption_device() self.ballot_marking_device = EncryptionMediator( self.internal_manifest, self.context, self.encryption_device) # get some fake ballots self.fake_cast_ballot = ballot_factory.get_fake_ballot( self.internal_manifest, "some-unique-ballot-id-cast") self.more_fake_ballots = [] for i in range(10): self.more_fake_ballots.append( ballot_factory.get_fake_ballot( self.internal_manifest, f"some-unique-ballot-id-cast{i}")) self.fake_spoiled_ballot = ballot_factory.get_fake_ballot( self.internal_manifest, "some-unique-ballot-id-spoiled") self.more_fake_spoiled_ballots = [] for i in range(2): self.more_fake_spoiled_ballots.append( ballot_factory.get_fake_ballot( self.internal_manifest, f"some-unique-ballot-id-spoiled{i}")) self.assertTrue( self.fake_cast_ballot.is_valid( self.internal_manifest.ballot_styles[0].object_id)) self.assertTrue( self.fake_spoiled_ballot.is_valid( self.internal_manifest.ballot_styles[0].object_id)) self.expected_plaintext_tally = accumulate_plaintext_ballots( [self.fake_cast_ballot] + self.more_fake_ballots) # Fill in the expected values with any missing selections # that were not made on any ballots selection_ids = { selection.object_id for contest in self.internal_manifest.contests for selection in contest.ballot_selections } missing_selection_ids = selection_ids.difference( set(self.expected_plaintext_tally)) for id in missing_selection_ids: self.expected_plaintext_tally[id] = 0 # Encrypt self.encrypted_fake_cast_ballot = self.ballot_marking_device.encrypt( self.fake_cast_ballot) self.encrypted_fake_spoiled_ballot = self.ballot_marking_device.encrypt( self.fake_spoiled_ballot) # encrypt some more fake ballots self.more_fake_encrypted_ballots = [] for fake_ballot in self.more_fake_ballots: self.more_fake_encrypted_ballots.append( self.ballot_marking_device.encrypt(fake_ballot)) # encrypt some more fake ballots self.more_fake_encrypted_spoiled_ballots = [] for fake_ballot in self.more_fake_spoiled_ballots: self.more_fake_encrypted_spoiled_ballots.append( self.ballot_marking_device.encrypt(fake_ballot)) # configure the ballot box ballot_store = DataStore() ballot_box = BallotBox(self.internal_manifest, self.context, ballot_store) ballot_box.cast(self.encrypted_fake_cast_ballot) ballot_box.spoil(self.encrypted_fake_spoiled_ballot) # Cast some more fake ballots for fake_ballot in self.more_fake_encrypted_ballots: ballot_box.cast(fake_ballot) # Spoil some more fake ballots for fake_ballot in self.more_fake_encrypted_spoiled_ballots: ballot_box.spoil(fake_ballot) # generate encrypted tally self.ciphertext_tally = tally_ballots(ballot_store, self.internal_manifest, self.context) self.ciphertext_ballots: Dict[BALLOT_ID, SubmittedBallot] = get_ballots( ballot_store, BallotBoxState.SPOILED) def tearDown(self): self.key_ceremony_mediator.reset( CeremonyDetails(self.NUMBER_OF_GUARDIANS, self.QUORUM)) # SHARE def test_compute_decryption_share(self): # Arrange guardian = self.guardians[0] # Act # Guardian doesn't give keys broken_secret_key = ZERO_MOD_Q broken_guardian_key_pair = ElectionKeyPair( guardian.id, guardian.sequence_order, ElGamalKeyPair(broken_secret_key, guardian._election_keys.key_pair.public_key), guardian._election_keys.polynomial, ) broken_share = compute_decryption_share( broken_guardian_key_pair, self.ciphertext_tally, self.context, ) # Assert self.assertIsNone(broken_share) # Act # Normal use case share = compute_decryption_share( guardian._election_keys, self.ciphertext_tally, self.context, ) # Assert self.assertIsNotNone(share) def test_compute_compensated_decryption_share(self): # Arrange guardian = self.guardians[0] missing_guardian = self.guardians[2] public_key = guardian.share_election_public_key() auxiliary_keys = guardian._auxiliary_keys missing_guardian_public_key = missing_guardian.share_election_public_key( ) missing_guardian_backup = missing_guardian._backups_to_share.get( guardian.id) # Act share = compute_compensated_decryption_share( public_key, auxiliary_keys, missing_guardian_public_key, missing_guardian_backup, self.ciphertext_tally, self.context, identity_auxiliary_decrypt, ) # Assert self.assertIsNotNone(share) # SELECTION def test_compute_selection(self): # Arrange first_selection = [ selection for contest in self.ciphertext_tally.contests.values() for selection in contest.selections.values() ][0] # act result = compute_decryption_share_for_selection( self.guardians[0]._election_keys, first_selection, self.context) # assert self.assertIsNotNone(result) def test_compute_compensated_selection(self): """ demonstrates the complete workflow for computing a comepnsated decryption share For one selection. It is useful for verifying that the workflow is correct """ # Arrange available_guardian_1 = self.guardians[0] available_guardian_2 = self.guardians[1] missing_guardian = self.guardians[2] available_guardian_1_key = available_guardian_1.share_election_public_key( ) available_guardian_2_key = available_guardian_2.share_election_public_key( ) missing_guardian_key = missing_guardian.share_election_public_key() first_selection = [ selection for contest in self.ciphertext_tally.contests.values() for selection in contest.selections.values() ][0] # Compute lagrange coefficients for the guardians that are present lagrange_0 = compute_lagrange_coefficient( available_guardian_1.sequence_order, *[available_guardian_2.sequence_order], ) lagrange_1 = compute_lagrange_coefficient( available_guardian_2.sequence_order, *[available_guardian_1.sequence_order], ) print(( f"lagrange: sequence_orders: ({available_guardian_1.sequence_order}, " f"{available_guardian_2.sequence_order}, {missing_guardian.sequence_order})\n" )) print(lagrange_0) print(lagrange_1) # compute their shares share_0 = compute_decryption_share_for_selection( available_guardian_1._election_keys, first_selection, self.context) share_1 = compute_decryption_share_for_selection( available_guardian_2._election_keys, first_selection, self.context) self.assertIsNotNone(share_0) self.assertIsNotNone(share_1) # compute compensations shares for the missing guardian compensation_0 = compute_compensated_decryption_share_for_selection( available_guardian_1.share_election_public_key(), available_guardian_1._auxiliary_keys, missing_guardian.share_election_public_key(), missing_guardian.share_election_partial_key_backup( available_guardian_1.id), first_selection, self.context, identity_auxiliary_decrypt, ) compensation_1 = compute_compensated_decryption_share_for_selection( available_guardian_2.share_election_public_key(), available_guardian_2._auxiliary_keys, missing_guardian.share_election_public_key(), missing_guardian.share_election_partial_key_backup( available_guardian_2.id), first_selection, self.context, identity_auxiliary_decrypt, ) self.assertIsNotNone(compensation_0) self.assertIsNotNone(compensation_1) print("\nSHARES:") print(compensation_0) print(compensation_1) # Check the share proofs self.assertTrue( compensation_0.proof.is_valid( first_selection.ciphertext, compute_recovery_public_key(available_guardian_1_key, missing_guardian_key), compensation_0.share, self.context.crypto_extended_base_hash, )) self.assertTrue( compensation_1.proof.is_valid( first_selection.ciphertext, compute_recovery_public_key(available_guardian_2_key, missing_guardian_key), compensation_1.share, self.context.crypto_extended_base_hash, )) share_pow_p = [ pow_p(compensation_0.share, lagrange_0), pow_p(compensation_1.share, lagrange_1), ] print("\nSHARE_POW_P") print(share_pow_p) # reconstruct the missing share from the compensation shares reconstructed_share = mult_p(*[ pow_p(compensation_0.share, lagrange_0), pow_p(compensation_1.share, lagrange_1), ]) print("\nRECONSTRUCTED SHARE\n") print(reconstructed_share) share_2 = create_ciphertext_decryption_selection( first_selection.object_id, missing_guardian.id, reconstructed_share, { available_guardian_1.id: compensation_0, available_guardian_2.id: compensation_1, }, ) # Decrypt the result result = decrypt_selection_with_decryption_shares( first_selection, { available_guardian_1.id: ( available_guardian_1.share_election_public_key().key, share_0, ), available_guardian_2.id: ( available_guardian_2.share_election_public_key().key, share_1, ), missing_guardian.id: ( missing_guardian.share_election_public_key().key, share_2, ), }, self.context.crypto_extended_base_hash, ) print(result) self.assertIsNotNone(result) self.assertEqual( result.tally, self.expected_plaintext_tally[first_selection.object_id]) def test_compute_compensated_selection_failure(self): # Arrange available_guardian = self.guardians[0] missing_guardian = self.guardians[2] first_selection = [ selection for contest in self.ciphertext_tally.contests.values() for selection in contest.selections.values() ][0] # Act # Get backup for missing guardian instead of one sent by guardian incorrect_backup = available_guardian.share_election_partial_key_backup( missing_guardian.id) result = compute_compensated_decryption_share_for_selection( available_guardian.share_election_public_key(), available_guardian._auxiliary_keys, missing_guardian.share_election_public_key(), incorrect_backup, first_selection, self.context, identity_auxiliary_decrypt, ) # Assert self.assertIsNone(result) def test_reconstruct_decryption_share(self): # Arrange available_guardians = self.guardians[0:2] available_guardians_keys = [ guardian.share_election_public_key() for guardian in available_guardians ] missing_guardian = self.guardians[2] missing_guardian_key = missing_guardian.share_election_public_key() missing_guardian_backups = { backup.designated_id: backup for backup in missing_guardian.share_election_partial_key_backups() } tally = self.ciphertext_tally # Act compensated_shares: Dict[GUARDIAN_ID, CompensatedDecryptionShare] = { available_guardian.id: compute_compensated_decryption_share( available_guardian.share_election_public_key(), available_guardian._auxiliary_keys, missing_guardian_key, missing_guardian_backups[available_guardian.id], tally, self.context, identity_auxiliary_decrypt, ) for available_guardian in available_guardians } lagrange_coefficients = compute_lagrange_coefficients_for_guardians( available_guardians_keys) share = reconstruct_decryption_share(missing_guardian_key, tally, compensated_shares, lagrange_coefficients) # Assert self.assertEqual(self.QUORUM, len(compensated_shares)) self.assertEqual(self.QUORUM, len(lagrange_coefficients)) self.assertIsNotNone(share) def test_reconstruct_decryption_shares_for_ballot(self): # Arrange available_guardians = self.guardians[0:2] available_guardians_keys = [ guardian.share_election_public_key() for guardian in available_guardians ] missing_guardian = self.guardians[2] missing_guardian_key = missing_guardian.share_election_public_key() missing_guardian_backups = { backup.designated_id: backup for backup in missing_guardian.share_election_partial_key_backups() } ballot = list(self.ciphertext_ballots.values())[0] # Act compensated_ballot_shares: Dict[GUARDIAN_ID, CompensatedDecryptionShare] = {} for available_guardian in available_guardians: compensated_share = compute_compensated_decryption_share_for_ballot( available_guardian.share_election_public_key(), available_guardian._auxiliary_keys, missing_guardian_key, missing_guardian_backups[available_guardian.id], ballot, self.context, identity_auxiliary_decrypt, ) if compensated_share: compensated_ballot_shares[ available_guardian.id] = compensated_share lagrange_coefficients = compute_lagrange_coefficients_for_guardians( available_guardians_keys) missing_ballot_share = reconstruct_decryption_share_for_ballot( missing_guardian_key, ballot, compensated_ballot_shares, lagrange_coefficients, ) # Assert self.assertEqual(self.QUORUM, len(lagrange_coefficients)) self.assertEqual(len(available_guardians), len(compensated_ballot_shares)) self.assertEqual(len(available_guardians), len(lagrange_coefficients)) self.assertIsNotNone(missing_ballot_share) def test_reconstruct_decryption_share_for_ballot(self): # Arrange available_guardians = self.guardians[0:2] available_guardians_keys = [ guardian.share_election_public_key() for guardian in available_guardians ] missing_guardian = self.guardians[2] missing_guardian_key = missing_guardian.share_election_public_key() missing_guardian_backups = { backup.designated_id: backup for backup in missing_guardian.share_election_partial_key_backups() } ballot = self.ciphertext_ballots[self.fake_spoiled_ballot.object_id] # Act compensated_shares: Dict[GUARDIAN_ID, CompensatedDecryptionShare] = { available_guardian.id: get_optional( compute_compensated_decryption_share_for_ballot( available_guardian.share_election_public_key(), available_guardian._auxiliary_keys, missing_guardian_key, missing_guardian_backups[available_guardian.id], ballot, self.context, identity_auxiliary_decrypt, )) for available_guardian in available_guardians } lagrange_coefficients = compute_lagrange_coefficients_for_guardians( available_guardians_keys) share = reconstruct_decryption_share_for_ballot( missing_guardian_key, ballot, compensated_shares, lagrange_coefficients) # Assert self.assertEqual(self.QUORUM, len(compensated_shares)) self.assertEqual(self.QUORUM, len(lagrange_coefficients)) self.assertIsNotNone(share)
class TestDecryptionMediator(TestCase): NUMBER_OF_GUARDIANS = 3 QUORUM = 2 CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM) def setUp(self): self.key_ceremony = KeyCeremonyMediator(self.CEREMONY_DETAILS) self.guardians: List[Guardian] = [] # Setup Guardians for i in range(self.NUMBER_OF_GUARDIANS): self.guardians.append( Guardian("guardian_" + str(i), i, self.NUMBER_OF_GUARDIANS, self.QUORUM) ) # Attendance (Public Key Share) for guardian in self.guardians: self.key_ceremony.announce(guardian) self.key_ceremony.orchestrate() self.key_ceremony.verify() self.joint_public_key = self.key_ceremony.publish_joint_key() self.assertIsNotNone(self.joint_public_key) # setup the election self.election = election_factory.get_fake_election() builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, self.election) self.assertIsNone(builder.build()) # Can't build without the public key builder.set_public_key(self.joint_public_key) self.metadata, self.context = get_optional(builder.build()) self.encryption_device = EncryptionDevice("location") self.ballot_marking_device = EncryptionMediator( self.metadata, self.context, self.encryption_device ) # get some fake ballots self.fake_cast_ballot = ballot_factory.get_fake_ballot( self.metadata, "some-unique-ballot-id-cast" ) self.fake_spoiled_ballot = ballot_factory.get_fake_ballot( self.metadata, "some-unique-ballot-id-spoiled" ) self.assertTrue( self.fake_cast_ballot.is_valid(self.metadata.ballot_styles[0].object_id) ) self.assertTrue( self.fake_spoiled_ballot.is_valid(self.metadata.ballot_styles[0].object_id) ) self.expected_plaintext_tally = accumulate_plaintext_ballots( [self.fake_cast_ballot] ) # Fill in any missing selections that were not made on any ballots selection_ids = set( [ selection.object_id for contest in self.metadata.contests for selection in contest.ballot_selections ] ) missing_selection_ids = selection_ids.difference( set(self.expected_plaintext_tally) ) for id in missing_selection_ids: self.expected_plaintext_tally[id] = 0 # Encrypt encrypted_fake_cast_ballot = self.ballot_marking_device.encrypt( self.fake_cast_ballot ) encrypted_fake_spoiled_ballot = self.ballot_marking_device.encrypt( self.fake_spoiled_ballot ) self.assertIsNotNone(encrypted_fake_cast_ballot) self.assertIsNotNone(encrypted_fake_spoiled_ballot) self.assertTrue( encrypted_fake_cast_ballot.is_valid_encryption( self.context.crypto_extended_base_hash, self.joint_public_key ) ) # configure the ballot box ballot_store = BallotStore() ballot_box = BallotBox(self.metadata, self.context, ballot_store) ballot_box.cast(encrypted_fake_cast_ballot) ballot_box.spoil(encrypted_fake_spoiled_ballot) # generate encrypted tally self.ciphertext_tally = tally_ballots(ballot_store, self.metadata, self.context) def test_announce(self): # Arrange subject = DecryptionMediator(self.metadata, self.context, self.ciphertext_tally) # act result = subject.announce(self.guardians[0]) # assert self.assertIsNotNone(result) # Can only announce once self.assertIsNone(subject.announce(self.guardians[0])) # Cannot submit another share internally self.assertFalse( subject._submit_decryption_share( DecryptionShare(self.guardians[0].object_id, {}, {}) ) ) # Cannot get plaintext tally without a quorum self.assertIsNone(subject.get_plaintext_tally()) def test_compute_selection(self): # Arrange first_selection = [ selection for contest in self.ciphertext_tally.cast.values() for selection in contest.tally_selections.values() ][0] # act result = _compute_decryption_for_selection( self.guardians[0], first_selection, self.context ) # assert self.assertIsNotNone(result) def test_decrypt_selection(self): # Arrange # find the first selection first_contest = [contest for contest in self.ciphertext_tally.cast.values()][0] first_selection = list(first_contest.tally_selections.values())[0] # precompute decryption shares for the guardians first_share = compute_decryption_share( self.guardians[0], self.ciphertext_tally, self.context ) second_share = compute_decryption_share( self.guardians[1], self.ciphertext_tally, self.context ) third_share = compute_decryption_share( self.guardians[2], self.ciphertext_tally, self.context ) shares = { self.guardians[0] .object_id: first_share.contests[first_contest.object_id] .selections[first_selection.object_id] .share, self.guardians[1] .object_id: second_share.contests[first_contest.object_id] .selections[first_selection.object_id] .share, self.guardians[2] .object_id: third_share.contests[first_contest.object_id] .selections[first_selection.object_id] .share, } # act result = decrypt_selection_with_decryption_shares(first_selection, shares) # assert self.assertIsNotNone(result) self.assertEqual( self.expected_plaintext_tally[first_selection.object_id], result.plaintext ) def test_decrypt_spoiled_ballots(self): # Arrange # precompute decryption shares for the guardians first_share = compute_decryption_share( self.guardians[0], self.ciphertext_tally, self.context ) second_share = compute_decryption_share( self.guardians[1], self.ciphertext_tally, self.context ) third_share = compute_decryption_share( self.guardians[2], self.ciphertext_tally, self.context ) shares = { self.guardians[0].object_id: first_share, self.guardians[1].object_id: second_share, self.guardians[2].object_id: third_share, } subject = DecryptionMediator(self.metadata, self.context, self.ciphertext_tally) # act result = subject._decrypt_spoiled_ballots( self.ciphertext_tally.spoiled_ballots, shares ) # assert self.assertIsNotNone(result) self.assertTrue(self.fake_spoiled_ballot.object_id in result) spoiled_ballot = result[self.fake_spoiled_ballot.object_id] for contest in self.fake_spoiled_ballot.contests: for selection in contest.ballot_selections: self.assertEqual( spoiled_ballot[contest.object_id] .selections[selection.object_id] .plaintext, result[self.fake_spoiled_ballot.object_id][contest.object_id] .selections[selection.object_id] .plaintext, ) def test_get_plaintext_tally_all_guardians_present_simple(self): # Arrange subject = DecryptionMediator(self.metadata, self.context, self.ciphertext_tally) # act for guardian in self.guardians: self.assertIsNotNone(subject.announce(guardian)) decrypted_tallies = subject.get_plaintext_tally() result = self._convert_to_selections(decrypted_tallies) # assert self.assertIsNotNone(result) self.assertEqual(self.expected_plaintext_tally, result) @settings( deadline=timedelta(milliseconds=10000), suppress_health_check=[HealthCheck.too_slow], max_examples=1, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given(data(), integers(1, 3), integers(2, 5)) def test_get_plaintext_tally_all_guardians_present( self, data, parties: int, contests: int ): # Arrange description = data.draw(election_descriptions(parties, contests)) builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, description) metadata, context = builder.set_public_key(self.joint_public_key).build() plaintext_ballots: List[PlaintextBallot] = data.draw( plaintext_voted_ballots(metadata, randrange(3, 6)) ) plaintext_tallies = accumulate_plaintext_ballots(plaintext_ballots) encrypted_tally = self._generate_encrypted_tally( metadata, context, plaintext_ballots ) subject = DecryptionMediator(metadata, context, encrypted_tally) # act for guardian in self.guardians: self.assertIsNotNone(subject.announce(guardian)) decrypted_tallies = subject.get_plaintext_tally() result = self._convert_to_selections(decrypted_tallies) # assert self.assertIsNotNone(result) self.assertEqual(plaintext_tallies, result) def _generate_encrypted_tally( self, metadata: InternalElectionDescription, context: CiphertextElectionContext, ballots: List[PlaintextBallot], ) -> CiphertextTally: # encrypt each ballot store = BallotStore() for ballot in ballots: encrypted_ballot = encrypt_ballot( ballot, metadata, context, int_to_q_unchecked(1) ) self.assertIsNotNone(encrypted_ballot) # add to the ballot store store.set( encrypted_ballot.object_id, from_ciphertext_ballot(encrypted_ballot, BallotBoxState.CAST), ) tally = tally_ballots(store, metadata, context) self.assertIsNotNone(tally) return get_optional(tally) def _convert_to_selections(self, tally: PlaintextTally) -> Dict[str, int]: plaintext_selections: Dict[str, int] = {} for _, contest in tally.contests.items(): for selection_id, selection in contest.selections.items(): plaintext_selections[selection_id] = selection.plaintext return plaintext_selections
class TestDecryptionMediator(TestCase): """Test suite for DecryptionMediator""" NUMBER_OF_GUARDIANS = 3 QUORUM = 2 CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM) internal_manifest: InternalManifest decryption_mediator_id = "mediator-id" def setUp(self): # Key Ceremony key_ceremony_mediator = KeyCeremonyMediator( "key_ceremony_mediator_mediator", self.CEREMONY_DETAILS) self.guardians: List[Guardian] = KeyCeremonyHelper.create_guardians( self.CEREMONY_DETAILS) KeyCeremonyHelper.perform_full_ceremony(self.guardians, key_ceremony_mediator) self.joint_public_key = key_ceremony_mediator.publish_joint_key() self.assertIsNotNone(self.joint_public_key) # Setup the election manifest = election_factory.get_fake_manifest() builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, manifest) self.assertIsNone( builder.build()) # Can't build without the public key builder.set_public_key(self.joint_public_key.joint_public_key) builder.set_commitment_hash(self.joint_public_key.commitment_hash) self.internal_manifest, self.context = get_optional(builder.build()) self.encryption_device = election_factory.get_encryption_device() self.ballot_marking_device = EncryptionMediator( self.internal_manifest, self.context, self.encryption_device) # get some fake ballots self.fake_cast_ballot = ballot_factory.get_fake_ballot( self.internal_manifest, "some-unique-ballot-id-cast") more_fake_ballots = [] for i in range(10): more_fake_ballots.append( ballot_factory.get_fake_ballot( self.internal_manifest, f"some-unique-ballot-id-cast{i}")) self.fake_spoiled_ballot = ballot_factory.get_fake_ballot( self.internal_manifest, "some-unique-ballot-id-spoiled") more_fake_spoiled_ballots = [] for i in range(2): more_fake_spoiled_ballots.append( ballot_factory.get_fake_ballot( self.internal_manifest, f"some-unique-ballot-id-spoiled{i}")) self.assertTrue( self.fake_cast_ballot.is_valid( self.internal_manifest.ballot_styles[0].object_id)) self.assertTrue( self.fake_spoiled_ballot.is_valid( self.internal_manifest.ballot_styles[0].object_id)) self.expected_plaintext_tally = accumulate_plaintext_ballots( [self.fake_cast_ballot] + more_fake_ballots) # Fill in the expected values with any missing selections # that were not made on any ballots selection_ids = { selection.object_id for contest in self.internal_manifest.contests for selection in contest.ballot_selections } missing_selection_ids = selection_ids.difference( set(self.expected_plaintext_tally)) for id in missing_selection_ids: self.expected_plaintext_tally[id] = 0 # Encrypt self.encrypted_fake_cast_ballot = self.ballot_marking_device.encrypt( self.fake_cast_ballot) self.encrypted_fake_spoiled_ballot = self.ballot_marking_device.encrypt( self.fake_spoiled_ballot) self.assertIsNotNone(self.encrypted_fake_cast_ballot) self.assertIsNotNone(self.encrypted_fake_spoiled_ballot) self.assertTrue( self.encrypted_fake_cast_ballot.is_valid_encryption( self.internal_manifest.manifest_hash, self.joint_public_key.joint_public_key, self.context.crypto_extended_base_hash, )) # encrypt some more fake ballots more_fake_encrypted_ballots = [] for fake_ballot in more_fake_ballots: more_fake_encrypted_ballots.append( self.ballot_marking_device.encrypt(fake_ballot)) # encrypt some more fake ballots self.more_fake_encrypted_spoiled_ballots = [] for fake_ballot in more_fake_spoiled_ballots: self.more_fake_encrypted_spoiled_ballots.append( self.ballot_marking_device.encrypt(fake_ballot)) # configure the ballot box ballot_store = DataStore() ballot_box = BallotBox(self.internal_manifest, self.context, ballot_store) ballot_box.cast(self.encrypted_fake_cast_ballot) ballot_box.spoil(self.encrypted_fake_spoiled_ballot) # Cast some more fake ballots for fake_ballot in more_fake_encrypted_ballots: ballot_box.cast(fake_ballot) # Spoil some more fake ballots for fake_ballot in self.more_fake_encrypted_spoiled_ballots: ballot_box.spoil(fake_ballot) # generate encrypted tally self.ciphertext_tally = tally_ballots(ballot_store, self.internal_manifest, self.context) self.ciphertext_ballots = list( get_ballots(ballot_store, BallotBoxState.SPOILED).values()) def test_announce(self): # Arrange mediator = DecryptionMediator( self.decryption_mediator_id, self.context, ) guardian = self.guardians[0] guardian_key = self.guardians[0].share_election_public_key() tally_share = guardian.compute_tally_share(self.ciphertext_tally, self.context) ballot_shares = {} # Act mediator.announce(guardian_key, tally_share, ballot_shares) # Assert self.assertEqual(len(mediator.get_available_guardians()), 1) # Act # Announce again mediator.announce(guardian_key, tally_share, ballot_shares) # Assert # Can only announce once self.assertEqual(len(mediator.get_available_guardians()), 1) # Cannot get plaintext tally or spoiled ballots without a quorum self.assertIsNone(mediator.get_plaintext_tally(self.ciphertext_tally)) self.assertIsNone( mediator.get_plaintext_ballots(self.ciphertext_ballots)) def test_get_plaintext_with_all_guardians_present(self): # Arrange mediator = DecryptionMediator( self.decryption_mediator_id, self.context, ) available_guardians = self.guardians DecryptionHelper.perform_decryption_setup( available_guardians, mediator, self.context, self.ciphertext_tally, self.ciphertext_ballots, ) # Act plaintext_tally = mediator.get_plaintext_tally(self.ciphertext_tally) plaintext_ballots = mediator.get_plaintext_ballots( self.ciphertext_ballots) # Convert to selections to check for the same tally selections = _convert_to_selections(plaintext_tally) # Verify we get the same tally back if we call again another_plaintext_tally = mediator.get_plaintext_tally( self.ciphertext_tally) # Assert self.assertIsNotNone(plaintext_tally) self.assertIsNotNone(plaintext_ballots) self.assertEqual(len(self.ciphertext_ballots), len(plaintext_ballots)) self.assertIsNotNone(selections) self.assertEqual(self.expected_plaintext_tally, selections) self.assertEqual(plaintext_tally, another_plaintext_tally) def test_get_plaintext_with_a_missing_guardian(self): # Arrange mediator = DecryptionMediator( self.decryption_mediator_id, self.context, ) available_guardians = self.guardians[0:2] all_guardian_keys = [ guardian.share_election_public_key() for guardian in self.guardians ] DecryptionHelper.perform_compensated_decryption_setup( available_guardians, all_guardian_keys, mediator, self.context, self.ciphertext_tally, self.ciphertext_ballots, ) # Act plaintext_tally = mediator.get_plaintext_tally(self.ciphertext_tally) plaintext_ballots = mediator.get_plaintext_ballots( self.ciphertext_ballots) # Convert to selections to check for the same tally selections = _convert_to_selections(plaintext_tally) # Verify we get the same tally back if we call again another_plaintext_tally = mediator.get_plaintext_tally( self.ciphertext_tally) # Assert self.assertIsNotNone(plaintext_tally) self.assertIsNotNone(plaintext_ballots) self.assertEqual(len(self.ciphertext_ballots), len(plaintext_ballots)) self.assertIsNotNone(selections) self.assertEqual(self.expected_plaintext_tally, selections) self.assertEqual(plaintext_tally, another_plaintext_tally) @settings( deadline=timedelta(milliseconds=15000), suppress_health_check=[HealthCheck.too_slow], max_examples=8, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given(data(), integers(1, 3), integers(2, 5)) def test_get_plaintext_tally_with_all_guardians_present( self, values, parties: int, contests: int): # Arrange description = values.draw(election_descriptions(parties, contests)) builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, description) internal_manifest, context = (builder.set_public_key( self.joint_public_key.joint_public_key).set_commitment_hash( self.joint_public_key.commitment_hash).build()) plaintext_ballots: List[PlaintextBallot] = values.draw( plaintext_voted_ballots(internal_manifest, randrange(3, 6))) expected_plaintext_tally = accumulate_plaintext_ballots( plaintext_ballots) encrypted_tally = self._generate_encrypted_tally( internal_manifest, context, plaintext_ballots) mediator = DecryptionMediator(self.decryption_mediator_id, context) available_guardians = self.guardians DecryptionHelper.perform_decryption_setup(available_guardians, mediator, context, encrypted_tally, []) # Act plaintext_tally = mediator.get_plaintext_tally(encrypted_tally) selections = _convert_to_selections(plaintext_tally) # Assert self.assertIsNotNone(plaintext_tally) self.assertIsNotNone(selections) self.assertEqual(expected_plaintext_tally, selections) def _generate_encrypted_tally( self, internal_manifest: InternalManifest, context: CiphertextElectionContext, ballots: List[PlaintextBallot], ) -> CiphertextTally: # encrypt each ballot store = DataStore() for ballot in ballots: encrypted_ballot = encrypt_ballot(ballot, internal_manifest, context, int_to_q_unchecked(1)) self.assertIsNotNone(encrypted_ballot) # add to the ballot store store.set( encrypted_ballot.object_id, from_ciphertext_ballot(encrypted_ballot, BallotBoxState.CAST), ) tally = tally_ballots(store, internal_manifest, context) self.assertIsNotNone(tally) return get_optional(tally)
from unittest import TestCase from electionguard.guardian import Guardian from electionguard.key_ceremony import ( CeremonyDetails, ElectionPartialKeyVerification, ) from electionguard.key_ceremony_mediator import KeyCeremonyMediator, GuardianPair from electionguardtest.key_ceremony_helper import KeyCeremonyHelper from electionguardtest.identity_encrypt import identity_auxiliary_decrypt NUMBER_OF_GUARDIANS = 2 QUORUM = 2 CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM) GUARDIAN_1_ID = "Guardian 1" GUARDIAN_2_ID = "Guardian 2" GUARDIAN_1 = Guardian(GUARDIAN_1_ID, 1, NUMBER_OF_GUARDIANS, QUORUM) GUARDIAN_2 = Guardian(GUARDIAN_2_ID, 2, NUMBER_OF_GUARDIANS, QUORUM) GUARDIANS = [GUARDIAN_1, GUARDIAN_2] class TestKeyCeremonyMediator(TestCase): """Key ceremony mediator tests""" def test_reset(self): # Arrange mediator = KeyCeremonyMediator("mediator_reset", CEREMONY_DETAILS) new_ceremony_details = CeremonyDetails(3, 3) mediator.reset(new_ceremony_details) self.assertEqual(mediator.ceremony_details, new_ceremony_details)