# -*- coding: utf-8 -*- """ Complexity estimates for solving LWE. .. moduleauthor:: Martin R. Albrecht """ from functools import partial from collections import OrderedDict from scipy.optimize import newton from sage.modules.all import vector from sage.calculus.var import var from sage.functions.log import exp, log from sage.functions.other import ceil, sqrt, floor, binomial, erf from sage.interfaces.magma import magma from sage.matrix.all import Matrix from sage.misc.all import cached_function from sage.misc.all import set_verbose, get_verbose, prod from sage.arith.srange import srange from sage.numerical.optimize import find_root from sage.rings.all import QQ, RR, ZZ, RealField, PowerSeriesRing, RDF from sage.symbolic.all import pi, e from sage.rings.infinity import PlusInfinity from sage.crypto.lwe import LWE, Regev, LindnerPeikert # config oo = PlusInfinity() tau_default = 0.3 # τ used in uSVP tau_prob_default = 0.1 # probability of success for given τ cfft = 1 # convolutions mod q enable_LP_estimates = True # enable LP estimates enable_fplll_estimates = False # enable fplll estimates # Utility Classes # class OutOfBoundsError(ValueError): """ Used to indicate a wrong value, for example delta_0 < 1. """ pass class InsufficientSamplesError(ValueError): """ Used to indicate the number of samples given is too small, especially useful for #samples <= 0. """ pass # Utility Functions # def binary_search_minimum(f, start, stop, param, extract=lambda x: x, *arg, **kwds): """ Return minimum of `f` if `f` is convex. :param start: start of range to search :param stop: stop of range to search (exclusive) :param param: the parameter to modify when calling `f` :param extract: comparison is performed on `extract(f(param=?, *args, **kwds))` """ return binary_search(f, start, stop, param, better=lambda x, best: extract(x)<=extract(best), *arg, **kwds) def binary_search(f, start, stop, param, better=lambda x, best: x<=best, *arg, **kwds): """ Searches for the `best` value in the interval [start,stop] depending on the given predicate `better`. :param start: start of range to search :param stop: stop of range to search (exclusive) :param param: the parameter to modify when calling `f` :param better: comparison is performed by evaluating `better(current, best)` """ kwds[param] = stop D = {} D[stop] = f(*arg, **kwds) best = D[stop] b = ceil((start+stop)/2) direction = 0 while True: if b == start: best = D[start] break if b not in D: kwds[param] = b D[b] = f(*arg, **kwds) if not better(D[b], best): if direction == 0: start = b b = ceil((stop+b)/2) else: stop = b b = floor((start+b)/2) else: best = D[b] if b-1 not in D: kwds[param] = b-1 D[b-1] = f(*arg, **kwds) if better(D[b-1], best): stop = b b = floor((b+start)/2) direction = 0 else: if b+1 not in D: kwds[param] = b+1 D[b+1] = f(*arg, **kwds) if not better(D[b+1], best): break else: start = b b = ceil((stop+b)/2) direction = 1 return best def cost_str(d, keyword_width=None, newline=None, round_bound=2048): """ Return string of key,value pairs as a string "key0: value0, key1: value1" :param d: report dictionary :keyword_width: keys are printed with this width EXAMPLE: By default dicts are unordered, hence the order of the output of this function is undefined:: sage: s = {"delta":5, "bar":2} sage: print cost_str(s) bar: 2, delta: 5 Use `OrderedDict` if you require ordered output:: sage: s = OrderedDict([(u"delta", 5), ("bar",2)]) sage: print cost_str(s) delta: 5, bar: 2 """ if d is None: return s = [] for k in d: v = d[k] if keyword_width: fmt = u"%%%ds" % keyword_width k = fmt % k if ZZ(1)/round_bound < v < round_bound or v == 0 or ZZ(-1)/round_bound > v > -round_bound: try: s.append(u"%s: %9d" % (k, ZZ(v))) except TypeError: if v < 2.0 and v >= 0.0: s.append(u"%s: %9.7f" % (k, v)) else: s.append(u"%s: %9.4f" % (k, v)) else: t = u"≈%s2^%.1f" % ("-" if v < 0 else "", log(abs(v), 2).n()) s.append(u"%s: %9s" % (k, t)) if not newline: return u", ".join(s) else: return u"\n".join(s) def cost_reorder(d, ordering): """ Return a new ordered dict from the key:value pairs in `d` but reordered such that the keys in ordering come first. :param d: input dictionary :param ordering: keys which should come first (in order) EXAMPLE:: sage: d = OrderedDict([("a",1),("b",2),("c",3)]); d OrderedDict([('a', 1), ('b', 2), ('c', 3)]) sage: cost_reorder(d, ["b","c","a"]) OrderedDict([('b', 2), ('c', 3), ('a', 1)]) """ keys = list(d) for key in ordering: keys.pop(keys.index(key)) keys = list(ordering) + keys r = OrderedDict() for key in keys: r[key] = d[key] return r def cost_filter(d, keys): """ Return new ordered dict from the key:value pairs in `d` restricted to the keys in `keys`. :param d: input dictionary :param keys: keys which should be copied (ordered) """ r = OrderedDict() for key in keys: r[key] = d[key] return r def cost_repeat(d, times, repeat=None): """ Return a report with all costs multiplied by `times`. :param d: a cost estimate :param times: the number of times it should be run :param repeat: toggle which fields ought to be repeated and which shouldn't :returns: a new cost estimate We maintain a local dictionary which decides if an entry is multiplied by `times` or not. For example, δ would not be multiplied but "\#bop" would be. This check is strict such that unknown entries raise an error. This is to enforce a decision on whether an entry should be multiplied by `times` if the function `report` reports on is called `times` often. EXAMPLE:: sage: n, alpha, q = unpack_lwe(Regev(128)) sage: print cost_str(cost_repeat(sis(n, alpha, q), 2^10)) bkz2: ≈2^85.4, oracle: ≈2^36.5, δ_0: 1.0089681, ... sage: print cost_str(cost_repeat(sis(n, alpha, q), 1)) bkz2: ≈2^75.4, oracle: ≈2^26.5, δ_0: 1.0089681, ... """ do_repeat = { u"bop": True, u"rop": True, u"oracle": True, u"bkz2": True, u"lp": True, u"ds": True, u"fplll": True, u"sieve": True, u"enum": True, u"enumop": True, u"log(eps)": False, u"quantum_sieve": True, u"mem": False, u"delta_0": False, u"beta": False, u"k": False, u"ε": False, u"D_reg": False, u"t": False, u"Pr[⊥]": False, # we are leaving probabilities alone u"m": False, u"dim": False, u"|v|": False, u"amplify": False, u"repeat": False, # we deal with it below u"c": False, } if repeat is not None: for key in repeat: do_repeat[key] = repeat[key] ret = OrderedDict() for key in d: try: if do_repeat[key]: ret[key] = times * d[key] else: ret[key] = d[key] except KeyError: raise NotImplementedError(u"You found a bug, this function does not know about '%s' but should."%key) ret[u"repeat"] = times * ret.get("repeat", 1) return ret def cost_combine(left, right, base=None): """Combine ``left`` and ``right``. :param left: cost dictionary :param right: cost dictionary :param base: add entries to ``base`` """ if base is None: cost = OrderedDict() else: cost = base for key in left: cost[key] = left[key] for key in right: cost[key] = right[key] return cost def stddevf(sigma): """ σ → std deviation :param sigma: Gaussian width parameter σ EXAMPLE:: sage: n = 64.0 sage: stddevf(n) 25.532... """ return sigma/sqrt(2*pi).n() def sigmaf(stddev): """ std deviation → σ :param sigma: standard deviation EXAMPLE:: sage: n = 64.0 sage: sigmaf(stddevf(n)) 64.000... """ RR = stddev.parent() return RR(sqrt(2*pi))*stddev def alphaf(sigma, q, sigma_is_stddev=False): """ σ, q → α :param sigma: Gaussian width parameter (or standard deviation if `sigma_is_stddev` is `True`) :param q: modulus :param sigma_is_stddev: if `True` then `sigma` is interpreted as the standard deviation :returns: α = σ/q or σ·sqrt(2π)/q depending on `sigma_is_stddev` :rtype: real number """ if sigma_is_stddev is False: return RR(sigma/q) else: return RR(sigmaf(sigma)/q) def amplify(target_success_probability, success_probability, majority=False): """ Return the number of trials needed to amplify current `success_probability` to `target_success_probability` :param target_success_probability: 0 < real value < 1 :param success_probability: 0 < real value < 1 :param majority: if `True` amplify a deicsional problem, not a computational one if `False` then we assume that we can check solutions, so one success suffices :returns: number of required trials to amplify """ prec = max(53, 2*ceil(abs(log(success_probability, 2))), 2*ceil(abs(log(1-success_probability, 2))), 2*ceil(abs(log(target_success_probability, 2))), 2*ceil(abs(log(1-target_success_probability, 2)))) RR = RealField(prec) if target_success_probability < success_probability: return RR(1) success_probability = RR(success_probability) target_success_probability = RR(target_success_probability) if majority: eps = success_probability/2 repeat = ceil(2*log(2 - 2*target_success_probability)/log(1 - 4*eps**2)) else: # target_success_probability = 1 - (1-success_probability)^trials repeat = ceil(log(1-target_success_probability)/log(1 -success_probability)) return repeat def rinse_and_repeat(f, n, alpha, q, success_probability=0.99, optimisation_target=u"bkz2", decision=True, samples=None, *args, **kwds): """Find best trade-off between success probability and running time. :param f: a function returning a cost estimate :param n: dimension > 0 :param alpha: fraction of the noise α < 1.0 :param q: modulus > 0 :param success_probability: target success probability :param optimisation_target: what value out to be minimized :param decision: ``True`` if ``f`` solves Decision-LWE, ``False`` for Search-LWE. :param samples: the number of available samples """ n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability) best = None step_size = 32 i = floor(-log(success_probability, 2)) has_solution = False while True: prob = min(2**-i, success_probability) try: current = f(n, alpha, q, optimisation_target=optimisation_target, success_probability=prob, samples=samples, *args, **kwds) repeat = amplify(success_probability, prob, majority=decision) do_repeat = None if samples is None else {"oracle": False} current = cost_repeat(current, repeat, do_repeat) has_solution = True except (OutOfBoundsError, InsufficientSamplesError) as err: key = list(best)[0] if best is not None else optimisation_target current = OrderedDict() current[key] = oo if get_verbose() >= 2: print err current["log(eps)"] = -i if get_verbose() >= 2: print cost_str(current) key = list(current)[0] if best is None: best = current i += step_size continue if key not in best or current[key] < best[key]: best = current i += step_size else: # we go back i = -best["log(eps)"] - step_size i += step_size/2 if i <= 0: i = step_size/2 # and half the step size step_size = step_size/2 if step_size == 0: break if not has_solution: raise RuntimeError("No solution found for chosen parameters.") return best @cached_function def distinguish_required_m(sigma, q, success_probability, other_sigma=None): RR = sigma.parent() if other_sigma is not None: sigma = RR(sqrt(sigma**2 + other_sigma**2)) adv = RR(exp(-RR(pi)*(RR(sigma/q)**2))) return RR(success_probability)/RR(adv**2) def uniform_variance_from_bounds(a, b, h=None): """ Variance for uniform distribution from bounds. :param a: :param b: :param h: number of non-zero components. :returns: :rtype: """ assert a < 0 and b > 0 and abs(a) == abs(b) if h is None: n = b - a + 1 return (n**2 - 1)/ZZ(12) else: # sage: var("i,a,b") # sage: p = 1/(b-a) # sage: sum(i^2*p, i, a, b) return (2*a**3 - 2*b**3 - 3*a**2 - 3*b**2 + a - b)/(6*ZZ(a - b)) def unpack_lwe(lwe): """ Return n, α, q given an LWE instance object. :param lwe: LWE object :returns: n, α, q :rtype: tuple """ n = lwe.n q = lwe.K.order() try: alpha = alphaf(sigmaf(lwe.D.sigma), q) except AttributeError: # older versions of Sage use stddev, not sigma alpha = alphaf(sigmaf(lwe.D.stddev), q) return n, alpha, q def unpack_lwe_dict(lwe): """ Return dictionary consisting of n, α, q and samples given an LWE instance object. :param lwe: LWE object :returns: "n": n, "alpha": α, "q": q, "samples": samples :rtype: dictionary """ n, alpha, q = unpack_lwe(lwe) samples = lwe.m return {"n": n, "alpha": alpha, "q": q, "samples": samples} def preprocess_params(n, alpha, q, success_probability=None, prec=None, samples=None): """ Check if parameters n, α, q are sound and return correct types. Also, if given, the soundness of the success probability and the number of samples is ensured. """ if n < 1: raise ValueError("LWE dimension must be greater than 0.") if alpha <= 0: raise ValueError("Fraction of noise must be > 0.") if q < 1: raise ValueError("LWE modulus must be greater than 0.") if samples is not None and samples < 1: raise ValueError("Given number of samples must be greater than 0.") if prec is None: prec = 128 RR = RealField(prec) n, alpha, q = ZZ(n), RR(alpha), ZZ(q), samples = ZZ(samples) if success_probability is not None: if success_probability >= 1 or success_probability <= 0: raise ValueError("success_probability must be between 0 and 1.") return n, alpha, q, RR(success_probability) else: return n, alpha, q ################################ # Section 2 # ################################ def switch_modulus(n, alpha, q, s_variance, h=None): """ Return modulus switched parameters. :param n: the number of variables in the LWE instance :param alpha: noise size :param q: modulus :param s_var: the variance of the secret :param h: number of non-zero components. If ``h`` is given, then ``s_var`` refers to the variance of non-zero components. EXAMPLE:: sage: switch_modulus(128, 0.01, 65537, uniform_variance_from_bounds(0,1)) (128, 0.0141421356237310, 410.000000000000) sage: switch_modulus(128, 0.001, 65537, uniform_variance_from_bounds(0,1)) (128, 0.00141421356237310, 4094.00000000000) sage: switch_modulus(128, 0.001, 65537, uniform_variance_from_bounds(-5,5)) (128, 0.00141421356237310, 25889.0000000000) """ if h is not None: length = h else: length = n p = RR(ceil(sqrt(2*pi*s_variance*length/ZZ(12)) / alpha)) if p < 32: # some random point # we can't pretend everything is uniform any more, p is too small p = RR(ceil(sqrt(2*pi*s_variance*length*2/ZZ(12)) / alpha)) beta = RR(sqrt(2)*alpha) return n, beta, p # Lattice Reduction def bkz_svp_repeat(n, k): """Return number of SVP calls in BKZ-k :param n: dimension :param k: block size .. note :: loosely based on experiments in [PhD:Chen13] """ return 8*n def _delta_0f(k): """ Compute `δ_0` from block size `k` without enforcing `k` in ZZ. δ_0 for k<=40 were computed as follows: ``` # -*- coding: utf-8 -*- from fpylll import BKZ, IntegerMatrix from multiprocessing import Pool from sage.all import mean, sqrt, exp, log, cputime d, trials = 320, 32 def f((A, beta)): par = BKZ.Param(block_size=beta, strategies=BKZ.DEFAULT_STRATEGY, flags=BKZ.AUTO_ABORT) q = A[-1, -1] d = A.nrows t = cputime() A = BKZ.reduction(A, par, float_type="dd") t = cputime(t) return t, exp(log(A[0].norm()/sqrt(q).n())/d) if __name__ == '__main__': for beta in (5, 10, 15, 20, 25, 28, 30, 35, 40): delta_0 = [] t = [] i = 0 while i < trials: threads = int(open("delta_0.nthreads").read()) # make sure this file exists pool = Pool(threads) A = [(IntegerMatrix.random(d, "qary", k=d//2, bits=50), beta) for j in range(threads)] for (t_, delta_0_) in pool.imap_unordered(f, A): t.append(t_) delta_0.append(delta_0_) i += threads print u"β: %2d, δ_0: %.5f, time: %5.1fs, (%2d,%2d)"%(beta, mean(delta_0), mean(t), i, threads) print ``` """ small = (( 2, 1.02190), # noqa ( 5, 1.01862), # noqa (10, 1.01616), (15, 1.01485), (20, 1.01420), (25, 1.01342), (28, 1.01331), (40, 1.01295)) if k <= 2: return RR(1.0219) elif k < 40: for i in range(1, len(small)): if small[i][0] > k: return RR(small[i-1][1]) elif k == 40: return RR(small[-1][1]) else: return RR(k/(2*pi*e) * (pi*k)**(1/k))**(1/(2*(k-1))) def delta_0f(k): """ Compute `δ_0` from block size `k`. """ k = ZZ(round(k)) return _delta_0f(k) def k_chen_secant(delta): """ Estimate required blocksize `k` for a given root-hermite factor δ based on [PhD:Chen13]_ :param delta: root-hermite factor EXAMPLE:: sage: 50 == k_chen(1.0121) True sage: 100 == k_chen(1.0093) True sage: k_chen(1.0024) # Chen reports 800 808 .. [PhD:Chen13] Yuanmi Chen. Réduction de réseau et sécurité concrète du chiffrement complètement homomorphe. PhD thesis, Paris 7, 2013. """ # newton() will produce a "warning", if two subsequent function values are # indistinguishable (i.e. equal in terms of machine precision). In this case # newton() will return the value k in the middle between the two values # k1,k2 for which the function values were indistinguishable. # Since f approaches zero for k->+Infinity, this may be the case for very # large inputs, like k=1e16. # For now, these warnings just get printed and the value k is used anyways. # This seems reasonable, since for such large inputs the exact value of k # doesn't make such a big difference. try: k = newton(lambda k: RR(_delta_0f(k) - delta), 100, fprime=None, args=(), tol=1.48e-08, maxiter=500) k = ceil(k) if k < 40: # newton may output k < 40. The old k_chen method wouldn't do this. For # consistency, call the old k_chen method, i.e. consider this try as "failed". raise RuntimeError("k < 40") return k except (RuntimeError, TypeError): # if something fails, use old k_chen method if get_verbose() >= 2: print "secant method failed, using k_chen_old(delta) instead!" k = k_chen_old(delta) return k def k_chen_find_root(delta): # handle k < 40 separately k = ZZ(40) if delta_0f(k) < delta: return k try: k = find_root(lambda k: RR(_delta_0f(k) - delta), 40, 2**16, maxiter=500) k = ceil(k) except RuntimeError: # finding root failed; reasons: # 1. maxiter not sufficient # 2. no root in given interval k = k_chen_old(delta) return k def k_chen(delta): # TODO: decide for one strategy (secant, find_root, old) and it's handling of errors. k = k_chen_find_root(delta) return k def k_chen_old(delta): """ Estimate required blocksize `k` for a given root-hermite factor δ based on [PhD:Chen13]_ :param delta: root-hermite factor EXAMPLE:: sage: 50 == k_chen(1.0121) True sage: 100 == k_chen(1.0093) True sage: k_chen(1.0024) # Chen reports 800 808 .. [PhD:Chen13] Yuanmi Chen. Réduction de réseau et sécurité concrète du chiffrement complètement homomorphe. PhD thesis, Paris 7, 2013. """ k = ZZ(40) while delta_0f(2*k) > delta: k *= 2 while delta_0f(k+10) > delta: k += 10 while True: if delta_0f(k) < delta: break k += 1 return k def bkz_runtime_delta_LP(delta, n): """ Runtime estimation assuming the Lindner-Peikert model. """ return RR(1.8/log(delta, 2) - 110 + log(2.3*10**9, 2)) def bkz_runtime_k_sieve_bdgl16_small(k, n): u""" Runtime estimation given `k` and assuming sieving is used to realise the SVP oracle. For small `k` we use estimates based on experiments in [BDGL16] :param k: block size :param n: lattice dimension .. [BDGL16] Becker, A., Ducas, L., Gama, N., & Laarhoven, T. (2016). New directions in nearest neighbor searching with applications to lattice sieving. In SODA 2016, (pp. 10–24). """ return RR(0.387*k + 16.4 + log(bkz_svp_repeat(n, k), 2)) def bkz_runtime_k_sieve_bdgl16_asymptotic(k, n): u""" Runtime estimation given `k` and assuming sieving is used to realise the SVP oracle. :param k: block size :param n: lattice dimension .. [BDGL16] Becker, A., Ducas, L., Gama, N., & Laarhoven, T. (2016). New directions in nearest neighbor searching with applications to lattice sieving. In SODA 2016, (pp. 10–24). """ # we simply pick the same additive constant 16.4 as for the experimental result in [BDGL16] return RR(0.292*k + 16.4 + log(bkz_svp_repeat(n, k), 2)) def bkz_runtime_k_quantum_sieve(k, n): """ Runtime estimation for quantum sieving. .. [LaaMosPol14] Thijs Laarhoven, Michele Mosca, & Joop van de Pol. Finding shortest lattice vectors faster using quantum search. Cryptology ePrint Archive, Report 2014/907, 2014. https://eprint.iacr.org/2014/907. """ return RR((0.265*k + 16.4 + log(bkz_svp_repeat(n, k), 2))) bkz_runtime_k_sieve_asymptotic = bkz_runtime_k_sieve_bdgl16_asymptotic bkz_runtime_k_sieve_small = bkz_runtime_k_sieve_bdgl16_small def bkz_runtime_k_sieve(k, n): u""" Runtime estimation given `k` and assuming sieving is used to realise the SVP oracle. :param k: block size :param n: lattice dimension """ if k <= 90: return bkz_runtime_k_sieve_small(k, n) else: return bkz_runtime_k_sieve_asymptotic(k, n) def bkz_runtime_k_bkz2(k, n): """ Runtime estimation given `k` and assuming [CheNgu12]_ estimates are correct. The constants in this function were derived as follows based on Table 4 in [CheNgu12]_:: sage: dim = [100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250] sage: nodes = [39.0, 44.0, 49.0, 54.0, 60.0, 66.0, 72.0, 78.0, 84.0, 96.0, 99.0, 105.0, 111.0, 120.0, 127.0, 134.0] # noqa sage: times = [c + log(200,2).n() for c in nodes] sage: T = zip(dim, nodes) sage: var("a,b,c,k") (a, b, c, k) sage: f = a*k*log(k, 2.0) + b*k + c sage: f = f.function(k) sage: f.subs(find_fit(T, f, solution_dict=True)) k |--> 0.270188776350190*k*log(k) - 1.0192050451318417*k + 16.10253135200765 .. [CheNgu12] Yuanmi Chen and Phong Q. Nguyen. BKZ 2.0: Better lattice security estimates (Full Version). 2012. http://www.di.ens.fr/~ychen/research/Full_BKZ.pdf """ repeat = log(bkz_svp_repeat(n, k), 2) return RR(0.270188776350190*k*log(k) - 1.0192050451318417*k + 16.10253135200765 + repeat) def bkz_runtime_delta_bkz2(delta, n): """ Runtime estimation extrapolated from BKZ 2.0 timings. """ k = k_chen(delta) return bkz_runtime_k_bkz2(k, n) def bkz_runtime_k_fplll(k, n): """ Runtime estimation extrapolated from fpLLL 4.0.4 experiments """ repeat = log(bkz_svp_repeat(n, k), 2) return RR(0.013487467331762426*k**2 - 0.28245244492771304*k + 21.017892848466957 + repeat) def bkz_runtime_delta(delta, n, log_repeat=0): """ Runtime estimates for BKZ (2.0) given δ and n """ if enable_LP_estimates: t_lp = bkz_runtime_delta_LP(delta, n) + log_repeat RR = delta.parent() k = k_chen(delta) t_sieve = RR(bkz_runtime_k_sieve(k, n) + log_repeat) t_bkz2 = RR(bkz_runtime_k_bkz2(k, n) + log_repeat) t_fplll = RR(bkz_runtime_k_fplll(k, n) + log_repeat) t_quantum_sieve = RR(bkz_runtime_k_quantum_sieve(k, n) + log_repeat) r = OrderedDict() r[u"delta_0"] = delta r[u"bkz2"] = RR(2)**t_bkz2 r[u"beta"] = k if enable_LP_estimates: r[u"lp"] = RR(2)**t_lp if enable_fplll_estimates: r[u"fplll"] = RR(2)**t_fplll r[u"sieve"] = RR(2)**t_sieve r[u"quantum_sieve"] = RR(2)**t_quantum_sieve return r def lattice_reduction_opt_m(n, q, delta): """ Return the (heuristically) optimal lattice dimension `m` :param n: dimension :param q: modulus :param delta: root Hermite factor `δ_0` """ return ZZ(round(sqrt(n*log(q, 2)/log(delta, 2)))) def sieve_or_enum(func): """ Take minimum of sieving or enumeration for lattice-based attacks. :param func: a lattice-reduction based estimator """ def wrapper(*args, **kwds): from copy import copy kwds = copy(kwds) if "optimisation_target" in kwds: del kwds["optimisation_target"] a = func(*args, optimisation_target="bkz2", **kwds) b = func(*args, optimisation_target="sieve", **kwds) if a["bkz2"] <= b["sieve"]: return a else: return b return wrapper def mitm(n, alpha, q, success_probability=0.99, secret_bounds=None, h=None, samples=None): """ Return meet-in-the-middle estimates. :param n: dimension :param alpha: noise parameter :param q: modulus :param success_probability: desired success probability :param secret_bounds: tuple with lower and upper bound on the secret :param samples: the number of available samples :returns: a cost estimate :rtype: OrderedDict """ n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability) ret = OrderedDict() RR = alpha.parent() m = None if samples is not None: if not samples > n: raise InsufficientSamplesError("Number of samples: %d" % samples) m = samples - n t = ceil(2*sqrt(log(n))) if secret_bounds is None: # assert((2*t*alpha)**m * (alpha*q)**(n/2) <= 2*n) m_required = ceil((log(2*n) - log(alpha*q)*(n/2))/log(2*t*alpha)) if m is not None and m < m_required: raise InsufficientSamplesError("Requirement not fulfilled. Number of samples: %d - %d < %d" % ( samples, n, m_required)) m = m_required if m*(2*alpha) > 1- 1/(2*n): raise ValueError("Cannot find m to satisfy constraints (noise too big).") ret["rop"] = RR((2*alpha*q+1)**(n/2) * 2*n * m) ret["mem"] = RR((2*alpha*q+1)**(n/2) * m) else: a, b = secret_bounds # assert((2*t*alpha)**m * (b-a+1)**(n/2) <= 2*n) m_required = ceil(log(2*n/((b-a+1)**(n/2)))/log(2*t*alpha)) if m is not None and m < m_required: raise InsufficientSamplesError("Requirement not fulfilled. Number of samples: %d - %d < %d" % ( samples, n, m_required)) m = m_required if (m*(2*alpha) > 1- 1/(2*n)): raise ValueError("Cannot find m to satisfy constraints (noise too big).") ret["rop"] = RR((b-a+1)**(n/2) * 2*n * m) ret["mem"] = RR((b-a+1)**(n/2) * m) ret["oracle"] = n + m ret["bop"] = RR(log(q, 2) * ret["rop"]) return cost_reorder(ret, ["bop", "oracle", "mem"]) # BKW def bkw(n, alpha, q, success_probability=0.99, optimisation_target="bop", prec=None, search=False, samples=None): """ Estimate the cost of running BKW to solve LWE :param n: dimension > 0 :param alpha: fraction of the noise α < 1.0 :param q: modulus > 0 :param success_probability: probability of success < 1.0 :param optimisation_target: field to use to decide if parameters are better :param prec: precision used for floating point computations :param search: if `True` solve Search-LWE, otherwise solve Decision-LWE :param samples: the number of available samples :returns: a cost estimate :rtype: OrderedDict """ if search: return bkw_search(n, alpha, q, success_probability, optimisation_target, prec, samples) else: return bkw_decision(n, alpha, q, success_probability, optimisation_target, prec, samples) def bkw_decision(n, alpha, q, success_probability=0.99, optimisation_target="bop", prec=None, samples=None): """ Estimate the cost of running BKW to solve Decision-LWE following [DCC:ACFFP15]_. :param n: dimension > 0 :param alpha: fraction of the noise α < 1.0 :param q: modulus > 0 :param success_probability: probability of success < 1.0 :param optimisation_target: field to use to decide if parameters are better :param prec: precision used for floating point computations :param samples: the number of available samples :returns: a cost estimate :rtype: OrderedDict .. [DCC:ACFFP15] Albrecht, M. R., Cid, C., Jean-Charles Faugère, Fitzpatrick, R., & Perret, L. (2015). On the complexity of the BKW algorithm on LWE. Designs, Codes & Cryptography, Volume 74, Issue 2, pp 325-354 """ n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability) sigma = alpha*q has_samples = samples is not None has_enough_samples = True RR = alpha.parent() def _run(t): a = RR(t*log(n, 2)) # target number of adds: a = t*log_2(n) b = RR(n/a) # window width sigma_final = RR(n**t).sqrt() * sigma # after n^t adds we get this σ m = distinguish_required_m(sigma_final, q, success_probability) tmp = a*(a-1)/2 * (n+1) - b*a*(a-1)/4 - b/6 * RR((a-1)**3 + 3/2*(a-1)**2 + (a-1)/2) stage1a = RR(q**b-1)/2 * tmp stage1b = m * (a/2 * (n + 2)) stage1 = stage1a + stage1b nrops = RR(stage1) nbops = RR(log(q, 2) * nrops) ncalls = RR(a * ceil(RR(q**b)/RR(2)) + m) nmem = ceil(RR(q**b)/2) * a * (n + 1 - b * (a-1)/2) current = OrderedDict([(u"t", t), (u"bop", nbops), (u"oracle", ncalls), (u"m", m), (u"mem", nmem), (u"rop", nrops), (u"a", a), (u"b", b), ]) if optimisation_target != u"oracle": current = cost_reorder(current, (optimisation_target, u"oracle", u"t")) else: current = cost_reorder(current, (optimisation_target, u"t")) return current best_runtime = None best_samples = None best = None may_terminate = False t = RR(2*(log(q, 2) - log(sigma, 2))/log(n, 2)) while True: current = _run(t) if get_verbose() >= 2: print cost_str(current) # Usually, both the fewest samples required and the best runtime are # provided when choosing 't' such that the two steps are balanced. But, # better safe than sorry, both cases are searched for independently. # So, 'best_samples' and 'best_runtime' are only used to ensure termination, # such that 't' is increased until both the best runtime and the fewest # samples were seen. The result to be returned is hold in 'best'. if has_samples: has_enough_samples = current["oracle"] < samples if not best_samples: best_samples = current else: if best_samples["oracle"] > current["oracle"]: best_samples = current if has_enough_samples and ( best is None or best[optimisation_target] > current[optimisation_target]): best = current else: if may_terminate: break may_terminate = True if not best_runtime: best_runtime = current else: if best_runtime[optimisation_target] > current[optimisation_target]: best_runtime = current if has_enough_samples and (best is None or best[optimisation_target] > current[optimisation_target]): best = current else: if not has_samples or may_terminate: break may_terminate = True t += 0.05 if best is None: raise InsufficientSamplesError("Too few samples (%d given) to achieve a success probability of %f." % ( samples, success_probability)) return best def bkw_search(n, alpha, q, success_probability=0.99, optimisation_target="bop", prec=None, samples=None): """ Estimate the cost of running BKW to solve Search-LWE following [C:DucTraVau15]_. :param n: dimension > 0 :param alpha: fraction of the noise α < 1.0 :param q: modulus > 0 :param success_probability: probability of success < 1.0 :param optimisation_target: field to use to decide if parameters are better :param prec: precision used for floating point computations :param samples: the number of available samples :returns: a cost estimate :rtype: OrderedDict .. [EC:DucTraVau15] Duc, A., Florian Tramèr, & Vaudenay, S. (2015). Better algorithms for LWE and LWR. """ n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability) sigma = stddevf(alpha*q) eps = success_probability has_samples = samples is not None has_enough_samples = True RR = alpha.parent() # "To simplify our result, we considered operations over C to have the same # complexity as operations over Z_q . We also took C_FFT = 1 which is the # best one can hope to obtain for a FFT." c_cost = 1 c_mem = 1 def _run(t): a = RR(t*log(n, 2)) # target number of adds: a = t*log_2(n) b = RR(n/a) # window width epp = (1- eps)/a m = lambda j, eps: 8 * b * log(q/eps) * (1 - (2 * pi**2 * sigma**2)/(q**2))**(-2**(a-j)) # noqa c1 = (q**b-1)/2 * ((a-1)*(a-2)/2 * (n+1) - b/6 * (a*(a-1) * (a-2))) c2 = sum([m(j, epp) * (a-1-j)/2 * (n+2) for j in range(a)]) c3 = (2*sum([m(j, epp) for j in range(a)]) + cfft * n * q**b * log(q, 2)) * c_cost c4 = (a-1)*(a-2) * b * (q**b - 1)/2 nrops = RR(c1 + c2 + c3 + c4) nbops = RR(log(q, 2) * nrops) ncalls = (a-1) * (q**b - 1)/2 + m(0, eps) nmem = ((q**b - 1)/2 * (a-1) * (n + 1 - b*(a-2)/2)) + m(0, eps) + c_mem * q**b current = OrderedDict([(u"t", t), (u"bop", nbops), (u"oracle", ncalls), (u"m", m(0, eps)), (u"mem", nmem), (u"rop", nrops), (u"a", a), (u"b", b), ]) if optimisation_target != u"oracle": current = cost_reorder(current, (optimisation_target, u"oracle", u"t")) else: current = cost_reorder(current, (optimisation_target, u"t")) return current best_runtime = None best_samples = None best = None may_terminate = False t = RR(2*(log(q, 2) - log(sigma, 2))/log(n, 2)) while True: current = _run(t) if get_verbose() >= 2: print cost_str(current) # Similar to BKW-Decision, both the fewest samples required and the # best runtime are provided when choosing 't' such that the two steps # are balanced. To be safe, every 't' is tested until the fewest number # of samples required is found. if has_samples: has_enough_samples = current["oracle"] < samples if not best_samples: best_samples = current else: if best_samples["oracle"] > current["oracle"]: best_samples = current if has_enough_samples and ( best is None or best[optimisation_target] > current[optimisation_target]): best = current else: if may_terminate: break may_terminate = True if not best_runtime: best_runtime = current else: if best_runtime[optimisation_target] > current[optimisation_target]: best_runtime = current if has_enough_samples and (best is None or best[optimisation_target] > current[optimisation_target]): best = current else: if not has_samples or may_terminate: break may_terminate = True t += 0.05 if best is None: raise InsufficientSamplesError("Too few samples (%d given) to achieve a success probability of %f." % ( samples, success_probability)) return best def _bkw_coded(n, alpha, q, t2, b, success_probability=0.99, ntest=None, secret_bounds=None, h=None): """ Estimate complexity of Coded-BKW as described in [C:GuoJohSta15] :param n: dimension > 0 :param alpha: fraction of the noise α < 1.0 :param q: modulus > 0 :param t2: number of coded BKW steps (≥ 0) :param b: table size (≥ 1) :param success_probability: probability of success < 1.0, IGNORED :param ntest: optional parameter ntest :returns: a cost estimate :rtype: OrderedDict .. note:: You probably want to call bkw_coded instead. """ n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability) sigma = stddevf(alpha*q) # [C:GuoJohSta15] use σ = standard deviation RR = alpha.parent() cost = OrderedDict() # Our cost is mainly determined by q**b, on the other hand there are # expressions in q**(l+1) below, hence, we set l = b - 1. This allows to # achieve the performance reported in [C:GuoJohSta15]. b = ZZ(b) cost["b"] = b l = b - 1 cost["l"] = l gamma = RR(1.2) # TODO make this dependent on success_probability if secret_bounds: d = secret_bounds[1] - secret_bounds[0] + 1 else: d = 3*sigma # TODO make this dependent on success_probability cost["d"] = d cost[u"γ"] = gamma def N(i, sigma_set): """ Return $N_i$ for the $i$-th $[N_i, b]$ linear code. :param i: index :param sigma_set: target noise level """ return floor(b/(1-log(12*sigma_set**2/ZZ(2)**i, q)/2)) def find_ntest(n, l, t1, t2, b): """ If the parameter `ntest` is not provided, we use this function to estimate it. :param n: dimension > 0 :param l: table size for hypothesis testing :param t1: number of normal BKW steps :param t2: number of coded BKW steps :param b: table size for BKW steps """ # there is no hypothesis testing because we have enough normal BKW # tables to cover all of of n if t1*b >= n: return 0 # solve for nest by aiming for ntop == 0 ntest = var("nest") sigma_set = sqrt(q**(2*(1-l/ntest))/12) ncod = sum([N(i, sigma_set) for i in range(1, t2+1)]) ntop = n - ncod - ntest - t1*b try: ntest = round(find_root(0 == ntop, 0, n)) except RuntimeError: # annoyingly we get a RuntimeError when find_root can't find a # solution, we translate to something more meaningful raise ValueError("Cannot find parameters for n=%d, l=%d, t1=%d, t2=%d, b=%d"%(n, l, t1, t2, b)) return ZZ(ntest) # we compute t1 from N_i by observing that any N_i ≤ b gives no advantage # over vanilla BKW, but the estimates for coded BKW always assume # quantisation noise, which is too pessimistic for N_i ≤ b. t1 = 0 if ntest is None: ntest_ = find_ntest(n, l, t1, t2, b) else: ntest_ = ntest sigma_set = sqrt(q**(2*(1-l/ntest_))/12) Ni = [N(i, sigma_set) for i in range(1, t2+1)] t1 = len([e for e in Ni if e <= b]) # there is no point in having more tables than needed to cover n if b*t1 > n: t1 = n//b t2 -= t1 cost["t1"] = t1 cost["t2"] = t2 # compute ntest with the t1 just computed if ntest is None: ntest = find_ntest(n, l, t1, t2, b) # if there's no ntest then there's no `σ_{set}` and hence no ncod if ntest: sigma_set = sqrt(q**(2*(1-l/ntest))/12) cost[u"σ_set"] = RR(sigma_set) ncod = sum([N(i, sigma_set) for i in range(1, t2+1)]) else: ncod = 0 ntot = ncod + ntest ntop = max(n - ncod - ntest - t1*b, 0) cost["ncod"] = ncod # coding step cost["ntop"] = ntop # guessing step, typically zero cost["ntest"] = ntest # hypothesis testing # Theorem 1: quantization noise + addition noise if secret_bounds: s_var = uniform_variance_from_bounds(*secret_bounds, h=h) coding_variance = s_var * sigma_set**2 * ntot else: coding_variance = gamma**2 * sigma**2 * sigma_set**2 * ntot sigma_final = RR(sqrt(2**(t1+t2) * sigma**2 + coding_variance)) cost[u"σ_final"] = RR(sigma_final) # we re-use our own estimator M = distinguish_required_m(sigmaf(sigma_final), q, success_probability) cost["m"] = M m = (t1+t2)*(q**b-1)/2 + M cost["oracle"] = RR(m) # Equation (7) n_ = n - t1*b C0 = (m-n_) * (n+1) * ceil(n_/(b-1)) assert(C0 >= 0) cost["C0(gauss)"] = RR(C0) # Equation (8) C1 = sum([(n+1-i*b)*(m - i*(q**b - 1)/2) for i in range(1, t1+1)]) assert(C1 >= 0) cost["C1(bkw)"] = RR(C1) # Equation (9) C2_ = sum([4*(M + i*(q**b - 1)/2)*N(i, sigma_set) for i in range(i, t2+1)]) C2 = RR(C2_) for i in range(i, t2+1): C2 += RR(ntop + ntest + sum([N(j, sigma_set) for j in range(1, i+1)]))*(M + (i-1)*(q**b - 1)/2) assert(C2 >= 0) cost["C2(coded)"] = RR(C2) # Equation (10) C3 = M*ntop*(2*d + 1)**ntop assert(C3 >= 0) cost["C3(guess)"] = RR(C3) # Equation (11) C4_ = 4*M*ntest C4 = C4_ + (2*d+1)**ntop * (cfft * q**(l+1) * (l+1) * log(q, 2) + q**(l+1)) assert(C4 >= 0) cost["C4(test)"] = RR(C4) C = (C0 + C1 + C2 + C3+ C4)/(erf(d/sqrt(2*sigma))**ntop) # TODO don't ignore success probability cost["rop"] = RR(C) cost["bop"] = RR(C)*log(RR(q), RR(2)) cost["mem"] = (t1+t2)*q**b cost = cost_reorder(cost, ["bop", "oracle", "m", "mem", "rop", "b", "t1", "t2"]) return cost def bkw_coded(n, alpha, q, success_probability=0.99, secret_bounds=None, h=None, cost_include=("bop", "oracle", "m", "mem", "rop", "b", "t1", "t2"), samples=None): """ Estimate complexity of Coded-BKW as described in [C:GuoJohSta15] by optimising parameters. :param n: dimension > 0 :param alpha: fraction of the noise α < 1.0 :param q: modulus > 0 :param success_probability: probability of success < 1.0, IGNORED :param samples: the number of available samples :returns: a cost estimate :rtype: OrderedDict EXAMPLE:: sage: n, alpha, q = unpack_lwe(Regev(64)) sage: print cost_str(bkw_coded(n, alpha, q)) bop: ≈2^53.1, oracle: ≈2^39.2, m: ≈2^30.2, mem: ≈2^40.2, rop: ≈2^49.5, ... """ bstart = ceil(log(q, 2)) def _run(b=2): # the noise is 2**(t1+t2) * something so there is no need to go beyond, say, q**2 return binary_search(_bkw_coded, 2, min(n//b, ceil(2*log(q, 2))), "t2", lambda x, best: x["rop"]<=best["rop"] and ( samples is None or best["oracle"]>samples or x["oracle"]<=samples), n, alpha, q, b=b, t2=0, secret_bounds=secret_bounds, h=h, success_probability=success_probability) best = binary_search(_run, 2, 3*bstart, "b", lambda x, best: x["rop"]<=best["rop"] and ( samples is None or best["oracle"]>samples or x["oracle"]<=samples), b=2) # binary search cannot "fail". It just outputs some X with X["oracle"]>samples. if samples is not None and best["oracle"] > samples: raise InsufficientSamplesError("Too few samples given (%d)." % samples) return best # Dual Strategy def _sis(n, alpha, q, success_probability=0.99, optimisation_target=u"bkz2", secret_bounds=None, h=None, samples=None): """Estimate cost of solving LWE by solving LWE. :param n: dimension > 0 :param alpha: fraction of the noise α < 1.0 :param q: modulus > 0 :param success_probability: probability of success < 1.0 :param secret_bounds: ignored :param h: ignored :param samples: the number of available samples :returns: a cost estimate :rtype: OrderedDict """ n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability) f = lambda eps: RR(sqrt(log(1/eps)/pi)) # noqa RR = alpha.parent() # we are solving Decision-LWE log_delta_0 = log(f(success_probability)/alpha, 2)**2 / (4*n*log(q, 2)) delta_0 = RR(2**log_delta_0) m_optimal = lattice_reduction_opt_m(n, q, delta_0) if samples is None or samples > m_optimal: m = m_optimal else: if not samples > 0: raise InsufficientSamplesError("Number of samples: %d" % samples) m = samples log_delta_0 = log(f(success_probability)/alpha, 2)/m - RR(log(q, 2)*n)/(m**2) delta_0 = RR(2**log_delta_0) # check for valid delta if delta_0 < 1: raise OutOfBoundsError(u"Detected delta_0 = %f < 1. Too few samples?!" % delta_0) ret = bkz_runtime_delta(delta_0, m) ret[u"oracle"] = m ret[u"|v|"] = RR(delta_0**m * q**(n/m)) ret[u"dim"] = m if optimisation_target != u"oracle": ret = cost_reorder(ret, [optimisation_target, u"oracle"]) else: ret = cost_reorder(ret, [optimisation_target]) return ret sis = partial(rinse_and_repeat, _sis) # Decoding @cached_function def gsa_basis(n, q, delta, m): """ Create the basis lengths. :param n: determinant is q^n :param q: determinant is q^n :param delta: root-Hermite factor :param m: lattice dimension .. note:: based on the GSA in [RSA:LinPei11]_ .. [RSA:LinPei11] Richard Lindner and Chris Peikert. Better key sizes (and attacks) for LWE-based encryption. In Aggelos Kiayias, editor, CT-RSA 2011, volume 6558 of LNCS, pages 319–339. Springer, February 2011. """ log_delta = RDF(log(delta)) log_q = RDF(log(q)) qnm = log_q*(n/m) qnm_p_log_delta_m = qnm + log_delta*m tmm1 = RDF(2*m/(m-1)) b = [(qnm_p_log_delta_m - log_delta*(tmm1 * i)) for i in xrange(m)] b = [log_q - b[-1-i] for i in xrange(m)] b = map(lambda x: x.exp(), b) return b def enum_cost(n, alpha, q, eps, delta_0, m=None, B=None, step=1, enums_per_clock=-15.1): """ Estimates the runtime for performing enumeration. :param n: dimension > 0 :param alpha: fraction of the noise α < 1.0 :param q: modulus > 0 :param eps: :param delta_0: :param m: :param B: :param step: changes the increments for the values of d[i] :param enums_per_clock: the log of the number of enumerations computed per clock cycle :returns: a cost estimate :rtype: OrderedDict """ RR = alpha.parent() step = RDF(step) if B is None: if m is None: m = lattice_reduction_opt_m(n, q, delta_0) B = gsa_basis(n, q, delta_0, m) d = [RDF(1)]*m bd = [d[i] * B[i] for i in xrange(m)] scaling_factor = RDF(sqrt(pi) / (2*alpha*q)) probs_bd = [RDF((bd[i] * scaling_factor)).erf() for i in xrange(m)] success_probability = prod(probs_bd) if RR(success_probability).is_NaN(): # try in higher precision step = RR(step) d = [RR(1)]*m bd = [d[i] * B[i] for i in xrange(m)] scaling_factor = RR(sqrt(pi) / (2*alpha*q)) probs_bd = [RR((bd[i] * scaling_factor)).erf() for i in xrange(m)] success_probability = prod(probs_bd) if success_probability.is_NaN(): return OrderedDict([(u"delta_0", delta_0), ("enum", oo), ("enumop", oo)]) # if m too small, probs_bd entries are of magnitude 1e-10 or # something like that. Therefore, success_probability=prod(probs_bd) # results in success_probability==0 and so, the loop never terminates. # To prevent this, success_probability should be calculated when needed, # i.e. at the end of each iteration and not be used to calculate things. # Then, a new problem arises: for step=1 this can take very long time, # starting at, for example, success_probability==1e-300. # Since this is a (rare) special case, an error is thrown. if not success_probability > 0: raise InsufficientSamplesError("success_probability == 0! Too few samples?!") bd = map(list, zip(bd, range(len(bd)))) bd = sorted(bd) import bisect last_success_probability = success_probability while success_probability < RDF(eps): v, i = bd.pop(0) d[i] += step v += B[i]*step last_success_probability = success_probability success_probability /= probs_bd[i] probs_bd[i] = (v * scaling_factor).erf() success_probability *= probs_bd[i] bisect.insort_left(bd, [v, i]) if success_probability == 0 or last_success_probability >= success_probability: return OrderedDict([(u"delta_0", delta_0), ("enum", oo), ("enumop", oo)]) r = OrderedDict([(u"delta_0", delta_0), ("enum", RR(prod(d))), ("enumop", RR(prod(d)) / RR(2)**enums_per_clock)]) return r def _decode(n, alpha, q, success_probability=0.99, enums_per_clock=-15.1, optimisation_target="bkz2", secret_bounds=None, h=None, samples=None): """ Estimates the optimal parameters for decoding attack :param n: dimension > 0 :param alpha: fraction of the noise α < 1.0 :param q: modulus > 0 :param success_probability: probability of success < 1.0 :param enums_per_clock: the log of the number of enumerations computed per clock cycle :param optimisation_target: lattice reduction estimate to use :param secret_bounds: ignored :param h: ignored :param samples: the number of available samples :returns: a cost estimate :rtype: OrderedDict """ n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability) if samples is not None and not samples > 1: raise InsufficientSamplesError("Number of samples: %d" % samples) RR = alpha.parent() # Number of samples are'nt considered here, because only a "good" starting delta_0 # is needed and there is no "good" value for number of samples known, # i.e. the given number of samples may not produce "good" results delta_0m1 = _sis(n, alpha, q, success_probability=success_probability)[u"delta_0"] - 1 step = RR(1.05) direction = -1 def combine(enum, bkz): current = OrderedDict() current["rop"] = enum["enumop"] + bkz[optimisation_target] for key in bkz: current[key] = bkz[key] for key in enum: current[key] = enum[key] current[u"oracle"] = m current = cost_reorder(current, ["rop", "oracle", optimisation_target]) return current depth = 6 current = None while True: delta_0 = 1 + delta_0m1 if delta_0 >= 1.0219 and current is not None: # LLL is enough break m_optimal = lattice_reduction_opt_m(n, q, delta_0) if samples is None or samples > m_optimal: m = m_optimal else: m = samples bkz = bkz_runtime_delta(delta_0, m) bkz["dim"] = m enum = enum_cost(n, alpha, q, success_probability, delta_0, m, enums_per_clock=enums_per_clock) current = combine(enum, bkz) # if lattice reduction is cheaper than enumration, make it more expensive if current[optimisation_target] < current["enumop"]: prev_direction = direction direction = -1 if direction != prev_direction: step = 1 + RR(step-1)/2 delta_0m1 /= step elif current[optimisation_target] > current["enumop"]: prev_direction = direction direction = 1 delta_0m1 *= step else: break if direction != prev_direction: step = 1 + RR(step-1)/2 depth -= 1 if depth == 0: break return current decode = partial(rinse_and_repeat, _decode, decision=False) # uSVP def kannan(n, alpha, q, tau=tau_default, tau_prob=tau_prob_default, success_probability=0.99, optimisation_target="bkz2", samples=None): """ Estimate optimal parameters for using Kannan-embedding to solve CVP. :param n: dimension > 0 :param alpha: fraction of the noise α < 1.0 :param q: modulus > 0 :param success_probability: probability of success < 1.0 :param samples: the number of available samples :returns: a cost estimate :rtype: OrderedDict """ # TODO: Include the attack described in # [RSA:BaiGal14] Shi Bai and Steven D. Galbraith. An improved compression technique # for signatures based on learning with errors. In Josh Benaloh, # editor, Topics in Cryptology – CT-RSA 2014, volume 8366 of Lecture # Notes in Computer Science, pages 28–47, San Francisco, CA, USA, # February 25–28, 2014. Springer, Heidelberg, Germany. # The estimation of computational cost is the same as kannan with dimension samples=n+m. n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability) RR = alpha.parent() log_delta_0 = log(tau*alpha*sqrt(e), 2)**2/(4*n*log(q, 2)) delta_0 = RR(2**log_delta_0) m_optimal = lattice_reduction_opt_m(n, q, delta_0) if samples is None or samples > m_optimal: m = m_optimal else: if not samples > 0: raise InsufficientSamplesError("Number of samples: %d" % samples) m = samples delta_0 = RR((q**(1-n/m)*sqrt(1/(e)) / (tau*alpha*q))**(1.0/m)) # check for valid delta if delta_0 < 1: raise OutOfBoundsError(u"Detected delta_0 = %f < 1. Too few samples?!" % delta_0) l2 = q**(1-n/m) * sqrt(m/(2*pi*e)) if l2 > q: raise NotImplementedError(u"Case where λ_2 = q not implemented.") repeat = amplify(success_probability, tau_prob) r = bkz_runtime_delta(delta_0, m, log(repeat, 2.0)) r[u"oracle"] = repeat*m if samples is None else m r[u"m"] = m r = cost_reorder(r, [optimisation_target, "oracle"]) if get_verbose() >= 2: print cost_str(r) return r # Gröbner bases def gb_complexity(m, n, d, omega=2, call_magma=True, d2=None): """Estimate the complexity of computing a Gröbner basis. Estimation is done for `m` polynomials of degree `d` in `n` variables under the assumption that the system is semi-regular. If `d2` is not ``None`` then `n` polynomials of degree are added to the system. This is to encode restrictions on the solution. For example, if the solution is either `0` or `1`, then `(x_i)⋅(x_i+1)` would evaluate to zero on it for any `x_i`. :param m: number of polynomials (integer > 0) :param n: number of variables (integer > 0) :param d: degree of all input polynomials :param omega: linear algebra exponent, i.e. matrix-multiplication costs `O(n^ω)` operations. :param call_magma: use Magma to perform computation (highly recommended) :param d2: secondary degree (integer > 0 or ``None``) :returns: A dictionary containing the expected degree of regularity "Dreg" (assuming the system is semi-regular), the number of base ring operations "rop", and the memory requirements in base ring elements "mem". :rtype: OrderedDict """ if m > n**d: m = n**d if call_magma: R = magma.PowerSeriesRing(QQ, 2*n) z = R.gen(1) coeff = lambda f, d: f.Coefficient(d) # noqa else: R = PowerSeriesRing(QQ, "z", 2*n) z = R.gen() coeff = lambda f, d: f[d] # noqa if d2 is None: s = (1-z**d)**m / (1-z)**n else: s = (1-z**d)**m * (1-z**d2)**n / (1-z)**n retval = OrderedDict([("Dreg", None), ("rop", None)]) for dreg in xrange(2*n): if coeff(s, dreg) < 0: break else: return retval retval["Dreg"] = dreg retval["rop"] = RR(binomial(n + dreg, dreg)**omega) retval["mem"] = RR(binomial(n + dreg, dreg)**2) return retval def arora_gb(n, alpha, q, success_probability=0.99, omega=2, call_magma=True, guess=0, d2=None, samples=None): if samples is not None: from warnings import warn warn("Given number of samples is ignored for arora_gb()!") n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability, prec=2*log(n, 2)*n) RR = alpha.parent() stddev = RR(stddevf(RR(alpha)*q)) if stddev >= 1.1*sqrt(n): return None if d2 is True: d2 = 2*ceil(3*stddev)+1 ps_single = lambda C: RR(1 - (2/(C*RR(sqrt(2*pi))) * exp(-C**2/2))) # noqa m = floor(exp(RR(0.75)*n)) d = n t = ZZ(floor((d-1)/2)) C = t/stddev pred = gb_complexity(m, n-guess, d, omega, call_magma, d2=d2) pred["t"] = t pred["oracle"] = m pred[u"Pr[⊥]"] = RR(m*(1-ps_single(C))) pred["bop"] = log(q, 2) * pred["rop"] pred = cost_reorder(pred, ["t", "bop", "oracle", "Dreg"]) if get_verbose() >= 2: print "PREDICTION:" print cost_str(pred) print print "ESTIMATION:" t = ceil(t/3) best = None stuck = 0 for t in srange(t, n): d = 2*t + 1 C = RR(t/stddev) if C < 1: # if C is too small, we ignore it continue # Pr[success]^m = Pr[overall success] m = log(success_probability, 2) / log(ps_single(C), 2) if m < n: continue m = floor(m) current = gb_complexity(m, n-guess, d, omega, call_magma, d2=d2) if current["Dreg"] is None: continue current["t"] = t current[u"Pr[⊥]"] = RR(1-success_probability) current["rop"] *= RR((3*stddev)**guess) current["bop"] = log(q, 2) * current["rop"] current["oracle"] = m current = cost_reorder(current, ["bop", "oracle", "t", "Dreg"]) if get_verbose() >= 2: print cost_str(current) if best is None: best = current else: if best["rop"] > current["rop"]: best = current stuck = 0 else: stuck += 1 if stuck >= 5: break return best # Exhaustive Search for Small Secrets def small_secret_guess(f, n, alpha, q, secret_bounds, h=None, samples=None, **kwds): size = secret_bounds[1]-secret_bounds[0] + 1 best = None step_size = 16 fail_attempts, max_fail_attempts = 0, 5 while step_size >= n: step_size /= 2 i = 0 while True: if i<0: break try: try: # some implementations make use of the secret_bounds parameter current = f(n-i, alpha, q, secret_bounds=secret_bounds, samples=samples, **kwds) except TypeError: current = f(n-i, alpha, q, samples=samples, **kwds) except (OutOfBoundsError, RuntimeError, InsufficientSamplesError) as err: if get_verbose() >= 2: print type(err).__name__, ":", err i += abs(step_size) fail_attempts += 1 if fail_attempts > max_fail_attempts: break continue if h is None or i current[key]: best = current i += step_size else: step_size = -1*step_size//2 i += step_size if step_size == 0: break if best is None: raise RuntimeError("No solution could be found.") return best # Modulus Switching def decode_small_secret_mod_switch_and_guess(n, alpha, q, secret_bounds, h=None, samples=None, **kwds): """Solve LWE by solving BDD for small secret instances. :param n: dimension > 0 :param alpha: fraction of the noise α < 1.0 :param q: modulus > 0 :param secret_bounds: :param h: number of non-zero components in the secret :param samples: the number of available samples """ s_var = uniform_variance_from_bounds(*secret_bounds, h=h) n, alpha, q = switch_modulus(n, alpha, q, s_var, h=h) return small_secret_guess(decode, n, alpha, q, secret_bounds, h=h, samples=samples, **kwds) def kannan_small_secret_mod_switch_and_guess(n, alpha, q, secret_bounds, h=None, samples=None, **kwds): """Solve LWE by Kannan embedding for small secret instances. :param n: dimension > 0 :param alpha: fraction of the noise α < 1.0 :param q: modulus > 0 :param secret_bounds: :param h: number of non-zero components in the secret. :param samples: the number of available samples """ s_var = uniform_variance_from_bounds(*secret_bounds, h=h) n, alpha, q = switch_modulus(n, alpha, q, s_var, h=h) return small_secret_guess(kannan, n, alpha, q, secret_bounds, h=h, samples=samples, **kwds) # Bai's and Galbraith's uSVP Attack def _bai_gal_small_secret(n, alpha, q, secret_bounds, tau=tau_default, tau_prob=tau_prob_default, success_probability=0.99, optimisation_target="bkz2", h=None, samples=None): """ :param n: dimension > 0 :param alpha: fraction of the noise α < 1.0 :param q: modulus > 0 :param tau: 0 < τ ≤ 1.0 :param success_probability: probability of success < 1.0 :param optimisation_target: field to use to decide if parameters are better :param h: number of non-zero components in the secret """ n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability) RR = alpha.parent() stddev = stddevf(alpha*q) a, b = secret_bounds if h is None: xi = RR(2)/(b-a) else: assert -a == b # TODO: don't be so lazy # we compute the stddev of |s| and xi to scale each component to σ on average variance = sum([ZZ(h)/n * i**2 for i in range(a, b+1) if i]) s_stddev = variance.sqrt() xi = ZZ(1)/s_stddev num = (log(q/stddev) - log(tau*sqrt(4*pi*e)))**2 * log(q/stddev) den = n*(2*log(q/stddev)-log(xi))**2 log_delta_0 = RR(num/den) delta_0 = RR(e**log_delta_0) m_prime_optimal = ceil(sqrt(n*(log(q)-log(stddev))/log_delta_0)) if samples is None or samples > m_prime_optimal - n: m_prime = m_prime_optimal else: m = samples m_prime = m+n num = m_prime*(log(q/stddev) - log(2*tau*sqrt(pi*e))) +n*log(xi)-n*log(q/stddev) den = m_prime**2 log_delta_0 = RR(num/den) delta_0 = RR(e**log_delta_0) m = m_prime - n l2 = RR((q**m * (xi*stddev)**n)**(1/m_prime) * sqrt(m_prime/(2*pi*e))) if l2 > q: raise NotImplementedError("Case λ_2 = q not implemented.") repeat = amplify(success_probability, tau_prob) r = bkz_runtime_delta(delta_0, m_prime, log(repeat, 2)) r[u"oracle"] = repeat*m if samples is None else m r[u"m"] = m if optimisation_target != u"oracle": r = cost_reorder(r, [optimisation_target, u"oracle"]) else: r = cost_reorder(r, [optimisation_target]) if get_verbose() >= 2: print cost_str(r) return r def bai_gal_small_secret(n, alpha, q, secret_bounds, tau=tau_default, tau_prob=tau_prob_default, success_probability=0.99, optimisation_target="bkz2", h=None, samples=None): """ Bai's and Galbraith's uSVP attack + small secret guessing [ACISP:BaiGal14]_ :param n: dimension > 0 :param alpha: fraction of the noise α < 1.0 :param q: modulus > 0 :param tau: 0 < τ ≤ 1.0 :param success_probability: probability of success < 1.0 :param optimisation_target: field to use to decide if parameters are better :param h: number of non-zero components in the secret :param samples: the number of available samples .. [ACISP:BaiGal14] Bai, S., & Galbraith, S. D. (2014). Lattice decoding attacks on binary LWE. In W. Susilo, & Y. Mu, ACISP 14 (pp. 322–337). """ return small_secret_guess(_bai_gal_small_secret, n, alpha, q, secret_bounds, tau=tau, tau_prob=tau_prob, success_probability=0.99, optimisation_target=optimisation_target, h=h, samples=samples) # Small, Sparse Secret SIS def success_probability_drop(n, h, k, fail=0): """ Probability ``k`` randomly sampled components have at most ``fail`` non-zero components amongst them. :param n: dimension of LWE samples :param h: number of non-zero components :param k: number of components to ignore :param fail: we tolerate ``fail`` number of non-zero components amongst the ``k`` ignored components """ N = n # population size K = n-h # number of success states in the population n = k # number of draws k = n - fail # number of observed successes return (binomial(K, k)*binomial(N-K, n-k)) / binomial(N, n) def drop_and_solve(f, n, alpha, q, secret_bounds=None, h=None, success_probability=0.99, optimisation_target=u"bkz2", postprocess=False, **kwds): """ Solve instances of dimension ``n-k`` with increasing ``k`` using ``f`` and pick parameters such that cost is reduced. :param n: dimension :param alpha: noise parameter :param q: modulus q :param secret_bounds: lower and upper bound on the secret :param h: number of non-zero components of the secret :param success_probability: target success probability :param optimisation_target: what value out to be minimized """ n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability) RR = alpha.parent() best = None # too small a step size leads to an early abort, too large a step # size means stepping over target step_size = int(n/32) k = 0 while True: current = f(n-k, alpha, q, success_probability=max(1-1/RR(2)**80, success_probability), optimisation_target=optimisation_target, h=h, secret_bounds=secret_bounds, **kwds) cost_lat = current[optimisation_target] cost_post = 0 probability = success_probability_drop(n, h, k) if postprocess: repeat = current["repeat"] dim = current["dim"] for i in range(1, k): # compute inner products with rest of A cost_post_i = 2 * repeat * dim * k # there are (k)(i) positions and max(s_i)-min(s_i) options per position # for each position we need to add/subtract the right elements cost_post_i += repeat * binomial(k, i) * (secret_bounds[1]-secret_bounds[0])**i * i if cost_post + cost_post_i >= cost_lat: postprocess = i break cost_post += cost_post_i probability += success_probability_drop(n, h, k, i) current["rop"] = cost_lat + cost_post current = cost_repeat(current, 1/probability) current["k"] = k current["postprocess"] = postprocess current = cost_reorder(current, ["rop"]) key = list(current)[0] if best is None: best = current k += step_size continue if current[key] < best[key]: best = current k += step_size else: # we go back k = best["k"] - step_size k += step_size/2 if k <= 0: k = step_size/2 # and half the step size step_size = step_size/2 if step_size == 0: break return best sis_drop_and_solve = partial(drop_and_solve, sis) decode_drop_and_solve = partial(drop_and_solve, decode) bai_gal_drop_and_solve = partial(drop_and_solve, bai_gal_small_secret) def sis_small_secret_mod_switch(n, alpha, q, secret_bounds, h=None, success_probability=0.99, optimisation_target=u"bkz2", c=None, use_lll=False, samples=None): """ Estimate cost of solveing LWE by finding small `(y,x/c)` such that `y A = c x`. :param n: dimension :param alpha: noise parameter :param q: modulus q :param secret_bounds: lower and upper bound on the secret :param h: number of non-zero components of the secret :param success_probability: target success probability :param optimisation_target: what value out to be minimized :param c: explicit constant `c` :param use_lll: use LLL calls to produce more small vectors """ if samples is not None: from warnings import warn warn("Given number of samples is ignored for sis_small_secret_mod_switch()!") n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability) RR = alpha.parent() assert(secret_bounds[0] == -1 and secret_bounds[1] == 1) if h is None: B = ZZ(secret_bounds[1] - secret_bounds[0] + 1) h = ceil((B-1)/B * n) # stddev of the error e = stddevf(alpha*q).n() delta_0 = sis(n, alpha, q, optimisation_target=optimisation_target)["delta_0"] best = None if c is None: c = RR(e*sqrt(2*n - n)/sqrt(h)) if use_lll: scale = 2 else: scale = 1 while True: m = lattice_reduction_opt_m(n, c*q, delta_0) # the vector found will have norm v = scale * delta_0**m * (q/c)**(n/m) # each component has stddev v_ v_ = v/RR(sqrt(m)) # we split our vector in two parts. # 1. v_r is multiplied with the error e (dimension m-n) # 2. v_l is the rounding noise (dimension n) # scale last q components down again. v_r = e*sqrt(m-n)*v_ v_l = c*sqrt(h)*v_ repeat = max(distinguish_required_m(v_r, q, success_probability, other_sigma=v_l), RR(1)) ret = bkz_runtime_delta(delta_0, m, log(repeat, 2)) if use_lll: if "lp" in ret: keys = ("bkz2", "sieve", "lp") else: keys = ("bkz2", "sieve") ret_lll = bkz_runtime_delta(delta_0, m) for key in keys: # CN11: LLL: n^3 log^2 B ret_lll[key] += (n**3 * log(q, 2)**2) * repeat ret = ret_lll ret[u"oracle"] = m * repeat ret[u"repeat"] = repeat ret[u"dim"] = m ret[u"c"] = c ret = cost_reorder(ret, [optimisation_target, u"oracle"]) if get_verbose() >= 2: print cost_str(ret) if best is None: best = ret if ret[optimisation_target] > best[optimisation_target]: break best = ret delta_0 = delta_0 + RR(0.00005) return best def applebaum_transform(n, alpha, q, m, secret_bounds, h=None): """ Swap part of the error vector with the secret as suggested in [EPRINT:GenHalSma12]_ .. [EPRINT:GenHalSma12] Gentry, C., Halevi, S., & Smart, N. P. (2012). Homomorphic evaluation of the AES circuit. :param n: dimension :param alpha: noise parameter :param q: modulus q :param m: number of samples in lattice. :param secret_bounds: lower and upper bound on the secret :param h: number of non-zero components of the secret :param success_probability: target success probability """ # we update alpha to reflect that we're replacing part of the error by the secret s_var = uniform_variance_from_bounds(*secret_bounds, h=h) e_var = stddevf(alpha*q)**2 stddev_ = ((n*s_var + (m-n)*e_var)/m).sqrt() alpha_ = alphaf(stddev_, q, sigma_is_stddev=True) alpha = alpha_ return n, alpha, q def applebaum(f, n, alpha, q, m, secret_bounds, h=None, success_probability=0.99, optimisation_target=u"bkz2", **kwds): """Run ``f`` after transforming LWE instance using ``applebaum_transform``. :param f: LWE solving cost function :param n: dimension :param alpha: noise parameter :param q: modulus q :param m: number of samples in lattice. :param secret_bounds: lower and upper bound on the secret :param h: number of non-zero components of the secret :param success_probability: target success probability :param optimisation_target: use this field to optimise """ n, alpha, q = applebaum_transform(n, alpha, q, m, secret_bounds, h) return f(n, alpha, q, success_probability=success_probability, optimisation_target=optimisation_target) sis_applebaum = partial(applebaum, sis) decode_applebaum = partial(applebaum, decode) # BKW for Small Secrets def bkw_small_secret_variances(q, a, b, kappa, o, RR=None): """ Helper function for small secret BKW variant. :param q: :param a: :param b: :param kappa: :param o: :param RR: :returns: :rtype: """ if RR is None: RR = RealField() q = RR(q) a = RR(a).round() b = RR(b) n = a*b kappa = RR(kappa) T = RR(2)**(b*kappa) n = RR(o)/RR(T*(a+1)) + RR(1) U_Var = lambda x: (x**2 - 1)/12 # noqa red_var = 2*U_Var(q/(2**kappa)) if o: c_ = map(RR, [0.0000000000000000, 0.4057993538687922, 0.6924478992819291, 0.7898852691349439, 0.8441959360364506, 0.8549679124679972, 0.8954469872316165, 0.9157093365103325, 0.9567635780119543, 0.9434245442818547, 0.9987153221343770]) M = Matrix(RR, a, a) # rows are tables, columns are entries those tables for l in range(M.ncols()): for c in range(l, M.ncols()): M[l, c] = U_Var(q) for l in range(1, a): for i in range(l): M[l, i] = red_var + sum(M[i+1:l].column(i)) bl = b*l if round(bl) < len(c_): c_tau = c_[round(bl)] else: c_tau = RR(1)/RR(5)*RR(sqrt(bl)) + RR(1)/RR(3) f = (c_tau*n**(~bl) + 1 - c_tau)**2 for i in range(l): M[l, i] = M[l, i]/f v = vector(RR, a) for i in range(a): v[i] = red_var + sum(M[i+1:].column(i)) else: v = vector(RR, a) for i in range(a)[::-1]: v[i] = 2**(a-i-1) * red_var if get_verbose() >= 3: print log(red_var, 2).str(), [RealField(14)(log(x, 2)).str() for x in v] return v def bkw_small_secret(n, alpha, q, success_probability=0.99, secret_bounds=(0, 1), t=None, o=0, samples=None): # noqa """ :param n: number of variables in the LWE instance :param alpha: standard deviation of the LWE instance :param q: size of the finite field (default: n^2) :param secret_bounds: minimum and maximum value of secret :param samples: the number of available samples """ def sigma2f(kappa): v = bkw_small_secret_variances(q, a, b, kappa, o, RR=RR) return sigmaf(sum([b * e * secret_variance for e in v], RR(0)).sqrt()) def Tf(kappa): return min(q**b, ZZ(2)**(b*kappa))/2 def ops_tf(kappa): T = Tf(kappa) return T * (a*(a-1)/2 * (n+1) - b*a*(a-1)/4 - b/6 * ((a-1)**3 + 3/2*(a-1)**2 + 1/RR(2)*(a-1))) def bkwssf(kappa): ret = OrderedDict() ret[u"κ"] = kappa m = distinguish_required_m(sigma_final, q, success_probability, sigma2f(kappa)) ret["m"] = m ropsm = (m + o) * (a/2 * (n + 2)) ropst = ops_tf(kappa) ret["rop"] = ropst + ropsm ret["bop"] = log(q, 2) * ret["rop"] T = Tf(kappa) ret["mem"] = T * a * (n + 1 - b * (a-1)/2) ret["oracle"] = T * a + ret["m"] + o return ret n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability, prec=4*n) RR = alpha.parent() sigma = alpha*q has_samples = samples is not None if o is None: best = bkw_small_secret(n, alpha, q, success_probability, secret_bounds, t=t, o=0) o = best["oracle"]/2 while True: try: current = bkw_small_secret(n, alpha, q, success_probability, secret_bounds, t=t, o=o) except InsufficientSamplesError: break if best is None or (current["bop"] < best["bop"] and (not has_samples or current["oracle"] <= samples)): best = current if current["bop"] > best["bop"]: break if get_verbose() >= 2: print cost_str(current) o = o/2 if has_samples and best["oracle"] > samples: raise InsufficientSamplesError("No solution could be found with given samples (%d)" % samples) return best if t is None: t = RR(2*(log(q, 2) - log(sigma, 2))/log(n, 2)) best = None while True: try: current = bkw_small_secret(n, alpha, q, success_probability, secret_bounds, t=t, o=o) except InsufficientSamplesError: break if best is None or (current["bop"] < best["bop"] and (not has_samples or current["oracle"] <= samples)): best = current if current["bop"] > best["bop"]: break if get_verbose() >= 2: print cost_str(current) t += 0.01 if has_samples and best["oracle"] > samples: raise InsufficientSamplesError("No solution could be found with given samples (%d)" % samples) return best secret_variance = uniform_variance_from_bounds(*secret_bounds) secret_variance = RR(secret_variance) a = RR(t*log(n, 2)) # the target number of additions: a = t*log_2(n) b = n/a # window width b = n/a sigma_final = RR(n**t).sqrt() * sigma # after n^t additions we get this stddev transformation_noise = sqrt(n * 1/RR(12) * secret_variance) kappa = ceil(log(round(q*transformation_noise/stddevf(sigma)), 2.0)) + 1 if kappa > ceil(log(q, 2)): kappa = ceil(log(q, 2)) best = None while kappa > 0: current = bkwssf(kappa) if best is None or (current["bop"] < best["bop"] and (not has_samples or current["oracle"] <= samples)): best = current if current["bop"] > best["bop"]: break kappa -= 1 if has_samples and best["oracle"] > samples: raise InsufficientSamplesError("No solution could be found with given samples (%d)" % samples) best["o"] = o best["t"] = t best["a"] = a best["b"] = b best = cost_reorder(best, ["bop", "oracle", "t", "m", "mem"]) return best # Arora-GB for Small Secrets def arora_gb_small_secret(n, alpha, q, secret_bounds, h=None, samples=None, **kwds): """FIXME! briefly describe function :param n: :param alpha: :param q: :param secret_bounds: :param h: :returns: :rtype: """ a, b = secret_bounds s_var = uniform_variance_from_bounds(*secret_bounds, h=h) n, alpha, q = switch_modulus(n, alpha, q, s_var, h=h) return arora_gb(n, alpha, q, d2=b-a+1, **kwds) # Toplevel function def estimate_lwe(n, alpha, q, samples=None, skip=None, small=False, secret_bounds=None, h=None): """ Estimate the complexity of solving LWE with the given parameters. :param n: :param alpha: :param q: :param skip: :param small: :param secret_bounds: :returns: :rtype: EXAMPLE:: sage: n, alpha, q = unpack_lwe(Regev(64)) sage: set_verbose(1) sage: d = estimate_lwe(n,alpha,q, skip="arora-gb") mitm bop: ≈2^133.2, oracle: 52, mem: ≈2^129.6, rop: ≈2^129.6 bkw bop: ≈2^53.1, oracle: ≈2^39.2, m: ≈2^30.2, mem: ≈2^40.2, ... sis bkz2: ≈2^34.5, oracle: ≈2^11.7, δ_0: 1.0130466, k: 40, ... dec bop: ≈2^33.9, oracle: 1288, δ_0: 1.0157998, bkz2: ≈2^32.8, ... kannan bkz2: ≈2^35.3, oracle: ≈2^12.9, δ_0: 1.0171085, k: 40, ... """ if not small: algorithms = OrderedDict([("mitm", mitm), ("bkw", bkw_coded), ("sis", sis), ("dec", decode), ("kannan", kannan), ("arora-gb", arora_gb)]) else: algorithms = OrderedDict([("mitm", mitm), ("bkw", bkw_coded), ("sis", partial(drop_and_solve, sis_small_secret_mod_switch)), ("dec", decode_small_secret_mod_switch_and_guess), ("kannan", kannan_small_secret_mod_switch_and_guess), ("baigal", bai_gal_small_secret), ("arora-gb", arora_gb_small_secret)]) if skip is None: skip = [] try: skip = [s.strip().lower() for s in skip.split(",")] except AttributeError: pass skip = [s.strip().lower() for s in skip] alg_width = max(len(key) for key in set(algorithms).difference(skip)) cost_kwds = {"keyword_width": 5} results = OrderedDict() for alg in algorithms: if alg not in skip: algf = algorithms[alg] if alg in ("dec", "sis", "kannan", "baigal"): algf = sieve_or_enum(algf) try: if small: tmp = algf(n, alpha, q, secret_bounds=secret_bounds, h=h, samples=samples) else: tmp = algf(n, alpha, q, samples=samples) if tmp: results[alg] = tmp if get_verbose() >= 1: print ("%%%ds" % alg_width) % alg, print cost_str(results[alg], **cost_kwds) except Exception as e: print u"Algorithm '%s' failed with message '%s'"%(alg, e) return results # Plots def plot_costs(LWE, N, skip=None, filename=None, small=False, secret_bounds=None): plots = {} for n in N: lwe = LWE(n) r = estimate_lwe(skip=skip, small=small, secret_bounds=secret_bounds, **unpack_lwe_dict(lwe)) if get_verbose() >= 1: print for key in r: value = r[key].values()[0] plots[key] = plots.get(key, tuple()) + ((n, log(value, 2)),) colors = ("#4C72B0", "#55A868", "#C44E52", "#8172B2", "#CCB974", "#64B5CD") import matplotlib.pyplot as plt plt.clf() plt.figure(1) for i, plot in enumerate(plots): x, y = [x_ for x_, y_ in plots[plot]], [y_ for x_, y_ in plots[plot]] plt.plot(x, y, label=plot, color=colors[i], linewidth=1.5) plt.legend(loc=2) plt.xlabel("n") plt.ylabel("$\log_2$(bop)") if small: plt.title(u"%s (%d-%d), $s ← %s^n$"%(LWE.__name__, N[0], N[-1], secret_bounds)) else: plt.title(u"%s (%d-%d)"%(LWE.__name__, N[0], N[-1])) if filename is None: if small: small_str = "-(%d,%d)"%(secret_bounds[0], secret_bounds[1]) else: small_str = "" filename="%s%s-%d-%d.pdf"%(LWE.__name__, small_str, N[0], N[-1]) plt.savefig(filename, dpi=128) def plot_fhe_costs(L, N, skip=None, filename=None, small=False, secret_bounds=None): plots = {} for n in N: params = fhe_params(L, n) r = estimate_lwe(*params, skip=skip, small=small, secret_bounds=secret_bounds) if get_verbose() >= 1: print for key in r: value = r[key].values()[0] plots[key] = plots.get(key, tuple()) + ((n, log(value, 2)),) colors = ("#4C72B0", "#55A868", "#C44E52", "#8172B2", "#CCB974", "#64B5CD") import matplotlib.pyplot as plt plt.clf() plt.figure(1) for i, plot in enumerate(plots): x, y = [x_ for x_, y_ in plots[plot]], [y_ for x_, y_ in plots[plot]] plt.plot(x, y, label=plot, color=colors[i], linewidth=1.5) plt.legend(loc=2) plt.xlabel("n") plt.ylabel("$\log_2$(bop)") if small: plt.title(u"FHE (%d-%d), $L=%d$, $s ← %s^n$"%(N[0], N[-1], L, secret_bounds)) else: plt.title(u"FHE (%d-%d), $L=%d$"%(N[0], N[-1], L)) if filename is None: if small: small_str = "-(%d,%d)"%(secret_bounds[0], secret_bounds[1]) else: small_str = "" filename="FHE-%d%s-%d-%d.pdf"%(L, small_str, N[0], N[-1]) plt.savefig(filename, dpi=128) # LaTeX tables dfs = "%4.0f" latex_config = { "mitm": OrderedDict([("bop", dfs), ("mem", dfs), ("oracle", dfs)]), "bkw": OrderedDict([("bop", dfs), ("mem", dfs), ("oracle", dfs)]), "arora-gb": OrderedDict([("bop", dfs), ("mem", dfs), ("oracle", dfs)]), "sis": OrderedDict([("bkz2", dfs), ("sieve", dfs), ("oracle", dfs), ("repeat", dfs)]), "kannan": OrderedDict([("bkz2", dfs), ("sieve", dfs), ("oracle", dfs), ("repeat", dfs)]), "baigal": OrderedDict([("bkz2", dfs), ("sieve", dfs), ("oracle", dfs), ("repeat", dfs)]), "dec": OrderedDict([("rop", dfs), ("enum", dfs), ("oracle", dfs), ("repeat", dfs)]), } def latex_cost_header(cur): header = [] header.append(r"\begin{scriptsize}") pretty_algorithm_names = { "mitm": "MitM", "bkw": "Coded-BKW", "arora-gb": "Arora-GB", "sis": "SIS", "kannan": "Kannan", "baigal": "Bai-Gal", "dec": "Dec" } pretty_column_names = { "oracle": "$\\Ldis$", "repeat": "g", } line = [r"\begin{tabular}{r"] for alg in cur: line.append("@{\hskip 8pt}") line.append("r" * len([key for key in latex_config[alg].keys() if key in cur[alg]])) line.append("}") header.append("".join(line)) header.append(r"\toprule") line = [" "] for alg in cur: count = len([key for key in latex_config[alg].keys() if key in cur[alg]]) line.append(r"\multicolumn{%d}{c}{%s}"%(count, pretty_algorithm_names[alg])) line = " & ".join(line) + "\\\\" header.append(line) header.append(r"\midrule") line = [" $n$"] for alg in cur: line.extend([pretty_column_names.get(key, key) for key in latex_config[alg].keys() if key in cur[alg]]) line = " & ".join(line) + "\\\\" header.append(line) header.append(r"\midrule") return header def latex_cost_row(cur): line = [] for alg in cur: cost = cur[alg] for col, format in latex_config[alg].iteritems(): if (col == "repeat" and col in cost) or col != "repeat": line.append(format % log(cost[col], 2)) return line def latex_cost_footer(name): footer = [] footer.append(r"\bottomrule") footer.append(r"\end{tabular}") footer.append(r"\end{scriptsize}") footer.append(r"\caption{%s}" % name) return footer def latex_costs(LWE, N, skip=None, small=False, secret_bounds=None): ret = [] for i, n in enumerate(N): line = ["%4d"%n] lwe = LWE(n) cur = estimate_lwe(skip=skip, small=small, secret_bounds=secret_bounds, **unpack_lwe_dict(lwe)) line.extend(latex_cost_row(cur)) line = " & ".join(line) + "\\\\" ret.append(line) if get_verbose() >= 1: print header = latex_cost_header(cur) if small: name = "%s with $\s[(i)] \sample \{%d,%d\}$"%(LWE.__name__, secret_bounds[0], secret_bounds[1]) else: name = LWE.__name__ footer = latex_cost_footer(name) ret = header + ret + footer return "\n".join(ret) def fhe_params(L, n): # Homomorphic Evaluation of the AES Circuit talks about σ^2 as variance so σ is stddev not width # parameter stddev = RR(3.2) xi = ZZ(8) q = ZZ(2)**(16.5*L + 5.4) * xi**(2*L-3) * n**L alpha = sigmaf(stddev)/q return n, alpha, q def latex_fhe_costs(N, l, secret_bounds, skip=None): ret = [] for n in N: line = ["%6d"%n] params = fhe_params(l, n) cur = estimate_lwe(*params, skip=skip, small=True, secret_bounds=secret_bounds) line.extend(latex_cost_row(cur)) line = " & ".join(line) + "\\\\" ret.append(line) if get_verbose() >= 1: print header = latex_cost_header(cur) name = "FHE with $L=%d$ with $\s[(i)] \sample \{%d,%d\}$"%(l, secret_bounds[0], secret_bounds[1]) footer = latex_cost_footer(name) ret = header + ret + footer return "\n".join(ret) def make_all_tables(): N = (64, 128, 256, 512, 1024) print latex_costs(Regev, N, skip=["arora-gb"]) print print latex_costs(Regev, N, small=True, secret_bounds=(0, 1), skip=["arora-gb"]) print print latex_costs(LindnerPeikert, N) print print latex_costs(LindnerPeikert, N, small=True, secret_bounds=(0, 1), skip=["arora-gb"]) print latex_fhe_costs([2**i for i in range(6, 12)], l=2, skip="Arora-GB", secret_bounds=(0, 1)) print print latex_fhe_costs([2**i for i in range(6, 15)], l=10, skip="Arora-GB", secret_bounds=(0, 1)) print def make_all_plots(): v = get_verbose() set_verbose(1) N = range(64, 400, 16) plot_costs(Regev, N, skip=["arora-gb", "mitm"]) plot_costs(LindnerPeikert, N, skip=["arora-gb", "mitm"]) plot_costs(Regev, N, small=True, secret_bounds=(0, 1), skip=["arora-gb"]) plot_costs(LindnerPeikert, N, small=True, secret_bounds=(0, 1), skip=["arora-gb"]) set_verbose(v) class SimpleLWE(LWE): """ LWE parameters with `σ=\sqrt{n/2π}` and `q = n^2`. """ def __init__(self, n): """ LWE parameters with `σ=\sqrt{n/2π}` and `q = n^2`. :param n: security parameter n (= dimension) """ from sage.stats.distributions.discrete_gaussian_integer import DiscreteGaussianDistributionIntegerSampler from sage.rings.arith import next_prime q = ZZ(next_prime(n**2)) s = sqrt(n) D = DiscreteGaussianDistributionIntegerSampler(s/sqrt(2*pi.n()), q) LWE.__init__(self, n=n, q=q, D=D, secret_dist=(0, 1))