def test_encrypt_simple_ballot_from_files_succeeds(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) election = election_factory.get_simple_election_from_file() metadata, context = election_factory.get_fake_ciphertext_election( election, keypair.public_key ) data = ballot_factory.get_simple_ballot_from_file() self.assertTrue(data.is_valid(metadata.ballot_styles[0].object_id)) device = EncryptionDevice("Location") subject = EncryptionMediator(metadata, context, device) # Act result = subject.encrypt(data) # Assert self.assertIsNotNone(result) self.assertEqual(data.object_id, result.object_id) self.assertTrue( result.is_valid_encryption( metadata.description_hash, keypair.public_key, context.crypto_extended_base_hash, ) )
def test_encrypt_ballot_with_stateful_composer_succeeds(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) manifest = election_factory.get_fake_manifest() internal_manifest, context = election_factory.get_fake_ciphertext_election( manifest, keypair.public_key) data = election_factory.get_fake_ballot(internal_manifest) self.assertTrue( data.is_valid(internal_manifest.ballot_styles[0].object_id)) device = election_factory.get_encryption_device() subject = EncryptionMediator(internal_manifest, context, device) # Act result = subject.encrypt(data) # Assert self.assertIsNotNone(result) self.assertTrue( result.is_valid_encryption( internal_manifest.manifest_hash, keypair.public_key, context.crypto_extended_base_hash, ))
def generate(self, number_of_ballots: int = DEFAULT_NUMBER_OF_BALLOTS): """ Generate the sample data set """ rmtree(RESULTS_DIR, ignore_errors=True) ( public_data, private_data, ) = self.election_factory.get_hamilton_election_with_encryption_context() plaintext_ballots = self.ballot_factory.generate_fake_plaintext_ballots_for_election( public_data.metadata, number_of_ballots ) self.encrypter = EncryptionMediator( public_data.metadata, public_data.context, self.encryption_device ) ciphertext_ballots: List[CiphertextBallot] = [] for plaintext_ballot in plaintext_ballots: ciphertext_ballots.append( get_optional(self.encrypter.encrypt(plaintext_ballot)) ) ballot_store = BallotStore() ballot_box = BallotBox(public_data.metadata, public_data.context, ballot_store) accepted_ballots: List[CiphertextAcceptedBallot] = [] for ballot in ciphertext_ballots: if randint(0, 100) % 10 == 0: accepted_ballots.append(ballot_box.spoil(ballot)) else: accepted_ballots.append(ballot_box.cast(ballot)) ciphertext_tally = get_optional( tally_ballots(ballot_store, public_data.metadata, public_data.context) ) decrypter = DecryptionMediator( public_data.metadata, public_data.context, ciphertext_tally ) for guardian in private_data.guardians: decrypter.announce(guardian) plaintext_tally = get_optional(decrypter.get_plaintext_tally()) publish( public_data.description, public_data.context, public_data.constants, accepted_ballots, ciphertext_tally, plaintext_tally, public_data.guardians, ) publish_private_data( plaintext_ballots, ciphertext_ballots, private_data.guardians )
def step_2_encrypt_votes(self) -> None: """ Using the `CiphertextElectionContext` encrypt ballots for the election """ # Configure the Encryption Device self.device = ElectionFactory.get_encryption_device() self.encrypter = EncryptionMediator(self.internal_manifest, self.context, self.device) self._assert_message( EncryptionDevice.__qualname__, f"Ready to encrypt at location: {self.device.location}", ) # Load some Ballots self.plaintext_ballots = BallotFactory().get_simple_ballots_from_file() self._assert_message( PlaintextBallot.__qualname__, f"Loaded ballots: {len(self.plaintext_ballots)}", len(self.plaintext_ballots) > 0, ) # Encrypt the Ballots for plaintext_ballot in self.plaintext_ballots: encrypted_ballot = self.encrypter.encrypt(plaintext_ballot) self._assert_message( EncryptionMediator.encrypt.__qualname__, f"Ballot Id: {plaintext_ballot.object_id}", encrypted_ballot is not None, ) self.ciphertext_ballots.append(get_optional(encrypted_ballot))
def test_decrypt_ballot_valid_input_missing_nonce_fails( self, keypair: ElGamalKeyPair ): # Arrange election = election_factory.get_simple_election_from_file() metadata, context = election_factory.get_fake_ciphertext_election( election, keypair.public_key ) data = ballot_factory.get_simple_ballot_from_file() device = EncryptionDevice("Location") operator = EncryptionMediator(metadata, context, device) # Act subject = operator.encrypt(data) self.assertIsNotNone(subject) subject.nonce = None missing_nonce_value = None result_from_nonce = decrypt_ballot_with_nonce( subject, metadata, context.crypto_extended_base_hash, keypair.public_key, ) result_from_nonce_seed = decrypt_ballot_with_nonce( subject, metadata, context.crypto_extended_base_hash, keypair.public_key, missing_nonce_value, ) # Assert self.assertIsNone(result_from_nonce) self.assertIsNone(result_from_nonce_seed)
def test_decrypt_ballot_valid_input_missing_nonce_fails( self, keypair: ElGamalKeyPair ): # Arrange election = election_factory.get_simple_manifest_from_file() internal_manifest, context = election_factory.get_fake_ciphertext_election( election, keypair.public_key ) data = ballot_factory.get_simple_ballot_from_file() device = election_factory.get_encryption_device() operator = EncryptionMediator(internal_manifest, context, device) # Act subject = operator.encrypt(data) self.assertIsNotNone(subject) subject.nonce = None missing_nonce_value = None result_from_nonce = decrypt_ballot_with_nonce( subject, internal_manifest, context.crypto_extended_base_hash, keypair.public_key, ) # SUGGEST this test is the same as the one above result_from_nonce_seed = decrypt_ballot_with_nonce( subject, internal_manifest, context.crypto_extended_base_hash, keypair.public_key, missing_nonce_value, ) # Assert self.assertIsNone(result_from_nonce) self.assertIsNone(result_from_nonce_seed)
def encrypt(ballot_as_dict: dict, encrypter: EncryptionMediator) -> CiphertextBallot: """ The ballot gets encrypted and stored in the ballots_encrypted field for the cast/spoil decision later. :param encrypter: :param ballot_as_dict: :return: """ ballot: PlaintextBallot = ballot_from_json(ballot_as_dict) # Encrypt the ballot encrypted_ballot: CiphertextBallot = encrypter.encrypt(ballot) return encrypted_ballot
def create() -> Tuple: """ An election with only one guardian and random keys gets generated. More configuration options and the ability to hold a key ceremony should be added later. """ # Open an election manifest file with open(os.path.join(ELECTION_MANIFEST), "r") as manifest: string_representation = manifest.read() election_description = ElectionDescription.from_json( string_representation) # Create an election builder instance, and configure it for a single public-private keypair. # in a real election, you would configure this for a group of guardians. See Key Ceremony for more information. # TODO: Allow real key ceremony builder = ElectionBuilder( number_of_guardians= 1, # since we will generate a single public-private keypair, we set this to 1 quorum= 1, # since we will generate a single public-private keypair, we set this to 1 description=election_description) # We simply generate a random keypair. For a real election this step should # be replaced by the key ceremony keypair = elgamal_keypair_random() builder.set_public_key(keypair.public_key) # get an `InternalElectionDescription` and `CiphertextElectionContext` # that are used for the remainder of the election. (metadata, context) = builder.build() # Configure an encryption device # In the docs the encrypter device gets defined when encrypting a ballot. # I think for our usecase it makes more sense to define one encrypter and use for the whole election device = EncryptionDevice("polling-place-one") encrypter = EncryptionMediator(metadata, context, device) store = BallotStore() ballot_box = BallotBox(metadata, context, store) return metadata, context, encrypter, ballot_box, store, keypair
class TestEndToEndElection(TestCase): """ Test a complete simple example of executing an End-to-End encrypted election. In a real world scenario all of these steps would not be completed on the same machine. """ NUMBER_OF_GUARDIANS = 5 QUORUM = 3 REMOVE_OUTPUT = False # Step 0 - Configure Election description: ElectionDescription election_builder: ElectionBuilder metadata: InternalElectionDescription context: CiphertextElectionContext constants: ElectionConstants # Step 1 - Key Ceremony mediator: KeyCeremonyMediator guardians: List[Guardian] = [] coefficient_validation_sets: List[CoefficientValidationSet] = [] # Step 2 - Encrypt Votes device: EncryptionDevice encrypter: EncryptionMediator plaintext_ballots: List[PlaintextBallot] ciphertext_ballots: List[CiphertextBallot] = [] # Step 3 - Cast and Spoil ballot_store: BallotStore ballot_box: BallotBox # Step 4 - Decrypt Tally ciphertext_tally: CiphertextTally plaintext_tally: PlaintextTally decrypter: DecryptionMediator def test_end_to_end_election(self) -> None: """ Execute the simplified end-to-end test demonstrating each component of the system. """ self.step_0_configure_election() self.step_1_key_ceremony() self.step_2_encrypt_votes() self.step_3_cast_and_spoil() self.step_4_decrypt_tally() self.step_5_publish_and_verify() def step_0_configure_election(self) -> None: """ To conduct an election, load an `ElectionDescription` file """ # Load a pre-configured Election Description # TODO: replace with complex election self.description = ElectionFactory().get_simple_election_from_file() print(f""" {'-'*40}\n # Election Summary: # Scope: {self.description.election_scope_id} # Geopolitical Units: {len(self.description.geopolitical_units)} # Parties: {len(self.description.parties)} # Candidates: {len(self.description.candidates)} # Contests: {len(self.description.contests)} # Ballot Styles: {len(self.description.ballot_styles)}\n {'-'*40}\n """) self._assert_message( ElectionDescription.is_valid.__qualname__, "Verify that the input election meta-data is well-formed", self.description.is_valid(), ) # Create an Election Builder self.election_builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, self.description) self._assert_message( ElectionBuilder.__qualname__, f"Created with number_of_guardians: {self.NUMBER_OF_GUARDIANS} quorum: {self.QUORUM}", ) # Move on to the Key Ceremony def step_1_key_ceremony(self) -> None: """ Using the NUMBER_OF_GUARDIANS, generate public-private keypairs and share representations of those keys with QUORUM of other Guardians. Then, combine the public election keys to make a joint election key that is used to encrypt ballots """ # Setup Guardians for i in range(self.NUMBER_OF_GUARDIANS): self.guardians.append( Guardian("guardian_" + str(i), i, self.NUMBER_OF_GUARDIANS, self.QUORUM)) # Setup Mediator self.mediator = KeyCeremonyMediator(self.guardians[0].ceremony_details) # Attendance (Public Key Share) for guardian in self.guardians: self.mediator.announce(guardian) self._assert_message( KeyCeremonyMediator.all_guardians_in_attendance.__qualname__, "Confirms all guardians have shared their public keys", self.mediator.all_guardians_in_attendance(), ) # Run the Key Ceremony process, # Which shares the keys among the guardians orchestrated = self.mediator.orchestrate() self._assert_message( KeyCeremonyMediator.orchestrate.__qualname__, "Executes the key exchange between guardians", orchestrated is not None, ) self._assert_message( KeyCeremonyMediator.all_election_partial_key_backups_available. __qualname__, "Confirm sall guardians have shared their partial key backups", self.mediator.all_election_partial_key_backups_available(), ) # Verification verified = self.mediator.verify() self._assert_message( KeyCeremonyMediator.verify.__qualname__, "Confirms all guardians truthfully executed the ceremony", verified, ) self._assert_message( KeyCeremonyMediator. all_election_partial_key_verifications_received.__qualname__, "Confirms all guardians have submitted a verification of the backups of all other guardians", self.mediator.all_election_partial_key_verifications_received(), ) self._assert_message( KeyCeremonyMediator.all_election_partial_key_backups_verified. __qualname__, "Confirms all guardians have verified the backups of all other guardians", self.mediator.all_election_partial_key_backups_verified(), ) # Joint Key joint_key = self.mediator.publish_joint_key() self._assert_message( KeyCeremonyMediator.publish_joint_key.__qualname__, "Publishes the Joint Election Key", joint_key is not None, ) # Save Validation Keys for guardian in self.guardians: self.coefficient_validation_sets.append( guardian.share_coefficient_validation_set()) # Build the Election self.election_builder.set_public_key(get_optional(joint_key)) self.metadata, self.context = get_optional( self.election_builder.build()) self.constants = ElectionConstants() # Move on to encrypting ballots def step_2_encrypt_votes(self) -> None: """ Using the `CiphertextElectionContext` encrypt ballots for the election """ # Configure the Encryption Device self.device = EncryptionDevice("polling-place-one") self.encrypter = EncryptionMediator(self.metadata, self.context, self.device) self._assert_message( EncryptionDevice.__qualname__, f"Ready to encrypt at location: {self.device.location}", ) # Load some Ballots self.plaintext_ballots = BallotFactory().get_simple_ballots_from_file() self._assert_message( PlaintextBallot.__qualname__, f"Loaded ballots: {len(self.plaintext_ballots)}", len(self.plaintext_ballots) > 0, ) # Encrypt the Ballots for plaintext_ballot in self.plaintext_ballots: encrypted_ballot = self.encrypter.encrypt(plaintext_ballot) self._assert_message( EncryptionMediator.encrypt.__qualname__, f"Ballot Id: {plaintext_ballot.object_id}", encrypted_ballot is not None, ) self.ciphertext_ballots.append(get_optional(encrypted_ballot)) # Next, we cast or spoil the ballots def step_3_cast_and_spoil(self) -> None: """ Accept each ballot by marking it as either cast or spoiled. This example demonstrates one way to accept ballots using the `BallotBox` class """ # Configure the Ballot Box self.ballot_store = BallotStore() self.ballot_box = BallotBox(self.metadata, self.context, self.ballot_store) # Randomly cast or spoil the ballots for ballot in self.ciphertext_ballots: # if randint(0, 1): # accepted_ballot = self.ballot_box.cast(ballot) # else: # accepted_ballot = self.ballot_box.spoil(ballot) accepted_ballot = self.ballot_box.cast(ballot) self._assert_message( BallotBox.__qualname__, f"Accepted Ballot Id: {ballot.object_id} state: {get_optional(accepted_ballot).state}", accepted_ballot is not None, ) def step_4_decrypt_tally(self) -> None: """ Homomorphically combine the selections made on all of the cast ballots and use the Available Guardians to decrypt the combined tally. In this way, no individual voter's cast ballot is ever decrypted drectly. """ # Generate a Homomorphically Accumulated Tally of the ballots self.ciphertext_tally = get_optional( tally_ballots(self.ballot_store, self.metadata, self.context)) self._assert_message( tally_ballots.__qualname__, f""" - cast: {self.ciphertext_tally.count()} - spoiled: {len(self.ciphertext_tally.spoiled_ballots)} Total: {len(self.ciphertext_tally)} """, self.ciphertext_tally is not None, ) # Configure the Decryption self.decrypter = DecryptionMediator(self.metadata, self.context, self.ciphertext_tally) # Announce each guardian as present for guardian in self.guardians: decryption_share = self.decrypter.announce(guardian) self._assert_message( DecryptionMediator.announce.__qualname__, f"Guardian Present: {guardian.object_id}", decryption_share is not None, ) # Get the Plain Text Tally self.plaintext_tally = get_optional( self.decrypter.get_plaintext_tally()) self._assert_message( DecryptionMediator.get_plaintext_tally.__qualname__, "Tally Decrypted", self.plaintext_tally is not None, ) # Now, compare the results self.compare_results() def compare_results(self) -> None: """ Compare the results to ensure the decryption was done correctly """ print(f""" {'-'*40} # Election Results: """) # Create a representation of each contest's tally selection_ids = [ selection.object_id for contest in self.metadata.contests for selection in contest.ballot_selections ] expected_plaintext_tally: Dict[str, int] = { key: 0 for key in selection_ids } # Tally the expected values from the loaded ballots for ballot in self.plaintext_ballots: if (get_optional(self.ballot_store.get( ballot.object_id)).state == BallotBoxState.CAST): for contest in ballot.contests: for selection in contest.ballot_selections: expected_plaintext_tally[ selection.object_id] += selection.to_int() # Compare the expected tally to the decrypted tally for tally_contest in self.plaintext_tally.contests.values(): print(f" Contest: {tally_contest.object_id}") for tally_selection in tally_contest.selections.values(): expected = expected_plaintext_tally[tally_selection.object_id] self._assert_message( f" - Selection: {tally_selection.object_id}", f"expected: {expected}, actual: {tally_selection.tally}", expected == tally_selection.tally, ) print(f"\n{'-'*40}\n") # Compare the expected values for each spoiled ballot for ballot_id, accepted_ballot in self.ciphertext_tally.spoiled_ballots.items( ): if accepted_ballot.state == BallotBoxState.SPOILED: for plaintext_ballot in self.plaintext_ballots: if ballot_id == plaintext_ballot.object_id: print(f"\nSpoiled Ballot: {ballot_id}") for contest in plaintext_ballot.contests: print(f"\n Contest: {contest.object_id}") for selection in contest.ballot_selections: expected = selection.to_int() decrypted_selection = ( self.plaintext_tally.spoiled_ballots[ ballot_id][contest.object_id]. selections[selection.object_id]) self._assert_message( f" - Selection: {selection.object_id}", f"expected: {expected}, actual: {decrypted_selection.tally}", expected == decrypted_selection.tally, ) def step_5_publish_and_verify(self) -> None: """Publish and verify steps of the election""" self.publish_results() self.verify_results() if self.REMOVE_OUTPUT: rmtree(RESULTS_DIR) def publish_results(self) -> None: """ Publish results/artifacts of the election """ publish( self.description, self.context, self.constants, [self.device], self.ballot_store.all(), self.ciphertext_tally.spoiled_ballots.values(), publish_ciphertext_tally(self.ciphertext_tally), self.plaintext_tally, self.coefficient_validation_sets, ) self._assert_message( "Publish", f"Artifacts published to: {RESULTS_DIR}", path.exists(RESULTS_DIR), ) def verify_results(self) -> None: """Verify results of election""" # Deserialize description_from_file = ElectionDescription.from_json_file( DESCRIPTION_FILE_NAME, RESULTS_DIR) self.assertEqual(self.description, description_from_file) context_from_file = CiphertextElectionContext.from_json_file( CONTEXT_FILE_NAME, RESULTS_DIR) self.assertEqual(self.context, context_from_file) constants_from_file = ElectionConstants.from_json_file( CONSTANTS_FILE_NAME, RESULTS_DIR) self.assertEqual(self.constants, constants_from_file) device_name = DEVICE_PREFIX + str(self.device.uuid) device_from_file = EncryptionDevice.from_json_file( device_name, DEVICES_DIR) self.assertEqual(self.device, device_from_file) ciphertext_ballots: List[CiphertextAcceptedBallot] = [] for ballot in self.ballot_store.all(): ballot_name = BALLOT_PREFIX + ballot.object_id ballot_from_file = CiphertextAcceptedBallot.from_json_file( ballot_name, BALLOTS_DIR) self.assertEqual(ballot, ballot_from_file) spoiled_ballots: List[CiphertextAcceptedBallot] = [] for spoiled_ballot in self.ciphertext_tally.spoiled_ballots.values(): ballot_name = BALLOT_PREFIX + spoiled_ballot.object_id spoiled_ballot_from_file = CiphertextAcceptedBallot.from_json_file( ballot_name, SPOILED_DIR) self.assertEqual(spoiled_ballot, spoiled_ballot_from_file) ciphertext_tally_from_file = PublishedCiphertextTally.from_json_file( ENCRYPTED_TALLY_FILE_NAME, RESULTS_DIR) self.assertEqual(publish_ciphertext_tally(self.ciphertext_tally), ciphertext_tally_from_file) plainttext_tally_from_file = PlaintextTally.from_json_file( TALLY_FILE_NAME, RESULTS_DIR) self.assertEqual(self.plaintext_tally, plainttext_tally_from_file) coefficient_validation_sets: List[CoefficientValidationSet] = [] for coefficient_validation_set in self.coefficient_validation_sets: set_name = COEFFICIENT_PREFIX + coefficient_validation_set.owner_id coefficient_validation_set_from_file = ( CoefficientValidationSet.from_json_file( set_name, COEFFICIENTS_DIR)) self.assertEqual(coefficient_validation_set, coefficient_validation_set_from_file) def _assert_message(self, name: str, message: str, condition: Union[Callable, bool] = True) -> None: if callable(condition): result = condition() else: result = condition print(f"{name}: {message}: {result}") self.assertTrue(result)
def test_decrypt_ballot_valid_input_succeeds(self, keypair: ElGamalKeyPair): """ Check that decryption works as expected by encrypting a ballot using the stateful `EncryptionMediator` and then calling the various decrypt functions. """ # TODO: Hypothesis test instead # Arrange election = election_factory.get_simple_election_from_file() metadata, context = election_factory.get_fake_ciphertext_election( election, keypair.public_key ) data = ballot_factory.get_simple_ballot_from_file() device = EncryptionDevice("Location") operator = EncryptionMediator(metadata, context, device) # Act subject = operator.encrypt(data) self.assertIsNotNone(subject) result_from_key = decrypt_ballot_with_secret( subject, metadata, context.crypto_extended_base_hash, keypair.public_key, keypair.secret_key, remove_placeholders=False, ) result_from_nonce = decrypt_ballot_with_nonce( subject, metadata, context.crypto_extended_base_hash, keypair.public_key, remove_placeholders=False, ) result_from_nonce_seed = decrypt_ballot_with_nonce( subject, metadata, context.crypto_extended_base_hash, keypair.public_key, subject.nonce, remove_placeholders=False, ) # Assert self.assertIsNotNone(result_from_key) self.assertIsNotNone(result_from_nonce) self.assertIsNotNone(result_from_nonce_seed) self.assertEqual(data.object_id, subject.object_id) self.assertEqual(data.object_id, result_from_key.object_id) self.assertEqual(data.object_id, result_from_nonce.object_id) self.assertEqual(data.object_id, result_from_nonce_seed.object_id) for description in metadata.get_contests_for(data.ballot_style): expected_entries = ( len(description.ballot_selections) + description.number_elected ) key_contest = [ contest for contest in result_from_key.contests if contest.object_id == description.object_id ][0] nonce_contest = [ contest for contest in result_from_nonce.contests if contest.object_id == description.object_id ][0] seed_contest = [ contest for contest in result_from_nonce_seed.contests if contest.object_id == description.object_id ][0] # Contests may not be voted on the ballot data_contest_exists = [ contest for contest in data.contests if contest.object_id == description.object_id ] if any(data_contest_exists): data_contest = data_contest_exists[0] else: data_contest = None self.assertTrue( key_contest.is_valid( description.object_id, expected_entries, description.number_elected, description.votes_allowed, ) ) self.assertTrue( nonce_contest.is_valid( description.object_id, expected_entries, description.number_elected, description.votes_allowed, ) ) self.assertTrue( seed_contest.is_valid( description.object_id, expected_entries, description.number_elected, description.votes_allowed, ) ) for selection_description in description.ballot_selections: key_selection = [ selection for selection in key_contest.ballot_selections if selection.object_id == selection_description.object_id ][0] nonce_selection = [ selection for selection in nonce_contest.ballot_selections if selection.object_id == selection_description.object_id ][0] seed_selection = [ selection for selection in seed_contest.ballot_selections if selection.object_id == selection_description.object_id ][0] # Selections may be undervoted for a specific contest if any(data_contest_exists): data_selection_exists = [ selection for selection in data_contest.ballot_selections if selection.object_id == selection_description.object_id ] else: data_selection_exists = [] if any(data_selection_exists): data_selection = data_selection_exists[0] self.assertTrue(data_selection.to_int() == key_selection.to_int()) self.assertTrue(data_selection.to_int() == nonce_selection.to_int()) self.assertTrue(data_selection.to_int() == seed_selection.to_int()) else: data_selection = None # TODO: also check edge cases such as: # - placeholder selections are true for under votes self.assertTrue(key_selection.is_valid(selection_description.object_id)) self.assertTrue( nonce_selection.is_valid(selection_description.object_id) ) self.assertTrue( seed_selection.is_valid(selection_description.object_id) )
def test_encrypt_ballot_with_derivative_nonces_regenerates_valid_proofs( self, keypair: ElGamalKeyPair): """ This test verifies that we can regenerate the contest and selection proofs from the cached nonce values """ # TODO: Hypothesis test instead # Arrange election = election_factory.get_simple_election_from_file() metadata, context = election_factory.get_fake_ciphertext_election( election, keypair.public_key) data = ballot_factory.get_simple_ballot_from_file() self.assertTrue(data.is_valid(metadata.ballot_styles[0].object_id)) device = EncryptionDevice("Location") subject = EncryptionMediator(metadata, context, device) # Act result = subject.encrypt(data) self.assertTrue( result.is_valid_encryption(context.crypto_extended_base_hash, keypair.public_key)) # Assert for contest in result.contests: # Find the contest description contest_description = list( filter(lambda i: i.object_id == contest.object_id, metadata.contests))[0] # Homomorpically accumulate the selection encryptions elgamal_accumulation = elgamal_add( * [selection.message for selection in contest.ballot_selections]) # accumulate the selection nonce's aggregate_nonce = add_q( *[selection.nonce for selection in contest.ballot_selections]) regenerated_constant = make_constant_chaum_pedersen( elgamal_accumulation, contest_description.number_elected, aggregate_nonce, keypair.public_key, add_q(contest.nonce, TWO_MOD_Q), ) self.assertTrue( regenerated_constant.is_valid(elgamal_accumulation, keypair.public_key)) for selection in contest.ballot_selections: # Since we know the nonce, we can decrypt the plaintext representation = selection.message.decrypt_known_nonce( keypair.public_key, selection.nonce) # one could also decrypt with the secret key: # representation = selection.message.decrypt(keypair.secret_key) regenerated_disjuctive = make_disjunctive_chaum_pedersen( selection.message, selection.nonce, keypair.public_key, add_q(selection.nonce, TWO_MOD_Q), representation, ) self.assertTrue( regenerated_disjuctive.is_valid(selection.message, keypair.public_key))
class TestEndToEndElection(TestCase): """ Test a complete simple example of executing an End-to-End encrypted election. In a real world scenario all of these steps would not be completed on the same machine. """ NUMBER_OF_GUARDIANS = 5 QUORUM = 3 REMOVE_OUTPUT = False # Step 0 - Configure Election manifest: Manifest election_builder: ElectionBuilder internal_manifest: InternalManifest context: CiphertextElectionContext constants: ElectionConstants # Step 1 - Key Ceremony mediator: KeyCeremonyMediator guardians: List[Guardian] = [] # Step 2 - Encrypt Votes device: EncryptionDevice encrypter: EncryptionMediator plaintext_ballots: List[PlaintextBallot] ciphertext_ballots: List[CiphertextBallot] = [] # Step 3 - Cast and Spoil ballot_store: DataStore ballot_box: BallotBox # Step 4 - Decrypt Tally ciphertext_tally: CiphertextTally plaintext_tally: PlaintextTally plaintext_spoiled_ballots: Dict[str, PlaintextTally] decryption_mediator: DecryptionMediator # Step 5 - Publish guardian_records: List[GuardianRecord] = [] def test_end_to_end_election(self) -> None: """ Execute the simplified end-to-end test demonstrating each component of the system. """ self.step_0_configure_election() self.step_1_key_ceremony() self.step_2_encrypt_votes() self.step_3_cast_and_spoil() self.step_4_decrypt_tally() self.step_5_publish_and_verify() def step_0_configure_election(self) -> None: """ To conduct an election, load an `Manifest` file """ # Load a pre-configured Election Description # TODO: replace with complex election self.manifest = ElectionFactory().get_simple_manifest_from_file() print(f""" {'-'*40}\n # Election Summary: # Scope: {self.manifest.election_scope_id} # Geopolitical Units: {len(self.manifest.geopolitical_units)} # Parties: {len(self.manifest.parties)} # Candidates: {len(self.manifest.candidates)} # Contests: {len(self.manifest.contests)} # Ballot Styles: {len(self.manifest.ballot_styles)}\n {'-'*40}\n """) self._assert_message( Manifest.is_valid.__qualname__, "Verify that the input election meta-data is well-formed", self.manifest.is_valid(), ) # Create an Election Builder self.election_builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, self.manifest) self._assert_message( ElectionBuilder.__qualname__, f"Created with number_of_guardians: {self.NUMBER_OF_GUARDIANS} quorum: {self.QUORUM}", ) # Move on to the Key Ceremony def step_1_key_ceremony(self) -> None: """ Using the NUMBER_OF_GUARDIANS, generate public-private keypairs and share representations of those keys with QUORUM of other Guardians. Then, combine the public election keys to make a joint election key that is used to encrypt ballots """ # Setup Guardians for i in range(self.NUMBER_OF_GUARDIANS): self.guardians.append( Guardian( "guardian_" + str(i + 1), i + 1, self.NUMBER_OF_GUARDIANS, self.QUORUM, )) # Setup Mediator self.mediator = KeyCeremonyMediator("mediator_1", self.guardians[0].ceremony_details) # ROUND 1: Public Key Sharing # Announce for guardian in self.guardians: self.mediator.announce(guardian.share_public_keys()) # Share Keys for guardian in self.guardians: announced_keys = self.mediator.share_announced() for key_set in announced_keys: if guardian.id is not key_set.election.owner_id: guardian.save_guardian_public_keys(key_set) self._assert_message( KeyCeremonyMediator.all_guardians_announced.__qualname__, "Confirms all guardians have shared their public keys", self.mediator.all_guardians_announced(), ) # ROUND 2: Election Partial Key Backup Sharing # Share Backups for sending_guardian in self.guardians: sending_guardian.generate_election_partial_key_backups( identity_auxiliary_encrypt) backups = [] for designated_guardian in self.guardians: if designated_guardian.id != sending_guardian.id: backups.append( sending_guardian.share_election_partial_key_backup( designated_guardian.id)) self.mediator.receive_backups(backups) self._assert_message( KeyCeremonyMediator.receive_backups.__qualname__, "Receive election partial key backups from key owning guardian", len(backups) == NUMBER_OF_GUARDIANS - 1, ) self._assert_message( KeyCeremonyMediator.all_backups_available.__qualname__, "Confirm all guardians have shared their election partial key backups", self.mediator.all_backups_available(), ) # Receive Backups for designated_guardian in self.guardians: backups = self.mediator.share_backups(designated_guardian.id) self._assert_message( KeyCeremonyMediator.share_backups.__qualname__, "Share election partial key backups for the designated guardian", len(backups) == NUMBER_OF_GUARDIANS - 1, ) for backup in backups: designated_guardian.save_election_partial_key_backup(backup) # ROUND 3: Verification of Backups # Verify Backups for designated_guardian in self.guardians: verifications = [] for backup_owner in self.guardians: if designated_guardian.id is not backup_owner.id: verification = ( designated_guardian.verify_election_partial_key_backup( backup_owner.id, identity_auxiliary_encrypt)) verifications.append(verification) self.mediator.receive_backup_verifications(verifications) self._assert_message( KeyCeremonyMediator.all_backups_verified.__qualname__, "Confirms all guardians have verified the backups of all other guardians", self.mediator.all_backups_verified(), ) # FINAL: Publish Joint Key joint_key = self.mediator.publish_joint_key() self._assert_message( KeyCeremonyMediator.publish_joint_key.__qualname__, "Publishes the Joint Election Key", joint_key is not None, ) # Build the Election self.election_builder.set_public_key( get_optional(joint_key).joint_public_key) self.election_builder.set_commitment_hash( get_optional(joint_key).commitment_hash) self.internal_manifest, self.context = get_optional( self.election_builder.build()) self.constants = ElectionConstants() # Move on to encrypting ballots def step_2_encrypt_votes(self) -> None: """ Using the `CiphertextElectionContext` encrypt ballots for the election """ # Configure the Encryption Device self.device = ElectionFactory.get_encryption_device() self.encrypter = EncryptionMediator(self.internal_manifest, self.context, self.device) self._assert_message( EncryptionDevice.__qualname__, f"Ready to encrypt at location: {self.device.location}", ) # Load some Ballots self.plaintext_ballots = BallotFactory().get_simple_ballots_from_file() self._assert_message( PlaintextBallot.__qualname__, f"Loaded ballots: {len(self.plaintext_ballots)}", len(self.plaintext_ballots) > 0, ) # Encrypt the Ballots for plaintext_ballot in self.plaintext_ballots: encrypted_ballot = self.encrypter.encrypt(plaintext_ballot) self._assert_message( EncryptionMediator.encrypt.__qualname__, f"Ballot Id: {plaintext_ballot.object_id}", encrypted_ballot is not None, ) self.ciphertext_ballots.append(get_optional(encrypted_ballot)) # Next, we cast or spoil the ballots def step_3_cast_and_spoil(self) -> None: """ Accept each ballot by marking it as either cast or spoiled. This example demonstrates one way to accept ballots using the `BallotBox` class """ # Configure the Ballot Box self.ballot_store = DataStore() self.ballot_box = BallotBox(self.internal_manifest, self.context, self.ballot_store) # Randomly cast or spoil the ballots for ballot in self.ciphertext_ballots: if randint(0, 1): submitted_ballot = self.ballot_box.cast(ballot) else: submitted_ballot = self.ballot_box.spoil(ballot) self._assert_message( BallotBox.__qualname__, f"Submitted Ballot Id: {ballot.object_id} state: {get_optional(submitted_ballot).state}", submitted_ballot is not None, ) def step_4_decrypt_tally(self) -> None: """ Homomorphically combine the selections made on all of the cast ballots and use the Available Guardians to decrypt the combined tally. In this way, no individual voter's cast ballot is ever decrypted drectly. """ # Generate a Homomorphically Accumulated Tally of the ballots self.ciphertext_tally = get_optional( tally_ballots(self.ballot_store, self.internal_manifest, self.context)) self.ciphertext_ballots = get_ballots(self.ballot_store, BallotBoxState.SPOILED) self._assert_message( tally_ballots.__qualname__, f""" - cast: {self.ciphertext_tally.cast()} - spoiled: {self.ciphertext_tally.spoiled()} Total: {len(self.ciphertext_tally)} """, self.ciphertext_tally is not None, ) # Configure the Decryption ciphertext_ballots = list(self.ciphertext_ballots.values()) self.decryption_mediator = DecryptionMediator( "decryption-mediator", self.context, ) # Announce each guardian as present count = 0 for guardian in self.guardians: guardian_key = guardian.share_election_public_key() tally_share = guardian.compute_tally_share(self.ciphertext_tally, self.context) ballot_shares = guardian.compute_ballot_shares( ciphertext_ballots, self.context) self.decryption_mediator.announce(guardian_key, tally_share, ballot_shares) count += 1 self._assert_message( DecryptionMediator.announce.__qualname__, f"Guardian Present: {guardian.id}", len(self.decryption_mediator.get_available_guardians()) == count, ) # Get the plaintext Tally self.plaintext_tally = get_optional( self.decryption_mediator.get_plaintext_tally( self.ciphertext_tally)) self._assert_message( DecryptionMediator.get_plaintext_tally.__qualname__, "Tally Decrypted", self.plaintext_tally is not None, ) # Get the plaintext Spoiled Ballots self.plaintext_spoiled_ballots = get_optional( self.decryption_mediator.get_plaintext_ballots(ciphertext_ballots)) self._assert_message( DecryptionMediator.get_plaintext_ballots.__qualname__, "Spoiled Ballots Decrypted", self.plaintext_tally is not None, ) # Now, compare the results self.compare_results() def compare_results(self) -> None: """ Compare the results to ensure the decryption was done correctly """ print(f""" {'-'*40} # Election Results: """) # Create a representation of each contest's tally selection_ids = [ selection.object_id for contest in self.manifest.contests for selection in contest.ballot_selections ] expected_plaintext_tally: Dict[str, int] = { key: 0 for key in selection_ids } # Tally the expected values from the loaded ballots for ballot in self.plaintext_ballots: if (get_optional(self.ballot_store.get( ballot.object_id)).state == BallotBoxState.CAST): for contest in ballot.contests: for selection in contest.ballot_selections: expected_plaintext_tally[ selection.object_id] += selection.vote # Compare the expected tally to the decrypted tally for tally_contest in self.plaintext_tally.contests.values(): print(f" Contest: {tally_contest.object_id}") for tally_selection in tally_contest.selections.values(): expected = expected_plaintext_tally[tally_selection.object_id] self._assert_message( f" - Selection: {tally_selection.object_id}", f"expected: {expected}, actual: {tally_selection.tally}", expected == tally_selection.tally, ) print(f"\n{'-'*40}\n") # Compare the expected values for each spoiled ballot for ballot in self.plaintext_ballots: if (get_optional(self.ballot_store.get( ballot.object_id)).state == BallotBoxState.SPOILED): print(f"\nSpoiled Ballot: {ballot.object_id}") for contest in ballot.contests: print(f"\n Contest: {contest.object_id}") for selection in contest.ballot_selections: expected = selection.vote decrypted_selection = (self.plaintext_spoiled_ballots[ ballot.object_id].contests[contest.object_id]. selections[selection.object_id]) self._assert_message( f" - Selection: {selection.object_id}", f"expected: {expected}, actual: {decrypted_selection.tally}", expected == decrypted_selection.tally, ) def step_5_publish_and_verify(self) -> None: """Publish and verify steps of the election""" self.publish_results() self.verify_results() if self.REMOVE_OUTPUT: rmtree(RESULTS_DIR) def publish_results(self) -> None: """ Publish results/artifacts of the election """ self.guardian_records = [ guardian.publish() for guardian in self.guardians ] publish( self.manifest, self.context, self.constants, [self.device], self.ballot_store.all(), self.plaintext_spoiled_ballots.values(), self.ciphertext_tally.publish(), self.plaintext_tally, self.guardian_records, RESULTS_DIR, ) self._assert_message( "Publish", f"Artifacts published to: {RESULTS_DIR}", path.exists(RESULTS_DIR), ) def verify_results(self) -> None: """Verify results of election""" # Deserialize manifest_from_file = Manifest.from_json_file(MANIFEST_FILE_NAME, RESULTS_DIR) self.assertEqual(self.manifest, manifest_from_file) context_from_file = CiphertextElectionContext.from_json_file( CONTEXT_FILE_NAME, RESULTS_DIR) self.assertEqual(self.context, context_from_file) constants_from_file = ElectionConstants.from_json_file( CONSTANTS_FILE_NAME, RESULTS_DIR) self.assertEqual(self.constants, constants_from_file) device_name = DEVICE_PREFIX + str(self.device.uuid) device_from_file = EncryptionDevice.from_json_file( device_name, DEVICES_DIR) self.assertEqual(self.device, device_from_file) for ballot in self.ballot_store.all(): name = BALLOT_PREFIX + ballot.object_id ballot_from_file = SubmittedBallot.from_json_file( name, BALLOTS_DIR) self.assertEqual(ballot, ballot_from_file) for spoiled_ballot in self.plaintext_spoiled_ballots.values(): name = BALLOT_PREFIX + spoiled_ballot.object_id spoiled_ballot_from_file = PlaintextTally.from_json_file( name, SPOILED_DIR) self.assertEqual(spoiled_ballot, spoiled_ballot_from_file) published_ciphertext_tally_from_file = PublishedCiphertextTally.from_json_file( ENCRYPTED_TALLY_FILE_NAME, RESULTS_DIR) self.assertEqual(self.ciphertext_tally.publish(), published_ciphertext_tally_from_file) plainttext_tally_from_file = PlaintextTally.from_json_file( TALLY_FILE_NAME, RESULTS_DIR) self.assertEqual(self.plaintext_tally, plainttext_tally_from_file) for guardian_record in self.guardian_records: set_name = COEFFICIENT_PREFIX + guardian_record.guardian_id guardian_record_from_file = GuardianRecord.from_json_file( set_name, GUARDIAN_DIR) self.assertEqual(guardian_record, guardian_record_from_file) def _assert_message(self, name: str, message: str, condition: Union[Callable, bool] = True) -> None: if callable(condition): result = condition() else: result = condition print(f"{name}: {message}: {result}") self.assertTrue(result)
class ElectionSampleDataGenerator: """ Generates sample data for an example election using the "Hamilton County" data set. """ election_factory: ElectionFactory ballot_factory: BallotFactory encryption_device: EncryptionDevice encrypter: EncryptionMediator def __init__(self) -> None: """Initialize the class""" self.election_factory = ElectionFactory() self.ballot_factory = BallotFactory() self.encryption_device = EncryptionDevice(f"polling-place-{str(uuid.uuid1())}") def generate( self, number_of_ballots: int = DEFAULT_NUMBER_OF_BALLOTS, spoil_rate: int = DEFAULT_SPOIL_RATE, use_all_guardians: bool = DEFAULT_USE_ALL_GUARDIANS, ): """ Generate the sample data set """ # Clear the results directory rmtree(RESULTS_DIR, ignore_errors=True) # Configure the election ( public_data, private_data, ) = self.election_factory.get_hamilton_election_with_encryption_context() plaintext_ballots = self.ballot_factory.generate_fake_plaintext_ballots_for_election( public_data.metadata, number_of_ballots ) self.encrypter = EncryptionMediator( public_data.metadata, public_data.context, self.encryption_device ) # Encrypt some ballots ciphertext_ballots: List[CiphertextBallot] = [] for plaintext_ballot in plaintext_ballots: ciphertext_ballots.append( get_optional(self.encrypter.encrypt(plaintext_ballot)) ) ballot_store = BallotStore() ballot_box = BallotBox(public_data.metadata, public_data.context, ballot_store) # Randomly cast/spoil the ballots accepted_ballots: List[CiphertextAcceptedBallot] = [] for ballot in ciphertext_ballots: if randint(0, 100) < spoil_rate: accepted_ballots.append(ballot_box.spoil(ballot)) else: accepted_ballots.append(ballot_box.cast(ballot)) # Tally ciphertext_tally = get_optional( tally_ballots(ballot_store, public_data.metadata, public_data.context) ) # Decrypt decrypter = DecryptionMediator( public_data.metadata, public_data.context, ciphertext_tally ) for i, guardian in enumerate(private_data.guardians): if use_all_guardians or i < QUORUM: decrypter.announce(guardian) plaintext_tally = get_optional(decrypter.get_plaintext_tally()) # Publish publish( public_data.description, public_data.context, public_data.constants, [self.encryption_device], accepted_ballots, ciphertext_tally.spoiled_ballots.values(), publish_ciphertext_tally(ciphertext_tally), plaintext_tally, public_data.guardians, ) publish_private_data( plaintext_ballots, ciphertext_ballots, private_data.guardians )
class ElectionSampleDataGenerator: """ Generates sample data for an example election using the "Hamilton County" data set. """ election_factory: ElectionFactory ballot_factory: BallotFactory encryption_device: EncryptionDevice encrypter: EncryptionMediator def __init__(self) -> None: """Initialize the class""" self.election_factory = ElectionFactory() self.ballot_factory = BallotFactory() self.encryption_device = EncryptionDevice(f"polling-place-{uuid.uuid1}") def generate(self, number_of_ballots: int = DEFAULT_NUMBER_OF_BALLOTS): """ Generate the sample data set """ rmtree(RESULTS_DIR, ignore_errors=True) ( public_data, private_data, ) = self.election_factory.get_hamilton_election_with_encryption_context() plaintext_ballots = self.ballot_factory.generate_fake_plaintext_ballots_for_election( public_data.metadata, number_of_ballots ) self.encrypter = EncryptionMediator( public_data.metadata, public_data.context, self.encryption_device ) ciphertext_ballots: List[CiphertextBallot] = [] for plaintext_ballot in plaintext_ballots: ciphertext_ballots.append( get_optional(self.encrypter.encrypt(plaintext_ballot)) ) ballot_store = BallotStore() ballot_box = BallotBox(public_data.metadata, public_data.context, ballot_store) accepted_ballots: List[CiphertextAcceptedBallot] = [] for ballot in ciphertext_ballots: if randint(0, 100) % 10 == 0: accepted_ballots.append(ballot_box.spoil(ballot)) else: accepted_ballots.append(ballot_box.cast(ballot)) ciphertext_tally = get_optional( tally_ballots(ballot_store, public_data.metadata, public_data.context) ) decrypter = DecryptionMediator( public_data.metadata, public_data.context, ciphertext_tally ) for guardian in private_data.guardians: decrypter.announce(guardian) plaintext_tally = get_optional(decrypter.get_plaintext_tally()) publish( public_data.description, public_data.context, public_data.constants, accepted_ballots, ciphertext_tally, plaintext_tally, public_data.guardians, ) publish_private_data( plaintext_ballots, ciphertext_ballots, private_data.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
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 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)
def generate( self, number_of_ballots: int = DEFAULT_NUMBER_OF_BALLOTS, cast_spoil_ratio: int = CAST_SPOIL_RATIO, ): """ Generate the sample data set """ # Clear the results directory rmtree(RESULTS_DIR, ignore_errors=True) # Configure the election ( public_data, private_data, ) = self.election_factory.get_hamilton_election_with_encryption_context( ) plaintext_ballots = self.ballot_factory.generate_fake_plaintext_ballots_for_election( public_data.metadata, number_of_ballots) self.encrypter = EncryptionMediator(public_data.metadata, public_data.context, self.encryption_device) # Encrypt some ballots ciphertext_ballots: List[CiphertextBallot] = [] for plaintext_ballot in plaintext_ballots: ciphertext_ballots.append( get_optional(self.encrypter.encrypt(plaintext_ballot))) ballot_store = BallotStore() ballot_box = BallotBox(public_data.metadata, public_data.context, ballot_store) # Randomly cast/spoil the ballots accepted_ballots: List[CiphertextAcceptedBallot] = [] for ballot in ciphertext_ballots: if randint(0, 100) % cast_spoil_ratio == 0: accepted_ballots.append(ballot_box.spoil(ballot)) else: accepted_ballots.append(ballot_box.cast(ballot)) # Tally ciphertext_tally = get_optional( tally_ballots(ballot_store, public_data.metadata, public_data.context)) # Decrypt decrypter = DecryptionMediator(public_data.metadata, public_data.context, ciphertext_tally) for i, guardian in enumerate(private_data.guardians): if i < QUORUM or not THRESHOLD_ONLY: decrypter.announce(guardian) plaintext_tally = get_optional(decrypter.get_plaintext_tally()) # Publish publish( public_data.description, public_data.context, public_data.constants, [self.encryption_device], accepted_ballots, ciphertext_tally.spoiled_ballots.values(), publish_ciphertext_tally(ciphertext_tally), plaintext_tally, public_data.guardians, ) publish_private_data(plaintext_ballots, ciphertext_ballots, private_data.guardians)
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)
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)
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 generate( self, number_of_ballots: int = DEFAULT_NUMBER_OF_BALLOTS, spoil_rate: int = DEFAULT_SPOIL_RATE, use_all_guardians: bool = DEFAULT_USE_ALL_GUARDIANS, ): """ Generate the sample data set """ # Clear the results directory rmtree(RESULTS_DIR, ignore_errors=True) # Configure the election ( manifest, private_data, ) = self.election_factory.get_hamilton_manifest_with_encryption_context() plaintext_ballots = ( self.ballot_factory.generate_fake_plaintext_ballots_for_election( manifest.internal_manifest, number_of_ballots ) ) self.encrypter = EncryptionMediator( manifest.internal_manifest, manifest.context, self.encryption_device ) # Encrypt some ballots ciphertext_ballots: List[CiphertextBallot] = [] for plaintext_ballot in plaintext_ballots: ciphertext_ballots.append( get_optional(self.encrypter.encrypt(plaintext_ballot)) ) ballot_store = DataStore() ballot_box = BallotBox( manifest.internal_manifest, manifest.context, ballot_store ) # Randomly cast/spoil the ballots submitted_ballots: List[SubmittedBallot] = [] for ballot in ciphertext_ballots: if randint(0, 100) < spoil_rate: submitted_ballots.append(ballot_box.spoil(ballot)) else: submitted_ballots.append(ballot_box.cast(ballot)) # Tally spoiled_ciphertext_ballots = get_ballots(ballot_store, BallotBoxState.SPOILED) ciphertext_tally = get_optional( tally_ballots(ballot_store, manifest.internal_manifest, manifest.context) ) # Decrypt mediator = DecryptionMediator("sample-manifest-decrypter", manifest.context) available_guardians = ( private_data.guardians if use_all_guardians else private_data.guardians[0:QUORUM] ) DecryptionHelper.perform_decryption_setup( available_guardians, mediator, manifest.context, ciphertext_tally, spoiled_ciphertext_ballots, ) plaintext_tally = get_optional(mediator.get_plaintext_tally(ciphertext_tally)) plaintext_spoiled_ballots = get_optional( mediator.get_plaintext_ballots(spoiled_ciphertext_ballots) ) # Publish publish( manifest.manifest, manifest.context, manifest.constants, [self.encryption_device], submitted_ballots, plaintext_spoiled_ballots.values(), ciphertext_tally.publish(), plaintext_tally, manifest.guardians, ) publish_private_data( plaintext_ballots, ciphertext_ballots, private_data.guardians )