def serialize_signature(self): result = bytearray() # Append challenges as bytes challenges = BitVector(size=0) for i in self.__signature.challenges: if (i == 0): challenges = challenges + BitVector(bitlist=[0, 0]) if (i == 1): challenges = challenges + BitVector(bitlist=[1, 0]) if (i == 2): challenges = challenges + BitVector(bitlist=[0, 1]) diff = 8 - (challenges.length() % 8) challenges = challenges + BitVector(intVal=0, size=diff) result.extend(bytes.fromhex(challenges.get_bitvector_in_hex())) # Append salt as bytes result.extend( bytes.fromhex(self.__signature.salt.get_bitvector_in_hex())) # Append all proofs for t in range(self.mpc_rounds): challenge_value = self.__signature.challenges[t] result.extend(self.__signature.proofs[t].view_3_commit) result.extend( bytes.fromhex(self.__signature.proofs[t].transcript. get_bitvector_in_hex())) result.extend( bytes.fromhex( self.__signature.proofs[t].seed_1.get_bitvector_in_hex())) result.extend( bytes.fromhex( self.__signature.proofs[t].seed_2.get_bitvector_in_hex())) if (challenge_value == 1 or challenge_value == 2): result.extend( bytes.fromhex(self.__signature.proofs[t].i_share. get_bitvector_in_hex())) self.__signature_ser = result
class BloomFilter(object): def __init__(self, array_size, hash_count): self.size = array_size #the size of bit array self.hash_count = hash_count #the number of probes for each item self.bit_array = BitVector(size = array_size) def add(self, item): exist = True for seed in xrange(self.hash_count): result = mmh3.hash(item, seed) % self.size if self.bit_array[result] == 0: self.bit_array[result] = 1 exist = False return exist def lookup(self, item): for seed in xrange(self.hash_count): result = mmh3.hash(item, seed) % self.size if self.bit_array[result] == 0: return False return True def __contains__(self, item): self.lookup(item) def __add__(self, bf): self.bit_array = self.bit_array | bf.bit_array return self def clear_all(self): self.bit_array.reset(0) def set_bit_array(self, ba): self.bit_array = ba def export_bloom(self, filename): with open(filename, 'wb') as f: f.write(str(self.size) + ":" + str(self.hash_count) + ":" + self.bit_array.get_bitvector_in_hex())
class LowMC: def __init__(self, param): print("LowMC init for " + param) # Set security parameters according to param if (param == 'picnic-L1'): self.blocksize = 128 self.keysize = 128 self.number_sboxes = 10 self.number_rounds = 20 self.filename = 'picnic-L1.dat' elif (param == 'picnic-L3'): self.blocksize = 192 self.keysize = 192 self.number_sboxes = 10 self.number_rounds = 30 self.filename = 'picnic-L3.dat' elif (param == 'picnic-L5'): self.blocksize = 256 self.keysize = 256 self.number_sboxes = 10 self.number_rounds = 38 self.filename = 'picnic-L5.dat' # Public variables self.plaintext = None # Private variables self.__priv_key = None self.__state = None self.__lin_layer = [] self.__lin_layer_inv = [] self.__round_consts = [] self.__round_key_mats = [] self.__sbox = [0x00, 0x01, 0x03, 0x06, 0x07, 0x04, 0x05, 0x02] self.__sbox_inv = [0x00, 0x01, 0x07, 0x02, 0x05, 0x06, 0x03, 0x04] # Call init functions self.__read_constants() self.__invert_lin_matrix() print("LowMC init done") ''' //////////////////////////// /// Public functions /// //////////////////////////// ''' # Generate private key with lenght self.keysize from urandom def generate_priv_key(self): temp_key = os.urandom(int(self.keysize / 8)) self.__priv_key = BitVector(rawbytes=temp_key) # Set private key # @param priv_key as raw bytes with length self.keysize def set_priv_key(self, priv_key): assert (len(priv_key) * 8 == self.keysize), "Private key has length != keysize" self.__priv_key = BitVector(rawbytes=priv_key) # Encrypts a plaintext # @param plaintext must be raw bytes with length blocksize # @return ciphertext as raw bytes with length blocksize def encrypt(self, plaintext): assert (len(plaintext) * 8) == self.blocksize, "Plaintext has length != blocksize" assert (self.__priv_key is not None), "Private key not set" self.__state = BitVector(rawbytes=plaintext) self.__key_addition(0) for i in range(self.number_rounds): self.__apply_sbox() self.__multiply_with_lin_mat(i) self.__state = self.__state ^ self.__round_consts[i] self.__key_addition(i + 1) result = bytes.fromhex(self.__state.get_bitvector_in_hex()) self.__state = None return result # Decrypts a ciphertext # @param ciphertext must be raw bytes with length blocksize # @return plaintext as raw bytes with length blocksize def decrypt(self, ciphertext): assert (len(ciphertext) * 8) == self.blocksize, "Ciphertext has length != blocksize" assert (self.__priv_key is not None), "Private key not set" self.__state = BitVector(rawbytes=ciphertext) for i in range(self.number_rounds, 0, -1): self.__key_addition(i) self.__state = self.__state ^ self.__round_consts[i - 1] self.__multiply_with_lin_mat_inv(i - 1) self.__apply_sbox_inv() self.__key_addition(0) result = bytes.fromhex(self.__state.get_bitvector_in_hex()) self.__state = None return result ''' ///////////////////////////// /// Picnic functions /// ///////////////////////////// ''' def mpc_matrix_mul_keys(self, outputs, inputs, r, players): tmp_inputs = copy.deepcopy(inputs) for player in range(players): for i in range(self.blocksize): outputs[player][i] = (self.__round_key_mats[r][i] & tmp_inputs[player]).count_bits() % 2 return outputs def mpc_matrix_mul_lin(self, outputs, inputs, r, players): tmp_inputs = copy.deepcopy(inputs) for player in range(players): for i in range(self.blocksize): outputs[player][i] = (self.__lin_layer[r][i] & tmp_inputs[player]).count_bits() % 2 return outputs def mpc_xor_rconsts(self, states, r): states[0] = states[0] ^ self.__round_consts[r] return states def mpc_xor_rconsts_verify(self, states, r, chal_trit): if (chal_trit == 0): states[0] = states[0] ^ self.__round_consts[r] if (chal_trit == 2): states[1] = states[1] ^ self.__round_consts[r] return states ''' ///////////////////////////// /// Private functions /// ///////////////////////////// ''' def __apply_sbox(self): result = BitVector(size=self.blocksize) state_copy = self.__state.deep_copy() # Copy the identity part of the message result_ident = state_copy[(3 * self.number_sboxes):self.blocksize] # Substitute the rest of the message with the sboxes # ---------------------------------------------------- # ATTENTION: The 3-bit chunks seem to be reversed # in the Picnic-Ref-Implementation, compared to the # LowMC-Ref-Implementation and the original LowMC-paper. # Example: state[0:3]='001' becomes '100' then gets sboxed # to '111' and reversed again for the state-update. # ---------------------------------------------------- state_copy = self.__state[0:(3 * self.number_sboxes)] result_sbox = BitVector(size=0) for i in range(self.number_sboxes): state_index = (3 * i) state_3_bits = state_copy[state_index:state_index + 3].reverse() sbox_3_bits = BitVector(intVal=self.__sbox[int(state_3_bits)], size=3).reverse() result_sbox = result_sbox + sbox_3_bits result = result_sbox + result_ident self.__state = result def __apply_sbox_inv(self): result = BitVector(size=self.blocksize) state_copy = self.__state.deep_copy() # Copy the identity part of the message result_ident = state_copy[(3 * self.number_sboxes):self.blocksize] # Substitute the rest of the message with the inverse sboxes # ---------------------------------------------------- # ATTENTION: The 3-bit chunks seem to be reversed # in the Picnic-Ref-Implementation, compared to the # LowMC-Ref-Implementation and the original LowMC-paper. # ---------------------------------------------------- state_copy = self.__state[0:(3 * self.number_sboxes)] result_sbox = BitVector(size=0) for i in range(self.number_sboxes): state_index = (3 * i) state_3_bits = state_copy[state_index:state_index + 3].reverse() sbox_3_bits = BitVector(intVal=self.__sbox_inv[int(state_3_bits)], size=3).reverse() result_sbox = result_sbox + sbox_3_bits result = result_sbox + result_ident self.__state = result def __multiply_with_lin_mat(self, r): result = BitVector(size=self.blocksize) for i in range(self.blocksize): result[i] = (self.__lin_layer[r][i] & self.__state).count_bits() % 2 self.__state = result def __multiply_with_lin_mat_inv(self, r): result = BitVector(size=self.blocksize) for i in range(self.blocksize): result[i] = (self.__lin_layer_inv[r][i] & self.__state).count_bits() % 2 self.__state = result def __key_addition(self, r): round_key = BitVector(size=self.keysize) for i in range(self.blocksize): round_key[i] = (self.__round_key_mats[r][i] & self.__priv_key).count_bits() % 2 self.__state = self.__state ^ round_key def __read_constants(self): with open(self.filename, 'r') as matfile: const_data = matfile.read() const_data_split = const_data.split('\n') # Check for correct parameters and file length params = const_data_split[0:3] assert params[0] == str( self.blocksize), "Wrong blocksize in data file!" assert params[1] == str(self.keysize), "Wrong keysize in data file!" assert params[2] == str( self.number_rounds), "Wrong number of rounds in data file!" assert (len(const_data_split) - 1) == \ 3 + (((self.number_rounds * 2) + 1) * self.blocksize) + self.number_rounds,\ "Wrong file size (number of lines)" # Linear layer matrices lines_offset = 3 lines_count = self.number_rounds * self.blocksize lin_layer = const_data_split[lines_offset:(lines_offset + lines_count)] for r in range(self.number_rounds): mat = [] for s in range(self.blocksize): bv = BitVector(bitstring=lin_layer[(r * self.blocksize) + s]) mat.append(bv) self.__lin_layer.append(mat) # Round constants lines_offset += lines_count lines_count = self.number_rounds round_consts = const_data_split[lines_offset:(lines_offset + lines_count)] for line in round_consts: self.__round_consts.append(BitVector(bitstring=line)) # Round key matrices lines_offset += lines_count lines_count = (self.number_rounds + 1) * self.blocksize round_key_mats = const_data_split[lines_offset:(lines_offset + lines_count)] for r in range(self.number_rounds + 1): mat = [] for s in range(self.blocksize): mat.append( BitVector(bitstring=round_key_mats[(r * self.blocksize) + s])) self.__round_key_mats.append(mat) def __invert_lin_matrix(self): self.__lin_layer_inv = [] for r in range(self.number_rounds): # Copy lin_layer mat = [] for i in range(self.blocksize): mat.append(self.__lin_layer[r][i].deep_copy()) # Create (initial identity) matrix, where the # inverted matrix will be stored in. inv_mat = [] for i in range(self.blocksize): temp_bv = BitVector(intVal=0, size=self.blocksize) temp_bv[i] = 1 inv_mat.append(temp_bv) # Transform to upper triangular matrix row = 0 for col in range(self.keysize): if (not mat[row][col]): r = row + 1 while ((r < self.blocksize) and (not mat[r][col])): r += 1 if (r >= self.blocksize): continue else: temp = mat[row] mat[row] = mat[r] mat[r] = temp temp = inv_mat[row] inv_mat[row] = inv_mat[r] inv_mat[r] = temp for i in range(row + 1, self.blocksize): if (mat[i][col]): mat[i] = mat[i] ^ mat[row] inv_mat[i] = inv_mat[i] ^ inv_mat[row] row += 1 # Transform to inverse matrix for col in range(self.keysize, 0, -1): for r in range(col - 1): if (mat[r][col - 1]): mat[r] = mat[r] ^ mat[col - 1] inv_mat[r] = inv_mat[r] ^ inv_mat[col - 1] self.__lin_layer_inv.append(inv_mat)
class Picnic: def __init__(self): # Public picnic parameters self.blocksize = 128 self.blocksize_bytes = int(self.blocksize / 8) self.keysize = 128 self.rounds = 20 self.sboxes = 10 self.mpc_rounds = 219 self.hash_length = 256 self.lowmc = LowMC('picnic-L1') # Private variables self.__priv_key = None self.__pub_key = None self.__views = [] self.__commitments = [] self.__seeds = [] self.__salt = None self.__tapes_pos = 0 self.__challenges = None self.__prove = None self.__signature = Signature() self.__signature_ser = None ########################## ### Sign and verify ### ########################## # Signing a message # @param message as bytes def sign(self, message): # Initialize empty views[mpc_rounds][players] for _ in range(self.mpc_rounds): three_views = [] for _ in range(3): single_view = View(self.blocksize, self.rounds, self.sboxes) three_views.append(single_view) self.__views.append(three_views) # Initialize empty commitments[mpc_rounds][players] for _ in range(self.mpc_rounds): three_commits = [] for _ in range(3): single_commit = Commitment(self.hash_length, 0) three_commits.append(single_commit) self.__commitments.append(three_commits) # Initialize seeds # Get one long shake_128 hash with length (3 * mpc_rounds + 1) * (blocksize / 8) # and split it afterwards into seeds and one salt shake128 = hashlib.shake_128() shake128.update(bytes.fromhex(self.__priv_key.get_bitvector_in_hex())) shake128.update(message) shake128.update( bytes.fromhex(self.__pub_key.public_key.get_bitvector_in_hex())) shake128.update(bytes.fromhex(self.__pub_key.p.get_bitvector_in_hex())) shake128.update(bytes([self.blocksize, 0])) long_hash = shake128.digest( ((3 * self.mpc_rounds) + 1) * self.blocksize_bytes) count = 0 for _ in range(self.mpc_rounds): three_seeds = [] for _ in range(3): single_seed = BitVector( rawbytes=long_hash[count:count + self.blocksize_bytes]) count += self.blocksize_bytes three_seeds.append(single_seed) self.__seeds.append(three_seeds) self.__salt = BitVector(rawbytes=long_hash[count:count + self.blocksize_bytes]) # A hack for the last 5 bytes of the tmp_view_raw is needed # and this seems as a bug (or unwanted behaviour) in the ref-sourcecode so far. new_end_of_tmp_view = bytearray([0, 0, 0, 0, 0]) # MPC Rounds for t in range(self.mpc_rounds): print("MPC round " + str(t)) tapes = [] self.__tapes_pos = 0 # Create tapes[0..2] and i_shares for j in range(2): length = int( (self.blocksize + 3 * self.rounds * self.sboxes) / 8) tmp_view_raw = self.mpc_create_random_tape( self.__seeds[t][j], self.__salt, t, j, length) + new_end_of_tmp_view self.__views[t][j].i_share = BitVector( rawbytes=tmp_view_raw[0:self.blocksize_bytes]) tapes.append( BitVector( rawbytes=tmp_view_raw[self.blocksize_bytes:length])) length_2 = int((3 * self.rounds * self.sboxes) / 8) tapes.append( BitVector(rawbytes=self.mpc_create_random_tape( self.__seeds[t][2], self.__salt, t, 2, length_2))) self.__views[t][2].i_share = self.__priv_key ^ \ self.__views[t][0].i_share ^ \ self.__views[t][1].i_share # Run MPC new_end_of_tmp_view = self.run_mpc(t, tapes, tmp_view_raw) # Calculate the commitments self.mpc_commit(t) # Calculate challenges self.__challenges = self.h3(message) # Calculate proofs self.__proofs = self.prove() # Copy proofs, challenges and salt to self.__signature self.__signature.proofs = self.__proofs self.__signature.challenges = self.__challenges self.__signature.salt = self.__salt def verify(self, message): # Reset private variables self.__views = [] self.__commitments = [] self.__seeds = [] self.__salt = None self.__tapes_pos = 0 self.__challenges = None self.__prove = None # Initialize empty commitments[mpc_rounds][players] for _ in range(self.mpc_rounds): three_commits = [] for _ in range(3): single_commit = Commitment(self.hash_length, 0) three_commits.append(single_commit) self.__commitments.append(three_commits) # Initialize empty outputs[mpc_rounds][players] outputs = [] for _ in range(self.mpc_rounds): three_outputs = [] for _ in range(3): single_output = BitVector(intVal=0, size=self.blocksize) three_outputs.append(single_output) outputs.append(three_outputs) # Initialize empty views[mpc_rounds][2] for _ in range(self.mpc_rounds): three_views = [] for _ in range(2): single_view = View(self.blocksize, self.rounds, self.sboxes) three_views.append(single_view) self.__views.append(three_views) for t in range(self.mpc_rounds): print("MPC Round: " + str(t)) tapes = [] self.__tapes_pos = 0 # Copy transcript to view[t][1] self.__views[t][1].transcript = self.__signature.proofs[ t].transcript # Rebuild tapes and i_shares in regard to the challenges chal_trit = self.__signature.challenges[t] tmp_view_raw = None tmp_view_raw_short = None # Calculate both i_shares if (chal_trit == 0): length = int( (self.blocksize + 3 * self.rounds * self.sboxes) / 8) tmp_view_raw = self.mpc_create_random_tape(self.__signature.proofs[t].seed_1, \ self.__signature.salt, \ t, 0, length) self.__views[t][0].i_share = BitVector( rawbytes=tmp_view_raw[0:self.blocksize_bytes]) tapes.append( BitVector( rawbytes=tmp_view_raw[self.blocksize_bytes:length])) tmp_view_raw = self.mpc_create_random_tape(self.__signature.proofs[t].seed_2, \ self.__signature.salt, \ t, 1, length) self.__views[t][1].i_share = BitVector( rawbytes=tmp_view_raw[0:self.blocksize_bytes]) tapes.append( BitVector( rawbytes=tmp_view_raw[self.blocksize_bytes:length])) # Calculate i_share for player 0 # i_share for player 1 was given in the proof if (chal_trit == 1): length = int( (self.blocksize + 3 * self.rounds * self.sboxes) / 8) tmp_view_raw = self.mpc_create_random_tape(self.__signature.proofs[t].seed_1, \ self.__signature.salt, \ t, 1, length) self.__views[t][0].i_share = BitVector( rawbytes=tmp_view_raw[0:self.blocksize_bytes]) tapes.append( BitVector( rawbytes=tmp_view_raw[self.blocksize_bytes:length])) length = int((3 * self.rounds * self.sboxes) / 8) tmp_view_raw_short = self.mpc_create_random_tape(self.__signature.proofs[t].seed_2, \ self.__signature.salt, \ t, 2, length) tapes.append(BitVector(rawbytes=tmp_view_raw_short[0:length])) self.__views[t][1].i_share = self.__signature.proofs[t].i_share # i_share for player 0 was given in the proof # Calculate i_share for player 1 if (chal_trit == 2): length = int((3 * self.rounds * self.sboxes) / 8) tmp_view_raw_short = self.mpc_create_random_tape(self.__signature.proofs[t].seed_1, \ self.__signature.salt, \ t, 2, length) tapes.append(BitVector(rawbytes=tmp_view_raw_short[0:length])) self.__views[t][0].i_share = self.__signature.proofs[t].i_share length = int( (self.blocksize + 3 * self.rounds * self.sboxes) / 8) tmp_view_raw = self.mpc_create_random_tape(self.__signature.proofs[t].seed_2, \ self.__signature.salt, \ t, 0, length) self.__views[t][1].i_share = BitVector( rawbytes=tmp_view_raw[0:self.blocksize_bytes]) tapes.append( BitVector( rawbytes=tmp_view_raw[self.blocksize_bytes:length])) # Run MPC self.run_mpc_verify(t, tapes, tmp_view_raw, chal_trit) # Calculate the commitments self.mpc_commit_verify(t, chal_trit) # Update outputs outputs[t][chal_trit] = self.__views[t][0].o_share outputs[t][(chal_trit + 1) % 3] = self.__views[t][1].o_share outputs[t][(chal_trit + 2) % 3] = self.__views[t][0].o_share ^ \ self.__views[t][1].o_share ^ \ self.__pub_key.public_key # Calculate challenges self.__challenges = self.h3_verify(message, outputs) if (self.__challenges == self.__signature.challenges): print("Signature verified") else: print("Signature verification failed!") ############################## ### LowMC MPC functions ### ############################## def run_mpc_verify(self, t, tapes, tmp_view_raw, chal_trit): key_shares = [] states = [] roundkeys = [] # Create empty roundkeys and states # Fill key_shares with views for i in range(2): roundkeys.append(BitVector(intVal=0, size=self.blocksize)) states.append(BitVector(intVal=0, size=self.blocksize)) key_shares.append(self.__views[t][i].i_share) # Init states by xor'ing plaintext and roundkeys states = self.mpc_xor_constant_verify(states, self.__pub_key.p, chal_trit) roundkeys = self.lowmc.mpc_matrix_mul_keys(roundkeys, key_shares, 0, 2) states = self.mpc_xor(states, roundkeys, 2) for r in range(self.rounds): states = self.mpc_sbox_verify(states, tapes, r, t) states = self.lowmc.mpc_matrix_mul_lin(states, states, r, 2) states = self.lowmc.mpc_xor_rconsts_verify(states, r, chal_trit) roundkeys = self.lowmc.mpc_matrix_mul_keys(roundkeys, key_shares, r + 1, 2) states = self.mpc_xor(states, roundkeys, 2) for i in range(2): self.__views[t][i].o_share = states[i] # Simulate LowMC for all three players def run_mpc(self, t, tapes, tmp_view_raw): key_shares = [] states = [] roundkeys = [] # Create empty roundkeys and states # Fill key_shares with views for i in range(3): roundkeys.append(BitVector(intVal=0, size=self.blocksize)) states.append(BitVector(intVal=0, size=self.blocksize)) key_shares.append(self.__views[t][i].i_share) # Init states by xor'ing plaintext and roundkeys states = self.mpc_xor_constant(states, self.__pub_key.p) roundkeys = self.lowmc.mpc_matrix_mul_keys(roundkeys, key_shares, 0, 3) states = self.mpc_xor(states, roundkeys, 3) for r in range(self.rounds): states = self.mpc_sbox(states, tapes, r, t) states = self.lowmc.mpc_matrix_mul_lin(states, states, r, 3) states = self.lowmc.mpc_xor_rconsts(states, r) roundkeys = self.lowmc.mpc_matrix_mul_keys(roundkeys, key_shares, r + 1, 3) states = self.mpc_xor(states, roundkeys, 3) for i in range(3): self.__views[t][i].o_share = states[i] # This is part of a hack for the end of tmp_view_raw new_end_of_tmp_view = bytes.fromhex( states[2][self.blocksize - 40:self.blocksize].get_bitvector_in_hex()) return new_end_of_tmp_view # MPC LowMC sbox for verifying def mpc_sbox_verify(self, states, tapes, r, t): a = (BitVector(intVal=0, size=3)) b = (BitVector(intVal=0, size=3)) c = (BitVector(intVal=0, size=3)) ab = (BitVector(intVal=0, size=3)) bc = (BitVector(intVal=0, size=3)) ca = (BitVector(intVal=0, size=3)) # Sbox'ing the first 3*self.sboxes bits for # all three players for i in range(0, (3 * self.sboxes), 3): for j in range(2): a[j] = states[j][i + 2] b[j] = states[j][i + 1] c[j] = states[j][i] ab = self.mpc_and_verify(a, b, tapes, r, t) bc = self.mpc_and_verify(b, c, tapes, r, t) ca = self.mpc_and_verify(c, a, tapes, r, t) for j in range(2): states[j][i + 2] = a[j] ^ bc[j] states[j][i + 1] = a[j] ^ b[j] ^ ca[j] states[j][i] = a[j] ^ b[j] ^ c[j] ^ ab[j] return states # MPC LowMC sbox for signing def mpc_sbox(self, states, tapes, r, t): a = (BitVector(intVal=0, size=3)) b = (BitVector(intVal=0, size=3)) c = (BitVector(intVal=0, size=3)) ab = (BitVector(intVal=0, size=3)) bc = (BitVector(intVal=0, size=3)) ca = (BitVector(intVal=0, size=3)) # Sbox'ing the first 3*self.sboxes bits for # all three players for i in range(0, (3 * self.sboxes), 3): for j in range(3): a[j] = states[j][i + 2] b[j] = states[j][i + 1] c[j] = states[j][i] ab = self.mpc_and(a, b, tapes, r, t) bc = self.mpc_and(b, c, tapes, r, t) ca = self.mpc_and(c, a, tapes, r, t) for j in range(3): states[j][i + 2] = a[j] ^ bc[j] states[j][i + 1] = a[j] ^ b[j] ^ ca[j] states[j][i] = a[j] ^ b[j] ^ c[j] ^ ab[j] return states # MPC LowMC AND for verifying def mpc_and_verify(self, in1, in2, tapes, r, t): rand = BitVector(intVal=0, size=2) rand[0] = tapes[0][self.__tapes_pos] rand[1] = tapes[1][self.__tapes_pos] result = BitVector(intVal=0, size=2) result[0] = (in1[0] & in2[1]) ^ \ (in1[1] & in2[0]) ^ \ (in1[0] & in2[0]) ^ \ rand[0] ^ \ rand[1] self.__views[t][0].transcript[self.__tapes_pos] = result[0] result[1] = self.__views[t][1].transcript[self.__tapes_pos] self.__tapes_pos += 1 return result # MPC LowMC AND for signing and updating the transcripts def mpc_and(self, in1, in2, tapes, r, t): rand = BitVector(intVal=0, size=3) rand[0] = tapes[0][self.__tapes_pos] rand[1] = tapes[1][self.__tapes_pos] rand[2] = tapes[2][self.__tapes_pos] result = BitVector(intVal=0, size=3) # Update the transcripts for all three players for i in range(3): result[i] = (in1[i] & in2[(i + 1) % 3]) ^ \ (in1[(i + 1) % 3] & in2[i]) ^ \ (in1[i] & in2[i]) ^ \ rand[i] ^ \ rand[(i + 1) % 3] self.__views[t][i].transcript[self.__tapes_pos] = result[i] self.__tapes_pos += 1 return result # MPC LowMC XOR constant verify def mpc_xor_constant_verify(self, ins, constant, chal_trit): if (chal_trit == 0): ins[0] = ins[0] ^ constant if (chal_trit == 2): ins[1] = ins[1] ^ constant return ins # MPC LowMC XOR a constant def mpc_xor_constant(self, ins, constant): ins[0] = ins[0] ^ constant return ins # MPC LowMC XOR outs and ins for all players def mpc_xor(self, outs, ins, players): for i in range(players): outs[i] = outs[i] ^ ins[i] return outs # Calculate the commitments, the third one is given in the proof def mpc_commit_verify(self, t, chal_trit): self.__commitments[t][chal_trit].hash = self.mpc_h0(t, 0) self.__commitments[t][(chal_trit + 1) % 3].hash = self.mpc_h0(t, 1) self.__commitments[t][ (chal_trit + 2) % 3].hash = self.__signature.proofs[t].view_3_commit def mpc_h0(self, t, player): # H4(seed[mpc_round][player]) shake128 = hashlib.shake_128() shake128.update(bytes([0x04])) if (player == 0): shake128.update( bytes.fromhex( self.__signature.proofs[t].seed_1.get_bitvector_in_hex())) if (player == 1): shake128.update( bytes.fromhex( self.__signature.proofs[t].seed_2.get_bitvector_in_hex())) h4 = shake128.digest(int(self.hash_length / 8)) # Calculate h0(h4, views[t]) shake128 = hashlib.shake_128() shake128.update(bytes([0x00])) shake128.update(h4) shake128.update( bytes.fromhex( self.__views[t][player].i_share.get_bitvector_in_hex())) shake128.update( bytes.fromhex( self.__views[t][player].transcript.get_bitvector_in_hex())) shake128.update( bytes.fromhex( self.__views[t][player].o_share.get_bitvector_in_hex())) return shake128.digest(int(self.hash_length / 8)) # Calculate the commitments by hashing the # seeds and views for all three players def mpc_commit(self, t): for i in range(3): # H4(seed[mpc_round][player]) shake128 = hashlib.shake_128() shake128.update(bytes([0x04])) shake128.update( bytes.fromhex(self.__seeds[t][i].get_bitvector_in_hex())) h4 = shake128.digest(int(self.hash_length / 8)) # Calculate h0(h4, views[t]) shake128 = hashlib.shake_128() shake128.update(bytes([0x00])) shake128.update(h4) shake128.update( bytes.fromhex( self.__views[t][i].i_share.get_bitvector_in_hex())) shake128.update( bytes.fromhex( self.__views[t][i].transcript.get_bitvector_in_hex())) shake128.update( bytes.fromhex( self.__views[t][i].o_share.get_bitvector_in_hex())) self.__commitments[t][i].hash = shake128.digest( int(self.hash_length / 8)) # Get one long hash for the random tapes def mpc_create_random_tape(self, seed, salt, mpc_round, player, length): # H2(seed[mpc_round][player]) shake128 = hashlib.shake_128() shake128.update(bytes([0x02])) shake128.update(bytes.fromhex(seed.get_bitvector_in_hex())) h2 = shake128.digest(int(self.hash_length / 8)) # Create random tape shake128 = hashlib.shake_128() shake128.update(h2) shake128.update(bytes.fromhex(salt.get_bitvector_in_hex())) shake128.update(bytes([mpc_round, 0])) shake128.update(bytes([player, 0])) length_le = length.to_bytes(2, byteorder='little') shake128.update(length.to_bytes(2, byteorder='little')) return shake128.digest(length) ################################ ### Challenges and proofs ### ################################ # Calculating the challenges in {0,1,2}* def h3_verify(self, message, outputs): shake128 = hashlib.shake_128() # Hash the output shares with prefix 0x01 shake128.update(bytes([0x01])) for t in range(self.mpc_rounds): for player in range(3): shake128.update( bytes.fromhex(outputs[t][player].get_bitvector_in_hex())) # Hash the commitments for t in range(self.mpc_rounds): for player in range(3): shake128.update(self.__commitments[t][player].hash) # Hash the circuit output circuit_output = outputs[0][0] ^ outputs[0][1] ^ outputs[0][2] shake128.update(bytes.fromhex(circuit_output.get_bitvector_in_hex())) # Hash p (plaintext), salt and message to get the challenge from it shake128.update(bytes.fromhex(self.__pub_key.p.get_bitvector_in_hex())) shake128.update( bytes.fromhex(self.__signature.salt.get_bitvector_in_hex())) shake128.update(message) tmp_hash = shake128.digest(int(self.hash_length / 8)) tmp_bitvector = BitVector(rawbytes=tmp_hash) bit_pos = 0 result = [] # Build the challenge in {0,1,2}* as a list. # Each two bits in the hash become one element in the challenge-list # until the challenge-list has the length of self.mpc_rounds. # There can be re-calculating of the hash to reach that length. while (1): a = tmp_bitvector[bit_pos] b = tmp_bitvector[bit_pos + 1] if (a == 0 and b == 0): result.append(0) if (a == 0 and b == 1): result.append(1) if (a == 1 and b == 0): result.append(2) bit_pos += 2 if (len(result) >= self.mpc_rounds): break if (bit_pos >= self.hash_length): shake128 = hashlib.shake_128() shake128.update(bytes([0x01])) shake128.update(tmp_hash) tmp_hash = shake128.digest(int(self.hash_length / 8)) tmp_bitvector = BitVector(rawbytes=tmp_hash) bit_pos = 0 return result # Calculating the challenges in {0,1,2}* def h3(self, message): shake128 = hashlib.shake_128() # Hash the output shares with prefix 0x01 shake128.update(bytes([0x01])) for t in range(self.mpc_rounds): for player in range(3): shake128.update( bytes.fromhex(self.__views[t] [player].o_share.get_bitvector_in_hex())) # Hash the commitments for t in range(self.mpc_rounds): for player in range(3): shake128.update(self.__commitments[t][player].hash) # Hash the circuit output circuit_output = self.__views[0][0].o_share ^ \ self.__views[0][1].o_share ^ \ self.__views[0][2].o_share shake128.update(bytes.fromhex(circuit_output.get_bitvector_in_hex())) # Hash p (plaintext), salt and message to get the challenge from it shake128.update(bytes.fromhex(self.__pub_key.p.get_bitvector_in_hex())) shake128.update(bytes.fromhex(self.__salt.get_bitvector_in_hex())) shake128.update(message) tmp_hash = shake128.digest(int(self.hash_length / 8)) tmp_bitvector = BitVector(rawbytes=tmp_hash) bit_pos = 0 result = [] # Build the challenge in {0,1,2}* as a list. # Each two bits in the hash become one element in the challenge-list # until the challenge-list has the length of self.mpc_rounds. # There can be re-calculating of the hash to reach that length. while (1): a = tmp_bitvector[bit_pos] b = tmp_bitvector[bit_pos + 1] if (a == 0 and b == 0): result.append(0) if (a == 0 and b == 1): result.append(1) if (a == 1 and b == 0): result.append(2) bit_pos += 2 if (len(result) >= self.mpc_rounds): break if (bit_pos >= self.hash_length): shake128 = hashlib.shake_128() shake128.update(bytes([0x01])) shake128.update(tmp_hash) tmp_hash = shake128.digest(int(self.hash_length / 8)) tmp_bitvector = BitVector(rawbytes=tmp_hash) bit_pos = 0 return result # Calculating the proofs from the challenges, seeds, # transcripts, i_shares and commitments def prove(self): proofs = [] for t in range(self.mpc_rounds): tmp_proof = Proof() challenge = self.__challenges[t] if (challenge == 0): tmp_proof.seed_1 = self.__seeds[t][0] tmp_proof.seed_2 = self.__seeds[t][1] if (challenge == 1): tmp_proof.seed_1 = self.__seeds[t][1] tmp_proof.seed_2 = self.__seeds[t][2] tmp_proof.i_share = self.__views[t][2].i_share if (challenge == 2): tmp_proof.seed_1 = self.__seeds[t][2] tmp_proof.seed_2 = self.__seeds[t][0] tmp_proof.i_share = self.__views[t][2].i_share tmp_proof.transcript = self.__views[t][(challenge + 1) % 3].transcript tmp_proof.view_3_commit = self.__commitments[t][(challenge + 2) % 3].hash proofs.append(tmp_proof) return proofs ########################### ### Helper functions ### ########################### # Set or generate the priv and pub key def generate_keys(self, p=None, priv_key=None): # Generate random p (plaintext) with length self.keysize if (p is None): raw_p = os.urandom(int(self.keysize / 8)) else: raw_p = p bitvector_p = BitVector(rawbytes=raw_p) # Generate private key with length self.keysize if (priv_key is None): raw_priv_key = os.urandom(int(self.keysize / 8)) else: raw_priv_key = priv_key self.__priv_key = BitVector(rawbytes=raw_priv_key) # Generate public key [c,p] self.lowmc.set_priv_key(raw_priv_key) raw_c = self.lowmc.encrypt(raw_p) bitvector_c = BitVector(rawbytes=raw_c) self.__pub_key = Publickey(bitvector_c, bitvector_p) # Serialize a full signature from self.__signature def serialize_signature(self): result = bytearray() # Append challenges as bytes challenges = BitVector(size=0) for i in self.__signature.challenges: if (i == 0): challenges = challenges + BitVector(bitlist=[0, 0]) if (i == 1): challenges = challenges + BitVector(bitlist=[1, 0]) if (i == 2): challenges = challenges + BitVector(bitlist=[0, 1]) diff = 8 - (challenges.length() % 8) challenges = challenges + BitVector(intVal=0, size=diff) result.extend(bytes.fromhex(challenges.get_bitvector_in_hex())) # Append salt as bytes result.extend( bytes.fromhex(self.__signature.salt.get_bitvector_in_hex())) # Append all proofs for t in range(self.mpc_rounds): challenge_value = self.__signature.challenges[t] result.extend(self.__signature.proofs[t].view_3_commit) result.extend( bytes.fromhex(self.__signature.proofs[t].transcript. get_bitvector_in_hex())) result.extend( bytes.fromhex( self.__signature.proofs[t].seed_1.get_bitvector_in_hex())) result.extend( bytes.fromhex( self.__signature.proofs[t].seed_2.get_bitvector_in_hex())) if (challenge_value == 1 or challenge_value == 2): result.extend( bytes.fromhex(self.__signature.proofs[t].i_share. get_bitvector_in_hex())) self.__signature_ser = result def deserialize_signature(self): # Create an empty signature self.__signature = Signature() self.__signature.proofs = [] bytes_pos = 0 # Get challenges challenge_length = math.ceil(2 * self.mpc_rounds / 8) challenges_bytes = self.__signature_ser[bytes_pos:bytes_pos + challenge_length] challenges_bitvector = BitVector(rawbytes=challenges_bytes) challenges = [] for i in range(0, (2 * self.mpc_rounds), 2): two_bits = str(challenges_bitvector[i:i + 2]) if (two_bits == '00'): challenges.append(0) if (two_bits == '10'): challenges.append(1) if (two_bits == '01'): challenges.append(2) self.__signature.challenges = challenges bytes_pos += challenge_length # Get salt salt_bytes = self.__signature_ser[bytes_pos:bytes_pos + self.blocksize_bytes] salt = BitVector(rawbytes=salt_bytes) self.__signature.salt = salt bytes_pos += self.blocksize_bytes # Deserialize the proofs in all mpc_rounds for t in range(self.mpc_rounds): proof = Proof() chal_bit = challenges[t] # Get view_3_commitment view_3_commit_bytes = self.__signature_ser[bytes_pos:bytes_pos + int(self.hash_length / 8)] proof.view_3_commit = view_3_commit_bytes bytes_pos += int(self.hash_length / 8) # Get transcript transcript_bytes = self.__signature_ser[bytes_pos:bytes_pos + int( (3 * self.rounds * self.sboxes) / 8)] proof.transcript = BitVector(rawbytes=transcript_bytes) bytes_pos += int((3 * self.rounds * self.sboxes) / 8) # Get seed_1 seed_1_bytes = self.__signature_ser[bytes_pos:bytes_pos + self.blocksize_bytes] proof.seed_1 = BitVector(rawbytes=seed_1_bytes) bytes_pos += self.blocksize_bytes # Get seed_2 seed_2_bytes = self.__signature_ser[bytes_pos:bytes_pos + self.blocksize_bytes] proof.seed_2 = BitVector(rawbytes=seed_2_bytes) bytes_pos += self.blocksize_bytes # If chal_bit is not 0, then get i_share if not (chal_bit == 0): i_share_bytes = self.__signature_ser[bytes_pos:bytes_pos + self.blocksize_bytes] proof.i_share = BitVector(rawbytes=i_share_bytes) bytes_pos += self.blocksize_bytes self.__signature.proofs.append(proof) return # Write serialized signature to file def write_ser_sig_to_file(self, filename): with open(filename, "w") as text_file: text_file.write(self.__signature_ser.hex().upper()) # Read serialized signature from file def read_ser_sig_from_file(self, filename): with open(filename, "r") as text_file: sig_ser_hex = text_file.read() self.__signature_ser = bytes.fromhex(sig_ser_hex) # Print out a (not serialized) signature from self.__signature def print_signature(self): print("Signature:") print("Salt: " + self.__salt.get_bitvector_in_hex()) for t in range(self.mpc_rounds): print("Iteration t: " + str(t)) print("e_" + str(t) + ": " + str(self.__signature.challenges[t])) print("b_" + str(t) + ": " + self.__signature.proofs[t].view_3_commit.hex()) print("transcript: " + self.__signature.proofs[t].transcript.get_bitvector_in_hex()) print("seed1: " + self.__signature.proofs[t].seed_1.get_bitvector_in_hex()) print("seed2: " + self.__signature.proofs[t].seed_2.get_bitvector_in_hex()) if (not (self.__signature.challenges[t] == 0)): print( "inputShare: " + self.__signature.proofs[t].i_share.get_bitvector_in_hex()) # Print out a serialized signature from self.__signature_ser def print_signature_ser(self): print(self.__signature_ser.hex().upper())
class LowMC(object): """LowMC blockcipher mainclass. For de- and encryption of message blocks with the LowMC blockipher. This class can handle the various Picnic security levels and is able to generate or set and store a single privat key. """ __slots__ = [ '__blocksize', '__keysize', '__number_sboxes', '__number_rounds', '__filename', '__blocksize_bytes', '__keysize_bytes', '__plaintext', '__priv_key', '__state', '__lin_layer', '__lin_layer_inv', '__round_consts', '__round_key_mats', '__sbox', '__sbox_inv' ] def __init__(self, param: str) -> None: """Instanciates a LowMC object. Args: param: A string containing the Picnic security level """ if (param == 'picnic-L1'): self.__blocksize = 128 self.__keysize = 128 self.__number_sboxes = 10 self.__number_rounds = 20 self.__filename = 'picnic-L1.dat' elif (param == 'picnic-L3'): self.__blocksize = 192 self.__keysize = 192 self.__number_sboxes = 10 self.__number_rounds = 30 self.__filename = 'picnic-L3.dat' elif (param == 'picnic-L5'): self.__blocksize = 256 self.__keysize = 256 self.__number_sboxes = 10 self.__number_rounds = 38 self.__filename = 'picnic-L5.dat' else: raise Exception( 'Argument is not a valid Picnic security Level: {}'.format( param)) self.__blocksize_bytes = int(self.__blocksize / 8) self.__keysize_bytes = int(self.__keysize / 8) self.__plaintext = None self.__priv_key = None self.__state = None self.__lin_layer = [] self.__lin_layer_inv = [] self.__round_consts = [] self.__round_key_mats = [] self.__sbox = [0x00, 0x01, 0x03, 0x06, 0x07, 0x04, 0x05, 0x02] self.__sbox_inv = [0x00, 0x01, 0x07, 0x02, 0x05, 0x06, 0x03, 0x04] self.__read_constants() self.__invert_lin_matrix() @property def private_key(self) -> bytes: """Private key getter. Getter method for returning the private key Returns: bytearray of the private key """ return self.__priv_key @private_key.setter def private_key(self, priv_key: Optional[bytes] = None) -> None: """Set or generate a private key. If no private key is provided as argument, one is generated from the CSPRNG of the underlying OS. This should be a plattform independend source for randomness. It will have the length self.__keysize_bytes. Args: priv_key: If provided, must be a bytearray of length self.__keysize_bytes """ if (priv_key is None): temp_key = os.urandom(int(self.__keysize_bytes)) self.__priv_key = BitVector(rawbytes=temp_key) else: assert (len(priv_key) == self.__keysize_bytes), \ "Private key has length != keysize" self.__priv_key = BitVector(rawbytes=priv_key) def encrypt(self, plaintext: bytes) -> bytes: """Encryption of a plaintext. Args: plaintext: Must be a bytearray of length self.__blocksize_bytes Returns: A bytearray containing the ciphertext of length self.__blocksize_bytes """ assert (len(plaintext) == self.__blocksize_bytes), \ "Plaintext has length != blocksize" assert (self.__priv_key is not None), "Private key not set" self.__state = BitVector(rawbytes=plaintext) self.__key_addition(0) for i in range(self.__number_rounds): self.__apply_sbox() self.__multiply_with_lin_mat(i) self.__state = self.__state ^ self.__round_consts[i] self.__key_addition(i + 1) result = bytes.fromhex(self.__state.get_bitvector_in_hex()) self.__state = None return result def decrypt(self, ciphertext: bytes) -> bytes: """Decryption of a ciphertext. Args: ciphertext: Must be a bytearray of length self.__blocksize_bytes Returns: bytearray containing the plaintext of length self.__blocksize_bytes """ assert (len(ciphertext) == self.__blocksize_bytes), \ "Ciphertext has length != blocksize" assert (self.__priv_key is not None), "Private key not set" self.__state = BitVector(rawbytes=ciphertext) for i in range(self.__number_rounds, 0, -1): self.__key_addition(i) self.__state = self.__state ^ self.__round_consts[i - 1] self.__multiply_with_lin_mat_inv(i - 1) self.__apply_sbox_inv() self.__key_addition(0) result = bytes.fromhex(self.__state.get_bitvector_in_hex()) self.__state = None return result def __apply_sbox(self) -> None: result = BitVector(size=self.__blocksize) state_copy = self.__state.deep_copy() # Copy the identity part of the message result_ident = state_copy[(3 * self.__number_sboxes):self.__blocksize] # Substitute the rest of the message with the sboxes # ---------------------------------------------------- # ATTENTION: The 3-bit chunks seem to be reversed # in the Picnic-Ref-Implementation, compared to the # LowMC-Ref-Implementation and the original LowMC-paper. # Example: state[0:3]='001' becomes '100' then gets sboxed # to '111' and reversed again for the state-update. # ---------------------------------------------------- state_copy = self.__state[0:(3 * self.__number_sboxes)] result_sbox = BitVector(size=0) for i in range(self.__number_sboxes): state_index = (3 * i) state_3_bits = state_copy[state_index:state_index + 3].reverse() sbox_3_bits = BitVector(intVal=self.__sbox[int(state_3_bits)], size=3).reverse() result_sbox = result_sbox + sbox_3_bits result = result_sbox + result_ident self.__state = result def __apply_sbox_inv(self) -> None: result = BitVector(size=self.__blocksize) state_copy = self.__state.deep_copy() # Copy the identity part of the message result_ident = state_copy[(3 * self.__number_sboxes):self.__blocksize] # Substitute the rest of the message with the inverse sboxes # ---------------------------------------------------- # ATTENTION: The 3-bit chunks seem to be reversed # in the Picnic-Ref-Implementation, compared to the # LowMC-Ref-Implementation and the original LowMC-paper. # ---------------------------------------------------- state_copy = self.__state[0:(3 * self.__number_sboxes)] result_sbox = BitVector(size=0) for i in range(self.__number_sboxes): state_index = (3 * i) state_3_bits = state_copy[state_index:state_index + 3].reverse() sbox_3_bits = BitVector(intVal=self.__sbox_inv[int(state_3_bits)], size=3).reverse() result_sbox = result_sbox + sbox_3_bits result = result_sbox + result_ident self.__state = result def __multiply_with_lin_mat(self, r: int) -> None: result = BitVector(size=self.__blocksize) for i in range(self.__blocksize): result[i] = (self.__lin_layer[r][i] & self.__state) \ .count_bits() % 2 self.__state = result def __multiply_with_lin_mat_inv(self, r: int) -> None: result = BitVector(size=self.__blocksize) for i in range(self.__blocksize): result[i] = (self.__lin_layer_inv[r][i] & self.__state) \ .count_bits() % 2 self.__state = result def __key_addition(self, r: int) -> None: round_key = BitVector(size=self.__keysize) for i in range(self.__blocksize): round_key[i] = (self.__round_key_mats[r][i] & self.__priv_key) \ .count_bits() % 2 self.__state = self.__state ^ round_key def __read_constants(self) -> None: with open(self.__filename, 'r') as matfile: const_data = matfile.read() const_data_split = const_data.split('\n') # Check for correct parameters and file length params = const_data_split[0:3] assert params[0] == str(self.__blocksize), \ "Wrong blocksize in data file!" assert params[1] == str(self.__keysize), \ "Wrong keysize in data file!" assert params[2] == str(self.__number_rounds), \ "Wrong number of rounds in data file!" assert (len(const_data_split) - 1) == 3 \ + (((self.__number_rounds * 2) + 1) * self.__blocksize) \ + self.__number_rounds, \ "Wrong file size (number of lines)" # Linear layer matrices lines_offset = 3 lines_count = self.__number_rounds * self.__blocksize lin_layer = const_data_split[lines_offset:(lines_offset + lines_count)] for r in range(self.__number_rounds): mat = [] for s in range(self.__blocksize): bv = BitVector(bitstring=lin_layer[(r * self.__blocksize) + s]) mat.append(bv) self.__lin_layer.append(mat) # Round constants lines_offset += lines_count lines_count = self.__number_rounds round_consts = const_data_split[lines_offset:(lines_offset + lines_count)] for line in round_consts: self.__round_consts.append(BitVector(bitstring=line)) # Round key matrices lines_offset += lines_count lines_count = (self.__number_rounds + 1) * self.__blocksize round_key_mats = const_data_split[lines_offset:(lines_offset + lines_count)] for r in range(self.__number_rounds + 1): mat = [] for s in range(self.__blocksize): mat.append( BitVector(bitstring=round_key_mats[(r * self.__blocksize) + s])) self.__round_key_mats.append(mat) def __invert_lin_matrix(self) -> None: self.__lin_layer_inv = [] for r in range(self.__number_rounds): # Copy lin_layer mat = [] for i in range(self.__blocksize): mat.append(self.__lin_layer[r][i].deep_copy()) # Create (initial identity) matrix, where the # inverted matrix will be stored in. inv_mat = [] for i in range(self.__blocksize): temp_bv = BitVector(intVal=0, size=self.__blocksize) temp_bv[i] = 1 inv_mat.append(temp_bv) # Transform to upper triangular matrix row = 0 for col in range(self.__keysize): if (not mat[row][col]): r = row + 1 while ((r < self.__blocksize) and (not mat[r][col])): r += 1 if (r >= self.__blocksize): continue else: temp = mat[row] mat[row] = mat[r] mat[r] = temp temp = inv_mat[row] inv_mat[row] = inv_mat[r] inv_mat[r] = temp for i in range(row + 1, self.__blocksize): if (mat[i][col]): mat[i] = mat[i] ^ mat[row] inv_mat[i] = inv_mat[i] ^ inv_mat[row] row += 1 # Transform to inverse matrix for col in range(self.__keysize, 0, -1): for r in range(col - 1): if (mat[r][col - 1]): mat[r] = mat[r] ^ mat[col - 1] inv_mat[r] = inv_mat[r] ^ inv_mat[col - 1] self.__lin_layer_inv.append(inv_mat)