class TestEncrypt(unittest.TestCase): 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 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)) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( ElectionFactory.get_selection_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) def test_encrypt_selection_valid_input_succeeds( self, selection_description: Tuple[str, SelectionDescription], keypair: ElGamalKeyPair, seed: ElementModQ, random_seed: int, ): # Arrange _, description = selection_description random = Random(random_seed) subject = ballot_factory.get_random_selection_from(description, random) # Act result = encrypt_selection(subject, description, keypair.public_key, seed) # Assert self.assertIsNotNone(result) self.assertIsNotNone(result.message) self.assertTrue( result.is_valid_encryption(description.crypto_hash(), keypair.public_key)) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( ElectionFactory.get_selection_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) def test_encrypt_selection_valid_input_tampered_encryption_fails( self, selection_description: Tuple[str, SelectionDescription], keypair: ElGamalKeyPair, seed: ElementModQ, random_seed: int, ): # Arrange _, description = selection_description random = Random(random_seed) subject = ballot_factory.get_random_selection_from(description, random) # Act result = encrypt_selection(subject, description, keypair.public_key, seed, should_verify_proofs=False) self.assertTrue( result.is_valid_encryption(description.crypto_hash(), keypair.public_key)) # tamper with the encryption malformed_encryption = deepcopy(result) malformed_message = malformed_encryption.message._replace( alpha=mult_p(result.message.alpha, TWO_MOD_P)) malformed_encryption.message = malformed_message # tamper with the proof malformed_proof = deepcopy(result) malformed_disjunctive = malformed_proof.proof._replace( a0=mult_p(result.proof.a0, TWO_MOD_P)) malformed_proof.proof = malformed_disjunctive # Assert self.assertFalse( malformed_encryption.is_valid_encryption(description.crypto_hash(), keypair.public_key)) self.assertFalse( malformed_proof.is_valid_encryption(description.crypto_hash(), keypair.public_key)) 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, nonce) # Assert self.assertIsNotNone(result) self.assertTrue( result.is_valid_encryption(hash_context, keypair.public_key)) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( ElectionFactory.get_contest_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) 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) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( ElectionFactory.get_contest_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) 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)) @unittest.skip("runs forever") @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( ElectionFactory.get_contest_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(1, 6), integers(), ) 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_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, seed, should_verify_proofs=True, ) self.assertIsNotNone(subject) 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, seed) self.assertIsNone(subject) 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_encrypt_ballot_with_stateful_composer_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) data = election_factory.get_fake_ballot(metadata) 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.assertTrue( result.is_valid_encryption(context.crypto_extended_base_hash, keypair.public_key)) 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(context.crypto_extended_base_hash, keypair.public_key)) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given(elgamal_keypairs()) 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 TestDecrypt(unittest.TestCase): @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given( ElectionFactory.get_selection_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) def test_decrypt_selection_valid_input_succeeds( self, selection_description: Tuple[str, SelectionDescription], keypair: ElGamalKeyPair, nonce_seed: ElementModQ, random_seed: int, ): # Arrange random = Random(random_seed) _, description = selection_description data = ballot_factory.get_random_selection_from(description, random) # Act subject = encrypt_selection(data, description, keypair.public_key, nonce_seed) self.assertIsNotNone(subject) result_from_key = decrypt_selection_with_secret( subject, description, keypair.public_key, keypair.secret_key ) result_from_nonce = decrypt_selection_with_nonce( subject, description, keypair.public_key ) result_from_nonce_seed = decrypt_selection_with_nonce( subject, description, keypair.public_key, nonce_seed ) # Assert self.assertIsNotNone(result_from_key) self.assertIsNotNone(result_from_nonce) self.assertIsNotNone(result_from_nonce_seed) self.assertEqual(data.plaintext, result_from_key.plaintext) self.assertEqual(data.plaintext, result_from_nonce.plaintext) self.assertEqual(data.plaintext, result_from_nonce_seed.plaintext) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given( ElectionFactory.get_selection_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) def test_decrypt_selection_valid_input_tampered_fails( self, selection_description: Tuple[str, SelectionDescription], keypair: ElGamalKeyPair, seed: ElementModQ, random_seed: int, ): # Arrange _, description = selection_description random = Random(random_seed) data = ballot_factory.get_random_selection_from(description, random) # Act subject = encrypt_selection(data, description, keypair.public_key, seed) # tamper with the encryption malformed_encryption = deepcopy(subject) malformed_message = malformed_encryption.message._replace( alpha=mult_p(subject.message.alpha, TWO_MOD_P) ) malformed_encryption.message = malformed_message # tamper with the proof malformed_proof = deepcopy(subject) malformed_disjunctive = malformed_proof.proof._replace( a0=mult_p(subject.proof.a0, TWO_MOD_P) ) malformed_proof.proof = malformed_disjunctive result_from_key_malformed_encryption = decrypt_selection_with_secret( malformed_encryption, description, keypair.public_key, keypair.secret_key ) result_from_key_malformed_proof = decrypt_selection_with_secret( malformed_proof, description, keypair.public_key, keypair.secret_key ) result_from_nonce_malformed_encryption = decrypt_selection_with_nonce( malformed_encryption, description, keypair.public_key ) result_from_nonce_malformed_proof = decrypt_selection_with_nonce( malformed_proof, description, keypair.public_key ) # Assert self.assertIsNone(result_from_key_malformed_encryption) self.assertIsNone(result_from_key_malformed_proof) self.assertIsNone(result_from_nonce_malformed_encryption) self.assertIsNone(result_from_nonce_malformed_proof) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given( ElectionFactory.get_selection_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) def test_decrypt_selection_tampered_nonce_fails( self, selection_description: Tuple[str, SelectionDescription], keypair: ElGamalKeyPair, nonce_seed: ElementModQ, random_seed: int, ): # Arrange random = Random(random_seed) _, description = selection_description data = ballot_factory.get_random_selection_from(description, random) # Act subject = encrypt_selection(data, description, keypair.public_key, nonce_seed) self.assertIsNotNone(subject) # Tamper with the nonce by setting it to an aribtrary value subject.nonce = nonce_seed result_from_nonce_seed = decrypt_selection_with_nonce( subject, description, keypair.public_key, nonce_seed ) # Assert self.assertIsNone(result_from_nonce_seed) @settings( deadline=timedelta(milliseconds=5000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given( ElectionFactory.get_contest_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) 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)) @settings( deadline=timedelta(milliseconds=5000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) @given( ElectionFactory.get_contest_description_well_formed(), elgamal_keypairs(), elements_mod_q_no_zero(), integers(), ) 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) @settings( deadline=timedelta(milliseconds=2000), 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(elgamal_keypairs()) 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) ) @settings( deadline=timedelta(milliseconds=2000), 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(elgamal_keypairs()) 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 get_well_formed_contest_and_ballot( draw: _DrawType, desc=get_contest_description_well_formed(), random_seed=integers() ) -> Tuple[ContestDescription, PlaintextBallotContest]: (object_id, contest_description_with_placeholders) = draw(desc)