Skip to content

Commit 6b1b96c

Browse files
rkorsakkeithrfung
authored andcommitted
🔓 Tally decryption endpoints (#73)
* 🔓 Tally decryption endpoints Adds the necessary endpoints to compute guardian shares and decrypt a tally *when all guardians are present*. * Add guardian-only tag to share decryption endpoint * Added tally decryption to Postman * Added docstrings for tally decryption endpoints * Fix some bugs introduced in tally decryption
1 parent 64162e0 commit 6b1b96c

File tree

7 files changed

+231
-24
lines changed

7 files changed

+231
-24
lines changed

app/api/v1/endpoints/tally.py

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
11
from electionguard.ballot import CiphertextAcceptedBallot
2+
from electionguard.decrypt_with_shares import decrypt_tally as decrypt
3+
from electionguard.decryption import compute_decryption_share
4+
from electionguard.decryption_share import TallyDecryptionShare
25
from electionguard.election import (
36
CiphertextElectionContext,
47
ElectionDescription,
58
InternalElectionDescription,
69
)
10+
from electionguard.serializable import write_json_object
711
from electionguard.tally import (
812
publish_ciphertext_tally,
13+
publish_plaintext_tally,
914
CiphertextTally,
1015
PublishedCiphertextTally,
1116
)
1217
from fastapi import APIRouter, Body, HTTPException
1318
from typing import Any, List, Tuple
1419

15-
16-
from ..models import StartTallyRequest, AppendTallyRequest
20+
from app.utils.serialize import read_json_object
21+
from ..models import (
22+
convert_guardian,
23+
convert_tally,
24+
AppendTallyRequest,
25+
DecryptTallyRequest,
26+
DecryptTallyShareRequest,
27+
StartTallyRequest,
28+
)
29+
from ..tags import GUARDIAN_ONLY
1730

1831
router = APIRouter()
1932

@@ -24,10 +37,10 @@ def start_tally(request: StartTallyRequest = Body(...)) -> Any:
2437
Start a new tally of a collection of ballots
2538
"""
2639

27-
ballots, description, context = parse_request(request)
40+
ballots, description, context = _parse_tally_request(request)
2841
tally = CiphertextTally("election-results", description, context)
2942

30-
return tally_ballots(tally, ballots)
43+
return _tally_ballots(tally, ballots)
3144

3245

3346
@router.post("/append")
@@ -36,16 +49,52 @@ def append_to_tally(request: AppendTallyRequest = Body(...)) -> Any:
3649
Append ballots into an existing tally
3750
"""
3851

39-
ballots, description, context = parse_request(request)
52+
ballots, description, context = _parse_tally_request(request)
53+
tally = convert_tally(request.encrypted_tally, description, context)
54+
55+
return _tally_ballots(tally, ballots)
56+
57+
58+
@router.post("/decrypt")
59+
def decrypt_tally(request: DecryptTallyRequest = Body(...)) -> Any:
60+
"""
61+
Decrypt a tally from a collection of decrypted guardian shares
62+
"""
63+
description = InternalElectionDescription(
64+
ElectionDescription.from_json_object(request.description)
65+
)
66+
context = CiphertextElectionContext.from_json_object(request.context)
67+
tally = convert_tally(request.encrypted_tally, description, context)
68+
69+
shares = {
70+
guardian_id: read_json_object(share, TallyDecryptionShare)
71+
for guardian_id, share in request.shares.items()
72+
}
4073

41-
published_tally = PublishedCiphertextTally.from_json_object(request.encrypted_tally)
42-
tally = CiphertextTally(published_tally.object_id, description, context)
43-
tally.cast = published_tally.cast
74+
full_plaintext_tally = decrypt(tally, shares, context)
75+
published_plaintext_tally = publish_plaintext_tally(full_plaintext_tally)
4476

45-
return tally_ballots(tally, ballots)
77+
return published_plaintext_tally.to_json_object()
4678

4779

48-
def parse_request(
80+
@router.post("/decrypt-share", tags=[GUARDIAN_ONLY])
81+
def decrypt_share(request: DecryptTallyShareRequest = Body(...)) -> Any:
82+
"""
83+
Decrypt a single guardian's share of a tally
84+
"""
85+
description = InternalElectionDescription(
86+
ElectionDescription.from_json_object(request.description)
87+
)
88+
context = CiphertextElectionContext.from_json_object(request.context)
89+
guardian = convert_guardian(request.guardian)
90+
tally = convert_tally(request.encrypted_tally, description, context)
91+
92+
share = compute_decryption_share(guardian, tally, context)
93+
94+
return write_json_object(share)
95+
96+
97+
def _parse_tally_request(
4998
request: StartTallyRequest,
5099
) -> Tuple[
51100
List[CiphertextAcceptedBallot],
@@ -65,9 +114,12 @@ def parse_request(
65114
return (ballots, internal_description, context)
66115

67116

68-
def tally_ballots(
117+
def _tally_ballots(
69118
tally: CiphertextTally, ballots: List[CiphertextAcceptedBallot]
70119
) -> PublishedCiphertextTally:
120+
"""
121+
Append a series of ballots to a new or existing tally
122+
"""
71123
tally_succeeded = tally.batch_append(ballots)
72124

73125
if tally_succeeded:

app/api/v1/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .ballot import *
22
from .base import *
3+
from .election import *
34
from .guardian import *
45
from .key import *
56
from .tally import *

app/api/v1/models/ballot.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
from typing import Any
22

33
from .base import Base
4+
from .election import ElectionDecription, CiphertextElectionContext
5+
6+
7+
CiphertextAcceptedBallot = Any
8+
CiphertextBallot = Any
49

510

611
class AcceptBallotRequest(Base):
7-
ballot: Any
8-
description: Any
9-
context: Any
12+
ballot: CiphertextBallot
13+
description: ElectionDecription
14+
context: CiphertextElectionContext

app/api/v1/models/election.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from typing import Any
2+
3+
ElectionDecription = Any
4+
5+
CiphertextElectionContext = Any

app/api/v1/models/guardian.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
from typing import Any, List, Optional
22

3+
import electionguard.election_polynomial
4+
import electionguard.elgamal
5+
import electionguard.guardian
6+
import electionguard.key_ceremony
7+
import electionguard.schnorr
8+
9+
from app.utils.serialize import read_json_object
10+
311
from .base import Base
412
from .key import AuxiliaryKeyPair, AuxiliaryPublicKey, ElectionKeyPair
513

@@ -54,3 +62,34 @@ class BackupChallengeRequest(Base):
5462
class ChallengeVerificationRequest(Base):
5563
verifier_id: str
5664
election_partial_key_challenge: ElectionPartialKeyChallenge
65+
66+
67+
def convert_guardian(api_guardian: Guardian) -> electionguard.guardian.Guardian:
68+
"""
69+
Convert an API Guardian model to a fully-hydrated SDK Guardian model.
70+
"""
71+
72+
guardian = electionguard.guardian.Guardian(
73+
api_guardian.id,
74+
api_guardian.sequence_order,
75+
api_guardian.number_of_guardians,
76+
api_guardian.quorum,
77+
)
78+
guardian._auxiliary_keys = electionguard.key_ceremony.AuxiliaryKeyPair(
79+
api_guardian.auxiliary_key_pair.public_key,
80+
api_guardian.auxiliary_key_pair.secret_key,
81+
)
82+
guardian._election_keys = electionguard.key_ceremony.ElectionKeyPair(
83+
read_json_object(
84+
guardian._election_keys.key_pair, electionguard.elgamal.ElGamalKeyPair
85+
),
86+
read_json_object(
87+
guardian._election_keys.proof, electionguard.schnorr.SchnorrProof
88+
),
89+
read_json_object(
90+
guardian._election_keys.polynomial,
91+
electionguard.election_polynomial.ElectionPolynomial,
92+
),
93+
)
94+
95+
return guardian

app/api/v1/models/tally.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,58 @@
1-
from typing import Any, List
1+
from typing import Any, Dict, List
2+
import electionguard.election
3+
import electionguard.tally
24

5+
from app.utils.serialize import read_json_object
6+
7+
from .ballot import CiphertextAcceptedBallot
38
from .base import Base
9+
from .election import ElectionDecription, CiphertextElectionContext
10+
from .guardian import Guardian
11+
12+
13+
PublishedCiphertextTally = Any
14+
TallyDecryptionShare = Any
415

516

617
class StartTallyRequest(Base):
7-
ballots: List[Any]
8-
description: Any
9-
context: Any
18+
ballots: List[CiphertextAcceptedBallot]
19+
description: ElectionDecription
20+
context: CiphertextElectionContext
1021

1122

1223
class AppendTallyRequest(StartTallyRequest):
13-
encrypted_tally: Any
24+
encrypted_tally: PublishedCiphertextTally
25+
26+
27+
class DecryptTallyRequest(Base):
28+
encrypted_tally: PublishedCiphertextTally
29+
shares: Dict[str, TallyDecryptionShare]
30+
description: ElectionDecription
31+
context: CiphertextElectionContext
32+
33+
34+
class DecryptTallyShareRequest(Base):
35+
encrypted_tally: PublishedCiphertextTally
36+
guardian: Guardian
37+
description: ElectionDecription
38+
context: CiphertextElectionContext
39+
40+
41+
def convert_tally(
42+
encrypted_tally: PublishedCiphertextTally,
43+
description: electionguard.election.InternalElectionDescription,
44+
context: electionguard.election.CiphertextElectionContext,
45+
) -> electionguard.tally.CiphertextTally:
46+
"""
47+
Convert to an SDK CiphertextTally model
48+
"""
49+
50+
published_tally = read_json_object(
51+
encrypted_tally, electionguard.tally.PublishedCiphertextTally
52+
)
53+
tally = electionguard.tally.CiphertextTally(
54+
published_tally.object_id, description, context
55+
)
56+
tally.cast = published_tally.cast
57+
58+
return tally

tests/ElectionGuard Web Api.postman_collection.json

Lines changed: 65 additions & 5 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)