def r_elgamal_add(progressbar_actor: Optional[ActorHandle], *counters: ElGamalCiphertext) -> ElGamalCiphertext: num_counters = len(counters) result = elgamal_add(*counters) if progressbar_actor: progressbar_actor.update_completed.remote("Tallies", num_counters) return result
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)
def _accumulate_encrypted_ballots( encrypted_zero: ElGamalCiphertext, ballots: List[CiphertextBallot]) -> Dict[str, ElGamalCiphertext]: """ Internal helper function for testing: takes a list of encrypted ballots as input, digs into all of the individual selections and then accumulates them, using their `object_id` fields as keys. This function only knows what to do with `n_of_m` elections. It's not a general-purpose tallying mechanism for other election types. Note that the output will include both "normal" and "placeholder" selections. :param encrypted_zero: an encrypted zero, used for the accumulation :param ballots: a list of encrypted ballots :return: a dict from selection object_id's to `ElGamalCiphertext` totals """ tally: Dict[str, ElGamalCiphertext] = {} for ballot in ballots: for contest in ballot.contests: for selection in contest.ballot_selections: desc_id = ( selection.object_id ) # this should be the same as in the PlaintextBallot! if desc_id not in tally: tally[desc_id] = encrypted_zero tally[desc_id] = elgamal_add(tally[desc_id], selection.ciphertext) return tally
def sequential_tally( ptallies: Sequence[Optional[TALLY_INPUT_TYPE]]) -> TALLY_TYPE: """ Internal function: sequentially tallies all of the ciphertext ballots, or other partial tallies, and returns a partial tally. If any input tally happens to be `None` or an empty dict, the result is an empty dict. """ # log_and_print(f"Sequential, local tally with {len(ptallies)} inputs") num_nones = sum([1 for p in ptallies if p is None or p == {}]) if num_nones > 0 in ptallies: log_and_print( f"Found {num_nones} failed partial tallies, returning an empty tally" ) return {} result: TALLY_TYPE = {} for ptally in ptallies: # we want do our computation purely in terms of TALLY_TYPE, so we'll convert CiphertextBallots if isinstance(ptally, CiphertextBallot): ptally = ciphertext_ballot_to_dict(ptally) if ptally is None: # should never happen, but paranoia to keep the type system happy return {} for k in ptally.keys(): if k not in result: result[k] = ptally[k] else: counter_sum = result[k] counter_partial = ptally[k] counter_sum = elgamal_add(counter_sum, counter_partial) result[k] = counter_sum return result
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_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 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_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)
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))