def test_tally_spoiled_ballots_accumulates_valid_tally( self, everything: ELECTIONS_AND_BALLOTS_TUPLE_TYPE): # Arrange metadata, ballots, secret_key, context = everything # Tally the plaintext ballots for comparison later plaintext_tallies = accumulate_plaintext_ballots(ballots) # encrypt each ballot store = BallotStore() seed_hash = EncryptionDevice("Location").get_hash() for ballot in ballots: encrypted_ballot = encrypt_ballot(ballot, metadata, context, seed_hash) seed_hash = encrypted_ballot.tracking_hash self.assertIsNotNone(encrypted_ballot) # add to the ballot store store.set( encrypted_ballot.object_id, from_ciphertext_ballot(encrypted_ballot, BallotBoxState.SPOILED), ) # act result = tally_ballots(store, metadata, context) self.assertIsNotNone(result) # Assert decrypted_tallies = self._decrypt_with_secret(result, secret_key) self.assertCountEqual(plaintext_tallies, decrypted_tallies) for value in decrypted_tallies.values(): self.assertEqual(0, value) self.assertEqual(len(ballots), len(result.spoiled_ballots))
def test_cast_ballot(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) election = election_factory.get_fake_election() metadata, context = election_factory.get_fake_ciphertext_election( election, keypair.public_key ) store = BallotStore() source = election_factory.get_fake_ballot(metadata) self.assertTrue(source.is_valid(metadata.ballot_styles[0].object_id)) # Act data = encrypt_ballot(source, metadata, context, SEED_HASH) result = accept_ballot(data, BallotBoxState.CAST, metadata, context, store) # Assert expected = store.get(source.object_id) self.assertEqual(expected.state, BallotBoxState.CAST) self.assertEqual(result.state, BallotBoxState.CAST) self.assertEqual(expected.object_id, result.object_id) # Test failure modes self.assertIsNone( accept_ballot(data, BallotBoxState.CAST, metadata, context, store) ) # cannot cast again self.assertIsNone( accept_ballot(data, BallotBoxState.SPOILED, metadata, context, store) ) # cannot cspoil a ballot already cast
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_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, ) # Send Over Network send_over_network(self.ciphertext_ballots)
def handle_ballot(request: AcceptBallotRequest, state: BallotBoxState) -> Any: ballot = CiphertextBallot.from_json_object(request.ballot) description = ElectionDescription.from_json_object(request.description) internal_description = InternalElectionDescription(description) context = CiphertextElectionContext.from_json_object(request.context) accepted_ballot = accept_ballot( ballot, state, internal_description, context, BallotStore(), ) return accepted_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
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 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 test_ballot_store(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) election = election_factory.get_fake_election() metadata, context = election_factory.get_fake_ciphertext_election( election, keypair.public_key) # get an encrypted fake ballot to work with fake_ballot = election_factory.get_fake_ballot(metadata) encrypted_ballot = encrypt_ballot(fake_ballot, metadata, context, SEED_HASH) # Set up the ballot store subject = BallotStore() data_cast = CiphertextAcceptedBallot( encrypted_ballot.object_id, encrypted_ballot.ballot_style, encrypted_ballot.description_hash, encrypted_ballot.previous_tracking_hash, encrypted_ballot.contests, encrypted_ballot.tracking_hash, encrypted_ballot.timestamp, ) data_cast.state = BallotBoxState.CAST data_spoiled = CiphertextAcceptedBallot( encrypted_ballot.object_id, encrypted_ballot.ballot_style, encrypted_ballot.description_hash, encrypted_ballot.previous_tracking_hash, encrypted_ballot.contests, encrypted_ballot.tracking_hash, encrypted_ballot.timestamp, ) data_spoiled.state = BallotBoxState.SPOILED self.assertIsNone(subject.get("cast")) self.assertIsNone(subject.get("spoiled")) # try to set a ballot with an unknown state self.assertFalse( subject.set( "unknown", CiphertextAcceptedBallot( encrypted_ballot.object_id, encrypted_ballot.ballot_style, encrypted_ballot.description_hash, encrypted_ballot.previous_tracking_hash, encrypted_ballot.contests, encrypted_ballot.tracking_hash, encrypted_ballot.timestamp, ), )) # Act self.assertTrue(subject.set("cast", data_cast)) self.assertTrue(subject.set("spoiled", data_spoiled)) self.assertEqual(subject.get("cast"), data_cast) self.assertEqual(subject.get("spoiled"), data_spoiled) self.assertEqual(subject.exists("cast"), (True, data_cast)) self.assertEqual(subject.exists("spoiled"), (True, data_spoiled)) # test mutate state data_cast.state = BallotBoxState.UNKNOWN self.assertEqual(subject.exists("cast"), (False, data_cast)) # test remove self.assertTrue(subject.set("cast", None)) self.assertEqual(subject.exists("cast"), (False, None))
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 test_tally_ballot_invalid_input_fails( self, everything: ELECTIONS_AND_BALLOTS_TUPLE_TYPE): # Arrange metadata, ballots, secret_key, context = everything # encrypt each ballot store = BallotStore() seed_hash = EncryptionDevice("Location").get_hash() for ballot in ballots: encrypted_ballot = encrypt_ballot(ballot, metadata, context, seed_hash) seed_hash = encrypted_ballot.tracking_hash self.assertIsNotNone(encrypted_ballot) # add to the ballot store store.set( encrypted_ballot.object_id, from_ciphertext_ballot(encrypted_ballot, BallotBoxState.CAST), ) subject = CiphertextTally("my-tally", metadata, context) # act cached_ballots = store.all() first_ballot = cached_ballots[0] first_ballot.state = BallotBoxState.UNKNOWN # verify an UNKNOWN state ballot fails self.assertIsNone(tally_ballot(first_ballot, subject)) self.assertFalse(subject.append(first_ballot)) # cast a ballot first_ballot.state = BallotBoxState.CAST self.assertTrue(subject.append(first_ballot)) # try to append a spoiled ballot first_ballot.state = BallotBoxState.SPOILED self.assertFalse(subject.append(first_ballot)) # Verify accumulation fails if the selection collection is empty if first_ballot.state == BallotBoxState.CAST: self.assertFalse( subject.cast[first_ballot.object_id].elgamal_accumulate([])) # pop the cast ballot subject._cast_ballot_ids.pop() # reset to cast first_ballot.state = BallotBoxState.CAST self.assertTrue( self._cannot_erroneously_mutate_state(subject, first_ballot, BallotBoxState.CAST)) self.assertTrue( self._cannot_erroneously_mutate_state(subject, first_ballot, BallotBoxState.SPOILED)) self.assertTrue( self._cannot_erroneously_mutate_state(subject, first_ballot, BallotBoxState.UNKNOWN)) # verify a spoiled ballot cannot be added twice first_ballot.state = BallotBoxState.SPOILED self.assertTrue(subject.append(first_ballot)) self.assertFalse(subject.append(first_ballot)) # verify an already spoiled ballot cannot be cast first_ballot.state = BallotBoxState.CAST self.assertFalse(subject.append(first_ballot)) # pop the spoiled ballot subject.spoiled_ballots.pop(first_ballot.object_id) # verify a cast ballot cannot be added twice first_ballot.state = BallotBoxState.CAST self.assertTrue(subject.append(first_ballot)) self.assertFalse(subject.append(first_ballot)) # verify an already cast ballot cannot be spoiled first_ballot.state = BallotBoxState.SPOILED self.assertFalse(subject.append(first_ballot))