Implement ballot auditing 🎉
This commit is contained in:
parent
39246d5df0
commit
ca542f9d9e
@ -34,6 +34,16 @@ class Ballot(EmbeddedObject):
|
|||||||
encrypted_answers = EmbeddedObjectListField()
|
encrypted_answers = EmbeddedObjectListField()
|
||||||
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()
|
||||||
@ -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())
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
|
||||||
# Choose an element 1 <= k <= p - 2
|
if randomness is None:
|
||||||
k = BigInt.crypto_random(ONE, self.group.p - TWO)
|
# Choose an element 1 <= k <= p - 2
|
||||||
|
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:
|
||||||
@ -136,12 +148,22 @@ class EGCiphertext(EmbeddedObject):
|
|||||||
delta = pow(self.public_key.X, k, self.public_key.group.p)
|
delta = pow(self.public_key.X, k, self.public_key.group.p)
|
||||||
|
|
||||||
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):
|
||||||
# Choose an element 1 <= k <= p - 2
|
if randomness is None:
|
||||||
r = BigInt.crypto_random(ONE, self.group.p - TWO)
|
# Choose an element 1 <= k <= p - 2
|
||||||
|
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
|
||||||
@ -166,6 +188,9 @@ class SEGCiphertext(EGCiphertext):
|
|||||||
c = SHA256().update_bigint(gs, self.gamma, self.delta).hash_as_bigint()
|
c = SHA256().update_bigint(gs, self.gamma, self.delta).hash_as_bigint()
|
||||||
|
|
||||||
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
|
||||||
|
@ -45,6 +45,14 @@ class BlockEncryptedAnswer(EncryptedAnswer):
|
|||||||
obj = EosObject.deserialise_and_unwrap(EosObject.from_json(m))
|
obj = EosObject.deserialise_and_unwrap(EosObject.from_json(m))
|
||||||
|
|
||||||
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()
|
||||||
|
@ -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
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
.hash {
|
.hash {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
word-wrap: break-word;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.superem {
|
.superem {
|
||||||
|
@ -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 %}
|
||||||
|
@ -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",
|
||||||
|
@ -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();
|
||||||
|
219
eosweb/core/templates/election/auditor.html
Normal file
219
eosweb/core/templates/election/auditor.html
Normal 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 %}
|
Loading…
Reference in New Issue
Block a user