예제 #1
0
    def test_tally_spoiled_ballots_accumulates_valid_tally(
            self, everything: ELECTIONS_AND_BALLOTS_TUPLE_TYPE):
        # Arrange
        metadata, ballots, secret_key, context = everything
        # Tally the plaintext ballots for comparison later
        plaintext_tallies = accumulate_plaintext_ballots(ballots)

        # encrypt each ballot
        store = BallotStore()
        seed_hash = EncryptionDevice("Location").get_hash()
        for ballot in ballots:
            encrypted_ballot = encrypt_ballot(ballot, metadata, context,
                                              seed_hash)
            seed_hash = encrypted_ballot.tracking_hash
            self.assertIsNotNone(encrypted_ballot)
            # add to the ballot store
            store.set(
                encrypted_ballot.object_id,
                from_ciphertext_ballot(encrypted_ballot,
                                       BallotBoxState.SPOILED),
            )

        # act
        result = tally_ballots(store, metadata, context)
        self.assertIsNotNone(result)

        # Assert
        decrypted_tallies = self._decrypt_with_secret(result, secret_key)
        self.assertCountEqual(plaintext_tallies, decrypted_tallies)
        for value in decrypted_tallies.values():
            self.assertEqual(0, value)
        self.assertEqual(len(ballots), len(result.spoiled_ballots))
    def test_cast_ballot(self):
        # Arrange
        keypair = elgamal_keypair_from_secret(int_to_q(2))
        election = election_factory.get_fake_election()
        metadata, context = election_factory.get_fake_ciphertext_election(
            election, keypair.public_key
        )
        store = BallotStore()
        source = election_factory.get_fake_ballot(metadata)
        self.assertTrue(source.is_valid(metadata.ballot_styles[0].object_id))

        # Act
        data = encrypt_ballot(source, metadata, context, SEED_HASH)
        result = accept_ballot(data, BallotBoxState.CAST, metadata, context, store)

        # Assert
        expected = store.get(source.object_id)
        self.assertEqual(expected.state, BallotBoxState.CAST)
        self.assertEqual(result.state, BallotBoxState.CAST)
        self.assertEqual(expected.object_id, result.object_id)

        # Test failure modes
        self.assertIsNone(
            accept_ballot(data, BallotBoxState.CAST, metadata, context, store)
        )  # cannot cast again
        self.assertIsNone(
            accept_ballot(data, BallotBoxState.SPOILED, metadata, context, store)
        )  # cannot cspoil a ballot already cast
    def generate(self, number_of_ballots: int = DEFAULT_NUMBER_OF_BALLOTS):
        """
        Generate the sample data set
        """

        rmtree(RESULTS_DIR, ignore_errors=True)

        (
            public_data,
            private_data,
        ) = self.election_factory.get_hamilton_election_with_encryption_context()
        plaintext_ballots = self.ballot_factory.generate_fake_plaintext_ballots_for_election(
            public_data.metadata, number_of_ballots
        )
        self.encrypter = EncryptionMediator(
            public_data.metadata, public_data.context, self.encryption_device
        )

        ciphertext_ballots: List[CiphertextBallot] = []
        for plaintext_ballot in plaintext_ballots:
            ciphertext_ballots.append(
                get_optional(self.encrypter.encrypt(plaintext_ballot))
            )

        ballot_store = BallotStore()
        ballot_box = BallotBox(public_data.metadata, public_data.context, ballot_store)

        accepted_ballots: List[CiphertextAcceptedBallot] = []
        for ballot in ciphertext_ballots:
            if randint(0, 100) % 10 == 0:
                accepted_ballots.append(ballot_box.spoil(ballot))
            else:
                accepted_ballots.append(ballot_box.cast(ballot))

        ciphertext_tally = get_optional(
            tally_ballots(ballot_store, public_data.metadata, public_data.context)
        )

        decrypter = DecryptionMediator(
            public_data.metadata, public_data.context, ciphertext_tally
        )

        for guardian in private_data.guardians:
            decrypter.announce(guardian)

        plaintext_tally = get_optional(decrypter.get_plaintext_tally())

        publish(
            public_data.description,
            public_data.context,
            public_data.constants,
            accepted_ballots,
            ciphertext_tally,
            plaintext_tally,
            public_data.guardians,
        )

        publish_private_data(
            plaintext_ballots, ciphertext_ballots, private_data.guardians
        )
예제 #4
0
    def step_3_cast_and_spoil(self) -> None:
        """
        Accept each ballot by marking it as either cast or spoiled.
        This example demonstrates one way to accept ballots using the `BallotBox` class
        """

        # Configure the Ballot Box
        self.ballot_store = BallotStore()
        self.ballot_box = BallotBox(self.metadata, self.context,
                                    self.ballot_store)

        # Randomly cast or spoil the ballots
        for ballot in self.ciphertext_ballots:
            # if randint(0, 1):
            #     accepted_ballot = self.ballot_box.cast(ballot)
            # else:
            #     accepted_ballot = self.ballot_box.spoil(ballot)
            accepted_ballot = self.ballot_box.cast(ballot)

            self._assert_message(
                BallotBox.__qualname__,
                f"Accepted Ballot Id: {ballot.object_id} state: {get_optional(accepted_ballot).state}",
                accepted_ballot is not None,
            )

            # Send Over Network
        send_over_network(self.ciphertext_ballots)
예제 #5
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
예제 #6
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
    def _generate_encrypted_tally(
        self,
        metadata: InternalElectionDescription,
        context: CiphertextElectionContext,
        ballots: List[PlaintextBallot],
    ) -> CiphertextTally:

        # encrypt each ballot
        store = BallotStore()
        for ballot in ballots:
            encrypted_ballot = encrypt_ballot(ballot, metadata, context,
                                              int_to_q_unchecked(1))
            self.assertIsNotNone(encrypted_ballot)
            # add to the ballot store
            store.set(
                encrypted_ballot.object_id,
                from_ciphertext_ballot(encrypted_ballot, BallotBoxState.CAST),
            )

        tally = tally_ballots(store, metadata, context)
        self.assertIsNotNone(tally)
        return get_optional(tally)
    def setUp(self):

        self.key_ceremony = KeyCeremonyMediator(self.CEREMONY_DETAILS)

        self.guardians: List[Guardian] = []

        # Setup Guardians
        for i in range(self.NUMBER_OF_GUARDIANS):
            sequence = i + 2
            self.guardians.append(
                Guardian(
                    "guardian_" + str(sequence),
                    sequence,
                    self.NUMBER_OF_GUARDIANS,
                    self.QUORUM,
                ))

        # Attendance (Public Key Share)
        for guardian in self.guardians:
            self.key_ceremony.announce(guardian)

        self.key_ceremony.orchestrate(identity_auxiliary_encrypt)
        self.key_ceremony.verify(identity_auxiliary_decrypt)

        self.joint_public_key = self.key_ceremony.publish_joint_key()
        self.assertIsNotNone(self.joint_public_key)

        # setup the election
        self.election = election_factory.get_fake_election()
        builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM,
                                  self.election)

        self.assertIsNone(
            builder.build())  # Can't build without the public key

        builder.set_public_key(self.joint_public_key)
        self.metadata, self.context = get_optional(builder.build())

        self.encryption_device = EncryptionDevice("location")
        self.ballot_marking_device = EncryptionMediator(
            self.metadata, self.context, self.encryption_device)

        # get some fake ballots
        self.fake_cast_ballot = ballot_factory.get_fake_ballot(
            self.metadata, "some-unique-ballot-id-cast")
        self.more_fake_ballots = []
        for i in range(10):
            self.more_fake_ballots.append(
                ballot_factory.get_fake_ballot(
                    self.metadata, f"some-unique-ballot-id-cast{i}"))
        self.fake_spoiled_ballot = ballot_factory.get_fake_ballot(
            self.metadata, "some-unique-ballot-id-spoiled")
        self.assertTrue(
            self.fake_cast_ballot.is_valid(
                self.metadata.ballot_styles[0].object_id))
        self.assertTrue(
            self.fake_spoiled_ballot.is_valid(
                self.metadata.ballot_styles[0].object_id))
        self.expected_plaintext_tally = accumulate_plaintext_ballots(
            [self.fake_cast_ballot] + self.more_fake_ballots)

        # Fill in the expected values with any missing selections
        # that were not made on any ballots
        selection_ids = set([
            selection.object_id for contest in self.metadata.contests
            for selection in contest.ballot_selections
        ])

        missing_selection_ids = selection_ids.difference(
            set(self.expected_plaintext_tally))

        for id in missing_selection_ids:
            self.expected_plaintext_tally[id] = 0

        # Encrypt
        encrypted_fake_cast_ballot = self.ballot_marking_device.encrypt(
            self.fake_cast_ballot)
        encrypted_fake_spoiled_ballot = self.ballot_marking_device.encrypt(
            self.fake_spoiled_ballot)
        self.assertIsNotNone(encrypted_fake_cast_ballot)
        self.assertIsNotNone(encrypted_fake_spoiled_ballot)
        self.assertTrue(
            encrypted_fake_cast_ballot.is_valid_encryption(
                self.context.crypto_extended_base_hash, self.joint_public_key))

        # encrypt some more fake ballots
        self.more_fake_encrypted_ballots = []
        for fake_ballot in self.more_fake_ballots:
            self.more_fake_encrypted_ballots.append(
                self.ballot_marking_device.encrypt(fake_ballot))

        # configure the ballot box
        ballot_store = BallotStore()
        ballot_box = BallotBox(self.metadata, self.context, ballot_store)
        ballot_box.cast(encrypted_fake_cast_ballot)
        ballot_box.spoil(encrypted_fake_spoiled_ballot)

        # Cast some more fake ballots
        for fake_ballot in self.more_fake_encrypted_ballots:
            ballot_box.cast(fake_ballot)

        # generate encrypted tally
        self.ciphertext_tally = tally_ballots(ballot_store, self.metadata,
                                              self.context)
예제 #9
0
    def test_ballot_store(self):

        # Arrange
        keypair = elgamal_keypair_from_secret(int_to_q(2))
        election = election_factory.get_fake_election()
        metadata, context = election_factory.get_fake_ciphertext_election(
            election, keypair.public_key)

        # get an encrypted fake ballot to work with
        fake_ballot = election_factory.get_fake_ballot(metadata)
        encrypted_ballot = encrypt_ballot(fake_ballot, metadata, context,
                                          SEED_HASH)

        # Set up the ballot store
        subject = BallotStore()
        data_cast = CiphertextAcceptedBallot(
            encrypted_ballot.object_id,
            encrypted_ballot.ballot_style,
            encrypted_ballot.description_hash,
            encrypted_ballot.previous_tracking_hash,
            encrypted_ballot.contests,
            encrypted_ballot.tracking_hash,
            encrypted_ballot.timestamp,
        )
        data_cast.state = BallotBoxState.CAST

        data_spoiled = CiphertextAcceptedBallot(
            encrypted_ballot.object_id,
            encrypted_ballot.ballot_style,
            encrypted_ballot.description_hash,
            encrypted_ballot.previous_tracking_hash,
            encrypted_ballot.contests,
            encrypted_ballot.tracking_hash,
            encrypted_ballot.timestamp,
        )
        data_spoiled.state = BallotBoxState.SPOILED

        self.assertIsNone(subject.get("cast"))
        self.assertIsNone(subject.get("spoiled"))

        # try to set a ballot with an unknown state
        self.assertFalse(
            subject.set(
                "unknown",
                CiphertextAcceptedBallot(
                    encrypted_ballot.object_id,
                    encrypted_ballot.ballot_style,
                    encrypted_ballot.description_hash,
                    encrypted_ballot.previous_tracking_hash,
                    encrypted_ballot.contests,
                    encrypted_ballot.tracking_hash,
                    encrypted_ballot.timestamp,
                ),
            ))

        # Act
        self.assertTrue(subject.set("cast", data_cast))
        self.assertTrue(subject.set("spoiled", data_spoiled))

        self.assertEqual(subject.get("cast"), data_cast)
        self.assertEqual(subject.get("spoiled"), data_spoiled)

        self.assertEqual(subject.exists("cast"), (True, data_cast))
        self.assertEqual(subject.exists("spoiled"), (True, data_spoiled))

        # test mutate state
        data_cast.state = BallotBoxState.UNKNOWN
        self.assertEqual(subject.exists("cast"), (False, data_cast))

        # test remove
        self.assertTrue(subject.set("cast", None))
        self.assertEqual(subject.exists("cast"), (False, None))
예제 #10
0
    def generate(
        self,
        number_of_ballots: int = DEFAULT_NUMBER_OF_BALLOTS,
        cast_spoil_ratio: int = CAST_SPOIL_RATIO,
    ):
        """
        Generate the sample data set
        """

        # Clear the results directory
        rmtree(RESULTS_DIR, ignore_errors=True)

        # Configure the election
        (
            public_data,
            private_data,
        ) = self.election_factory.get_hamilton_election_with_encryption_context(
        )
        plaintext_ballots = self.ballot_factory.generate_fake_plaintext_ballots_for_election(
            public_data.metadata, number_of_ballots)
        self.encrypter = EncryptionMediator(public_data.metadata,
                                            public_data.context,
                                            self.encryption_device)

        # Encrypt some ballots
        ciphertext_ballots: List[CiphertextBallot] = []
        for plaintext_ballot in plaintext_ballots:
            ciphertext_ballots.append(
                get_optional(self.encrypter.encrypt(plaintext_ballot)))

        ballot_store = BallotStore()
        ballot_box = BallotBox(public_data.metadata, public_data.context,
                               ballot_store)

        # Randomly cast/spoil the ballots
        accepted_ballots: List[CiphertextAcceptedBallot] = []
        for ballot in ciphertext_ballots:
            if randint(0, 100) % cast_spoil_ratio == 0:
                accepted_ballots.append(ballot_box.spoil(ballot))
            else:
                accepted_ballots.append(ballot_box.cast(ballot))

        # Tally
        ciphertext_tally = get_optional(
            tally_ballots(ballot_store, public_data.metadata,
                          public_data.context))

        # Decrypt
        decrypter = DecryptionMediator(public_data.metadata,
                                       public_data.context, ciphertext_tally)

        for i, guardian in enumerate(private_data.guardians):
            if i < QUORUM or not THRESHOLD_ONLY:
                decrypter.announce(guardian)

        plaintext_tally = get_optional(decrypter.get_plaintext_tally())

        # Publish
        publish(
            public_data.description,
            public_data.context,
            public_data.constants,
            [self.encryption_device],
            accepted_ballots,
            ciphertext_tally.spoiled_ballots.values(),
            publish_ciphertext_tally(ciphertext_tally),
            plaintext_tally,
            public_data.guardians,
        )

        publish_private_data(plaintext_ballots, ciphertext_ballots,
                             private_data.guardians)
예제 #11
0
    def test_tally_ballot_invalid_input_fails(
            self, everything: ELECTIONS_AND_BALLOTS_TUPLE_TYPE):

        # Arrange
        metadata, ballots, secret_key, context = everything

        # encrypt each ballot
        store = BallotStore()
        seed_hash = EncryptionDevice("Location").get_hash()
        for ballot in ballots:
            encrypted_ballot = encrypt_ballot(ballot, metadata, context,
                                              seed_hash)
            seed_hash = encrypted_ballot.tracking_hash
            self.assertIsNotNone(encrypted_ballot)
            # add to the ballot store
            store.set(
                encrypted_ballot.object_id,
                from_ciphertext_ballot(encrypted_ballot, BallotBoxState.CAST),
            )

        subject = CiphertextTally("my-tally", metadata, context)

        # act
        cached_ballots = store.all()
        first_ballot = cached_ballots[0]
        first_ballot.state = BallotBoxState.UNKNOWN

        # verify an UNKNOWN state ballot fails
        self.assertIsNone(tally_ballot(first_ballot, subject))
        self.assertFalse(subject.append(first_ballot))

        # cast a ballot
        first_ballot.state = BallotBoxState.CAST
        self.assertTrue(subject.append(first_ballot))

        # try to append a spoiled ballot
        first_ballot.state = BallotBoxState.SPOILED
        self.assertFalse(subject.append(first_ballot))

        # Verify accumulation fails if the selection collection is empty
        if first_ballot.state == BallotBoxState.CAST:
            self.assertFalse(
                subject.cast[first_ballot.object_id].elgamal_accumulate([]))

        # pop the cast ballot
        subject._cast_ballot_ids.pop()

        # reset to cast
        first_ballot.state = BallotBoxState.CAST

        self.assertTrue(
            self._cannot_erroneously_mutate_state(subject, first_ballot,
                                                  BallotBoxState.CAST))

        self.assertTrue(
            self._cannot_erroneously_mutate_state(subject, first_ballot,
                                                  BallotBoxState.SPOILED))

        self.assertTrue(
            self._cannot_erroneously_mutate_state(subject, first_ballot,
                                                  BallotBoxState.UNKNOWN))

        # verify a spoiled ballot cannot be added twice
        first_ballot.state = BallotBoxState.SPOILED
        self.assertTrue(subject.append(first_ballot))
        self.assertFalse(subject.append(first_ballot))

        # verify an already spoiled ballot cannot be cast
        first_ballot.state = BallotBoxState.CAST
        self.assertFalse(subject.append(first_ballot))

        # pop the spoiled ballot
        subject.spoiled_ballots.pop(first_ballot.object_id)

        # verify a cast ballot cannot be added twice
        first_ballot.state = BallotBoxState.CAST
        self.assertTrue(subject.append(first_ballot))
        self.assertFalse(subject.append(first_ballot))

        # verify an already cast ballot cannot be spoiled
        first_ballot.state = BallotBoxState.SPOILED
        self.assertFalse(subject.append(first_ballot))