├── README.md ├── impossible_differentials ├── aes.py ├── aes_equiv_sbox.pkl ├── arbitrary_sbox_8_8.pkl ├── identity_sbox_8.pkl ├── main_aes.py ├── main_skinny.py ├── primitive.py ├── skinny.py ├── skinny_sbox.pkl └── utilities.py └── sbox ├── arbitrary_sbox_gen.py ├── check_model.sage ├── convex_hull.sage ├── identity_sbox_gen.py ├── minimize.py └── utilities.py /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | This repository contains the code used to obtain some of our results in the paper "Efficient MILP modelings for Sboxes and Linear Layers of SPN ciphers." 4 | 5 | - `sbox` contains python (or SageMath) files that are useful for modeling 4-bit Sboxes. 6 | - `impossible_differentials` contains python files that build Gurobi models for searching for impossible differentials for Skinny and the AES. 7 | 8 | We have not published the code for 8-bit Sboxes yet but the models of the DDTs used in impossible differential search are given as pickle files. 9 | 10 | ## Overview of the `sbox` directory 11 | 12 | - `arbitrary_sbox_gen.py` generates a pickle file for an arbitrary Sbox DDT (only transitions zero -> non zero are impossible). 13 | - `check_model.sage` checks the correctness of a model of a DDT given by a pickle file. 14 | - `convex_hull.sage` generates a big set of inequalities with the convex hull technique. Uses the SageMath Sboxes and Polyhedra tools. 15 | - `identity_sbox_gen.py` generates a pickle file for the identity Sbox DDT. 16 | - `minimize.py` performs step 2 given a big set of inequalities with greedy or minimization techniques. 17 | - `utilities.py` defines some small useful functions for the other files. 18 | 19 | ## Overview of the `impossible_differentials` directory 20 | 21 | - `aes_equiv_sbox.pkl` is the model of the DDT of an affine equivalent AES Sbox. 22 | - `aes.py` builds and tests the Gurobi model for the AES. 23 | - `arbitrary_sbox_8_8.pkl` is the model of the DDT of an arbitrary 8-bit Sbox (for testing purposes). 24 | - `identity_sbox_8.pkl` is the model of the DDT of the identity 8-bit Sbox (testing). 25 | - `main_aes.py` launches the search for impossible differentials for the AES. 26 | - `main_skinny.py` is the same for Skinny. 27 | - `primitive.py` contains classes and functions common to `aes.py` and `skinny.py`. 28 | - `skinny.py` builds and tests the Gurobi model for Skinny. 29 | - `skinny_sbox.pkl` is the model of the DDT of the Skinny 8-bit Sbox. 30 | - `utilities.py` defines small useful functions. 31 | 32 | -------------------------------------------------------------------------------- /impossible_differentials/aes.py: -------------------------------------------------------------------------------- 1 | import gurobipy 2 | from primitive import AesLike 3 | from itertools import product as itp 4 | import utilities 5 | 6 | # Compute the ShiftRows transformation. 7 | shift_rows = [0] * 16 8 | for nib in range(16): 9 | row = nib % 4 10 | new_col = nib // 4 11 | old_col = (new_col + row) % 4 12 | shift_rows[nib] = (4 * old_col) + row 13 | 14 | # A = (M|I) where M is the MixColumns matrix. Little Endian. 15 | mixcolA_original = [ 16 | 0x101018180, 17 | 0x202028381, 18 | 0x404040602, 19 | 0x808088C84, 20 | 0x1010109888, 21 | 0x2020203010, 22 | 0x4040406020, 23 | 0x808080C040, 24 | 0x10001818001, 25 | 0x20002838102, 26 | 0x40004060204, 27 | 0x800088C8408, 28 | 0x100010988810, 29 | 0x200020301020, 30 | 0x400040602040, 31 | 0x800080C04080, 32 | 0x1000081800101, 33 | 0x2000083810202, 34 | 0x4000006020404, 35 | 0x800008C840808, 36 | 0x10000098881010, 37 | 0x20000030102020, 38 | 0x40000060204040, 39 | 0x800000C0408080, 40 | 0x100000080010181, 41 | 0x200000081020283, 42 | 0x400000002040406, 43 | 0x80000008408088C, 44 | 0x1000000088101098, 45 | 0x2000000010202030, 46 | 0x4000000020404060, 47 | 0x80000000408080C0, 48 | ] 49 | 50 | # A = P x (M|I) x Q. Matrix that needs less inequalities. 51 | mixcolA = [ 52 | 0x1010101000080, 53 | 0x202020301, 54 | 0x404040602, 55 | 0x8000808840C0000, 56 | 0x1010101808, 57 | 0x2020203010, 58 | 0x4040406020, 59 | 0x808080C040, 60 | 0x101000100800100, 61 | 0x20002030102, 62 | 0x40004060204, 63 | 0x80808000000840C, 64 | 0x100010180810, 65 | 0x200020301020, 66 | 0x400040602040, 67 | 0x800080C04080, 68 | 0x101010000008001, 69 | 0x2000003010202, 70 | 0x4000006020404, 71 | 0x808080C000084, 72 | 0x10000018081010, 73 | 0x20000030102020, 74 | 0x40000060204040, 75 | 0x800000C0408080, 76 | 0x100010180010000, 77 | 0x200000001020203, 78 | 0x400000002040406, 79 | 0x808000800840C00, 80 | 0x1000000008101018, 81 | 0x2000000010202030, 82 | 0x4000000020404060, 83 | 0x80000000408080C0, 84 | ] 85 | 86 | # A = (I|I). Useful for testing purposes. 87 | mixcolA_identity = [(1 ^ (1 << 32)) << i for i in range(32)] 88 | 89 | # Those functions apply the affine change in the SBox when we choose 90 | # the affine equivalent SBox. 91 | def qmat(x): 92 | y = x ^ ((x >> 7) & 1) ^ (((x >> 7) & 1) << 3) 93 | return y 94 | 95 | 96 | def qmat_on_state(x): 97 | out = 0 98 | for i in range(16): 99 | out ^= qmat((x >> (8 * i)) & 0xFF) << (8 * i) 100 | return out 101 | 102 | 103 | class Aes(AesLike): 104 | """ Gurobi Model for AES differential trails. """ 105 | 106 | def __init__(self, nb_rounds, sbox_file, mixcol="equiv"): 107 | 108 | # Different choices of MixColumns models. 109 | if mixcol == "equiv": 110 | mixcol_hex = mixcolA 111 | elif mixcol == "origin": 112 | mixcol_hex = mixcolA_original 113 | else: 114 | mixcol_hex = mixcolA_identity 115 | 116 | mixcol_matrix = [] 117 | for row in mixcol_hex: 118 | bit_list = utilities.bits(row, 64) 119 | mixcol_matrix += [bit_list] 120 | 121 | self.mat = mixcol_matrix 122 | self.mixcol = mixcol 123 | 124 | AesLike.__init__(self, 128, nb_rounds, sbox_file) 125 | 126 | def linear_layer(self, x_in, x_out): 127 | # Reversing the bytes because of the AES bytes being 128 | # ordered in big endian. 129 | x_in2 = [] 130 | for nib in range(16): 131 | x_in2 += [x_in[(8 * (15 - nib)) + i] for i in range(8)] 132 | x_in2 = x_in 133 | 134 | x_out2 = [] 135 | for nib in range(16): 136 | x_out2 += [x_out[(8 * (15 - nib)) + i] for i in range(8)] 137 | x_out2 = x_out 138 | 139 | x_in = [ 140 | x_in[(8 * shift_rows[i // 8]) + (i % 8)] for i in range(128) 141 | ] # variables after the shift_rows, at the input of mixcol 142 | 143 | for col in range(4): 144 | bit_list = [x_in[(32 * col) + i] for i in range(32)] + [ 145 | x_out[(32 * col) + i] for i in range(32) 146 | ] 147 | 148 | self.add_bin_matrix_constr(self.mat, bit_list, 0, mode="binary") 149 | 150 | def set_output_diff(self, out_diff): 151 | """ 152 | Sets the output difference for impossible differential 153 | search. Replaces the original function if this model 154 | uses the equivalent linear mixcolumns. Indeed, this 155 | mixcolumns matrix has a modified input and then a 156 | modified output for the Sbox. This function makes it 157 | transparent. 158 | """ 159 | if self.mixcol == "equiv": 160 | x = qmat_on_state(out_diff) 161 | else: 162 | x = out_diff 163 | AesLike.set_output_diff(self, x) 164 | 165 | def last_output_diff(self): 166 | """ 167 | Returns the value of the output difference in the last solution. 168 | Replacement for the same reason as set_output_diff. 169 | """ 170 | out_diff = AesLike.last_output_diff(self) 171 | 172 | if self.mixcol == "equiv": 173 | return qmat_on_state(out_diff) 174 | else: 175 | return out_diff 176 | 177 | def get_state_out_sbox(self, r): 178 | """ 179 | Gets the state at the output of round r. 180 | Replacement for the same reason as set_output_diff. 181 | """ 182 | x = AesLike.get_state_out_sbox(self, r) 183 | if self.mixcol == "equiv": 184 | x = qmat_on_state(x) 185 | 186 | return x 187 | 188 | def set_active_input_cell(self, cell): 189 | """ 190 | Adds constraints that speed up the computation when one input byte is active. 191 | Those new constraints just ecplicit which variables are 0 for sure in and out 192 | the first and second SBox layers. 193 | """ 194 | assert 0 <= cell and cell < 16 195 | # Checks that no active input cell has been set before. 196 | for i in range(16): 197 | key = "in_cell_{}".format(i) 198 | assert key not in self.misc 199 | 200 | key = "in_cell_{}".format(cell) 201 | self.misc[key] = set() 202 | 203 | # For each state bit... 204 | for i in range(128): 205 | # Fix 0 values in and out the first SBox layer. 206 | if i // 8 != cell: 207 | self.misc[key].add(self.model.addConstr(self.in_sbox[0, i] == 0)) 208 | self.misc[key].add(self.model.addConstr(self.out_sbox[0, i] == 0)) 209 | 210 | # Fix 0 values in and out the second SBox layer. 211 | if self.nb_rounds >= 2 and shift_rows[i // 8] // 4 != cell // 4: 212 | self.misc[key].add(self.model.addConstr(self.in_sbox[1, i] == 0)) 213 | self.misc[key].add(self.model.addConstr(self.out_sbox[1, i] == 0)) 214 | 215 | def unset_active_input_cell(self): 216 | """ 217 | Removes active input bytes constraints. 218 | """ 219 | for cell in range(16): 220 | key = "in_cell_{}".format(cell) 221 | if key in self.misc: 222 | for constr in self.misc[key]: 223 | self.model.remove(constr) 224 | del self.misc[key] 225 | 226 | def unset_active_output_cell(self): 227 | """ 228 | Same as above but for the output. 229 | """ 230 | for cell in range(16): 231 | key = "out_cell_{}".format(cell) 232 | if key in self.misc: 233 | for constr in self.misc[key]: 234 | self.model.remove(constr) 235 | del self.misc[key] 236 | 237 | def set_active_output_cell(self, cell): 238 | """ 239 | Same as above but for the output. 240 | """ 241 | assert 0 <= cell and cell < 16 242 | for i in range(16): 243 | key = "out_cell_{}".format(i) 244 | assert key not in self.misc 245 | 246 | key = "out_cell_{}".format(cell) 247 | self.misc[key] = set() 248 | 249 | for i in range(128): 250 | if i // 8 != cell: 251 | self.misc[key].add( 252 | self.model.addConstr(self.in_sbox[self.nb_rounds - 1, i] == 0) 253 | ) 254 | self.misc[key].add( 255 | self.model.addConstr(self.out_sbox[self.nb_rounds - 1, i] == 0) 256 | ) 257 | 258 | if self.nb_rounds >= 2 and (i // 8) // 4 != shift_rows[cell] // 4: 259 | self.misc[key].add( 260 | self.model.addConstr(self.in_sbox[self.nb_rounds - 2, i] == 0) 261 | ) 262 | self.misc[key].add( 263 | self.model.addConstr(self.out_sbox[self.nb_rounds - 2, i] == 0) 264 | ) 265 | 266 | 267 | # Some test vectors from the FIPS197. 268 | original_tv = [ 269 | (0x84FB386F1AE1AC977941DD70832DD769, 0x9F487F794F955F662AFC86ABD7F1AB29), 270 | (0x1F770C64F0B579DEAAAC432C3D37CF0E, 0xB7A53ECBBF9D75A0C40EFC79B674CC11), 271 | (0x684AF5BC0ACCE85564BB0878242ED2ED, 0x7A1E98BDACB6D1141A6944DD06EB2D3E), 272 | (0x9316DD47C2FA92834390A1DE43E43F23, 0xAAA755B34CFFE57CEF6F98E1F01C13E6), 273 | ] 274 | 275 | shift_rows_tv = [ 276 | (0x63CAB7040953D051CD60E0E7BA70E18C, 0x6353E08C0960E104CD70B751BACAD0E7), 277 | (0x84FB386F1AE1AC97DF5CFD237C49946B, 0x84E1FD6B1A5C946FDF4938977CFBAC23), 278 | ] 279 | 280 | equiv_tv = [ 281 | (0x00102030405060708090A0B0C0D0E0F0, 0x63CAB7040953D051CD60E0E7BA70E18C), 282 | (0x89D810E8855ACE682D1843D8CB128FE4, 0xA761CA9B97BE8B45D8AD1A611FC97369), 283 | (0x4915598F55E5D7A0DACA94FA1F0A63F7, 0x3B59CB73FCD90EE05774222DC067FB68), 284 | (0xFA636A2825B339C940668A3157244D17, 0x2DFB02343F6D12DD09337EC75B36E3F0), 285 | (0x247240236966B3FA6ED2753288425B6C, 0x36400926F9336D2D9FB59D23C42C3950), 286 | ] 287 | 288 | # For stupid endianness reasons... 289 | def reverse(x): 290 | y = 0 291 | for i in range(16): 292 | y ^= ((x >> (8 * i)) & 0xFF) << (8 * (15 - i)) 293 | return y 294 | 295 | 296 | original_tv = [(reverse(x), reverse(y)) for (x, y) in original_tv] 297 | shift_rows_tv = [(reverse(x), reverse(y)) for (x, y) in shift_rows_tv] 298 | equiv_tv = [(reverse(x), reverse(y)) for (x, y) in equiv_tv] 299 | 300 | 301 | def test_shift_rows(): 302 | mid = Aes(2, "identity_sbox_8.pkl", mixcol="identity") 303 | mid.model.setParam("LogToConsole", 0) 304 | for (x, y) in shift_rows_tv: 305 | mid.set_input_diff(x) 306 | mid.set_output_diff(y) 307 | mid.model.optimize() 308 | assert mid.model.status == gurobipy.GRB.OPTIMAL 309 | print("SR ok") 310 | 311 | 312 | def test_original_lin_layer(): 313 | mid = Aes(2, "identity_sbox_8.pkl", mixcol="origin") 314 | mid.model.setParam("LogToConsole", 0) 315 | for (x, y) in original_tv: 316 | mid.set_input_diff(x) 317 | mid.set_output_diff(y) 318 | mid.model.optimize() 319 | assert mid.model.status == gurobipy.GRB.OPTIMAL 320 | 321 | mid.set_input_diff(x ^ 1) 322 | mid.model.optimize() 323 | assert mid.model.status == gurobipy.GRB.INFEASIBLE 324 | print("Original Linear Layer ok") 325 | 326 | 327 | def test_equiv_lin_layer(): 328 | mid = Aes(2, "identity_sbox_8.pkl", mixcol="equiv") 329 | mid.model.setParam("LogToConsole", 0) 330 | for (x, y) in original_tv: 331 | xx = 0 332 | for i in range(16): 333 | xx ^= qmat((x >> (8 * i)) & 0xFF) << (8 * i) 334 | yy = 0 335 | for i in range(16): 336 | yy ^= qmat((y >> (8 * i)) & 0xFF) << (8 * i) 337 | mid.set_input_diff(xx) 338 | mid.set_output_diff(yy) 339 | mid.model.optimize() 340 | assert mid.model.status == gurobipy.GRB.OPTIMAL 341 | 342 | mid.set_input_diff(xx ^ 1) 343 | mid.model.optimize() 344 | assert mid.model.status == gurobipy.GRB.INFEASIBLE 345 | print("Original Equiv Linear Layer ok") 346 | 347 | 348 | def test_equiv_sbox(sbox_file): 349 | mid = Aes(1, sbox_file, mixcol="equiv") 350 | mid.model.setParam("LogToConsole", 0) 351 | nb_vec = len(equiv_tv) // 2 352 | test_vectors = [ 353 | (x0 ^ x1, y0 ^ y1) 354 | for ((x0, y0), (x1, y1)) in itp(equiv_tv, equiv_tv) 355 | if x0 != x1 356 | ] 357 | xx = 0 358 | for i in range(16): 359 | xx ^= i << (8 * i) 360 | for (x, y) in test_vectors: 361 | mid.set_input_diff(x) 362 | mid.set_output_diff(y) 363 | mid.model.optimize() 364 | assert mid.model.status == gurobipy.GRB.OPTIMAL 365 | mid.set_input_diff(x ^ xx) 366 | mid.model.optimize() 367 | assert mid.model.status == gurobipy.GRB.INFEASIBLE 368 | print("Equiv Sbox ok") 369 | 370 | 371 | if __name__ == "__main__": 372 | test_shift_rows() 373 | test_original_lin_layer() 374 | test_equiv_lin_layer() 375 | test_equiv_sbox("greedy_sbox_ineg_aes_equiv.pkl") 376 | -------------------------------------------------------------------------------- /impossible_differentials/aes_equiv_sbox.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnlcog/efficient_milp_modelings/80ea54c6628535877cb2c45c23d56589f05873f9/impossible_differentials/aes_equiv_sbox.pkl -------------------------------------------------------------------------------- /impossible_differentials/arbitrary_sbox_8_8.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnlcog/efficient_milp_modelings/80ea54c6628535877cb2c45c23d56589f05873f9/impossible_differentials/arbitrary_sbox_8_8.pkl -------------------------------------------------------------------------------- /impossible_differentials/identity_sbox_8.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnlcog/efficient_milp_modelings/80ea54c6628535877cb2c45c23d56589f05873f9/impossible_differentials/identity_sbox_8.pkl -------------------------------------------------------------------------------- /impossible_differentials/main_aes.py: -------------------------------------------------------------------------------- 1 | from aes import * 2 | import argparse 3 | import itertools 4 | 5 | if __name__ == "__main__": 6 | 7 | nb_rounds = 5 8 | 9 | parser = argparse.ArgumentParser( 10 | description="Launches search for impossible differentials in 5 rounds of AES " 11 | + "with chosen active input and output cells." 12 | ) 13 | parser.add_argument( 14 | "in_cell", type=int, help="Nibble of input.", choices=[i for i in range(16)], 15 | ) 16 | parser.add_argument( 17 | "out_cell", type=int, help="Nibble of output.", choices=[i for i in range(16)], 18 | ) 19 | args = parser.parse_args() 20 | 21 | in_cell = args.in_cell 22 | out_cell = args.out_cell 23 | 24 | # Main model. 25 | mid = Aes(nb_rounds, "aes_equiv_sbox.pkl") 26 | mid.model.setParam("LogToConsole", 0) 27 | mid.set_active_input_cell(in_cell) 28 | mid.set_active_output_cell(out_cell) 29 | 30 | # Auxiliary input model. 31 | aux_in = Aes(2, "aes_equiv_sbox.pkl") 32 | aux_in.model.setParam("LogToConsole", 0) 33 | aux_in.set_active_input_cell(in_cell) 34 | 35 | # Auxiliary output model. 36 | aux_out = Aes(2, "aes_equiv_sbox.pkl") 37 | aux_out.model.setParam("LogToConsole", 0) 38 | aux_out.set_active_output_cell(out_cell) 39 | 40 | the_dict = dict() 41 | for valx in range(1, 1 << 8): 42 | x = valx << (8 * in_cell) 43 | the_dict[x] = set() 44 | for valy in range(1, 1 << 8): 45 | y = valy << (8 * out_cell) 46 | the_dict[x].add(y) 47 | 48 | message = "Aes 5r in {} out {}.".format(in_cell, out_cell) 49 | mid.equimip_search(the_dict, aux_in, aux_out, message=message) 50 | -------------------------------------------------------------------------------- /impossible_differentials/main_skinny.py: -------------------------------------------------------------------------------- 1 | from skinny import * 2 | import argparse 3 | import itertools 4 | 5 | if __name__ == "__main__": 6 | 7 | nb_rounds = 13 8 | 9 | parser = argparse.ArgumentParser( 10 | description="Launches search for impossible differentials on 13 rounds of Skinny " 11 | + "with chosen active input cell." 12 | ) 13 | parser.add_argument( 14 | "cell", type=int, help="Active input cell.", choices=[i for i in range(16)], 15 | ) 16 | args = parser.parse_args() 17 | 18 | cell = args.cell 19 | 20 | # Main model 21 | mid = Skinny(nb_rounds, "skinny_sbox.pkl") 22 | mid.model.setParam("LogToConsole", 0) 23 | 24 | # Auxiliary input model 25 | aux_in = Skinny(2, "skinny_sbox.pkl") 26 | aux_in.model.setParam("LogToConsole", 0) 27 | 28 | # Auxiliary output model 29 | aux_out = Skinny(2, "skinny_sbox.pkl") 30 | aux_out.model.setParam("LogToConsole", 0) 31 | 32 | for out_cell in range(16): 33 | the_dict = dict() 34 | for valx in range(1, 1 << 8): 35 | x = valx << (8 * cell) 36 | the_dict[x] = set() 37 | for valy in range(1, 1 << 8): 38 | y = valy << (8 * out_cell) 39 | the_dict[x].add(y) 40 | 41 | message = "Skinny {}r in {} out {}.".format(nb_rounds, cell, out_cell) 42 | res = mid.equimip_search(the_dict, aux_in, aux_out, message=message) 43 | -------------------------------------------------------------------------------- /impossible_differentials/primitive.py: -------------------------------------------------------------------------------- 1 | from gurobipy import * 2 | import pickle 3 | import utilities 4 | from itertools import product as itp 5 | import random 6 | import time 7 | 8 | 9 | class Primitive: 10 | """ Gurobi Model of a cryptographic primitive for differential properties. """ 11 | 12 | def __init__(self, in_size, out_size): 13 | # Dictionnary of Sbx modelings used in this primitive. 14 | self.sbox_modelings = {} 15 | 16 | # Input and output sizes. 17 | self.in_size = in_size 18 | self.out_size = out_size 19 | 20 | # Input and output Gurobi variables. 21 | self.in_var = {} 22 | self.out_var = {} 23 | 24 | # Miscellaneous objects 25 | self.misc = {} 26 | 27 | # Gurobi Model 28 | self.model = Model() 29 | 30 | def add_sbox_modeling(self, file_name, other_name=None): 31 | """ 32 | Loads the pickle file file_name of an Sbox modeling. 33 | The pickle file should contain a set of lists representing 34 | inequalities. 35 | If ineg is such a list, the input coefficients are first, 36 | then come the output coefficients and finally comes the constant. 37 | The inequality is then: 38 | sum(input[i] * ineg[i]) + sum(output[i] * ineg[i + len(input)]) 39 | + ineg[len(input) + len(output)] >= 0 40 | """ 41 | with open(file_name, "rb") as f: 42 | if other_name is None: 43 | other_name = file_name 44 | (in_size, out_size, ddt, ineq) = pickle.load(f) 45 | self.sbox_modelings[other_name] = ( 46 | utilities.ddt_rows(ddt, in_size, out_size), 47 | utilities.ddt_cols(ddt, in_size, out_size), 48 | ineq, 49 | ) 50 | 51 | def add_sbox_constr(self, sbox_name, a, b): 52 | """ 53 | Adds constraints for one sbox registered in 54 | add_sbox_modelings[sbox_name]. 55 | a is a list of input variables, 56 | b is a list of output variables, 57 | """ 58 | 59 | n = len(a) 60 | m = len(b) 61 | (_, _, ineqs) = self.sbox_modelings[sbox_name] 62 | # ineqs = self.sbox_modelings[sbox_name] 63 | for ineg in ineqs: 64 | assert len(ineg) == n + m + 1 65 | self.model.addConstr( 66 | quicksum(ineg[i] * a[i] for i in range(n)) 67 | + quicksum(ineg[i + n] * b[i] for i in range(m)) 68 | + ineg[n + m] 69 | >= 0 70 | ) 71 | 72 | def add_xor_constr(self, variables, offset=0, mode="binary"): 73 | """ 74 | If mode = "binary", adds the $2^{n-1}$ constraints modeling 75 | the XOR constraint x[0] ^ ... ^ x[n-1] = offset 76 | where x is variables. 77 | If mode = "integer", models the same XOR constraint with a dummy 78 | integer variable t with x[0] + ... + x[n-1] = 2 * t + offset. 79 | """ 80 | x = variables 81 | n = len(x) 82 | 83 | if mode == "binary" or mode == "both": 84 | for i in range(1 << n): 85 | bit_list = utilities.bits(i, n) 86 | if sum(bit_list) % 2 == (1 - offset): 87 | constraint = quicksum( 88 | x[j] if bit_list[j] == 0 else 1 - x[j] for j in range(n) 89 | ) 90 | self.model.addConstr(constraint >= 1) 91 | if mode == "integer" or mode == "both": 92 | offset = offset % 2 93 | 94 | t = self.model.addVar( 95 | name="dummy_xor", lb=0, ub=(n // 2) + (n % 2), vtype=GRB.INTEGER 96 | ) 97 | self.model.addConstr(quicksum(x) == (2 * t) + offset) 98 | 99 | def add_bin_matrix_constr(self, matrix, x, b, mode="binary"): 100 | """ 101 | Adds constraints given by matrix * x = b 102 | where x is a list of GRB.BINARY variables 103 | and b is a constant given as an integer. 104 | """ 105 | y = utilities.bits(b, len(matrix)) 106 | 107 | for i in range(len(matrix)): 108 | row = matrix[i] 109 | assert len(row) == len(x) 110 | variables = [x[j] for j in range(len(x)) if row[j] != 0] 111 | self.add_xor_constr(variables, offset=y[i], mode=mode) 112 | 113 | def last_input_diff(self): 114 | """ 115 | Returns the value of the input difference in the last solution. 116 | """ 117 | x = 0 118 | for i in range(self.in_size): 119 | if self.in_var[i].x >= 0.5: 120 | x ^= 1 << i 121 | 122 | return x 123 | 124 | def last_output_diff(self): 125 | """ 126 | Returns the value of the output difference in the last solution. 127 | """ 128 | x = 0 129 | for i in range(self.out_size): 130 | if self.out_var[i].x >= 0.5: 131 | x ^= 1 << i 132 | 133 | return x 134 | 135 | def set_input_diff(self, in_diff): 136 | """ 137 | Sets the input difference for impossible differential 138 | search. 139 | """ 140 | bit_list = utilities.bits(in_diff, self.in_size) 141 | 142 | for i in range(self.in_size): 143 | key = "in_{}".format(i) 144 | if key in self.misc: 145 | self.model.remove(self.misc[key]) 146 | self.misc[key] = self.model.addConstr(self.in_var[i] == bit_list[i]) 147 | 148 | def set_output_diff(self, out_diff): 149 | """ 150 | Sets the output difference for impossible differential 151 | search. 152 | """ 153 | bit_list = utilities.bits(out_diff, self.out_size) 154 | 155 | for i in range(self.out_size): 156 | key = "out_{}".format(i) 157 | if key in self.misc: 158 | self.model.remove(self.misc[key]) 159 | self.misc[key] = self.model.addConstr(self.out_var[i] == bit_list[i]) 160 | 161 | def set_search_space(self, the_set): 162 | """ 163 | Sets the search space for the next search of 164 | impossible differentials. 165 | """ 166 | self.misc["imp_diff_search_space"] = the_set 167 | 168 | def format_state(self, x): 169 | """ 170 | Gives a nice representation of a state. 171 | """ 172 | return "{}".format(x) 173 | 174 | def is_possible(self, x, y): 175 | """ 176 | Outputs whether the pair (x, y) is a possible transition 177 | or not. 178 | """ 179 | self.set_input_diff(x) 180 | self.set_output_diff(y) 181 | self.model.optimize() 182 | status = self.model.status 183 | output_choices = [ 184 | gurobipy.GRB.OPTIMAL, 185 | gurobipy.GRB.INFEASIBLE, 186 | ] 187 | assert status in output_choices 188 | return status == gurobipy.GRB.OPTIMAL 189 | 190 | 191 | class AesLike(Primitive): 192 | """ 193 | Gurobi Model of an AES-like primitive. 194 | Ie, when there is only one permutation SBox 195 | and one linear layer, 196 | """ 197 | 198 | def __init__(self, state_size, nb_rounds, sbox_file): 199 | 200 | Primitive.__init__(self, state_size, state_size) 201 | 202 | self.nb_rounds = nb_rounds 203 | self.sbox_name = sbox_file 204 | 205 | with open(sbox_file, "rb") as f: 206 | (in_nibble_size, out_nibble_size, _, _) = pickle.load(f) 207 | 208 | assert in_nibble_size == out_nibble_size 209 | nibble_size = in_nibble_size 210 | 211 | assert state_size % nibble_size == 0 212 | nb_nibbles = state_size // nibble_size 213 | 214 | self.add_sbox_modeling(sbox_file) 215 | 216 | self.state_size = state_size 217 | self.nibble_size = nibble_size 218 | self.nb_nibbles = nb_nibbles 219 | 220 | in_sbox = {} # in_sbox for sbox input 221 | out_sbox = {} # out_sbox for sbox output 222 | 223 | for i, j in itp(range(nb_rounds), range(state_size)): 224 | in_sbox[i, j] = self.model.addVar( 225 | name="in_sbox_\{%s, %s\}" % (i, j), vtype=gurobipy.GRB.BINARY 226 | ) 227 | for i, j in itp(range(nb_rounds), range(state_size)): 228 | out_sbox[i, j] = self.model.addVar( 229 | name="out_sbox_\{%s, %s\}" % (i, j), vtype=gurobipy.GRB.BINARY 230 | ) 231 | 232 | for i in range(state_size): 233 | self.in_var[i] = in_sbox[0, i] 234 | self.out_var[i] = out_sbox[nb_rounds - 1, i] 235 | 236 | for i in range(nb_rounds): 237 | self.subcell( 238 | [in_sbox[i, j] for j in range(state_size)], 239 | [out_sbox[i, j] for j in range(state_size)], 240 | ) 241 | 242 | for i in range(nb_rounds - 1): 243 | self.linear_layer( 244 | [out_sbox[i, j] for j in range(state_size)], 245 | [in_sbox[i + 1, j] for j in range(state_size)], 246 | ) 247 | 248 | self.in_sbox = in_sbox 249 | self.out_sbox = out_sbox 250 | 251 | random.seed() 252 | 253 | def subcell(self, in_sbox, out_sbox): 254 | n = self.nb_nibbles 255 | d = self.nibble_size 256 | for nibble in range(n): 257 | a = [in_sbox[(d * nibble) + i] for i in range(d)] 258 | b = [out_sbox[(d * nibble) + i] for i in range(d)] 259 | self.add_sbox_constr(self.sbox_name, a, b) 260 | 261 | def linear_layer(self, x_in, x_out): 262 | """ 263 | Adds linear layer constraints for one round. 264 | """ 265 | # To be implemented for each primitive 266 | raise NotImplementedError 267 | 268 | def format_state(self, x): 269 | """ 270 | Gives a nice representation of a state. 271 | A state is represented as a list of tuples 272 | with the cell number and the value of this cell. 273 | """ 274 | if x == 0: 275 | return "0" 276 | 277 | string = "[" 278 | bit_list = utilities.bits(x, self.state_size) 279 | for i in range(self.state_size): 280 | d = self.nibble_size 281 | bit = i % d 282 | 283 | if bit == 0: 284 | val = 0 285 | val ^= bit_list[i] << bit 286 | 287 | if bit == d - 1: 288 | if val != 0: 289 | string += "(nib: {:2}, val: {:3x}), ".format(i // d, val) 290 | 291 | return string[:-2] + "]" 292 | 293 | def get_state_in_sbox(self, r): 294 | """ 295 | Gets the state at the input of round r. 296 | """ 297 | assert r < self.nb_rounds 298 | out = 0 299 | for i in range(self.state_size): 300 | if self.in_sbox[r, i].x >= 0.5: 301 | out ^= 1 << i 302 | return out 303 | 304 | def get_state_out_sbox(self, r): 305 | """ 306 | Gets the state at the output of round r. 307 | """ 308 | assert r < self.nb_rounds 309 | out = 0 310 | for i in range(self.state_size): 311 | if self.out_sbox[r, i].x >= 0.5: 312 | out ^= 1 << i 313 | return out 314 | 315 | def equimip_search(self, the_dict, aux_in, aux_out, message=""): 316 | """ 317 | More general version of the differential possibility equivalence technique 318 | of Sasaki and Todo EC17. 319 | the_dict: map (python dict) from an input difference to try to 320 | a set of output differences to try with it. 321 | aux_in: auxiliary input model of the same class with a smaller 322 | number of rounds. 323 | aux_out: same for output. 324 | """ 325 | 326 | r_in = aux_in.nb_rounds - 1 327 | r_out = aux_out.nb_rounds - 1 328 | assert r_in + r_out < self.nb_rounds 329 | assert r_in >= 0 330 | assert r_out >= 0 331 | 332 | # Useful for printing progress. START 333 | def remaining(the_dict): 334 | return sum([len(the_dict[x]) for x in iter(the_dict)]) 335 | 336 | def spaces(x): 337 | return "".join([" " for i in range(x)]) 338 | 339 | out = [] 340 | length = remaining(the_dict) 341 | nb_done = 0 342 | nb_milp = 0 343 | nb_milp_x = 0 344 | nb_milp_y = 0 345 | discarded = 0 346 | 347 | absolute_time = time.time() 348 | 349 | print( 350 | "| {}Message |".format(spaces(len(message) - 7)) 351 | + " {}Time |".format(spaces(17 - 4)) 352 | + " Results / {:10} |".format(length) 353 | + " {}MIP queries |".format(spaces(20 - 11)) 354 | + " x queries |" 355 | + " y queries |" 356 | + " Dis. rate |" 357 | + " Found |" 358 | ) 359 | 360 | def outer_printer(): 361 | seconds = int(time.time() - absolute_time) 362 | minutes = seconds // 60 363 | hours = minutes // 60 364 | str_time = "{:4}h, {:2}min, {:2}s".format(hours, minutes % 60, seconds % 60) 365 | 366 | return ( 367 | "| {} | {} | {:5.1f} % = {:10} |".format( 368 | message, str_time, (100.0 * nb_done) / length, nb_done, 369 | ) 370 | + " {:10} = {:5.2f} % |".format(nb_milp, (100.0 * nb_milp) / length,) 371 | + " {:10} |".format(nb_milp_x,) 372 | + " {:10} |".format(nb_milp_y,) 373 | + " {:5.1f} % |".format( 374 | (100.0 * discarded) / (nb_milp_x + nb_milp_y) 375 | if nb_milp_x + nb_milp_y != 0 376 | else 0, 377 | ) 378 | + " {:5} |".format(len(out)) 379 | ) 380 | 381 | # END 382 | 383 | # While there are difference pairs to try... 384 | while len(the_dict) >= 1: 385 | # x is the input difference we are going to try. 386 | x = list(the_dict)[0] 387 | while len(the_dict[x]) >= 1: 388 | outer_print = outer_printer() 389 | print(outer_print, end="\n") 390 | 391 | # y is the input difference we are going to try. 392 | y = the_dict[x].pop() 393 | 394 | # Printing this message while the solver is running 395 | # on the main model self (in the function self.is_possible) 396 | # This computation can last for a few hours. 397 | print( 398 | "MIP query on input {} and output {}".format( 399 | self.format_state(x), self.format_state(y), 400 | ), 401 | end="\r", 402 | ) 403 | possible = self.is_possible(x, y) 404 | nb_milp += 1 405 | 406 | # If we have found an impossible differential, add it to the output. 407 | if not possible: 408 | out.append((x, y)) 409 | # Else use the differential possibility equivalence technique. 410 | else: 411 | # Get the middle values in the computed path. 412 | x_mid = self.get_state_out_sbox(r_in) 413 | y_mid = self.get_state_in_sbox(self.nb_rounds - r_out - 1) 414 | 415 | to_discard = [] 416 | 417 | # Printer related stuff. START 418 | 419 | visited = 0 420 | rem = remaining(the_dict) 421 | 422 | class inner_printer: 423 | def __init__(self): 424 | self.timer = time.time() 425 | 426 | def go(self): 427 | if (time.time() - self.timer) >= 0.5: 428 | self.timer = time.time() 429 | print( 430 | "Discarding progress " 431 | + "{:.1f} % rate {:.1f} %".format( 432 | (100.0 * visited) / rem, 433 | (100.0 * len(to_discard)) / visited, 434 | ) 435 | + spaces(30), 436 | end="\r", 437 | ) 438 | 439 | ip = inner_printer() 440 | 441 | # END 442 | 443 | # For each possible input difference... 444 | for x_start in the_dict.keys(): 445 | # We first try to compute the beginning of the path 446 | # between x_start and x_mid (if x_start is not the initial x). 447 | try_y = x == x_start 448 | if not try_y: 449 | nb_milp_x += 1 450 | try_y = aux_in.is_possible(x_start, x_mid) 451 | 452 | # If a path from x_start to x_mid is found... 453 | if try_y: 454 | # For each output difference y to try with input x_start... 455 | for y in the_dict[x_start]: 456 | nb_milp_y += 1 457 | visited += 1 458 | # We check whether there is a path between y_mid and y. 459 | if aux_out.is_possible(y_mid, y): 460 | # If it is the case, we will discard the pair (x_start, y) 461 | # from input/output pairs to try. 462 | to_discard.append((x_start, y)) 463 | discarded += 1 464 | ip.go() 465 | else: 466 | visited += len(the_dict[x_start]) 467 | ip.go() 468 | 469 | for (x, y) in to_discard: 470 | the_dict[x].remove(y) 471 | 472 | nb_done = length - remaining(the_dict) 473 | 474 | # Check and clean the set of input/output pairs to try. 475 | assert len(the_dict[x]) == 0 476 | keys = list(the_dict) 477 | for key in keys: 478 | if len(the_dict[key]) == 0: 479 | del the_dict[key] 480 | 481 | nb_done = length - remaining(the_dict) 482 | outer_print = outer_printer() 483 | print(outer_print, end="\n") 484 | 485 | return out 486 | 487 | def minimize_active_sboxes(self): 488 | """ 489 | Computes the minimum number of active SBoxes. 490 | """ 491 | local_constraints = [] 492 | 493 | # variables in y model whether a CELL is active or not. 494 | y = dict() 495 | for r, i in itp(range(self.nb_rounds), range(self.nb_nibbles)): 496 | y[r, i] = self.model.addVar( 497 | name="active_({}, {})".format(r, i), vtype=GRB.BINARY, obj=1.0, 498 | ) 499 | bits = [ 500 | self.in_sbox[r, (self.nibble_size * i) + j] 501 | for j in range(self.nibble_size) 502 | ] 503 | constr = self.model.addGenConstrOr(y[r, i], bits) 504 | local_constraints.append(constr) 505 | 506 | # We fix at least one active input cell. 507 | constr = self.model.addConstr( 508 | quicksum(y[0, i] for i in range(self.nb_nibbles)) >= 1 509 | ) 510 | local_constraints.append(constr) 511 | 512 | self.model.optimize() 513 | 514 | output = [] 515 | if self.model.status == gurobipy.GRB.OPTIMAL: 516 | for r, i in itp(range(self.nb_rounds), range(self.nb_nibbles)): 517 | if y[r, i].x >= 0.5: 518 | output.append((r, i)) 519 | assert len(output) == int(self.model.objVal) 520 | 521 | else: 522 | print("Optimization ended with status {}".format(self.model.status)) 523 | exit(1) 524 | 525 | # We clean the model from local constraints and variables. 526 | for constr in local_constraints: 527 | self.model.remove(constr) 528 | for r, i in itp(range(self.nb_rounds), range(self.nb_nibbles)): 529 | self.model.remove(y[r, i]) 530 | 531 | return output 532 | -------------------------------------------------------------------------------- /impossible_differentials/skinny.py: -------------------------------------------------------------------------------- 1 | import gurobipy 2 | from primitive import AesLike 3 | from itertools import product as itp 4 | from itertools import starmap as itsm 5 | import utilities 6 | 7 | shift_rows = [0, 1, 2, 3, 7, 4, 5, 6, 10, 11, 8, 9, 13, 14, 15, 12] 8 | 9 | mixcol_equiv = [ 10 | [0, 0, 0, 1, 1, 0, 0, 1], 11 | [1, 0, 0, 0, 0, 1, 0, 0], 12 | [0, 1, 1, 0, 0, 0, 1, 0], 13 | [1, 0, 1, 0, 0, 0, 0, 1], 14 | ] 15 | 16 | mixcol_origin = [ 17 | [1, 0, 1, 1, 1, 0, 0, 0], 18 | [1, 0, 0, 0, 0, 1, 0, 0], 19 | [0, 1, 1, 0, 0, 0, 1, 0], 20 | [1, 0, 1, 0, 0, 0, 0, 1], 21 | ] 22 | 23 | 24 | class Skinny(AesLike): 25 | """ Gurobi Model for Skinny-128 differential trails. """ 26 | 27 | def __init__(self, nb_rounds, sbox_file, mixcol="equiv"): 28 | 29 | # Different choices of MixColumns models. 30 | if mixcol == "equiv": 31 | self.mixcol = mixcol_equiv 32 | else: 33 | self.mixcol = mixcol_origin 34 | AesLike.__init__(self, 128, nb_rounds, sbox_file) 35 | 36 | def linear_layer(self, x_in, x_out): 37 | x_in = [ 38 | x_in[(8 * shift_rows[i // 8]) + (i % 8)] for i in range(128) 39 | ] # variables after the shift_rows, at the input of mixcol 40 | 41 | for col, bit in itp(range(4), range(8)): 42 | bit_list = [x_in[(32 * row) + (8 * col) + bit] for row in range(4)] + [ 43 | x_out[(32 * row) + (8 * col) + bit] for row in range(4) 44 | ] 45 | 46 | self.add_bin_matrix_constr( 47 | self.mixcol, bit_list, 0, mode="binary", 48 | ) 49 | 50 | 51 | def lin_layer(x): 52 | """ Skinny linear layer implementation for testing purposes. """ 53 | # Collect in cells 54 | nib = {} 55 | for i in range(16): 56 | nib[i] = (x >> (8 * i)) & 0xFF 57 | 58 | # Shift rows 59 | nib2 = {} 60 | for row in range(4): 61 | for j in range(4): 62 | nib2[(4 * row) + ((j + row) % 4)] = nib[(4 * row) + j] 63 | 64 | # Collect in rows 65 | row = {} 66 | for i in range(4): 67 | row[i] = 0 68 | for j in range(4): 69 | row[i] ^= nib2[(4 * i) + j] << (8 * j) 70 | 71 | # Mixcolumns 72 | row[1] ^= row[2] 73 | row[2] ^= row[0] 74 | row[3] ^= row[2] 75 | 76 | row2 = {} 77 | for i in range(4): 78 | row2[i] = row[(i - 1) % 4] 79 | 80 | # Collect in int 81 | out = 0 82 | for i in range(4): 83 | out ^= row2[i] << (32 * i) 84 | 85 | return out 86 | 87 | 88 | def test_linear_layer(): 89 | """ 90 | Testing the modeling of the linear layer with identity Sbox. 91 | """ 92 | n = 10 93 | mid = Skinny(n, "identity_sbox_8.pkl") 94 | mid.model.setParam("LogToConsole", 0) 95 | 96 | the_set = set() 97 | for i in range(16): 98 | x = 1 << (8 * i) 99 | y = x 100 | for j in range(n - 1): 101 | y = lin_layer(y) 102 | the_set.add((x, y)) 103 | 104 | res = mid.search_impossible_diff(the_set, message="Linear layer test.") 105 | assert len(res) == 0 106 | 107 | the_set = set() 108 | for i in range(16): 109 | x = 1 << (8 * i) 110 | y = x 111 | for j in range(n - 1): 112 | y = lin_layer(y) 113 | the_set.add((x, y ^ 1)) 114 | 115 | res = mid.search_impossible_diff(the_set, message="Linear layer test.") 116 | assert len(res) == 16 117 | 118 | print("Linear layer test OK.") 119 | 120 | 121 | def test_paper_single_impossible_diff(): 122 | """ 123 | Testing the model with arbitrary Sbox against 124 | the truncated impossible differential trail given in the 125 | eprint paper: 126 | The SKINNY Family of Block Ciphers and its Low-Latency Variant MANTIS. 127 | from the authors of Skinny. 128 | """ 129 | n = 11 130 | mid = Skinny(n, "arbitrary_sbox_8_8.pkl") 131 | mid.model.setParam("LogToConsole", 0) 132 | 133 | the_set = set() 134 | x = 1 << (8 * 12) 135 | y = 1 << (8 * 7) 136 | the_set.add((x, y)) 137 | 138 | res = mid.search_impossible_diff( 139 | the_set, message="Paper single impossible differential." 140 | ) 141 | assert len(res) == 1 142 | 143 | print("Single impossible differential OK.") 144 | 145 | 146 | def test_paper_all_impossible_diff(): 147 | """ 148 | Same as above with all the impossible differentials. 149 | """ 150 | n = 12 151 | mid = Skinny(n, "arbitrary_sbox_8_8.pkl") 152 | mid.model.setParam("LogToConsole", 0) 153 | 154 | the_set = set() 155 | for i in range(16): 156 | x = 1 << (8 * i) 157 | for j in range(16): 158 | y = 1 << (8 * j) 159 | the_set.add((x, y)) 160 | 161 | res = mid.search_impossible_diff(the_set, message="Paper impossible differentials.") 162 | 163 | assert len(res) == 12 164 | 165 | print("All impossible differentials test OK.") 166 | 167 | 168 | if __name__ == "__main__": 169 | """ 170 | This section aims at testing this MIP model of Skinny 171 | for impossible differential search. 172 | """ 173 | test_linear_layer() 174 | test_paper_single_impossible_diff() 175 | test_paper_all_impossible_diff() 176 | -------------------------------------------------------------------------------- /impossible_differentials/skinny_sbox.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnlcog/efficient_milp_modelings/80ea54c6628535877cb2c45c23d56589f05873f9/impossible_differentials/skinny_sbox.pkl -------------------------------------------------------------------------------- /impossible_differentials/utilities.py: -------------------------------------------------------------------------------- 1 | def bits(n, size): 2 | output = [0] * size 3 | for i in range(size): 4 | output[i] = (n >> i) & 1 5 | return output 6 | 7 | 8 | def log2(n): 9 | return n.bit_length() - 1 10 | 11 | 12 | def ddt_rows(ddt, in_size, out_size): 13 | out = {} 14 | for a in range(1 << in_size): 15 | out[a] = set() 16 | for b in range(1 << out_size): 17 | if ddt[a, b] != 0: 18 | out[a].add(b) 19 | return out 20 | 21 | 22 | def ddt_cols(ddt, in_size, out_size): 23 | out = {} 24 | for b in range(1 << out_size): 25 | out[b] = set() 26 | for a in range(1 << in_size): 27 | if ddt[a, b] != 0: 28 | out[b].add(a) 29 | return out 30 | 31 | 32 | # def product(my_list, repeat=1): 33 | # # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy 34 | # # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111 35 | # pools = [tuple(pool) for pool in my_list] * repeat 36 | # result = [[]] 37 | # for pool in pools: 38 | # result = [x + [y] for x in result for y in pool] 39 | # for prod in result: 40 | # yield tuple(prod) 41 | 42 | 43 | def hwt(x): 44 | return bin(x).count("1") 45 | -------------------------------------------------------------------------------- /sbox/arbitrary_sbox_gen.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import sys 3 | 4 | 5 | def generate_arbitrary_sbox(in_size, out_size): 6 | """ 7 | Generates an arbitrary sbox pickle file as explained in 8 | Sasaki Todo EC17. 9 | """ 10 | ineg_set = set() 11 | for output in range(out_size): 12 | ineg = ([1] * in_size) + ([0] * (out_size + 1)) 13 | ineg[output + in_size] = -1 14 | ineg_set.add(tuple(ineg)) 15 | for inp in range(in_size): 16 | ineg = ([0] * in_size) + ([1] * out_size) + [0] 17 | ineg[inp] = -1 18 | ineg_set.add(tuple(ineg)) 19 | 20 | ddt = {} 21 | for a in range(1 << in_size): 22 | for b in range(1 << out_size): 23 | ddt[a, b] = 1 if a != b else 0 24 | 25 | with open("arbitrary_sbox_{}_{}.pkl".format(in_size, out_size), "wb") as f: 26 | pickle.dump((in_size, out_size, ddt, ineg_set), f, 3) 27 | 28 | 29 | if __name__ == "__main__": 30 | try: 31 | input_size = int(sys.argv[1]) 32 | except IndexError: 33 | raise SystemExit( 34 | "Usage: {} ".format(sys.argv[0]) + " " 35 | ) 36 | 37 | try: 38 | output_size = int(sys.argv[2]) 39 | except IndexError: 40 | output_size = input_size 41 | 42 | generate_arbitrary_sbox(input_size, output_size) 43 | -------------------------------------------------------------------------------- /sbox/check_model.sage: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pickle 3 | from sage.crypto.sboxes import SBox 4 | from sage.crypto.sboxes import sboxes 5 | from operator import xor 6 | 7 | 8 | def point_kept(in_size, out_size, inequality, a, b): 9 | """ 10 | Checks whether inequality is satisfied by point (a, b) 11 | """ 12 | result = inequality[in_size + out_size] 13 | for i in range(in_size): 14 | result += inequality[i] * ((a >> i) & 1) 15 | for i in range(out_size): 16 | result += inequality[i + in_size] * ((b >> i) & 1) 17 | return result >= 0 18 | 19 | 20 | def check_ddt_ineq(in_size, out_size, ddt, ineqs): 21 | """ 22 | Checks the sbox model given by ineqs against the ddt. 23 | """ 24 | ddt_ones = [(a, b) for (a, b) in ddt if ddt[a, b] != 0] 25 | ddt_zeros = set([(a, b) for (a, b) in ddt if ddt[a, b] == 0]) 26 | length = len(ineqs) 27 | i = 0 28 | for ineq in ineqs: 29 | print(" {} / {}".format(i, length), end="\r") 30 | for (a, b) in ddt_ones: 31 | assert point_kept(in_size, out_size, ineq, a, b) 32 | elim = [] 33 | for (a, b) in ddt_zeros: 34 | if not point_kept(in_size, out_size, ineq, a, b): 35 | elim.append((a, b)) 36 | for (a, b) in elim: 37 | ddt_zeros.remove((a, b)) 38 | i += 1 39 | assert len(ddt_zeros) == 0 40 | 41 | 42 | def aes_equiv(): 43 | """ 44 | Generates the affine equivalent Sbox for the AES 45 | """ 46 | 47 | def qmat(x): 48 | y = xor(x, xor((x >> 7) & 1, ((x >> 7) & 1) << 3)) 49 | return y 50 | 51 | aes = sboxes["AES"] 52 | value_table = [qmat(aes[x]) for x in range(256)] 53 | return SBox(value_table) 54 | 55 | 56 | if __name__ == "__main__": 57 | 58 | parser = argparse.ArgumentParser( 59 | description="Checks wether the sbox model in the pickle file is correct." 60 | ) 61 | parser.add_argument( 62 | "sbox", 63 | type=str, 64 | choices=list(sboxes.keys()) + ["AES_equiv"], 65 | help="Sbox that should be modeled.", 66 | ) 67 | parser.add_argument( 68 | "ineq_file", 69 | type=str, 70 | help="Pickle file containing model" + "(in_size, out_size, ddt, ineq_set).", 71 | ) 72 | args = parser.parse_args() 73 | 74 | if args.sbox == "AES_equiv": 75 | sbox = aes_equiv() 76 | else: 77 | sbox = sboxes[args.sbox] 78 | 79 | i = sbox.input_size() 80 | o = sbox.output_size() 81 | ddt_sage = sbox.difference_distribution_table() 82 | 83 | with open(args.ineq_file, "rb") as f: 84 | data = pickle.load(f) 85 | 86 | for x in list(data): 87 | print(type(x)) 88 | 89 | (in_size, out_size, ddt, ineq_set) = data 90 | 91 | # Checks the first three elements of the tuple. 92 | assert in_size == i 93 | assert out_size == o 94 | for a in range(1 << i): 95 | for b in range(1 << o): 96 | assert ddt[a, b] == ddt_sage[a, b] 97 | 98 | check_ddt_ineq(in_size, out_size, ddt, ineq_set) 99 | 100 | print( 101 | "This model of {} with {} inequalities is correct.".format( 102 | args.sbox, len(ineq_set) 103 | ) 104 | ) 105 | -------------------------------------------------------------------------------- /sbox/convex_hull.sage: -------------------------------------------------------------------------------- 1 | from utilities import * 2 | from sage.crypto.sboxes import SBox 3 | from sage.crypto.sboxes import sboxes 4 | import itertools 5 | import pickle 6 | import time 7 | import argparse 8 | 9 | 10 | def sbox(sbox_name): 11 | """ 12 | Returns the SBox object from its name. 13 | """ 14 | if sbox_name == "Keccak": 15 | return SBox( 16 | [ 17 | 0, 18 | 9, 19 | 18, 20 | 11, 21 | 5, 22 | 12, 23 | 22, 24 | 15, 25 | 10, 26 | 3, 27 | 24, 28 | 1, 29 | 13, 30 | 4, 31 | 30, 32 | 7, 33 | 20, 34 | 21, 35 | 6, 36 | 23, 37 | 17, 38 | 16, 39 | 2, 40 | 19, 41 | 26, 42 | 27, 43 | 8, 44 | 25, 45 | 29, 46 | 28, 47 | 14, 48 | 31, 49 | ] 50 | ) 51 | elif sbox_name == "Pyj3": 52 | return SBox([1, 3, 6, 5, 2, 4, 7, 0]) 53 | elif sbox_name == "Pyj4": 54 | return SBox([2, 0xD, 3, 9, 7, 0xB, 0xA, 6, 0xE, 0, 0xF, 4, 8, 5, 1, 0xC]) 55 | else: 56 | return sboxes[sbox_name] 57 | 58 | 59 | def sets_from_ddt(ddt): 60 | """ 61 | Given a DDT, returns the sets of possible and impossible points. 62 | """ 63 | pos_trans = set([]) 64 | imp_trans = set([]) 65 | n, m = ddt.dimensions() 66 | for in_diff in range(n): 67 | for out_diff in range(m): 68 | point = int(in_diff + (out_diff << log2(n))) 69 | if ddt[in_diff, out_diff] == 0: 70 | imp_trans.add(point) 71 | else: 72 | pos_trans.add(point) 73 | return (pos_trans, imp_trans) 74 | 75 | 76 | def inequalities(sbox, nb_faces=2): 77 | """ 78 | Given an SBox object, computes big set of inequalities 79 | with faces from the convex hull of possible points and 80 | the additions of at most nb_faces of them. 81 | """ 82 | ddt = sbox.difference_distribution_table() 83 | in_size = sbox.input_size() 84 | out_size = sbox.output_size() 85 | n = in_size + out_size 86 | 87 | pos_trans, imp_trans = sets_from_ddt(ddt) 88 | point_set = imp_trans.copy() 89 | 90 | print("Building polyhedron") 91 | pos_polyhedron = Polyhedron(vertices=[bits(point, n) for point in pos_trans]) 92 | 93 | # We collect faces of the convex hull as pure python objects. 94 | ineqs = [[int(x) for x in ineg] for ineg in pos_polyhedron.inequalities()] 95 | 96 | # Our personal way of writing an inequality puts the constant 97 | # at the end of the tuple. 98 | ineqs = [tuple(ineg[1:] + [ineg[0]]) for ineg in ineqs] 99 | 100 | eqs = pos_polyhedron.equations() 101 | assert len(eqs) == 0 102 | 103 | # We keep the polyhedron faces. 104 | ineg_set = set([x for x in ineqs]) 105 | 106 | print("Computing discarded points for faces.") 107 | ineg_to_points = {} 108 | for ineg in ineg_set: 109 | ineg_to_points[ineg] = set([]) 110 | for point in point_set: 111 | if not (point_kept(point, ineg)): 112 | ineg_to_points[ineg].add(point) 113 | 114 | print("Currently {} inequalities. Removing inclusions...".format(len(ineg_set))) 115 | for ineg1 in ineg_set: 116 | if ineg1 in ineg_to_points: 117 | for ineg2 in ineg_set: 118 | if (ineg1 != ineg2) and (ineg2 in ineg_to_points): 119 | if ineg_to_points[ineg2] <= ineg_to_points[ineg1]: 120 | del ineg_to_points[ineg2] 121 | 122 | ineg_set = set(ineg_to_points.keys()) 123 | 124 | ineqs = ineg_set.copy() 125 | 126 | print("Now {} inequalities.".format(len(ineg_set))) 127 | 128 | if nb_faces >= 2: 129 | 130 | # We add new equations obtained by summing up to nb_faces faces. 131 | count = 0 132 | for center_point in pos_trans: 133 | count += 1 134 | print("center point: {}".format(count)) 135 | 136 | faces = [face for face in ineqs if vertex_is_on_face(center_point, face)] 137 | print(" # of faces: {}".format(len(faces))) 138 | 139 | local_inegs = set([x for x in faces]) 140 | add_counter = 0 141 | total_counter = 0 142 | 143 | for face_indices in itertools.product(range(len(faces)), repeat=nb_faces): 144 | total_counter += 1 145 | new_ineq = [int(0) for i in range(n + 1)] 146 | for face_index in face_indices: 147 | face = faces[face_index] 148 | for i in range(n + 1): 149 | new_ineq[i] += face[i] 150 | new_ineg = tuple(new_ineq) 151 | 152 | if new_ineg not in ineg_set: 153 | 154 | new_elim_points = set([]) 155 | for point in point_set: 156 | if not (point_kept(point, new_ineg)): 157 | new_elim_points.add(point) 158 | 159 | to_add = True 160 | for ineg in local_inegs: 161 | if new_elim_points <= ineg_to_points[ineg]: 162 | to_add = False 163 | break 164 | 165 | if to_add: 166 | add_counter += 1 167 | local_inegs.add(new_ineg) 168 | ineg_set.add(new_ineg) 169 | ineg_to_points[new_ineg] = new_elim_points 170 | 171 | print(" added {} inequalities.".format(add_counter)) 172 | 173 | print("Currently {} inequalities. Removing inclusions...".format(len(ineg_set))) 174 | for ineg1 in ineg_set: 175 | if ineg1 in ineg_to_points: 176 | for ineg2 in ineg_set: 177 | if (ineg1 != ineg2) and (ineg2 in ineg_to_points): 178 | if ineg_to_points[ineg2] <= ineg_to_points[ineg1]: 179 | del ineg_to_points[ineg2] 180 | 181 | ineg_set = set(ineg_to_points.keys()) 182 | 183 | print("Building point_to_inegs then checking.") 184 | point_to_inegs = {} 185 | for point in point_set: 186 | point_to_inegs[point] = set([]) 187 | for ineg in ineg_set: 188 | if point in ineg_to_points[ineg]: 189 | point_to_inegs[point].add(ineg) 190 | assert len(point_to_inegs[point]) >= 1 191 | 192 | for point in pos_trans: 193 | for ineg in ineg_set: 194 | assert point_kept(point, ineg) 195 | 196 | print("Finished :" + " {} inequalities".format(len(ineg_set))) 197 | 198 | # pure python results 199 | n = int(n) 200 | pure_ddt = dict() 201 | for a in range(int(1) << int(in_size)): 202 | for b in range(int(1) << int(out_size)): 203 | pure_ddt[int(a), int(b)] = int(ddt[a, b]) 204 | in_size = int(in_size) 205 | out_size = int(out_size) 206 | 207 | return ( 208 | in_size, 209 | out_size, 210 | pure_ddt, 211 | ineg_set, 212 | point_set, 213 | ineg_to_points, 214 | point_to_inegs, 215 | ) 216 | 217 | 218 | if __name__ == "__main__": 219 | parser = argparse.ArgumentParser( 220 | description="Compute big set of inequalities with convex hull technique." 221 | ) 222 | parser.add_argument( 223 | "sbox_name", type=str, 224 | ) 225 | parser.add_argument( 226 | "nb_faces", type=int, 227 | ) 228 | args = parser.parse_args() 229 | 230 | res = inequalities(sbox(args.sbox_name), args.nb_faces) 231 | 232 | print("Writing pickle file.") 233 | with open("{}_hull_{}.pkl".format(args.sbox_name, args.nb_faces), "wb") as f: 234 | pickle.dump(res, f, int(3)) 235 | -------------------------------------------------------------------------------- /sbox/identity_sbox_gen.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import sys 3 | 4 | 5 | def generate_identity_sbox(size): 6 | ineg_set = set() 7 | for i in range(size): 8 | ineg = [0] * (2 * size + 1) 9 | ineg[i] = 1 10 | ineg[i + size] = -1 11 | ineg_set.add(tuple(ineg)) 12 | ineg = [0] * (2 * size + 1) 13 | ineg[i] = -1 14 | ineg[i + size] = 1 15 | ineg_set.add(tuple(ineg)) 16 | 17 | ddt = {} 18 | ddt[0, 0] = 1 19 | for a in range(1, 1 << size): 20 | for b in range(1 << size): 21 | ddt[a, b] = 0 if a != b else 1 22 | ddt[0, a] = 0 23 | 24 | with open("identity_sbox_{}.pkl".format(size), "wb") as f: 25 | pickle.dump((size, size, ddt, ineg_set), f, 3) 26 | 27 | 28 | if __name__ == "__main__": 29 | try: 30 | size = int(sys.argv[1]) 31 | except IndexError: 32 | raise SystemExit("Usage: {} ".format(sys.argv[0]) + "") 33 | 34 | generate_identity_sbox(size) 35 | -------------------------------------------------------------------------------- /sbox/minimize.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pickle 3 | from gurobipy import * 4 | 5 | 6 | def build_model(ineq_set, point_set, ineq_to_points, point_to_ineqs, start=None): 7 | """ 8 | For the mode milp, this function builds the model as explained in ST17a. 9 | """ 10 | model = Model("small milp") 11 | # model.hideOutput() 12 | 13 | z = {} 14 | for ineq in ineq_set: 15 | z[ineq] = model.addVar(name="z_{}".format(ineq), vtype=GRB.BINARY) 16 | if start is not None: 17 | for ineq in ineq_set: 18 | if ineq in start: 19 | z[ineq].start = 1.0 20 | else: 21 | z[ineq].start = 0.0 22 | 23 | for point in point_set: 24 | model.addConstr( 25 | quicksum(z[ineq] for ineq in point_to_ineqs[point]) >= 1, 26 | name="point {}".format(point), 27 | ) 28 | return (model, z) 29 | 30 | 31 | def optimize( 32 | ineq_set, 33 | point_set, 34 | ineq_to_points, 35 | point_to_ineqs, 36 | number=None, 37 | start=None, 38 | threshold=None, 39 | ): 40 | """ 41 | Main function for the mode milp. 42 | ineq_set: big set of inequalities 43 | point_set: set of impossible points 44 | ineq_to_points: the map from inequalities to the impossible points they discard 45 | point_to_ineqs: the map from impossible points to the inequalities that discard them 46 | number: 47 | None -> minimize as much as possible 48 | _ -> the exact number of inequalities desired in the sbox model 49 | start: optional starting solution (usually given by the greedy algorithm) 50 | threshold: optional parameter to speed up the computation when ineq_set is too large 51 | only keeps the best inequalities for each impossible point for 52 | the computation 53 | """ 54 | 55 | # If there is a threshold, we update ineq_set and point_to_ineqs 56 | if threshold is not None: 57 | mut_ineq_set = set() 58 | mut_point_to_ineqs = {} 59 | 60 | # For each point, we keep the best ineqs 61 | for point in point_set: 62 | key = lambda ineq: len(ineq_to_points[ineq]) 63 | sorted_ineqs = sorted(list(point_to_ineqs[point]), key=key) 64 | best_ineqs = sorted_ineqs[:threshold] 65 | mut_point_to_ineqs[point] = set(best_ineqs) 66 | mut_ineq_set.update(set(best_ineqs)) 67 | 68 | # And we keep the ineqs in the start set 69 | if start is not None: 70 | for ineq in start: 71 | mut_ineq_set.add(ineq) 72 | for point in ineq_to_points[ineq]: 73 | mut_point_to_ineqs[point].add(ineq) 74 | 75 | ineq_set = mut_ineq_set 76 | point_to_ineqs = mut_point_to_ineqs 77 | 78 | model, z = build_model(ineq_set, point_set, ineq_to_points, point_to_ineqs, start) 79 | 80 | if number is None: 81 | model.setObjective(quicksum(z[ineq] for ineq in ineq_set), GRB.MINIMIZE) 82 | else: 83 | model.addConstr(quicksum(z[ineq] for ineq in ineq_set) <= number) 84 | model.setObjective( 85 | quicksum(z[ineq] * len(ineq_to_points[ineq]) for ineq in ineq_set), 86 | GRB.MAXIMIZE, 87 | ) 88 | 89 | model.optimize() 90 | 91 | status = model.getAttr(GRB.Attr.Status) 92 | 93 | if status == GRB.INF_OR_UNBD or status == GRB.INFEASIBLE or status == GRB.UNBOUNDED: 94 | print("The model cannot be solved because it is infeasible or unbounded") 95 | sys.exit(1) 96 | if status != GRB.OPTIMAL: 97 | print("Optimization was stopped with status ", status) 98 | sys.exit(1) 99 | 100 | final_ineq_set = set() 101 | for ineq in ineq_set: 102 | if z[ineq].x >= 0.5: 103 | final_ineq_set.add(ineq) 104 | 105 | return final_ineq_set 106 | 107 | 108 | def greedy_start( 109 | ineq_set, point_set, ineq_to_points, 110 | ): 111 | """ 112 | Main function for the mode greedy. 113 | See optilize() for parameters description. 114 | """ 115 | 116 | mut_point_set = point_set.copy() 117 | mut_ineq_set = ineq_set.copy() 118 | mut_ineq_to_points = ineq_to_points.copy() 119 | 120 | greedy_ineqs = set() 121 | 122 | while len(mut_point_set) > 0: 123 | # Search for best ineq 124 | best = mut_ineq_set.pop() 125 | mut_ineq_set.add(best) 126 | for ineq in mut_ineq_set: 127 | if len(mut_ineq_to_points[ineq]) > len(mut_ineq_to_points[best]): 128 | best = ineq 129 | points_of_best = mut_ineq_to_points.pop(best) 130 | mut_ineq_set.remove(best) 131 | greedy_ineqs.add(best) 132 | 133 | # Remove the points discarded by best from point sets 134 | mut_point_set.difference_update(points_of_best) 135 | for ineq in mut_ineq_set: 136 | mut_ineq_to_points[ineq].difference_update(points_of_best) 137 | 138 | return greedy_ineqs 139 | 140 | 141 | if __name__ == "__main__": 142 | 143 | parser = argparse.ArgumentParser( 144 | description="Reduction from big sets of inequalities to sbox models." 145 | ) 146 | parser.add_argument( 147 | "ineq_file", 148 | type=str, 149 | help="Pickle file containing tuple " 150 | + "(in_size, out_size, ddt, ineq_set, point_set, " 151 | + "ineq_to_points, point_to_ineqs).", 152 | ) 153 | parser.add_argument( 154 | "-m", 155 | type=str, 156 | dest="mode", 157 | default="greedy", 158 | choices=["milp", "greedy"], 159 | help="Either milp or greedy.", 160 | ) 161 | parser.add_argument( 162 | "-n", 163 | type=int, 164 | dest="number", 165 | help="Number of inequalities if the chosen mode is milp.", 166 | ) 167 | parser.add_argument( 168 | "-t", 169 | type=int, 170 | dest="threshold", 171 | help="Number of inequalities per point kept for building the model" 172 | + " if the chosen mode is milp.", 173 | ) 174 | parser.add_argument( 175 | "-sf", 176 | type=str, 177 | dest="start_file", 178 | help="Pickle file with a starting solution (set of inequalities)" 179 | + " if the chosen mode is milp.", 180 | ) 181 | args = parser.parse_args() 182 | 183 | output_file = "sbox_{}".format(args.ineq_file) 184 | 185 | with open(args.ineq_file, "rb") as f: 186 | ( 187 | in_size, 188 | out_size, 189 | ddt, 190 | ineq_set, 191 | point_set, 192 | ineq_to_points, 193 | point_to_ineqs, 194 | ) = pickle.load(f) 195 | 196 | if args.mode == "greedy": 197 | final_set = greedy_start(ineq_set, point_set, ineq_to_points) 198 | output_file = "greedy_" + output_file 199 | else: 200 | if args.start_file is not None: 201 | with open(args.start_file, "rb") as f: 202 | (_, _, ddt_start, ineq_start) = pickle.load(f) 203 | assert ddt_start == ddt 204 | else: 205 | ineq_start = None 206 | 207 | final_set = optimize( 208 | ineq_set, 209 | point_set, 210 | ineq_to_points, 211 | point_to_ineqs, 212 | number=args.number, 213 | start=ineq_start, 214 | threshold=args.threshold, 215 | ) 216 | 217 | if args.number is None: 218 | output_file = "milp_minimal_" + output_file 219 | else: 220 | output_file = "milp_{}_".format(args.number) + output_file 221 | 222 | with open(output_file, "wb") as f: 223 | pickle.dump((in_size, out_size, ddt, final_set), f, 3) 224 | -------------------------------------------------------------------------------- /sbox/utilities.py: -------------------------------------------------------------------------------- 1 | def bits(n, size): 2 | output = [0] * size 3 | for i in range(size): 4 | output[i] = (n >> i) & 1 5 | return output 6 | 7 | 8 | def log2(n): 9 | return n.bit_length() - 1 10 | 11 | 12 | def scalar_prod(a, b): 13 | res = 0 14 | for x, y in zip(a, b): 15 | res += x * y 16 | return res 17 | 18 | 19 | def point_kept(point, ineg): 20 | alpha = scalar_prod(bits(point, len(ineg) - 1), ineg[:-1]) + ineg[-1] 21 | return alpha >= 0 22 | 23 | 24 | def vertex_is_on_face(point, ineg): 25 | alpha = scalar_prod(bits(point, len(ineg) - 1), ineg[:-1]) + ineg[-1] 26 | return alpha == 0 27 | 28 | 29 | def xor_cons(model, input_list, output): 30 | # output represents a bit and input_list 31 | # several bits 32 | n = len(input_list) 33 | 34 | for i in range(1 << n): 35 | bit_list = bits(i, n) 36 | constant = -1 + sum(bit_list) 37 | constraint = qs( 38 | input_list[j] if bit_list[j] == 0 else -input_list[j] for j in range(n) 39 | ) 40 | xor = 0 41 | for j in range(n): 42 | xor ^= bit_list[j] 43 | 44 | if xor == 0: 45 | constraint -= output 46 | constant += 1 47 | else: 48 | constraint += output 49 | 50 | 51 | def nor_cons(model, input_list, output): 52 | n = len(input_list) 53 | 54 | for i in range(1 << n): 55 | bit_list = bits(i, n) 56 | constant = -1 + sum(bit_list) 57 | constraint = qs( 58 | input_list[j] if bit_list[j] == 0 else -input_list[j] for j in range(n) 59 | ) 60 | res = 1 61 | for j in range(n): 62 | res &= 1 - bit_list[j] 63 | 64 | if res == 0: 65 | constraint -= output 66 | constant += 1 67 | else: 68 | constraint += output 69 | --------------------------------------------------------------------------------