class TestNonces(unittest.TestCase): @given(elements_mod_q()) def test_nonces_iterable(self, seed: ElementModQ): n = Nonces(seed) i = iter(n) q0 = next(i) q1 = next(i) self.assertTrue(q0 != q1) @given(elements_mod_q(), integers(min_value=0, max_value=1000000)) def test_nonces_deterministic(self, seed: ElementModQ, i: int): n1 = Nonces(seed) n2 = Nonces(seed) self.assertEqual(n1[i], n2[i]) @given( elements_mod_q(), elements_mod_q(), integers(min_value=0, max_value=1000000), ) def test_nonces_seed_matters(self, seed1: ElementModQ, seed2: ElementModQ, i: int): assume(seed1 != seed2) n1 = Nonces(seed1) n2 = Nonces(seed2) self.assertNotEqual(n1[i], n2[i]) @given(elements_mod_q()) def test_nonces_with_slices(self, seed: ElementModQ): n = Nonces(seed) count: int = 0 l: List[ElementModQ] = [] for i in iter(n): count += 1 l.append(i) if count == 10: break self.assertEqual(len(l), 10) l2 = Nonces(seed)[0:10] self.assertEqual(len(l2), 10) self.assertEqual(l, l2) def test_nonces_type_errors(self): n = Nonces(int_to_q_unchecked(3)) self.assertRaises(TypeError, len, n) self.assertRaises(TypeError, lambda: n[1:]) self.assertRaises(TypeError, lambda: n.get_with_headers(-1))
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 TestEquality(unittest.TestCase): @given(elements_mod_q(), elements_mod_q()) def testPsNotEqualToQs(self, q: ElementModQ, q2: ElementModQ): p = int_to_p_unchecked(q.to_int()) p2 = int_to_p_unchecked(q2.to_int()) # same value should imply they're equal self.assertEqual(p, q) self.assertEqual(q, p) if q.to_int() != q2.to_int(): # these are genuinely different numbers self.assertNotEqual(q, q2) self.assertNotEqual(p, p2) self.assertNotEqual(q, p2) self.assertNotEqual(p, q2) # of course, we're going to make sure that a number is equal to itself self.assertEqual(p, p) self.assertEqual(q, q)
class TestElections(unittest.TestCase): @settings( deadline=timedelta(milliseconds=2000), suppress_health_check=[HealthCheck.too_slow], max_examples=10, ) @given(election_descriptions()) def test_generators_yield_valid_output(self, ed: ElectionDescription): """ Tests that our Hypothesis election strategies generate "valid" output, also exercises the full stack of `is_valid` methods. """ self.assertTrue(ed.is_valid()) @settings( deadline=timedelta(milliseconds=10000), 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], ) @given( integers(1, 3).flatmap(lambda n: elections_and_ballots(n)), elements_mod_q(), ) def test_accumulation_encryption_decryption( self, everything: ELECTIONS_AND_BALLOTS_TUPLE_TYPE, nonce: ElementModQ, ): """ Tests that decryption is the inverse of encryption over arbitrarily generated elections and ballots. This test uses an abitrarily generated dataset with a single public-private keypair for the election encryption context. It also manually verifies that homomorphic accumulation works as expected. """ # Arrange election_description, metadata, ballots, secret_key, context = everything # Tally the plaintext ballots for comparison later plaintext_tallies = accumulate_plaintext_ballots(ballots) num_ballots = len(ballots) num_contests = len(metadata.contests) zero_nonce, *nonces = Nonces(nonce)[:num_ballots + 1] self.assertEqual(len(nonces), num_ballots) self.assertTrue(len(metadata.contests) > 0) # Generatea valid encryption of zero encrypted_zero = elgamal_encrypt(0, zero_nonce, context.elgamal_public_key) # Act encrypted_ballots = [] # encrypt each ballot for i in range(num_ballots): encrypted_ballot = encrypt_ballot(ballots[i], metadata, context, SEED_HASH, nonces[i]) encrypted_ballots.append(encrypted_ballot) # sanity check the encryption self.assertIsNotNone(encrypted_ballot) self.assertEqual(num_contests, len(encrypted_ballot.contests)) # decrypt the ballot with secret and verify it matches the plaintext decrypted_ballot = decrypt_ballot_with_secret( ballot=encrypted_ballot, election_metadata=metadata, crypto_extended_base_hash=context.crypto_extended_base_hash, public_key=context.elgamal_public_key, secret_key=secret_key, remove_placeholders=True, ) self.assertEqual(ballots[i], decrypted_ballot) # homomorphically accumualte the encrypted ballot representations encrypted_tallies = _accumulate_encrypted_ballots( encrypted_zero, encrypted_ballots) decrypted_tallies = {} for object_id in encrypted_tallies.keys(): decrypted_tallies[object_id] = encrypted_tallies[ object_id].decrypt(secret_key) # loop through the contest descriptions and verify # the decrypted tallies match the plaintext tallies for contest in metadata.contests: # Sanity check the generated data self.assertTrue(len(contest.ballot_selections) > 0) self.assertTrue(len(contest.placeholder_selections) > 0) decrypted_selection_tallies = [ decrypted_tallies[selection.object_id] for selection in contest.ballot_selections ] decrypted_placeholder_tallies = [ decrypted_tallies[placeholder.object_id] for placeholder in contest.placeholder_selections ] plaintext_tally_values = [ plaintext_tallies[selection.object_id] for selection in contest.ballot_selections ] # verify the plaintext tallies match the decrypted tallies self.assertEqual(decrypted_selection_tallies, plaintext_tally_values) # validate the right number of selections including placeholders across all ballots self.assertEqual( contest.number_elected * num_ballots, sum(decrypted_selection_tallies) + sum(decrypted_placeholder_tallies), )
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 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))
class TestDominionHypotheses(unittest.TestCase): @given( integers(1, 3).flatmap( lambda i: dominion_cvrs(max_rows=50, max_votes_per_race=i))) @settings( deadline=timedelta(milliseconds=10000), 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_max_votes_per_race_sanity(self, cvrs: str) -> None: parsed = read_dominion_csv(StringIO(cvrs)) self.assertIsNotNone(parsed) @given(ballots_and_context(), elements_mod_q()) @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_eg_conversion(self, state: DominionBallotsAndContext, seed: ElementModQ) -> None: ied = InternalElectionDescription(state.ed) ballot_box = BallotBox(ied, state.cec) seed_hash = EncryptionDevice("Location").get_hash() nonces = Nonces(seed)[0:len(state.ballots)] for b, n in zip(state.ballots, nonces): eb = encrypt_ballot(b, ied, state.cec, seed_hash, n) self.assertIsNotNone(eb) pb = decrypt_ballot_with_secret( eb, ied, state.cec.crypto_extended_base_hash, state.cec.elgamal_public_key, state.secret_key, ) self.assertEqual(b, pb) self.assertGreater(len(eb.contests), 0) cast_result = ballot_box.cast(eb) self.assertIsNotNone(cast_result) tally = tally_ballots(ballot_box._store, ied, state.cec) self.assertIsNotNone(tally) results = decrypt_tally_with_secret(tally, state.secret_key) self.assertEqual(len(results.keys()), len(state.id_map.keys())) for obj_id in results.keys(): self.assertIn(obj_id, state.id_map) cvr_sum = int(state.dominion_cvrs.data[state.id_map[obj_id]].sum()) decryption = results[obj_id] self.assertEqual(cvr_sum, decryption) @given(dominion_cvrs(max_rows=10)) @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_csv_metadata_roundtrip(self, cvrs: str) -> None: parsed = read_dominion_csv(StringIO(cvrs)) self.assertIsNotNone(parsed) original_metadata = parsed.dataframe_without_selections() csv_data = original_metadata.to_csv(index=False, quoting=csv.QUOTE_NONNUMERIC) reloaded_metadata = pd.read_csv(StringIO(csv_data)) self.assertTrue(original_metadata.equals(reloaded_metadata))