def test_encrypt_ballot_simple_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) nonce_seed = TWO_MOD_Q # TODO: Ballot Factory subject = election_factory.get_fake_ballot(internal_manifest) self.assertTrue( subject.is_valid(internal_manifest.ballot_styles[0].object_id)) # Act result = encrypt_ballot(subject, internal_manifest, context, SEED) result_from_seed = encrypt_ballot(subject, internal_manifest, context, SEED, nonce_seed) # Assert self.assertIsNotNone(result) self.assertIsNotNone(result.code) self.assertIsNotNone(result_from_seed) self.assertTrue( result.is_valid_encryption( internal_manifest.manifest_hash, keypair.public_key, context.crypto_extended_base_hash, )) self.assertTrue( result_from_seed.is_valid_encryption( internal_manifest.manifest_hash, keypair.public_key, context.crypto_extended_base_hash, ))
def test_encrypt_ballot_simple_succeeds(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) nonce_seed = TWO_MOD_Q # TODO: Ballot Factory subject = election_factory.get_fake_ballot(metadata) self.assertTrue(subject.is_valid(metadata.ballot_styles[0].object_id)) # Act result = encrypt_ballot(subject, metadata, context, SEED_HASH) tracker_code = result.get_tracker_code() result_from_seed = encrypt_ballot(subject, metadata, context, SEED_HASH, nonce_seed) # Assert self.assertIsNotNone(result) self.assertIsNotNone(result.tracking_id) self.assertIsNotNone(tracker_code) self.assertIsNotNone(result_from_seed) self.assertTrue( result.is_valid_encryption(context.crypto_extended_base_hash, keypair.public_key)) self.assertTrue( result_from_seed.is_valid_encryption( context.crypto_extended_base_hash, keypair.public_key))
def test_tally_cast_ballots_accumulates_valid_tally( self, everything: ELECTIONS_AND_BALLOTS_TUPLE_TYPE): # Arrange ( _election_description, internal_manifest, ballots, secret_key, context, ) = everything # Tally the plaintext ballots for comparison later plaintext_tallies = accumulate_plaintext_ballots(ballots) # encrypt each ballot store = DataStore() encryption_seed = ElectionFactory.get_encryption_device().get_hash() for ballot in ballots: encrypted_ballot = encrypt_ballot(ballot, internal_manifest, context, encryption_seed) encryption_seed = encrypted_ballot.code self.assertIsNotNone(encrypted_ballot) # add to the ballot store store.set( encrypted_ballot.object_id, from_ciphertext_ballot(encrypted_ballot, BallotBoxState.CAST), ) # act result = tally_ballots(store, internal_manifest, context) self.assertIsNotNone(result) # Assert decrypted_tallies = self._decrypt_with_secret(result, secret_key) self.assertEqual(plaintext_tallies, decrypted_tallies)
def test_eg_conversion(self, state: DominionBallotsAndContext, seed: ElementModQ) -> None: ied = InternalElectionDescription(state.ed) ballot_box = BallotBox(ied, state.cec) seed_hash = EncryptionDevice("Location").get_hash() nonces = Nonces(seed)[0:len(state.ballots)] for b, n in zip(state.ballots, nonces): eb = encrypt_ballot(b, ied, state.cec, seed_hash, n) self.assertIsNotNone(eb) pb = decrypt_ballot_with_secret( eb, ied, state.cec.crypto_extended_base_hash, state.cec.elgamal_public_key, state.secret_key, ) self.assertEqual(b, pb) self.assertGreater(len(eb.contests), 0) cast_result = ballot_box.cast(eb) self.assertIsNotNone(cast_result) tally = tally_ballots(ballot_box._store, ied, state.cec) self.assertIsNotNone(tally) results = decrypt_tally_with_secret(tally, state.secret_key) self.assertEqual(len(results.keys()), len(state.id_map.keys())) for obj_id in results.keys(): self.assertIn(obj_id, state.id_map) cvr_sum = int(state.dominion_cvrs.data[state.id_map[obj_id]].sum()) decryption = results[obj_id] self.assertEqual(cvr_sum, decryption)
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 encrypt_ballots(request: EncryptBallotsRequest = Body(...)) -> Any: """ Encrypt one or more ballots """ ballots = [ PlaintextBallot.from_json_object(ballot) for ballot in request.ballots ] description = InternalElectionDescription( ElectionDescription.from_json_object(request.description)) context = CiphertextElectionContext.from_json_object(request.context) seed_hash = read_json_object(request.seed_hash, ElementModQ) nonce: Optional[ElementModQ] = (read_json_object( request.nonce, ElementModQ) if request.nonce else None) encrypted_ballots = [] current_hash = seed_hash for ballot in ballots: encrypted_ballot = encrypt_ballot(ballot, description, context, current_hash, nonce) if not encrypted_ballot: raise HTTPException(status_code=500, detail="Ballot failed to encrypt") encrypted_ballots.append(encrypted_ballot) current_hash = get_optional(encrypted_ballot.tracking_hash) response = EncryptBallotsResponse( encrypted_ballots=[ ballot.to_json_object() for ballot in encrypted_ballots ], next_seed_hash=write_json_object(current_hash), ) return response
def encrypt(self, ballot: dict, deterministic: bool = False) -> dict: if not self.context.joint_key: raise MissingJointKey() ballot_style: str = self.context.election.ballot_styles[0].object_id contests: List[PlaintextBallotContest] = [] for contest in self.context.election_metadata.get_contests_for( ballot_style): selections: List[PlaintextBallotSelection] = [ selection_from( selection, False, selection.object_id in ballot[contest.object_id]) for selection in contest.ballot_selections ] contests.append( PlaintextBallotContest(contest.object_id, selections)) plaintext_ballot = PlaintextBallot(self.ballot_id, ballot_style, contests) # TODO: store the audit information somewhere encrypted_ballot = serialize( encrypt_ballot(plaintext_ballot, self.context.election_metadata, self.context.election_context, ElementModQ(0), self.context.joint_key if deterministic else None, True)) return encrypted_ballot
def test_ballot_box_spoil_ballot(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) store = DataStore() source = election_factory.get_fake_ballot(internal_manifest) self.assertTrue( source.is_valid(internal_manifest.ballot_styles[0].object_id)) # Act data = encrypt_ballot(source, internal_manifest, context, SEED) subject = BallotBox(internal_manifest, context, store) result = subject.spoil(data) # Assert expected = store.get(source.object_id) self.assertEqual(expected.state, BallotBoxState.SPOILED) self.assertEqual(result.state, BallotBoxState.SPOILED) self.assertEqual(expected.object_id, result.object_id) # Test failure modes self.assertIsNone(subject.spoil(data)) # cannot spoil again self.assertIsNone( subject.cast(data)) # cannot cast a ballot alraedy spoiled
def setUp(self) -> None: # Election setup election_factory = ElectionFactory() keypair = elgamal_keypair_from_secret(int_to_q(2)) manifest = election_factory.get_fake_manifest() ( self.internal_manifest, self.context, ) = election_factory.get_fake_ciphertext_election( manifest, keypair.public_key) device_hash = ElectionFactory.get_encryption_device().get_hash() # Arrange ballots self.plaintext_ballot = election_factory.get_fake_ballot( self.internal_manifest) ciphertext_ballot = encrypt_ballot(self.plaintext_ballot, self.internal_manifest, self.context, device_hash) self.ballot_nonce = ciphertext_ballot.nonce self.submitted_ballot = from_ciphertext_ballot(ciphertext_ballot, BallotBoxState.CAST)
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 encrypt_ballot_helper( ied: InternalElectionDescription, cec: CiphertextElectionContext, seed_hash: ElementModQ, input_tuple: Tuple[PlaintextBallot, ElementModQ], ) -> CiphertextBallot: # pragma: no cover """ Given a ballot and the associated metadata, encrypt it. Note that this method is meant to be used with `functools.partial`, so we can create a function that only takes the final tuple argument while remembering all the rest. """ b, n = input_tuple # Coverage note: you'll see a directive on this method and on the other methods # used for the parallel mapping. For whatever reason, the Python coverage tool # can't figure out that they're running, so we'll exclude them. # Performance note: Nearly 2x performance boost by disabling proof verification # here. We do verify the tally proofs at the end, so doing all this extra work # here is in the "would be nice if cycles were free" category, but this is the # inner loop of the most performance-sensitive part of our code. return get_optional( encrypt_ballot(b, ied, cec, seed_hash, n, should_verify_proofs=False))
def test_tally_ballot_invalid_input_fails( self, everything: ELECTIONS_AND_BALLOTS_TUPLE_TYPE): # Arrange ( _election_description, internal_manifest, ballots, _secret_key, context, ) = everything # encrypt each ballot store = DataStore() encryption_seed = ElectionFactory.get_encryption_device().get_hash() for ballot in ballots: encrypted_ballot = encrypt_ballot(ballot, internal_manifest, context, encryption_seed) encryption_seed = encrypted_ballot.code self.assertIsNotNone(encrypted_ballot) # add to the ballot store store.set( encrypted_ballot.object_id, from_ciphertext_ballot(encrypted_ballot, BallotBoxState.CAST), ) tally = CiphertextTally("my-tally", internal_manifest, 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, tally)) self.assertFalse(tally.append(first_ballot)) # cast a ballot first_ballot.state = BallotBoxState.CAST self.assertTrue(tally.append(first_ballot)) # try to append a spoiled ballot first_ballot.state = BallotBoxState.SPOILED self.assertFalse(tally.append(first_ballot)) # Verify accumulation fails if the selection collection is empty if first_ballot.state == BallotBoxState.CAST: self.assertFalse( tally.contests[first_ballot.object_id].accumulate_contest([])) # pylint: disable=protected-access # pop the cast ballot tally._cast_ballot_ids.pop() # reset to cast first_ballot.state = BallotBoxState.CAST self.assertTrue( self._cannot_erroneously_mutate_state(tally, first_ballot, BallotBoxState.CAST)) self.assertTrue( self._cannot_erroneously_mutate_state(tally, first_ballot, BallotBoxState.SPOILED)) self.assertTrue( self._cannot_erroneously_mutate_state(tally, first_ballot, BallotBoxState.UNKNOWN)) # verify a cast ballot cannot be added twice first_ballot.state = BallotBoxState.CAST self.assertTrue(tally.append(first_ballot)) self.assertFalse(tally.append(first_ballot)) # verify an already submitted ballot cannot be changed or readded first_ballot.state = BallotBoxState.SPOILED self.assertFalse(tally.append(first_ballot))
def r_encrypt_and_write( ied: InternalElectionDescription, cec: CiphertextElectionContext, seed_hash: ElementModQ, root_dir: Optional[str], manifest_aggregator: Optional[ActorHandle], progressbar_actor: Optional[ActorHandle], bpf: BallotPlaintextFactory, nonces: Nonces, nonce_indices: List[int], *plaintext_ballot_dicts: Dict[str, Any], ) -> Optional[TALLY_TYPE]: # pragma: no cover """ Remotely encrypts a list of ballots and their associated nonces. If a `root_dir` is specified, the encrypted ballots are written to disk, otherwise no disk activity. What's returned is a `RemoteTallyResult`. If the ballots were written, the `manifest_aggregator` actor will be notified. A "partial tally" of the encrypted ballots is returned. """ try: manifest = make_fresh_manifest( root_dir) if root_dir is not None else None num_ballots = len(plaintext_ballot_dicts) assert (len(nonce_indices) == num_ballots ), "mismatching numbers of nonces and ballots!" assert num_ballots > 0, "need at least one ballot" ptally_final: Optional[TALLY_TYPE] = None for i in range(0, num_ballots): pballot = bpf.row_to_plaintext_ballot(plaintext_ballot_dicts[i]) cballot = ciphertext_ballot_to_accepted( get_optional( encrypt_ballot( pballot, ied, cec, seed_hash, nonces[nonce_indices[i]], should_verify_proofs=False, ))) if manifest is not None: manifest.write_ciphertext_ballot(cballot, num_retries=NUM_WRITE_RETRIES) if progressbar_actor is not None: progressbar_actor.update_completed.remote("Ballots", 1) ptally = ciphertext_ballot_to_dict(cballot) ptally_final = (sequential_tally([ptally_final, ptally]) if ptally_final else ptally) if progressbar_actor is not None: progressbar_actor.update_completed.remote("Tallies", 1) if manifest is not None and manifest_aggregator is not None: manifest_aggregator.add.remote(manifest) return ptally_final except Exception as e: log_and_print(f"Unexpected exception in r_encrypt_and_write: {e}", True) return None
def test_accumulation_encryption_decryption( self, everything: ELECTIONS_AND_BALLOTS_TUPLE_TYPE, nonce: ElementModQ, ): """ Tests that decryption is the inverse of encryption over arbitrarily generated elections and ballots. This test uses an abitrarily generated dataset with a single public-private keypair for the election encryption context. It also manually verifies that homomorphic accumulation works as expected. """ # Arrange election_description, metadata, ballots, secret_key, context = everything # Tally the plaintext ballots for comparison later plaintext_tallies = accumulate_plaintext_ballots(ballots) num_ballots = len(ballots) num_contests = len(metadata.contests) zero_nonce, *nonces = Nonces(nonce)[:num_ballots + 1] self.assertEqual(len(nonces), num_ballots) self.assertTrue(len(metadata.contests) > 0) # Generatea valid encryption of zero encrypted_zero = elgamal_encrypt(0, zero_nonce, context.elgamal_public_key) # Act encrypted_ballots = [] # encrypt each ballot for i in range(num_ballots): encrypted_ballot = encrypt_ballot(ballots[i], metadata, context, SEED_HASH, nonces[i]) encrypted_ballots.append(encrypted_ballot) # sanity check the encryption self.assertIsNotNone(encrypted_ballot) self.assertEqual(num_contests, len(encrypted_ballot.contests)) # decrypt the ballot with secret and verify it matches the plaintext decrypted_ballot = decrypt_ballot_with_secret( ballot=encrypted_ballot, election_metadata=metadata, crypto_extended_base_hash=context.crypto_extended_base_hash, public_key=context.elgamal_public_key, secret_key=secret_key, remove_placeholders=True, ) self.assertEqual(ballots[i], decrypted_ballot) # homomorphically accumualte the encrypted ballot representations encrypted_tallies = _accumulate_encrypted_ballots( encrypted_zero, encrypted_ballots) decrypted_tallies = {} for object_id in encrypted_tallies.keys(): decrypted_tallies[object_id] = encrypted_tallies[ object_id].decrypt(secret_key) # loop through the contest descriptions and verify # the decrypted tallies match the plaintext tallies for contest in metadata.contests: # Sanity check the generated data self.assertTrue(len(contest.ballot_selections) > 0) self.assertTrue(len(contest.placeholder_selections) > 0) decrypted_selection_tallies = [ decrypted_tallies[selection.object_id] for selection in contest.ballot_selections ] decrypted_placeholder_tallies = [ decrypted_tallies[placeholder.object_id] for placeholder in contest.placeholder_selections ] plaintext_tally_values = [ plaintext_tallies[selection.object_id] for selection in contest.ballot_selections ] # verify the plaintext tallies match the decrypted tallies self.assertEqual(decrypted_selection_tallies, plaintext_tally_values) # validate the right number of selections including placeholders across all ballots self.assertEqual( contest.number_elected * num_ballots, sum(decrypted_selection_tallies) + sum(decrypted_placeholder_tallies), )
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 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))