| # Version 3 of Apache STeVe |
| |
| ## History |
| |
| v1 was a set of command-line tools to run voting on a host server, with the |
| people ssh'ing to that server to cast votes. |
| |
| v2 was a webapp and data server to run the voting process, utilizing emailed |
| tokens as "authentication" for those voting. A later modification used LDAP |
| to look up that token, so that email was not required (which also prevents |
| email interception of a voting token, which anybody could use). |
| |
| v3 is intended (primarily) to revamp the data model and storage and the webui |
| framework, using more recent technologies for greater leverage. |
| |
| For background reference in this README, see the documentation for the |
| [database schema](docs/schema.md) |
| |
| ## Getting Started |
| |
| Read the [Getting Started Guide](docs/quickstart.md) to set up a development |
| environment. |
| |
| ## Data Model |
| |
| v2 is the initial guide for a data model, to be used by v3. |
| |
| The top-level item is an **Election**, and our design-point (in terms of scale) |
| is to manage hundreds of these. Each Election contains some simple metadata. |
| |
| The number of **Persons** is numbered in low thousands, and is the entire union |
| of people who may be eligible to vote in any of the Elections. |
| |
| The **Issues** are the union of all issues across all Elections, and expected to |
| number in tens of thousands. |
| |
| There is a mapping table that specifies which **Persons** are eligible to vote |
| in which **Issues**, which will likely reach low millions. |
| |
| Lastly, there is the set of **Votes** which may reach many millions. |
| |
| **Note**: these are initial scaling estimates, and the underlying SQLite database |
| should easily scale to these levels and beyond. |
| |
| ### Vote Monitors |
| |
| v2 had the notion of a "Vote Monitor" which will not be used in v3. There are |
| no known items to monitor. |
| |
| The owner/creator of an Election will be given a dashboard to view progress, |
| in an anonymized form. |
| |
| ## Hashes and Anonymity |
| |
| The recorded Votes must be as anonymized as possible. The goal is to |
| detach Persons from their recorded Votes on Issues in a given Election. |
| The data "at rest" cannot be decrypted without significant work. |
| |
| It is presumed that the "root" users of the team operating the software would be |
| able to unmask Persons and view their votes. |
| |
| Cryptographic hashes, techniques, and ciphers are used to create anonymity. |
| |
| ## Integrity |
| |
| When an Election is "opened for voting", all metadata, Persons, and Issues, |
| are used to construct a singular hash (`opened_key`) that identifies |
| the precise state of |
| the Election. This hash is used to prevent any post-opening tampering of the |
| Election, the Persons of record, or the ballot. |
| |
| The recorded votes use the `opened_key` to produce an anonymized token |
| for each Person and each Issue, and it is used as part of the vote encryption |
| process. Any attempt to alter the election will produce a new `opened_key` |
| value, implying that any recorded vote becomes entirely useless (the vote |
| can not be matched to a Person, to an Issue, nor decrypted). |
| |
| ## Data at Rest |
| |
| (for details, see **Implementation** below) |
| |
| The recorded votes are encrypted when at rest in the SQLite database. Each |
| vote is recorded using a token (`vote_token`) genearated as a hash of the |
| Person that performed the vote and the issue voted upon. Thus, a cursory |
| examination of the recorded votes will not |
| reveal people's name, nor the issues voted upon. |
| |
| To reveal the votes for computing a final tally of an Issue, the |
| `vote_token` will be reconstructed for each voter, and used to query |
| the corresponding votes for the tally (only most-recent vote used). |
| |
| The votes will be decrypted and fed into the issue's tally |
| function (based on the vote type, eg. yes/no/abstain, or Single |
| Transferable Vote). |
| |
| When a Person loads their ballot, and needs to know which issues have |
| not (yet) been voted upon, then we compute a `vote_token` for each |
| eligible Issue, then look into the **Votes** table for rows. |
| The actual vote does not need to be decrypted for this process. |
| |
| ## Implementation |
| |
| All information/data is recorded within a site-wide SQLite database, |
| using [this schema](docs/schema.md) |
| (see also: [schema.sql](schema.sql)). |
| |
| Some notes on implementation, hashing, storage, at-rest encryption, etc. |
| |
| ``` |
| ElectionID := 40 bits, as 10 hex characters |
| PersonID := availid from iclas.txt # for ASF usage |
| IssueID := 40 bits, as 10 hex characters |
| |
| ElectionData := Tuple[ ElectionID, Title ] |
| IssueData := Tuple[ IssueID, Title, Description, VoteType, VoteOptions ] |
| PersonData := Tuple[ PersonID, Email ] |
| BLOCK := ElectionData + sorted(IssueData) + sorted(PersonData) |
| OpenedKey := Hash(BLOCK, Salt(each-election)) |
| |
| pair = Tuple[ PersonID, IssueID ] |
| votestring = TBD, based on vote type |
| |
| VoteToken = Hash(OpenedKey + PersonID + IssueID, Salt(each-pair)) |
| VoteKey := PBKDF(VoteToken, Salt(each-pair)) |
| Vote := Tuple[ VoteToken, Encrypt(VoteKey, votestring) ] |
| ``` |
| |
| `ElectionID` and `IssueID` are generated 10-character hex values, using |
| `secrets.token_hex(5)` for cryptographic-level entropy. The 10 characters |
| is chosen because these values are visible in URLs and should not be too |
| confusing for humans. At 40 bits, the chance for collision is over a |
| million generated values. When generating a new ID, if a collision |
| actually occurs, then a new ID will be generated and tried. |
| |
| When an **Election** is Opened for voting, the `OpenedKey` is calculated, stored, |
| and used for further work. The `OpenedKey` is primarily used to resist tampering |
| with the ballot definition, and to salt hash of later operations. |
| |
| The `Hash()` function is **Argon2**[^argon2], producing 32 bytes. |
| Note that `Hash()` is |
| computationally/memory intensive, in order to make "unmasking" of votes |
| somewhat costly for **root**. Yet it needs to be reasonable to decrypt |
| the votestrings for final tallying (eg. after ballot-close, **several hours** |
| to decrypt all the votes and perform the tally). |
| |
| The `Salt()` function is `secrets.token_bytes(16)` to produce 16 bytes of |
| cryptographic-level entropy, suitable for use by the Argon2 hash functions. |
| The salt values should never be transmitted. |
| |
| `Encrypt()` and `Decrypt()` are a **symmetric** encryption algorithm, |
| so that votestrings can be recovered. This will |
| be implemented using the `Fernet` system[^fernet] in the `cryptography` Python |
| package. Note that Argon2 produces 32 byte hash values, which matches |
| the 32 bytes needed for a Fernet key. |
| |
| ### Storage and Transmission |
| |
| **IMPORTANT**: the `VoteToken` should never be |
| stored in a way that ties them to the PersonID and IssueID. The |
| `VoteKey` should never be stored. Instead, the `Salt()` values |
| are stored, and the token and key are computed when needed. |
| |
| In general, the expense of the `Hash()` function should not be short-circuited |
| by storing the result. Any attacker must perform the work. During normal |
| operation of the voting system, each call of the `Hash()` function should be |
| within human-reasonable time limits (but unreasonable to perform in bulk). |
| |
| Note that `VoteToken` is stored as part of each `Vote`, |
| but that token provides no mapping back to a Person or Issue. |
| |
| The `ElectionID` and `IssueID` are visible to users, and will be encoded |
| as hex digits to make them relatively human-consumable. |
| |
| ### Entropy |
| |
| There is high-entropy in the following values: `ElectionID`, `UserID`, |
| `VoteToken`, the two salts, and the computed (never-stored) `VoteKey`. |
| |
| The `PersonID` is considered low-entropy, as it is likely a username. |
| |
| Low-entropy implies a threat vector, where an attacker could use various |
| techniques to try "all values". However, it is combined into the |
| `VoteToken` with the 40-bit high-entropy `IssueID`, the 256-bit |
| high-entropy OpenedKey, and a 128-bit high-entropy salt value. |
| |
| The `VoteKey` is a key-stretched `VoteToken` and also considered as |
| high-entropy and infeasible to crack. |
| |
| ### (Re)Tally Process |
| |
| Querying the set of Issues for those associated with an ElectionID is |
| straight-forward. |
| |
| To tally a specific issue: |
| |
| 1. For each Person eligible to vote on this issue, compute a `VoteToken` |
| 2. Find the **most-recent** vote using the `VoteToken` |
| 3. Decrypt the ciphertext to produce the original `votestring` |
| 4. Feed these votes into the tally mechanism for the Issue's vote type. |
| |
| ## API Documentation |
| |
| This is _TBD_ |
| |
| A basic example of using the API is available via the |
| [code coverage testing script](tests/check_coverage.py). |
| |
| ## Testing |
| |
| See [tests/README.md](tests/README.md) for details on testing the codebase. |
| |
| ## Threat Model |
| |
| There are two primary threat vectors that can compromise the cryptographic |
| records of elections, people, issues, and their votes: |
| |
| 1. **root** on the system |
| 2. Remote Code Execution (RCE) that can surface necessary rows from the database |
| |
| [^fernet]: https://cryptography.io/en/latest/fernet/ |
| [^argon2]: https://passlib.readthedocs.io/en/stable/lib/passlib.hash.argon2.html |