diff --git a/eos/core/hashing/__init__.py b/eos/core/hashing/__init__.py index ef40bb7..4bd0da7 100644 --- a/eos/core/hashing/__init__.py +++ b/eos/core/hashing/__init__.py @@ -46,6 +46,8 @@ else: # ============== class SHA256: + nbits = 256 + def __init__(self): if is_python: self.impl = hashlib.sha256() diff --git a/eos/psr/bitstream.py b/eos/psr/bitstream.py index 9035062..9641f9f 100644 --- a/eos/psr/bitstream.py +++ b/eos/psr/bitstream.py @@ -16,12 +16,13 @@ from eos.core.bigint import * from eos.core.objects import * +from eos.core.hashing import * class BitStream(EosObject): - def __init__(self, value=None): + def __init__(self, value=None, nbits=None): if value: self.impl = value - self.nbits = self.impl.nbits() + self.nbits = nbits if nbits else self.impl.nbits() else: self.impl = ZERO self.nbits = 0 @@ -58,6 +59,17 @@ class BitStream(EosObject): self.ptr += nbits self.nbits += nbits + # Append to the end without affecting ptr + def append(self, bits, nbits=None): + if nbits is None: + nbits = bits.nbits() + if nbits < bits.nbits(): + raise Exception('Too many bits to append to BitString') + + self.impl = (self.impl << nbits) | bits + self.nbits += nbits + self.remaining += nbits + def read_string(self): length = self.read(32) length = length.__int__() # JS attempts to call this twice if we do it in one line @@ -124,3 +136,23 @@ class BitStream(EosObject): @classmethod def deserialise(cls, value): return cls(value) + +class InfiniteHashBitStream(BitStream): + def __init__(self, seed): + self.sha = SHA256() + self.sha.update_bigint(seed) + self.ctr = 0 + self.sha.update_text(str(self.ctr)) + + super().__init__(self.sha.hash_as_bigint(), self.sha.nbits) + + def read(self, nbits=None): + # 11000110110 + # ^---- + if nbits is None: + nbits = self.remaining + while nbits > self.remaining: + self.ctr += 1 + self.sha.update_text(str(self.ctr)) + self.append(self.sha.hash_as_bigint(), self.sha.nbits) + return super().read(nbits) diff --git a/eos/psr/election.py b/eos/psr/election.py index 49a6ce0..b049109 100644 --- a/eos/psr/election.py +++ b/eos/psr/election.py @@ -16,6 +16,7 @@ from eos.core.bigint import * from eos.core.objects import * +from eos.core.hashing import * from eos.base.election import * from eos.psr.bitstream import * from eos.psr.crypto import * @@ -48,15 +49,29 @@ class Trustee(EmbeddedObject): email = StringField() class MixChallengeResponse(EmbeddedObject): - index = IntField() + challenge_index = IntField() + response_index = IntField() reenc = EmbeddedObjectListField(BigInt) rand = EmbeddedObjectField(BigInt) class MixingTrustee(Trustee): mixed_questions = ListField(EmbeddedObjectListField(BlockEncryptedAnswer)) commitments = ListField(EmbeddedObjectListField(BigInt)) - challenge = EmbeddedObjectField(BigInt) + challenge = EmbeddedObjectListField(BigInt) response = ListField(EmbeddedObjectListField(MixChallengeResponse)) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.mixnets = [] # TODO: Remove this stuff + + def compute_challenge(self, question_num): + sha = SHA256() + trustees = self.recurse_parents(Election).mixing_trustees + for i in range(len(trustees)): + sha.update_text(EosObject.to_json(MixingTrustee._fields['mixed_questions'].element_field.serialise(trustees[i].mixed_questions[question_num]))) + for i in range(self._instance[1]): + sha.update_text(EosObject.to_json(MixingTrustee._fields['response'].element_field.serialise(trustees[i].response[question_num]))) + return sha class PSRElection(Election): _db_name = Election._name diff --git a/eos/psr/tests.py b/eos/psr/tests.py index 8b14122..80c950b 100644 --- a/eos/psr/tests.py +++ b/eos/psr/tests.py @@ -242,7 +242,8 @@ class ElectionTestCase(EosTestCase): # Do the mix for i in range(len(election.questions)): for j in range(len(election.mixing_trustees)): - mixnet = RPCMixnet(j) + # Wouldn't happen server-side IRL + election.mixing_trustees[j].mixnets.append(RPCMixnet(j)) if j > 0: orig_answers = election.mixing_trustees[j - 1].mixed_questions[i] else: @@ -250,7 +251,7 @@ class ElectionTestCase(EosTestCase): for voter in election.voters: for ballot in voter.ballots: orig_answers.append(ballot.encrypted_answers[i]) - shuffled_answers, commitments = mixnet.shuffle(orig_answers) + shuffled_answers, commitments = election.mixing_trustees[j].mixnets[i].shuffle(orig_answers) election.mixing_trustees[j].mixed_questions.append(EosList(shuffled_answers)) election.mixing_trustees[j].commitments.append(EosList(commitments)) @@ -261,7 +262,30 @@ class ElectionTestCase(EosTestCase): election.workflow.get_task('eos.psr.workflow.TaskVerifyMixes').enter() election.save() - # TODO + # Record challenge responses + for i in range(len(election.questions)): + for j in range(len(election.mixing_trustees)): + trustee = election.mixing_trustees[j] + if j % 2 == 0: + trustee.challenge.append(trustee.compute_challenge(i).hash_as_bigint()) + else: + trustee.challenge.append(election.mixing_trustees[j - 1].challenge[i]) + challenge_bs = InfiniteHashBitStream(trustee.challenge[i]) + + trustee.response.append(EosList()) + + nbits = BigInt(len(trustee.mixed_questions[i])).nbits() + for k in range(len(trustee.mixed_questions[i])): + challenge_bit = challenge_bs.read(1) + should_reveal = ((j % 2) == (challenge_bit % 2)) + if should_reveal: + response = trustee.mixnets[i].challenge(k) + trustee.response[i].append(MixChallengeResponse( + challenge_index=k, + response_index=response[0], + reenc=response[1], + rand=response[2] + )) election.workflow.get_task('eos.psr.workflow.TaskVerifyMixes').exit() election.save()