-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathparallel_grover.py
More file actions
376 lines (321 loc) · 15.3 KB
/
parallel_grover.py
File metadata and controls
376 lines (321 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
#!/usr/bin/env sage
from sage.all import RealField, pi, arcsin, sqrt, sin, log, floor, ceil, e, exp, Infinity
R = RealField(1000)
def theta(N, t):
"""
Given search space of size N with t solutions, returns angle theta that each Grover iteration advances.
"""
return R(arcsin(sqrt(R(t)/N)))
def p_succ(i, N, t):
"""
Success probability after i iterations of measuring a solution.
:params i: Grover's iterations
:param N: Grover's search space size
:params t: target solutions in search space
"""
return R((sin((2*i+1)*theta(N, t)))**2)
def p_succ_outer(p, S):
"""
Success probability of outer parallelisation strategy using S computers, each with success probability p.
:params p: success probability for single computer
:params S: total number of parallel computers
"""
return R((1-(1-p)**S))
def p_succ_outer_inv(P, S):
return R(p_succ_outer(P, R(1)/S))
def iterations(p, N, t):
"""
Grover succeeds with probability p(j) = sin^2((2j+1)theta).
This is the inverse function for obtaining the number of iterations j from theta and p(j).
:params p: Grover's success prob
:params N: Grover's search space size
:params t: targets. Right now, assumed to be 1.
"""
return (R(arcsin(sqrt(p)))/theta(N,t) - 1)/2
def cost_out_p(p, S, w, d, N, t):
"""
DW cost for running Grover on S machines to achieve success probability p.
:params p: Grover's success prob
:params S: total number of parallel computers
:params w: width of the Grover oracle in qubits (=G_W)
:params d: depth of the Grover oracle (=G_D)
:params N: Grover's search space size
:params t: targets, i.e. number of solutions to the problem
"""
return S * w * d * iterations(p, N, t)
def printS(P, w, d, N, t, Smax, M):
step = (Smax-1)//M
for i in xrange(M+1):
Si = 1 + step*i
p = p_succ_outer_inv(P, Si)
it = iterations(p, N, t)
c = cost_out_p(p, Si, w, d, N, t)
print "S = %s, p = %.2f, iter = %.2f, cost = %.2f" % (Si, p, it, log(c, 2))
# ////////////////////////////////////////////////////////
def format_pow(a):
"""
Given positive real a, format as [0, 1) x 2 ^ {integer power}
:params a: positive real
:returns (fa, ea): a = fa x 2 ^ ea
"""
loga = log(R(a), 2)
try:
ea = floor(loga)
except:
import pdb; pdb.set_trace()
fa = R(a)/2**ea
return (fa, ea)
logMDs = [40, 64, 96]
def spurious_keys_probability(k, n, r, S):
"""
Inner parallelization can result in reducing the number of plaintext-ciphertext blocks
required for a success probability=1 attack, by separating spurious keys in
different search spaces.
We compute here the probability that this is _not_ the case.
:params k: key length
:params n: block size
:params r: number of plaintext-ciphertext pairs
:params S: number of key space subsets/parallel instances
"""
p = 1 - exp(-2**(k-n*r)/S)
return p
def instances(GD, N, MD, p):
"""
The formula from "Optimizing the oracle under a depth limit". Assuming single-target, t = 1.
:params GD: Grover's oracle depth
:params N: keyspace size
:params MD: MAXDEPTH
:params p: target success probability
Assuming p = 1
In depth MD can fit j = floor(MD/GD) iterations.
These give probability 1 for a search space of size M.
p(j) = sin((2j+1)theta)**2
1 = sin((2j+1)theta)**2
1 = sin((2j+1)theta)
(2j+1)theta = pi/2
theta = pi/(2(2j+1)) = sqrt(t/M) = 1/sqrt(M).
sqrt(M) = 2(2j+1)/pi
M = (2(2j+1)/pi)**2
Hence need S = ceil(N/M) machines.
S = ceil(N/(2(2*floor(MD/GD)+1)/pi)**2)
Else
Could either lower each individual computer's success prob, since the target is inside only one computer's state.
Then given a requested p, we have
p = sin((2j+1)theta)**2
arcsine(sqrt(p)) = (2j+1)theta = (2j+1)/sqrt(M)
M = (2j+1)**2/arcsine(sqrt(p))**2
S = ceil(N*(arcsine(sqrt(p))/(2j+1))**2)
Or could just run full depth but have less machines.
For a target p, one would choose to have ceil(p*S) machines, where S is chosen as in the p = 1 case.
Then look at which of both strategies gives lower cost.
"""
# compute the p=1 case first
S1 = ceil(N/(2*(2*floor(MD/GD)+1)/pi)**2)
# An alternative reasoning giving the same result for p == 1 (up to a very small difference):
# Inner parallelisation gives sqrt(S) speedup without loosing success prob.
# Set ceil(sqrt(N) * pi/4) * GD/sqrt(S) = MAXDEPTH
# S1 = float(ceil(((pi*sqrt(N)/4) * GD / MD)**2))
if p == 1:
return S1
else:
Sp = ceil(N*(arcsin(sqrt(R(p)))/(2*floor(MD/GD)+1))**2)
if ceil(p*S1) == Sp:
print "NOTE: for (GD, log2(N), log2(MD), p) == (%d, %.2f, %.2f, %.2f) naive reduction of parallel machines is not worse!" % (GD, log(N, 2).n(), log(MD, 2).n(), p)
elif ceil(p*S1) < Sp:
print "NOTE: for (GD, log2(N), log2(MD), p) == (%d, %.2f, %.2f, %.2f) naive reduction of parallel machines is better!" % (GD, log(N, 2).n(), log(MD, 2).n(), p)
res = min(Sp, ceil(p*S1))
return res
def GroverDWCost(cipher, GDs, Ws, Gs, key_pc, ps, spurious_key_threshold=2**(-20), caption="", ignore_r=False, lowmc=False):
"""
Given a single-target Grover problem limited by MAXDEPTH, computes the appropriate inner-parallel strategy.
"""
# print "Unique key:"
for p in ps:
# print "@@@ target p = %.2f" % p
table = """
\\begin{table}
\\centering
\\renewcommand{\\tabcolsep}{0.05in}
\\renewcommand{\\arraystretch}{1.3}
\\resizebox{\\textwidth}{!}{
\\begin{tabular}{lccccccccc}
\\toprule\n"""
table += " scheme & \\texttt{MD} & %s $S$ & %s $D$ & $W$ & $G$-cost & $DW$-cost \\\\ \midrule\n" % (
"" if ignore_r else "$r$ &",
"" if ignore_r else "$\\log_2{(\\text{SKP})}$ &",
)
for lMD in logMDs:
# print "@@@ MAXDEPTH = 2^%s" % lMD
# table += " \\multicolumn{6}{c}$\\nistmaxdepth = 2^{%d}$} \\\\ \midrule\n" % lMD
# table += "\\midrule"
MD = 2**lMD
for i in range(len(key_pc)):
# obtain size of search problem and maxdepth info
lk, pairs = key_pc[i]
N = 2**lk
# compute iterations for targetted success p with traditional Grover (1 target only)
ip = iterations(p, N, 1)
"""
we look at how likely are spurious keys in the subset we are searching.
if they arent (< 1/(1<<20)), then we can reduce r, even down to 1!
only cost the lowest r, and re-cost the value of W. calling this full function
with different r' != r > whatever we actually need, should result in the same costs
we also want to deal with the no parallelism required case of aes128 in md96 (check whatsapp)
"""
for r in range(1, pairs+1):
GD = GDs[r][i]
# Total depth for traditional Grover
Dp = ip*GD
# Used depth: either MAXDEPTH or the depth of traditional grover
D = min(Dp, MD)
# How many quantum computers are needed for targetted MAXDEPTH?
S = instances(GD, N, MD, p)
skp = spurious_keys_probability(lk, 128 if not lowmc else lk, r, S)
c_p = pi/4
GW = Ws[r][i]
GG = Gs[r][i]
if S > 1:
G_cost = c_p**2 * 2**(lk-lMD) * GD * GG
DW_cost = c_p**2 * 2**(lk-lMD) * GD**2 * GW
else:
# Serial Grover!
G_cost = floor(c_p * 2**(lk/2)) * GG
DW_cost = GW * D
# print "@@@@ k = %d,\tMD = %d,\tlog(S) = %.1f,\tr = %d,\tlog2(spurious key prob) %.1f,\tG-cost %.1f,\tDW-cost %.1f" % (lk, lMD, R(log(S,2)), r, R(log(skp,2)), R(log(G_cost,2)), R(log(DW_cost,2)))
if skp <= spurious_key_threshold:
pairs = r
break
# total width given S parallel instances
W = S * Ws[pairs][i]
# Formatting circuit size
Sf, Se = format_pow(S)
Df, De = format_pow(D)
Wf, We = format_pow(W)
DWf, DWe = format_pow(D*W)
G_costf, G_coste = format_pow(G_cost)
DW_costf, DW_coste = format_pow(DW_cost)
# timeCf, timeCe = format_pow(S * key_pc[i][1])
# timeQf, timeQe = format_pow(D)
# print "%s-%s: " % (cipher, lk),
# print "S = %.2f \cdot 2^{%s}, D = %.2f \cdot 2^{%s}, W = %.2f \cdot 2^{%s}, DW = %.2f \cdot 2^{%s}" % (Sf, Se, Df, De, Wf, We, DWf, DWe)
table += " \\%s-%d & $2^{%d}$ & %s $%.2f \cdot 2^{%s}$ & %s $%.2f \cdot 2^{%s}$ & $%.2f \cdot 2^{%s}$ & $%.2f \cdot 2^{%s}$ & $%.2f \cdot 2^{%s}$ \\\\\n" % (
cipher, lk, lMD,
"" if ignore_r else (" $%d$ &" % pairs),
Sf, Se,
"" if ignore_r else ("$%s$ &" % (("%.2f" % R(log(skp,2))) if R(log(skp,2)) > (-Infinity) else "-\infty")), # prob
Df, De,
Wf, We,
# DWf, DWe,
G_costf, G_coste,
DW_costf, DW_coste,
# timeCf, timeCe, # Clas
# cipher, lk,
# timeQf, timeQe, # Quant
)
table += " \\midrule\n"
table += """ \end{tabular}
}
\\caption{%s}
\\end{table}\n""" % caption
print table
def CostAES(t_depth_only, in_place_mixcolumn):
if t_depth_only:
# indexed by number of p-c pairs
GDs = {
1: [121, 120, 126],
2: [121, 120, 126],
3: [121, 120, 126],
}
else:
# indexed by number of p-c pairs
if in_place_mixcolumn:
GDs = {
1: [2816, 2978, 3353],
2: [2815, 2981, 3356],
3: [2830, 2982, 3347],
}
else:
GDs = {
1: [2086, 1879, 1951],
2: [2096, 1890, 1952],
3: [2102, 1888, 1956],
}
# indexed by number of p-c pairs
if in_place_mixcolumn:
Ws = {
1: [257 + 1408, 321 + 1664, 385 + 1920],
2: [385 + 2944, 449 + 3520, 513 + 4096],
3: [513 + 4480, 577 + 5376, 641 + 6272],
}
Gs = {
1: [292313 + 84428 + 54908 + 13727, 329697 + 94316 + 61436 + 15359, 404139 + 116286 + 75580 + 18895],
2: [585051 + 169184 + 109820 + 27455, 659727 + 188520 + 122876 + 30719, 808071 + 231124 + 151164 + 37791],
3: [877081 + 252524 + 164732 + 41183, 989879 + 282968 + 184316 + 46079, 1212905 + 347766 + 226748 + 56687]
}
else:
Ws = {
1: [257 + 2560, 321 + 3072, 385 + 3584],
2: [385 + 5248, 449 + 6336, 513 + 7424],
3: [513 + 7936, 577 + 9600, 641 + 11264],
}
Gs = {
1: [294863 + 84488 + 54908 + 13727, 332665 + 94092 + 61436 + 15359, 407667 + 116062 + 75580 + 18895],
2: [589643 + 168288 + 109820 + 27455, 665899 + 188544 + 122876 + 30719, 815645 + 231712 + 151164 + 37791],
3: [884817 + 252876 + 164732 + 41183, 999491 + 283712 + 184316 + 46079, 1223087 + 346290 + 226748 + 56687]
}
# print "@@@ T-depth only: %s" % t_depth_only
# print "@@@ in-place MC: %s" % in_place_mixcolumn
# provide r guaranteeing no spurious keys. internally it tries to use less
# caption = "\\aes: D = %s, %s MixColumn" % ("T-depth" if t_depth_only else "full depth", "in-place" if in_place_mixcolumn else "Maximov's")
caption = "Circuit sizes for parallel Grover key search against \\aes (using %s MixColumn implementation), using \emph{inner} parallelization (cf. Section~\\ref{sec:groverparallel}). \\texttt{MD} is \\nistmaxdepth, $r$ is the number of plaintext-ciphertext pairs used in the Grover oracle, S is the number of subsets in which the key-space is divided into, SKP is the probability of spurious keys being present in the subset where the target key is present, D and W are the %sdepth and qubit width of the full circuit, DW is the %sdepth~$\\times$~width circuit cost. After the Grover search is completed, each of the S measured candidate keys is classically checked against 2 (resp. 2, 3) plaintext-ciphertext pairs for \\aes-128 (resp. -192, -256)." % (
"an in-place" if in_place_mixcolumn else "Maximov's~\\cite{cryptoeprint:2019:833}",
"T-" if t_depth_only else "full ",
"T-" if t_depth_only else "full ",
)
GroverDWCost("aes", GDs, Ws, Gs, [(128, 2), (192, 2), (256, 3)], [1], caption=caption) #, 0.5, R(1/e)])
def CostLowMC(t_depth_only):
if t_depth_only:
# indexed by number of p-c pairs
GDs = {
1: [41, 61, 77],
2: [41, 61, 77]
}
else:
# indexed by number of p-c pairs
GDs = {
1: [98709, 319323, 693477],
2: [98707, 319329, 693483]
}
# indexed by number of p-c pairs
Ws = {
1: [257 + 1328, 385 + 1992, 513 + 2536],
2: [385 + 2784, 577 + 4176, 769 + 5328],
}
Gs = {
1: [690961 + 5917 + 8908 + 191, 2273397 + 10881 + 13364 + 286, 5072343 + 16209 + 16980 + 372],
2: [1382143 + 11774 + 17820 + 362, 4547191 + 21783 + 26732 + 576, 10145281 + 32567 + 33964 + 783]
}
# print "T-depth only: %s" % t_depth_only
# Not sure it makes sense to have lower than 1 prob for these next values
caption = "Circuit sizes for parallel Grover key search against \\lowmc, using \\emph{inner} parallelization (cf. Section~\\ref{sec:groverparallel}). \\texttt{MD} is \\nistmaxdepth, S is the number of subsets in which the key-space is divided into, D and W are the %sdepth and qubit width of the full circuit, DW is the %sdepth~$\\times$~width circuit cost. The Grover oracle is always implemented using a single plaintext-ciphertext pair, given that this is sufficient for key-recovery againist \picnic. After the Grover search is completed, each of the S measured candidate keys is classically checked against 1 plaintext-ciphertext pairs." % (
"T-" if t_depth_only else "full ",
"T-" if t_depth_only else "full ",
)
GroverDWCost("lowmc", GDs, Ws, Gs, [(128, 2), (192, 2), (256, 2)], [1], spurious_key_threshold=2**(-20), caption=caption, ignore_r=False, lowmc=True)
# print "@@@ ########## in-place MC"
CostAES(False, True)
CostAES(True, True)
# print "@@@"
# print "@@@"
# print "@@@ ########## Maximov's MC"
CostAES(False, False)
CostAES(True, False)
# print "@@@"
# print "@@@"
# print "@@@ ########## LowMC"
CostLowMC(False)
# print "@@@"
# print "@@@"
CostLowMC(True)