Skip to content

Commit 067fe90

Browse files
authored
🔓 Ballot decryption endpoints for all guardians (#94)
* 🔓 Ballot decryption endpoints * Change ballot share route to `decrypt-shares` for clarity * Pylint fixes * Move ballot indexing to a helper function
1 parent 1870a69 commit 067fe90

File tree

7 files changed

+249
-26
lines changed

7 files changed

+249
-26
lines changed

app/api/v1/guardian/ballot.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from typing import Any
2+
from electionguard.ballot import CiphertextAcceptedBallot
3+
from electionguard.decryption import compute_decryption_share_for_ballot
4+
from electionguard.election import CiphertextElectionContext
5+
from electionguard.scheduler import Scheduler
6+
from electionguard.serializable import write_json_object
7+
from fastapi import APIRouter, Body
8+
9+
from ..models import (
10+
convert_guardian,
11+
DecryptBallotSharesRequest,
12+
DecryptBallotSharesResponse,
13+
)
14+
from ..tags import TALLY
15+
16+
router = APIRouter()
17+
18+
19+
@router.post("/decrypt-shares", tags=[TALLY])
20+
def decrypt_ballot_shares(request: DecryptBallotSharesRequest = Body(...)) -> Any:
21+
"""
22+
Decrypt this guardian's share of one or more ballots
23+
"""
24+
ballots = [
25+
CiphertextAcceptedBallot.from_json_object(ballot)
26+
for ballot in request.encrypted_ballots
27+
]
28+
context = CiphertextElectionContext.from_json_object(request.context)
29+
guardian = convert_guardian(request.guardian)
30+
31+
scheduler = Scheduler()
32+
shares = [
33+
compute_decryption_share_for_ballot(guardian, ballot, context, scheduler)
34+
for ballot in ballots
35+
]
36+
37+
response = DecryptBallotSharesResponse(
38+
shares=[write_json_object(share) for share in shares]
39+
)
40+
41+
return response

app/api/v1/guardian/routes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from fastapi import APIRouter
2+
from . import ballot
23
from . import guardian
34
from . import key
45
from . import tally
@@ -7,4 +8,5 @@
78

89
router.include_router(guardian.router, prefix="/guardian")
910
router.include_router(key.router, prefix="/key")
11+
router.include_router(ballot.router, prefix="/ballot")
1012
router.include_router(tally.router, prefix="/tally")

app/api/v1/mediator/ballot.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
from typing import Any, Optional
2-
from electionguard.ballot import CiphertextBallot, PlaintextBallot
1+
from typing import Any, Dict, List, Optional
2+
from electionguard.ballot import (
3+
CiphertextAcceptedBallot,
4+
CiphertextBallot,
5+
PlaintextBallot,
6+
)
37
from electionguard.ballot_box import accept_ballot, BallotBoxState
8+
from electionguard.decrypt_with_shares import decrypt_ballot
9+
from electionguard.decryption_share import BallotDecryptionShare
410
from electionguard.ballot_store import BallotStore
511
from electionguard.election import (
612
CiphertextElectionContext,
@@ -10,11 +16,17 @@
1016
from electionguard.encrypt import encrypt_ballot
1117
from electionguard.group import ElementModQ
1218
from electionguard.serializable import read_json_object, write_json_object
19+
from electionguard.types import BALLOT_ID, GUARDIAN_ID
1320
from electionguard.utils import get_optional
1421
from fastapi import APIRouter, Body, HTTPException
1522

16-
from ..models import AcceptBallotRequest, EncryptBallotsRequest, EncryptBallotsResponse
17-
from ..tags import CAST_AND_SPOIL, ENCRYPT_BALLOTS
23+
from ..models import (
24+
AcceptBallotRequest,
25+
DecryptBallotsRequest,
26+
EncryptBallotsRequest,
27+
EncryptBallotsResponse,
28+
)
29+
from ..tags import CAST_AND_SPOIL, ENCRYPT_BALLOTS, TALLY
1830

1931
router = APIRouter()
2032

@@ -33,6 +45,34 @@ def cast_ballot(request: AcceptBallotRequest = Body(...)) -> Any:
3345
return casted_ballot.to_json_object()
3446

3547

48+
@router.post("/decrypt", tags=[TALLY])
49+
def decrypt_ballots(request: DecryptBallotsRequest = Body(...)) -> Any:
50+
ballots = [
51+
CiphertextAcceptedBallot.from_json_object(ballot)
52+
for ballot in request.encrypted_ballots
53+
]
54+
context: CiphertextElectionContext = CiphertextElectionContext.from_json_object(
55+
request.context
56+
)
57+
58+
all_shares: List[BallotDecryptionShare] = [
59+
read_json_object(share, BallotDecryptionShare)
60+
for shares in request.shares.values()
61+
for share in shares
62+
]
63+
shares_by_ballot = index_shares_by_ballot(all_shares)
64+
65+
extended_base_hash = context.crypto_extended_base_hash
66+
decrypted_ballots = {
67+
ballot.object_id: decrypt_ballot(
68+
ballot, shares_by_ballot[ballot.object_id], extended_base_hash
69+
)
70+
for ballot in ballots
71+
}
72+
73+
return write_json_object(decrypted_ballots)
74+
75+
3676
@router.post("/spoil", tags=[CAST_AND_SPOIL])
3777
def spoil_ballot(request: AcceptBallotRequest = Body(...)) -> Any:
3878
"""
@@ -96,3 +136,18 @@ def handle_ballot(request: AcceptBallotRequest, state: BallotBoxState) -> Any:
96136
)
97137

98138
return accepted_ballot
139+
140+
141+
def index_shares_by_ballot(
142+
shares: List[BallotDecryptionShare],
143+
) -> Dict[BALLOT_ID, Dict[GUARDIAN_ID, BallotDecryptionShare]]:
144+
"""
145+
Construct a lookup by ballot ID containing the dictionary of shares needed
146+
to decrypt that ballot.
147+
"""
148+
shares_by_ballot: Dict[str, Dict[str, BallotDecryptionShare]] = {}
149+
for share in shares:
150+
ballot_shares = shares_by_ballot.setdefault(share.ballot_id, {})
151+
ballot_shares[share.guardian_id] = share
152+
153+
return shares_by_ballot

app/api/v1/mediator/tally.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
publish_ciphertext_tally,
1313
publish_plaintext_tally,
1414
CiphertextTally,
15-
PublishedCiphertextTally,
1615
)
1716
from fastapi import APIRouter, Body, HTTPException
1817

@@ -70,6 +69,11 @@ def decrypt_tally(request: DecryptTallyRequest = Body(...)) -> Any:
7069
}
7170

7271
full_plaintext_tally = decrypt(tally, shares, context)
72+
if not full_plaintext_tally:
73+
raise HTTPException(
74+
status_code=500,
75+
detail="Unable to decrypt tally",
76+
)
7377
published_plaintext_tally = publish_plaintext_tally(full_plaintext_tally)
7478

7579
return published_plaintext_tally.to_json_object()
@@ -97,7 +101,7 @@ def _parse_tally_request(
97101

98102
def _tally_ballots(
99103
tally: CiphertextTally, ballots: List[CiphertextAcceptedBallot]
100-
) -> PublishedCiphertextTally:
104+
) -> Any:
101105
"""
102106
Append a series of ballots to a new or existing tally
103107
"""

app/api/v1/models/ballot.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
from typing import Any, List, Optional
1+
from typing import Any, Dict, List, Optional
22

33
from .base import Base
44
from .election import ElectionDescription, CiphertextElectionContext
5+
from .guardian import Guardian, GuardianId
56

67

78
__all__ = [
9+
"AcceptBallotRequest",
810
"CiphertextAcceptedBallot",
911
"CiphertextBallot",
10-
"PlaintextBallot",
11-
"AcceptBallotRequest",
12+
"DecryptBallotSharesRequest",
13+
"DecryptBallotSharesResponse",
14+
"DecryptBallotsRequest",
1215
"EncryptBallotsRequest",
1316
"EncryptBallotsResponse",
17+
"PlaintextBallot",
1418
]
1519

20+
BallotDecryptionShare = Any
1621
CiphertextAcceptedBallot = Any
1722
CiphertextBallot = Any
1823
PlaintextBallot = Any
@@ -24,6 +29,22 @@ class AcceptBallotRequest(Base):
2429
context: CiphertextElectionContext
2530

2631

32+
class DecryptBallotsRequest(Base):
33+
encrypted_ballots: List[CiphertextAcceptedBallot]
34+
shares: Dict[GuardianId, List[BallotDecryptionShare]]
35+
context: CiphertextElectionContext
36+
37+
38+
class DecryptBallotSharesRequest(Base):
39+
encrypted_ballots: List[CiphertextAcceptedBallot]
40+
guardian: Guardian
41+
context: CiphertextElectionContext
42+
43+
44+
class DecryptBallotSharesResponse(Base):
45+
shares: List[BallotDecryptionShare]
46+
47+
2748
class EncryptBallotsRequest(Base):
2849
ballots: List[PlaintextBallot]
2950
seed_hash: str

app/api/v1/models/guardian.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import electionguard.election_polynomial
44
import electionguard.elgamal
5+
import electionguard.group
56
import electionguard.guardian
67
import electionguard.key_ceremony
78
import electionguard.schnorr
@@ -27,6 +28,7 @@
2728
ElectionPolynomial = Any
2829
ElectionPartialKeyBackup = Any
2930
ElectionPartialKeyChallenge = Any
31+
GuardianId = Any
3032

3133

3234
class Guardian(Base):
@@ -89,19 +91,25 @@ def convert_guardian(api_guardian: Guardian) -> electionguard.guardian.Guardian:
8991
api_guardian.number_of_guardians,
9092
api_guardian.quorum,
9193
)
94+
9295
guardian._auxiliary_keys = electionguard.key_ceremony.AuxiliaryKeyPair(
9396
api_guardian.auxiliary_key_pair.public_key,
9497
api_guardian.auxiliary_key_pair.secret_key,
9598
)
99+
100+
election_public_key = read_json_object(
101+
api_guardian.election_key_pair.public_key, electionguard.group.ElementModP
102+
)
103+
election_secret_key = read_json_object(
104+
api_guardian.election_key_pair.secret_key, electionguard.group.ElementModQ
105+
)
96106
guardian._election_keys = electionguard.key_ceremony.ElectionKeyPair(
107+
electionguard.elgamal.ElGamalKeyPair(election_secret_key, election_public_key),
97108
read_json_object(
98-
guardian._election_keys.key_pair, electionguard.elgamal.ElGamalKeyPair
99-
),
100-
read_json_object(
101-
guardian._election_keys.proof, electionguard.schnorr.SchnorrProof
109+
api_guardian.election_key_pair.proof, electionguard.schnorr.SchnorrProof
102110
),
103111
read_json_object(
104-
guardian._election_keys.polynomial,
112+
api_guardian.election_key_pair.polynomial,
105113
electionguard.election_polynomial.ElectionPolynomial,
106114
),
107115
)

tests/ElectionGuard Web Api.postman_collection.json

Lines changed: 104 additions & 12 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)