def test_encrypt_contest_valid_input_succeeds( self, contest_description: ContestDescription, keypair: ElGamalKeyPair, nonce_seed: ElementModQ, random_seed: int, ): # Arrange _, description = contest_description random = Random(random_seed) subject = ballot_factory.get_random_contest_from(description, random) # Act result = encrypt_contest(subject, description, keypair.public_key, nonce_seed) # Assert self.assertIsNotNone(result) self.assertTrue( result.is_valid_encryption(description.crypto_hash(), keypair.public_key)) # The encrypted contest should include an entry for each possible selection # and placeholders for each seat expected_entries = (len(description.ballot_selections) + description.number_elected) self.assertEqual(len(result.ballot_selections), expected_entries)
def test_encrypt_contest_overvote_fails( self, contest_description: ContestDescription, keypair: ElGamalKeyPair, seed: ElementModQ, overvotes: int, random_seed: int, ): # Arrange _, description = contest_description random = Random(random_seed) subject = ballot_factory.get_random_contest_from(description, random) highest_sequence = max( *[ selection.sequence_order for selection in description.ballot_selections ], 1, ) for i in range(overvotes): extra = ballot_factory.get_random_selection_from( description.ballot_selections[0], random) extra.sequence_order = highest_sequence + i + 1 subject.ballot_selections.append(extra) # Act result = encrypt_contest(subject, description, keypair.public_key, seed) # Assert self.assertIsNone(result)
def test_encrypt_simple_contest_referendum_succeeds(self): # Arrange keypair = elgamal_keypair_from_secret(int_to_q(2)) nonce = randbelow(Q) ballot_selections = [ SelectionDescription( "some-object-id-affirmative", "some-candidate-id-affirmative", 0 ), SelectionDescription( "some-object-id-negative", "some-candidate-id-negative", 1 ), ] placeholder_selections = [ SelectionDescription( "some-object-id-placeholder", "some-candidate-id-placeholder", 2 ) ] metadata = ContestDescriptionWithPlaceholders( "some-contest-object-id", "some-electoral-district-id", 0, VoteVariationType.one_of_m, 1, 1, "some-referendum-contest-name", ballot_selections, None, None, placeholder_selections, ) hash_context = metadata.crypto_hash() subject = contest_from(metadata) self.assertTrue( subject.is_valid( metadata.object_id, len(metadata.ballot_selections), metadata.number_elected, metadata.votes_allowed, ) ) # Act result = encrypt_contest( subject, metadata, keypair.public_key, ONE_MOD_Q, nonce ) # Assert self.assertIsNotNone(result) self.assertTrue( result.is_valid_encryption(hash_context, keypair.public_key, ONE_MOD_Q) )
def test_encrypt_contest_duplicate_selection_object_ids_fails(self): """ This is an example test of a failing test where the contest description is malformed """ random_seed = 0 description = ContestDescription( object_id="[email protected]", electoral_district_id="[email protected]", sequence_order=1, vote_variation=VoteVariationType.n_of_m, number_elected=1, votes_allowed=1, name="", ballot_selections=[ SelectionDescription( object_id="[email protected]", candidate_id="*****@*****.**", sequence_order=0, ), # Note the selection description is the same as the first sequence element SelectionDescription( object_id="[email protected]", candidate_id="*****@*****.**", sequence_order=1, ), ], ) keypair = elgamal_keypair_from_secret(TWO_MOD_Q) seed = ONE_MOD_Q # Bypass checking the validity of the description data = ballot_factory.get_random_contest_from( description, Random(0), suppress_validity_check=True ) placeholders = generate_placeholder_selections_from( description, description.number_elected ) description_with_placeholders = contest_description_with_placeholders_from( description, placeholders ) # Act subject = encrypt_contest( data, description_with_placeholders, keypair.public_key, ONE_MOD_Q, seed ) self.assertIsNone(subject)
def test_encrypt_contest_valid_input_tampered_proof_fails( self, contest_description: ContestDescription, keypair: ElGamalKeyPair, nonce_seed: ElementModQ, random_seed: int, ): # Arrange _, description = contest_description random = Random(random_seed) subject = ballot_factory.get_random_contest_from(description, random) # Act result = encrypt_contest( subject, description, keypair.public_key, ONE_MOD_Q, nonce_seed ) self.assertTrue( result.is_valid_encryption( description.crypto_hash(), keypair.public_key, ONE_MOD_Q ) ) # tamper with the proof malformed_proof = deepcopy(result) altered_a = mult_p(result.proof.pad, TWO_MOD_P) malformed_disjunctive = ConstantChaumPedersenProof( altered_a, malformed_proof.proof.data, malformed_proof.proof.challenge, malformed_proof.proof.response, malformed_proof.proof.constant, ) malformed_proof.proof = malformed_disjunctive # remove the proof missing_proof = deepcopy(result) missing_proof.proof = None # Assert self.assertFalse( malformed_proof.is_valid_encryption( description.crypto_hash(), keypair.public_key, ONE_MOD_Q ) ) self.assertFalse( missing_proof.is_valid_encryption( description.crypto_hash(), keypair.public_key, ONE_MOD_Q ) )
def test_encrypt_contest_manually_formed_contest_description_valid_succeeds(self): description = ContestDescription( object_id="[email protected]", electoral_district_id="[email protected]", sequence_order=1, vote_variation=VoteVariationType.n_of_m, number_elected=1, votes_allowed=1, name="", ballot_selections=[ SelectionDescription( object_id="[email protected]", candidate_id="*****@*****.**", sequence_order=0, ), SelectionDescription( object_id="[email protected]", candidate_id="*****@*****.**", sequence_order=1, ), ], ballot_title=None, ballot_subtitle=None, ) keypair = elgamal_keypair_from_secret(TWO_MOD_Q) seed = ONE_MOD_Q #################### data = ballot_factory.get_random_contest_from(description, Random(0)) placeholders = generate_placeholder_selections_from( description, description.number_elected ) description_with_placeholders = contest_description_with_placeholders_from( description, placeholders ) # Act subject = encrypt_contest( data, description_with_placeholders, keypair.public_key, ONE_MOD_Q, seed, should_verify_proofs=True, ) self.assertIsNotNone(subject)
def test_encrypt_contest_valid_input_tampered_proof_fails( self, contest_description: ContestDescription, keypair: ElGamalKeyPair, nonce_seed: ElementModQ, random_seed: int, ): # Arrange _, description = contest_description random = Random(random_seed) subject = ballot_factory.get_random_contest_from(description, random) # Act result = encrypt_contest(subject, description, keypair.public_key, nonce_seed) self.assertTrue( result.is_valid_encryption(description.crypto_hash(), keypair.public_key)) # tamper with the proof malformed_proof = deepcopy(result) malformed_disjunctive = malformed_proof.proof._replace( a=mult_p(result.proof.a, TWO_MOD_P)) malformed_proof.proof = malformed_disjunctive # remove the proof missing_proof = deepcopy(result) missing_proof.proof = None # Assert self.assertFalse( malformed_proof.is_valid_encryption(description.crypto_hash(), keypair.public_key)) self.assertFalse( missing_proof.is_valid_encryption(description.crypto_hash(), keypair.public_key))
def test_decrypt_contest_invalid_input_fails( self, contest_description: Tuple[str, ContestDescription], keypair: ElGamalKeyPair, nonce_seed: ElementModQ, random_seed: int, ): # Arrange _, description = contest_description random = Random(random_seed) data = ballot_factory.get_random_contest_from(description, random) placeholders = generate_placeholder_selections_from( description, description.number_elected ) description_with_placeholders = contest_description_with_placeholders_from( description, placeholders ) self.assertTrue(description_with_placeholders.is_valid()) # Act subject = encrypt_contest( data, description_with_placeholders, keypair.public_key, nonce_seed ) self.assertIsNotNone(subject) # tamper with the nonce subject.nonce = int_to_q_unchecked(1) result_from_nonce = decrypt_contest_with_nonce( subject, description_with_placeholders, keypair.public_key, remove_placeholders=False, ) result_from_nonce_seed = decrypt_contest_with_nonce( subject, description_with_placeholders, keypair.public_key, nonce_seed, remove_placeholders=False, ) # Assert self.assertIsNone(result_from_nonce) self.assertIsNone(result_from_nonce_seed) # Tamper with the encryption subject.ballot_selections[0].message = ElGamalCiphertext(TWO_MOD_P, TWO_MOD_P) result_from_key_tampered = decrypt_contest_with_secret( subject, description_with_placeholders, keypair.public_key, keypair.secret_key, remove_placeholders=False, ) result_from_nonce_tampered = decrypt_contest_with_nonce( subject, description_with_placeholders, keypair.public_key, remove_placeholders=False, ) result_from_nonce_seed_tampered = decrypt_contest_with_nonce( subject, description_with_placeholders, keypair.public_key, nonce_seed, remove_placeholders=False, ) # Assert self.assertIsNone(result_from_key_tampered) self.assertIsNone(result_from_nonce_tampered) self.assertIsNone(result_from_nonce_seed_tampered)
def test_decrypt_contest_valid_input_succeeds( self, contest_description: Tuple[str, ContestDescription], keypair: ElGamalKeyPair, nonce_seed: ElementModQ, random_seed: int, ): # Arrange _, description = contest_description random = Random(random_seed) data = ballot_factory.get_random_contest_from(description, random) placeholders = generate_placeholder_selections_from( description, description.number_elected ) description_with_placeholders = contest_description_with_placeholders_from( description, placeholders ) self.assertTrue(description_with_placeholders.is_valid()) # Act subject = encrypt_contest( data, description_with_placeholders, keypair.public_key, nonce_seed ) self.assertIsNotNone(subject) # Decrypt the contest, but keep the placeholders # so we can verify the selection count matches as expected in the test result_from_key = decrypt_contest_with_secret( subject, description_with_placeholders, keypair.public_key, keypair.secret_key, remove_placeholders=False, ) result_from_nonce = decrypt_contest_with_nonce( subject, description_with_placeholders, keypair.public_key, remove_placeholders=False, ) result_from_nonce_seed = decrypt_contest_with_nonce( subject, description_with_placeholders, keypair.public_key, nonce_seed, remove_placeholders=False, ) # Assert self.assertIsNotNone(result_from_key) self.assertIsNotNone(result_from_nonce) self.assertIsNotNone(result_from_nonce_seed) # The decrypted contest should include an entry for each possible selection # and placeholders for each seat expected_entries = ( len(description.ballot_selections) + description.number_elected ) self.assertTrue( result_from_key.is_valid( description.object_id, expected_entries, description.number_elected, description.votes_allowed, ) ) self.assertTrue( result_from_nonce.is_valid( description.object_id, expected_entries, description.number_elected, description.votes_allowed, ) ) self.assertTrue( result_from_nonce_seed.is_valid( description.object_id, expected_entries, description.number_elected, description.votes_allowed, ) ) # Assert the ballot selections sum to the expected number of selections key_selected = sum( [selection.to_int() for selection in result_from_key.ballot_selections] ) nonce_selected = sum( [selection.to_int() for selection in result_from_nonce.ballot_selections] ) seed_selected = sum( [ selection.to_int() for selection in result_from_nonce_seed.ballot_selections ] ) self.assertEqual(key_selected, nonce_selected) self.assertEqual(seed_selected, nonce_selected) self.assertEqual(description.number_elected, key_selected) # Assert each selection is valid for selection_description in description.ballot_selections: key_selection = [ selection for selection in result_from_key.ballot_selections if selection.object_id == selection_description.object_id ][0] nonce_selection = [ selection for selection in result_from_nonce.ballot_selections if selection.object_id == selection_description.object_id ][0] seed_selection = [ selection for selection in result_from_nonce_seed.ballot_selections if selection.object_id == selection_description.object_id ][0] data_selections_exist = [ selection for selection in data.ballot_selections if selection.object_id == selection_description.object_id ] # It's possible there are no selections in the original data collection # since it is valid to pass in a ballot that is not complete if any(data_selections_exist): self.assertTrue( data_selections_exist[0].to_int() == key_selection.to_int() ) self.assertTrue( data_selections_exist[0].to_int() == nonce_selection.to_int() ) self.assertTrue( data_selections_exist[0].to_int() == seed_selection.to_int() ) # 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))