def ciphertext_elections(draw: _DrawType, election_description: ElectionDescription): """ Generates a `CiphertextElectionContext` with a single public-private key pair as the encryption context. In a real election, the key ceremony would be used to generate a shared public key. :param draw: Hidden argument, used by Hypothesis. :param election_description: An `ElectionDescription` object, with which the `CiphertextElectionContext` will be associated :return: a tuple of a `CiphertextElectionContext` and the secret key associated with it """ secret_key, public_key = draw(elgamal_keypairs()) commitment_hash = draw(elements_mod_q_no_zero()) ciphertext_election_with_secret: CIPHERTEXT_ELECTIONS_TUPLE_TYPE = ( secret_key, make_ciphertext_election_context( number_of_guardians=1, quorum=1, elgamal_public_key=public_key, commitment_hash=commitment_hash, description_hash=election_description.crypto_hash(), ), ) return ciphertext_election_with_secret
def elgamal_keypairs(draw: _DrawType): """ Generates an arbitrary ElGamal secret/public keypair. :param draw: Hidden argument, used by Hypothesis. """ e = draw(elements_mod_q_no_zero()) return elgamal_keypair_from_secret(e if e != ONE_MOD_Q else TWO_MOD_Q)
class TestChaumPedersen(TestCase): def test_cp_proofs_simple(self): keypair = elgamal_keypair_from_secret(TWO_MOD_Q) nonce = ONE_MOD_Q seed = TWO_MOD_Q message = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) decryption = message.partial_decrypt(keypair.secret_key) proof = make_chaum_pedersen( message, keypair.secret_key, decryption, seed, ONE_MOD_Q ) bad_proof = make_chaum_pedersen( message, keypair.secret_key, TWO_MOD_Q, seed, ONE_MOD_Q ) self.assertTrue( proof.is_valid(message, keypair.public_key, decryption, ONE_MOD_Q) ) self.assertFalse( bad_proof.is_valid(message, keypair.public_key, decryption, ONE_MOD_Q) ) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( elgamal_keypairs(), elements_mod_q_no_zero(), elements_mod_q(), integers(0, 100), integers(0, 100), ) def test_ccp_proof( self, keypair: ElGamalKeyPair, nonce: ElementModQ, seed: ElementModQ, constant: int, bad_constant: int, ): if constant == bad_constant: bad_constant = constant + 1 message = get_optional(elgamal_encrypt(constant, nonce, keypair.public_key)) decryption = message.partial_decrypt(keypair.secret_key) proof = make_chaum_pedersen( message, keypair.secret_key, decryption, seed, ONE_MOD_Q ) bad_proof = make_chaum_pedersen( message, keypair.secret_key, int_to_p(bad_constant), seed, ONE_MOD_Q ) self.assertTrue( proof.is_valid(message, keypair.public_key, decryption, ONE_MOD_Q) ) self.assertFalse( bad_proof.is_valid(message, keypair.public_key, decryption, ONE_MOD_Q) )
class ElGamalTest(unittest.TestCase): @given( integers(0, 20), elements_mod_q_no_zero(), elements_mod_q_no_zero(), elgamal_keypairs(), ) def test_reencryption( self, plaintext: int, nonce1: ElementModQ, nonce2: ElementModQ, keypair: ElGamalKeyPair, ) -> None: c1 = get_optional(elgamal_encrypt(plaintext, nonce1, keypair.public_key)) c2 = get_optional(elgamal_reencrypt(keypair.public_key, nonce2, c1)) self.assertEqual(plaintext, c1.decrypt(keypair.secret_key)) self.assertNotEqual(c1, c2) self.assertEqual(plaintext, c2.decrypt(keypair.secret_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)
class TestDisjunctiveChaumPedersen(TestCase): def test_djcp_proofs_simple(self): # doesn't get any simpler than this keypair = elgamal_keypair_from_secret(TWO_MOD_Q) nonce = ONE_MOD_Q seed = TWO_MOD_Q message0 = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) proof0 = make_disjunctive_chaum_pedersen_zero( message0, nonce, keypair.public_key, seed ) proof0bad = make_disjunctive_chaum_pedersen_one( message0, nonce, keypair.public_key, seed ) self.assertTrue(proof0.is_valid(message0, keypair.public_key)) self.assertFalse(proof0bad.is_valid(message0, keypair.public_key)) message1 = get_optional(elgamal_encrypt(1, nonce, keypair.public_key)) proof1 = make_disjunctive_chaum_pedersen_one( message1, nonce, keypair.public_key, seed ) proof1bad = make_disjunctive_chaum_pedersen_zero( message1, nonce, keypair.public_key, seed ) self.assertTrue(proof1.is_valid(message1, keypair.public_key)) self.assertFalse(proof1bad.is_valid(message1, keypair.public_key)) def test_djcp_proof_invalid_inputs(self): # this is here to push up our coverage keypair = elgamal_keypair_from_secret(TWO_MOD_Q) nonce = ONE_MOD_Q seed = TWO_MOD_Q message0 = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) self.assertRaises( Exception, make_disjunctive_chaum_pedersen, message0, nonce, keypair.public_key, seed, 3, ) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given(elgamal_keypairs(), elements_mod_q_no_zero(), elements_mod_q()) def test_djcp_proof_zero( self, keypair: ElGamalKeyPair, nonce: ElementModQ, seed: ElementModQ ): message = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) proof = make_disjunctive_chaum_pedersen_zero( message, nonce, keypair.public_key, seed ) proof_bad = make_disjunctive_chaum_pedersen_one( message, nonce, keypair.public_key, seed ) self.assertTrue(proof.is_valid(message, keypair.public_key)) self.assertFalse(proof_bad.is_valid(message, keypair.public_key)) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given(elgamal_keypairs(), elements_mod_q_no_zero(), elements_mod_q()) def test_djcp_proof_one( self, keypair: ElGamalKeyPair, nonce: ElementModQ, seed: ElementModQ ): message = get_optional(elgamal_encrypt(1, nonce, keypair.public_key)) proof = make_disjunctive_chaum_pedersen_one( message, nonce, keypair.public_key, seed ) proof_bad = make_disjunctive_chaum_pedersen_zero( message, nonce, keypair.public_key, seed ) self.assertTrue(proof.is_valid(message, keypair.public_key)) self.assertFalse(proof_bad.is_valid(message, keypair.public_key)) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given(elgamal_keypairs(), elements_mod_q_no_zero(), elements_mod_q()) def test_djcp_proof_broken( self, keypair: ElGamalKeyPair, nonce: ElementModQ, seed: ElementModQ ): # verify two different ways to generate an invalid C-P proof. message = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) message_bad = get_optional(elgamal_encrypt(2, nonce, keypair.public_key)) proof = make_disjunctive_chaum_pedersen_zero( message, nonce, keypair.public_key, seed ) proof_bad = make_disjunctive_chaum_pedersen_zero( message_bad, nonce, keypair.public_key, seed ) self.assertFalse(proof_bad.is_valid(message_bad, keypair.public_key)) self.assertFalse(proof.is_valid(message_bad, keypair.public_key))
class TestConstantChaumPedersen(TestCase): def test_ccp_proofs_simple_encryption_of_zero(self): keypair = elgamal_keypair_from_secret(TWO_MOD_Q) nonce = ONE_MOD_Q seed = TWO_MOD_Q message = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) proof = make_constant_chaum_pedersen( message, 0, nonce, keypair.public_key, seed ) bad_proof = make_constant_chaum_pedersen( message, 1, nonce, keypair.public_key, seed ) self.assertTrue(proof.is_valid(message, keypair.public_key)) self.assertFalse(bad_proof.is_valid(message, keypair.public_key)) def test_ccp_proofs_simple_encryption_of_one(self): keypair = elgamal_keypair_from_secret(TWO_MOD_Q) nonce = ONE_MOD_Q seed = TWO_MOD_Q message = get_optional(elgamal_encrypt(1, nonce, keypair.public_key)) proof = make_constant_chaum_pedersen( message, 1, nonce, keypair.public_key, seed ) bad_proof = make_constant_chaum_pedersen( message, 0, nonce, keypair.public_key, seed ) self.assertTrue(proof.is_valid(message, keypair.public_key)) self.assertFalse(bad_proof.is_valid(message, keypair.public_key)) @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( elgamal_keypairs(), elements_mod_q_no_zero(), elements_mod_q(), integers(0, 100), integers(0, 100), ) def test_ccp_proof( self, keypair: ElGamalKeyPair, nonce: ElementModQ, seed: ElementModQ, constant: int, bad_constant: int, ): # assume() slows down the test-case generation # so assume(constant != bad_constant) if constant == bad_constant: bad_constant = constant + 1 message = get_optional(elgamal_encrypt(constant, nonce, keypair.public_key)) message_bad = get_optional( elgamal_encrypt(bad_constant, nonce, keypair.public_key) ) proof = make_constant_chaum_pedersen( message, constant, nonce, keypair.public_key, seed ) self.assertTrue(proof.is_valid(message, keypair.public_key)) proof_bad1 = make_constant_chaum_pedersen( message_bad, constant, nonce, keypair.public_key, seed ) self.assertFalse(proof_bad1.is_valid(message_bad, keypair.public_key)) proof_bad2 = make_constant_chaum_pedersen( message, bad_constant, nonce, keypair.public_key, seed ) self.assertFalse(proof_bad2.is_valid(message, keypair.public_key)) proof_bad3 = proof._replace(constant=-1) self.assertFalse(proof_bad3.is_valid(message, keypair.public_key))
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 TestModularArithmetic(unittest.TestCase): @given(elements_mod_q()) def test_add_q(self, q: ElementModQ): as_int = add_q(q, 1) as_elem = add_q(q, ElementModQ(1)) self.assertEqual(as_int, as_elem) @given(elements_mod_q()) def test_a_plus_bc_q(self, q: ElementModQ): as_int = a_plus_bc_q(q, 1, 1) as_elem = a_plus_bc_q(q, ElementModQ(1), ElementModQ(1)) self.assertEqual(as_int, as_elem) @given(elements_mod_q()) def test_a_minus_b_q(self, q: ElementModQ): as_int = a_minus_b_q(q, 1) as_elem = a_minus_b_q(q, ElementModQ(1)) self.assertEqual(as_int, as_elem) @given(elements_mod_q()) def test_div_q(self, q: ElementModQ): as_int = div_q(q, 1) as_elem = div_q(q, ElementModQ(1)) self.assertEqual(as_int, as_elem) @given(elements_mod_p()) def test_div_p(self, p: ElementModQ): as_int = div_p(p, 1) as_elem = div_p(p, ElementModP(1)) self.assertEqual(as_int, as_elem) def test_no_mult_inv_of_zero(self): self.assertRaises(Exception, mult_inv_p, ZERO_MOD_P) @given(elements_mod_p_no_zero()) def test_mult_inverses(self, elem: ElementModP): inv = mult_inv_p(elem) self.assertEqual(mult_p(elem, inv), ONE_MOD_P) @given(elements_mod_p()) def test_mult_identity(self, elem: ElementModP): self.assertEqual(elem, mult_p(elem)) def test_mult_noargs(self): self.assertEqual(ONE_MOD_P, mult_p()) def test_add_noargs(self): self.assertEqual(ZERO_MOD_Q, add_q()) def test_properties_for_constants(self): self.assertNotEqual(G, 1) self.assertEqual((R * Q) % P, P - 1) self.assertLess(Q, P) self.assertLess(G, P) self.assertLess(R, P) def test_simple_powers(self): gp = int_to_p(G) self.assertEqual(gp, g_pow_p(ONE_MOD_Q)) self.assertEqual(ONE_MOD_P, g_pow_p(ZERO_MOD_Q)) @given(elements_mod_q()) def test_in_bounds_q(self, q: ElementModQ): self.assertTrue(q.is_in_bounds()) too_big = q.to_int() + Q too_small = q.to_int() - Q self.assertFalse(int_to_q_unchecked(too_big).is_in_bounds()) self.assertFalse(int_to_q_unchecked(too_small).is_in_bounds()) self.assertEqual(None, int_to_q(too_big)) self.assertEqual(None, int_to_q(too_small)) @given(elements_mod_p()) def test_in_bounds_p(self, p: ElementModP): self.assertTrue(p.is_in_bounds()) too_big = p.to_int() + P too_small = p.to_int() - P self.assertFalse(int_to_p_unchecked(too_big).is_in_bounds()) self.assertFalse(int_to_p_unchecked(too_small).is_in_bounds()) self.assertEqual(None, int_to_p(too_big)) self.assertEqual(None, int_to_p(too_small)) @given(elements_mod_q_no_zero()) def test_in_bounds_q_no_zero(self, q: ElementModQ): self.assertTrue(q.is_in_bounds_no_zero()) self.assertFalse(ZERO_MOD_Q.is_in_bounds_no_zero()) self.assertFalse(int_to_q_unchecked(q.to_int() + Q).is_in_bounds_no_zero()) self.assertFalse(int_to_q_unchecked(q.to_int() - Q).is_in_bounds_no_zero()) @given(elements_mod_p_no_zero()) def test_in_bounds_p_no_zero(self, p: ElementModP): self.assertTrue(p.is_in_bounds_no_zero()) self.assertFalse(ZERO_MOD_P.is_in_bounds_no_zero()) self.assertFalse(int_to_p_unchecked(p.to_int() + P).is_in_bounds_no_zero()) self.assertFalse(int_to_p_unchecked(p.to_int() - P).is_in_bounds_no_zero()) @given(elements_mod_q()) def test_large_values_rejected_by_int_to_q(self, q: ElementModQ): oversize = q.to_int() + Q self.assertEqual(None, int_to_q(oversize))