From 322b8e347a4c299f1ae9390dfccc188d5934427c Mon Sep 17 00:00:00 2001 From: Lee Richardson Date: Tue, 26 Apr 2022 14:06:26 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20#355=20Guardian=20Reconstruction?= =?UTF-8?q?=20(#608)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Ability to instantiate guardians from private records * Remove guardian_election_partial_key_verifications and backups_to_share per PR * Unit test guardian from private record construction * Fix tests and linting * Make a from_nonce per PR * Refactor to classmethod per PR * Refactor move the constructors next to each other * Move generate_election_key_pair into dedicated ctor per PR * Restore missing PrivateGuardianRecord fields aka backups_to_share and guardian_election_partial_key_verifications * ♻️ Refactor around ElectionKeyPair * ♻️ Remove lint skip * Final fixup * Remove accidentally added file Co-authored-by: Keith Fung --- docs/1_Key_Ceremony.md | 17 +-- src/electionguard/guardian.py | 116 +++++++++++------- .../e2e_steps/input_retrieval_step.py | 2 +- .../helpers/key_ceremony_orchestrator.py | 2 +- tests/integration/test_end_to_end_election.py | 2 +- tests/unit/test_guardian.py | 90 +++++++------- tests/unit/test_key_ceremony_mediator.py | 8 +- 7 files changed, 133 insertions(+), 104 deletions(-) diff --git a/docs/1_Key_Ceremony.md b/docs/1_Key_Ceremony.md index 9e18b73a..b94606aa 100644 --- a/docs/1_Key_Ceremony.md +++ b/docs/1_Key_Ceremony.md @@ -1,21 +1,24 @@ # Key Ceremony -The ElectionGuard Key Ceremony is the process used by Election Officials to share encryption keys for an election. Before an election, a fixed number of Guardians are selection to hold the private keys needed to decrypt the election results. A Quorum count of Guardians can also be specified to compensate for guardians who may be missing at the time of Decryption. For instance, 5 Guardians may be selected to hold the keys, but only 3 of them are required to decrypt the election results. +The ElectionGuard Key Ceremony is the process used by Election Officials to share encryption keys for an election. Before an election, a fixed number of Guardians are selection to hold the private keys needed to decrypt the election results. A Quorum count of Guardians can also be specified to compensate for guardians who may be missing at the time of Decryption. For instance, 5 Guardians may be selected to hold the keys, but only 3 of them are required to decrypt the election results. Guardians are typically Election Officials, Trustees Canvass Board Members, Government Officials or other trusted authorities who are responsible and accountable for conducting the election. ## Summary -The Key Ceremony is broken into several high-level steps. Each Guardian must _announce_ their _attendance_ in the key ceremony, generate their own public-private key pairs, and then _share_ those key pairs with the Quorum. Then the data that is shared is mathematically verified using Non-Interactive Zero Knowledge Proofs, and finally a _joint public key_ is created to encrypt ballots in the election. +The Key Ceremony is broken into several high-level steps. Each Guardian must _announce_ their _attendance_ in the key ceremony, generate their own public-private key pairs, and then _share_ those key pairs with the Quorum. Then the data that is shared is mathematically verified using Non-Interactive Zero Knowledge Proofs, and finally a _joint public key_ is created to encrypt ballots in the election. ### Attendance + Guardians exchange all public keys and ensure each fellow guardian has received an election public key ensuring at all guardians are in attendance. ### Key Sharing -Guardians generate a partial key backup for each guardian and share with that designated key with that guardian. Then each designated guardian sends a verification back to the sender. The sender then publishes to the group when all verifications are received. + +Guardians generate a partial key backup for each guardian and share with that designated key with that guardian. Then each designated guardian sends a verification back to the sender. The sender then publishes to the group when all verifications are received. ### Joint Key -The final step is to publish the joint election key after all keys and backups have been shared. + +The final step is to publish the joint election key after all keys and backups have been shared. ## Glossary @@ -36,7 +39,7 @@ This is a detailed description of the entire Key Ceremony Process 3. Each guardian must generate their `election key pair` _(ElGamal key pair)_. This will generate a corresponding Schnorr `proof` and `polynomial` used for generating `election partial key backups` for sharing. 4. Each guardian must give the other guardians their `election public key` directly or through a mediator. 5. Each guardian must check if all `election public keys` are received. -6. Each guardian must generate `election partial key backup` for each other guardian. The guardian will use their `polynomial` and the designated guardian's `sequence_order` to create the value. +6. Each guardian must generate `election partial key backup` for each other guardian. The guardian will use their `polynomial` and the designated guardian's `sequence_order` to create the value. 7. Each guardian must send each encrypted `election partial key backup` to the designated guardian directly or through a `mediator`. 8. Each guardian checks if all encrypted `election partial key backups` have been received by their recipient guardian directly or through a mediator. 9. Each recipient guardian decrypts each received encrypted `election partial key backup` @@ -70,7 +73,7 @@ guardians: List[Guardian] # Setup Guardians for i in range(NUMBER_OF_GUARDIANS): guardians.append( - Guardian(f"some_guardian_id_{str(i)}", i, NUMBER_OF_GUARDIANS, QUORUM) + Guardian.from_nonce(f"some_guardian_id_{str(i)}", i, NUMBER_OF_GUARDIANS, QUORUM) ) mediator = KeyCeremonyMediator(details) @@ -92,4 +95,4 @@ joint_public_key = mediator.publish_joint_key() ## Implementation Considerations -ElectionGuard can be run without the key ceremony. The key ceremony is the recommended process to generate keys for live end-to-end verifiable elections, however this process may not be necessary for other use cases such as privacy preserving risk limiting audits. +ElectionGuard can be run without the key ceremony. The key ceremony is the recommended process to generate keys for live end-to-end verifiable elections, however this process may not be necessary for other use cases such as privacy preserving risk limiting audits. diff --git a/src/electionguard/guardian.py b/src/electionguard/guardian.py index 6ca57525..b1cadbb2 100644 --- a/src/electionguard/guardian.py +++ b/src/electionguard/guardian.py @@ -1,5 +1,5 @@ # pylint: disable=too-many-public-methods -# pylint: disable=too-many-instance-attributes + from dataclasses import dataclass from typing import Dict, List, Optional, TypeVar @@ -110,7 +110,6 @@ class PrivateGuardianRecord: """Verifications of other guardian's backups""" -# pylint: disable=too-many-instance-attributes class Guardian: """ Guardian of election responsible for safeguarding information and decrypting results. @@ -119,11 +118,8 @@ class Guardian: The second half relates to the decryption process. """ - id: str - sequence_order: int # Cannot be zero - ceremony_details: CeremonyDetails - _election_keys: ElectionKeyPair + ceremony_details: CeremonyDetails _backups_to_share: Dict[GuardianId, ElectionPartialKeyBackup] """ @@ -150,45 +146,84 @@ class Guardian: def __init__( self, - id: str, - sequence_order: int, - number_of_guardians: int, - quorum: int, - nonce_seed: Optional[ElementModQ] = None, + key_pair: ElectionKeyPair, + ceremony_details: CeremonyDetails, + election_public_keys: Dict[GuardianId, ElectionPublicKey] = None, + partial_key_backups: Dict[GuardianId, ElectionPartialKeyBackup] = None, + backups_to_share: Dict[GuardianId, ElectionPartialKeyBackup] = None, + guardian_election_partial_key_verifications: Dict[ + GuardianId, ElectionPartialKeyVerification + ] = None, ) -> None: """ Initialize a guardian with the specified arguments. - :param id: the unique identifier for the guardian - :param sequence_order: a unique number in [1, 256) that identifies this guardian - :param number_of_guardians: the total number of guardians that will participate in the election - :param quorum: the count of guardians necessary to decrypt - :param nonce_seed: an optional `ElementModQ` value that can be used to generate the `ElectionKeyPair`. - It is recommended to only use this field for testing. + :param key_pair The key pair the guardian generated during a key ceremony + :param ceremony_details The details of the key ceremony + :param election_public_keys the public keys the guardian generated during a key ceremony + :param partial_key_backups the partial key backups the guardian generated during a key ceremony """ - self.id = id - self.sequence_order = sequence_order - self.set_ceremony_details(number_of_guardians, quorum) - self._backups_to_share = {} - self._guardian_election_public_keys = {} - self._guardian_election_partial_key_backups = {} - self._guardian_election_partial_key_verifications = {} - self.generate_election_key_pair(nonce_seed if nonce_seed is not None else None) + self._election_keys = key_pair + self.ceremony_details = ceremony_details - def reset(self, number_of_guardians: int, quorum: int) -> None: - """ - Reset guardian to initial state. + # Reduce this ⬇️ + self._backups_to_share = {} if backups_to_share is None else backups_to_share + self._guardian_election_public_keys = ( + {} if election_public_keys is None else election_public_keys + ) + self._guardian_election_partial_key_backups = ( + {} if partial_key_backups is None else partial_key_backups + ) + self._guardian_election_partial_key_verifications = ( + {} + if guardian_election_partial_key_verifications is None + else guardian_election_partial_key_verifications + ) - :param number_of_guardians: Number of guardians in election - :param quorum: Quorum of guardians required to decrypt - """ - self._backups_to_share.clear() - self._guardian_election_public_keys.clear() - self._guardian_election_partial_key_backups.clear() - self._guardian_election_partial_key_verifications.clear() - self.set_ceremony_details(number_of_guardians, quorum) - self.generate_election_key_pair() + self.save_guardian_key(key_pair.share()) + + @property + def id(self) -> GuardianId: + return self._election_keys.owner_id + + @property + def sequence_order(self) -> int: + return self._election_keys.sequence_order + + @classmethod + def from_nonce( + cls, + id: str, + sequence_order: int, + number_of_guardians: int, + quorum: int, + nonce: ElementModQ = None, + ) -> "Guardian": + """Creates a guardian with an `ElementModQ` value that will be used to generate + the `ElectionKeyPair`. If no nonce provided, this will be generated automatically. + This method should generally only be used for testing.""" + key_pair = generate_election_key_pair(id, sequence_order, quorum, nonce) + ceremony_details = CeremonyDetails(number_of_guardians, quorum) + return cls(key_pair, ceremony_details) + + @classmethod + def from_private_record( + cls, + private_guardian_record: PrivateGuardianRecord, + number_of_guardians: int, + quorum: int, + ) -> "Guardian": + guardian = cls( + private_guardian_record.election_keys, + CeremonyDetails(number_of_guardians, quorum), + private_guardian_record.guardian_election_public_keys, + private_guardian_record.guardian_election_partial_key_backups, + private_guardian_record.backups_to_share, + private_guardian_record.guardian_election_partial_key_verifications, + ) + + return guardian def publish(self) -> GuardianRecord: """Publish record of guardian with all required information.""" @@ -215,13 +250,6 @@ def set_ceremony_details(self, number_of_guardians: int, quorum: int) -> None: self.ceremony_details = CeremonyDetails(number_of_guardians, quorum) # Public Keys - def generate_election_key_pair(self, nonce: ElementModQ = None) -> None: - """Generate election key pair for encrypting/decrypting election.""" - self._election_keys = generate_election_key_pair( - self.id, self.sequence_order, self.ceremony_details.quorum, nonce - ) - self.save_guardian_key(self.share_key()) - def share_key(self) -> ElectionPublicKey: """ Share election public key with another guardian. diff --git a/src/electionguard_cli/e2e_steps/input_retrieval_step.py b/src/electionguard_cli/e2e_steps/input_retrieval_step.py index 53dabc97..b238f0e0 100644 --- a/src/electionguard_cli/e2e_steps/input_retrieval_step.py +++ b/src/electionguard_cli/e2e_steps/input_retrieval_step.py @@ -65,7 +65,7 @@ def _get_guardians(number_of_guardians: int, quorum: int) -> List[Guardian]: guardians: List[Guardian] = [] for i in range(number_of_guardians): guardians.append( - Guardian( + Guardian.from_nonce( str(i + 1), i + 1, number_of_guardians, diff --git a/src/electionguard_tools/helpers/key_ceremony_orchestrator.py b/src/electionguard_tools/helpers/key_ceremony_orchestrator.py index 9c9a2342..32ceca30 100644 --- a/src/electionguard_tools/helpers/key_ceremony_orchestrator.py +++ b/src/electionguard_tools/helpers/key_ceremony_orchestrator.py @@ -11,7 +11,7 @@ class KeyCeremonyOrchestrator: @staticmethod def create_guardians(ceremony_details: CeremonyDetails) -> List[Guardian]: return [ - Guardian( + Guardian.from_nonce( str(i + 1), i + 1, ceremony_details.number_of_guardians, diff --git a/tests/integration/test_end_to_end_election.py b/tests/integration/test_end_to_end_election.py index 372124e6..6c6280d0 100644 --- a/tests/integration/test_end_to_end_election.py +++ b/tests/integration/test_end_to_end_election.py @@ -184,7 +184,7 @@ def step_1_key_ceremony(self) -> None: # Setup Guardians for i in range(self.NUMBER_OF_GUARDIANS): self.guardians.append( - Guardian( + Guardian.from_nonce( str(i + 1), i + 1, self.NUMBER_OF_GUARDIANS, diff --git a/tests/unit/test_guardian.py b/tests/unit/test_guardian.py index c7d366cf..01c0a636 100644 --- a/tests/unit/test_guardian.py +++ b/tests/unit/test_guardian.py @@ -23,25 +23,35 @@ class TestGuardian(BaseTestCase): """Guardian tests""" - def test_reset(self) -> None: - guardian = Guardian( + def test_import_from_guardian_private_record(self) -> None: + # Arrange + guardian_expected = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) - expected_number_of_guardians = 10 - expected_quorum = 4 + private_guardian_record = guardian_expected.export_private_data() # Act - guardian.reset(expected_number_of_guardians, expected_quorum) + guardian_actual = Guardian.from_private_record( + private_guardian_record, NUMBER_OF_GUARDIANS, QUORUM + ) # Assert + # pylint: disable=protected-access self.assertEqual( - expected_number_of_guardians, guardian.ceremony_details.number_of_guardians + guardian_actual._election_keys, guardian_expected._election_keys + ) + self.assertEqual( + guardian_actual._guardian_election_public_keys, + guardian_expected._guardian_election_public_keys, + ) + self.assertEqual( + guardian_actual._guardian_election_partial_key_backups, + guardian_expected._guardian_election_partial_key_backups, ) - self.assertEqual(expected_quorum, guardian.ceremony_details.quorum) def test_set_ceremony_details(self) -> None: # Arrange - guardian = Guardian( + guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) expected_number_of_guardians = 10 @@ -58,7 +68,7 @@ def test_set_ceremony_details(self) -> None: def test_share_key(self) -> None: # Arrange - guardian = Guardian( + guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) @@ -74,10 +84,10 @@ def test_share_key(self) -> None: def test_save_guardian_key(self) -> None: # Arrange - guardian = Guardian( + guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) - other_guardian = Guardian( + other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) key = other_guardian.share_key() @@ -90,10 +100,10 @@ def test_save_guardian_key(self) -> None: def test_all_guardian_keys_received(self) -> None: # Arrange - guardian = Guardian( + guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) - other_guardian = Guardian( + other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) key = other_guardian.share_key() @@ -105,28 +115,12 @@ def test_all_guardian_keys_received(self) -> None: # Assert self.assertTrue(guardian.all_guardian_keys_received()) - def test_generate_election_key_pair(self) -> None: - # Arrange - guardian = Guardian( - SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM - ) - first_public_key = guardian.share_key() - - # Act - guardian.generate_election_key_pair() - second_public_key = guardian.share_key() - - # Assert - self.assertIsNotNone(second_public_key) - self.assertIsNotNone(second_public_key.key) - self.assertNotEqual(first_public_key.key, second_public_key.key) - def test_share_backups(self) -> None: # Arrange - guardian = Guardian( + guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) - other_guardian = Guardian( + other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) @@ -149,10 +143,10 @@ def test_share_backups(self) -> None: def test_save_election_partial_key_backup(self) -> None: # Arrange - guardian = Guardian( + guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) - other_guardian = Guardian( + other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) @@ -168,10 +162,10 @@ def test_save_election_partial_key_backup(self) -> None: def test_all_election_partial_key_backups_received(self) -> None: # Arrange # Round 1 - guardian = Guardian( + guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) - other_guardian = Guardian( + other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) @@ -189,10 +183,10 @@ def test_all_election_partial_key_backups_received(self) -> None: def test_verify_election_partial_key_backup(self) -> None: # Arrange # Round 1 - guardian = Guardian( + guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) - other_guardian = Guardian( + other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) @@ -217,13 +211,13 @@ def test_verify_election_partial_key_backup(self) -> None: def test_verify_election_partial_key_challenge(self) -> None: # Arrange - guardian = Guardian( + guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) - other_guardian = Guardian( + other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) - alternate_verifier = Guardian( + alternate_verifier = Guardian.from_nonce( ALTERNATE_VERIFIER_ID, ALTERNATE_VERIFIER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, @@ -247,10 +241,10 @@ def test_verify_election_partial_key_challenge(self) -> None: def test_publish_election_backup_challenge(self) -> None: # Arrange - guardian = Guardian( + guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) - other_guardian = Guardian( + other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) @@ -272,10 +266,10 @@ def test_publish_election_backup_challenge(self) -> None: def test_save_election_partial_key_verification(self) -> None: # Arrange # Round 1 - guardian = Guardian( + guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) - other_guardian = Guardian( + other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) @@ -298,10 +292,10 @@ def test_save_election_partial_key_verification(self) -> None: def test_all_election_partial_key_backups_verified(self) -> None: # Arrange # Round 1 - guardian = Guardian( + guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) - other_guardian = Guardian( + other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) @@ -325,10 +319,10 @@ def test_all_election_partial_key_backups_verified(self) -> None: def test_publish_joint_key(self) -> None: # Arrange # Round 1 - guardian = Guardian( + guardian = Guardian.from_nonce( SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) - other_guardian = Guardian( + other_guardian = Guardian.from_nonce( RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM ) guardian.save_guardian_key(other_guardian.share_key()) diff --git a/tests/unit/test_key_ceremony_mediator.py b/tests/unit/test_key_ceremony_mediator.py index 7fd08028..8b1e157a 100644 --- a/tests/unit/test_key_ceremony_mediator.py +++ b/tests/unit/test_key_ceremony_mediator.py @@ -27,8 +27,12 @@ class TestKeyCeremonyMediator(BaseTestCase): def setUp(self) -> None: super().setUp() - self.GUARDIAN_1 = Guardian(GUARDIAN_1_ID, 1, NUMBER_OF_GUARDIANS, QUORUM) - self.GUARDIAN_2 = Guardian(GUARDIAN_2_ID, 2, NUMBER_OF_GUARDIANS, QUORUM) + self.GUARDIAN_1 = Guardian.from_nonce( + GUARDIAN_1_ID, 1, NUMBER_OF_GUARDIANS, QUORUM + ) + self.GUARDIAN_2 = Guardian.from_nonce( + GUARDIAN_2_ID, 2, NUMBER_OF_GUARDIANS, QUORUM + ) self.GUARDIANS = [self.GUARDIAN_1, self.GUARDIAN_2] def test_reset(self) -> None: