Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚧 #355 Guardian Reconstruction #608

Merged
merged 13 commits into from
Apr 26, 2022
17 changes: 10 additions & 7 deletions docs/1_Key_Ceremony.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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`
Expand Down Expand Up @@ -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_context_info(f"some_guardian_id_{str(i)}", i, NUMBER_OF_GUARDIANS, QUORUM)
lprichar marked this conversation as resolved.
Show resolved Hide resolved
)

mediator = KeyCeremonyMediator(details)
Expand All @@ -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.
86 changes: 78 additions & 8 deletions src/electionguard/guardian.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,13 @@ def __init__(
sequence_order: int,
lprichar marked this conversation as resolved.
Show resolved Hide resolved
number_of_guardians: int,
quorum: int,
nonce_seed: Optional[ElementModQ] = None,
election_keys: ElectionKeyPair = None,
lprichar marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand All @@ -163,18 +169,82 @@ def __init__(
: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 election_keys the private keys the guardian generated during a 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._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
)

if not election_keys is None:
self._election_keys = election_keys

@classmethod
def from_context_info(
lprichar marked this conversation as resolved.
Show resolved Hide resolved
cls,
id: str,
sequence_order: int,
number_of_guardians: int,
quorum: int,
) -> "Guardian":
guardian = cls(id, sequence_order, number_of_guardians, quorum)
guardian.generate_election_key_pair(None)
return guardian

@classmethod
def from_nonce(
cls,
id: str,
sequence_order: int,
number_of_guardians: int,
quorum: int,
nonce_seed: Optional[ElementModQ] = None,
) -> "Guardian":
"""Creates a guardian with an `ElementModQ` value that will be used to generate
the `ElectionKeyPair`. This method should generally only be used for testing."""
lprichar marked this conversation as resolved.
Show resolved Hide resolved

guardian = cls(
id,
sequence_order,
number_of_guardians,
quorum,
)
guardian.generate_election_key_pair(nonce_seed)
lprichar marked this conversation as resolved.
Show resolved Hide resolved
return guardian

@classmethod
def from_private_record(
cls,
private_guardian_record: PrivateGuardianRecord,
number_of_guardians: int,
quorum: int,
) -> "Guardian":
guardian = cls(
private_guardian_record.guardian_id,
private_guardian_record.election_keys.sequence_order,
number_of_guardians,
quorum,
private_guardian_record.election_keys,
lprichar marked this conversation as resolved.
Show resolved Hide resolved
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,
)

self.generate_election_key_pair(nonce_seed if nonce_seed is not None else None)
return guardian

def reset(self, number_of_guardians: int, quorum: int) -> None:
"""
Expand Down
2 changes: 1 addition & 1 deletion src/electionguard_cli/e2e_steps/input_retrieval_step.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_context_info(
str(i + 1),
i + 1,
number_of_guardians,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class KeyCeremonyOrchestrator:
@staticmethod
def create_guardians(ceremony_details: CeremonyDetails) -> List[Guardian]:
return [
Guardian(
Guardian.from_context_info(
str(i + 1),
i + 1,
ceremony_details.number_of_guardians,
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_end_to_end_election.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_context_info(
str(i + 1),
i + 1,
self.NUMBER_OF_GUARDIANS,
Expand Down
76 changes: 43 additions & 33 deletions tests/unit/test_guardian.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_context_info(
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_context_info(
SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
expected_number_of_guardians = 10
Expand All @@ -58,7 +68,7 @@ def test_set_ceremony_details(self) -> None:

def test_share_key(self) -> None:
# Arrange
guardian = Guardian(
guardian = Guardian.from_context_info(
SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)

Expand All @@ -74,10 +84,10 @@ def test_share_key(self) -> None:

def test_save_guardian_key(self) -> None:
# Arrange
guardian = Guardian(
guardian = Guardian.from_context_info(
SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
other_guardian = Guardian(
other_guardian = Guardian.from_context_info(
RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
key = other_guardian.share_key()
Expand All @@ -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_context_info(
SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
other_guardian = Guardian(
other_guardian = Guardian.from_context_info(
RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
key = other_guardian.share_key()
Expand All @@ -107,7 +117,7 @@ def test_all_guardian_keys_received(self) -> None:

def test_generate_election_key_pair(self) -> None:
# Arrange
guardian = Guardian(
guardian = Guardian.from_context_info(
SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
first_public_key = guardian.share_key()
Expand All @@ -123,10 +133,10 @@ def test_generate_election_key_pair(self) -> None:

def test_share_backups(self) -> None:
# Arrange
guardian = Guardian(
guardian = Guardian.from_context_info(
SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
other_guardian = Guardian(
other_guardian = Guardian.from_context_info(
RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
guardian.save_guardian_key(other_guardian.share_key())
Expand All @@ -149,10 +159,10 @@ def test_share_backups(self) -> None:

def test_save_election_partial_key_backup(self) -> None:
# Arrange
guardian = Guardian(
guardian = Guardian.from_context_info(
SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
other_guardian = Guardian(
other_guardian = Guardian.from_context_info(
RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
guardian.save_guardian_key(other_guardian.share_key())
Expand All @@ -168,10 +178,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_context_info(
SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
other_guardian = Guardian(
other_guardian = Guardian.from_context_info(
RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
guardian.save_guardian_key(other_guardian.share_key())
Expand All @@ -189,10 +199,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_context_info(
SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
other_guardian = Guardian(
other_guardian = Guardian.from_context_info(
RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
guardian.save_guardian_key(other_guardian.share_key())
Expand All @@ -217,13 +227,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_context_info(
SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
other_guardian = Guardian(
other_guardian = Guardian.from_context_info(
RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
alternate_verifier = Guardian(
alternate_verifier = Guardian.from_context_info(
ALTERNATE_VERIFIER_ID,
ALTERNATE_VERIFIER_SEQUENCE_ORDER,
NUMBER_OF_GUARDIANS,
Expand All @@ -247,10 +257,10 @@ def test_verify_election_partial_key_challenge(self) -> None:

def test_publish_election_backup_challenge(self) -> None:
# Arrange
guardian = Guardian(
guardian = Guardian.from_context_info(
SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
other_guardian = Guardian(
other_guardian = Guardian.from_context_info(
RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
guardian.save_guardian_key(other_guardian.share_key())
Expand All @@ -272,10 +282,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_context_info(
SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
other_guardian = Guardian(
other_guardian = Guardian.from_context_info(
RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
guardian.save_guardian_key(other_guardian.share_key())
Expand All @@ -298,10 +308,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_context_info(
SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
other_guardian = Guardian(
other_guardian = Guardian.from_context_info(
RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
guardian.save_guardian_key(other_guardian.share_key())
Expand All @@ -325,10 +335,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_context_info(
SENDER_GUARDIAN_ID, SENDER_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
other_guardian = Guardian(
other_guardian = Guardian.from_context_info(
RECIPIENT_GUARDIAN_ID, RECIPIENT_SEQUENCE_ORDER, NUMBER_OF_GUARDIANS, QUORUM
)
guardian.save_guardian_key(other_guardian.share_key())
Expand Down
Loading