コード例 #1
0
def ciphertext_elections(draw: _DrawType,
                         election_description: ElectionDescription):
    """
    Generates a `CiphertextElectionContext` with a single public-private key pair as the encryption context.

    In a real election, the key ceremony would be used to generate a shared public key.

    :param draw: Hidden argument, used by Hypothesis.
    :param election_description: An `ElectionDescription` object, with
    which the `CiphertextElectionContext` will be associated
    :return: a tuple of a `CiphertextElectionContext` and the secret key associated with it
    """
    secret_key, public_key = draw(elgamal_keypairs())
    commitment_hash = draw(elements_mod_q_no_zero())
    ciphertext_election_with_secret: CIPHERTEXT_ELECTIONS_TUPLE_TYPE = (
        secret_key,
        make_ciphertext_election_context(
            number_of_guardians=1,
            quorum=1,
            elgamal_public_key=public_key,
            commitment_hash=commitment_hash,
            description_hash=election_description.crypto_hash(),
        ),
    )
    return ciphertext_election_with_secret
コード例 #2
0
class TestChaumPedersen(TestCase):
    def test_cp_proofs_simple(self):
        keypair = elgamal_keypair_from_secret(TWO_MOD_Q)
        nonce = ONE_MOD_Q
        seed = TWO_MOD_Q
        message = get_optional(elgamal_encrypt(0, nonce, keypair.public_key))
        decryption = message.partial_decrypt(keypair.secret_key)
        proof = make_chaum_pedersen(
            message, keypair.secret_key, decryption, seed, ONE_MOD_Q
        )
        bad_proof = make_chaum_pedersen(
            message, keypair.secret_key, TWO_MOD_Q, seed, ONE_MOD_Q
        )
        self.assertTrue(
            proof.is_valid(message, keypair.public_key, decryption, ONE_MOD_Q)
        )
        self.assertFalse(
            bad_proof.is_valid(message, keypair.public_key, decryption, ONE_MOD_Q)
        )

    @settings(
        deadline=timedelta(milliseconds=2000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(
        elgamal_keypairs(),
        elements_mod_q_no_zero(),
        elements_mod_q(),
        integers(0, 100),
        integers(0, 100),
    )
    def test_ccp_proof(
        self,
        keypair: ElGamalKeyPair,
        nonce: ElementModQ,
        seed: ElementModQ,
        constant: int,
        bad_constant: int,
    ):
        if constant == bad_constant:
            bad_constant = constant + 1

        message = get_optional(elgamal_encrypt(constant, nonce, keypair.public_key))
        decryption = message.partial_decrypt(keypair.secret_key)
        proof = make_chaum_pedersen(
            message, keypair.secret_key, decryption, seed, ONE_MOD_Q
        )
        bad_proof = make_chaum_pedersen(
            message, keypair.secret_key, int_to_p(bad_constant), seed, ONE_MOD_Q
        )
        self.assertTrue(
            proof.is_valid(message, keypair.public_key, decryption, ONE_MOD_Q)
        )
        self.assertFalse(
            bad_proof.is_valid(message, keypair.public_key, decryption, ONE_MOD_Q)
        )
コード例 #3
0
ファイル: test_tally.py プロジェクト: nealmcb/arlo-e2e
class TestFastTallies(unittest.TestCase):
    def setUp(self) -> None:
        self.pool = Pool(cpu_count())

    def tearDown(self) -> None:
        self.pool.close()

    @given(dominion_cvrs(max_rows=50), elgamal_keypairs(), booleans())
    @settings(
        deadline=timedelta(milliseconds=50000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=5,
        # disabling the "shrink" phase, because it runs very slowly
        phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target],
    )
    def test_end_to_end(self, input: str, keypair: ElGamalKeyPair,
                        use_keypair: bool) -> None:
        coverage.process_startup(
        )  # necessary for coverage testing to work in parallel

        cvrs = read_dominion_csv(StringIO(input))
        self.assertIsNotNone(cvrs)

        _, ballots, _ = cvrs.to_election_description()
        assert len(ballots) > 0, "can't have zero ballots!"

        if use_keypair:
            tally = fast_tally_everything(cvrs,
                                          self.pool,
                                          verbose=True,
                                          secret_key=keypair.secret_key)
        else:
            tally = fast_tally_everything(cvrs, self.pool, verbose=True)

        self.assertTrue(tally.all_proofs_valid(verbose=True))

        # Now, while we've got a tally and a set of cvrs, we'll test some of the other utility
        # methods that we have. This is going to be much faster than regenerating cvrs and tallies.

        # TODO: tests for get_contest_titles_matching and get_ballot_styles_for_contest_titles

        for ballot_style in cvrs.metadata.style_map.keys():
            ballots_query = tally.get_ballots_matching_ballot_styles(
                [ballot_style])
            ballots_pandas = cvrs.data[cvrs.data.BallotType == ballot_style]

            self.assertEqual(len(ballots_pandas), len(ballots_query))
コード例 #4
0
class ElGamalTest(unittest.TestCase):
    @given(
        integers(0, 20),
        elements_mod_q_no_zero(),
        elements_mod_q_no_zero(),
        elgamal_keypairs(),
    )
    def test_reencryption(
        self,
        plaintext: int,
        nonce1: ElementModQ,
        nonce2: ElementModQ,
        keypair: ElGamalKeyPair,
    ) -> None:
        c1 = get_optional(elgamal_encrypt(plaintext, nonce1, keypair.public_key))
        c2 = get_optional(elgamal_reencrypt(keypair.public_key, nonce2, c1))
        self.assertEqual(plaintext, c1.decrypt(keypair.secret_key))
        self.assertNotEqual(c1, c2)
        self.assertEqual(plaintext, c2.decrypt(keypair.secret_key))
コード例 #5
0
ファイル: test_ray_reduce.py プロジェクト: nealmcb/arlo-e2e
class TestRayReduce(unittest.TestCase):
    def setUp(self) -> None:
        ray_init_localhost()

    @settings(
        deadline=timedelta(milliseconds=50000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(
        lists(integers(min_value=0, max_value=1), min_size=20, max_size=200),
        elgamal_keypairs(),
    )
    def test_reduce_with_ray_wait_no_progress(self, counters: List[int],
                                              keypair: ElGamalKeyPair) -> None:
        nonces = Nonces(int_to_q(3))[0:len(counters)]

        ciphertexts: List[ObjectRef] = [
            r_encrypt.remote(None, p, n, keypair.public_key)
            for p, n in zip(counters, nonces)
        ]

        # compute in parallel
        ptotal = ray.get(
            ray_reduce_with_ray_wait(
                inputs=ciphertexts,
                shard_size=3,
                reducer_first_arg=None,
                reducer=r_elgamal_add.remote,
                progressbar=None,
                progressbar_key="Tallies",
                timeout=None,
                verbose=True,
            ))

        # recompute serially
        stotal = elgamal_add(*ray.get(ciphertexts))

        self.assertEqual(stotal, ptotal)

    @settings(
        deadline=timedelta(milliseconds=50000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(
        lists(integers(min_value=0, max_value=1), min_size=20, max_size=200),
        elgamal_keypairs(),
    )
    def test_reduce_with_ray_wait_with_progress(
            self, counters: List[int], keypair: ElGamalKeyPair) -> None:
        nonces = Nonces(int_to_q(3))[0:len(counters)]
        pbar = ProgressBar({
            "Ballots": len(counters),
            "Tallies": len(counters),
            "Iterations": 0
        })

        ciphertexts: List[ObjectRef] = [
            r_encrypt.remote(pbar.actor, p, n, keypair.public_key)
            for p, n in zip(counters, nonces)
        ]

        # compute in parallel
        ptotal = ray.get(
            ray_reduce_with_ray_wait(
                inputs=ciphertexts,
                shard_size=3,
                reducer_first_arg=pbar.actor,
                reducer=r_elgamal_add.remote,
                progressbar=pbar,
                progressbar_key="Tallies",
                timeout=None,
                verbose=False,
            ))

        # recompute serially
        stotal = elgamal_add(*ray.get(ciphertexts))

        self.assertEqual(stotal, ptotal)

    @settings(
        deadline=timedelta(milliseconds=50000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(
        lists(integers(min_value=0, max_value=1), min_size=20, max_size=200),
        elgamal_keypairs(),
    )
    def test_reduce_with_rounds_without_progress(
            self, counters: List[int], keypair: ElGamalKeyPair) -> None:
        nonces = Nonces(int_to_q(3))[0:len(counters)]

        ciphertexts: List[ObjectRef] = [
            r_encrypt.remote(None, p, n, keypair.public_key)
            for p, n in zip(counters, nonces)
        ]

        # compute in parallel
        ptotal = ray.get(
            ray_reduce_with_rounds(
                inputs=ciphertexts,
                shard_size=3,
                reducer_first_arg=None,
                reducer=r_elgamal_add.remote,
                progressbar=None,
                verbose=True,
            ))

        # recompute serially
        stotal = elgamal_add(*ray.get(ciphertexts))

        self.assertEqual(stotal, ptotal)

    @unittest.skip("doesn't complete here, but works fine elsewhere; weird")
    @settings(
        deadline=timedelta(milliseconds=50000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(
        lists(integers(min_value=0, max_value=1), min_size=20, max_size=200),
        elgamal_keypairs(),
    )
    def test_reduce_with_rounds_with_progress(self, counters: List[int],
                                              keypair: ElGamalKeyPair) -> None:
        nonces = Nonces(int_to_q(3))[0:len(counters)]
        pbar = ProgressBar({
            "Ballots": len(counters),
            "Tallies": len(counters),
            "Iterations": 0
        })

        ciphertexts: List[ObjectRef] = [
            r_encrypt.remote(pbar.actor, p, n, keypair.public_key)
            for p, n in zip(counters, nonces)
        ]

        # compute in parallel
        ptotal = ray.get(
            ray_reduce_with_rounds(
                inputs=ciphertexts,
                shard_size=3,
                reducer_first_arg=pbar.actor,
                reducer=r_elgamal_add.remote,
                progressbar=pbar,
                progressbar_key="Tallies",
                verbose=False,
            ))

        # recompute serially
        stotal = elgamal_add(*ray.get(ciphertexts))

        self.assertEqual(stotal, ptotal)
コード例 #6
0
class TestElGamal(unittest.TestCase):
    def test_simple_elgamal_encryption_decryption(self):
        nonce = ONE_MOD_Q
        secret_key = TWO_MOD_Q
        keypair = get_optional(elgamal_keypair_from_secret(secret_key))
        public_key = keypair.public_key

        self.assertLess(public_key.to_int(), P)
        elem = g_pow_p(ZERO_MOD_Q)
        self.assertEqual(elem, ONE_MOD_P)  # g^0 == 1

        ciphertext = get_optional(elgamal_encrypt(0, nonce,
                                                  keypair.public_key))
        self.assertEqual(G, ciphertext.alpha.to_int())
        self.assertEqual(
            pow(ciphertext.alpha.to_int(), secret_key.to_int(), P),
            pow(public_key.to_int(), nonce.to_int(), P),
        )
        self.assertEqual(
            ciphertext.beta.to_int(),
            pow(public_key.to_int(), nonce.to_int(), P),
        )

        plaintext = ciphertext.decrypt(keypair.secret_key)

        self.assertEqual(0, plaintext)

    @given(integers(0, 100), elgamal_keypairs())
    def test_elgamal_encrypt_requires_nonzero_nonce(self, message: int,
                                                    keypair: ElGamalKeyPair):
        self.assertEqual(
            None, elgamal_encrypt(message, ZERO_MOD_Q, keypair.public_key))

    def test_elgamal_keypair_from_secret_requires_key_greater_than_one(self):
        self.assertEqual(None, elgamal_keypair_from_secret(ZERO_MOD_Q))
        self.assertEqual(None, elgamal_keypair_from_secret(ONE_MOD_Q))

    @given(integers(0, 100), elements_mod_q_no_zero(), elgamal_keypairs())
    def test_elgamal_encryption_decryption_inverses(self, message: int,
                                                    nonce: ElementModQ,
                                                    keypair: ElGamalKeyPair):
        ciphertext = get_optional(
            elgamal_encrypt(message, nonce, keypair.public_key))
        plaintext = ciphertext.decrypt(keypair.secret_key)

        self.assertEqual(message, plaintext)

    @given(integers(0, 100), elements_mod_q_no_zero(), elgamal_keypairs())
    def test_elgamal_encryption_decryption_with_known_nonce_inverses(
            self, message: int, nonce: ElementModQ, keypair: ElGamalKeyPair):
        ciphertext = get_optional(
            elgamal_encrypt(message, nonce, keypair.public_key))
        plaintext = ciphertext.decrypt_known_nonce(keypair.public_key, nonce)

        self.assertEqual(message, plaintext)

    @given(elgamal_keypairs())
    def test_elgamal_generated_keypairs_are_within_range(
            self, keypair: ElGamalKeyPair):
        self.assertLess(keypair.public_key.to_int(), P)
        self.assertLess(keypair.secret_key.to_int(), G)
        self.assertEqual(g_pow_p(keypair.secret_key), keypair.public_key)

    @given(
        elgamal_keypairs(),
        integers(0, 100),
        elements_mod_q_no_zero(),
        integers(0, 100),
        elements_mod_q_no_zero(),
    )
    def test_elgamal_add_homomorphic_accumulation_decrypts_successfully(
        self,
        keypair: ElGamalKeyPair,
        m1: int,
        r1: ElementModQ,
        m2: int,
        r2: ElementModQ,
    ):
        c1 = get_optional(elgamal_encrypt(m1, r1, keypair.public_key))
        c2 = get_optional(elgamal_encrypt(m2, r2, keypair.public_key))
        c_sum = elgamal_add(c1, c2)
        total = c_sum.decrypt(keypair.secret_key)

        self.assertEqual(total, m1 + m2)

    def test_elgamal_add_requires_args(self):
        self.assertRaises(Exception, elgamal_add)

    @given(elgamal_keypairs())
    def test_elgamal_keypair_produces_valid_residue(self, keypair):
        self.assertTrue(keypair.public_key.is_valid_residue())

    def test_elgamal_keypair_random(self):
        # Act
        random_keypair = elgamal_keypair_random()
        random_keypair_two = elgamal_keypair_random()

        # Assert
        self.assertIsNotNone(random_keypair)
        self.assertIsNotNone(random_keypair.public_key)
        self.assertIsNotNone(random_keypair.secret_key)
        self.assertNotEqual(random_keypair, random_keypair_two)

    def test_elgamal_combine_public_keys(self):
        # Arrange
        random_keypair = elgamal_keypair_random()
        random_keypair_two = elgamal_keypair_random()
        public_keys = [
            random_keypair.public_key, random_keypair_two.public_key
        ]

        # Act
        joint_key = elgamal_combine_public_keys(public_keys)

        # Assert
        self.assertIsNotNone(joint_key)
        self.assertNotEqual(joint_key, random_keypair.public_key)
        self.assertNotEqual(joint_key, random_keypair_two.public_key)

    # Here's an oddball test: checking whether running lots of parallel exponentiations yields the
    # correct answer. It certainly *should* work, but this verifies that nothing weird is happening
    # in the GMPY2 library, with it's C code below that.
    def test_gmpy2_parallelism_is_safe(self):
        cpus = cpu_count()
        problem_size = 1000
        secret_keys = Nonces(int_to_q_unchecked(3))[
            0:problem_size]  # list of 1000 might-as-well-be-random Q's
        log_info(
            f"testing GMPY2 powmod parallelism safety (cpus = {cpus}, problem_size = {problem_size})"
        )

        # compute in parallel
        start = timer()
        p = Pool(cpus)
        keypairs = p.map(elgamal_keypair_from_secret, secret_keys)
        end1 = timer()

        # verify scalar
        for keypair in keypairs:
            self.assertEqual(
                keypair.public_key,
                elgamal_keypair_from_secret(keypair.secret_key).public_key,
            )
        end2 = timer()
        p.close(
        )  # apparently necessary to avoid warnings from the Pool system
        log_info(f"Parallelism speedup: {(end2 - end1) / (end1 - start):.3f}")
コード例 #7
0
class TestDecrypt(unittest.TestCase):
    @settings(
        deadline=timedelta(milliseconds=2000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
        # disabling the "shrink" phase, because it runs very slowly
        phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target],
    )
    @given(
        ElectionFactory.get_selection_description_well_formed(),
        elgamal_keypairs(),
        elements_mod_q_no_zero(),
        integers(),
    )
    def test_decrypt_selection_valid_input_succeeds(
        self,
        selection_description: Tuple[str, SelectionDescription],
        keypair: ElGamalKeyPair,
        nonce_seed: ElementModQ,
        random_seed: int,
    ):

        # Arrange
        random = Random(random_seed)
        _, description = selection_description
        data = ballot_factory.get_random_selection_from(description, random)

        # Act
        subject = encrypt_selection(data, description, keypair.public_key, nonce_seed)
        self.assertIsNotNone(subject)

        result_from_key = decrypt_selection_with_secret(
            subject, description, keypair.public_key, keypair.secret_key
        )
        result_from_nonce = decrypt_selection_with_nonce(
            subject, description, keypair.public_key
        )
        result_from_nonce_seed = decrypt_selection_with_nonce(
            subject, description, keypair.public_key, nonce_seed
        )

        # Assert
        self.assertIsNotNone(result_from_key)
        self.assertIsNotNone(result_from_nonce)
        self.assertIsNotNone(result_from_nonce_seed)
        self.assertEqual(data.plaintext, result_from_key.plaintext)
        self.assertEqual(data.plaintext, result_from_nonce.plaintext)
        self.assertEqual(data.plaintext, result_from_nonce_seed.plaintext)

    @settings(
        deadline=timedelta(milliseconds=2000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
        # disabling the "shrink" phase, because it runs very slowly
        phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target],
    )
    @given(
        ElectionFactory.get_selection_description_well_formed(),
        elgamal_keypairs(),
        elements_mod_q_no_zero(),
        integers(),
    )
    def test_decrypt_selection_valid_input_tampered_fails(
        self,
        selection_description: Tuple[str, SelectionDescription],
        keypair: ElGamalKeyPair,
        seed: ElementModQ,
        random_seed: int,
    ):

        # Arrange
        _, description = selection_description
        random = Random(random_seed)
        data = ballot_factory.get_random_selection_from(description, random)

        # Act
        subject = encrypt_selection(data, description, keypair.public_key, seed)

        # tamper with the encryption
        malformed_encryption = deepcopy(subject)
        malformed_message = malformed_encryption.message._replace(
            alpha=mult_p(subject.message.alpha, TWO_MOD_P)
        )
        malformed_encryption.message = malformed_message

        # tamper with the proof
        malformed_proof = deepcopy(subject)
        malformed_disjunctive = malformed_proof.proof._replace(
            a0=mult_p(subject.proof.a0, TWO_MOD_P)
        )
        malformed_proof.proof = malformed_disjunctive

        result_from_key_malformed_encryption = decrypt_selection_with_secret(
            malformed_encryption, description, keypair.public_key, keypair.secret_key
        )

        result_from_key_malformed_proof = decrypt_selection_with_secret(
            malformed_proof, description, keypair.public_key, keypair.secret_key
        )

        result_from_nonce_malformed_encryption = decrypt_selection_with_nonce(
            malformed_encryption, description, keypair.public_key
        )
        result_from_nonce_malformed_proof = decrypt_selection_with_nonce(
            malformed_proof, description, keypair.public_key
        )

        # Assert
        self.assertIsNone(result_from_key_malformed_encryption)
        self.assertIsNone(result_from_key_malformed_proof)
        self.assertIsNone(result_from_nonce_malformed_encryption)
        self.assertIsNone(result_from_nonce_malformed_proof)

    @settings(
        deadline=timedelta(milliseconds=2000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
        # disabling the "shrink" phase, because it runs very slowly
        phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target],
    )
    @given(
        ElectionFactory.get_selection_description_well_formed(),
        elgamal_keypairs(),
        elements_mod_q_no_zero(),
        integers(),
    )
    def test_decrypt_selection_tampered_nonce_fails(
        self,
        selection_description: Tuple[str, SelectionDescription],
        keypair: ElGamalKeyPair,
        nonce_seed: ElementModQ,
        random_seed: int,
    ):

        # Arrange
        random = Random(random_seed)
        _, description = selection_description
        data = ballot_factory.get_random_selection_from(description, random)

        # Act
        subject = encrypt_selection(data, description, keypair.public_key, nonce_seed)
        self.assertIsNotNone(subject)

        # Tamper with the nonce by setting it to an aribtrary value
        subject.nonce = nonce_seed

        result_from_nonce_seed = decrypt_selection_with_nonce(
            subject, description, keypair.public_key, nonce_seed
        )

        # Assert
        self.assertIsNone(result_from_nonce_seed)

    @settings(
        deadline=timedelta(milliseconds=5000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
        # disabling the "shrink" phase, because it runs very slowly
        phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target],
    )
    @given(
        ElectionFactory.get_contest_description_well_formed(),
        elgamal_keypairs(),
        elements_mod_q_no_zero(),
        integers(),
    )
    def test_decrypt_contest_valid_input_succeeds(
        self,
        contest_description: Tuple[str, ContestDescription],
        keypair: ElGamalKeyPair,
        nonce_seed: ElementModQ,
        random_seed: int,
    ):

        # Arrange
        _, description = contest_description
        random = Random(random_seed)
        data = ballot_factory.get_random_contest_from(description, random)

        placeholders = generate_placeholder_selections_from(
            description, description.number_elected
        )
        description_with_placeholders = contest_description_with_placeholders_from(
            description, placeholders
        )

        self.assertTrue(description_with_placeholders.is_valid())

        # Act
        subject = encrypt_contest(
            data, description_with_placeholders, keypair.public_key, nonce_seed
        )
        self.assertIsNotNone(subject)

        # Decrypt the contest, but keep the placeholders
        # so we can verify the selection count matches as expected in the test
        result_from_key = decrypt_contest_with_secret(
            subject,
            description_with_placeholders,
            keypair.public_key,
            keypair.secret_key,
            remove_placeholders=False,
        )
        result_from_nonce = decrypt_contest_with_nonce(
            subject,
            description_with_placeholders,
            keypair.public_key,
            remove_placeholders=False,
        )
        result_from_nonce_seed = decrypt_contest_with_nonce(
            subject,
            description_with_placeholders,
            keypair.public_key,
            nonce_seed,
            remove_placeholders=False,
        )

        # Assert
        self.assertIsNotNone(result_from_key)
        self.assertIsNotNone(result_from_nonce)
        self.assertIsNotNone(result_from_nonce_seed)

        # The decrypted contest should include an entry for each possible selection
        # and placeholders for each seat
        expected_entries = (
            len(description.ballot_selections) + description.number_elected
        )
        self.assertTrue(
            result_from_key.is_valid(
                description.object_id,
                expected_entries,
                description.number_elected,
                description.votes_allowed,
            )
        )
        self.assertTrue(
            result_from_nonce.is_valid(
                description.object_id,
                expected_entries,
                description.number_elected,
                description.votes_allowed,
            )
        )
        self.assertTrue(
            result_from_nonce_seed.is_valid(
                description.object_id,
                expected_entries,
                description.number_elected,
                description.votes_allowed,
            )
        )

        # Assert the ballot selections sum to the expected number of selections
        key_selected = sum(
            [selection.to_int() for selection in result_from_key.ballot_selections]
        )
        nonce_selected = sum(
            [selection.to_int() for selection in result_from_nonce.ballot_selections]
        )
        seed_selected = sum(
            [
                selection.to_int()
                for selection in result_from_nonce_seed.ballot_selections
            ]
        )

        self.assertEqual(key_selected, nonce_selected)
        self.assertEqual(seed_selected, nonce_selected)
        self.assertEqual(description.number_elected, key_selected)

        # Assert each selection is valid
        for selection_description in description.ballot_selections:

            key_selection = [
                selection
                for selection in result_from_key.ballot_selections
                if selection.object_id == selection_description.object_id
            ][0]
            nonce_selection = [
                selection
                for selection in result_from_nonce.ballot_selections
                if selection.object_id == selection_description.object_id
            ][0]
            seed_selection = [
                selection
                for selection in result_from_nonce_seed.ballot_selections
                if selection.object_id == selection_description.object_id
            ][0]

            data_selections_exist = [
                selection
                for selection in data.ballot_selections
                if selection.object_id == selection_description.object_id
            ]

            # It's possible there are no selections in the original data collection
            # since it is valid to pass in a ballot that is not complete
            if any(data_selections_exist):
                self.assertTrue(
                    data_selections_exist[0].to_int() == key_selection.to_int()
                )
                self.assertTrue(
                    data_selections_exist[0].to_int() == nonce_selection.to_int()
                )
                self.assertTrue(
                    data_selections_exist[0].to_int() == seed_selection.to_int()
                )

            # TODO: also check edge cases such as:
            # - placeholder selections are true for under votes

            self.assertTrue(key_selection.is_valid(selection_description.object_id))
            self.assertTrue(nonce_selection.is_valid(selection_description.object_id))
            self.assertTrue(seed_selection.is_valid(selection_description.object_id))

    @settings(
        deadline=timedelta(milliseconds=5000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
        # disabling the "shrink" phase, because it runs very slowly
        phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target],
    )
    @given(
        ElectionFactory.get_contest_description_well_formed(),
        elgamal_keypairs(),
        elements_mod_q_no_zero(),
        integers(),
    )
    def test_decrypt_contest_invalid_input_fails(
        self,
        contest_description: Tuple[str, ContestDescription],
        keypair: ElGamalKeyPair,
        nonce_seed: ElementModQ,
        random_seed: int,
    ):

        # Arrange
        _, description = contest_description
        random = Random(random_seed)
        data = ballot_factory.get_random_contest_from(description, random)

        placeholders = generate_placeholder_selections_from(
            description, description.number_elected
        )
        description_with_placeholders = contest_description_with_placeholders_from(
            description, placeholders
        )

        self.assertTrue(description_with_placeholders.is_valid())

        # Act
        subject = encrypt_contest(
            data, description_with_placeholders, keypair.public_key, nonce_seed
        )
        self.assertIsNotNone(subject)

        # tamper with the nonce
        subject.nonce = int_to_q_unchecked(1)

        result_from_nonce = decrypt_contest_with_nonce(
            subject,
            description_with_placeholders,
            keypair.public_key,
            remove_placeholders=False,
        )
        result_from_nonce_seed = decrypt_contest_with_nonce(
            subject,
            description_with_placeholders,
            keypair.public_key,
            nonce_seed,
            remove_placeholders=False,
        )

        # Assert
        self.assertIsNone(result_from_nonce)
        self.assertIsNone(result_from_nonce_seed)

        # Tamper with the encryption
        subject.ballot_selections[0].message = ElGamalCiphertext(TWO_MOD_P, TWO_MOD_P)

        result_from_key_tampered = decrypt_contest_with_secret(
            subject,
            description_with_placeholders,
            keypair.public_key,
            keypair.secret_key,
            remove_placeholders=False,
        )
        result_from_nonce_tampered = decrypt_contest_with_nonce(
            subject,
            description_with_placeholders,
            keypair.public_key,
            remove_placeholders=False,
        )
        result_from_nonce_seed_tampered = decrypt_contest_with_nonce(
            subject,
            description_with_placeholders,
            keypair.public_key,
            nonce_seed,
            remove_placeholders=False,
        )

        # Assert
        self.assertIsNone(result_from_key_tampered)
        self.assertIsNone(result_from_nonce_tampered)
        self.assertIsNone(result_from_nonce_seed_tampered)

    @settings(
        deadline=timedelta(milliseconds=2000),
        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(elgamal_keypairs())
    def test_decrypt_ballot_valid_input_succeeds(self, keypair: ElGamalKeyPair):
        """
        Check that decryption works as expected by encrypting a ballot using the stateful `EncryptionMediator`
        and then calling the various decrypt functions.
        """

        # TODO: Hypothesis test instead

        # Arrange
        election = election_factory.get_simple_election_from_file()
        metadata, context = election_factory.get_fake_ciphertext_election(
            election, keypair.public_key
        )

        data = ballot_factory.get_simple_ballot_from_file()
        device = EncryptionDevice("Location")
        operator = EncryptionMediator(metadata, context, device)

        # Act
        subject = operator.encrypt(data)
        self.assertIsNotNone(subject)

        result_from_key = decrypt_ballot_with_secret(
            subject,
            metadata,
            context.crypto_extended_base_hash,
            keypair.public_key,
            keypair.secret_key,
            remove_placeholders=False,
        )
        result_from_nonce = decrypt_ballot_with_nonce(
            subject,
            metadata,
            context.crypto_extended_base_hash,
            keypair.public_key,
            remove_placeholders=False,
        )
        result_from_nonce_seed = decrypt_ballot_with_nonce(
            subject,
            metadata,
            context.crypto_extended_base_hash,
            keypair.public_key,
            subject.nonce,
            remove_placeholders=False,
        )

        # Assert
        self.assertIsNotNone(result_from_key)
        self.assertIsNotNone(result_from_nonce)
        self.assertIsNotNone(result_from_nonce_seed)
        self.assertEqual(data.object_id, subject.object_id)
        self.assertEqual(data.object_id, result_from_key.object_id)
        self.assertEqual(data.object_id, result_from_nonce.object_id)
        self.assertEqual(data.object_id, result_from_nonce_seed.object_id)

        for description in metadata.get_contests_for(data.ballot_style):

            expected_entries = (
                len(description.ballot_selections) + description.number_elected
            )

            key_contest = [
                contest
                for contest in result_from_key.contests
                if contest.object_id == description.object_id
            ][0]
            nonce_contest = [
                contest
                for contest in result_from_nonce.contests
                if contest.object_id == description.object_id
            ][0]
            seed_contest = [
                contest
                for contest in result_from_nonce_seed.contests
                if contest.object_id == description.object_id
            ][0]

            # Contests may not be voted on the ballot
            data_contest_exists = [
                contest
                for contest in data.contests
                if contest.object_id == description.object_id
            ]
            if any(data_contest_exists):
                data_contest = data_contest_exists[0]
            else:
                data_contest = None

            self.assertTrue(
                key_contest.is_valid(
                    description.object_id,
                    expected_entries,
                    description.number_elected,
                    description.votes_allowed,
                )
            )
            self.assertTrue(
                nonce_contest.is_valid(
                    description.object_id,
                    expected_entries,
                    description.number_elected,
                    description.votes_allowed,
                )
            )
            self.assertTrue(
                seed_contest.is_valid(
                    description.object_id,
                    expected_entries,
                    description.number_elected,
                    description.votes_allowed,
                )
            )

            for selection_description in description.ballot_selections:

                key_selection = [
                    selection
                    for selection in key_contest.ballot_selections
                    if selection.object_id == selection_description.object_id
                ][0]
                nonce_selection = [
                    selection
                    for selection in nonce_contest.ballot_selections
                    if selection.object_id == selection_description.object_id
                ][0]
                seed_selection = [
                    selection
                    for selection in seed_contest.ballot_selections
                    if selection.object_id == selection_description.object_id
                ][0]

                # Selections may be undervoted for a specific contest
                if any(data_contest_exists):
                    data_selection_exists = [
                        selection
                        for selection in data_contest.ballot_selections
                        if selection.object_id == selection_description.object_id
                    ]
                else:
                    data_selection_exists = []

                if any(data_selection_exists):
                    data_selection = data_selection_exists[0]
                    self.assertTrue(data_selection.to_int() == key_selection.to_int())
                    self.assertTrue(data_selection.to_int() == nonce_selection.to_int())
                    self.assertTrue(data_selection.to_int() == seed_selection.to_int())
                else:
                    data_selection = None

                # TODO: also check edge cases such as:
                # - placeholder selections are true for under votes

                self.assertTrue(key_selection.is_valid(selection_description.object_id))
                self.assertTrue(
                    nonce_selection.is_valid(selection_description.object_id)
                )
                self.assertTrue(
                    seed_selection.is_valid(selection_description.object_id)
                )

    @settings(
        deadline=timedelta(milliseconds=2000),
        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(elgamal_keypairs())
    def test_decrypt_ballot_valid_input_missing_nonce_fails(
        self, keypair: ElGamalKeyPair
    ):

        # Arrange
        election = election_factory.get_simple_election_from_file()
        metadata, context = election_factory.get_fake_ciphertext_election(
            election, keypair.public_key
        )

        data = ballot_factory.get_simple_ballot_from_file()
        device = EncryptionDevice("Location")
        operator = EncryptionMediator(metadata, context, device)

        # Act
        subject = operator.encrypt(data)
        self.assertIsNotNone(subject)
        subject.nonce = None

        missing_nonce_value = None

        result_from_nonce = decrypt_ballot_with_nonce(
            subject, metadata, context.crypto_extended_base_hash, keypair.public_key,
        )
        result_from_nonce_seed = decrypt_ballot_with_nonce(
            subject,
            metadata,
            context.crypto_extended_base_hash,
            keypair.public_key,
            missing_nonce_value,
        )

        # Assert
        self.assertIsNone(result_from_nonce)
        self.assertIsNone(result_from_nonce_seed)
コード例 #8
0
class TestDisjunctiveChaumPedersen(TestCase):
    def test_djcp_proofs_simple(self):
        # doesn't get any simpler than this
        keypair = elgamal_keypair_from_secret(TWO_MOD_Q)
        nonce = ONE_MOD_Q
        seed = TWO_MOD_Q
        message0 = get_optional(elgamal_encrypt(0, nonce, keypair.public_key))
        proof0 = make_disjunctive_chaum_pedersen_zero(
            message0, nonce, keypair.public_key, seed
        )
        proof0bad = make_disjunctive_chaum_pedersen_one(
            message0, nonce, keypair.public_key, seed
        )
        self.assertTrue(proof0.is_valid(message0, keypair.public_key))
        self.assertFalse(proof0bad.is_valid(message0, keypair.public_key))

        message1 = get_optional(elgamal_encrypt(1, nonce, keypair.public_key))
        proof1 = make_disjunctive_chaum_pedersen_one(
            message1, nonce, keypair.public_key, seed
        )
        proof1bad = make_disjunctive_chaum_pedersen_zero(
            message1, nonce, keypair.public_key, seed
        )
        self.assertTrue(proof1.is_valid(message1, keypair.public_key))
        self.assertFalse(proof1bad.is_valid(message1, keypair.public_key))

    def test_djcp_proof_invalid_inputs(self):
        # this is here to push up our coverage
        keypair = elgamal_keypair_from_secret(TWO_MOD_Q)
        nonce = ONE_MOD_Q
        seed = TWO_MOD_Q
        message0 = get_optional(elgamal_encrypt(0, nonce, keypair.public_key))
        self.assertRaises(
            Exception,
            make_disjunctive_chaum_pedersen,
            message0,
            nonce,
            keypair.public_key,
            seed,
            3,
        )

    @settings(
        deadline=timedelta(milliseconds=2000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(elgamal_keypairs(), elements_mod_q_no_zero(), elements_mod_q())
    def test_djcp_proof_zero(
        self, keypair: ElGamalKeyPair, nonce: ElementModQ, seed: ElementModQ
    ):
        message = get_optional(elgamal_encrypt(0, nonce, keypair.public_key))
        proof = make_disjunctive_chaum_pedersen_zero(
            message, nonce, keypair.public_key, seed
        )
        proof_bad = make_disjunctive_chaum_pedersen_one(
            message, nonce, keypair.public_key, seed
        )
        self.assertTrue(proof.is_valid(message, keypair.public_key))
        self.assertFalse(proof_bad.is_valid(message, keypair.public_key))

    @settings(
        deadline=timedelta(milliseconds=2000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(elgamal_keypairs(), elements_mod_q_no_zero(), elements_mod_q())
    def test_djcp_proof_one(
        self, keypair: ElGamalKeyPair, nonce: ElementModQ, seed: ElementModQ
    ):
        message = get_optional(elgamal_encrypt(1, nonce, keypair.public_key))
        proof = make_disjunctive_chaum_pedersen_one(
            message, nonce, keypair.public_key, seed
        )
        proof_bad = make_disjunctive_chaum_pedersen_zero(
            message, nonce, keypair.public_key, seed
        )
        self.assertTrue(proof.is_valid(message, keypair.public_key))
        self.assertFalse(proof_bad.is_valid(message, keypair.public_key))

    @settings(
        deadline=timedelta(milliseconds=2000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(elgamal_keypairs(), elements_mod_q_no_zero(), elements_mod_q())
    def test_djcp_proof_broken(
        self, keypair: ElGamalKeyPair, nonce: ElementModQ, seed: ElementModQ
    ):
        # verify two different ways to generate an invalid C-P proof.
        message = get_optional(elgamal_encrypt(0, nonce, keypair.public_key))
        message_bad = get_optional(elgamal_encrypt(2, nonce, keypair.public_key))
        proof = make_disjunctive_chaum_pedersen_zero(
            message, nonce, keypair.public_key, seed
        )
        proof_bad = make_disjunctive_chaum_pedersen_zero(
            message_bad, nonce, keypair.public_key, seed
        )

        self.assertFalse(proof_bad.is_valid(message_bad, keypair.public_key))
        self.assertFalse(proof.is_valid(message_bad, keypair.public_key))
コード例 #9
0
class TestConstantChaumPedersen(TestCase):
    def test_ccp_proofs_simple_encryption_of_zero(self):
        keypair = elgamal_keypair_from_secret(TWO_MOD_Q)
        nonce = ONE_MOD_Q
        seed = TWO_MOD_Q
        message = get_optional(elgamal_encrypt(0, nonce, keypair.public_key))
        proof = make_constant_chaum_pedersen(
            message, 0, nonce, keypair.public_key, seed
        )
        bad_proof = make_constant_chaum_pedersen(
            message, 1, nonce, keypair.public_key, seed
        )
        self.assertTrue(proof.is_valid(message, keypair.public_key))
        self.assertFalse(bad_proof.is_valid(message, keypair.public_key))

    def test_ccp_proofs_simple_encryption_of_one(self):
        keypair = elgamal_keypair_from_secret(TWO_MOD_Q)
        nonce = ONE_MOD_Q
        seed = TWO_MOD_Q
        message = get_optional(elgamal_encrypt(1, nonce, keypair.public_key))
        proof = make_constant_chaum_pedersen(
            message, 1, nonce, keypair.public_key, seed
        )
        bad_proof = make_constant_chaum_pedersen(
            message, 0, nonce, keypair.public_key, seed
        )
        self.assertTrue(proof.is_valid(message, keypair.public_key))
        self.assertFalse(bad_proof.is_valid(message, keypair.public_key))

    @settings(
        deadline=timedelta(milliseconds=2000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(
        elgamal_keypairs(),
        elements_mod_q_no_zero(),
        elements_mod_q(),
        integers(0, 100),
        integers(0, 100),
    )
    def test_ccp_proof(
        self,
        keypair: ElGamalKeyPair,
        nonce: ElementModQ,
        seed: ElementModQ,
        constant: int,
        bad_constant: int,
    ):
        # assume() slows down the test-case generation
        # so assume(constant != bad_constant)
        if constant == bad_constant:
            bad_constant = constant + 1

        message = get_optional(elgamal_encrypt(constant, nonce, keypair.public_key))
        message_bad = get_optional(
            elgamal_encrypt(bad_constant, nonce, keypair.public_key)
        )

        proof = make_constant_chaum_pedersen(
            message, constant, nonce, keypair.public_key, seed
        )
        self.assertTrue(proof.is_valid(message, keypair.public_key))

        proof_bad1 = make_constant_chaum_pedersen(
            message_bad, constant, nonce, keypair.public_key, seed
        )
        self.assertFalse(proof_bad1.is_valid(message_bad, keypair.public_key))

        proof_bad2 = make_constant_chaum_pedersen(
            message, bad_constant, nonce, keypair.public_key, seed
        )
        self.assertFalse(proof_bad2.is_valid(message, keypair.public_key))

        proof_bad3 = proof._replace(constant=-1)
        self.assertFalse(proof_bad3.is_valid(message, keypair.public_key))
コード例 #10
0
class TestEncrypt(unittest.TestCase):
    def test_encrypt_simple_selection_succeeds(self):

        # Arrange
        keypair = elgamal_keypair_from_secret(int_to_q(2))
        nonce = randbelow(Q)
        metadata = SelectionDescription("some-selection-object-id",
                                        "some-candidate-id", 1)
        hash_context = metadata.crypto_hash()

        subject = selection_from(metadata)
        self.assertTrue(subject.is_valid(metadata.object_id))

        # Act
        result = encrypt_selection(subject, metadata, keypair.public_key,
                                   nonce)

        # Assert
        self.assertIsNotNone(result)
        self.assertIsNotNone(result.message)
        self.assertTrue(
            result.is_valid_encryption(hash_context, keypair.public_key))

    def test_encrypt_simple_selection_malformed_data_fails(self):

        # Arrange
        keypair = elgamal_keypair_from_secret(int_to_q(2))
        nonce = randbelow(Q)
        metadata = SelectionDescription("some-selection-object-id",
                                        "some-candidate-id", 1)
        hash_context = metadata.crypto_hash()

        subject = selection_from(metadata)
        self.assertTrue(subject.is_valid(metadata.object_id))

        # Act
        result = encrypt_selection(subject, metadata, keypair.public_key,
                                   nonce)

        # tamper with the description_hash
        malformed_description_hash = deepcopy(result)
        malformed_description_hash.description_hash = TWO_MOD_Q

        # remove the proof
        missing_proof = deepcopy(result)
        missing_proof.proof = None

        # Assert
        self.assertFalse(
            malformed_description_hash.is_valid_encryption(
                hash_context, keypair.public_key))
        self.assertFalse(
            missing_proof.is_valid_encryption(hash_context,
                                              keypair.public_key))

    @settings(
        deadline=timedelta(milliseconds=2000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(
        ElectionFactory.get_selection_description_well_formed(),
        elgamal_keypairs(),
        elements_mod_q_no_zero(),
        integers(),
    )
    def test_encrypt_selection_valid_input_succeeds(
        self,
        selection_description: Tuple[str, SelectionDescription],
        keypair: ElGamalKeyPair,
        seed: ElementModQ,
        random_seed: int,
    ):

        # Arrange
        _, description = selection_description
        random = Random(random_seed)
        subject = ballot_factory.get_random_selection_from(description, random)

        # Act
        result = encrypt_selection(subject, description, keypair.public_key,
                                   seed)

        # Assert
        self.assertIsNotNone(result)
        self.assertIsNotNone(result.message)
        self.assertTrue(
            result.is_valid_encryption(description.crypto_hash(),
                                       keypair.public_key))

    @settings(
        deadline=timedelta(milliseconds=2000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(
        ElectionFactory.get_selection_description_well_formed(),
        elgamal_keypairs(),
        elements_mod_q_no_zero(),
        integers(),
    )
    def test_encrypt_selection_valid_input_tampered_encryption_fails(
        self,
        selection_description: Tuple[str, SelectionDescription],
        keypair: ElGamalKeyPair,
        seed: ElementModQ,
        random_seed: int,
    ):

        # Arrange
        _, description = selection_description
        random = Random(random_seed)
        subject = ballot_factory.get_random_selection_from(description, random)

        # Act
        result = encrypt_selection(subject,
                                   description,
                                   keypair.public_key,
                                   seed,
                                   should_verify_proofs=False)
        self.assertTrue(
            result.is_valid_encryption(description.crypto_hash(),
                                       keypair.public_key))

        # tamper with the encryption
        malformed_encryption = deepcopy(result)
        malformed_message = malformed_encryption.message._replace(
            alpha=mult_p(result.message.alpha, TWO_MOD_P))
        malformed_encryption.message = malformed_message

        # tamper with the proof
        malformed_proof = deepcopy(result)
        malformed_disjunctive = malformed_proof.proof._replace(
            a0=mult_p(result.proof.a0, TWO_MOD_P))
        malformed_proof.proof = malformed_disjunctive

        # Assert
        self.assertFalse(
            malformed_encryption.is_valid_encryption(description.crypto_hash(),
                                                     keypair.public_key))
        self.assertFalse(
            malformed_proof.is_valid_encryption(description.crypto_hash(),
                                                keypair.public_key))

    def test_encrypt_simple_contest_referendum_succeeds(self):
        # Arrange
        keypair = elgamal_keypair_from_secret(int_to_q(2))
        nonce = randbelow(Q)
        ballot_selections = [
            SelectionDescription("some-object-id-affirmative",
                                 "some-candidate-id-affirmative", 0),
            SelectionDescription("some-object-id-negative",
                                 "some-candidate-id-negative", 1),
        ]
        placeholder_selections = [
            SelectionDescription("some-object-id-placeholder",
                                 "some-candidate-id-placeholder", 2)
        ]
        metadata = ContestDescriptionWithPlaceholders(
            "some-contest-object-id",
            "some-electoral-district-id",
            0,
            VoteVariationType.one_of_m,
            1,
            1,
            "some-referendum-contest-name",
            ballot_selections,
            None,
            None,
            placeholder_selections,
        )
        hash_context = metadata.crypto_hash()

        subject = contest_from(metadata)
        self.assertTrue(
            subject.is_valid(
                metadata.object_id,
                len(metadata.ballot_selections),
                metadata.number_elected,
                metadata.votes_allowed,
            ))

        # Act
        result = encrypt_contest(subject, metadata, keypair.public_key, nonce)

        # Assert
        self.assertIsNotNone(result)
        self.assertTrue(
            result.is_valid_encryption(hash_context, keypair.public_key))

    @settings(
        deadline=timedelta(milliseconds=2000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(
        ElectionFactory.get_contest_description_well_formed(),
        elgamal_keypairs(),
        elements_mod_q_no_zero(),
        integers(),
    )
    def test_encrypt_contest_valid_input_succeeds(
        self,
        contest_description: ContestDescription,
        keypair: ElGamalKeyPair,
        nonce_seed: ElementModQ,
        random_seed: int,
    ):

        # Arrange
        _, description = contest_description
        random = Random(random_seed)
        subject = ballot_factory.get_random_contest_from(description, random)

        # Act
        result = encrypt_contest(subject, description, keypair.public_key,
                                 nonce_seed)

        # Assert
        self.assertIsNotNone(result)
        self.assertTrue(
            result.is_valid_encryption(description.crypto_hash(),
                                       keypair.public_key))

        # The encrypted contest should include an entry for each possible selection
        # and placeholders for each seat
        expected_entries = (len(description.ballot_selections) +
                            description.number_elected)
        self.assertEqual(len(result.ballot_selections), expected_entries)

    @settings(
        deadline=timedelta(milliseconds=2000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(
        ElectionFactory.get_contest_description_well_formed(),
        elgamal_keypairs(),
        elements_mod_q_no_zero(),
        integers(),
    )
    def test_encrypt_contest_valid_input_tampered_proof_fails(
        self,
        contest_description: ContestDescription,
        keypair: ElGamalKeyPair,
        nonce_seed: ElementModQ,
        random_seed: int,
    ):

        # Arrange
        _, description = contest_description
        random = Random(random_seed)
        subject = ballot_factory.get_random_contest_from(description, random)

        # Act
        result = encrypt_contest(subject, description, keypair.public_key,
                                 nonce_seed)
        self.assertTrue(
            result.is_valid_encryption(description.crypto_hash(),
                                       keypair.public_key))

        # tamper with the proof
        malformed_proof = deepcopy(result)
        malformed_disjunctive = malformed_proof.proof._replace(
            a=mult_p(result.proof.a, TWO_MOD_P))
        malformed_proof.proof = malformed_disjunctive

        # remove the proof
        missing_proof = deepcopy(result)
        missing_proof.proof = None

        # Assert
        self.assertFalse(
            malformed_proof.is_valid_encryption(description.crypto_hash(),
                                                keypair.public_key))
        self.assertFalse(
            missing_proof.is_valid_encryption(description.crypto_hash(),
                                              keypair.public_key))

    @unittest.skip("runs forever")
    @settings(
        deadline=timedelta(milliseconds=2000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(
        ElectionFactory.get_contest_description_well_formed(),
        elgamal_keypairs(),
        elements_mod_q_no_zero(),
        integers(1, 6),
        integers(),
    )
    def test_encrypt_contest_overvote_fails(
        self,
        contest_description: ContestDescription,
        keypair: ElGamalKeyPair,
        seed: ElementModQ,
        overvotes: int,
        random_seed: int,
    ):
        # Arrange
        _, description = contest_description
        random = Random(random_seed)
        subject = ballot_factory.get_random_contest_from(description, random)

        highest_sequence = max(
            *[
                selection.sequence_order
                for selection in description.ballot_selections
            ],
            1,
        )

        for i in range(overvotes):
            extra = ballot_factory.get_random_selection_from(
                description.ballot_selections[0], random)
            extra.sequence_order = highest_sequence + i + 1
            subject.ballot_selections.append(extra)

        # Act
        result = encrypt_contest(subject, description, keypair.public_key,
                                 seed)

        # Assert
        self.assertIsNone(result)

    def test_encrypt_contest_manually_formed_contest_description_valid_succeeds(
            self):
        description = ContestDescription(
            object_id="[email protected]",
            electoral_district_id="[email protected]",
            sequence_order=1,
            vote_variation=VoteVariationType.n_of_m,
            number_elected=1,
            votes_allowed=1,
            name="",
            ballot_selections=[
                SelectionDescription(
                    object_id="[email protected]",
                    candidate_id="*****@*****.**",
                    sequence_order=0,
                ),
                SelectionDescription(
                    object_id="[email protected]",
                    candidate_id="*****@*****.**",
                    sequence_order=1,
                ),
            ],
            ballot_title=None,
            ballot_subtitle=None,
        )

        keypair = elgamal_keypair_from_secret(TWO_MOD_Q)
        seed = ONE_MOD_Q

        ####################
        data = ballot_factory.get_random_contest_from(description, Random(0))

        placeholders = generate_placeholder_selections_from(
            description, description.number_elected)
        description_with_placeholders = contest_description_with_placeholders_from(
            description, placeholders)

        # Act
        subject = encrypt_contest(
            data,
            description_with_placeholders,
            keypair.public_key,
            seed,
            should_verify_proofs=True,
        )
        self.assertIsNotNone(subject)

    def test_encrypt_contest_duplicate_selection_object_ids_fails(self):
        """
        This is an example test of a failing test where the contest description 
        is malformed
        """
        random_seed = 0

        description = ContestDescription(
            object_id="[email protected]",
            electoral_district_id="[email protected]",
            sequence_order=1,
            vote_variation=VoteVariationType.n_of_m,
            number_elected=1,
            votes_allowed=1,
            name="",
            ballot_selections=[
                SelectionDescription(
                    object_id="[email protected]",
                    candidate_id="*****@*****.**",
                    sequence_order=0,
                ),
                # Note the selection description is the same as the first sequence element
                SelectionDescription(
                    object_id="[email protected]",
                    candidate_id="*****@*****.**",
                    sequence_order=1,
                ),
            ],
        )

        keypair = elgamal_keypair_from_secret(TWO_MOD_Q)
        seed = ONE_MOD_Q

        # Bypass checking the validity of the description
        data = ballot_factory.get_random_contest_from(
            description, Random(0), suppress_validity_check=True)

        placeholders = generate_placeholder_selections_from(
            description, description.number_elected)
        description_with_placeholders = contest_description_with_placeholders_from(
            description, placeholders)

        # Act
        subject = encrypt_contest(data, description_with_placeholders,
                                  keypair.public_key, seed)
        self.assertIsNone(subject)

    def test_encrypt_ballot_simple_succeeds(self):

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

        # TODO: Ballot Factory
        subject = election_factory.get_fake_ballot(metadata)
        self.assertTrue(subject.is_valid(metadata.ballot_styles[0].object_id))

        # Act
        result = encrypt_ballot(subject, metadata, context, SEED_HASH)
        tracker_code = result.get_tracker_code()
        result_from_seed = encrypt_ballot(subject, metadata, context,
                                          SEED_HASH, nonce_seed)

        # Assert
        self.assertIsNotNone(result)
        self.assertIsNotNone(result.tracking_id)
        self.assertIsNotNone(tracker_code)
        self.assertIsNotNone(result_from_seed)
        self.assertTrue(
            result.is_valid_encryption(context.crypto_extended_base_hash,
                                       keypair.public_key))
        self.assertTrue(
            result_from_seed.is_valid_encryption(
                context.crypto_extended_base_hash, keypair.public_key))

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

        data = election_factory.get_fake_ballot(metadata)
        self.assertTrue(data.is_valid(metadata.ballot_styles[0].object_id))

        device = EncryptionDevice("Location")
        subject = EncryptionMediator(metadata, context, device)

        # Act
        result = subject.encrypt(data)

        # Assert
        self.assertIsNotNone(result)
        self.assertTrue(
            result.is_valid_encryption(context.crypto_extended_base_hash,
                                       keypair.public_key))

    def test_encrypt_simple_ballot_from_files_succeeds(self):
        # Arrange
        keypair = elgamal_keypair_from_secret(int_to_q(2))
        election = election_factory.get_simple_election_from_file()
        metadata, context = election_factory.get_fake_ciphertext_election(
            election, keypair.public_key)

        data = ballot_factory.get_simple_ballot_from_file()
        self.assertTrue(data.is_valid(metadata.ballot_styles[0].object_id))

        device = EncryptionDevice("Location")
        subject = EncryptionMediator(metadata, context, device)

        # Act
        result = subject.encrypt(data)

        # Assert
        self.assertIsNotNone(result)
        self.assertEqual(data.object_id, result.object_id)
        self.assertTrue(
            result.is_valid_encryption(context.crypto_extended_base_hash,
                                       keypair.public_key))

    @settings(
        deadline=timedelta(milliseconds=2000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
    )
    @given(elgamal_keypairs())
    def test_encrypt_ballot_with_derivative_nonces_regenerates_valid_proofs(
            self, keypair: ElGamalKeyPair):
        """
        This test verifies that we can regenerate the contest and selection proofs from the cached nonce values
        """

        # TODO: Hypothesis test instead

        # Arrange
        election = election_factory.get_simple_election_from_file()
        metadata, context = election_factory.get_fake_ciphertext_election(
            election, keypair.public_key)

        data = ballot_factory.get_simple_ballot_from_file()
        self.assertTrue(data.is_valid(metadata.ballot_styles[0].object_id))

        device = EncryptionDevice("Location")
        subject = EncryptionMediator(metadata, context, device)

        # Act
        result = subject.encrypt(data)
        self.assertTrue(
            result.is_valid_encryption(context.crypto_extended_base_hash,
                                       keypair.public_key))

        # Assert
        for contest in result.contests:
            # Find the contest description
            contest_description = list(
                filter(lambda i: i.object_id == contest.object_id,
                       metadata.contests))[0]

            # Homomorpically accumulate the selection encryptions
            elgamal_accumulation = elgamal_add(
                *
                [selection.message for selection in contest.ballot_selections])
            # accumulate the selection nonce's
            aggregate_nonce = add_q(
                *[selection.nonce for selection in contest.ballot_selections])

            regenerated_constant = make_constant_chaum_pedersen(
                elgamal_accumulation,
                contest_description.number_elected,
                aggregate_nonce,
                keypair.public_key,
                add_q(contest.nonce, TWO_MOD_Q),
            )

            self.assertTrue(
                regenerated_constant.is_valid(elgamal_accumulation,
                                              keypair.public_key))

            for selection in contest.ballot_selections:
                # Since we know the nonce, we can decrypt the plaintext
                representation = selection.message.decrypt_known_nonce(
                    keypair.public_key, selection.nonce)

                # one could also decrypt with the secret key:
                # representation = selection.message.decrypt(keypair.secret_key)

                regenerated_disjuctive = make_disjunctive_chaum_pedersen(
                    selection.message,
                    selection.nonce,
                    keypair.public_key,
                    add_q(selection.nonce, TWO_MOD_Q),
                    representation,
                )

                self.assertTrue(
                    regenerated_disjuctive.is_valid(selection.message,
                                                    keypair.public_key))
コード例 #11
0
class TestElGamal(unittest.TestCase):
    """ElGamal tests"""
    def test_simple_elgamal_encryption_decryption(self):
        nonce = ONE_MOD_Q
        secret_key = TWO_MOD_Q
        keypair = get_optional(elgamal_keypair_from_secret(secret_key))
        public_key = keypair.public_key

        self.assertLess(public_key.to_int(), P)
        elem = g_pow_p(ZERO_MOD_Q)
        self.assertEqual(elem, ONE_MOD_P)  # g^0 == 1

        ciphertext = get_optional(elgamal_encrypt(0, nonce,
                                                  keypair.public_key))
        self.assertEqual(G, ciphertext.pad.to_int())
        self.assertEqual(
            pow(ciphertext.pad.to_int(), secret_key.to_int(), P),
            pow(public_key.to_int(), nonce.to_int(), P),
        )
        self.assertEqual(
            ciphertext.data.to_int(),
            pow(public_key.to_int(), nonce.to_int(), P),
        )

        plaintext = ciphertext.decrypt(keypair.secret_key)

        self.assertEqual(0, plaintext)

    @given(integers(0, 100), elgamal_keypairs())
    def test_elgamal_encrypt_requires_nonzero_nonce(self, message: int,
                                                    keypair: ElGamalKeyPair):
        self.assertEqual(
            None, elgamal_encrypt(message, ZERO_MOD_Q, keypair.public_key))

    def test_elgamal_keypair_from_secret_requires_key_greater_than_one(self):
        self.assertEqual(None, elgamal_keypair_from_secret(ZERO_MOD_Q))
        self.assertEqual(None, elgamal_keypair_from_secret(ONE_MOD_Q))

    @given(integers(0, 100), elements_mod_q_no_zero(), elgamal_keypairs())
    def test_elgamal_encryption_decryption_inverses(self, message: int,
                                                    nonce: ElementModQ,
                                                    keypair: ElGamalKeyPair):
        ciphertext = get_optional(
            elgamal_encrypt(message, nonce, keypair.public_key))
        plaintext = ciphertext.decrypt(keypair.secret_key)

        self.assertEqual(message, plaintext)

    @given(integers(0, 100), elements_mod_q_no_zero(), elgamal_keypairs())
    def test_elgamal_encryption_decryption_with_known_nonce_inverses(
            self, message: int, nonce: ElementModQ, keypair: ElGamalKeyPair):
        ciphertext = get_optional(
            elgamal_encrypt(message, nonce, keypair.public_key))
        plaintext = ciphertext.decrypt_known_nonce(keypair.public_key, nonce)

        self.assertEqual(message, plaintext)

    @given(elgamal_keypairs())
    def test_elgamal_generated_keypairs_are_within_range(
            self, keypair: ElGamalKeyPair):
        self.assertLess(keypair.public_key.to_int(), P)
        self.assertLess(keypair.secret_key.to_int(), Q)
        self.assertEqual(g_pow_p(keypair.secret_key), keypair.public_key)

    @given(
        elgamal_keypairs(),
        integers(0, 100),
        elements_mod_q_no_zero(),
        integers(0, 100),
        elements_mod_q_no_zero(),
    )
    def test_elgamal_add_homomorphic_accumulation_decrypts_successfully(
        self,
        keypair: ElGamalKeyPair,
        m1: int,
        r1: ElementModQ,
        m2: int,
        r2: ElementModQ,
    ):
        c1 = get_optional(elgamal_encrypt(m1, r1, keypair.public_key))
        c2 = get_optional(elgamal_encrypt(m2, r2, keypair.public_key))
        c_sum = elgamal_add(c1, c2)
        total = c_sum.decrypt(keypair.secret_key)

        self.assertEqual(total, m1 + m2)

    def test_elgamal_add_requires_args(self):
        self.assertRaises(Exception, elgamal_add)

    @given(elgamal_keypairs())
    def test_elgamal_keypair_produces_valid_residue(self, keypair):
        self.assertTrue(keypair.public_key.is_valid_residue())

    def test_elgamal_keypair_random(self):
        # Act
        random_keypair = elgamal_keypair_random()
        random_keypair_two = elgamal_keypair_random()

        # Assert
        self.assertIsNotNone(random_keypair)
        self.assertIsNotNone(random_keypair.public_key)
        self.assertIsNotNone(random_keypair.secret_key)
        self.assertNotEqual(random_keypair, random_keypair_two)

    def test_elgamal_combine_public_keys(self):
        # Arrange
        random_keypair = elgamal_keypair_random()
        random_keypair_two = elgamal_keypair_random()
        public_keys = [
            random_keypair.public_key, random_keypair_two.public_key
        ]

        # Act
        joint_key = elgamal_combine_public_keys(public_keys)

        # Assert
        self.assertIsNotNone(joint_key)
        self.assertNotEqual(joint_key, random_keypair.public_key)
        self.assertNotEqual(joint_key, random_keypair_two.public_key)

    def test_gmpy2_parallelism_is_safe(self):
        """
        Ensures running lots of parallel exponentiations still yields the correct answer.
        This verifies that nothing incorrect is happening in the GMPY2 library
        """

        # Arrange
        scheduler = Scheduler()
        problem_size = 1000
        random_secret_keys = Nonces(int_to_q_unchecked(3))[0:problem_size]
        log_info(
            f"testing GMPY2 powmod parallelism safety (cpus = {scheduler.cpu_count}, problem_size = {problem_size})"
        )

        # Act
        start = timer()
        keypairs = scheduler.schedule(
            elgamal_keypair_from_secret,
            [list([secret_key]) for secret_key in random_secret_keys],
        )
        end1 = timer()

        # Assert
        for keypair in keypairs:
            self.assertEqual(
                keypair.public_key,
                elgamal_keypair_from_secret(keypair.secret_key).public_key,
            )
        end2 = timer()
        scheduler.close()
        log_info(f"Parallelism speedup: {(end2 - end1) / (end1 - start):.3f}")
コード例 #12
0
ファイル: test_ray_tally.py プロジェクト: nealmcb/arlo-e2e
class TestRayTallies(unittest.TestCase):
    def removeTree(self) -> None:
        try:
            shutil.rmtree("ftally_output", ignore_errors=True)
            shutil.rmtree("rtally_output", ignore_errors=True)
        except FileNotFoundError:
            # okay if it's not there
            pass

    def setUp(self) -> None:
        cpus = cpu_count()
        ray_init_localhost(num_cpus=cpus)
        self.pool = Pool(cpus)
        self.removeTree()
        coverage.process_startup()  # necessary for coverage testing to work in parallel

    def tearDown(self) -> None:
        self.pool.close()
        self.removeTree()

    @given(dominion_cvrs(max_rows=120), elgamal_keypairs(), booleans())
    @settings(
        deadline=timedelta(milliseconds=50000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=10,
        # disabling the "shrink" phase, because it runs very slowly
        phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target],
    )
    def test_ray_end_to_end(
        self, input: str, keypair: ElGamalKeyPair, use_keypair: bool
    ) -> None:
        self.removeTree()

        cvrs = read_dominion_csv(StringIO(input))
        self.assertIsNotNone(cvrs)

        _, ballots, _ = cvrs.to_election_description()
        assert len(ballots) > 0, "can't have zero ballots!"

        print(f"End-to-end Ray test with {len(ballots)} ballot(s).")
        if use_keypair:
            rtally = ray_tally_everything(
                cvrs,
                verbose=True,
                secret_key=keypair.secret_key,
                root_dir="rtally_output",
                use_progressbar=False,
            )
        else:
            rtally = ray_tally_everything(
                cvrs, verbose=True, root_dir="rtally_output", use_progressbar=False
            )

        ftally = rtally.to_fast_tally()
        self.assertTrue(ftally.all_proofs_valid(verbose=False))

        # now, we'll write everything to the filesystem and make sure we get the
        # same stuff

        fmanifest = write_fast_tally(ftally, "ftally_output")
        rmanifest = write_ray_tally(rtally, "rtally_output")

        # we can't just assert equality of the manifests, because the root_dirs are different
        self.assertTrue(fmanifest.equivalent(rmanifest))
        self.removeTree()

    @given(dominion_cvrs(max_rows=5), elgamal_keypairs())
    @settings(
        deadline=timedelta(milliseconds=50000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=5,
        # disabling the "shrink" phase, because it runs very slowly
        phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target],
    )
    def test_ray_and_multiprocessing_agree(
        self, input: str, keypair: ElGamalKeyPair
    ) -> None:
        self.removeTree()
        # Normally these are generated internally, but by making them be the same, we take all
        # the non-determinism out of the tally_everything methods and get identical results.
        seed_hash = rand_q()
        master_nonce = rand_q()
        date = datetime.now()

        cvrs = read_dominion_csv(StringIO(input))
        self.assertIsNotNone(cvrs)

        _, ballots, _ = cvrs.to_election_description()
        assert len(ballots) > 0, "can't have zero ballots!"

        print(f"Comparing tallies with {len(ballots)} ballot(s).")

        tally = fast_tally_everything(
            cvrs,
            verbose=False,
            date=date,
            secret_key=keypair.secret_key,
            pool=self.pool,
            seed_hash=seed_hash,
            master_nonce=master_nonce,
            use_progressbar=False,
        )
        rtally = ray_tally_everything(
            cvrs,
            verbose=False,
            date=date,
            secret_key=keypair.secret_key,
            seed_hash=seed_hash,
            master_nonce=master_nonce,
            root_dir="rtally_output",
            use_progressbar=False,
        )

        self.assertEqual(tally, rtally.to_fast_tally())
コード例 #13
0
ファイル: test_publish.py プロジェクト: nealmcb/arlo-e2e
class TestTallyPublishing(unittest.TestCase):
    def removeTree(self) -> None:
        try:
            shutil.rmtree(TALLY_TESTING_DIR, ignore_errors=True)
            shutil.rmtree(DECRYPTED_DIR, ignore_errors=True)
        except FileNotFoundError:
            # okay if it's not there
            pass

    def setUp(self) -> None:
        self.removeTree()
        self.pool = Pool(cpu_count())
        ray_init_localhost()
        coverage.process_startup(
        )  # necessary for coverage testing to work in parallel

    def tearDown(self) -> None:
        self.removeTree()
        self.pool.close()

    @given(dominion_cvrs(max_rows=50), booleans(), elgamal_keypairs())
    @settings(
        deadline=timedelta(milliseconds=50000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=5,
        # disabling the "shrink" phase, because it runs very slowly
        phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target],
    )
    def test_end_to_end_publications(self, input: str, check_proofs: bool,
                                     keypair: ElGamalKeyPair) -> None:
        coverage.process_startup(
        )  # necessary for coverage testing to work in parallel
        self.removeTree(
        )  # if there's anything leftover from a prior run, get rid of it

        cvrs = read_dominion_csv(StringIO(input))
        self.assertIsNotNone(cvrs)

        _, ballots, _ = cvrs.to_election_description()
        assert len(ballots) > 0, "can't have zero ballots!"

        results = fast_tally_everything(cvrs,
                                        self.pool,
                                        secret_key=keypair.secret_key,
                                        verbose=True)

        self.assertTrue(results.all_proofs_valid(self.pool))

        # dump files out to disk
        write_fast_tally(results, TALLY_TESTING_DIR)
        log_and_print(
            "tally_testing written, proceeding to read it back in again")

        # now, read it back again!
        results2 = load_fast_tally(
            TALLY_TESTING_DIR,
            check_proofs=check_proofs,
            pool=self.pool,
            verbose=True,
            recheck_ballots_and_tallies=True,
        )
        self.assertIsNotNone(results2)

        log_and_print("tally_testing got non-null result!")

        self.assertTrue(
            _list_eq(results.encrypted_ballots, results2.encrypted_ballots))
        self.assertTrue(results.equivalent(results2, keypair, self.pool))

        # Make sure there's an index.html file; throws an exception if it's missing
        self.assertIsNotNone(stat(path.join(TALLY_TESTING_DIR, "index.html")))

        # And lastly, while we're here, we'll use all this machinery to exercise the ballot decryption
        # read/write facilities.

        ied = InternalElectionDescription(results.election_description)

        log_and_print("decrypting one more time")
        pballots = decrypt_ballots(
            ied,
            results.context.crypto_extended_base_hash,
            keypair,
            self.pool,
            results.encrypted_ballots,
        )
        self.assertEqual(len(pballots), len(results.encrypted_ballots))
        self.assertNotIn(None, pballots)

        # for speed, we're only going to do this for the first ballot, not all of them
        pballot = pballots[0]
        eballot = results.encrypted_ballots[0]
        bid = pballot.ballot.object_id
        self.assertTrue(
            verify_proven_ballot_proofs(
                results.context.crypto_extended_base_hash,
                keypair.public_key,
                eballot,
                pballot,
            ))
        write_proven_ballot(pballot, DECRYPTED_DIR)
        self.assertTrue(exists_proven_ballot(bid, DECRYPTED_DIR))
        self.assertFalse(exists_proven_ballot(bid + "0", DECRYPTED_DIR))
        self.assertEqual(pballot, load_proven_ballot(bid, DECRYPTED_DIR))

        self.removeTree()  # clean up our mess

    @given(dominion_cvrs(max_rows=50), booleans(), elgamal_keypairs())
    @settings(
        deadline=timedelta(milliseconds=50000),
        suppress_health_check=[HealthCheck.too_slow],
        max_examples=5,
        # disabling the "shrink" phase, because it runs very slowly
        phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target],
    )
    def test_end_to_end_publications_ray(self, input: str, check_proofs: bool,
                                         keypair: ElGamalKeyPair) -> None:
        self.removeTree(
        )  # if there's anything leftover from a prior run, get rid of it

        cvrs = read_dominion_csv(StringIO(input))
        self.assertIsNotNone(cvrs)

        _, ballots, _ = cvrs.to_election_description()
        assert len(ballots) > 0, "can't have zero ballots!"

        results = ray_tally_everything(
            cvrs,
            secret_key=keypair.secret_key,
            verbose=True,
            root_dir=TALLY_TESTING_DIR,
        )

        self.assertTrue(results.all_proofs_valid())

        # dump files out to disk
        write_ray_tally(results, TALLY_TESTING_DIR)
        log_and_print(
            "tally_testing written, proceeding to read it back in again")

        # now, read it back again!
        results2 = load_ray_tally(
            TALLY_TESTING_DIR,
            check_proofs=check_proofs,
            verbose=True,
            recheck_ballots_and_tallies=True,
        )
        self.assertIsNotNone(results2)

        log_and_print("tally_testing got non-null result!")

        self.assertTrue(
            _list_eq(results.encrypted_ballots, results2.encrypted_ballots))
        self.assertTrue(results.equivalent(results2, keypair))
        self.removeTree()  # clean up our mess