def test_encrypt_simple_selection_malformed_data_fails(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) nonce = randbelow(Q) metadata = SelectionDescription("some-selection-object-id", "some-candidate-id", 1) hash_context = metadata.crypto_hash() subject = selection_from(metadata) self.assertTrue(subject.is_valid(metadata.object_id)) # Act result = encrypt_selection(subject, metadata, keypair.public_key, nonce) # tamper with the description_hash malformed_description_hash = deepcopy(result) malformed_description_hash.description_hash = TWO_MOD_Q # remove the proof missing_proof = deepcopy(result) missing_proof.proof = None # Assert self.assertFalse( malformed_description_hash.is_valid_encryption( hash_context, keypair.public_key)) self.assertFalse( missing_proof.is_valid_encryption(hash_context, keypair.public_key))
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 plaintext_voted_ballot(draw: _DrawType, metadata: InternalElectionDescription): """ Given an `InternalElectionDescription` object, generates an arbitrary `PlaintextBallot` with the choices made randomly. :param draw: Hidden argument, used by Hypothesis. :param metadata: Any `InternalElectionDescription` """ num_ballot_styles = len(metadata.ballot_styles) assert num_ballot_styles > 0, "invalid election with no ballot styles" # pick a ballot style at random ballot_style = metadata.ballot_styles[draw( integers(0, num_ballot_styles - 1))] contests = metadata.get_contests_for(ballot_style.object_id) assert len(contests) > 0, "invalid ballot style with no contests in it" voted_contests: List[PlaintextBallotContest] = [] for contest in contests: assert contest.is_valid(), "every contest needs to be valid" n = contest.number_elected # we need exactly this many 1's, and the rest 0's ballot_selections = contest.ballot_selections assert len(ballot_selections) >= n random = Random(draw(integers())) random.shuffle(ballot_selections) cut_point = draw(integers(0, n)) yes_votes = ballot_selections[:cut_point] no_votes = ballot_selections[cut_point:] voted_selections = [ selection_from( description, is_placeholder=False, is_affirmative=True) for description in yes_votes ] + [ selection_from( description, is_placeholder=False, is_affirmative=False) for description in no_votes ] voted_contests.append( PlaintextBallotContest(contest.object_id, voted_selections)) return PlaintextBallot(str(draw(uuids())), ballot_style.object_id, voted_contests)
def get_random_selection_from( description: SelectionDescription, random_source: Random, is_placeholder=False, ) -> PlaintextBallotSelection: selected = bool(random_source.randint(0, 1)) return selection_from(description, is_placeholder, selected)
def test_encrypt_simple_selection_succeeds(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) nonce = randbelow(Q) metadata = SelectionDescription("some-selection-object-id", "some-candidate-id", 1) hash_context = metadata.crypto_hash() subject = selection_from(metadata) self.assertTrue(subject.is_valid(metadata.object_id)) # Act result = encrypt_selection(subject, metadata, keypair.public_key, nonce) # Assert self.assertIsNotNone(result) self.assertIsNotNone(result.message) self.assertTrue( result.is_valid_encryption(hash_context, keypair.public_key))
def get_random_contest_from( self, description: ContestDescription, random: Random, suppress_validity_check=False, with_trues=False, ) -> PlaintextBallotContest: """ Get a randomly filled contest for the given description that may be undervoted and may include explicitly false votes. Since this is only used for testing, the random number generator (`random`) must be provided to make this function deterministic. """ if not suppress_validity_check: assert description.is_valid( ), "the contest description must be valid" selections: List[PlaintextBallotSelection] = list() voted = 0 for selection_description in description.ballot_selections: selection = self.get_random_selection_from(selection_description, random) # the caller may force a true value voted += selection.to_int() if voted <= 1 and selection.to_int() and with_trues: selections.append(selection) continue # Possibly append the true selection, indicating an undervote if voted <= description.number_elected and bool( random.randint(0, 1)) == 1: selections.append(selection) # Possibly append the false selections as well, indicating some choices # may be explicitly false elif bool(random.randint(0, 1)) == 1: selections.append(selection_from(selection_description)) return PlaintextBallotContest(description.object_id, selections)
def row_to_plaintext_ballot(self, row: Dict[str, Any]) -> PlaintextBallot: ballot_type = row["BallotType"] ballot_id = row["BallotId"] pbcontests: List[PlaintextBallotContest] = [] contest_titles: Set[str] = self.style_map[ballot_type] for title in contest_titles: # This is insanely complicated. The challenge is that we have the Dominion data structures, # which has its own column names, but we have to connect that with all of the ElectionGuard # structures, which don't just let you follow from one to the other. Instead, it's a twisty # world of object_ids. Thus, we need mappings to go from one to the next, and have to do all # this extra bookkeeping, in Python dictionaries, to make all the connections. contest = self.contest_map[title] candidate_ids = [s.candidate_id for s in contest.ballot_selections] column_names = [ self.all_candidate_ids_to_columns[c] for c in candidate_ids ] voter_intents = [row[x] > 0 for x in column_names] selections: List[SelectionDescription] = contest.ballot_selections plaintexts = [ selection_from( description=selections[i], is_placeholder=False, is_affirmative=voter_intents[i], ) for i in range(0, len(selections)) ] pbcontests.append( PlaintextBallotContest( object_id=contest.object_id, ballot_selections=plaintexts, )) return PlaintextBallot( object_id=ballot_id, ballot_style=self.ballotstyle_map[ballot_type].object_id, contests=pbcontests, )