Skip to content

Commit

Permalink
Feature/election placeholders (#21)
Browse files Browse the repository at this point in the history
* 📝 Add VSCode Recommended Extensions

Add recommendations to ensure users know to install python tools.

* 🧹 Configure VSCode Formatting & Linting Settings

- Enable pylint settings and mypy type checking
- Set the formatting to rely on black

* ✅ Configure Pytest for VS Code


__init__ is necessary to ensure tests are run properly.

* 🔧Configure Github PR Workflow

🔧Configure MyPy to match tox file
🔧Add Black Configuration 
🔧Update Pylint configuration

* 📝Update Readme for first time users

- Isolate Contributing file
- Update Readme with usable dev steps

* ✨MacOS Workflow

* 🧹 Add Dev Lint Dependencies to Pipfile

🛠 Allow Prerelease for Black

* 🔧 Add Linting Bypass and Adjustments to MyPy

- MyPy check is silly and needs to have direct directories and files
- Due to errors with last PR some pieces need to temporarily be bypassed.

* Add InternalElectionDescription

generate placeholders for contests when the election is set up.  Use a builder pattern for setting up an election.

* rename Election to ElectionDescription

* Move Election builder to its own file

__future__ annotations is causing the json serialization to fail.  annotations are necessary to support fluent interface typing for methods on class instances which return their encapsulating type.

see: ramonhagenaars/jsons#26

and ramonhagenaars/jsons#100

until this is fixed, the builder pattern must exist in it's own file

* clean up mypy, move proof generation to class objects

* add decrypt contest and decrypt ballot methods

* Create ObjectBase

* decrypt using derivative nonce seeds for the ballot

when decrypting, allow the consumer to specify the nonce seed to use when generating the nonce for decrypting selections.

* proofs are optional on cyphertext resources

add a few missing tests

* remove contest file

* migrate optional handlers to .utils

* update extended data field

* allow nonce values to be null on cyphertext ballot elements

* rename election builder

* nonce seed to elementmodq

* rename election object base

* provide context for integer params

Co-authored-by: Keith Fung <keith.fung@infernored.com>
  • Loading branch information
AddressXception and keithrfung committed May 7, 2020
1 parent 8cf9c7b commit f9b5fad
Show file tree
Hide file tree
Showing 23 changed files with 1,164 additions and 1,003 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,9 @@ dmypy.json

# PyCharm
.idea/

coverage/

cov.xml

.DS_Store
138 changes: 108 additions & 30 deletions src/electionguard/ballot.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
from dataclasses import dataclass, field
from dataclasses import dataclass, field, InitVar
from distutils import util
from typing import Optional, List

from .chaum_pedersen import ConstantChaumPedersenProof, DisjunctiveChaumPedersenProof
from .election import Contest, Selection
from .chaum_pedersen import (
ConstantChaumPedersenProof,
DisjunctiveChaumPedersenProof,
make_constant_chaum_pedersen,
make_disjunctive_chaum_pedersen
)
from .elgamal import ElGamalCiphertext, elgamal_add
from .group import add_q, ElementModP, ElementModQ, ZERO_MOD_Q
from .hash import CryptoHashCheckable, hash_elems
from .logs import log_warning
from .serializable import Serializable
from .election_object_base import ElectionObjectBase

@dataclass
class PlaintextBallotSelection(Selection):
class ExtendedData(object):
value: str
length: int

@dataclass
class PlaintextBallotSelection(ElectionObjectBase):
"""
A BallotSelection represents an individual selection on a ballot.
Expand All @@ -36,8 +45,8 @@ class PlaintextBallotSelection(Selection):
# determines if this is a placeholder selection
is_placeholder_selection: bool = field(default=False)

# an optional field of arbitrary data
extra_data: Optional[str] = field(default=None)
# an optional field of arbitrary data, such as the value of a write-in candidate
extended_data: Optional[ExtendedData] = field(default=None)

def is_valid(self, expected_object_id: str) -> bool:
"""
Expand Down Expand Up @@ -77,7 +86,7 @@ def to_int(self) -> int:
return as_int

@dataclass
class CyphertextBallotSelection(Contest, CryptoHashCheckable):
class CyphertextBallotSelection(ElectionObjectBase, CryptoHashCheckable):
"""
A CyphertextBallotSelection represents an individual encrypted selection on a ballot.
Expand Down Expand Up @@ -108,6 +117,12 @@ class CyphertextBallotSelection(Contest, CryptoHashCheckable):
# The encrypted representation of the plaintext field
message: ElGamalCiphertext

elgamal_public_key: InitVar[ElementModP]

proof_seed: InitVar[ElementModQ]

selection_representation: InitVar[int]

# determines if this is a placeholder selection
is_placeholder_selection: bool = field(default=False)

Expand All @@ -120,10 +135,23 @@ class CyphertextBallotSelection(Contest, CryptoHashCheckable):

# the proof that demonstrates the selection is an encryption of 0 or 1,
# and was encrypted using the `nonce`
proof: Optional[DisjunctiveChaumPedersenProof] = field(default=None)
proof: Optional[DisjunctiveChaumPedersenProof] = field(init=False, default=None)

def __post_init__(self):
self.crypto_hash = self.crypto_hash_with(self.description_hash)
# encrypted representation of the extended_data field
exnteded_data: Optional[ElGamalCiphertext] = field(default=None)

def __post_init__(self, elgamal_public_key: ElementModP, proof_seed: ElementModQ, selection_representation: int) -> None:
object.__setattr__(self, 'crypto_hash', self.crypto_hash_with(self.description_hash))

if self.nonce is not None:
object.__setattr__(self, 'proof', make_disjunctive_chaum_pedersen(
self.message,
self.nonce,
elgamal_public_key,
proof_seed,
selection_representation,
)
)

def is_valid_encryption(self, seed_hash: ElementModQ, elgamal_public_key: ElementModP) -> bool:
"""
Expand Down Expand Up @@ -173,7 +201,7 @@ def crypto_hash_with(self, seed_hash: ElementModQ) -> ElementModQ:
return hash_elems(seed_hash, self.message.crypto_hash())

@dataclass
class PlaintextBallotContest(Contest):
class PlaintextBallotContest(ElectionObjectBase):
"""
A PlaintextBallotContest represents the selections made by a voter for a specific ContestDescription
Expand Down Expand Up @@ -228,7 +256,7 @@ def is_valid(
return True

@dataclass
class CyphertextBallotContest(Contest, CryptoHashCheckable):
class CyphertextBallotContest(ElectionObjectBase, CryptoHashCheckable):
"""
A CyphertextBallotContest represents the selections made by a voter for a specific ContestDescription
Expand All @@ -250,25 +278,52 @@ class CyphertextBallotContest(Contest, CryptoHashCheckable):
# collection of ballot selections
ballot_selections: List[CyphertextBallotSelection]

# Hash of the encrypted values
crypto_hash: ElementModQ = field(init=False)
elgamal_public_key: InitVar[ElementModP]

proof_seed: InitVar[ElementModQ]

number_elected: InitVar[int]

# the nonce used to generate the encryption
# this value is sensitive & should be treated as a secret
nonce: Optional[ElementModQ] = field(default=None)

# Hash of the encrypted values
crypto_hash: ElementModQ = field(init=False)

# the proof demonstrates the sum of the selections does not exceed the maximum
# available selections for the contest, and that the proof was generated with the nonce
proof: Optional[ConstantChaumPedersenProof] = field(default=None)
proof: Optional[ConstantChaumPedersenProof] = field(init=False)

def __post_init__(self):
self.crypto_hash = self.crypto_hash_with(self.description_hash)
def __post_init__(self, elgamal_public_key: ElementModP, proof_seed: ElementModQ, number_elected: int) -> None:
object.__setattr__(self, 'crypto_hash',self.crypto_hash_with(self.description_hash))

aggregate = self.aggregate_nonce()

if aggregate is not None:
# Generate the proof when the object is created
object.__setattr__(self, 'proof', make_constant_chaum_pedersen(
message=self.elgamal_accumulate(),
constant=number_elected,
r=aggregate,
k=elgamal_public_key,
seed=proof_seed
)
)

def aggregate_nonce(self) -> ElementModQ:
def aggregate_nonce(self) -> Optional[ElementModQ]:
"""
:return: an aggregate nonce for the contest composed of the nonces of the selections
"""
return add_q(*[selection.nonce for selection in self.ballot_selections])
selection_nonces: List[ElementModQ] = list()
for selection in self.ballot_selections:
if selection.nonce is None:
log_warning(f"missing nonce values for contest {self.object_id} cannot calculate aggregate nonce")
return None
else:
selection_nonces.append(selection.nonce)

return add_q(*selection_nonces)

def crypto_hash_with(self, seed_hash: ElementModQ) -> ElementModQ:
"""
Expand Down Expand Up @@ -334,14 +389,12 @@ def is_valid_encryption(self, seed_hash: ElementModQ, elgamal_public_key: Elemen
return self.proof.is_valid(elgamal_accumulation, elgamal_public_key)

@dataclass
class PlaintextBallot(Serializable):
class PlaintextBallot(ElectionObjectBase):
"""
A PlaintextBallot represents a voters selections for a given ballot and ballot style
:field object_id: A unique Ballot ID that is relevant to the external system
"""

# A unique Ballot ID that is relevant to the external system
object_id: str

# The `object_id` of the `BallotStyle` in the `Election` Manifest
ballot_style: str

Expand All @@ -357,18 +410,16 @@ def is_valid(self, expected_ballot_style_id: str) -> bool:
return True

@dataclass
class CyphertextBallot(Serializable, CryptoHashCheckable):
class CyphertextBallot(ElectionObjectBase, CryptoHashCheckable):
"""
A CyphertextBallot represents a voters encrypted selections for a given ballot and ballot style
When a ballot is in it's complete, encrypted state, the `nonce` is the master nonce
from which all other nonces can be derived to encrypt the ballot. Allong with the `nonce`
fields on `Ballotcontest` and `BallotSelection`, this value is sensitive.
:field object_id: A unique Ballot ID that is relevant to the external system
"""

# A unique Ballot ID that is relevant to the external system
object_id: str

# The `object_id` of the `BallotStyl` in the `Election` Manifest
ballot_style: str

Expand All @@ -386,11 +437,29 @@ class CyphertextBallot(Serializable, CryptoHashCheckable):
nonce: Optional[ElementModQ] = field(default=None)

# the unique ballot tracking id for this ballot
tracking_id: Optional[str] = field(default=None)
tracking_id: str = field(init=False)

def __post_init__(self):
def __post_init__(self) -> None:
self.crypto_hash = self.crypto_hash_with(self.description_hash)

# TODO: Generate Tracking code
self.tracking_id = "abc123"

@property
def hashed_ballot_nonce(self) -> Optional[ElementModQ]:
"""
:return: a hash value derivedd from the description hash, the object id, and the nonce value
suitable for deriving other nonce values on the ballot
"""

if self.nonce is None:
log_warning(f"missing nonce for ballot {self.object_id} could not derive from null nonce")
return None

return hashed_ballot_nonce(
self.description_hash, self.object_id, self.nonce
)

def crypto_hash_with(self, seed_hash: ElementModQ) -> ElementModQ:
"""
Given an encrypted Ballot, generates a hash, suitable for rolling up
Expand Down Expand Up @@ -451,3 +520,12 @@ def is_valid_encryption(self, seed_hash: ElementModQ, elgamal_public_key: Elemen
)
)
return all(valid_proofs)

def hashed_ballot_nonce(extended_base_hash: ElementModQ, ballot_object_id: str, random_master_nonce: ElementModQ) -> ElementModQ:
"""
:return: a hash value derivedd from the description hash, the object id, and the nonce value
suitable for deriving other nonce values on the ballot
"""
return hash_elems(
extended_base_hash, ballot_object_id, random_master_nonce
)

0 comments on commit f9b5fad

Please sign in to comment.