Implement ballot auditing 🎉

This commit is contained in:
RunasSudo 2017-11-28 22:43:32 +11:00
parent 39246d5df0
commit ca542f9d9e
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
11 changed files with 323 additions and 28 deletions

View File

@ -35,6 +35,16 @@ class Ballot(EmbeddedObject):
election_id = UUIDField() election_id = UUIDField()
election_hash = StringField() election_hash = StringField()
answers = EmbeddedObjectListField(is_hashed=False)
def deaudit(self):
encrypted_answers_deaudit = EosList()
for i in range(len(self.encrypted_answers)):
encrypted_answers_deaudit.append(self.encrypted_answers[i].deaudit())
return Ballot(encrypted_answers=encrypted_answers_deaudit, election_id=self.election_id, election_hash=self.election_hash)
class Vote(EmbeddedObject): class Vote(EmbeddedObject):
ballot = EmbeddedObjectField() ballot = EmbeddedObjectField()
cast_at = DateTimeField() cast_at = DateTimeField()
@ -96,7 +106,7 @@ class ApprovalQuestion(Question):
max_choices = IntField() max_choices = IntField()
def pretty_answer(self, answer): def pretty_answer(self, answer):
return ', '.join([self.choices[choice] for choice in answer.choices]) return ', '.join([self.choices[answer.choices[i]] for i in range(len(answer.choices))])
class ApprovalAnswer(Answer): class ApprovalAnswer(Answer):
choices = ListField(IntField()) choices = ListField(IntField())
@ -107,7 +117,7 @@ class PreferentialQuestion(Question):
max_choices = IntField() max_choices = IntField()
def pretty_answer(self, answer): def pretty_answer(self, answer):
return ', '.join([self.choices[choice] for choice in answer.choices]) return ', '.join([self.choices[answer.choices[i]] for i in range(len(answer.choices))])
class PreferentialAnswer(Answer): class PreferentialAnswer(Answer):
choices = ListField(IntField()) choices = ListField(IntField())

View File

@ -130,6 +130,8 @@ class BigInt(EosObject):
@classmethod @classmethod
def deserialise(cls, value): def deserialise(cls, value):
if value is None:
return None
return cls(value) return cls(value)
# Returns a random BigInt from lower_bound to upper_bound, both inclusive # Returns a random BigInt from lower_bound to upper_bound, both inclusive

View File

@ -51,6 +51,8 @@ class BigInt(EosObject):
@classmethod @classmethod
def deserialise(cls, value): def deserialise(cls, value):
if value is None:
return None
return cls(value) return cls(value)
# Returns a random BigInt from lower_bound to upper_bound, both inclusive # Returns a random BigInt from lower_bound to upper_bound, both inclusive

View File

@ -56,36 +56,48 @@ class EGPublicKey(EmbeddedObject):
return self.group.q.nbits() - 1 return self.group.q.nbits() - 1
# HAC 8.18 # HAC 8.18
def _encrypt(self, message): def _encrypt(self, message, randomness=None):
if message <= ZERO: if message <= ZERO:
raise Exception('Invalid message') raise Exception('Invalid message')
if message >= self.group.p: if message >= self.group.p:
raise Exception('Invalid message') raise Exception('Invalid message')
if randomness is None:
# Choose an element 1 <= k <= p - 2 # Choose an element 1 <= k <= p - 2
k = BigInt.crypto_random(ONE, self.group.p - TWO) k = BigInt.crypto_random(ONE, self.group.p - TWO)
else:
k = randomness
gamma = pow(self.group.g, k, self.group.p) gamma = pow(self.group.g, k, self.group.p)
delta = (message * pow(self.X, k, self.group.p)) % self.group.p delta = (message * pow(self.X, k, self.group.p)) % self.group.p
return EGCiphertext(public_key=self, gamma=gamma, delta=delta) return EGCiphertext(public_key=self, gamma=gamma, delta=delta, m0=message, randomness=k)
# Adida 2008 # Adida 2008
def encrypt(self, message): def message_to_m0(self, message):
m0 = message + ONE
if pow(m0, self.group.q, self.group.p) == ONE:
# m0 is already in G_q
return m0
else:
# For the life of me I can't find any reputable references for this aside from Adida 2008...
m0 = (-m0) % self.group.p
return m0
def m0_to_message(self, m0):
if m0 < self.group.q:
return m0 - ONE
else:
return ((-m0) % self.group.p) - ONE
def encrypt(self, message, randomness=None):
if message < ZERO: if message < ZERO:
raise Exception('Invalid message') raise Exception('Invalid message')
if message >= self.group.q: if message >= self.group.q:
raise Exception('Invalid message') raise Exception('Invalid message')
m0 = message + ONE return self._encrypt(self.message_to_m0(message), randomness)
if pow(m0, self.group.q, self.group.p) == ONE:
# m0 is already in G_q
return self._encrypt(m0)
else:
# For the life of me I can't find any reputable references for this aside from Adida 2008...
m0 = (-m0) % self.group.p
return self._encrypt(m0)
class EGPrivateKey(EmbeddedObject): class EGPrivateKey(EmbeddedObject):
pk_class = EGPublicKey pk_class = EGPublicKey
@ -118,16 +130,16 @@ class EGPrivateKey(EmbeddedObject):
pt = (gamma_inv * ciphertext.delta) % self.public_key.group.p pt = (gamma_inv * ciphertext.delta) % self.public_key.group.p
# Undo the encryption mapping # Undo the encryption mapping
if pt < self.public_key.group.q: return self.public_key.m0_to_message(pt)
return pt - ONE
else:
return ((-pt) % self.public_key.group.p) - ONE
class EGCiphertext(EmbeddedObject): class EGCiphertext(EmbeddedObject):
public_key = EmbeddedObjectField(EGPublicKey) public_key = EmbeddedObjectField(EGPublicKey)
gamma = EmbeddedObjectField(BigInt) # G^k gamma = EmbeddedObjectField(BigInt) # G^k
delta = EmbeddedObjectField(BigInt) # M X^k delta = EmbeddedObjectField(BigInt) # M X^k
randomness = EmbeddedObjectField(BigInt, is_hashed=False)
m0 = EmbeddedObjectField(BigInt, is_hashed=False)
def reencrypt(self, k=None): def reencrypt(self, k=None):
# Generate an encryption of one # Generate an encryption of one
if k is None: if k is None:
@ -137,11 +149,21 @@ class EGCiphertext(EmbeddedObject):
return EGCiphertext(public_key=self.public_key, gamma=((self.gamma * gamma) % self.public_key.group.p), delta=((self.delta * delta) % self.public_key.group.p)), k return EGCiphertext(public_key=self.public_key, gamma=((self.gamma * gamma) % self.public_key.group.p), delta=((self.delta * delta) % self.public_key.group.p)), k
def deaudit(self):
return EGCiphertext(public_key=self.public_key, gamma=self.gamma, delta=self.delta)
def is_randomness_valid(self):
ct = self.public_key._encrypt(self.m0, self.randomness)
return ct.gamma == self.gamma and ct.delta == self.delta
# Signed ElGamal per Schnorr & Jakobssen # Signed ElGamal per Schnorr & Jakobssen
class SEGPublicKey(EGPublicKey): class SEGPublicKey(EGPublicKey):
def _encrypt(self, message): def _encrypt(self, message, randomness=None):
if randomness is None:
# Choose an element 1 <= k <= p - 2 # Choose an element 1 <= k <= p - 2
r = BigInt.crypto_random(ONE, self.group.p - TWO) r = BigInt.crypto_random(ONE, self.group.p - TWO)
else:
r = randomness
s = BigInt.crypto_random(ONE, self.group.p - TWO) s = BigInt.crypto_random(ONE, self.group.p - TWO)
gamma = pow(self.group.g, r, self.group.p) # h gamma = pow(self.group.g, r, self.group.p) # h
@ -151,7 +173,7 @@ class SEGPublicKey(EGPublicKey):
z = s + c*r z = s + c*r
return SEGCiphertext(public_key=self, gamma=gamma, delta=delta, c=c, z=z) return SEGCiphertext(public_key=self, gamma=gamma, delta=delta, c=c, z=z, m0=message, randomness=r)
class SEGPrivateKey(EGPrivateKey): class SEGPrivateKey(EGPrivateKey):
pk_class = SEGPublicKey pk_class = SEGPublicKey
@ -167,6 +189,9 @@ class SEGCiphertext(EGCiphertext):
return self.c == c return self.c == c
def deaudit(self):
return SEGCiphertext(public_key=self.public_key, gamma=self.gamma, delta=self.delta, c=self.c, z=self.z)
class Polynomial(EmbeddedObject): class Polynomial(EmbeddedObject):
coefficients = EmbeddedObjectListField(BigInt) # x^0, x^1, ... x^n coefficients = EmbeddedObjectListField(BigInt) # x^0, x^1, ... x^n
modulus = EmbeddedObjectField(BigInt) modulus = EmbeddedObjectField(BigInt)

View File

@ -46,6 +46,14 @@ class BlockEncryptedAnswer(EncryptedAnswer):
return obj return obj
def deaudit(self):
blocks_deaudit = EosList()
for i in range(len(self.blocks)):
blocks_deaudit.append(self.blocks[i].deaudit())
return BlockEncryptedAnswer(blocks=blocks_deaudit)
class Trustee(EmbeddedObject): class Trustee(EmbeddedObject):
name = StringField() name = StringField()
email = StringField() email = StringField()

View File

@ -242,6 +242,10 @@ def election_api_cast_vote(election):
'vote': EosObject.serialise_and_wrap(vote, should_protect=True) 'vote': EosObject.serialise_and_wrap(vote, should_protect=True)
}), mimetype='application/json') }), mimetype='application/json')
@app.route('/auditor')
def auditor():
return flask.render_template('election/auditor.html')
@app.route('/debug') @app.route('/debug')
def debug(): def debug():
assert False assert False

View File

@ -23,7 +23,7 @@
.hash { .hash {
font-family: monospace; font-family: monospace;
word-wrap: break-word; word-break: break-all;
} }
.superem { .superem {

View File

@ -19,10 +19,25 @@
#} #}
{% block content %} {% block content %}
TODO <h2>Ready to audit your ballot</h2>
<div class="ui negative message">
<p>Your vote has <span class="superem">not</span> yet been cast. Please follow the instructions to continue.</p>
</div>
<p>The following is your ballot with fingerprint <span class="hash">{{ eosjs.eos.core.hashing.__all__.SHA256().update_obj(ballot).hash_as_b64() }}</span>, decrypted and ready for auditing.</p>
<div class="ui form">
{# For some reason nunjucks doesn't like calling this the normal way #}
<textarea>{{ ballot.to_json(ballot.serialise_and_wrap(ballot)) }}</textarea>
</div>
<p>You may provide the above data to a trusted third-party, or use the <a href="/auditor?electionUrl={{ election_base_url }}" target="_blank">Eos auditor</a> to verify that your ballot was prepared correctly.</p>
<p>Once you are satisfied that your ballot has been prepared correctly, click ‘Continue’. Your ballot will be prepared again, to protect your secrecy, and you may choose to audit your ballot again, or proceed to cast your ballot.</p>
{% endblock %} {% endblock %}
{% block buttons %} {% block buttons %}
<button class="ui left floated button" onclick="prevTemplate();">Back</a> {# Dirty trick to go back to the encryption step #}
<button class="ui right floated primary button" onclick="nextTemplate();">Continue</button> <button class="ui right floated primary button" onclick="nextTemplate(-2);">Continue</button>
{% endblock %} {% endblock %}

View File

@ -83,6 +83,9 @@
$("#cast_prompt").hide(); $("#cast_prompt").hide();
$("#casting").show(); $("#casting").show();
// Prepare ballot
booth.ballot = booth.ballot.deaudit();
$.ajax({ $.ajax({
url: "{{ election_base_url }}cast_ballot", url: "{{ election_base_url }}cast_ballot",
type: "POST", type: "POST",

View File

@ -21,11 +21,18 @@
<script> <script>
boothWorker.onmessage = function(msg) { boothWorker.onmessage = function(msg) {
try { try {
rawAnswers = [];
for (var answer_json of booth.answers) {
rawAnswers.push(eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(answer_json, null));
}
encryptedAnswers = []; encryptedAnswers = [];
for (var encrypted_answer_json of msg.data) { for (var encrypted_answer_json of msg.data) {
encryptedAnswers.push(eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(encrypted_answer_json, null)); encryptedAnswers.push(eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(encrypted_answer_json, null));
} }
booth.ballot = eosjs.eos.base.election.__all__.Ballot(); booth.ballot = eosjs.eos.base.election.__all__.Ballot();
booth.ballot.answers = rawAnswers;
booth.ballot.encrypted_answers = encryptedAnswers; booth.ballot.encrypted_answers = encryptedAnswers;
booth.ballot.election_id = election._id; booth.ballot.election_id = election._id;
booth.ballot.election_hash = eosjs.eos.core.hashing.__all__.SHA256().update_obj(election).hash_as_b64(); booth.ballot.election_hash = eosjs.eos.core.hashing.__all__.SHA256().update_obj(election).hash_as_b64();

View File

@ -0,0 +1,219 @@
{% extends 'base.html' %}
{#
Eos - Verifiable elections
Copyright © 2017 RunasSudo (Yingtong Li)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% block title %}Ballot auditor{% endblock %}
{% block content %}
<h2>Ballot auditor</h2>
<div class="ui form">
<div class="field">
<label for="audit_election_url">Election URL</label>
<input id="audit_election_url" placeholder="https://example.com/01234567-89ab-cdef-ghij-klmnopqrstuv" type="text">
</div>
<div class="ui error message" id="audit_error_load">
<p>There was an error loading the election data. Please check the election URL and try again. If the problem persists, contact the election administrator.</p>
<p class="techdetails"></p>
</div>
<div class="field">
<label for="audit_ballot">Ballot data</label>
<textarea id="audit_ballot" placeholder='{"type": "eos.base.election.AuditBallot", "value": …}'></textarea>
</div>
<button class="ui primary button" onclick="loadElection();">Audit ballot</button>
</div>
<div class="ui hidden warning message" id="audit_contents">
<div class="header">Ballot contents</div>
<div id="audit_contents_inner"></div>
</div>
<div class="ui hidden negative message" id="audit_not_yet_cast">
<p>Your vote has <span class="superem">not</span> yet been cast. If you are satisfied with your ballot, you must return to the voting booth by closing this page and following the instructions.</p>
</div>
<div class="ui hidden message" id="audit_result">
<div class="header">Audit result</div>
<div id="audit_result_inner"></div>
</div>
{% endblock %}
{% block basecontent %}
{{ super() }}
<script src="{{ url_for('static', filename='js/eosjs.js') }}"></script>
<script>
var election = null;
var auditBallot = null;
function loadElection() {
$.ajax({ url: $("#audit_election_url").val(), dataType: "text" })
.done(function(data) {
try {
election = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.__all__.EosObject.from_json(data), null);
} catch (err) {
loadError(err);
throw err;
}
electionLoaded();
})
.fail(function(xhr, status, err) {
loadError(err);
throw err;
});
}
function electionLoaded() {
$("#audit_error_load").removeClass("visible");
// Audit ballot
var auditResult = document.querySelector("#audit_result");
auditResult.className = "ui message";
var auditResultInner = document.querySelector("#audit_result_inner");
auditResultInner.innerHTML = '<p>Auditing your ballot… Please wait.</p>';
try {
var result = doAuditBallot(auditResult, auditResultInner);
if (result) {
auditResult.className = "ui success message";
} else {
auditResultInner.innerHTML += '<p><i class="remove icon"></i> <b>The ballot has not been correctly prepared.</b></p>';
auditResult.className = "ui error message";
}
} catch (err) {
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Unknown error: ' + err + '</p>';
auditResultInner.innerHTML += '<p><i class="remove icon"></i> <b>The ballot has not been correctly prepared.</b></p>';
auditResult.className = "ui error message";
throw err;
} finally {
document.querySelector("#audit_not_yet_cast").className = "ui negative message";
}
}
function doAuditBallot(auditResult, auditResultInner) {
auditBallot = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.__all__.EosObject.from_json($("#audit_ballot").val()));
//auditResultInner.innerHTML += '<p><i class="checkmark icon"></i> Data is in ballot format.</p>';
if (!eosjs.isinstance(auditBallot, eosjs.eos.base.election.__all__.Ballot)) {
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: The data is not in ballot format.</p>';
return false;
}
if (!auditBallot.answers || !auditBallot.answers.__len__ || auditBallot.answers.__len__() === 0) {
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: The data is not in audit ballot format.</p>';
return false;
}
auditResultInner.innerHTML += '<p><i class="checkmark icon"></i> The data is in audit ballot format.</p>';
if (auditBallot.election_id !== election._id) {
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: The ballot corresponds to a different election.</p>';
return false;
}
auditResultInner.innerHTML += '<p><i class="checkmark icon"></i> The ballot election ID is correct.</p>';
if (auditBallot.election_hash !== eosjs.eos.core.hashing.__all__.SHA256().update_obj(election).hash_as_b64()) {
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: The ballot corresponds to a different election.</p>';
return false;
}
auditResultInner.innerHTML += '<p><i class="checkmark icon"></i> The ballot election hash is correct.</p>';
for (var questionNum = 0; questionNum < auditBallot.encrypted_answers.__len__(); questionNum++) {
auditResultInner.innerHTML += '<p><i class="info circle icon"></i> Question number ' + (questionNum + 1) + ':</p>';
// Compute expected plaintexts
var pt = eosjs.eos.core.objects.__all__.EosObject.to_json(eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(auditBallot.answers.__getitem__(questionNum)));
var bs = eosjs.eos.psr.bitstream.__all__.BitStream();
bs.write_string(pt);
bs.multiple_of(election.public_key.nbits(), true);
var messages = [];
function callback(val) {
messages.push(val);
}
bs.map(callback, election.public_key.nbits());
var encryptedAnswer = auditBallot.encrypted_answers.__getitem__(questionNum);
for (var blockNum = 0; blockNum < encryptedAnswer.blocks.__len__(); blockNum++) {
var block = encryptedAnswer.blocks.__getitem__(blockNum);
// TODO: Implement this in Python
if (!block.randomness) {
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: Block ' + blockNum + ' ciphertext is not a valid audit ciphertext.</p>';
return false;
}
if (!block.is_signature_valid()) {
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: Block ' + blockNum + ' signature is not valid.</p>';
return false;
}
if (block.randomness.__lt__(eosjs.eos.core.bigint.__all__.ONE) || block.randomness.__gt__(election.public_key.group.p.__sub__(eosjs.eos.core.bigint.__all__.TWO))) {
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: Block ' + blockNum + ' randomness is not valid.</p>';
return false;
}
if (!block.is_randomness_valid()) {
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: Block ' + blockNum + ' randomness does not match ciphertext.</p>';
return false;
}
if (!block.m0.__eq__(election.public_key.message_to_m0(messages[blockNum]))) {
auditResultInner.innerHTML += '<p><i class="remove icon"></i> Error: Block ' + blockNum + ' plaintext does not match claimed plaintext.</p>';
return false;
}
}
auditResultInner.innerHTML += '<p><i class="checkmark icon"></i> Question number ' + (questionNum + 1) + ' passed validation.</p>';
}
// Passed validation
var auditContents = document.querySelector("#audit_contents");
auditContents.className = "ui warning message";
var auditContentsInner = document.querySelector("#audit_contents_inner");
auditContentsInner.innerHTML = '';
auditContentsInner.innerHTML += '<p><i class="info circle icon"></i> <b>Please check that the following details match your intended selections:</b></p>';
for (var questionNum = 0; questionNum < auditBallot.encrypted_answers.__len__(); questionNum++) {
auditContentsInner.innerHTML += '<p><i class="icon"></i> Question ' + (questionNum + 1) + ': ' + election.questions.__getitem__(questionNum).pretty_answer(auditBallot.answers.__getitem__(questionNum)) + '</p>';
}
auditContentsInner.innerHTML += '<p><i class="info circle icon"></i> <b>Please check that the ballot fingerprint you recorded matches the following computed ballot fingerprint: <span class="hash">' + eosjs.eos.core.hashing.__all__.SHA256().update_obj(auditBallot).hash_as_b64() + '</span>.</b></p>';
auditContentsInner.innerHTML += '<p><i class="checkmark icon"></i> If the selections are correct, and the ballot fingerprint matches, then the ballot has been prepared correctly.</p>';
return true;
}
function loadError(err) {
if (err) {
$("#audit_error_load .techdetails").text("Technical details: " + err);
$("#audit_error_load .techdetails").show();
} else {
$("#audit_error_load .techdetails").hide();
}
$("#audit_error_load").addClass("visible");
}
if (location.search.indexOf("electionUrl=") >= 0) {
var electionUrl = (location.search.split('electionUrl=')[1]||'').split('&')[0];
$("#audit_election_url").val(electionUrl);
}
</script>
{% endblock %}