コード例 #1
0
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
コード例 #2
0
    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())
コード例 #3
0
    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
            )
コード例 #4
0
    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)
コード例 #5
0
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
コード例 #6
0
    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
コード例 #7
0
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()),
    )
コード例 #8
0
    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)
コード例 #9
0
    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")
コード例 #10
0
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
コード例 #11
0
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)
コード例 #12
0
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)
コード例 #13
0
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)
コード例 #14
0
    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
コード例 #15
0
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)
コード例 #16
0
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
コード例 #17
0
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()
コード例 #18
0
    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,
        )
コード例 #19
0
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
コード例 #20
0
    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
コード例 #21
0
    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