2021-02-23 election
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Elected by acclamation | |
Michael Kozakewich | |
Kyle Martin | |
Edwin Amsler | |
Thor Robinson | |
Pietra Shirley |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Ninjas | |
Pirates | |
Clowns |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Greeting Skullspace members and observers, | |
I am the election officer for the Skullspace AGM to be held Tuesday | |
February 23, 2021 at 6pm Winnipeg time. | |
There are no bylaw amendments on the agenda this year. | |
I will be sending out voting codes to eligible voters by email this | |
evening. If you do not receive a voting code by Monday morning, please | |
be in touch with myself <mark@parit.ca> and Michael Kozakewich | |
<michael.kozakewich@skullspace.ca> . | |
There are five positions to fill on our board of directors. You may vote | |
for up to 5 candidates. | |
Nominations will close at the start of the meeting shortly after 6pm. | |
I'm tracking known nominees at: | |
https://gist.github.com/markjenkins/4a76817c741aea3639af276b2bd20fed#file-2021-02-23_boardcandidates-txt | |
This will be updated at the close of nominations. | |
You can simply vote by emailing me back your voting code (like past | |
elections), or by following the more complicated protocol and channels | |
detailed below which will allow me to irrevocably blind myself from who | |
votes for who. This way, no-one can ever compel me to disclose those votes. | |
Votes for board candidates will be accepted until 8pm Winnipeg time, | |
after which I will count ballots. | |
If we do not have more than 5 nominees, the candidates will win their | |
positions by acclamation. For your amusement, I will conduct a mock | |
election where you may pick up to two of the following candidates: | |
Ninjas | |
Pirates | |
Clowns | |
The mock election will close at 7pm. | |
----------- | |
Blinding protocol instructions: | |
The opt-in blinding voting protocol I have developed is documented at | |
https://gist.githubusercontent.com/markjenkins/4a76817c741aea3639af276b2bd20fed/raw/skullspace_election_protocol_MarkJenkins_version_2.txt | |
You don't really need to read all that to help me achieve blinding on | |
your vote. | |
Just do these two steps | |
1) Between now and 5:30pm on election day, use your voting code to run | |
this interactive python3 program: | |
https://gist.githubusercontent.com/markjenkins/4a76817c741aea3639af276b2bd20fed/raw/new_subkeys_2021-02-23.py | |
Email me the two "rekey:" lines and two "rekeysig" lines that you | |
generate. (sub-key derivation string and signature) | |
Feel free to encrypt with my GPG key. | |
Don't lose the "rekey:" lines. If you do accidentally lose them, contact | |
me by 5:30pm 2021-02-23. | |
At 5:45pm I will be generating sub-keys with these and deleting the | |
derivation text you will have sent me. | |
2) Whenever you're ready to vote (before or after nominations close...), | |
download the two nominee lists | |
https://gist.githubusercontent.com/markjenkins/4a76817c741aea3639af276b2bd20fed/raw/2021-02-23_boardcandidates.txt | |
https://gist.githubusercontent.com/markjenkins/4a76817c741aea3639af276b2bd20fed/raw/2021-02-23_mockcandidates.txt | |
And run this interactive python3 program in the same directory | |
https://gist.githubusercontent.com/markjenkins/4a76817c741aea3639af276b2bd20fed/raw/hmac_vote_2021-02-23.py | |
Send me any "ballot:" output lines. (there will be one "ballot:" per | |
candidate voted for) | |
You can send your ballot: code through an insecure, unauthenticated channel: | |
1) unencrypted email | |
2) Skullspace slack #general or PM me on SKSP slack | |
3) #skullspace on Freenode (IRC) or PM to markjenkinsznc | |
4) Contact form on my website https://markjenkins.ca/contact/ between | |
4pm and 8pm. | |
(I get too much spam to check outside of that) | |
5) Through a trusted friend. (get them to use a public channel so you | |
know the message got through) | |
There are ways to use these things to ensure that I don't even know who | |
voted, let alone for who. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-----BEGIN PGP SIGNATURE----- | |
iQEzBAABCAAdFiEEHe6TzNol+KP541epqPhkk6pNsfsFAmAy51kACgkQqPhkk6pN | |
sft2vwf+JPkbIQedUl2+YQPyBN4U2CkbUbh41rpBFbix+/BpYGxktlRisxdpwmDo | |
ZFhTvSR6wSCICw61simZHnq70nri//Sp/EmoMhryL7FmQDn2dCK00ecFEo7fMpoQ | |
E3BmKB6PGnzINbglY08zvB9nkICPAYQFqiHQ09imUO0+baRQw4zwOPqj+s2GN+hC | |
0hggtnZLxEpoHu6HfsbOlrhxqikgX3lDpe+iFMeJAjpRW3kmK1kRWeuGvWUpoZuS | |
ifSZDsEQp2ke2Cbc5m8Lmlh6EHMOdk2wAAjVWN7iQDT+kgLJBNwEfjFYR6kSe3jm | |
SEFmOlZBPm1uLFGP4qO0Y2s+F8pefw== | |
=FmZd | |
-----END PGP SIGNATURE----- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from __future__ import print_function | |
from __future__ import division | |
from random import SystemRandom | |
from base64 import b64encode | |
from sys import argv, version_info | |
if version_info[0] >= 3: | |
def eight_bit_int_to_byte(n): | |
return int.to_bytes(n, 1, 'big') | |
else: | |
eight_bit_int_to_byte = chr | |
KEY_SIZE = 64 # 64 bits | |
assert( KEY_SIZE % 8 == 0 ) # KEY_SIZE must be a multiple of 8 bits | |
sysrandom = SystemRandom() | |
def generate_voting_code(): | |
random_64_bits_as_bytes = b''.join( | |
eight_bit_int_to_byte(sysrandom.getrandbits(8)) | |
for i in range(KEY_SIZE//8) ) | |
return "masterkey:" + b64encode(random_64_bits_as_bytes).decode('ascii') | |
n_codes = 1 if len(argv) < 2 else int(argv[1]) | |
print( '\n'.join( generate_voting_code() for i in range(n_codes) ) ) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
BOARD_ELECTION_VOTE_LIMIT = 5 # at most 5 positions on the board | |
MOCK_ELECTION_VOTE_LIMIT = 2 | |
ELECTIONS = ( | |
('2021-02-23 board', '2021-02-23_boardcandidates.txt', | |
[], BOARD_ELECTION_VOTE_LIMIT), | |
('2021-02-23 mock', '2021-02-23_mockcandidates.txt', | |
[], MOCK_ELECTION_VOTE_LIMIT), | |
) | |
SHORT_MASTER_KEY_LENGTH = 8 # 64 bits, 8 bytes | |
SUB_KEY_SIG_TRUNCATE = 8 # 64 bits, 8 bytes | |
BALLOT_SIZE_BYTES = 8 | |
MASTERKEY_PREFIX = "masterkey:" | |
REKEY_PREFIX = "rekey:" | |
BALLOT_PREFIX = "ballot:" | |
from random import SystemRandom | |
import hmac | |
import hashlib | |
import base64 | |
import binascii | |
import sys | |
from os.path import exists | |
if sys.version_info[0] < 3: | |
print("Python 3 required. (tested on 3.5.2)") | |
exit(1) | |
# load candidate lists into the empty lists ELECTIONS[0][2] | |
# and ELECTIONS[1][2] above | |
for election, candidate_list_filename, candidate_list, limit in ELECTIONS: | |
if not exists(candidate_list_filename): | |
print( | |
"%s was not found in current working directory" % | |
candidate_list_filename) | |
exit(1) | |
with open(candidate_list_filename) as f: | |
candidate_list.extend( line.strip() for line in f ) | |
SystemRandom().shuffle(candidate_list) | |
def create_code_signed_ballot(sub_key, vote): | |
ballot_hmac = hmac.new(sub_key, vote.encode('UTF-8'), | |
digestmod=hashlib.sha256) | |
base_64_bytes = base64.b64encode(ballot_hmac.digest()[0:BALLOT_SIZE_BYTES]) | |
return BALLOT_PREFIX + base_64_bytes.decode('ascii') | |
def valid_candidate(candidate_selection, | |
candidates, candidate_lookup_by_number): | |
if candidate_selection in candidates: | |
return candidate_selection | |
else: | |
try: | |
candidate_number = int(candidate_selection) | |
except ValueError: | |
return None | |
else: # only if conversion of candidate_selection to int works | |
if candidate_number in candidate_lookup_by_number: | |
return candidate_lookup_by_number[candidate_number] | |
return None | |
def get_valid_votes(candidates, max_votes): | |
candidates_enumerated = list(enumerate(candidates, 1)) | |
candidate_lookup_by_number = dict( candidates_enumerated ) | |
print("Vote for up to %d candidates by matching name or number, " | |
"seperated by comma (',')" % max_votes) | |
print("write-in ballots are not supported by this interface") | |
print() | |
while True: | |
print() # blank line | |
for i, candidate in candidates_enumerated: | |
print("%d) %s" % (i, candidate)) | |
candidate_selection_w_newline = input("Who are you voting for? > ") | |
candidate_selection = candidate_selection_w_newline.strip() | |
# split up the candidate selection by comma | |
selected_candidates = candidate_selection.split(",") | |
# ask again if too many were voted for or none were | |
if len(selected_candidates) > max_votes or len(selected_candidates) <1: | |
continue | |
# remove any whitespace | |
selected_candidates = [ c.strip() for c in selected_candidates ] | |
# validate each candidate selection, replacing with None if | |
# invalid, and replacing any numbers with names | |
selected_candidates = [ | |
valid_candidate(c, candidates, candidate_lookup_by_number) | |
for c in selected_candidates] | |
# verify all selected candidates came out okay, no None | |
if all( c !=None for c in selected_candidates ): | |
return selected_candidates | |
def get_valid_master_code(): | |
while True: | |
code_w_newline = input("What's your master code/key? > ") | |
mastercode_ascii = code_w_newline.strip() | |
if mastercode_ascii.startswith(MASTERKEY_PREFIX): | |
# remove masterkey: prefix | |
mastercode_ascii = mastercode_ascii[len(MASTERKEY_PREFIX):] | |
prefix_seen = True | |
else: | |
prefix_seen = False | |
try: | |
decoded_code = base64.b64decode(mastercode_ascii, validate=True) | |
except binascii.Error: | |
print("invalid code") | |
else: | |
if len(decoded_code) == SHORT_MASTER_KEY_LENGTH: | |
break | |
else: | |
print("invalid code") | |
print("Your code is:") | |
print(mastercode_ascii) | |
print() | |
return decoded_code | |
def get_election_choice(): | |
while True: | |
print("Which election are you voting in?") | |
# start enumeration at 1 so they are numbered 1), 2)... | |
for i, (election_name, a, b, c) in enumerate(ELECTIONS, 1): | |
print("%d) %s" % (i, election_name) ) | |
try: | |
election_choice = int(input("> ")) | |
except ValueError: | |
print() | |
else: | |
if 1<= election_choice <= len(ELECTIONS): | |
# election_choice-1 because the UI enumerates from 1) | |
choice_of_election = ELECTIONS[election_choice-1] | |
return ( | |
choice_of_election[0], # election name | |
choice_of_election[2], # candidate list | |
choice_of_election[3], # vote limit | |
) # end tuple | |
def get_valid_sub_key_derivation_string(election_name): | |
print("If you provided the election officer a sub-key derivation string " | |
"add it here. Otherwise, leave this blank (hit enter) or " | |
"type default") | |
print() | |
rekey_prefix = REKEY_PREFIX + election_name + ' ' | |
print("""accepted input formats for non-default are: | |
%sYOUR_DERIVATION_STRING | |
or | |
YOUR_DERIVATION_STRING | |
""" % rekey_prefix) | |
while True: | |
sub_key_derivation_string = input( | |
"sub-key derivation string > ").strip() | |
if ( sub_key_derivation_string.startswith(rekey_prefix) and | |
len(sub_key_derivation_string) > len(rekey_prefix) ): | |
return sub_key_derivation_string[ len(rekey_prefix): ] | |
elif sub_key_derivation_string in ("", "default", election_name): | |
return election_name | |
else: | |
return sub_key_derivation_string | |
def main(): | |
election_name, candidates, vote_limit = get_election_choice() | |
master_key_64 = get_valid_master_code() | |
master_key_256_sha = hashlib.sha256(master_key_64) | |
master_key_256 = master_key_256_sha.digest() | |
subkey_derivation_string = get_valid_sub_key_derivation_string( | |
election_name) | |
print() | |
if subkey_derivation_string == election_name: | |
print("you picked the default sub-key derivation string") | |
else: | |
print("your custom sub-key derivation string was") | |
print(subkey_derivation_string) | |
rekey_msg = ( | |
REKEY_PREFIX + election_name + ' ' + subkey_derivation_string) | |
sub_key_hmac = hmac.new( | |
master_key_256, rekey_msg.encode('ascii'), hashlib.sha256) | |
sig_64 = sub_key_hmac.digest()[0:SUB_KEY_SIG_TRUNCATE] | |
print("your signature when you provided this to the election officer " | |
"was: ") | |
print("rekeysig:" + base64.b64encode(sig_64).decode('ascii')) | |
print() | |
sub_key_sha256 = master_key_256_sha.copy() | |
sub_key_sha256.update(subkey_derivation_string.encode('ascii')) | |
sub_key = sub_key_sha256.digest() | |
votes = get_valid_votes(candidates, vote_limit) | |
print("your ballots are:") | |
for vote in votes: | |
code_signed_ballot = create_code_signed_ballot(sub_key, vote) | |
print(code_signed_ballot) | |
if __name__ == "__main__": | |
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
ELECTIONS = ('2021-02-23 board', '2021-02-23 mock') | |
MASTERKEY_PREFIX = "masterkey:" | |
SHORT_MASTER_KEY_LENGTH = 8 # 64 bits, 8 bytes | |
SUB_KEY_DERIVE_LENGTH = 256//8 # 256 bits, 32 bytes | |
SUB_KEY_SIG_TRUNCATE = 8 # 64 bits, 8 bytes | |
import sys | |
import hmac | |
import binascii | |
from random import SystemRandom | |
from base64 import b64encode, b64decode | |
from hashlib import sha256 | |
if sys.version_info[0] < 3: | |
print("Python 3 is required to run this script. Tested on 3.5.2") | |
exit(1) | |
def eight_bit_int_to_byte(n): | |
return int.to_bytes(n, 1, 'big') | |
def rand_bytes(num_bytes): | |
return b''.join( | |
eight_bit_int_to_byte(sysrandom.getrandbits(8)) | |
for i in range(num_bytes)) | |
sysrandom = SystemRandom() | |
mastercode_ascii = input( | |
"enter your master voting code (%s prefix) > " % MASTERKEY_PREFIX) | |
mastercode_ascii = mastercode_ascii.strip() | |
if mastercode_ascii.startswith(MASTERKEY_PREFIX): | |
# remove masterkey: prefix | |
mastercode_ascii = mastercode_ascii[len(MASTERKEY_PREFIX):] | |
prefix_seen = True | |
else: | |
prefix_seen = False | |
try: | |
master_key_64 = b64decode(mastercode_ascii, validate=True) | |
except binascii.Error: | |
print("invalid master voting code") | |
exit(1) | |
if len(master_key_64) != SHORT_MASTER_KEY_LENGTH: | |
print("invalid master voting code length") | |
exit(1) | |
if not prefix_seen: | |
print("master voting code accepted without %s prefix" % MASTERKEY_PREFIX) | |
master_key_256_sha = sha256(master_key_64) | |
master_key_256 = master_key_256_sha.digest() | |
for election in ELECTIONS: | |
sub_key_derive_bytes = rand_bytes(SUB_KEY_DERIVE_LENGTH) | |
sub_key_derive_string = b64encode(sub_key_derive_bytes).decode('ascii') | |
# uncomment this to test that | |
# rekey:2021-02-23 mock 7OJyWgwvhS8= | |
# results in | |
# rekeysig:35+MYeBRF4E= | |
#sub_key_derive_string = '7OJyWgwvhS8=' | |
rekey_msg = "rekey:%s %s" %(election, sub_key_derive_string) | |
print(rekey_msg) | |
sub_key_hmac = hmac.new(master_key_256, rekey_msg.encode('ascii'), sha256) | |
sig_64 = sub_key_hmac.digest()[0:SUB_KEY_SIG_TRUNCATE] | |
print("rekeysig:" + b64encode(sig_64).decode('ascii')) | |
print(""" | |
Send the above rekey: and rekeysig: lines to """ | |
"""Mark Jenkins <mark@parit.ca> prior to 2021-02-23 5:30pm. | |
You must retain the rekey values for your records in order to vote. | |
If you lose them prior to 5:45pm Winnipeg time 2021-02-23, contact Mark. | |
If you lose them after that you're out of luck, Mark will be blind to """ | |
"""which re-keyed sub-keys belong to which voters. | |
At your option, you can encrypt emails to Mark with GPG key 0xA8F86493AA4DB1FB | |
http://keys.gnupg.net/pks/lookup?search=0xA8F86493AA4DB1FB&fingerprint=on&op=index | |
https://markjenkins.ca/gpg/ | |
""") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This is a description of version 2 of the skullspace election protocol while Mark Jenkins is election officer. | |
The election officer emails random codes to each eligible voter. | |
A voter can simply vote by providing their code and who/what they are voting for in any form of correspondence with the election officer. | |
This is allowed, but it makes it easy for the election officer to know who was voted for. | |
The purpose of this protocol is to make it easier for the election officer to blind themselves from knowing who was voted for, as long as there is also co-operation from the voters who want to help that along. | |
Furthermore, blinding can protect the electoral officer from being compelled to reveal who voted for who/what. | |
The protocol does not assure the voters that the election officer is blind though, constructing such a system is harder and out of scope in this election. It's up to the election officer to do their part in blinding. | |
In version 1 of the protocol, used for the summer 2020 byelection, a candidate was named as a message and a hmac signature was constructed using the voting code as the key and the correspondence between keys and voters was removed. The keys/codes issued were thus only useful for that election. | |
In version 2, there are four main changes: | |
1) The keys sent to voters are issued to be indefinite for communication between voter and election officer Mark Jenkins, covering subsequent elections. | |
2) A procedure exists for deriving and setting an election specific sub-key. | |
3) All code types will have an identifying prefix | |
4) Shorter codes | |
The correspondence between keys and voters will be retained for subsequent elections. (In an encrypted file available only to the electoral officer) | |
Such keys will not be handed off to a successor election officer. Re-keying between voter and election officer is possible prior to and after the election. (Same keys will not be re-sent out, lost key will lead to a new key) | |
The key / code sent to voters are shortened to 64bits. To identify them, they will have a prefix "masterkey:" The keys themselves will still be base64 encoded. | |
An example key that we will use throughout here is | |
masterkey:jxpJx5uErQ8= | |
In hexadecimal, the same key is: | |
8F 1A 49 C7 9B 84 AD 0F | |
(note, the masterkey: prefix isn't part of this hex representation) | |
When used in the subsequent algorithms, these masterkeys should be stretched to 256 bits with the sha256 hash for consistency with subsequent 256 bit primitives before performing any other operations. | |
So our example 64 bit master key stretches to: | |
4B D4 02 9D 91 99 F4 E9 35 4A 90 46 69 4B 7C 23 DB CB 1F 22 22 48 8E 3C F1 D9 9F 0E 9C 49 1D 14 | |
(hex) | |
or | |
S9QCnZGZ9Ok1SpBGaUt8I9vLHyIiSI488dmfDpxJHRQ= | |
(base64 with a prefix) | |
For each election, the election officer will publish a default derivation path from master key to sub-key. The derivation path will be an ascii string (of printable characters) describing the relevant election. | |
For example, the 2021 board election default derivation path will be | |
'2021-02-23 board' | |
(quotes excluded) | |
If we don't have a board election due to the number of candidates, the default derivation path for the mock election will be | |
'2021-02-23 mock' | |
(quotes excluded) | |
The sub-key for each voter will consist of the sha256 digest that resulted from key stretching being updated with the derivation path, encoded as ascii. | |
Python 3 example: | |
from hashlib import sha256 | |
from base64 import b64decode | |
master_key_64 = b64decode('jxpJx5uErQ8=') | |
master_key_256_sha = sha256(master_key_64) | |
print(master_key_256_sha.digest().hex().upper()) | |
# 4BD4029D9199F4E9354A9046694B7C23DBCB1F2222488E3CF1D99F0E9C491D14 | |
sub_key_board = master_key_256_sha.copy() | |
sub_key_board.update('2021-02-23 board'.encode('ascii')) | |
print(sub_key_board.digest().hex().upper()) | |
# A0FCD32E68936B0E54A74226D1CA02A68042E805C39A78D87A2A1199610C1763 | |
sub_key_mock = master_key_256_sha.copy() | |
sub_key_mock.update('2021-02-23 mock'.encode('ascii')) | |
print(sub_key_mock.digest().hex().upper()) | |
# 16FD00196A7DFFFE5B982B4B89BBF6BA5FF5B0FD3CC71C50812F1BDB402B8A8F | |
The 2021-02-23 election does not include any bylaw amendment resolutions. | |
In the future, we may be electing a board and passing bylaw amendments. In that case, each bylaw amendment will be treated as a separate election with its own default sub-key derivation string, e.g. | |
'2000-01-05 amendment #1' | |
'2000-01-05 amendment #2' | |
Shortly before the election meeting is convened, the electoral officer will generate a list of sub-keys for all voters. One list per matter up for election. The list will be cryptographically shuffled. As such, there won't be a visible connection between sub-keys and the master keys that can be looked up casually. | |
But, with programming, an electoral officer could easily reconnect the sub-keys to their original master keys and eligible voters by re-generating the sub-keys with the master keys and assigned persons noted. | |
Therefore, we give the voters the option to designate their own sub-key derivation prior to the election. Receiving these re-keyings prior to the sub-key generation allows the electoral officer to delete all connection to the master key at the time of sub-key generation. (the blinding process) | |
A voter can use their master key to hmac sign this re-keying operation. | |
The voter sends a message prefixed with "rekey:" followed by the original derivation string for the relevant election, followed by a space, followed by additional, printable ascii characters which will be their own personal derivation string | |
Example | |
rekey:2021-02-23 mock 7OJyWgwvhS8= | |
The voter signs this rekeying operation by using the sha256 hmac operation, with their master key as the key and the message (including the rekey prefix), encoded to bytes with ascii. The output of that operation is 256 bits. We truncate to first the 64 bits (8 bytes), encode with base64 and transmit with a rekeysig: prefix. | |
So, for the voter with master key jxpJx5uErQ8=, who wants to use an alternate derivation string 7OJyWgwvhS8= for the 2021-02-23 mock election, they transmit | |
rekey:2021-02-23 mock 7OJyWgwvhS8= | |
and | |
rekeysig:35+MYeBRF4E= | |
#!/usr/bin/env python3 | |
import hmac | |
from hashlib import sha256 | |
from base64 import b64decode, b64encode | |
master_key_64 = b64decode('jxpJx5uErQ8=') | |
master_key_256_sha = sha256(master_key_64) | |
master_key_256 = master_key_256_sha.digest() | |
h = hmac.new(master_key_256, 'rekey:2021-02-23 mock 7OJyWgwvhS8='.encode('ascii'), sha256) | |
sig_64 = h.digest()[0:8] | |
print("rekeysig:" + b64encode(sig_64).decode('ascii')) | |
Casting of ballots is generating an hmac signature with the derived sub-key (default or rekey) and a UTF-8 encoding of a candidate name. (The choice of UTF-8 allows for non-ascii characters to be part of a candidate name) | |
Truncate the generated signature to the first 64 bits (8 bytes), encode with base64, and prefix with "ballot:". | |
The ballot code can be transmitted through any channel approved by the electoral officer. This can include untrusted channels and even persons proxying. | |
A typical board of directors election will involve greater than 5 candidates, but only being allowed to vote for up to 5. (greater than N candidates, <= N ballots cast per voter). Cast a seperate ballot for each candidate you are voting for: | |
For example, the candidates for the 2021-02-23 mock election are | |
Ninjas | |
Pirates | |
Clowns | |
and you may only vote for 2. | |
#!/usr/bin/env python3 | |
import hmac | |
from hashlib import sha256 | |
from base64 import b64decode, b64encode | |
master_key_64 = b64decode('jxpJx5uErQ8=') | |
master_key_256_sha = sha256(master_key_64) | |
sub_key_mock_sha256 = master_key_256_sha.copy() | |
sub_key_mock_sha256.update('2021-02-23 mock'.encode('ascii')) | |
sub_key_mock = sub_key_mock_sha256.digest() | |
pirates_ballot = hmac.new(sub_key_mock, "Pirates".encode("UTF-8"), sha256) | |
pirates_ballot_64 = pirates_ballot.digest()[0:8] | |
ninjas_ballot = hmac.new(sub_key_mock, "Ninjas".encode("UTF-8"), sha256) | |
ninjas_ballot_64 = ninjas_ballot.digest()[0:8] | |
print("ballot:" + b64encode(pirates_ballot_64).decode('ascii')) | |
print("ballot:" + b64encode(ninjas_ballot_64).decode('ascii')) | |
ballot:s6p+KaQuEj4= | |
ballot:N+LP6Cvko64= | |
If we hold a mock election, write-in candidates will also be allowed. Convey the name before the ballot code separated by a space | |
Elon ballot:Lx/Ld5yxVZc= | |
The electoral officer may report some of the write-in submissions after the election at their discretion. | |
If we hold a real election, there will be a fixed candidate list published at the time of nominations closing with the official spelling for each candidate. This will be a UTF-8 encoded text file with newline septation. | |
Technically in the real election, write-in of last minute nominees is also possible, but discouraged. If anyone does this the electoral officer will have to take additional care to ensure no-one gets multiple votes from the same voter with alternate name spelling, such as writing in both "Elon" and "E Musk". A distinction will also have to be made between duly nominated write-ins and those who are not. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment