Example #1
0
    def test_reset(self):
        # Arrange
        mediator = KeyCeremonyMediator("mediator_reset", CEREMONY_DETAILS)
        new_ceremony_details = CeremonyDetails(3, 3)

        mediator.reset(new_ceremony_details)
        self.assertEqual(mediator.ceremony_details, new_ceremony_details)
Example #2
0
    def fail_round_3(guardians: List[Guardian],
                     mediator: KeyCeremonyMediator) -> GuardianPair:
        """Perform Round 3 including verifying backups but fail a single backup"""

        failing_guardian_pair = GuardianPair(guardians[0].id, guardians[1].id)

        for guardian in guardians:
            for other_guardian in guardians:
                verifications = []
                if guardian.id is not other_guardian.id:
                    verification = guardian.verify_election_partial_key_backup(
                        other_guardian.id)
                    if (verification.owner_id is failing_guardian_pair.owner_id
                            and verification.designated_id is
                            failing_guardian_pair.designated_id):
                        verification = ElectionPartialKeyVerification(
                            failing_guardian_pair.owner_id,
                            failing_guardian_pair.designated_id,
                            failing_guardian_pair.designated_id,
                            False,
                        )
                    verifications.append(verification)
                mediator.receive_backup_verifications(verifications)

        return failing_guardian_pair
Example #3
0
 def __init__(self, manifest_file: str, manifest_path: str, guardians: int,
              quorom: int):
     self.NUMBER_OF_GUARDIANS = guardians
     self.QUORUM = quorom
     self.manifest_path = manifest_path
     self.manifest_file = manifest_file
     self.guardians: List[Guardian] = []
     self.key_ceremony_mediator = KeyCeremonyMediator(
         CeremonyDetails(self.NUMBER_OF_GUARDIANS, self.QUORUM))
Example #4
0
    def perform_round_1(guardians: List[Guardian],
                        mediator: KeyCeremonyMediator) -> None:
        """Perform Round 1 including announcing guardians and sharing public keys"""

        for guardian in guardians:
            mediator.announce(guardian.share_public_keys())

        for guardian in guardians:
            other_guardian_keys = mediator.share_announced(guardian.id)
            for guardian_public_keys in other_guardian_keys:
                guardian.save_guardian_public_keys(guardian_public_keys)
Example #5
0
    def perform_round_3(guardians: List[Guardian],
                        mediator: KeyCeremonyMediator):
        """Perform Round 3 including verifying backups"""

        for guardian in guardians:
            for other_guardian in guardians:
                verifications = []
                if guardian.id is not other_guardian.id:
                    verifications.append(
                        guardian.verify_election_partial_key_backup(
                            other_guardian.id, identity_auxiliary_decrypt))
                mediator.receive_backup_verifications(verifications)
Example #6
0
    def perform_round_2(guardians: List[Guardian],
                        mediator: KeyCeremonyMediator) -> None:
        """Perform Round 2 including generating backups and sharing backups"""

        for guardian in guardians:
            guardian.generate_election_partial_key_backups(
                identity_auxiliary_encrypt)
            mediator.receive_backups(
                guardian.share_election_partial_key_backups())

        for guardian in guardians:
            backups = mediator.share_backups(guardian.id)
            for backup in backups:
                guardian.save_election_partial_key_backup(backup)
Example #7
0
    def get_hamilton_election_with_encryption_context(
        self, ) -> Tuple[AllPublicElectionData, AllPrivateElectionData]:
        guardians: List[Guardian] = []
        coefficient_validation_sets: List[CoefficientValidationSet] = []

        # Configure the election builder
        description = self.get_hamilton_election_from_file()
        builder = ElectionBuilder(NUMBER_OF_GUARDIANS, QUORUM, description)

        # Setup Guardians
        for i in range(NUMBER_OF_GUARDIANS):
            guardians.append(
                Guardian(
                    "hamilton-county-canvass-board-member-" + str(i),
                    i,
                    NUMBER_OF_GUARDIANS,
                    QUORUM,
                ))

        # Run the key ceremony
        mediator = KeyCeremonyMediator(guardians[0].ceremony_details)
        for guardian in guardians:
            mediator.announce(guardian)
        mediator.orchestrate()
        mediator.verify()

        # Joint Key
        joint_key = mediator.publish_joint_key()

        # Save Validation Keys
        for guardian in guardians:
            coefficient_validation_sets.append(
                guardian.share_coefficient_validation_set())

        builder.set_public_key(get_optional(joint_key).joint_public_key)
        builder.set_commitment_hash(get_optional(joint_key).commitment_hash)
        metadata, context = get_optional(builder.build())
        constants = ElectionConstants()

        return (
            AllPublicElectionData(description, metadata, context, constants,
                                  coefficient_validation_sets),
            AllPrivateElectionData(guardians),
        )
Example #8
0
    def test_exchange_of_backups(self):
        """Round 2: Exchange of election partial key backups"""

        # Arrange
        mediator = KeyCeremonyMediator("mediator_backups_exchange",
                                       CEREMONY_DETAILS)
        KeyCeremonyHelper.perform_round_1(GUARDIANS, mediator)

        # Round 2 - Guardians Only
        GUARDIAN_1.generate_election_partial_key_backups()
        GUARDIAN_2.generate_election_partial_key_backups()
        backup_from_1_for_2 = GUARDIAN_1.share_election_partial_key_backup(
            GUARDIAN_2_ID)
        backup_from_2_for_1 = GUARDIAN_2.share_election_partial_key_backup(
            GUARDIAN_1_ID)

        # Act
        mediator.receive_backups([backup_from_1_for_2])

        # Assert
        self.assertFalse(mediator.all_backups_available())

        # Act
        mediator.receive_backups([backup_from_2_for_1])

        # Assert
        self.assertTrue(mediator.all_backups_available())

        # Act
        guardian1_backups = mediator.share_backups(GUARDIAN_1_ID)
        guardian2_backups = mediator.share_backups(GUARDIAN_2_ID)

        # Assert
        self.assertIsNotNone(guardian1_backups)
        self.assertIsNotNone(guardian2_backups)
        self.assertEqual(len(guardian1_backups), 1)
        self.assertEqual(len(guardian2_backups), 1)
        for backup in guardian1_backups:
            self.assertEqual(backup.designated_id, GUARDIAN_1_ID)
        for backup in guardian2_backups:
            self.assertEqual(backup.designated_id, GUARDIAN_2_ID)
        self.assertEqual(guardian1_backups[0], backup_from_2_for_1)
        self.assertEqual(guardian2_backups[0], backup_from_1_for_2)
Example #9
0
    def test_exchange_of_election_public_keys(self):
        # Arrange
        mediator = KeyCeremonyMediator(CEREMONY_DETAILS)

        # Act
        mediator.receive_election_public_key(
            GUARDIAN_1.share_election_public_key())

        # Assert
        self.assertFalse(mediator.all_election_public_keys_available())
        partial_list = mediator.share_election_public_keys()
        self.assertIsNotNone(partial_list)
        self.assertEqual(len(partial_list), 1)

        # Act
        mediator.receive_election_public_key(
            GUARDIAN_2.share_election_public_key())

        # Assert
        self.assertTrue(mediator.all_election_public_keys_available())
        partial_list = mediator.share_election_public_keys()
        self.assertIsNotNone(partial_list)
        self.assertEqual(len(partial_list), 2)
    def get_hamilton_manifest_with_encryption_context(
        self, ) -> Tuple[AllPublicElectionData, AllPrivateElectionData]:
        guardians: List[Guardian] = []
        guardian_records: List[GuardianRecord] = []

        # Configure the election builder
        manifest = self.get_hamilton_manifest_from_file()
        builder = ElectionBuilder(NUMBER_OF_GUARDIANS, QUORUM, manifest)

        # Run the Key Ceremony
        ceremony_details = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM)
        guardians = KeyCeremonyHelper.create_guardians(ceremony_details)
        mediator = KeyCeremonyMediator("key-ceremony-mediator",
                                       ceremony_details)
        KeyCeremonyHelper.perform_full_ceremony(guardians, mediator)

        # Final: Joint Key
        joint_key = mediator.publish_joint_key()

        # Publish Guardian Records
        guardian_records = [guardian.publish() for guardian in guardians]

        builder.set_public_key(get_optional(joint_key).joint_public_key)
        builder.set_commitment_hash(get_optional(joint_key).commitment_hash)
        internal_manifest, context = get_optional(builder.build())
        constants = ElectionConstants()

        return (
            AllPublicElectionData(
                manifest,
                internal_manifest,
                context,
                constants,
                guardian_records,
            ),
            AllPrivateElectionData(guardians),
        )
Example #11
0
    def test_mediator_takes_attendance(self):
        # Arrange
        mediator = KeyCeremonyMediator(CEREMONY_DETAILS)

        # Act
        mediator.confirm_presence_of_guardian(GUARDIAN_1.share_public_keys())

        # Assert
        self.assertFalse(mediator.all_guardians_in_attendance())

        # Act
        mediator.confirm_presence_of_guardian(GUARDIAN_2.share_public_keys())

        # Assert
        self.assertTrue(mediator.all_guardians_in_attendance())

        # Act
        guardians = mediator.share_guardians_in_attendance()

        # Assert
        self.assertIsNotNone(guardians)
        self.assertEqual(len(guardians), NUMBER_OF_GUARDIANS)
Example #12
0
class ElectionGuardTestClient:
    def __init__(self, manifest_file: str, manifest_path: str, guardians: int,
                 quorom: int):
        self.NUMBER_OF_GUARDIANS = guardians
        self.QUORUM = quorom
        self.manifest_path = manifest_path
        self.manifest_file = manifest_file
        self.guardians: List[Guardian] = []
        self.key_ceremony_mediator = KeyCeremonyMediator(
            CeremonyDetails(self.NUMBER_OF_GUARDIANS, self.QUORUM))

    def setupGuardians(self):
        # 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_mediator.announce(guardian)

    def setupKeyCeromony(self):
        self.setupGuardians()
        self.key_ceremony_mediator.orchestrate(identity_auxiliary_encrypt)
        self.verification_results = self.key_ceremony_mediator.verify(
            identity_auxiliary_decrypt)
        self.joint_public_key = self.key_ceremony_mediator.publish_joint_key()

    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
Example #13
0
    def test_take_attendance(self):
        """Round 1: Mediator takes attendance and guardians announce"""

        # Arrange
        mediator = KeyCeremonyMediator("mediator_attendance", CEREMONY_DETAILS)

        # Act
        mediator.announce(GUARDIAN_1.share_public_keys())

        # Assert
        self.assertFalse(mediator.all_guardians_announced())

        # Act
        mediator.announce(GUARDIAN_2.share_public_keys())

        # Assert
        self.assertTrue(mediator.all_guardians_announced())

        # Act
        guardian_key_sets = mediator.share_announced()

        # Assert
        self.assertIsNotNone(guardian_key_sets)
        self.assertEqual(len(guardian_key_sets), NUMBER_OF_GUARDIANS)
Example #14
0
    def test_exchange_of_election_partial_key_backup(self):
        # Arrange
        mediator = KeyCeremonyMediator(CEREMONY_DETAILS)
        mediator.confirm_presence_of_guardian(GUARDIAN_1.share_public_keys())
        mediator.confirm_presence_of_guardian(GUARDIAN_2.share_public_keys())
        backup_from_1_for_2 = GUARDIAN_1.share_election_partial_key_backup(
            GUARDIAN_2_ID)
        backup_from_2_for_1 = GUARDIAN_2.share_election_partial_key_backup(
            GUARDIAN_1_ID)

        # Act
        mediator.receive_election_partial_key_backup(backup_from_1_for_2)

        # Assert
        self.assertFalse(mediator.all_election_partial_key_backups_available())

        # Act
        mediator.receive_election_partial_key_backup(backup_from_2_for_1)

        # Assert
        self.assertTrue(mediator.all_election_partial_key_backups_available())

        # Act
        guardian1_backups = mediator.share_election_partial_key_backups_to_guardian(
            GUARDIAN_1_ID)
        guardian2_backups = mediator.share_election_partial_key_backups_to_guardian(
            GUARDIAN_2_ID)

        # Assert
        self.assertIsNotNone(guardian1_backups)
        self.assertIsNotNone(guardian2_backups)
        self.assertEqual(len(guardian1_backups), 1)
        self.assertEqual(len(guardian2_backups), 1)
        for backup in guardian1_backups:
            self.assertEqual(backup.designated_id, GUARDIAN_1_ID)
        for backup in guardian2_backups:
            self.assertEqual(backup.designated_id, GUARDIAN_2_ID)
        self.assertEqual(guardian1_backups[0], backup_from_2_for_1)
        self.assertEqual(guardian2_backups[0], backup_from_1_for_2)
Example #15
0
    def test_partial_key_backup_verification_success(self):
        """
        Test for the happy path of the verification process where each key is successfully verified and no bad actors.
        """
        # Arrange
        mediator = KeyCeremonyMediator(CEREMONY_DETAILS)
        mediator.confirm_presence_of_guardian(GUARDIAN_1.share_public_keys())
        mediator.confirm_presence_of_guardian(GUARDIAN_2.share_public_keys())
        mediator.receive_election_partial_key_backup(
            GUARDIAN_1.share_election_partial_key_backup(GUARDIAN_2_ID))
        mediator.receive_election_partial_key_backup(
            GUARDIAN_2.share_election_partial_key_backup(GUARDIAN_1_ID))
        GUARDIAN_1.save_election_partial_key_backup(
            mediator.share_election_partial_key_backups_to_guardian(
                GUARDIAN_1_ID)[0])
        GUARDIAN_2.save_election_partial_key_backup(
            mediator.share_election_partial_key_backups_to_guardian(
                GUARDIAN_2_ID)[0])
        verification1 = GUARDIAN_1.verify_election_partial_key_backup(
            GUARDIAN_2_ID, identity_auxiliary_decrypt)
        verification2 = GUARDIAN_2.verify_election_partial_key_backup(
            GUARDIAN_1_ID, identity_auxiliary_decrypt)

        # Act
        mediator.receive_election_partial_key_verification(verification1)

        # Assert
        self.assertFalse(
            mediator.all_election_partial_key_verifications_received())
        self.assertFalse(mediator.all_election_partial_key_backups_verified())
        self.assertIsNone(mediator.publish_joint_key())

        # Act
        mediator.receive_election_partial_key_verification(verification2)
        joint_key = mediator.publish_joint_key()

        # Assert
        self.assertTrue(
            mediator.all_election_partial_key_verifications_received())
        self.assertTrue(mediator.all_election_partial_key_backups_verified())
        self.assertIsNotNone(joint_key)
Example #16
0
    def test_partial_key_backup_verification_failure(self):
        """
        In this case, the recipient guardian does not correctly verify the sent key backup.
        This failed verificaton requires the sender create a challenge and a new verifier aka another guardian must verify this challenge. 
        """
        # Arrange
        mediator = KeyCeremonyMediator(CEREMONY_DETAILS)
        mediator.confirm_presence_of_guardian(GUARDIAN_1.share_public_keys())
        mediator.confirm_presence_of_guardian(GUARDIAN_2.share_public_keys())
        mediator.receive_election_partial_key_backup(
            GUARDIAN_1.share_election_partial_key_backup(GUARDIAN_2_ID))
        mediator.receive_election_partial_key_backup(
            GUARDIAN_2.share_election_partial_key_backup(GUARDIAN_1_ID))
        GUARDIAN_1.save_election_partial_key_backup(
            mediator.share_election_partial_key_backups_to_guardian(
                GUARDIAN_1_ID)[0])
        GUARDIAN_2.save_election_partial_key_backup(
            mediator.share_election_partial_key_backups_to_guardian(
                GUARDIAN_2_ID)[0])
        verification1 = GUARDIAN_1.verify_election_partial_key_backup(
            GUARDIAN_2_ID, identity_auxiliary_decrypt)
        verification2 = GUARDIAN_2.verify_election_partial_key_backup(
            GUARDIAN_1_ID, identity_auxiliary_decrypt)

        # Act
        failed_verification2 = ElectionPartialKeyVerification(
            verification2.owner_id,
            verification2.designated_id,
            verification2.verifier_id,
            False,
        )
        mediator.receive_election_partial_key_verification(verification1)
        mediator.receive_election_partial_key_verification(
            failed_verification2)
        failed_pairs = mediator.share_failed_partial_key_verifications()
        missing_challenges = mediator.share_missing_election_partial_key_challenges(
        )

        # Assert
        self.assertTrue(
            mediator.all_election_partial_key_verifications_received())
        self.assertFalse(mediator.all_election_partial_key_backups_verified())
        self.assertIsNone(mediator.publish_joint_key())
        self.assertEqual(len(failed_pairs), 1)
        self.assertEqual(failed_pairs[0],
                         GuardianPair(GUARDIAN_1_ID, GUARDIAN_2_ID))
        self.assertEqual(len(missing_challenges), 1)
        self.assertEqual(missing_challenges[0],
                         GuardianPair(GUARDIAN_1_ID, GUARDIAN_2_ID))

        # Act
        challenge = GUARDIAN_1.publish_election_backup_challenge(GUARDIAN_2_ID)
        mediator.receive_election_partial_key_challenge(challenge)
        no_missing_challenges = mediator.share_missing_election_partial_key_challenges(
        )

        # Assert
        self.assertFalse(mediator.all_election_partial_key_backups_verified())
        self.assertEqual(len(no_missing_challenges), 0)

        # Act
        challenges = mediator.share_open_election_partial_key_challenges()
        challenge_verification = VERIFIER.verify_election_partial_key_challenge(
            challenges[0])
        mediator.receive_election_partial_key_verification(
            challenge_verification)
        joint_key = mediator.publish_joint_key()

        # Assert
        self.assertEqual(len(challenges), 1)
        self.assertTrue(mediator.all_election_partial_key_backups_verified())
        self.assertIsNotNone(joint_key)
class TestEndToEndElection(TestCase):
    """
    Test a complete simple example of executing an End-to-End encrypted election.
    In a real world scenario all of these steps would not be completed on the same machine.
    """

    NUMBER_OF_GUARDIANS = 5
    QUORUM = 3

    REMOVE_OUTPUT = False

    # Step 0 - Configure Election
    manifest: Manifest
    election_builder: ElectionBuilder
    internal_manifest: InternalManifest
    context: CiphertextElectionContext
    constants: ElectionConstants

    # Step 1 - Key Ceremony
    mediator: KeyCeremonyMediator
    guardians: List[Guardian] = []

    # Step 2 - Encrypt Votes
    device: EncryptionDevice
    encrypter: EncryptionMediator
    plaintext_ballots: List[PlaintextBallot]
    ciphertext_ballots: List[CiphertextBallot] = []

    # Step 3 - Cast and Spoil
    ballot_store: DataStore
    ballot_box: BallotBox

    # Step 4 - Decrypt Tally
    ciphertext_tally: CiphertextTally
    plaintext_tally: PlaintextTally
    plaintext_spoiled_ballots: Dict[str, PlaintextTally]
    decryption_mediator: DecryptionMediator

    # Step 5 - Publish
    guardian_records: List[GuardianRecord] = []

    def test_end_to_end_election(self) -> None:
        """
        Execute the simplified end-to-end test demonstrating each component of the system.
        """
        self.step_0_configure_election()
        self.step_1_key_ceremony()
        self.step_2_encrypt_votes()
        self.step_3_cast_and_spoil()
        self.step_4_decrypt_tally()
        self.step_5_publish_and_verify()

    def step_0_configure_election(self) -> None:
        """
        To conduct an election, load an `Manifest` file
        """

        # Load a pre-configured Election Description
        # TODO: replace with complex election
        self.manifest = ElectionFactory().get_simple_manifest_from_file()
        print(f"""
            {'-'*40}\n
            # Election Summary:
            # Scope: {self.manifest.election_scope_id}
            # Geopolitical Units: {len(self.manifest.geopolitical_units)}
            # Parties: {len(self.manifest.parties)}
            # Candidates: {len(self.manifest.candidates)}
            # Contests: {len(self.manifest.contests)}
            # Ballot Styles: {len(self.manifest.ballot_styles)}\n
            {'-'*40}\n
            """)
        self._assert_message(
            Manifest.is_valid.__qualname__,
            "Verify that the input election meta-data is well-formed",
            self.manifest.is_valid(),
        )

        # Create an Election Builder
        self.election_builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS,
                                                self.QUORUM, self.manifest)
        self._assert_message(
            ElectionBuilder.__qualname__,
            f"Created with number_of_guardians: {self.NUMBER_OF_GUARDIANS} quorum: {self.QUORUM}",
        )

        # Move on to the Key Ceremony

    def step_1_key_ceremony(self) -> None:
        """
        Using the NUMBER_OF_GUARDIANS, generate public-private keypairs and share
        representations of those keys with QUORUM of other Guardians.  Then, combine
        the public election keys to make a joint election key that is used to encrypt ballots
        """

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

        # Setup Mediator
        self.mediator = KeyCeremonyMediator("mediator_1",
                                            self.guardians[0].ceremony_details)

        # ROUND 1: Public Key Sharing
        # Announce
        for guardian in self.guardians:
            self.mediator.announce(guardian.share_public_keys())

        # Share Keys
        for guardian in self.guardians:
            announced_keys = self.mediator.share_announced()
            for key_set in announced_keys:
                if guardian.id is not key_set.election.owner_id:
                    guardian.save_guardian_public_keys(key_set)

        self._assert_message(
            KeyCeremonyMediator.all_guardians_announced.__qualname__,
            "Confirms all guardians have shared their public keys",
            self.mediator.all_guardians_announced(),
        )

        # ROUND 2: Election Partial Key Backup Sharing
        # Share Backups
        for sending_guardian in self.guardians:
            sending_guardian.generate_election_partial_key_backups(
                identity_auxiliary_encrypt)
            backups = []
            for designated_guardian in self.guardians:
                if designated_guardian.id != sending_guardian.id:
                    backups.append(
                        sending_guardian.share_election_partial_key_backup(
                            designated_guardian.id))
            self.mediator.receive_backups(backups)
            self._assert_message(
                KeyCeremonyMediator.receive_backups.__qualname__,
                "Receive election partial key backups from key owning guardian",
                len(backups) == NUMBER_OF_GUARDIANS - 1,
            )

        self._assert_message(
            KeyCeremonyMediator.all_backups_available.__qualname__,
            "Confirm all guardians have shared their election partial key backups",
            self.mediator.all_backups_available(),
        )

        # Receive Backups
        for designated_guardian in self.guardians:
            backups = self.mediator.share_backups(designated_guardian.id)
            self._assert_message(
                KeyCeremonyMediator.share_backups.__qualname__,
                "Share election partial key backups for the designated guardian",
                len(backups) == NUMBER_OF_GUARDIANS - 1,
            )
            for backup in backups:
                designated_guardian.save_election_partial_key_backup(backup)

        # ROUND 3: Verification of Backups
        # Verify Backups
        for designated_guardian in self.guardians:
            verifications = []
            for backup_owner in self.guardians:
                if designated_guardian.id is not backup_owner.id:
                    verification = (
                        designated_guardian.verify_election_partial_key_backup(
                            backup_owner.id, identity_auxiliary_encrypt))
                    verifications.append(verification)
            self.mediator.receive_backup_verifications(verifications)

        self._assert_message(
            KeyCeremonyMediator.all_backups_verified.__qualname__,
            "Confirms all guardians have verified the backups of all other guardians",
            self.mediator.all_backups_verified(),
        )

        # FINAL: Publish Joint Key
        joint_key = self.mediator.publish_joint_key()
        self._assert_message(
            KeyCeremonyMediator.publish_joint_key.__qualname__,
            "Publishes the Joint Election Key",
            joint_key is not None,
        )

        # Build the Election
        self.election_builder.set_public_key(
            get_optional(joint_key).joint_public_key)
        self.election_builder.set_commitment_hash(
            get_optional(joint_key).commitment_hash)
        self.internal_manifest, self.context = get_optional(
            self.election_builder.build())
        self.constants = ElectionConstants()

        # Move on to encrypting ballots

    def step_2_encrypt_votes(self) -> None:
        """
        Using the `CiphertextElectionContext` encrypt ballots for the election
        """

        # Configure the Encryption Device
        self.device = ElectionFactory.get_encryption_device()
        self.encrypter = EncryptionMediator(self.internal_manifest,
                                            self.context, self.device)
        self._assert_message(
            EncryptionDevice.__qualname__,
            f"Ready to encrypt at location: {self.device.location}",
        )

        # Load some Ballots
        self.plaintext_ballots = BallotFactory().get_simple_ballots_from_file()
        self._assert_message(
            PlaintextBallot.__qualname__,
            f"Loaded ballots: {len(self.plaintext_ballots)}",
            len(self.plaintext_ballots) > 0,
        )

        # Encrypt the Ballots
        for plaintext_ballot in self.plaintext_ballots:
            encrypted_ballot = self.encrypter.encrypt(plaintext_ballot)
            self._assert_message(
                EncryptionMediator.encrypt.__qualname__,
                f"Ballot Id: {plaintext_ballot.object_id}",
                encrypted_ballot is not None,
            )
            self.ciphertext_ballots.append(get_optional(encrypted_ballot))

        # Next, we cast or spoil the ballots

    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 = DataStore()
        self.ballot_box = BallotBox(self.internal_manifest, self.context,
                                    self.ballot_store)

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

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

    def step_4_decrypt_tally(self) -> None:
        """
        Homomorphically combine the selections made on all of the cast ballots
        and use the Available Guardians to decrypt the combined tally.
        In this way, no individual voter's cast ballot is ever decrypted drectly.
        """

        # Generate a Homomorphically Accumulated Tally of the ballots
        self.ciphertext_tally = get_optional(
            tally_ballots(self.ballot_store, self.internal_manifest,
                          self.context))
        self.ciphertext_ballots = get_ballots(self.ballot_store,
                                              BallotBoxState.SPOILED)
        self._assert_message(
            tally_ballots.__qualname__,
            f"""
            - cast: {self.ciphertext_tally.cast()}
            - spoiled: {self.ciphertext_tally.spoiled()}
            Total: {len(self.ciphertext_tally)}
            """,
            self.ciphertext_tally is not None,
        )

        # Configure the Decryption
        ciphertext_ballots = list(self.ciphertext_ballots.values())
        self.decryption_mediator = DecryptionMediator(
            "decryption-mediator",
            self.context,
        )

        # Announce each guardian as present
        count = 0
        for guardian in self.guardians:
            guardian_key = guardian.share_election_public_key()
            tally_share = guardian.compute_tally_share(self.ciphertext_tally,
                                                       self.context)
            ballot_shares = guardian.compute_ballot_shares(
                ciphertext_ballots, self.context)
            self.decryption_mediator.announce(guardian_key, tally_share,
                                              ballot_shares)
            count += 1
            self._assert_message(
                DecryptionMediator.announce.__qualname__,
                f"Guardian Present: {guardian.id}",
                len(self.decryption_mediator.get_available_guardians()) ==
                count,
            )

        # Get the plaintext Tally
        self.plaintext_tally = get_optional(
            self.decryption_mediator.get_plaintext_tally(
                self.ciphertext_tally))
        self._assert_message(
            DecryptionMediator.get_plaintext_tally.__qualname__,
            "Tally Decrypted",
            self.plaintext_tally is not None,
        )

        # Get the plaintext Spoiled Ballots
        self.plaintext_spoiled_ballots = get_optional(
            self.decryption_mediator.get_plaintext_ballots(ciphertext_ballots))
        self._assert_message(
            DecryptionMediator.get_plaintext_ballots.__qualname__,
            "Spoiled Ballots Decrypted",
            self.plaintext_tally is not None,
        )

        # Now, compare the results
        self.compare_results()

    def compare_results(self) -> None:
        """
        Compare the results to ensure the decryption was done correctly
        """
        print(f"""
            {'-'*40}
            # Election Results:

            """)

        # Create a representation of each contest's tally
        selection_ids = [
            selection.object_id for contest in self.manifest.contests
            for selection in contest.ballot_selections
        ]
        expected_plaintext_tally: Dict[str, int] = {
            key: 0
            for key in selection_ids
        }

        # Tally the expected values from the loaded ballots
        for ballot in self.plaintext_ballots:
            if (get_optional(self.ballot_store.get(
                    ballot.object_id)).state == BallotBoxState.CAST):
                for contest in ballot.contests:
                    for selection in contest.ballot_selections:
                        expected_plaintext_tally[
                            selection.object_id] += selection.vote

        # Compare the expected tally to the decrypted tally
        for tally_contest in self.plaintext_tally.contests.values():
            print(f" Contest: {tally_contest.object_id}")
            for tally_selection in tally_contest.selections.values():
                expected = expected_plaintext_tally[tally_selection.object_id]
                self._assert_message(
                    f"   - Selection: {tally_selection.object_id}",
                    f"expected: {expected}, actual: {tally_selection.tally}",
                    expected == tally_selection.tally,
                )
        print(f"\n{'-'*40}\n")

        # Compare the expected values for each spoiled ballot
        for ballot in self.plaintext_ballots:
            if (get_optional(self.ballot_store.get(
                    ballot.object_id)).state == BallotBoxState.SPOILED):
                print(f"\nSpoiled Ballot: {ballot.object_id}")
                for contest in ballot.contests:
                    print(f"\n Contest: {contest.object_id}")
                    for selection in contest.ballot_selections:
                        expected = selection.vote
                        decrypted_selection = (self.plaintext_spoiled_ballots[
                            ballot.object_id].contests[contest.object_id].
                                               selections[selection.object_id])
                        self._assert_message(
                            f"   - Selection: {selection.object_id}",
                            f"expected: {expected}, actual: {decrypted_selection.tally}",
                            expected == decrypted_selection.tally,
                        )

    def step_5_publish_and_verify(self) -> None:
        """Publish and verify steps of the election"""
        self.publish_results()
        self.verify_results()

        if self.REMOVE_OUTPUT:
            rmtree(RESULTS_DIR)

    def publish_results(self) -> None:
        """
        Publish results/artifacts of the election
        """

        self.guardian_records = [
            guardian.publish() for guardian in self.guardians
        ]

        publish(
            self.manifest,
            self.context,
            self.constants,
            [self.device],
            self.ballot_store.all(),
            self.plaintext_spoiled_ballots.values(),
            self.ciphertext_tally.publish(),
            self.plaintext_tally,
            self.guardian_records,
            RESULTS_DIR,
        )
        self._assert_message(
            "Publish",
            f"Artifacts published to: {RESULTS_DIR}",
            path.exists(RESULTS_DIR),
        )

    def verify_results(self) -> None:
        """Verify results of election"""

        # Deserialize
        manifest_from_file = Manifest.from_json_file(MANIFEST_FILE_NAME,
                                                     RESULTS_DIR)
        self.assertEqual(self.manifest, manifest_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)

        for ballot in self.ballot_store.all():
            name = BALLOT_PREFIX + ballot.object_id
            ballot_from_file = SubmittedBallot.from_json_file(
                name, BALLOTS_DIR)
            self.assertEqual(ballot, ballot_from_file)

        for spoiled_ballot in self.plaintext_spoiled_ballots.values():
            name = BALLOT_PREFIX + spoiled_ballot.object_id
            spoiled_ballot_from_file = PlaintextTally.from_json_file(
                name, SPOILED_DIR)
            self.assertEqual(spoiled_ballot, spoiled_ballot_from_file)

        published_ciphertext_tally_from_file = PublishedCiphertextTally.from_json_file(
            ENCRYPTED_TALLY_FILE_NAME, RESULTS_DIR)
        self.assertEqual(self.ciphertext_tally.publish(),
                         published_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)

        for guardian_record in self.guardian_records:
            set_name = COEFFICIENT_PREFIX + guardian_record.guardian_id
            guardian_record_from_file = GuardianRecord.from_json_file(
                set_name, GUARDIAN_DIR)
            self.assertEqual(guardian_record, guardian_record_from_file)

    def _assert_message(self,
                        name: str,
                        message: str,
                        condition: Union[Callable, bool] = True) -> None:
        if callable(condition):
            result = condition()
        else:
            result = condition

        print(f"{name}: {message}: {result}")
        self.assertTrue(result)
    def step_1_key_ceremony(self) -> None:
        """
        Using the NUMBER_OF_GUARDIANS, generate public-private keypairs and share
        representations of those keys with QUORUM of other Guardians.  Then, combine
        the public election keys to make a joint election key that is used to encrypt ballots
        """

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

        # Setup Mediator
        self.mediator = KeyCeremonyMediator("mediator_1",
                                            self.guardians[0].ceremony_details)

        # ROUND 1: Public Key Sharing
        # Announce
        for guardian in self.guardians:
            self.mediator.announce(guardian.share_public_keys())

        # Share Keys
        for guardian in self.guardians:
            announced_keys = self.mediator.share_announced()
            for key_set in announced_keys:
                if guardian.id is not key_set.election.owner_id:
                    guardian.save_guardian_public_keys(key_set)

        self._assert_message(
            KeyCeremonyMediator.all_guardians_announced.__qualname__,
            "Confirms all guardians have shared their public keys",
            self.mediator.all_guardians_announced(),
        )

        # ROUND 2: Election Partial Key Backup Sharing
        # Share Backups
        for sending_guardian in self.guardians:
            sending_guardian.generate_election_partial_key_backups(
                identity_auxiliary_encrypt)
            backups = []
            for designated_guardian in self.guardians:
                if designated_guardian.id != sending_guardian.id:
                    backups.append(
                        sending_guardian.share_election_partial_key_backup(
                            designated_guardian.id))
            self.mediator.receive_backups(backups)
            self._assert_message(
                KeyCeremonyMediator.receive_backups.__qualname__,
                "Receive election partial key backups from key owning guardian",
                len(backups) == NUMBER_OF_GUARDIANS - 1,
            )

        self._assert_message(
            KeyCeremonyMediator.all_backups_available.__qualname__,
            "Confirm all guardians have shared their election partial key backups",
            self.mediator.all_backups_available(),
        )

        # Receive Backups
        for designated_guardian in self.guardians:
            backups = self.mediator.share_backups(designated_guardian.id)
            self._assert_message(
                KeyCeremonyMediator.share_backups.__qualname__,
                "Share election partial key backups for the designated guardian",
                len(backups) == NUMBER_OF_GUARDIANS - 1,
            )
            for backup in backups:
                designated_guardian.save_election_partial_key_backup(backup)

        # ROUND 3: Verification of Backups
        # Verify Backups
        for designated_guardian in self.guardians:
            verifications = []
            for backup_owner in self.guardians:
                if designated_guardian.id is not backup_owner.id:
                    verification = (
                        designated_guardian.verify_election_partial_key_backup(
                            backup_owner.id, identity_auxiliary_encrypt))
                    verifications.append(verification)
            self.mediator.receive_backup_verifications(verifications)

        self._assert_message(
            KeyCeremonyMediator.all_backups_verified.__qualname__,
            "Confirms all guardians have verified the backups of all other guardians",
            self.mediator.all_backups_verified(),
        )

        # FINAL: Publish Joint Key
        joint_key = self.mediator.publish_joint_key()
        self._assert_message(
            KeyCeremonyMediator.publish_joint_key.__qualname__,
            "Publishes the Joint Election Key",
            joint_key is not None,
        )

        # Build the Election
        self.election_builder.set_public_key(
            get_optional(joint_key).joint_public_key)
        self.election_builder.set_commitment_hash(
            get_optional(joint_key).commitment_hash)
        self.internal_manifest, self.context = get_optional(
            self.election_builder.build())
        self.constants = ElectionConstants()
Example #19
0
class TestDecryption(TestCase):
    """Test decryption methods"""

    NUMBER_OF_GUARDIANS = 3
    QUORUM = 2
    CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM)

    def setUp(self):

        # Key Ceremony
        self.key_ceremony_mediator = KeyCeremonyMediator(
            "key_ceremony_mediator_mediator", self.CEREMONY_DETAILS)
        self.guardians: List[Guardian] = KeyCeremonyHelper.create_guardians(
            self.CEREMONY_DETAILS)
        KeyCeremonyHelper.perform_full_ceremony(self.guardians,
                                                self.key_ceremony_mediator)
        self.joint_public_key = self.key_ceremony_mediator.publish_joint_key()

        # Setup the election
        manifest = election_factory.get_fake_manifest()
        builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM,
                                  manifest)
        builder.set_public_key(self.joint_public_key.joint_public_key)
        builder.set_commitment_hash(self.joint_public_key.commitment_hash)
        self.internal_manifest, self.context = get_optional(builder.build())

        self.encryption_device = election_factory.get_encryption_device()
        self.ballot_marking_device = EncryptionMediator(
            self.internal_manifest, self.context, self.encryption_device)

        # get some fake ballots
        self.fake_cast_ballot = ballot_factory.get_fake_ballot(
            self.internal_manifest, "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.internal_manifest, f"some-unique-ballot-id-cast{i}"))
        self.fake_spoiled_ballot = ballot_factory.get_fake_ballot(
            self.internal_manifest, "some-unique-ballot-id-spoiled")
        self.more_fake_spoiled_ballots = []
        for i in range(2):
            self.more_fake_spoiled_ballots.append(
                ballot_factory.get_fake_ballot(
                    self.internal_manifest,
                    f"some-unique-ballot-id-spoiled{i}"))
        self.assertTrue(
            self.fake_cast_ballot.is_valid(
                self.internal_manifest.ballot_styles[0].object_id))
        self.assertTrue(
            self.fake_spoiled_ballot.is_valid(
                self.internal_manifest.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 = {
            selection.object_id
            for contest in self.internal_manifest.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
        self.encrypted_fake_cast_ballot = self.ballot_marking_device.encrypt(
            self.fake_cast_ballot)
        self.encrypted_fake_spoiled_ballot = self.ballot_marking_device.encrypt(
            self.fake_spoiled_ballot)

        # 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))
        # encrypt some more fake ballots
        self.more_fake_encrypted_spoiled_ballots = []
        for fake_ballot in self.more_fake_spoiled_ballots:
            self.more_fake_encrypted_spoiled_ballots.append(
                self.ballot_marking_device.encrypt(fake_ballot))

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

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

        # generate encrypted tally
        self.ciphertext_tally = tally_ballots(ballot_store,
                                              self.internal_manifest,
                                              self.context)
        self.ciphertext_ballots: Dict[BALLOT_ID,
                                      SubmittedBallot] = get_ballots(
                                          ballot_store, BallotBoxState.SPOILED)

    def tearDown(self):
        self.key_ceremony_mediator.reset(
            CeremonyDetails(self.NUMBER_OF_GUARDIANS, self.QUORUM))

    # SHARE
    def test_compute_decryption_share(self):
        # Arrange
        guardian = self.guardians[0]

        # Act
        # Guardian doesn't give keys
        broken_secret_key = ZERO_MOD_Q
        broken_guardian_key_pair = ElectionKeyPair(
            guardian.id,
            guardian.sequence_order,
            ElGamalKeyPair(broken_secret_key,
                           guardian._election_keys.key_pair.public_key),
            guardian._election_keys.polynomial,
        )

        broken_share = compute_decryption_share(
            broken_guardian_key_pair,
            self.ciphertext_tally,
            self.context,
        )

        # Assert
        self.assertIsNone(broken_share)

        # Act
        # Normal use case
        share = compute_decryption_share(
            guardian._election_keys,
            self.ciphertext_tally,
            self.context,
        )

        # Assert
        self.assertIsNotNone(share)

    def test_compute_compensated_decryption_share(self):
        # Arrange
        guardian = self.guardians[0]
        missing_guardian = self.guardians[2]

        public_key = guardian.share_election_public_key()
        auxiliary_keys = guardian._auxiliary_keys
        missing_guardian_public_key = missing_guardian.share_election_public_key(
        )
        missing_guardian_backup = missing_guardian._backups_to_share.get(
            guardian.id)

        # Act
        share = compute_compensated_decryption_share(
            public_key,
            auxiliary_keys,
            missing_guardian_public_key,
            missing_guardian_backup,
            self.ciphertext_tally,
            self.context,
            identity_auxiliary_decrypt,
        )

        # Assert
        self.assertIsNotNone(share)

    # SELECTION
    def test_compute_selection(self):
        # Arrange
        first_selection = [
            selection for contest in self.ciphertext_tally.contests.values()
            for selection in contest.selections.values()
        ][0]

        # act
        result = compute_decryption_share_for_selection(
            self.guardians[0]._election_keys, first_selection, self.context)

        # assert
        self.assertIsNotNone(result)

    def test_compute_compensated_selection(self):
        """
        demonstrates the complete workflow for computing a comepnsated decryption share
        For one selection. It is useful for verifying that the workflow is correct
        """
        # Arrange
        available_guardian_1 = self.guardians[0]
        available_guardian_2 = self.guardians[1]
        missing_guardian = self.guardians[2]
        available_guardian_1_key = available_guardian_1.share_election_public_key(
        )
        available_guardian_2_key = available_guardian_2.share_election_public_key(
        )
        missing_guardian_key = missing_guardian.share_election_public_key()

        first_selection = [
            selection for contest in self.ciphertext_tally.contests.values()
            for selection in contest.selections.values()
        ][0]

        # Compute lagrange coefficients for the guardians that are present
        lagrange_0 = compute_lagrange_coefficient(
            available_guardian_1.sequence_order,
            *[available_guardian_2.sequence_order],
        )
        lagrange_1 = compute_lagrange_coefficient(
            available_guardian_2.sequence_order,
            *[available_guardian_1.sequence_order],
        )

        print((
            f"lagrange: sequence_orders: ({available_guardian_1.sequence_order}, "
            f"{available_guardian_2.sequence_order}, {missing_guardian.sequence_order})\n"
        ))

        print(lagrange_0)
        print(lagrange_1)

        # compute their shares
        share_0 = compute_decryption_share_for_selection(
            available_guardian_1._election_keys, first_selection, self.context)

        share_1 = compute_decryption_share_for_selection(
            available_guardian_2._election_keys, first_selection, self.context)

        self.assertIsNotNone(share_0)
        self.assertIsNotNone(share_1)

        # compute compensations shares for the missing guardian
        compensation_0 = compute_compensated_decryption_share_for_selection(
            available_guardian_1.share_election_public_key(),
            available_guardian_1._auxiliary_keys,
            missing_guardian.share_election_public_key(),
            missing_guardian.share_election_partial_key_backup(
                available_guardian_1.id),
            first_selection,
            self.context,
            identity_auxiliary_decrypt,
        )

        compensation_1 = compute_compensated_decryption_share_for_selection(
            available_guardian_2.share_election_public_key(),
            available_guardian_2._auxiliary_keys,
            missing_guardian.share_election_public_key(),
            missing_guardian.share_election_partial_key_backup(
                available_guardian_2.id),
            first_selection,
            self.context,
            identity_auxiliary_decrypt,
        )

        self.assertIsNotNone(compensation_0)
        self.assertIsNotNone(compensation_1)

        print("\nSHARES:")
        print(compensation_0)
        print(compensation_1)

        # Check the share proofs
        self.assertTrue(
            compensation_0.proof.is_valid(
                first_selection.ciphertext,
                compute_recovery_public_key(available_guardian_1_key,
                                            missing_guardian_key),
                compensation_0.share,
                self.context.crypto_extended_base_hash,
            ))

        self.assertTrue(
            compensation_1.proof.is_valid(
                first_selection.ciphertext,
                compute_recovery_public_key(available_guardian_2_key,
                                            missing_guardian_key),
                compensation_1.share,
                self.context.crypto_extended_base_hash,
            ))

        share_pow_p = [
            pow_p(compensation_0.share, lagrange_0),
            pow_p(compensation_1.share, lagrange_1),
        ]

        print("\nSHARE_POW_P")
        print(share_pow_p)

        # reconstruct the missing share from the compensation shares
        reconstructed_share = mult_p(*[
            pow_p(compensation_0.share, lagrange_0),
            pow_p(compensation_1.share, lagrange_1),
        ])

        print("\nRECONSTRUCTED SHARE\n")
        print(reconstructed_share)

        share_2 = create_ciphertext_decryption_selection(
            first_selection.object_id,
            missing_guardian.id,
            reconstructed_share,
            {
                available_guardian_1.id: compensation_0,
                available_guardian_2.id: compensation_1,
            },
        )

        # Decrypt the result
        result = decrypt_selection_with_decryption_shares(
            first_selection,
            {
                available_guardian_1.id: (
                    available_guardian_1.share_election_public_key().key,
                    share_0,
                ),
                available_guardian_2.id: (
                    available_guardian_2.share_election_public_key().key,
                    share_1,
                ),
                missing_guardian.id: (
                    missing_guardian.share_election_public_key().key,
                    share_2,
                ),
            },
            self.context.crypto_extended_base_hash,
        )

        print(result)

        self.assertIsNotNone(result)
        self.assertEqual(
            result.tally,
            self.expected_plaintext_tally[first_selection.object_id])

    def test_compute_compensated_selection_failure(self):
        # Arrange
        available_guardian = self.guardians[0]
        missing_guardian = self.guardians[2]

        first_selection = [
            selection for contest in self.ciphertext_tally.contests.values()
            for selection in contest.selections.values()
        ][0]

        # Act
        # Get backup for missing guardian instead of one sent by guardian
        incorrect_backup = available_guardian.share_election_partial_key_backup(
            missing_guardian.id)

        result = compute_compensated_decryption_share_for_selection(
            available_guardian.share_election_public_key(),
            available_guardian._auxiliary_keys,
            missing_guardian.share_election_public_key(),
            incorrect_backup,
            first_selection,
            self.context,
            identity_auxiliary_decrypt,
        )

        # Assert
        self.assertIsNone(result)

    def test_reconstruct_decryption_share(self):
        # Arrange
        available_guardians = self.guardians[0:2]
        available_guardians_keys = [
            guardian.share_election_public_key()
            for guardian in available_guardians
        ]
        missing_guardian = self.guardians[2]
        missing_guardian_key = missing_guardian.share_election_public_key()
        missing_guardian_backups = {
            backup.designated_id: backup
            for backup in
            missing_guardian.share_election_partial_key_backups()
        }
        tally = self.ciphertext_tally

        # Act
        compensated_shares: Dict[GUARDIAN_ID, CompensatedDecryptionShare] = {
            available_guardian.id: compute_compensated_decryption_share(
                available_guardian.share_election_public_key(),
                available_guardian._auxiliary_keys,
                missing_guardian_key,
                missing_guardian_backups[available_guardian.id],
                tally,
                self.context,
                identity_auxiliary_decrypt,
            )
            for available_guardian in available_guardians
        }

        lagrange_coefficients = compute_lagrange_coefficients_for_guardians(
            available_guardians_keys)

        share = reconstruct_decryption_share(missing_guardian_key, tally,
                                             compensated_shares,
                                             lagrange_coefficients)

        # Assert
        self.assertEqual(self.QUORUM, len(compensated_shares))
        self.assertEqual(self.QUORUM, len(lagrange_coefficients))
        self.assertIsNotNone(share)

    def test_reconstruct_decryption_shares_for_ballot(self):
        # Arrange
        available_guardians = self.guardians[0:2]
        available_guardians_keys = [
            guardian.share_election_public_key()
            for guardian in available_guardians
        ]
        missing_guardian = self.guardians[2]
        missing_guardian_key = missing_guardian.share_election_public_key()
        missing_guardian_backups = {
            backup.designated_id: backup
            for backup in
            missing_guardian.share_election_partial_key_backups()
        }
        ballot = list(self.ciphertext_ballots.values())[0]

        # Act
        compensated_ballot_shares: Dict[GUARDIAN_ID,
                                        CompensatedDecryptionShare] = {}
        for available_guardian in available_guardians:
            compensated_share = compute_compensated_decryption_share_for_ballot(
                available_guardian.share_election_public_key(),
                available_guardian._auxiliary_keys,
                missing_guardian_key,
                missing_guardian_backups[available_guardian.id],
                ballot,
                self.context,
                identity_auxiliary_decrypt,
            )
            if compensated_share:
                compensated_ballot_shares[
                    available_guardian.id] = compensated_share

        lagrange_coefficients = compute_lagrange_coefficients_for_guardians(
            available_guardians_keys)

        missing_ballot_share = reconstruct_decryption_share_for_ballot(
            missing_guardian_key,
            ballot,
            compensated_ballot_shares,
            lagrange_coefficients,
        )

        # Assert
        self.assertEqual(self.QUORUM, len(lagrange_coefficients))
        self.assertEqual(len(available_guardians),
                         len(compensated_ballot_shares))
        self.assertEqual(len(available_guardians), len(lagrange_coefficients))
        self.assertIsNotNone(missing_ballot_share)

    def test_reconstruct_decryption_share_for_ballot(self):
        # Arrange
        available_guardians = self.guardians[0:2]
        available_guardians_keys = [
            guardian.share_election_public_key()
            for guardian in available_guardians
        ]
        missing_guardian = self.guardians[2]
        missing_guardian_key = missing_guardian.share_election_public_key()
        missing_guardian_backups = {
            backup.designated_id: backup
            for backup in
            missing_guardian.share_election_partial_key_backups()
        }
        ballot = self.ciphertext_ballots[self.fake_spoiled_ballot.object_id]

        # Act
        compensated_shares: Dict[GUARDIAN_ID, CompensatedDecryptionShare] = {
            available_guardian.id: get_optional(
                compute_compensated_decryption_share_for_ballot(
                    available_guardian.share_election_public_key(),
                    available_guardian._auxiliary_keys,
                    missing_guardian_key,
                    missing_guardian_backups[available_guardian.id],
                    ballot,
                    self.context,
                    identity_auxiliary_decrypt,
                ))
            for available_guardian in available_guardians
        }

        lagrange_coefficients = compute_lagrange_coefficients_for_guardians(
            available_guardians_keys)

        share = reconstruct_decryption_share_for_ballot(
            missing_guardian_key, ballot, compensated_shares,
            lagrange_coefficients)

        # Assert
        self.assertEqual(self.QUORUM, len(compensated_shares))
        self.assertEqual(self.QUORUM, len(lagrange_coefficients))
        self.assertIsNotNone(share)
Example #20
0
class TestEndToEndElection(TestCase):
    """
    Test a complete simple example of executing an End-to-End encrypted election.
    In a real world scenario all of these steps would not be completed on the same machine.
    """

    NUMBER_OF_GUARDIANS = 5
    QUORUM = 3

    REMOVE_OUTPUT = False

    # Step 0 - Configure Election
    description: ElectionDescription
    election_builder: ElectionBuilder
    metadata: InternalElectionDescription
    context: CiphertextElectionContext
    constants: ElectionConstants

    # Step 1 - Key Ceremony
    mediator: KeyCeremonyMediator
    guardians: List[Guardian] = []
    coefficient_validation_sets: List[CoefficientValidationSet] = []

    # Step 2 - Encrypt Votes
    device: EncryptionDevice
    encrypter: EncryptionMediator
    plaintext_ballots: List[PlaintextBallot]
    ciphertext_ballots: List[CiphertextBallot] = []

    # Step 3 - Cast and Spoil
    ballot_store: BallotStore
    ballot_box: BallotBox

    # Step 4 - Decrypt Tally
    ciphertext_tally: CiphertextTally
    plaintext_tally: PlaintextTally
    decrypter: DecryptionMediator

    def test_end_to_end_election(self) -> None:
        """
        Execute the simplified end-to-end test demonstrating each component of the system.
        """
        self.step_0_configure_election()
        self.step_1_key_ceremony()
        self.step_2_encrypt_votes()
        self.step_3_cast_and_spoil()
        self.step_4_decrypt_tally()
        self.step_5_publish_and_verify()

    def step_0_configure_election(self) -> None:
        """
        To conduct an election, load an `ElectionDescription` file
        """

        # Load a pre-configured Election Description
        # TODO: replace with complex election
        self.description = ElectionFactory().get_simple_election_from_file()
        print(f"""
            {'-'*40}\n
            # Election Summary:
            # Scope: {self.description.election_scope_id}
            # Geopolitical Units: {len(self.description.geopolitical_units)}
            # Parties: {len(self.description.parties)}
            # Candidates: {len(self.description.candidates)}
            # Contests: {len(self.description.contests)}
            # Ballot Styles: {len(self.description.ballot_styles)}\n
            {'-'*40}\n
            """)
        self._assert_message(
            ElectionDescription.is_valid.__qualname__,
            "Verify that the input election meta-data is well-formed",
            self.description.is_valid(),
        )

        # Create an Election Builder
        self.election_builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS,
                                                self.QUORUM, self.description)
        self._assert_message(
            ElectionBuilder.__qualname__,
            f"Created with number_of_guardians: {self.NUMBER_OF_GUARDIANS} quorum: {self.QUORUM}",
        )

        # Move on to the Key Ceremony

    def step_1_key_ceremony(self) -> None:
        """
        Using the NUMBER_OF_GUARDIANS, generate public-private keypairs and share
        representations of those keys with QUORUM of other Guardians.  Then, combine
        the public election keys to make a joint election key that is used to encrypt ballots
        """

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

        # Setup Mediator
        self.mediator = KeyCeremonyMediator(self.guardians[0].ceremony_details)

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

        self._assert_message(
            KeyCeremonyMediator.all_guardians_in_attendance.__qualname__,
            "Confirms all guardians have shared their public keys",
            self.mediator.all_guardians_in_attendance(),
        )

        # Run the Key Ceremony process,
        # Which shares the keys among the guardians
        orchestrated = self.mediator.orchestrate()
        self._assert_message(
            KeyCeremonyMediator.orchestrate.__qualname__,
            "Executes the key exchange between guardians",
            orchestrated is not None,
        )

        self._assert_message(
            KeyCeremonyMediator.all_election_partial_key_backups_available.
            __qualname__,
            "Confirm sall guardians have shared their partial key backups",
            self.mediator.all_election_partial_key_backups_available(),
        )

        # Verification
        verified = self.mediator.verify()
        self._assert_message(
            KeyCeremonyMediator.verify.__qualname__,
            "Confirms all guardians truthfully executed the ceremony",
            verified,
        )

        self._assert_message(
            KeyCeremonyMediator.
            all_election_partial_key_verifications_received.__qualname__,
            "Confirms all guardians have submitted a verification of the backups of all other guardians",
            self.mediator.all_election_partial_key_verifications_received(),
        )

        self._assert_message(
            KeyCeremonyMediator.all_election_partial_key_backups_verified.
            __qualname__,
            "Confirms all guardians have verified the backups of all other guardians",
            self.mediator.all_election_partial_key_backups_verified(),
        )

        # Joint Key
        joint_key = self.mediator.publish_joint_key()
        self._assert_message(
            KeyCeremonyMediator.publish_joint_key.__qualname__,
            "Publishes the Joint Election Key",
            joint_key is not None,
        )

        # Save Validation Keys
        for guardian in self.guardians:
            self.coefficient_validation_sets.append(
                guardian.share_coefficient_validation_set())

        # Build the Election
        self.election_builder.set_public_key(get_optional(joint_key))
        self.metadata, self.context = get_optional(
            self.election_builder.build())
        self.constants = ElectionConstants()

        # Move on to encrypting ballots

    def step_2_encrypt_votes(self) -> None:
        """
        Using the `CiphertextElectionContext` encrypt ballots for the election
        """

        # Configure the Encryption Device
        self.device = EncryptionDevice("polling-place-one")
        self.encrypter = EncryptionMediator(self.metadata, self.context,
                                            self.device)
        self._assert_message(
            EncryptionDevice.__qualname__,
            f"Ready to encrypt at location: {self.device.location}",
        )

        # Load some Ballots
        self.plaintext_ballots = BallotFactory().get_simple_ballots_from_file()
        self._assert_message(
            PlaintextBallot.__qualname__,
            f"Loaded ballots: {len(self.plaintext_ballots)}",
            len(self.plaintext_ballots) > 0,
        )

        # Encrypt the Ballots
        for plaintext_ballot in self.plaintext_ballots:
            encrypted_ballot = self.encrypter.encrypt(plaintext_ballot)
            self._assert_message(
                EncryptionMediator.encrypt.__qualname__,
                f"Ballot Id: {plaintext_ballot.object_id}",
                encrypted_ballot is not None,
            )
            self.ciphertext_ballots.append(get_optional(encrypted_ballot))

        # Next, we cast or spoil the ballots

    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,
            )

    def step_4_decrypt_tally(self) -> None:
        """
        Homomorphically combine the selections made on all of the cast ballots
        and use the Available Guardians to decrypt the combined tally.
        In this way, no individual voter's cast ballot is ever decrypted drectly.
        """

        # Generate a Homomorphically Accumulated Tally of the ballots
        self.ciphertext_tally = get_optional(
            tally_ballots(self.ballot_store, self.metadata, self.context))
        self._assert_message(
            tally_ballots.__qualname__,
            f"""
            - cast: {self.ciphertext_tally.count()} 
            - spoiled: {len(self.ciphertext_tally.spoiled_ballots)}
            Total: {len(self.ciphertext_tally)}
            """,
            self.ciphertext_tally is not None,
        )

        # Configure the Decryption
        self.decrypter = DecryptionMediator(self.metadata, self.context,
                                            self.ciphertext_tally)

        # Announce each guardian as present
        for guardian in self.guardians:
            decryption_share = self.decrypter.announce(guardian)
            self._assert_message(
                DecryptionMediator.announce.__qualname__,
                f"Guardian Present: {guardian.object_id}",
                decryption_share is not None,
            )

        # Get the Plain Text Tally
        self.plaintext_tally = get_optional(
            self.decrypter.get_plaintext_tally())
        self._assert_message(
            DecryptionMediator.get_plaintext_tally.__qualname__,
            "Tally Decrypted",
            self.plaintext_tally is not None,
        )

        # Now, compare the results
        self.compare_results()

    def compare_results(self) -> None:
        """
        Compare the results to ensure the decryption was done correctly
        """
        print(f"""
            {'-'*40}
            # Election Results:

            """)

        # Create a representation of each contest's tally
        selection_ids = [
            selection.object_id for contest in self.metadata.contests
            for selection in contest.ballot_selections
        ]
        expected_plaintext_tally: Dict[str, int] = {
            key: 0
            for key in selection_ids
        }

        # Tally the expected values from the loaded ballots
        for ballot in self.plaintext_ballots:
            if (get_optional(self.ballot_store.get(
                    ballot.object_id)).state == BallotBoxState.CAST):
                for contest in ballot.contests:
                    for selection in contest.ballot_selections:
                        expected_plaintext_tally[
                            selection.object_id] += selection.to_int()

        # Compare the expected tally to the decrypted tally
        for tally_contest in self.plaintext_tally.contests.values():
            print(f" Contest: {tally_contest.object_id}")
            for tally_selection in tally_contest.selections.values():
                expected = expected_plaintext_tally[tally_selection.object_id]
                self._assert_message(
                    f"   - Selection: {tally_selection.object_id}",
                    f"expected: {expected}, actual: {tally_selection.tally}",
                    expected == tally_selection.tally,
                )
        print(f"\n{'-'*40}\n")

        # Compare the expected values for each spoiled ballot
        for ballot_id, accepted_ballot in self.ciphertext_tally.spoiled_ballots.items(
        ):
            if accepted_ballot.state == BallotBoxState.SPOILED:
                for plaintext_ballot in self.plaintext_ballots:
                    if ballot_id == plaintext_ballot.object_id:
                        print(f"\nSpoiled Ballot: {ballot_id}")
                        for contest in plaintext_ballot.contests:
                            print(f"\n Contest: {contest.object_id}")
                            for selection in contest.ballot_selections:
                                expected = selection.to_int()
                                decrypted_selection = (
                                    self.plaintext_tally.spoiled_ballots[
                                        ballot_id][contest.object_id].
                                    selections[selection.object_id])
                                self._assert_message(
                                    f"   - Selection: {selection.object_id}",
                                    f"expected: {expected}, actual: {decrypted_selection.tally}",
                                    expected == decrypted_selection.tally,
                                )

    def step_5_publish_and_verify(self) -> None:
        """Publish and verify steps of the election"""
        self.publish_results()
        self.verify_results()

        if self.REMOVE_OUTPUT:
            rmtree(RESULTS_DIR)

    def publish_results(self) -> None:
        """
        Publish results/artifacts of the election
        """
        publish(
            self.description,
            self.context,
            self.constants,
            [self.device],
            self.ballot_store.all(),
            self.ciphertext_tally.spoiled_ballots.values(),
            publish_ciphertext_tally(self.ciphertext_tally),
            self.plaintext_tally,
            self.coefficient_validation_sets,
        )
        self._assert_message(
            "Publish",
            f"Artifacts published to: {RESULTS_DIR}",
            path.exists(RESULTS_DIR),
        )

    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 _assert_message(self,
                        name: str,
                        message: str,
                        condition: Union[Callable, bool] = True) -> None:
        if callable(condition):
            result = condition()
        else:
            result = condition

        print(f"{name}: {message}: {result}")
        self.assertTrue(result)
class TestDecryptionMediator(TestCase):
    NUMBER_OF_GUARDIANS = 3
    QUORUM = 2
    CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM)

    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)

    def tearDown(self):
        self.key_ceremony.reset(
            CeremonyDetails(self.NUMBER_OF_GUARDIANS, self.QUORUM))

    def test_announce(self):
        # Arrange
        subject = DecryptionMediator(self.metadata, self.context,
                                     self.ciphertext_tally)

        # act
        result = subject.announce(self.guardians[0])

        # assert
        self.assertIsNotNone(result)

        # Can only announce once
        self.assertIsNotNone(subject.announce(self.guardians[0]))

        # Cannot submit another share internally
        self.assertFalse(
            subject._submit_decryption_share(
                TallyDecryptionShare(self.guardians[0].object_id, ZERO_MOD_P,
                                     {}, {})))

        # Cannot get plaintext tally without a quorum
        self.assertIsNone(subject.get_plaintext_tally())

    def test_compute_selection(self):
        # Arrange
        first_selection = [
            selection for contest in self.ciphertext_tally.cast.values()
            for selection in contest.tally_selections.values()
        ][0]

        # act
        result = compute_decryption_share_for_selection(
            self.guardians[0], first_selection, self.context)

        # assert
        self.assertIsNotNone(result)

    def test_compute_compensated_selection_failure(self):
        # Arrange
        first_selection = [
            selection for contest in self.ciphertext_tally.cast.values()
            for selection in contest.tally_selections.values()
        ][0]

        # Act
        self.guardians[0]._guardian_election_partial_key_backups.pop(
            self.guardians[2].object_id)

        self.assertIsNone(self.guardians[0].recovery_public_key_for(
            self.guardians[2].object_id))

        result = compute_compensated_decryption_share_for_selection(
            self.guardians[0],
            self.guardians[2].object_id,
            first_selection,
            self.context,
            identity_auxiliary_decrypt,
        )

        # Assert
        self.assertIsNone(result)

    def test_compute_compensated_selection(self):
        """
        demonstrates the complete workflow for computing a comepnsated decryption share
        For one selection. It is useful for verifying that the workflow is correct
        """
        # Arrange
        first_selection = [
            selection for contest in self.ciphertext_tally.cast.values()
            for selection in contest.tally_selections.values()
        ][0]

        # Compute lagrange coefficients for the guardians that are present
        lagrange_0 = compute_lagrange_coefficient(
            self.guardians[0].sequence_order,
            *[self.guardians[1].sequence_order],
        )
        lagrange_1 = compute_lagrange_coefficient(
            self.guardians[1].sequence_order,
            *[self.guardians[0].sequence_order],
        )

        print(
            f"lagrange: sequence_orders: ({self.guardians[0].sequence_order}, {self.guardians[1].sequence_order}, {self.guardians[2].sequence_order})\n"
        )

        print(lagrange_0)
        print(lagrange_1)

        # compute their shares
        share_0 = compute_decryption_share_for_selection(
            self.guardians[0], first_selection, self.context)

        share_1 = compute_decryption_share_for_selection(
            self.guardians[1], first_selection, self.context)

        self.assertIsNotNone(share_0)
        self.assertIsNotNone(share_1)

        # compute compensations shares for the missing guardian
        compensation_0 = compute_compensated_decryption_share_for_selection(
            self.guardians[0],
            self.guardians[2].object_id,
            first_selection,
            self.context,
            identity_auxiliary_decrypt,
        )

        compensation_1 = compute_compensated_decryption_share_for_selection(
            self.guardians[1],
            self.guardians[2].object_id,
            first_selection,
            self.context,
            identity_auxiliary_decrypt,
        )

        self.assertIsNotNone(compensation_0)
        self.assertIsNotNone(compensation_1)

        print("\nSHARES:")
        print(compensation_0)
        print(compensation_1)

        # Check the share proofs
        self.assertTrue(
            compensation_0.proof.is_valid(
                first_selection.message,
                get_optional(self.guardians[0].recovery_public_key_for(
                    self.guardians[2].object_id)),
                compensation_0.share,
                self.context.crypto_extended_base_hash,
            ))

        self.assertTrue(
            compensation_1.proof.is_valid(
                first_selection.message,
                get_optional(self.guardians[1].recovery_public_key_for(
                    self.guardians[2].object_id)),
                compensation_1.share,
                self.context.crypto_extended_base_hash,
            ))

        share_pow_p = [
            pow_p(compensation_0.share, lagrange_0),
            pow_p(compensation_1.share, lagrange_1),
        ]

        print("\nSHARE_POW_P")
        print(share_pow_p)

        # reconstruct the missing share from the compensation shares
        reconstructed_share = mult_p(*[
            pow_p(compensation_0.share, lagrange_0),
            pow_p(compensation_1.share, lagrange_1),
        ])

        print("\nRECONSTRUCTED SHARE\n")
        print(reconstructed_share)

        share_2 = CiphertextDecryptionSelection(
            first_selection.object_id,
            self.guardians[2].object_id,
            first_selection.description_hash,
            reconstructed_share,
            {
                self.guardians[0].object_id: compensation_0,
                self.guardians[1].object_id: compensation_1,
            },
        )

        # Decrypt the result
        result = decrypt_selection_with_decryption_shares(
            first_selection,
            {
                self.guardians[0].object_id: (
                    self.guardians[0].share_election_public_key().key,
                    share_0,
                ),
                self.guardians[1].object_id: (
                    self.guardians[1].share_election_public_key().key,
                    share_1,
                ),
                self.guardians[2].object_id: (
                    self.guardians[2].share_election_public_key().key,
                    share_2,
                ),
            },
            self.context.crypto_extended_base_hash,
        )

        print(result)

        self.assertIsNotNone(result)
        self.assertEqual(
            result.plaintext,
            self.expected_plaintext_tally[first_selection.object_id])

    def test_decrypt_selection_all_present(self):
        # Arrange

        # find the first selection
        first_contest = [
            contest for contest in self.ciphertext_tally.cast.values()
        ][0]
        first_selection = list(first_contest.tally_selections.values())[0]

        # precompute decryption shares for the guardians
        first_share = compute_decryption_share(self.guardians[0],
                                               self.ciphertext_tally,
                                               self.context)
        second_share = compute_decryption_share(self.guardians[1],
                                                self.ciphertext_tally,
                                                self.context)
        third_share = compute_decryption_share(self.guardians[2],
                                               self.ciphertext_tally,
                                               self.context)

        # build type: Dict[GUARDIAN_ID, Tuple[ELECTION_PUBLIC_KEY, TallyDecryptionShare]]
        shares = {
            self.guardians[0].object_id: (
                self.guardians[0].share_election_public_key().key,
                first_share.contests[first_contest.object_id].selections[
                    first_selection.object_id],
            ),
            self.guardians[1].object_id: (
                self.guardians[1].share_election_public_key().key,
                second_share.contests[first_contest.object_id].selections[
                    first_selection.object_id],
            ),
            self.guardians[2].object_id: (
                self.guardians[2].share_election_public_key().key,
                third_share.contests[first_contest.object_id].selections[
                    first_selection.object_id],
            ),
        }

        # act
        result = decrypt_selection_with_decryption_shares(
            first_selection, shares, self.context.crypto_extended_base_hash)

        # assert
        self.assertIsNotNone(result)
        self.assertEqual(
            self.expected_plaintext_tally[first_selection.object_id],
            result.plaintext)

    def test_decrypt_spoiled_ballots_all_guardians_present(self):
        # Arrange
        # precompute decryption shares for the guardians
        first_share = compute_decryption_share(self.guardians[0],
                                               self.ciphertext_tally,
                                               self.context)
        second_share = compute_decryption_share(self.guardians[1],
                                                self.ciphertext_tally,
                                                self.context)
        third_share = compute_decryption_share(self.guardians[2],
                                               self.ciphertext_tally,
                                               self.context)
        shares = {
            self.guardians[0].object_id: first_share,
            self.guardians[1].object_id: second_share,
            self.guardians[2].object_id: third_share,
        }

        subject = DecryptionMediator(self.metadata, self.context,
                                     self.ciphertext_tally)

        # act
        result = decrypt_spoiled_ballots(
            self.ciphertext_tally.spoiled_ballots,
            shares,
            self.context.crypto_extended_base_hash,
        )

        # assert
        self.assertIsNotNone(result)
        self.assertTrue(self.fake_spoiled_ballot.object_id in result)

        spoiled_ballot = result[self.fake_spoiled_ballot.object_id]
        for contest in self.fake_spoiled_ballot.contests:
            for selection in contest.ballot_selections:
                self.assertEqual(
                    spoiled_ballot[contest.object_id].selections[
                        selection.object_id].plaintext,
                    result[self.fake_spoiled_ballot.object_id][
                        contest.object_id].selections[
                            selection.object_id].plaintext,
                )

    def test_get_plaintext_tally_all_guardians_present_simple(self):
        # Arrange
        subject = DecryptionMediator(self.metadata, self.context,
                                     self.ciphertext_tally)

        # act
        for guardian in self.guardians:
            self.assertIsNotNone(subject.announce(guardian))

        decrypted_tallies = subject.get_plaintext_tally()
        result = self._convert_to_selections(decrypted_tallies)

        # assert
        self.assertIsNotNone(result)
        self.assertEqual(self.expected_plaintext_tally, result)

        # Verify we get the same tally back if we call again
        another_decrypted_tally = subject.get_plaintext_tally()

        self.assertEqual(decrypted_tallies, another_decrypted_tally)

    def test_get_plaintext_tally_compensate_missing_guardian_simple(self):

        # Arrange
        subject = DecryptionMediator(self.metadata, self.context,
                                     self.ciphertext_tally)

        # Act

        self.assertIsNotNone(subject.announce(self.guardians[0]))
        self.assertIsNotNone(subject.announce(self.guardians[1]))

        # explicitly compensate to demonstrate that this is possible, but not required
        self.assertIsNotNone(
            subject.compensate(self.guardians[2].object_id,
                               identity_auxiliary_decrypt))

        decrypted_tallies = subject.get_plaintext_tally()
        self.assertIsNotNone(decrypted_tallies)
        result = self._convert_to_selections(decrypted_tallies)

        # assert
        self.assertIsNotNone(result)
        print(result)
        self.assertEqual(self.expected_plaintext_tally, result)

    @settings(
        deadline=timedelta(milliseconds=15000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=8,
        # disabling the "shrink" phase, because it runs very slowly
        phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target],
    )
    @given(data(), integers(1, 3), integers(2, 5))
    def test_get_plaintext_tally_all_guardians_present(self, data,
                                                       parties: int,
                                                       contests: int):
        # Arrange
        description = data.draw(election_descriptions(parties, contests))
        builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM,
                                  description)
        metadata, context = builder.set_public_key(
            self.joint_public_key).build()

        plaintext_ballots: List[PlaintextBallot] = data.draw(
            plaintext_voted_ballots(metadata, randrange(3, 6)))
        plaintext_tallies = accumulate_plaintext_ballots(plaintext_ballots)

        encrypted_tally = self._generate_encrypted_tally(
            metadata, context, plaintext_ballots)

        subject = DecryptionMediator(metadata, context, encrypted_tally)

        # act
        for guardian in self.guardians:
            self.assertIsNotNone(subject.announce(guardian))

        decrypted_tallies = subject.get_plaintext_tally()
        result = self._convert_to_selections(decrypted_tallies)

        # assert
        self.assertIsNotNone(result)
        self.assertEqual(plaintext_tallies, result)

    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 _convert_to_selections(self, tally: PlaintextTally) -> Dict[str, int]:
        plaintext_selections: Dict[str, int] = {}
        for _, contest in tally.contests.items():
            for selection_id, selection in contest.selections.items():
                plaintext_selections[selection_id] = selection.plaintext

        return plaintext_selections
class TestDecryptWithShares(TestCase):
    """Test decrypt with shares methods"""

    NUMBER_OF_GUARDIANS = 3
    QUORUM = 2
    CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM)

    def setUp(self):

        # Key Ceremony
        self.key_ceremony_mediator = KeyCeremonyMediator(
            "key_ceremony_mediator_mediator", self.CEREMONY_DETAILS)
        self.guardians: List[Guardian] = KeyCeremonyHelper.create_guardians(
            self.CEREMONY_DETAILS)
        KeyCeremonyHelper.perform_full_ceremony(self.guardians,
                                                self.key_ceremony_mediator)
        self.joint_public_key = self.key_ceremony_mediator.publish_joint_key()

        # Setup the election
        manifest = election_factory.get_fake_manifest()
        builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM,
                                  manifest)
        builder.set_public_key(self.joint_public_key.joint_public_key)
        builder.set_commitment_hash(self.joint_public_key.commitment_hash)
        self.internal_manifest, self.context = get_optional(builder.build())

        self.encryption_device = election_factory.get_encryption_device()
        self.ballot_marking_device = EncryptionMediator(
            self.internal_manifest, self.context, self.encryption_device)

        # get some fake ballots
        self.fake_cast_ballot = ballot_factory.get_fake_ballot(
            self.internal_manifest, "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.internal_manifest, f"some-unique-ballot-id-cast{i}"))
        self.fake_spoiled_ballot = ballot_factory.get_fake_ballot(
            self.internal_manifest, "some-unique-ballot-id-spoiled")
        self.more_fake_spoiled_ballots = []
        for i in range(2):
            self.more_fake_spoiled_ballots.append(
                ballot_factory.get_fake_ballot(
                    self.internal_manifest,
                    f"some-unique-ballot-id-spoiled{i}"))
        self.assertTrue(
            self.fake_cast_ballot.is_valid(
                self.internal_manifest.ballot_styles[0].object_id))
        self.assertTrue(
            self.fake_spoiled_ballot.is_valid(
                self.internal_manifest.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 = {
            selection.object_id
            for contest in self.internal_manifest.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
        self.encrypted_fake_cast_ballot = self.ballot_marking_device.encrypt(
            self.fake_cast_ballot)
        self.encrypted_fake_spoiled_ballot = self.ballot_marking_device.encrypt(
            self.fake_spoiled_ballot)

        # 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))
        # encrypt some more fake ballots
        self.more_fake_encrypted_spoiled_ballots = []
        for fake_ballot in self.more_fake_spoiled_ballots:
            self.more_fake_encrypted_spoiled_ballots.append(
                self.ballot_marking_device.encrypt(fake_ballot))

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

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

        # generate encrypted tally
        self.ciphertext_tally = tally_ballots(ballot_store,
                                              self.internal_manifest,
                                              self.context)
        self.ciphertext_ballots = get_ballots(ballot_store,
                                              BallotBoxState.SPOILED)

    def tearDown(self):
        self.key_ceremony_mediator.reset(
            CeremonyDetails(self.NUMBER_OF_GUARDIANS, self.QUORUM))

    def test_decrypt_selection_with_all_guardians_present(self):
        # Arrange
        available_guardians = self.guardians

        # find the first selection
        first_contest = list(self.ciphertext_tally.contests.values())[0]
        first_selection = list(first_contest.selections.values())[0]

        print(first_contest.object_id)
        print(first_selection.object_id)

        # precompute decryption shares for specific selection for the guardians
        shares: Dict[GUARDIAN_ID,
                     Tuple[ELECTION_PUBLIC_KEY, DecryptionShare]] = {
                         guardian.id: (
                             guardian.share_election_public_key().key,
                             compute_decryption_share(
                                 guardian._election_keys,
                                 self.ciphertext_tally,
                                 self.context,
                             ).contests[first_contest.object_id].selections[
                                 first_selection.object_id],
                         )
                         for guardian in available_guardians
                     }

        # Act
        result = decrypt_selection_with_decryption_shares(
            first_selection, shares, self.context.crypto_extended_base_hash)

        # Assert
        self.assertIsNotNone(result)
        self.assertEqual(
            self.expected_plaintext_tally[first_selection.object_id],
            result.tally)

    def test_decrypt_ballot_with_all_guardians_present(self):
        # Arrange
        # precompute decryption shares for the guardians
        available_guardians = self.guardians
        plaintext_ballot = self.fake_cast_ballot
        encrypted_ballot = self.encrypted_fake_cast_ballot
        shares = {
            available_guardian.id: compute_decryption_share_for_ballot(
                available_guardian._election_keys,
                encrypted_ballot,
                self.context,
            )
            for available_guardian in available_guardians
        }

        # act
        result = decrypt_ballot(
            encrypted_ballot,
            shares,
            self.context.crypto_extended_base_hash,
        )

        # assert
        self.assertIsNotNone(result)

        for contest in plaintext_ballot.contests:
            for selection in contest.ballot_selections:
                expected_tally = selection.vote
                actual_tally = (result.contests[contest.object_id].selections[
                    selection.object_id].tally)
                self.assertEqual(expected_tally, actual_tally)

    def test_decrypt_ballot_with_missing_guardians(self):
        # Arrange
        # precompute decryption shares for the guardians
        plaintext_ballot = self.fake_cast_ballot
        encrypted_ballot = self.encrypted_fake_cast_ballot
        available_guardians = self.guardians[0:2]
        missing_guardian = self.guardians[2]

        available_shares = {
            available_guardian.id: compute_decryption_share_for_ballot(
                available_guardian._election_keys,
                encrypted_ballot,
                self.context,
            )
            for available_guardian in available_guardians
        }

        compensated_shares = {
            available_guardian.id:
            compute_compensated_decryption_share_for_ballot(
                available_guardian.share_election_public_key(),
                available_guardian._auxiliary_keys,
                missing_guardian.share_election_public_key(),
                get_optional(
                    available_guardian._guardian_election_partial_key_backups.
                    get(missing_guardian.id)),
                encrypted_ballot,
                self.context,
                identity_auxiliary_decrypt,
            )
            for available_guardian in available_guardians
        }

        lagrange_coefficients = compute_lagrange_coefficients_for_guardians([
            guardian.share_election_public_key()
            for guardian in available_guardians
        ])

        reconstructed_share = reconstruct_decryption_share_for_ballot(
            missing_guardian.share_election_public_key(),
            encrypted_ballot,
            compensated_shares,
            lagrange_coefficients,
        )

        all_shares = {
            **available_shares, missing_guardian.id: reconstructed_share
        }

        # act
        result = decrypt_ballot(
            encrypted_ballot,
            all_shares,
            self.context.crypto_extended_base_hash,
        )

        # assert
        self.assertIsNotNone(result)

        for contest in plaintext_ballot.contests:
            for selection in contest.ballot_selections:
                expected_tally = selection.vote
                actual_tally = (result.contests[contest.object_id].selections[
                    selection.object_id].tally)
                self.assertEqual(expected_tally, actual_tally)
Example #23
0
    def test_partial_key_backup_verification_failure(self):
        """
        In this case, the recipient guardian does not correctly verify the sent key backup.
        This failed verificaton requires the sender create a challenge and a new verifier
        aka another guardian must verify this challenge.
        """
        # Arrange
        mediator = KeyCeremonyMediator("mediator_challenge", CEREMONY_DETAILS)
        KeyCeremonyHelper.perform_round_1(GUARDIANS, mediator)
        KeyCeremonyHelper.perform_round_2(GUARDIANS, mediator)

        # Round 3 - Guardians only
        verification1 = GUARDIAN_1.verify_election_partial_key_backup(
            GUARDIAN_2_ID, identity_auxiliary_decrypt)

        # Act
        failed_verification2 = ElectionPartialKeyVerification(
            GUARDIAN_1_ID,
            GUARDIAN_2_ID,
            GUARDIAN_2_ID,
            False,
        )
        mediator.receive_backup_verifications(
            [verification1, failed_verification2])

        state = mediator.get_verification_state()

        # Assert
        self.assertTrue(state.all_sent)
        self.assertFalse(state.all_verified)
        self.assertIsNone(mediator.publish_joint_key())
        self.assertEqual(len(state.failed_verifications), 1)
        self.assertEqual(state.failed_verifications[0],
                         GuardianPair(GUARDIAN_1_ID, GUARDIAN_2_ID))

        # Act
        challenge = GUARDIAN_1.publish_election_backup_challenge(GUARDIAN_2_ID)
        mediator.verify_challenge(challenge)
        new_state = mediator.get_verification_state()
        all_verified = mediator.all_backups_verified()
        joint_key = mediator.publish_joint_key()

        # Assert
        self.assertTrue(new_state.all_sent)
        self.assertTrue(new_state.all_verified)
        self.assertEqual(len(new_state.failed_verifications), 0)
        self.assertTrue(all_verified)
        self.assertIsNotNone(joint_key)
Example #24
0
    def test_partial_key_backup_verification_success(self):
        """
        Test for the happy path of the verification process where each key is successfully verified and no bad actors.
        """
        # Arrange
        mediator = KeyCeremonyMediator("mediator_verification",
                                       CEREMONY_DETAILS)
        KeyCeremonyHelper.perform_round_1(GUARDIANS, mediator)
        KeyCeremonyHelper.perform_round_2(GUARDIANS, mediator)

        # Round 3 - Guardians only
        verification1 = GUARDIAN_1.verify_election_partial_key_backup(
            GUARDIAN_2_ID, identity_auxiliary_decrypt)
        verification2 = GUARDIAN_2.verify_election_partial_key_backup(
            GUARDIAN_1_ID, identity_auxiliary_decrypt)

        # Act
        mediator.receive_backup_verifications([verification1])

        # Assert
        self.assertFalse(mediator.get_verification_state().all_sent)
        self.assertFalse(mediator.all_backups_verified())
        self.assertIsNone(mediator.publish_joint_key())

        # Act
        mediator.receive_backup_verifications([verification2])
        joint_key = mediator.publish_joint_key()

        # Assert
        self.assertTrue(mediator.get_verification_state().all_sent)
        self.assertTrue(mediator.all_backups_verified())
        self.assertIsNotNone(joint_key)
    def setUp(self):

        # Key Ceremony
        self.key_ceremony_mediator = KeyCeremonyMediator(
            "key_ceremony_mediator_mediator", self.CEREMONY_DETAILS)
        self.guardians: List[Guardian] = KeyCeremonyHelper.create_guardians(
            self.CEREMONY_DETAILS)
        KeyCeremonyHelper.perform_full_ceremony(self.guardians,
                                                self.key_ceremony_mediator)
        self.joint_public_key = self.key_ceremony_mediator.publish_joint_key()

        # Setup the election
        manifest = election_factory.get_fake_manifest()
        builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM,
                                  manifest)
        builder.set_public_key(self.joint_public_key.joint_public_key)
        builder.set_commitment_hash(self.joint_public_key.commitment_hash)
        self.internal_manifest, self.context = get_optional(builder.build())

        self.encryption_device = election_factory.get_encryption_device()
        self.ballot_marking_device = EncryptionMediator(
            self.internal_manifest, self.context, self.encryption_device)

        # get some fake ballots
        self.fake_cast_ballot = ballot_factory.get_fake_ballot(
            self.internal_manifest, "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.internal_manifest, f"some-unique-ballot-id-cast{i}"))
        self.fake_spoiled_ballot = ballot_factory.get_fake_ballot(
            self.internal_manifest, "some-unique-ballot-id-spoiled")
        self.more_fake_spoiled_ballots = []
        for i in range(2):
            self.more_fake_spoiled_ballots.append(
                ballot_factory.get_fake_ballot(
                    self.internal_manifest,
                    f"some-unique-ballot-id-spoiled{i}"))
        self.assertTrue(
            self.fake_cast_ballot.is_valid(
                self.internal_manifest.ballot_styles[0].object_id))
        self.assertTrue(
            self.fake_spoiled_ballot.is_valid(
                self.internal_manifest.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 = {
            selection.object_id
            for contest in self.internal_manifest.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
        self.encrypted_fake_cast_ballot = self.ballot_marking_device.encrypt(
            self.fake_cast_ballot)
        self.encrypted_fake_spoiled_ballot = self.ballot_marking_device.encrypt(
            self.fake_spoiled_ballot)

        # 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))
        # encrypt some more fake ballots
        self.more_fake_encrypted_spoiled_ballots = []
        for fake_ballot in self.more_fake_spoiled_ballots:
            self.more_fake_encrypted_spoiled_ballots.append(
                self.ballot_marking_device.encrypt(fake_ballot))

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

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

        # generate encrypted tally
        self.ciphertext_tally = tally_ballots(ballot_store,
                                              self.internal_manifest,
                                              self.context)
        self.ciphertext_ballots = get_ballots(ballot_store,
                                              BallotBoxState.SPOILED)
class TestDecryptionMediator(TestCase):
    NUMBER_OF_GUARDIANS = 3
    QUORUM = 2
    CEREMONY_DETAILS = CeremonyDetails(NUMBER_OF_GUARDIANS, QUORUM)

    def setUp(self):

        self.key_ceremony = KeyCeremonyMediator(self.CEREMONY_DETAILS)

        self.guardians: List[Guardian] = []

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

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

        self.key_ceremony.orchestrate()
        self.key_ceremony.verify()

        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.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]
        )

        # Fill in 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
            )
        )

        # 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)

        # generate encrypted tally
        self.ciphertext_tally = tally_ballots(ballot_store, self.metadata, self.context)

    def test_announce(self):
        # Arrange
        subject = DecryptionMediator(self.metadata, self.context, self.ciphertext_tally)

        # act
        result = subject.announce(self.guardians[0])

        # assert
        self.assertIsNotNone(result)

        # Can only announce once
        self.assertIsNone(subject.announce(self.guardians[0]))

        # Cannot submit another share internally
        self.assertFalse(
            subject._submit_decryption_share(
                DecryptionShare(self.guardians[0].object_id, {}, {})
            )
        )

        # Cannot get plaintext tally without a quorum
        self.assertIsNone(subject.get_plaintext_tally())

    def test_compute_selection(self):
        # Arrange
        first_selection = [
            selection
            for contest in self.ciphertext_tally.cast.values()
            for selection in contest.tally_selections.values()
        ][0]

        # act
        result = _compute_decryption_for_selection(
            self.guardians[0], first_selection, self.context
        )

        # assert
        self.assertIsNotNone(result)

    def test_decrypt_selection(self):
        # Arrange

        # find the first selection
        first_contest = [contest for contest in self.ciphertext_tally.cast.values()][0]
        first_selection = list(first_contest.tally_selections.values())[0]

        # precompute decryption shares for the guardians
        first_share = compute_decryption_share(
            self.guardians[0], self.ciphertext_tally, self.context
        )
        second_share = compute_decryption_share(
            self.guardians[1], self.ciphertext_tally, self.context
        )
        third_share = compute_decryption_share(
            self.guardians[2], self.ciphertext_tally, self.context
        )
        shares = {
            self.guardians[0]
            .object_id: first_share.contests[first_contest.object_id]
            .selections[first_selection.object_id]
            .share,
            self.guardians[1]
            .object_id: second_share.contests[first_contest.object_id]
            .selections[first_selection.object_id]
            .share,
            self.guardians[2]
            .object_id: third_share.contests[first_contest.object_id]
            .selections[first_selection.object_id]
            .share,
        }

        # act
        result = decrypt_selection_with_decryption_shares(first_selection, shares)

        # assert
        self.assertIsNotNone(result)
        self.assertEqual(
            self.expected_plaintext_tally[first_selection.object_id], result.plaintext
        )

    def test_decrypt_spoiled_ballots(self):
        # Arrange
        # precompute decryption shares for the guardians
        first_share = compute_decryption_share(
            self.guardians[0], self.ciphertext_tally, self.context
        )
        second_share = compute_decryption_share(
            self.guardians[1], self.ciphertext_tally, self.context
        )
        third_share = compute_decryption_share(
            self.guardians[2], self.ciphertext_tally, self.context
        )
        shares = {
            self.guardians[0].object_id: first_share,
            self.guardians[1].object_id: second_share,
            self.guardians[2].object_id: third_share,
        }

        subject = DecryptionMediator(self.metadata, self.context, self.ciphertext_tally)

        # act
        result = subject._decrypt_spoiled_ballots(
            self.ciphertext_tally.spoiled_ballots, shares
        )

        # assert
        self.assertIsNotNone(result)
        self.assertTrue(self.fake_spoiled_ballot.object_id in result)

        spoiled_ballot = result[self.fake_spoiled_ballot.object_id]
        for contest in self.fake_spoiled_ballot.contests:
            for selection in contest.ballot_selections:
                self.assertEqual(
                    spoiled_ballot[contest.object_id]
                    .selections[selection.object_id]
                    .plaintext,
                    result[self.fake_spoiled_ballot.object_id][contest.object_id]
                    .selections[selection.object_id]
                    .plaintext,
                )

    def test_get_plaintext_tally_all_guardians_present_simple(self):
        # Arrange
        subject = DecryptionMediator(self.metadata, self.context, self.ciphertext_tally)

        # act
        for guardian in self.guardians:
            self.assertIsNotNone(subject.announce(guardian))

        decrypted_tallies = subject.get_plaintext_tally()
        result = self._convert_to_selections(decrypted_tallies)

        # assert
        self.assertIsNotNone(result)
        self.assertEqual(self.expected_plaintext_tally, result)

    @settings(
        deadline=timedelta(milliseconds=10000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=1,
        # disabling the "shrink" phase, because it runs very slowly
        phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target],
    )
    @given(data(), integers(1, 3), integers(2, 5))
    def test_get_plaintext_tally_all_guardians_present(
        self, data, parties: int, contests: int
    ):
        # Arrange
        description = data.draw(election_descriptions(parties, contests))
        builder = ElectionBuilder(self.NUMBER_OF_GUARDIANS, self.QUORUM, description)
        metadata, context = builder.set_public_key(self.joint_public_key).build()

        plaintext_ballots: List[PlaintextBallot] = data.draw(
            plaintext_voted_ballots(metadata, randrange(3, 6))
        )
        plaintext_tallies = accumulate_plaintext_ballots(plaintext_ballots)

        encrypted_tally = self._generate_encrypted_tally(
            metadata, context, plaintext_ballots
        )

        subject = DecryptionMediator(metadata, context, encrypted_tally)

        # act
        for guardian in self.guardians:
            self.assertIsNotNone(subject.announce(guardian))

        decrypted_tallies = subject.get_plaintext_tally()
        result = self._convert_to_selections(decrypted_tallies)

        # assert
        self.assertIsNotNone(result)
        self.assertEqual(plaintext_tallies, result)

    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 _convert_to_selections(self, tally: PlaintextTally) -> Dict[str, int]:
        plaintext_selections: Dict[str, int] = {}
        for _, contest in tally.contests.items():
            for selection_id, selection in contest.selections.items():
                plaintext_selections[selection_id] = selection.plaintext

        return plaintext_selections
    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)
Example #28
0
    def step_1_key_ceremony(self) -> None:
        """
        Using the NUMBER_OF_GUARDIANS, generate public-private keypairs and share
        representations of those keys with QUORUM of other Guardians.  Then, combine
        the public election keys to make a joint election key that is used to encrypt ballots
        """

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

        # Setup Mediator
        self.mediator = KeyCeremonyMediator(self.guardians[0].ceremony_details)

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

        self._assert_message(
            KeyCeremonyMediator.all_guardians_in_attendance.__qualname__,
            "Confirms all guardians have shared their public keys",
            self.mediator.all_guardians_in_attendance(),
        )

        # Run the Key Ceremony process,
        # Which shares the keys among the guardians
        orchestrated = self.mediator.orchestrate()
        self._assert_message(
            KeyCeremonyMediator.orchestrate.__qualname__,
            "Executes the key exchange between guardians",
            orchestrated is not None,
        )

        self._assert_message(
            KeyCeremonyMediator.all_election_partial_key_backups_available.
            __qualname__,
            "Confirm sall guardians have shared their partial key backups",
            self.mediator.all_election_partial_key_backups_available(),
        )

        # Verification
        verified = self.mediator.verify()
        self._assert_message(
            KeyCeremonyMediator.verify.__qualname__,
            "Confirms all guardians truthfully executed the ceremony",
            verified,
        )

        self._assert_message(
            KeyCeremonyMediator.
            all_election_partial_key_verifications_received.__qualname__,
            "Confirms all guardians have submitted a verification of the backups of all other guardians",
            self.mediator.all_election_partial_key_verifications_received(),
        )

        self._assert_message(
            KeyCeremonyMediator.all_election_partial_key_backups_verified.
            __qualname__,
            "Confirms all guardians have verified the backups of all other guardians",
            self.mediator.all_election_partial_key_backups_verified(),
        )

        # Joint Key
        joint_key = self.mediator.publish_joint_key()
        self._assert_message(
            KeyCeremonyMediator.publish_joint_key.__qualname__,
            "Publishes the Joint Election Key",
            joint_key is not None,
        )

        # Save Validation Keys
        for guardian in self.guardians:
            self.coefficient_validation_sets.append(
                guardian.share_coefficient_validation_set())

        # Build the Election
        self.election_builder.set_public_key(get_optional(joint_key))
        self.metadata, self.context = get_optional(
            self.election_builder.build())
        self.constants = ElectionConstants()