├── 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]) + "