diff --git a/eos/base/workflow.py b/eos/base/workflow.py index 6651086..8d7ac8d 100644 --- a/eos/base/workflow.py +++ b/eos/base/workflow.py @@ -61,7 +61,7 @@ class WorkflowTask(EmbeddedObject): @classmethod def satisfies(cls, descriptor): - return cls._name == descriptor or descriptor in cls.provides + return cls._name == descriptor or descriptor in cls.provides or (descriptor in EosObject.objects and issubclass(cls, EosObject.objects[descriptor])) def on_enter(self): self.exit() diff --git a/eos/core/objects/__init__.py b/eos/core/objects/__init__.py index 4b52bb6..63b82c6 100644 --- a/eos/core/objects/__init__.py +++ b/eos/core/objects/__init__.py @@ -71,7 +71,6 @@ class PrimitiveField(Field): DictField = PrimitiveField IntField = PrimitiveField -ListField = PrimitiveField StringField = PrimitiveField class EmbeddedObjectField(Field): @@ -161,8 +160,10 @@ class EosObject(metaclass=EosObjectType): @staticmethod def serialise_and_wrap(value, object_type=None): if object_type: - return value.serialise() - return {'type': value._name, 'value': value.serialise()} + if value: + return value.serialise() + return None + return {'type': value._name, 'value': (value.serialise() if value else None)} @staticmethod def deserialise_and_unwrap(value, object_type=None): @@ -206,6 +207,13 @@ class EosList(EosObject): self.impl = list(*args) + def post_init(self): + for i in range(len(self.impl)): + val = self.impl[i] + val._instance = (self, i) + if not val._inited: + val.post_init() + # Lists in JS are implemented as native Arrays, so no cheating here :( def __len__(self): return len(self.impl) diff --git a/eos/psr/election.py b/eos/psr/election.py index 412331a..49a6ce0 100644 --- a/eos/psr/election.py +++ b/eos/psr/election.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from eos.core.bigint import * from eos.core.objects import * from eos.base.election import * from eos.psr.bitstream import * @@ -46,9 +47,16 @@ class Trustee(EmbeddedObject): name = StringField() email = StringField() +class MixChallengeResponse(EmbeddedObject): + index = IntField() + reenc = EmbeddedObjectListField(BigInt) + rand = EmbeddedObjectField(BigInt) + class MixingTrustee(Trustee): - mix_order = IntField() - mixed_questions = EmbeddedObjectListField() + mixed_questions = ListField(EmbeddedObjectListField(BlockEncryptedAnswer)) + commitments = ListField(EmbeddedObjectListField(BigInt)) + challenge = EmbeddedObjectField(BigInt) + response = ListField(EmbeddedObjectListField(MixChallengeResponse)) class PSRElection(Election): _db_name = Election._name diff --git a/eos/psr/mixnet.py b/eos/psr/mixnet.py index 8568e45..564280c 100644 --- a/eos/psr/mixnet.py +++ b/eos/psr/mixnet.py @@ -19,7 +19,11 @@ from eos.core.objects import * from eos.psr.election import * class RPCMixnet: - def __init__(self): + def __init__(self, mix_order): + self.mix_order = mix_order + + self.is_left = (self.mix_order % 2 == 0) + self.params = [] def random_permutation(self, n): @@ -54,26 +58,26 @@ class RPCMixnet: # Record the parameters permutations_and_reenc.append([permutations[i], block_reencryptions, block.public_key.group.random_element(), block.public_key.group.random_element()]) - commitments_left = [] - for i in range(len(permutations_and_reenc)): - val = permutations_and_reenc[i] - val_json = [val[0], [str(x) for x in val[1]], str(val[2])] - commitments_left.append(EosObject.to_sha256(EosObject.to_json(val_json))[0]) - - commitments_right = [] - for i in range(len(permutations_and_reenc)): - # Find the answer that went to 'i' - idx = next(idx for idx in range(len(permutations_and_reenc)) if permutations_and_reenc[idx][0] == i) - val = permutations_and_reenc[idx] - - val_json = [idx, [str(x) for x in val[1]], str(val[3])] - commitments_right.append(EosObject.to_sha256(EosObject.to_json(val_json))[0]) + commitments = [] + if self.is_left: + for i in range(len(permutations_and_reenc)): + val = permutations_and_reenc[i] + val_obj = MixChallengeResponse(index=val[0], reenc=val[1], rand=val[2]) + commitments.append(EosObject.to_sha256(EosObject.to_json(val_obj.serialise()))[1]) + else: + for i in range(len(permutations_and_reenc)): + # Find the answer that went to 'i' + idx = next(idx for idx in range(len(permutations_and_reenc)) if permutations_and_reenc[idx][0] == i) + val = permutations_and_reenc[idx] + + val_obj = MixChallengeResponse(index=idx, reenc=val[1], rand=val[3]) + commitments.append(EosObject.to_sha256(EosObject.to_json(val_obj.serialise()))[1]) self.params = permutations_and_reenc - return shuffled_answers, commitments_left, commitments_right + return shuffled_answers, commitments - def challenge(self, i, is_left): - if is_left: + def challenge(self, i): + if self.is_left: val = self.params[i] return [val[0], val[1], val[2]] else: diff --git a/eos/psr/tests.py b/eos/psr/tests.py index 3e2ca86..2d9ce0e 100644 --- a/eos/psr/tests.py +++ b/eos/psr/tests.py @@ -119,7 +119,7 @@ class MixnetTestCase(EosTestCase): # Generate plaintexts pts = [] - for i in range(10): + for i in range(4): pts.append(sk.public_key.group.random_element()) # Encrypt plaintexts @@ -130,44 +130,50 @@ class MixnetTestCase(EosTestCase): ct = bs.map(sk.public_key.encrypt, sk.public_key.group.p.nbits() - 1) answers.append(BlockEncryptedAnswer(blocks=ct)) - # Set up mixnet - mixnet = RPCMixnet() - - # Mix answers - shuffled_answers, commitments_left, commitments_right = mixnet.shuffle(answers) - - # Decrypt shuffle - msgs = [] - for i in range(len(shuffled_answers)): - bs = BitStream.unmap(shuffled_answers[i].blocks, sk.decrypt, sk.public_key.group.p.nbits() - 1) - m = bs.read() - msgs.append(m) - - # Check decryption - self.assertEqual(set(int(x) for x in pts), set(int(x) for x in msgs)) - - # Check commitments - def verify_shuffle(idx_left, idx_right, reencs): - claimed_blocks = shuffled_answers[idx_right].blocks - for j in range(len(answers[idx_left].blocks)): - reencrypted_block, _ = answers[idx_left].blocks[j].reencrypt(reencs[j]) - self.assertEqual(claimed_blocks[j].gamma, reencrypted_block.gamma) - self.assertEqual(claimed_blocks[j].delta, reencrypted_block.delta) - - for i in range(len(pts)): - # Left - perm, reencs, rand = mixnet.challenge(i, True) - val_json = [perm, [str(x) for x in reencs], str(rand)] - self.assertEqual(commitments_left[i], EosObject.to_sha256(EosObject.to_json(val_json))[0]) - verify_shuffle(i, perm, reencs) + def do_mixnet(mix_order): + # Set up mixnet + mixnet = RPCMixnet(mix_order) - # Right - perm, reencs, rand = mixnet.challenge(i, False) - val_json = [perm, [str(x) for x in reencs], str(rand)] - self.assertEqual(commitments_right[i], EosObject.to_sha256(EosObject.to_json(val_json))[0]) - verify_shuffle(perm, i, reencs) + # Mix answers + shuffled_answers, commitments = mixnet.shuffle(answers) + + # Decrypt shuffle + msgs = [] + for i in range(len(shuffled_answers)): + bs = BitStream.unmap(shuffled_answers[i].blocks, sk.decrypt, sk.public_key.group.p.nbits() - 1) + m = bs.read() + msgs.append(m) + + # Check decryption + self.assertEqual(set(int(x) for x in pts), set(int(x) for x in msgs)) + + # Check commitments + def verify_shuffle(idx_left, idx_right, reencs): + claimed_blocks = shuffled_answers[idx_right].blocks + for j in range(len(answers[idx_left].blocks)): + reencrypted_block, _ = answers[idx_left].blocks[j].reencrypt(reencs[j]) + self.assertEqual(claimed_blocks[j].gamma, reencrypted_block.gamma) + self.assertEqual(claimed_blocks[j].delta, reencrypted_block.delta) + + for i in range(len(pts)): + perm, reencs, rand = mixnet.challenge(i) + val_obj = MixChallengeResponse(index=perm, reenc=reencs, rand=rand) + self.assertEqual(commitments[i], EosObject.to_sha256(EosObject.to_json(val_obj.serialise()))[1]) + + if mixnet.is_left: + verify_shuffle(i, perm, reencs) + else: + verify_shuffle(perm, i, reencs) + + # NB: This isn't doing it in sequence, it's just testing a left mixnet and a right mixnet respectively + do_mixnet(0) + do_mixnet(1) class ElectionTestCase(EosTestCase): + @classmethod + def setUpClass(cls): + client.drop_database('test') + def do_task_assert(self, election, task, next_task): self.assertEqual(election.workflow.get_task(task).status, WorkflowTask.Status.READY) if next_task is not None: @@ -191,7 +197,7 @@ class ElectionTestCase(EosTestCase): election.voters.append(voter) for i in range(3): - mixing_trustee = MixingTrustee(mix_order=i) + mixing_trustee = MixingTrustee() election.mixing_trustees.append(mixing_trustee) election.sk = EGPrivateKey.generate() @@ -225,7 +231,38 @@ class ElectionTestCase(EosTestCase): election.save() # Close voting - self.do_task_assert(election, 'eos.base.workflow.TaskCloseVoting', 'eos.base.workflow.TaskDecryptVotes') + self.do_task_assert(election, 'eos.base.workflow.TaskCloseVoting', 'eos.psr.workflow.TaskMixVotes') + election.save() + + # Mix votes + election.workflow.get_task('eos.psr.workflow.TaskMixVotes').enter() + election.save() + + # Do the mix + for i in range(len(election.questions)): + for j in range(len(election.mixing_trustees)): + mixnet = RPCMixnet(j) + if j > 0: + orig_answers = election.mixing_trustees[j - 1].mixed_questions[i] + else: + orig_answers = [] + for voter in election.voters: + for ballot in voter.ballots: + orig_answers.append(ballot.encrypted_answers[i]) + shuffled_answers, commitments = mixnet.shuffle(orig_answers) + election.mixing_trustees[j].mixed_questions.append(EosList(shuffled_answers)) + election.mixing_trustees[j].commitments.append(EosList(commitments)) + + election.workflow.get_task('eos.psr.workflow.TaskMixVotes').exit() + election.save() + + # Verify mixes + election.workflow.get_task('eos.psr.workflow.TaskVerifyMixes').enter() + election.save() + + # TODO + + election.workflow.get_task('eos.psr.workflow.TaskVerifyMixes').exit() election.save() # Decrypt votes, for realsies diff --git a/eos/psr/workflow.py b/eos/psr/workflow.py index f6b9087..83a09f4 100644 --- a/eos/psr/workflow.py +++ b/eos/psr/workflow.py @@ -16,10 +16,41 @@ from eos.core.objects import * from eos.base.workflow import * +import eos.base.workflow # Concrete tasks # ============== +class TaskMixVotes(WorkflowTask): + depends_on = ['eos.base.workflow.TaskCloseVoting'] + + def on_enter(self): + # Do not automatically exit this task + pass + +class TaskVerifyMixes(WorkflowTask): + depends_on = ['eos.psr.workflow.TaskMixVotes'] + + def on_enter(self): + # Do not automatically exit this task + pass + +class TaskDecryptVotes(eos.base.workflow.TaskDecryptVotes): + depends_on = ['eos.psr.workflow.TaskVerifyMixes'] + + def on_enter(self): + election = self.recurse_parents('eos.base.election.Election') + + for _ in range(len(election.questions)): + election.results.append(EosObject.objects['eos.base.election.RawResult']()) + + for i in range(len(election.mixing_trustees[-1].mixed_questions)): + for encrypted_answer in election.mixing_trustees[-1].mixed_questions[i]: + answer = encrypted_answer.decrypt() + election.results[i].answers.append(answer) + + self.exit() + # Concrete workflows # ================== @@ -30,5 +61,7 @@ class PSRWorkflow(Workflow): self.tasks.append(TaskConfigureElection()) self.tasks.append(TaskOpenVoting()) self.tasks.append(TaskCloseVoting()) - self.tasks.append(TaskDecryptVotes()) + self.tasks.append(TaskMixVotes()) + self.tasks.append(TaskVerifyMixes()) + self.tasks.append(TaskDecryptVotes()) # The PSR one, not the base one self.tasks.append(TaskReleaseResults())