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, ONE_MOD_Q, seed) proof0bad = make_disjunctive_chaum_pedersen_one( message0, nonce, keypair.public_key, ONE_MOD_Q, seed) self.assertTrue( proof0.is_valid(message0, keypair.public_key, ONE_MOD_Q)) self.assertFalse( proof0bad.is_valid(message0, keypair.public_key, ONE_MOD_Q)) message1 = get_optional(elgamal_encrypt(1, nonce, keypair.public_key)) proof1 = make_disjunctive_chaum_pedersen_one(message1, nonce, keypair.public_key, ONE_MOD_Q, seed) proof1bad = make_disjunctive_chaum_pedersen_zero( message1, nonce, keypair.public_key, ONE_MOD_Q, seed) self.assertTrue( proof1.is_valid(message1, keypair.public_key, ONE_MOD_Q)) self.assertFalse( proof1bad.is_valid(message1, keypair.public_key, ONE_MOD_Q))
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))
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))
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_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)
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)
def elgamal_reencrypt( public_key: ElementModP, nonce: ElementModQ, ciphertext: ElGamalCiphertext ) -> Optional[ElGamalCiphertext]: return flatmap_optional( elgamal_encrypt(0, nonce, public_key), lambda zero: elgamal_add(zero, ciphertext), )
def test_electionguard_basics(self) -> None: plaintexts = range(0, 1000) nonces = Nonces(int_to_q(3)) keypair = elgamal_keypair_random() r_public_key = ray.put(keypair.public_key) start = timer() serial_ciphertexts: List[ElGamalCiphertext] = [ elgamal_encrypt(p, n, keypair.public_key) for p, n in zip(plaintexts, nonces) ] serial_time = timer() # List[ObjectRef[ElGamalCiphertext] parallel_ciphertext_objects: List[ObjectRef] = [ r_encrypt.remote(p, n, r_public_key) for p, n in zip(plaintexts, nonces) ] parallel_ciphertexts: List[ElGamalCiphertext] = ray.get( parallel_ciphertext_objects ) parallel_time = timer() self.assertEqual(serial_ciphertexts, parallel_ciphertexts) print( f"Parallel speedup: {(serial_time - start) / (parallel_time - serial_time):.3f}x" )
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) )
def r_encrypt( plaintext: int, nonce: ElementModQ, public_key: ElementModP ) -> ElGamalCiphertext: if not isinstance(public_key, ElementModP): # paranoid type checking, while still getting used to working with Ray print(f"expected ElementModP, got {str(type(public_key))}") return elgamal_encrypt(plaintext, nonce, public_key)
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))
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))
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))
def r_encrypt( progressbar_actor: Optional[ActorHandle], plaintext: int, nonce: ElementModQ, public_key: ElementModP, ) -> ElGamalCiphertext: if not isinstance(public_key, ElementModP): # paranoid type checking, while still getting used to working with Ray print(f"expected ElementModP, got {str(type(public_key))}") if progressbar_actor: progressbar_actor.update_completed.remote("Ballots", 1) return elgamal_encrypt(plaintext, nonce, public_key)
def is_valid(self) -> bool: if self.constants != ElectionConstants(): log_error("Mismatching election constants!") return False # super-cheesy unit test to make sure keypair works m1 = randbelow(5) m2 = randbelow(5) nonce1 = rand_q() nonce2 = rand_q() c1 = get_optional(elgamal_encrypt(m1, nonce1, self.keypair.public_key)) c2 = get_optional(elgamal_encrypt(m2, nonce2, self.keypair.public_key)) csum = elgamal_add(c1, c2) psum = csum.decrypt(self.keypair.secret_key) if psum != m1 + m2: log_error("The given keypair didn't work for basic ElGamal math") return False return True
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))
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, )
def chaum_pedersen_bench(bi: BenchInput) -> Tuple[float, float]: """ Given an input (instance of the BenchInput tuple), constructs and validates a disjunctive Chaum-Pedersen proof, returning the time (in seconds) to do each operation. """ (keypair, r, s) = bi ciphertext = get_optional(elgamal_encrypt(0, r, keypair.public_key)) start1 = timer() proof = make_disjunctive_chaum_pedersen_zero(ciphertext, r, keypair.public_key, s) end1 = timer() valid = proof.is_valid(ciphertext, keypair.public_key) end2 = timer() if not valid: raise Exception( "Wasn't expecting an invalid proof during a benchmark!") return end1 - start1, end2 - end1
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), )
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 fast_tally_everything( cvrs: DominionCSV, pool: Optional[Pool] = None, verbose: bool = True, date: Optional[datetime] = None, seed_hash: Optional[ElementModQ] = None, master_nonce: Optional[ElementModQ] = None, secret_key: Optional[ElementModQ] = None, use_progressbar: bool = True, ) -> FastTallyEverythingResults: """ This top-level function takes a collection of Dominion CVRs and produces everything that we might want for arlo-e2e: a list of encrypted ballots, their encrypted and decrypted tally, and proofs of the correctness of the whole thing. The election `secret_key` is an optional parameter. If absent, a random keypair is generated and used. Similarly, if a `seed_hash` or `master_nonce` is not provided, random ones are generated and used. For parallelism, a `multiprocessing.pool.Pool` may be provided, and should result in significant speedups on multicore computers. If absent, the computation will proceed sequentially. """ rows, cols = cvrs.data.shape if date is None: date = datetime.now() parse_time = timer() log_and_print(f"Rows: {rows}, cols: {cols}", verbose) ed, ballots, id_map = cvrs.to_election_description(date=date) assert len(ballots) > 0, "can't have zero ballots!" keypair = (elgamal_keypair_random() if secret_key is None else elgamal_keypair_from_secret(secret_key)) assert keypair is not None, "unexpected failure with keypair computation" secret_key, public_key = keypair # This computation exists only to cause side-effects in the DLog engine, so the lame nonce is not an issue. assert len(ballots) == get_optional( elgamal_encrypt(m=len(ballots), nonce=int_to_q_unchecked(3), public_key=public_key)).decrypt( secret_key), "got wrong ElGamal decryption!" dlog_prime_time = timer() log_and_print( f"DLog prime time (n={len(ballots)}): {dlog_prime_time - parse_time: .3f} sec", verbose, ) cec = make_ciphertext_election_context( number_of_guardians=1, quorum=1, elgamal_public_key=public_key, description_hash=ed.crypto_hash(), ) ied = InternalElectionDescription(ed) # REVIEW THIS: is this cryptographically sound? Is the seed_hash properly a secret? Should # it go in the output? The nonces are clearly secret. If you know them, you can decrypt. if seed_hash is None: seed_hash = rand_q() if master_nonce is None: master_nonce = rand_q() nonces: List[ElementModQ] = Nonces(master_nonce)[0:len(ballots)] # even if verbose is false, we still want to see the progress bar for the encryption cballots = fast_encrypt_ballots(ballots, ied, cec, seed_hash, nonces, pool, use_progressbar=use_progressbar) eg_encrypt_time = timer() log_and_print( f"Encryption time: {eg_encrypt_time - dlog_prime_time: .3f} sec", verbose) log_and_print( f"Encryption rate: {rows / (eg_encrypt_time - dlog_prime_time): .3f} ballot/sec", verbose, ) tally: TALLY_TYPE = fast_tally_ballots(cballots, pool) eg_tabulate_time = timer() log_and_print( f"Tabulation time: {eg_tabulate_time - eg_encrypt_time: .3f} sec", verbose) log_and_print( f"Tabulation rate: {rows / (eg_tabulate_time - eg_encrypt_time): .3f} ballot/sec", verbose, ) log_and_print( f"Encryption and tabulation: {rows} ballots / {eg_tabulate_time - dlog_prime_time: .3f} sec = {rows / (eg_tabulate_time - dlog_prime_time): .3f} ballot/sec", verbose, ) assert tally is not None, "tally failed!" if verbose: # pragma: no cover print("Decryption & Proofs: ") decrypted_tally: DECRYPT_TALLY_OUTPUT_TYPE = fast_decrypt_tally( tally, cec, keypair, seed_hash, pool, verbose) eg_decryption_time = timer() log_and_print( f"Decryption time: {eg_decryption_time - eg_tabulate_time: .3f} sec", verbose) log_and_print( f"Decryption rate: {len(decrypted_tally.keys()) / (eg_decryption_time - eg_tabulate_time): .3f} selection/sec", verbose, ) # Sanity-checking logic: make sure we don't have any unexpected keys, and that the decrypted totals # match up with the columns in the original plaintext data. for obj_id in decrypted_tally.keys(): assert obj_id in id_map, "object_id in results that we don't know about!" cvr_sum = int(cvrs.data[id_map[obj_id]].sum()) decryption, proof = decrypted_tally[obj_id] assert cvr_sum == decryption, f"decryption failed for {obj_id}" # Assemble the data structure that we're returning. Having nonces in the ciphertext makes these # structures sensitive for writing out to disk, but otherwise they're ready to go. reported_tally: Dict[str, SelectionInfo] = { k: SelectionInfo( object_id=k, encrypted_tally=tally[k], # we need to forcibly convert mpz to int here to make serialization work properly decrypted_tally=int(decrypted_tally[k][0]), proof=decrypted_tally[k][1], ) for k in tally.keys() } # strips the ballots of their nonces, which is important because those could allow for decryption accepted_ballots = [ciphertext_ballot_to_accepted(x) for x in cballots] return FastTallyEverythingResults( metadata=cvrs.metadata, cvr_metadata=cvrs.dataframe_without_selections(), election_description=ed, encrypted_ballot_memos={ ballot.object_id: make_memo_value(ballot) for ballot in accepted_ballots }, tally=SelectionTally(reported_tally), context=cec, )