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
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 TestFastTallies(unittest.TestCase): def setUp(self) -> None: self.pool = Pool(cpu_count()) def tearDown(self) -> None: self.pool.close() @given(dominion_cvrs(max_rows=50), elgamal_keypairs(), booleans()) @settings( deadline=timedelta(milliseconds=50000), suppress_health_check=[HealthCheck.too_slow], max_examples=5, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) def test_end_to_end(self, input: str, keypair: ElGamalKeyPair, use_keypair: bool) -> None: coverage.process_startup( ) # necessary for coverage testing to work in parallel cvrs = read_dominion_csv(StringIO(input)) self.assertIsNotNone(cvrs) _, ballots, _ = cvrs.to_election_description() assert len(ballots) > 0, "can't have zero ballots!" if use_keypair: tally = fast_tally_everything(cvrs, self.pool, verbose=True, secret_key=keypair.secret_key) else: tally = fast_tally_everything(cvrs, self.pool, verbose=True) self.assertTrue(tally.all_proofs_valid(verbose=True)) # Now, while we've got a tally and a set of cvrs, we'll test some of the other utility # methods that we have. This is going to be much faster than regenerating cvrs and tallies. # TODO: tests for get_contest_titles_matching and get_ballot_styles_for_contest_titles for ballot_style in cvrs.metadata.style_map.keys(): ballots_query = tally.get_ballots_matching_ballot_styles( [ballot_style]) ballots_pandas = cvrs.data[cvrs.data.BallotType == ballot_style] self.assertEqual(len(ballots_pandas), len(ballots_query))
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 TestRayReduce(unittest.TestCase): def setUp(self) -> None: ray_init_localhost() @settings( deadline=timedelta(milliseconds=50000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( lists(integers(min_value=0, max_value=1), min_size=20, max_size=200), elgamal_keypairs(), ) def test_reduce_with_ray_wait_no_progress(self, counters: List[int], keypair: ElGamalKeyPair) -> None: nonces = Nonces(int_to_q(3))[0:len(counters)] ciphertexts: List[ObjectRef] = [ r_encrypt.remote(None, p, n, keypair.public_key) for p, n in zip(counters, nonces) ] # compute in parallel ptotal = ray.get( ray_reduce_with_ray_wait( inputs=ciphertexts, shard_size=3, reducer_first_arg=None, reducer=r_elgamal_add.remote, progressbar=None, progressbar_key="Tallies", timeout=None, verbose=True, )) # recompute serially stotal = elgamal_add(*ray.get(ciphertexts)) self.assertEqual(stotal, ptotal) @settings( deadline=timedelta(milliseconds=50000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( lists(integers(min_value=0, max_value=1), min_size=20, max_size=200), elgamal_keypairs(), ) def test_reduce_with_ray_wait_with_progress( self, counters: List[int], keypair: ElGamalKeyPair) -> None: nonces = Nonces(int_to_q(3))[0:len(counters)] pbar = ProgressBar({ "Ballots": len(counters), "Tallies": len(counters), "Iterations": 0 }) ciphertexts: List[ObjectRef] = [ r_encrypt.remote(pbar.actor, p, n, keypair.public_key) for p, n in zip(counters, nonces) ] # compute in parallel ptotal = ray.get( ray_reduce_with_ray_wait( inputs=ciphertexts, shard_size=3, reducer_first_arg=pbar.actor, reducer=r_elgamal_add.remote, progressbar=pbar, progressbar_key="Tallies", timeout=None, verbose=False, )) # recompute serially stotal = elgamal_add(*ray.get(ciphertexts)) self.assertEqual(stotal, ptotal) @settings( deadline=timedelta(milliseconds=50000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( lists(integers(min_value=0, max_value=1), min_size=20, max_size=200), elgamal_keypairs(), ) def test_reduce_with_rounds_without_progress( self, counters: List[int], keypair: ElGamalKeyPair) -> None: nonces = Nonces(int_to_q(3))[0:len(counters)] ciphertexts: List[ObjectRef] = [ r_encrypt.remote(None, p, n, keypair.public_key) for p, n in zip(counters, nonces) ] # compute in parallel ptotal = ray.get( ray_reduce_with_rounds( inputs=ciphertexts, shard_size=3, reducer_first_arg=None, reducer=r_elgamal_add.remote, progressbar=None, verbose=True, )) # recompute serially stotal = elgamal_add(*ray.get(ciphertexts)) self.assertEqual(stotal, ptotal) @unittest.skip("doesn't complete here, but works fine elsewhere; weird") @settings( deadline=timedelta(milliseconds=50000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given( lists(integers(min_value=0, max_value=1), min_size=20, max_size=200), elgamal_keypairs(), ) def test_reduce_with_rounds_with_progress(self, counters: List[int], keypair: ElGamalKeyPair) -> None: nonces = Nonces(int_to_q(3))[0:len(counters)] pbar = ProgressBar({ "Ballots": len(counters), "Tallies": len(counters), "Iterations": 0 }) ciphertexts: List[ObjectRef] = [ r_encrypt.remote(pbar.actor, p, n, keypair.public_key) for p, n in zip(counters, nonces) ] # compute in parallel ptotal = ray.get( ray_reduce_with_rounds( inputs=ciphertexts, shard_size=3, reducer_first_arg=pbar.actor, reducer=r_elgamal_add.remote, progressbar=pbar, progressbar_key="Tallies", verbose=False, )) # recompute serially stotal = elgamal_add(*ray.get(ciphertexts)) self.assertEqual(stotal, ptotal)
class TestElGamal(unittest.TestCase): def test_simple_elgamal_encryption_decryption(self): nonce = ONE_MOD_Q secret_key = TWO_MOD_Q keypair = get_optional(elgamal_keypair_from_secret(secret_key)) public_key = keypair.public_key self.assertLess(public_key.to_int(), P) elem = g_pow_p(ZERO_MOD_Q) self.assertEqual(elem, ONE_MOD_P) # g^0 == 1 ciphertext = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) self.assertEqual(G, ciphertext.alpha.to_int()) self.assertEqual( pow(ciphertext.alpha.to_int(), secret_key.to_int(), P), pow(public_key.to_int(), nonce.to_int(), P), ) self.assertEqual( ciphertext.beta.to_int(), pow(public_key.to_int(), nonce.to_int(), P), ) plaintext = ciphertext.decrypt(keypair.secret_key) self.assertEqual(0, plaintext) @given(integers(0, 100), elgamal_keypairs()) def test_elgamal_encrypt_requires_nonzero_nonce(self, message: int, keypair: ElGamalKeyPair): self.assertEqual( None, elgamal_encrypt(message, ZERO_MOD_Q, keypair.public_key)) def test_elgamal_keypair_from_secret_requires_key_greater_than_one(self): self.assertEqual(None, elgamal_keypair_from_secret(ZERO_MOD_Q)) self.assertEqual(None, elgamal_keypair_from_secret(ONE_MOD_Q)) @given(integers(0, 100), elements_mod_q_no_zero(), elgamal_keypairs()) def test_elgamal_encryption_decryption_inverses(self, message: int, nonce: ElementModQ, keypair: ElGamalKeyPair): ciphertext = get_optional( elgamal_encrypt(message, nonce, keypair.public_key)) plaintext = ciphertext.decrypt(keypair.secret_key) self.assertEqual(message, plaintext) @given(integers(0, 100), elements_mod_q_no_zero(), elgamal_keypairs()) def test_elgamal_encryption_decryption_with_known_nonce_inverses( self, message: int, nonce: ElementModQ, keypair: ElGamalKeyPair): ciphertext = get_optional( elgamal_encrypt(message, nonce, keypair.public_key)) plaintext = ciphertext.decrypt_known_nonce(keypair.public_key, nonce) self.assertEqual(message, plaintext) @given(elgamal_keypairs()) def test_elgamal_generated_keypairs_are_within_range( self, keypair: ElGamalKeyPair): self.assertLess(keypair.public_key.to_int(), P) self.assertLess(keypair.secret_key.to_int(), G) self.assertEqual(g_pow_p(keypair.secret_key), keypair.public_key) @given( elgamal_keypairs(), integers(0, 100), elements_mod_q_no_zero(), integers(0, 100), elements_mod_q_no_zero(), ) def test_elgamal_add_homomorphic_accumulation_decrypts_successfully( self, keypair: ElGamalKeyPair, m1: int, r1: ElementModQ, m2: int, r2: ElementModQ, ): c1 = get_optional(elgamal_encrypt(m1, r1, keypair.public_key)) c2 = get_optional(elgamal_encrypt(m2, r2, keypair.public_key)) c_sum = elgamal_add(c1, c2) total = c_sum.decrypt(keypair.secret_key) self.assertEqual(total, m1 + m2) def test_elgamal_add_requires_args(self): self.assertRaises(Exception, elgamal_add) @given(elgamal_keypairs()) def test_elgamal_keypair_produces_valid_residue(self, keypair): self.assertTrue(keypair.public_key.is_valid_residue()) def test_elgamal_keypair_random(self): # Act random_keypair = elgamal_keypair_random() random_keypair_two = elgamal_keypair_random() # Assert self.assertIsNotNone(random_keypair) self.assertIsNotNone(random_keypair.public_key) self.assertIsNotNone(random_keypair.secret_key) self.assertNotEqual(random_keypair, random_keypair_two) def test_elgamal_combine_public_keys(self): # Arrange random_keypair = elgamal_keypair_random() random_keypair_two = elgamal_keypair_random() public_keys = [ random_keypair.public_key, random_keypair_two.public_key ] # Act joint_key = elgamal_combine_public_keys(public_keys) # Assert self.assertIsNotNone(joint_key) self.assertNotEqual(joint_key, random_keypair.public_key) self.assertNotEqual(joint_key, random_keypair_two.public_key) # Here's an oddball test: checking whether running lots of parallel exponentiations yields the # correct answer. It certainly *should* work, but this verifies that nothing weird is happening # in the GMPY2 library, with it's C code below that. def test_gmpy2_parallelism_is_safe(self): cpus = cpu_count() problem_size = 1000 secret_keys = Nonces(int_to_q_unchecked(3))[ 0:problem_size] # list of 1000 might-as-well-be-random Q's log_info( f"testing GMPY2 powmod parallelism safety (cpus = {cpus}, problem_size = {problem_size})" ) # compute in parallel start = timer() p = Pool(cpus) keypairs = p.map(elgamal_keypair_from_secret, secret_keys) end1 = timer() # verify scalar for keypair in keypairs: self.assertEqual( keypair.public_key, elgamal_keypair_from_secret(keypair.secret_key).public_key, ) end2 = timer() p.close( ) # apparently necessary to avoid warnings from the Pool system log_info(f"Parallelism speedup: {(end2 - end1) / (end1 - start):.3f}")
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 TestElGamal(unittest.TestCase): """ElGamal tests""" def test_simple_elgamal_encryption_decryption(self): nonce = ONE_MOD_Q secret_key = TWO_MOD_Q keypair = get_optional(elgamal_keypair_from_secret(secret_key)) public_key = keypair.public_key self.assertLess(public_key.to_int(), P) elem = g_pow_p(ZERO_MOD_Q) self.assertEqual(elem, ONE_MOD_P) # g^0 == 1 ciphertext = get_optional(elgamal_encrypt(0, nonce, keypair.public_key)) self.assertEqual(G, ciphertext.pad.to_int()) self.assertEqual( pow(ciphertext.pad.to_int(), secret_key.to_int(), P), pow(public_key.to_int(), nonce.to_int(), P), ) self.assertEqual( ciphertext.data.to_int(), pow(public_key.to_int(), nonce.to_int(), P), ) plaintext = ciphertext.decrypt(keypair.secret_key) self.assertEqual(0, plaintext) @given(integers(0, 100), elgamal_keypairs()) def test_elgamal_encrypt_requires_nonzero_nonce(self, message: int, keypair: ElGamalKeyPair): self.assertEqual( None, elgamal_encrypt(message, ZERO_MOD_Q, keypair.public_key)) def test_elgamal_keypair_from_secret_requires_key_greater_than_one(self): self.assertEqual(None, elgamal_keypair_from_secret(ZERO_MOD_Q)) self.assertEqual(None, elgamal_keypair_from_secret(ONE_MOD_Q)) @given(integers(0, 100), elements_mod_q_no_zero(), elgamal_keypairs()) def test_elgamal_encryption_decryption_inverses(self, message: int, nonce: ElementModQ, keypair: ElGamalKeyPair): ciphertext = get_optional( elgamal_encrypt(message, nonce, keypair.public_key)) plaintext = ciphertext.decrypt(keypair.secret_key) self.assertEqual(message, plaintext) @given(integers(0, 100), elements_mod_q_no_zero(), elgamal_keypairs()) def test_elgamal_encryption_decryption_with_known_nonce_inverses( self, message: int, nonce: ElementModQ, keypair: ElGamalKeyPair): ciphertext = get_optional( elgamal_encrypt(message, nonce, keypair.public_key)) plaintext = ciphertext.decrypt_known_nonce(keypair.public_key, nonce) self.assertEqual(message, plaintext) @given(elgamal_keypairs()) def test_elgamal_generated_keypairs_are_within_range( self, keypair: ElGamalKeyPair): self.assertLess(keypair.public_key.to_int(), P) self.assertLess(keypair.secret_key.to_int(), Q) self.assertEqual(g_pow_p(keypair.secret_key), keypair.public_key) @given( elgamal_keypairs(), integers(0, 100), elements_mod_q_no_zero(), integers(0, 100), elements_mod_q_no_zero(), ) def test_elgamal_add_homomorphic_accumulation_decrypts_successfully( self, keypair: ElGamalKeyPair, m1: int, r1: ElementModQ, m2: int, r2: ElementModQ, ): c1 = get_optional(elgamal_encrypt(m1, r1, keypair.public_key)) c2 = get_optional(elgamal_encrypt(m2, r2, keypair.public_key)) c_sum = elgamal_add(c1, c2) total = c_sum.decrypt(keypair.secret_key) self.assertEqual(total, m1 + m2) def test_elgamal_add_requires_args(self): self.assertRaises(Exception, elgamal_add) @given(elgamal_keypairs()) def test_elgamal_keypair_produces_valid_residue(self, keypair): self.assertTrue(keypair.public_key.is_valid_residue()) def test_elgamal_keypair_random(self): # Act random_keypair = elgamal_keypair_random() random_keypair_two = elgamal_keypair_random() # Assert self.assertIsNotNone(random_keypair) self.assertIsNotNone(random_keypair.public_key) self.assertIsNotNone(random_keypair.secret_key) self.assertNotEqual(random_keypair, random_keypair_two) def test_elgamal_combine_public_keys(self): # Arrange random_keypair = elgamal_keypair_random() random_keypair_two = elgamal_keypair_random() public_keys = [ random_keypair.public_key, random_keypair_two.public_key ] # Act joint_key = elgamal_combine_public_keys(public_keys) # Assert self.assertIsNotNone(joint_key) self.assertNotEqual(joint_key, random_keypair.public_key) self.assertNotEqual(joint_key, random_keypair_two.public_key) def test_gmpy2_parallelism_is_safe(self): """ Ensures running lots of parallel exponentiations still yields the correct answer. This verifies that nothing incorrect is happening in the GMPY2 library """ # Arrange scheduler = Scheduler() problem_size = 1000 random_secret_keys = Nonces(int_to_q_unchecked(3))[0:problem_size] log_info( f"testing GMPY2 powmod parallelism safety (cpus = {scheduler.cpu_count}, problem_size = {problem_size})" ) # Act start = timer() keypairs = scheduler.schedule( elgamal_keypair_from_secret, [list([secret_key]) for secret_key in random_secret_keys], ) end1 = timer() # Assert for keypair in keypairs: self.assertEqual( keypair.public_key, elgamal_keypair_from_secret(keypair.secret_key).public_key, ) end2 = timer() scheduler.close() log_info(f"Parallelism speedup: {(end2 - end1) / (end1 - start):.3f}")
class TestRayTallies(unittest.TestCase): def removeTree(self) -> None: try: shutil.rmtree("ftally_output", ignore_errors=True) shutil.rmtree("rtally_output", ignore_errors=True) except FileNotFoundError: # okay if it's not there pass def setUp(self) -> None: cpus = cpu_count() ray_init_localhost(num_cpus=cpus) self.pool = Pool(cpus) self.removeTree() coverage.process_startup() # necessary for coverage testing to work in parallel def tearDown(self) -> None: self.pool.close() self.removeTree() @given(dominion_cvrs(max_rows=120), elgamal_keypairs(), booleans()) @settings( deadline=timedelta(milliseconds=50000), 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], ) def test_ray_end_to_end( self, input: str, keypair: ElGamalKeyPair, use_keypair: bool ) -> None: self.removeTree() cvrs = read_dominion_csv(StringIO(input)) self.assertIsNotNone(cvrs) _, ballots, _ = cvrs.to_election_description() assert len(ballots) > 0, "can't have zero ballots!" print(f"End-to-end Ray test with {len(ballots)} ballot(s).") if use_keypair: rtally = ray_tally_everything( cvrs, verbose=True, secret_key=keypair.secret_key, root_dir="rtally_output", use_progressbar=False, ) else: rtally = ray_tally_everything( cvrs, verbose=True, root_dir="rtally_output", use_progressbar=False ) ftally = rtally.to_fast_tally() self.assertTrue(ftally.all_proofs_valid(verbose=False)) # now, we'll write everything to the filesystem and make sure we get the # same stuff fmanifest = write_fast_tally(ftally, "ftally_output") rmanifest = write_ray_tally(rtally, "rtally_output") # we can't just assert equality of the manifests, because the root_dirs are different self.assertTrue(fmanifest.equivalent(rmanifest)) self.removeTree() @given(dominion_cvrs(max_rows=5), elgamal_keypairs()) @settings( deadline=timedelta(milliseconds=50000), suppress_health_check=[HealthCheck.too_slow], max_examples=5, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) def test_ray_and_multiprocessing_agree( self, input: str, keypair: ElGamalKeyPair ) -> None: self.removeTree() # Normally these are generated internally, but by making them be the same, we take all # the non-determinism out of the tally_everything methods and get identical results. seed_hash = rand_q() master_nonce = rand_q() date = datetime.now() cvrs = read_dominion_csv(StringIO(input)) self.assertIsNotNone(cvrs) _, ballots, _ = cvrs.to_election_description() assert len(ballots) > 0, "can't have zero ballots!" print(f"Comparing tallies with {len(ballots)} ballot(s).") tally = fast_tally_everything( cvrs, verbose=False, date=date, secret_key=keypair.secret_key, pool=self.pool, seed_hash=seed_hash, master_nonce=master_nonce, use_progressbar=False, ) rtally = ray_tally_everything( cvrs, verbose=False, date=date, secret_key=keypair.secret_key, seed_hash=seed_hash, master_nonce=master_nonce, root_dir="rtally_output", use_progressbar=False, ) self.assertEqual(tally, rtally.to_fast_tally())
class TestTallyPublishing(unittest.TestCase): def removeTree(self) -> None: try: shutil.rmtree(TALLY_TESTING_DIR, ignore_errors=True) shutil.rmtree(DECRYPTED_DIR, ignore_errors=True) except FileNotFoundError: # okay if it's not there pass def setUp(self) -> None: self.removeTree() self.pool = Pool(cpu_count()) ray_init_localhost() coverage.process_startup( ) # necessary for coverage testing to work in parallel def tearDown(self) -> None: self.removeTree() self.pool.close() @given(dominion_cvrs(max_rows=50), booleans(), elgamal_keypairs()) @settings( deadline=timedelta(milliseconds=50000), suppress_health_check=[HealthCheck.too_slow], max_examples=5, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) def test_end_to_end_publications(self, input: str, check_proofs: bool, keypair: ElGamalKeyPair) -> None: coverage.process_startup( ) # necessary for coverage testing to work in parallel self.removeTree( ) # if there's anything leftover from a prior run, get rid of it cvrs = read_dominion_csv(StringIO(input)) self.assertIsNotNone(cvrs) _, ballots, _ = cvrs.to_election_description() assert len(ballots) > 0, "can't have zero ballots!" results = fast_tally_everything(cvrs, self.pool, secret_key=keypair.secret_key, verbose=True) self.assertTrue(results.all_proofs_valid(self.pool)) # dump files out to disk write_fast_tally(results, TALLY_TESTING_DIR) log_and_print( "tally_testing written, proceeding to read it back in again") # now, read it back again! results2 = load_fast_tally( TALLY_TESTING_DIR, check_proofs=check_proofs, pool=self.pool, verbose=True, recheck_ballots_and_tallies=True, ) self.assertIsNotNone(results2) log_and_print("tally_testing got non-null result!") self.assertTrue( _list_eq(results.encrypted_ballots, results2.encrypted_ballots)) self.assertTrue(results.equivalent(results2, keypair, self.pool)) # Make sure there's an index.html file; throws an exception if it's missing self.assertIsNotNone(stat(path.join(TALLY_TESTING_DIR, "index.html"))) # And lastly, while we're here, we'll use all this machinery to exercise the ballot decryption # read/write facilities. ied = InternalElectionDescription(results.election_description) log_and_print("decrypting one more time") pballots = decrypt_ballots( ied, results.context.crypto_extended_base_hash, keypair, self.pool, results.encrypted_ballots, ) self.assertEqual(len(pballots), len(results.encrypted_ballots)) self.assertNotIn(None, pballots) # for speed, we're only going to do this for the first ballot, not all of them pballot = pballots[0] eballot = results.encrypted_ballots[0] bid = pballot.ballot.object_id self.assertTrue( verify_proven_ballot_proofs( results.context.crypto_extended_base_hash, keypair.public_key, eballot, pballot, )) write_proven_ballot(pballot, DECRYPTED_DIR) self.assertTrue(exists_proven_ballot(bid, DECRYPTED_DIR)) self.assertFalse(exists_proven_ballot(bid + "0", DECRYPTED_DIR)) self.assertEqual(pballot, load_proven_ballot(bid, DECRYPTED_DIR)) self.removeTree() # clean up our mess @given(dominion_cvrs(max_rows=50), booleans(), elgamal_keypairs()) @settings( deadline=timedelta(milliseconds=50000), suppress_health_check=[HealthCheck.too_slow], max_examples=5, # disabling the "shrink" phase, because it runs very slowly phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target], ) def test_end_to_end_publications_ray(self, input: str, check_proofs: bool, keypair: ElGamalKeyPair) -> None: self.removeTree( ) # if there's anything leftover from a prior run, get rid of it cvrs = read_dominion_csv(StringIO(input)) self.assertIsNotNone(cvrs) _, ballots, _ = cvrs.to_election_description() assert len(ballots) > 0, "can't have zero ballots!" results = ray_tally_everything( cvrs, secret_key=keypair.secret_key, verbose=True, root_dir=TALLY_TESTING_DIR, ) self.assertTrue(results.all_proofs_valid()) # dump files out to disk write_ray_tally(results, TALLY_TESTING_DIR) log_and_print( "tally_testing written, proceeding to read it back in again") # now, read it back again! results2 = load_ray_tally( TALLY_TESTING_DIR, check_proofs=check_proofs, verbose=True, recheck_ballots_and_tallies=True, ) self.assertIsNotNone(results2) log_and_print("tally_testing got non-null result!") self.assertTrue( _list_eq(results.encrypted_ballots, results2.encrypted_ballots)) self.assertTrue(results.equivalent(results2, keypair)) self.removeTree() # clean up our mess