def ciphertext_elections(draw: _DrawType, election_description: ElectionDescription): """ Generates a `CiphertextElectionContext` with a single public-private key pair as the encryption context. In a real election, the key ceremony would be used to generate a shared public key. :param draw: Hidden argument, used by Hypothesis. :param election_description: An `ElectionDescription` object, with which the `CiphertextElectionContext` will be associated :return: a tuple of a `CiphertextElectionContext` and the secret key associated with it """ secret_key, public_key = draw(elgamal_keypairs()) commitment_hash = draw(elements_mod_q_no_zero()) ciphertext_election_with_secret: CIPHERTEXT_ELECTIONS_TUPLE_TYPE = ( secret_key, make_ciphertext_election_context( number_of_guardians=1, quorum=1, elgamal_public_key=public_key, commitment_hash=commitment_hash, description_hash=election_description.crypto_hash(), ), ) return ciphertext_election_with_secret
def 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())
def verify_results(self) -> None: """Verify results of election""" # Deserialize description_from_file = ElectionDescription.from_json_file( DESCRIPTION_FILE_NAME, RESULTS_DIR ) self.assertEqual(self.description, description_from_file) context_from_file = CiphertextElectionContext.from_json_file( CONTEXT_FILE_NAME, RESULTS_DIR ) self.assertEqual(self.context, context_from_file) constants_from_file = ElectionConstants.from_json_file( CONSTANTS_FILE_NAME, RESULTS_DIR ) self.assertEqual(self.constants, constants_from_file) device_name = DEVICE_PREFIX + str(self.device.uuid) device_from_file = EncryptionDevice.from_json_file(device_name, DEVICES_DIR) self.assertEqual(self.device, device_from_file) ciphertext_ballots: List[CiphertextAcceptedBallot] = [] for ballot in self.ballot_store.all(): ballot_name = BALLOT_PREFIX + ballot.object_id ballot_from_file = CiphertextAcceptedBallot.from_json_file( ballot_name, BALLOTS_DIR ) self.assertEqual(ballot, ballot_from_file) spoiled_ballots: List[CiphertextAcceptedBallot] = [] for spoiled_ballot in self.ciphertext_tally.spoiled_ballots.values(): ballot_name = BALLOT_PREFIX + spoiled_ballot.object_id spoiled_ballot_from_file = CiphertextAcceptedBallot.from_json_file( ballot_name, SPOILED_DIR ) self.assertEqual(spoiled_ballot, spoiled_ballot_from_file) ciphertext_tally_from_file = PublishedCiphertextTally.from_json_file( ENCRYPTED_TALLY_FILE_NAME, RESULTS_DIR ) self.assertEqual( publish_ciphertext_tally(self.ciphertext_tally), ciphertext_tally_from_file ) plainttext_tally_from_file = PlaintextTally.from_json_file( TALLY_FILE_NAME, RESULTS_DIR ) self.assertEqual(self.plaintext_tally, plainttext_tally_from_file) coefficient_validation_sets: List[CoefficientValidationSet] = [] for coefficient_validation_set in self.coefficient_validation_sets: set_name = COEFFICIENT_PREFIX + coefficient_validation_set.owner_id coefficient_validation_set_from_file = CoefficientValidationSet.from_json_file( set_name, COEFFICIENTS_DIR ) self.assertEqual( coefficient_validation_set, coefficient_validation_set_from_file )
def test_publish(self) -> None: # Arrange now = datetime.now(timezone.utc) description = ElectionDescription( "", ElectionType.unknown, now, now, [], [], [], [], [], [] ) context = CiphertextElectionContext(1, 1, ONE_MOD_P, ONE_MOD_Q) constants = ElectionConstants() devices = [] coefficients = [CoefficientValidationSet("", [], [])] encrypted_ballots = [] tally = PlaintextTally("", [], []) # Act publish( description, context, constants, devices, encrypted_ballots, CiphertextTally("", description, context), tally, coefficients, ) # Assert self.assertTrue(path.exists(RESULTS_DIR)) # Cleanup rmtree(RESULTS_DIR)
def encrypt_ballots(request: EncryptBallotsRequest = Body(...)) -> Any: """ Encrypt one or more ballots """ ballots = [ PlaintextBallot.from_json_object(ballot) for ballot in request.ballots ] description = InternalElectionDescription( ElectionDescription.from_json_object(request.description)) context = CiphertextElectionContext.from_json_object(request.context) seed_hash = read_json_object(request.seed_hash, ElementModQ) nonce: Optional[ElementModQ] = (read_json_object( request.nonce, ElementModQ) if request.nonce else None) encrypted_ballots = [] current_hash = seed_hash for ballot in ballots: encrypted_ballot = encrypt_ballot(ballot, description, context, current_hash, nonce) if not encrypted_ballot: raise HTTPException(status_code=500, detail="Ballot failed to encrypt") encrypted_ballots.append(encrypted_ballot) current_hash = get_optional(encrypted_ballot.tracking_hash) response = EncryptBallotsResponse( encrypted_ballots=[ ballot.to_json_object() for ballot in encrypted_ballots ], next_seed_hash=write_json_object(current_hash), ) return response
def get_hamilton_election_from_file(self) -> ElectionDescription: with open( os.path.join(data, "hamilton-county", "election_manifest.json"), "r") as subject: result = subject.read() target = ElectionDescription.from_json(result) return target
def election_descriptions(draw: _DrawType, max_num_parties: int = 3, max_num_contests: int = 3): """ Generates an `ElectionDescription` -- the top-level object describing an election. :param draw: Hidden argument, used by Hypothesis. :param max_num_parties: The largest number of parties that will be generated (default: 3) :param max_num_contests: The largest number of contests that will be generated (default: 3) """ assert max_num_parties > 0, "need at least one party" assert max_num_contests > 0, "need at least one contest" geo_units = [draw(geopolitical_units())] num_parties: int = draw(integers(1, max_num_parties)) # keep this small so tests run faster parties: List[Party] = draw(party_lists(num_parties)) num_contests: int = draw(integers(1, max_num_contests)) # generate a collection candidates mapped to contest descriptions candidate_contests: List[Tuple[List[Candidate], ContestDescription]] = [ draw(contest_descriptions(i, parties, geo_units)) for i in range(num_contests) ] assert len(candidate_contests) > 0 candidates_ = reduce( lambda a, b: a + b, [candidate_contest[0] for candidate_contest in candidate_contests], ) contests = [ candidate_contest[1] for candidate_contest in candidate_contests ] styles = [draw(ballot_styles(parties, geo_units))] # maybe later on we'll do something more complicated with dates start_date = draw(datetimes()) end_date = start_date return ElectionDescription( election_scope_id=draw(emails()), spec_version="v0.95", type=ElectionType.general, # good enough for now start_date=start_date, end_date=end_date, geopolitical_units=geo_units, parties=parties, candidates=candidates_, contests=contests, ballot_styles=styles, name=draw(internationalized_texts()), contact_information=draw(contact_infos()), )
def build_election(self, election_creation: dict): self.election = ElectionDescription.from_json_object( complete_election_description(election_creation['description'])) if not self.election.is_valid(): raise InvalidElectionDescription() self.number_of_guardians = len(election_creation['trustees']) self.quorum = election_creation['scheme']['parameters']['quorum'] self.election_builder = ElectionBuilder(self.number_of_guardians, self.quorum, self.election)
def test_simple_election_can_serialize(self): # Arrange subject = election_factory.get_simple_election_from_file() intermediate = subject.to_json() # Act result = ElectionDescription.from_json(intermediate) # Assert self.assertIsNotNone(result.election_scope_id) self.assertEqual(result.election_scope_id, "jefferson-county-primary")
def handle_ballot(request: AcceptBallotRequest, state: BallotBoxState) -> Any: ballot = CiphertextBallot.from_json_object(request.ballot) description = ElectionDescription.from_json_object(request.description) internal_description = InternalElectionDescription(description) context = CiphertextElectionContext.from_json_object(request.context) accepted_ballot = accept_ballot( ballot, state, internal_description, context, BallotStore(), ) return accepted_ballot
def build_election_context(request: ElectionContextRequest = Body(...)) -> Any: """ Build a CiphertextElectionContext for a given election """ description: ElectionDescription = ElectionDescription.from_json_object( request.description) elgamal_public_key: ElementModP = read_json_object( request.elgamal_public_key, ElementModP) number_of_guardians = request.number_of_guardians quorum = request.quorum context = make_ciphertext_election_context(number_of_guardians, quorum, elgamal_public_key, description.crypto_hash()) return write_json_object(context)
def _parse_tally_request( request: StartTallyRequest, ) -> Tuple[List[CiphertextAcceptedBallot], InternalElectionDescription, CiphertextElectionContext, ]: """ Deserialize common tally request values """ ballots = [ CiphertextAcceptedBallot.from_json_object(ballot) for ballot in request.ballots ] description = ElectionDescription.from_json_object(request.description) internal_description = InternalElectionDescription(description) context = CiphertextElectionContext.from_json_object(request.context) return (ballots, internal_description, context)
def decrypt_share( request: DecryptTallyShareRequest = Body(...), scheduler: Scheduler = Depends(get_scheduler), ) -> Any: """ Decrypt a single guardian's share of a tally """ description = InternalElectionDescription( ElectionDescription.from_json_object(request.description) ) context = CiphertextElectionContext.from_json_object(request.context) guardian = convert_guardian(request.guardian) tally = convert_tally(request.encrypted_tally, description, context) share = compute_decryption_share(guardian, tally, context, scheduler) return write_json_object(share)
def setupElectionBuilder(self): # Create an election builder instance, and configure it for a single public-private keypair. # in a real election, you would configure this for a group of guardians. See Key Ceremony for more information. with open(os.path.join(self.manifest_path, self.manifest_file), "r") as manifest: string_representation = manifest.read() election_description = ElectionDescription.from_json( string_representation) builder = ElectionBuilder( number_of_guardians=self. NUMBER_OF_GUARDIANS, # since we will generate a single public-private keypair, we set this to 1 quorum=self. QUORUM, # since we will generate a single public-private keypair, we set this to 1 description=election_description) builder.set_public_key(self.joint_public_key) self.metadata, self.context = get_optional(builder.build()) self.builder = builder
def validate_election_description( request: ValidateElectionDescriptionRequest = Body(...), schema: Any = Depends(get_description_schema), ) -> Any: """ Validate an Election description or manifest for a given election """ success = True message = "Election description successfully validated" details = "" # Check schema schema = request.schema_override if request.schema_override else schema (schema_success, error_details) = validate_json_schema(request.description, schema) if not schema_success: success = schema_success message = "Election description did not match schema" details = error_details # Check object parse description: Optional[ElectionDescription] = None if success: try: description = ElectionDescription.from_json_object( request.description) except Exception: # pylint: disable=broad-except success = False message = "Election description could not be read from JSON" if success: if description: valid_success = description.is_valid() if not valid_success: message = "Election description was not valid well formed data" # Check return ValidationResponse(success=success, message=message, details=details)
def create() -> Tuple: """ An election with only one guardian and random keys gets generated. More configuration options and the ability to hold a key ceremony should be added later. """ # Open an election manifest file with open(os.path.join(ELECTION_MANIFEST), "r") as manifest: string_representation = manifest.read() election_description = ElectionDescription.from_json( string_representation) # Create an election builder instance, and configure it for a single public-private keypair. # in a real election, you would configure this for a group of guardians. See Key Ceremony for more information. # TODO: Allow real key ceremony builder = ElectionBuilder( number_of_guardians= 1, # since we will generate a single public-private keypair, we set this to 1 quorum= 1, # since we will generate a single public-private keypair, we set this to 1 description=election_description) # We simply generate a random keypair. For a real election this step should # be replaced by the key ceremony keypair = elgamal_keypair_random() builder.set_public_key(keypair.public_key) # get an `InternalElectionDescription` and `CiphertextElectionContext` # that are used for the remainder of the election. (metadata, context) = builder.build() # Configure an encryption device # In the docs the encrypter device gets defined when encrypting a ballot. # I think for our usecase it makes more sense to define one encrypter and use for the whole election device = EncryptionDevice("polling-place-one") encrypter = EncryptionMediator(metadata, context, device) store = BallotStore() ballot_box = BallotBox(metadata, context, store) return metadata, context, encrypter, ballot_box, store, keypair
def decrypt_tally(request: DecryptTallyRequest = Body(...)) -> Any: """ Decrypt a tally from a collection of decrypted guardian shares """ description = InternalElectionDescription( ElectionDescription.from_json_object(request.description)) context = CiphertextElectionContext.from_json_object(request.context) tally = convert_tally(request.encrypted_tally, description, context) shares = { guardian_id: read_json_object(share, TallyDecryptionShare) for guardian_id, share in request.shares.items() } full_plaintext_tally = decrypt(tally, shares, context) if not full_plaintext_tally: raise HTTPException( status_code=500, detail="Unable to decrypt tally", ) published_plaintext_tally = publish_plaintext_tally(full_plaintext_tally) return published_plaintext_tally.to_json_object()
def _to_election_description_common( self, date: Optional[datetime] = None ) -> Tuple[ElectionDescription, Dict[str, str], Dict[ str, ContestDescription], Dict[str, BallotStyle], BallotPlaintextFactory, ]: # ballotstyle_map: Dict[str, BallotStyle], if date is None: date = datetime.now() party_uids = UidMaker("party") party_map = { p: Party( object_id=party_uids.next(), ballot_name=_str_to_internationalized_text_en(p), ) for p in self.metadata.all_parties } # A ballot style is a subset of all of the contests on the ballot. Luckily, we have a column # in the data ("ballot type"), which is exactly what we need. # "Geopolitical units" are meant to be their own thing (cities, counties, precincts, etc.), # but we don't have any data at all about them in the Dominion CVR file. Our current hack # is that we're making them one-to-one with contests. contest_uids = UidMaker("contest") gp_uids = UidMaker("gpunit") contest_map: Dict[str, ContestDescription] = {} all_candidates: List[Candidate] = [] all_gps: List[GeopoliticalUnit] = [] all_candidate_ids_to_columns: Dict[str, str] = {} for name in self.metadata.contest_map.keys(): ( contest_description, candidates, gp, candidate_id_to_column, ) = self._contest_name_to_description( name=name, contest_uid_maker=contest_uids, gp_uid_maker=gp_uids, ) contest_map[name] = contest_description all_candidates = all_candidates + candidates all_gps.append(gp) for c in candidate_id_to_column.keys(): all_candidate_ids_to_columns[c] = candidate_id_to_column[c] # ballotstyle_uids = UidMaker("ballotstyle") ballotstyle_map: Dict[str, BallotStyle] = { bt: self._ballot_style_from_id(bt, party_map, contest_map) for bt in self.metadata.ballot_types.keys() } bpf = BallotPlaintextFactory( self.metadata.style_map, contest_map, ballotstyle_map, all_candidate_ids_to_columns, ) return ( ElectionDescription( name=_str_to_internationalized_text_en( self.metadata.election_name), election_scope_id=self.metadata.election_name, type=ElectionType.unknown, start_date=date, end_date=date, geopolitical_units=all_gps, parties=list(party_map.values()), candidates=all_candidates, contests=list(contest_map.values()), ballot_styles=list(ballotstyle_map.values()), ), all_candidate_ids_to_columns, contest_map, ballotstyle_map, bpf, )
def verify( description: ElectionDescription, context: CiphertextElectionContext, constants: ElectionConstants, devices: Iterable[EncryptionDevice], ciphertext_ballots: Iterable[CiphertextAcceptedBallot], spoiled_ballots: Iterable[CiphertextAcceptedBallot], ciphertext_tally: PublishedCiphertextTally, plaintext_tally: PlaintextTally, coefficient_validation_sets: Iterable[CoefficientValidationSet] = None ) -> bool: """ Returns whether the election results provided as arguments represent a valid ElectionGuard election. Verification details can be emitted by setting the logging level.""" # Verify election paramter cryptographic values election_parameters: Invariants = Invariants('Election Parameters') election_parameters.ensure('p is correct', constants.large_prime == P) election_parameters.ensure('q is correct', constants.small_prime == Q) election_parameters.ensure('r is correct', constants.cofactor == R) election_parameters.ensure('g is correct', constants.generator == G) election_parameters.ensure('k ≥ 1', context.quorum >= 1) election_parameters.ensure('k ≤ n', context.number_of_guardians >= context.quorum) election_parameters.ensure( 'Q = H(p,Q,g,n,k,d)', context.crypto_base_hash == hash_elems(P, Q, G, context.number_of_guardians, context.quorum, description.crypto_hash())) if not election_parameters.validate(): return False # Verify guardian public key values public_keys: Invariants = Invariants('Guardian Public Keys') elgamal_public_key: ElementModP = int_to_p(1) for guardian in coefficient_validation_sets: elgamal_public_key = mult_p( elgamal_public_key, get_first_el(guardian.coefficient_commitments)) for proof in guardian.coefficient_proofs: # Warning: This definition follows the electionguard package in deviating from the official spec public_keys.ensure( 'cᵢⱼ = H(Kᵢⱼ,hᵢⱼ)', proof.challenge == hash_elems(proof.public_key, proof.commitment)) public_keys.ensure( 'gᵘⁱʲ mod p = hᵢⱼKᵢⱼᶜⁱ mod p', pow_p(constants.generator, proof.response) == mult_p( proof.commitment, pow_p(proof.public_key, proof.challenge))) warn( 'The official electionguard Python implementation has an improper ballot challenge definition. This error will be ignored by this verifier.' ) public_keys.ensure('K = ∏ᵢ₌₁ⁿ Kᵢ mod p', context.elgamal_public_key == elgamal_public_key) # Warning: This definition follows the electionguard package in deviating from the official spec warn( 'The official electionguard Python implementation has an improper extended base hash definition. This error will be ignored by this verifier.' ) public_keys.ensure( 'Q̅ = H(Q,K)', context.crypto_extended_base_hash == hash_elems( context.crypto_base_hash, context.elgamal_public_key)) if not public_keys.validate(): return False # Verify ballot selection encryptions ballot_selections: Invariants = Invariants('Ballot Selection Encryptions') for ballot in ciphertext_ballots: for contest in ballot.contests: for selection in contest.ballot_selections: ballot_selections.ensure( 'α ∈ Zₚʳ', selection.ciphertext.pad.is_valid_residue()) ballot_selections.ensure( 'β ∈ Zₚʳ', selection.ciphertext.data.is_valid_residue()) ballot_selections.ensure( 'a₀ ∈ Zₚʳ', selection.proof.proof_zero_pad.is_valid_residue()) ballot_selections.ensure( 'b₀ ∈ Zₚʳ', selection.proof.proof_zero_data.is_valid_residue()) ballot_selections.ensure( 'a₁ ∈ Zₚʳ', selection.proof.proof_one_pad.is_valid_residue()) ballot_selections.ensure( 'b₁ ∈ Zₚʳ', selection.proof.proof_one_pad.is_valid_residue()) ballot_selections.ensure( 'c = H(Q̅,α,β,a₀,b₀,a₁,b₁)', selection.proof.challenge == hash_elems( context.crypto_extended_base_hash, selection.ciphertext.pad, selection.ciphertext.data, selection.proof.proof_zero_pad, selection.proof.proof_zero_data, selection.proof.proof_one_pad, selection.proof.proof_one_data)) ballot_selections.ensure( 'c₀ ∈ Zᵩ', selection.proof.proof_zero_challenge.is_in_bounds()) ballot_selections.ensure( 'c₁ ∈ Zᵩ', selection.proof.proof_one_challenge.is_in_bounds()) ballot_selections.ensure( 'v₀ ∈ Zᵩ', selection.proof.proof_zero_response.is_in_bounds()) ballot_selections.ensure( 'v₁ ∈ Zᵩ', selection.proof.proof_one_response.is_in_bounds()) ballot_selections.ensure( 'c = c₀+c₁ mod q', selection.proof.challenge == add_q( selection.proof.proof_zero_challenge, selection.proof.proof_one_challenge)) ballot_selections.ensure( 'gᵛ⁰ = a₀αᶜ⁰ (mod p)', pow_p(constants.generator, selection.proof.proof_zero_response) == mult_p( selection.proof.proof_zero_pad, pow_p(selection.ciphertext.pad, selection.proof.proof_zero_challenge))) ballot_selections.ensure( 'gᵛ¹ = a₁αᶜ¹ (mod p)', pow_p(constants.generator, selection.proof.proof_one_response) == mult_p( selection.proof.proof_one_pad, pow_p(selection.ciphertext.pad, selection.proof.proof_one_challenge))) ballot_selections.ensure( 'Kᵛ⁰ = b₀βᶜ⁰ (mod p)', pow_p(context.elgamal_public_key, selection.proof.proof_zero_response) == mult_p( selection.proof.proof_zero_data, pow_p(selection.ciphertext.data, selection.proof.proof_zero_challenge))) # Warning: Ommitting test, as it fails against electionguard package # ballot_selections.ensure('gᶜ¹Kᵛ¹ = b₁βᶜ¹ (mod p)', mult_p(pow_p(constants.generator, selection.proof.proof_one_challenge), pow_p(context.elgamal_public_key, selection.proof.proof_one_response)) == mult_p(selection.proof.proof_one_pad, pow_p(selection.ciphertext.data, selection.proof.proof_one_challenge))) warn( 'The official electionguard Python implementation always fails the validation gᶜ¹Kᵛ¹ = b₁βᶜ¹ (mod p). This error will be ignored by this verifier.' ) if not ballot_selections.validate(): return False # Verify adherence to vote limits vote_limits: Invariants = Invariants('Vote Limits') contests: Contests = Contests(description) for ballot in ciphertext_ballots: for contest in ballot.contests: contest_description = contests[contest.object_id] vote_limits.ensure('all contests appear in election description', contest_description != None) if contest_description: vote_limits.ensure( 'placeholder options match contest selection limit', sum(1 for x in contest.ballot_selections if x.is_placeholder_selection) == contest_description.votes_allowed) vote_limits.ensure('V ∈ Zᵩ', contest.proof.response.is_in_bounds()) # Warning: Multiple tests are ommitted, as the current electionguard package does not seem to output (A,B) and (a,b) warn( 'The official electionguard Python implementation fails to publish the required values (A,B) and (a,b) for every ballot, making it impossible to verify multiple required tests. This error will be ignored by this verifier.' ) if not vote_limits.validate(): return False # Verify ballot chaining ballot_chaining: Invariants = Invariants('Ballot Chaining') # Warning: It is currently not possible to verify ballot chaining, as the electionguard package contains the following errors: # - Fails to establish an ordering of published encrypted ballots by providing a suitable index field # - Contains no "first" ballot with previous_hash == H₀ = H(Q̅), per the specification # - Fails to include any ballot device information in the hash calculation, as required by the electionguard spec warn( 'The official electionguard Python implementation fails to index ballots and adhere to the proper ballot chaining hash definition. This error will be ignored by this verifier.' ) if not ballot_chaining.validate(): return False # Verify correctness of ballot aggregation and partial decryptions ballot_aggregations: Invariants = Invariants( 'Ballot Aggregations & Partial Decryptions') guardians: Guardians = Guardians(coefficient_validation_sets) for contest in plaintext_tally.contests.values(): for selection in contest.selections.values(): A: ElementModP = int_to_p(1) B: ElementModP = int_to_p(1) for ballot in ciphertext_ballots: if ballot.state == BallotBoxState.CAST: ballot_selection: CiphertextBallotSelection = get_selection( ballot, contest.object_id, selection.object_id) if ballot_selection: A = mult_p(A, ballot_selection.ciphertext.pad) B = mult_p(B, ballot_selection.ciphertext.data) ballot_aggregations.ensure('A = ∏ⱼαⱼ', selection.message.pad == A) ballot_aggregations.ensure('B = ∏ⱼβⱼ', selection.message.data == B) for share in selection.shares: if share.proof: ballot_aggregations.ensure( 'vᵢ ∈ Zᵩ', share.proof.response.is_in_bounds()) ballot_aggregations.ensure( 'aᵢ ∈ Zₚʳ', share.proof.pad.is_valid_residue()) ballot_aggregations.ensure( 'bᵢ ∈ Zₚʳ', share.proof.data.is_valid_residue()) ballot_aggregations.ensure( 'cᵢ = H(Q̅,A,B,aᵢ,bᵢ,Mᵢ)', share.proof.challenge == hash_elems( context.crypto_extended_base_hash, selection.message.pad, selection.message.data, share.proof.pad, share.proof.data, share.share)) ballot_aggregations.ensure( 'Aᵛⁱ = bᵢMᵢᶜⁱ (mod p)', pow_p(selection.message.pad, share.proof.response) == mult_p( share.proof.data, pow_p(share.share, share.proof.challenge))) if share.guardian_id in guardians.guardians: ballot_aggregations.ensure( 'gᵛⁱ = aᵢKᵢᶜⁱ (mod p)', pow_p(constants.generator, share.proof.response) == mult_p( share.proof.pad, pow_p( get_first_el( guardians[share.guardian_id]. coefficient_commitments), share.proof.challenge))) else: ballot_aggregations.ensure( 'tally share guardians are valid election guardians', False) if not ballot_aggregations.validate(): return False # Verify correctness of recovered data for missing guardians missing_guardians: Invariants = Invariants( 'Recovered Data for Missing Guardians') for contest in plaintext_tally.contests.values(): for selection in contest.selections.values(): for share in selection.shares: missing_guardians.ensure( 'tally share contains exactly one proof or recovered part', (not share.proof) ^ (not share.recovered_parts)) if share.recovered_parts: for part in share.recovered_parts.values(): missing_guardians.ensure( 'vᵢₗ ∈ Zᵩ', part.proof.response.is_in_bounds()) missing_guardians.ensure( 'aᵢₗ ∈ Zₚʳ', part.proof.pad.is_valid_residue()) missing_guardians.ensure( 'bᵢₗ ∈ Zₚʳ', part.proof.data.is_valid_residue()) missing_guardians.ensure( 'cᵢₗ = H(Q̅,A,B,aᵢₗ,bᵢₗ,Mᵢₗ)', part.proof.challenge == hash_elems( context.crypto_extended_base_hash, selection.message.pad, selection.message.data, part.proof.pad, part.proof.data, part.share)) missing_guardians.ensure( 'Aᵛⁱˡ = bᵢₗMᵢₗᶜⁱˡ (mod p)', pow_p(selection.message.pad, part.proof.response) == mult_p( part.proof.data, pow_p(part.share, part.proof.challenge))) if part.guardian_id in guardians.guardians: missing_guardians.ensure( 'gᵛⁱˡ = aᵢₗ(∏ⱼ₌₀ᵏ⁻¹Kᵢⱼˡʲ)ᶜⁱˡ (mod p)', pow_p(constants.generator, part.proof.response) == mult_p( part.proof.pad, pow_p(part.recovery_key, part.proof.challenge))) else: missing_guardians.ensure( 'tally share reconstruction guardians are valid election guardians', False) if not missing_guardians.validate(): return False # Verify correctness of construction of replacement partial decryptions reconstructed_decryptions: Invariants = Invariants( 'Reconstructed Partial Decryptions for Missing Guardians') # Warning: the Lagrange coefficients used in reconstruction are not published. Because of this, it is impossible to verify: # - whether the Lagrange coefficients are correctly computed # - whether the missing tally shares, which depend on the Lagrange coefficients, are correctly computed. warn( 'The official electionguard Python implementation fails to publish Lagrange coefficients for missing guardian reconstructions, making it impossible to verify these values. This error will be ignored by this verifier.' ) if not reconstructed_decryptions.validate(): return False # Verify correct decryption of tallies tally_decryption: Invariants = Invariants('Decryption of Tallies') for contest in plaintext_tally.contests.values(): tally_decryption.ensure( 'tally contest label exists in ballot coding file', contest.object_id in contests.contests) for selection in contest.selections.values(): tally_decryption.ensure( 'B = M (∏ᵢ₌₁ⁿ Mᵢ) mod p', selection.message.data == mult_p( selection.value, *map(lambda x: x.share, selection.shares))) tally_decryption.ensure( 'M = gᵗ mod p', selection.value == pow_p(constants.generator, selection.tally)) if not tally_decryption.validate(): return False # Verify spoiled ballots spoils: Invariants = Invariants('Spoiled Ballots') for ballot in plaintext_tally.spoiled_ballots.values(): for contest in ballot.values(): tally_decryption.ensure( 'tally contest label exists in ballot coding file', contest.object_id in contests.contests) for selection in contest.selections.values(): for share in selection.shares: spoils.ensure( 'tally share contains exactly one proof or recovered part', (not share.proof) ^ (not share.recovered_parts)) if share.proof: spoils.ensure('vᵢ ∈ Zᵩ', share.proof.response.is_in_bounds()) spoils.ensure('aᵢ ∈ Zₚʳ', share.proof.pad.is_valid_residue()) spoils.ensure('bᵢ ∈ Zₚʳ', share.proof.data.is_valid_residue()) spoils.ensure( 'cᵢ = H(Q̅,A,B,aᵢ,bᵢ,Mᵢ)', share.proof.challenge == hash_elems( context.crypto_extended_base_hash, selection.message.pad, selection.message.data, share.proof.pad, share.proof.data, share.share)) spoils.ensure( 'Aᵛⁱ = bᵢMᵢᶜⁱ (mod p)', pow_p(selection.message.pad, share.proof.response) == mult_p( share.proof.data, pow_p(share.share, share.proof.challenge))) if share.guardian_id in guardians.guardians: spoils.ensure( 'gᵛⁱ = aᵢKᵢᶜⁱ (mod p)', pow_p(constants.generator, share.proof.response) == mult_p( share.proof.pad, pow_p( get_first_el( guardians[share.guardian_id]. coefficient_commitments), share.proof.challenge))) else: spoils.ensure( 'tally share guardians are valid election guardians', False) if share.recovered_parts: for part in share.recovered_parts.values(): spoils.ensure('vᵢₗ ∈ Zᵩ', part.proof.response.is_in_bounds()) spoils.ensure('aᵢₗ ∈ Zₚʳ', part.proof.pad.is_valid_residue()) spoils.ensure('bᵢₗ ∈ Zₚʳ', part.proof.data.is_valid_residue()) spoils.ensure( 'cᵢₗ = H(Q̅,A,B,aᵢₗ,bᵢₗ,Mᵢₗ)', part.proof.challenge == hash_elems( context.crypto_extended_base_hash, selection.message.pad, selection.message.data, part.proof.pad, part.proof.data, part.share)) spoils.ensure( 'Aᵛⁱˡ = bᵢₗMᵢₗᶜⁱˡ (mod p)', pow_p(selection.message.pad, part.proof.response) == mult_p( part.proof.data, pow_p(part.share, part.proof.challenge))) if part.guardian_id in guardians.guardians: spoils.ensure( 'gᵛⁱˡ = aᵢₗ(∏ⱼ₌₀ᵏ⁻¹Kᵢⱼˡʲ)ᶜⁱˡ (mod p)', pow_p(constants.generator, part.proof.response) == mult_p( part.proof.pad, pow_p(part.recovery_key, part.proof.challenge))) else: spoils.ensure( 'tally share reconstruction guardians are valid election guardians', False) spoils.ensure( 'B = M (∏ᵢ₌₁ⁿ Mᵢ) mod p', selection.message.data == mult_p( selection.value, *map(lambda x: x.share, selection.shares))) spoils.ensure( 'M = gᵗ mod p', selection.value == pow_p(constants.generator, selection.tally)) # Warning: All other warnings also apply to spoiled ballots. warn('All other warnings also apply to spoiled ballot verification steps.') if not spoils.validate(): return False # All verification steps have succeeded return True
def get_fake_election(self) -> ElectionDescription: """ Get a single Fake Election object that is manually constructed with default values """ fake_ballot_style = BallotStyle("some-ballot-style-id") fake_ballot_style.geopolitical_unit_ids = ["some-geopoltical-unit-id"] fake_referendum_ballot_selections = [ # Referendum selections are simply a special case of `candidate` in the object model SelectionDescription("some-object-id-affirmative", "some-candidate-id-1", 0), SelectionDescription("some-object-id-negative", "some-candidate-id-2", 1), ] sequence_order = 0 number_elected = 1 votes_allowed = 1 fake_referendum_contest = ReferendumContestDescription( "some-referendum-contest-object-id", "some-geopoltical-unit-id", sequence_order, VoteVariationType.one_of_m, number_elected, votes_allowed, "some-referendum-contest-name", fake_referendum_ballot_selections, ) fake_candidate_ballot_selections = [ SelectionDescription("some-object-id-candidate-1", "some-candidate-id-1", 0), SelectionDescription("some-object-id-candidate-2", "some-candidate-id-2", 1), SelectionDescription("some-object-id-candidate-3", "some-candidate-id-3", 2), ] sequence_order_2 = 1 number_elected_2 = 2 votes_allowed_2 = 2 fake_candidate_contest = CandidateContestDescription( "some-candidate-contest-object-id", "some-geopoltical-unit-id", sequence_order_2, VoteVariationType.one_of_m, number_elected_2, votes_allowed_2, "some-candidate-contest-name", fake_candidate_ballot_selections, ) fake_election = ElectionDescription( election_scope_id="some-scope-id", type=ElectionType.unknown, start_date=datetime.now(), end_date=datetime.now(), geopolitical_units=[ GeopoliticalUnit( "some-geopoltical-unit-id", "some-gp-unit-name", ReportingUnitType.unknown, ) ], parties=[Party("some-party-id-1"), Party("some-party-id-2")], candidates=[ Candidate("some-candidate-id-1"), Candidate("some-candidate-id-2"), Candidate("some-candidate-id-3"), ], contests=[fake_referendum_contest, fake_candidate_contest], ballot_styles=[fake_ballot_style], ) return fake_election
def _get_election_from_file(self, filename: str) -> ElectionDescription: with open(os.path.join(data, filename), "r") as subject: result = subject.read() target = ElectionDescription.from_json(result) return target