Skip to content

Instantly share code, notes, and snippets.

@markjenkins
Last active February 24, 2021 00:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save markjenkins/4a76817c741aea3639af276b2bd20fed to your computer and use it in GitHub Desktop.
Save markjenkins/4a76817c741aea3639af276b2bd20fed to your computer and use it in GitHub Desktop.
2021-02-23 election
Elected by acclamation
Michael Kozakewich
Kyle Martin
Edwin Amsler
Thor Robinson
Pietra Shirley
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.
-----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-----
#!/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) ) )
#!/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()
#!/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 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