# -*- coding: utf-8 -*- """ Seucrity estimates for the Hybrid Decoding attack Requires a local copy of the LWE Estimator from https://bitbucket.org/malb/lwe-estimator/src/master/estimator.py in a folder called "estimator" """ from sage.all import ZZ, binomial, sqrt, log, exp, oo, pi, prod, RR from sage.probability.probability_distribution import RealDistribution from estimator import estimator as est from concrete_params import concrete_LWE_params ## Core cost models core_sieve = lambda beta, d, B: ZZ(2)**RR(0.292*beta + 16.4) core_qsieve = lambda beta, d, B: ZZ(2)**RR(0.265*beta + 16.4) ## Utility functions def sq_GSO(d, beta, det): """ Return squared GSO lengths after lattice reduction according to the GSA :param q: LWE modulus :param d: lattice dimension :param beta: blocksize used in BKZ :param det: lattice determinant """ r = [] for i in range(d): r_i = est.delta_0f(beta)**(((-2*d*i) / (d-1))+d) * det**(1/d) r.append(r_i**2) return r def babai_probability_wun16(r, norm): """ Compute the probability of Babai's Nearest Plane, using techniques from the NTRULPrime submission to NIST :param r: squared GSO lengths :param norm: expected norm of the target vector """ R = [RR(sqrt(t)/(2*norm)) for t in r] T = RealDistribution('beta', ((len(r)-1)/2,1./2)) probs = [1 - T.cum_distribution_function(1 - s**2) for s in R] return prod(probs) ## Estimate hybrid decoding attack complexity def hybrid_decoding_attack(n, alpha, q, m, secret_distribution, beta, tau = None, mitm=True, reduction_cost_model=est.BKZ.sieve): """ Estimate cost of the Hybrid Attack, :param n: LWE dimension `n > 0` :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/sqrt{2π}` :param q: modulus `0 < q` :param m: number of LWE samples `m > 0` :param secret_distribution: distribution of secret :param beta: BKZ block size β :param tau: guessing dimension τ :param mitm: simulate MITM approach (√ of search space) :param reduction_cost_model: BKZ reduction cost model EXAMPLE: hybrid_decoding_attack(beta = 100, tau = 250, mitm = True, reduction_cost_model = est.BKZ.sieve, **example_64()) rop: 2^65.1 pre: 2^64.8 enum: 2^62.5 beta: 100 |S|: 2^73.1 prob: 0.104533 scale: 12.760 pp: 11 d: 1798 repeat: 42 """ n, alpha, q = est.Param.preprocess(n, alpha, q) # d is the dimension of the attack lattice d = m + n - tau # h is the Hamming weight of the secret # NOTE: binary secrets are assumed to have Hamming weight ~n/2, ternary secrets ~2n/3 # this aligns with the assumptions made in the LWE Estimator h = est.SDis.nonzero(secret_distribution, n=n) sd = alpha*q/sqrt(2*pi) # compute the scaling factor used in the primal lattice to balance the secret and error scale = est._primal_scale_factor(secret_distribution, alpha=alpha, q=q, n=n) # 1. get squared-GSO lengths via the Geometric Series Assumption # we could also consider using the BKZ simulator, using the GSA is conservative r = sq_GSO(d, beta, q**m * scale**(n-tau)) # 2. Costs bkz_cost = est.lattice_reduction_cost(reduction_cost_model, est.delta_0f(beta), d) enm_cost = est.Cost() enm_cost["rop"] = d**2/(2**1.06) # 3. Size of search space # We need to do one BDD call at least search_space, prob, hw = ZZ(1), 1.0, 0 # if mitm is True, sqrt speedup in the guessing phase. This allows us to square the size # of the search space at no extra cost. # NOTE: we conservatively assume that this mitm process succeeds with probability 1. ssf = sqrt if mitm else lambda x: x # use the secret distribution bounds to determine the size of the search space a, b = est.SDis.bounds(secret_distribution) # perform "searching". This part of the code balances the enm_cost with the cost of lattice # reduction, where enm_cost is the total cost of calling Babai's algorithm on each vector in # the search space. if tau: prob = est.success_probability_drop(n, h, tau) hw = 1 while hw < h and hw < tau: prob += est.success_probability_drop(n, h, tau, fail=hw) search_space += binomial(tau, hw) * (b-a)**hw if enm_cost.repeat(ssf(search_space))["rop"] > bkz_cost["rop"]: # we moved too far, so undo prob -= est.success_probability_drop(n, h, tau, fail=hw) search_space -= binomial(tau, hw) * (b-a)**hw hw -= 1 break hw += 1 enm_cost = enm_cost.repeat(ssf(search_space)) # we use the expectation of the target norm. This could be longer, or shorter, for any given instance. target_norm = sqrt(m * sd**2 + h * RR((n-tau)/n) * scale**2) # account for the success probability of Babai's algorithm prob*=babai_probability_wun16(r, target_norm) # create a cost string, as in the LWE Estimator, to store the attack parameters and costs ret = est.Cost() ret["rop"] = bkz_cost["rop"] + enm_cost["rop"] ret["pre"] = bkz_cost["rop"] ret["enum"] = enm_cost["rop"] ret["beta"] = beta ret["|S|"] = search_space ret["prob"] = prob ret["scale"] = scale ret["pp"] = hw ret["d"] = d ret["tau"] = tau # 5. Repeat whole experiment ~1/prob times ret = ret.repeat(est.amplify(0.99, prob), select={"rop": True, "pre": True, "enum": True, "beta": False, "d": False, "|S|": False, "scale": False, "prob": False, "pp": False, "tau": False}) return ret ## Optimize attack parameters def parameter_search(n, alpha, q, m, secret_distribution, mitm = True, reduction_cost_model=est.BKZ.sieve): """ :param n: LWE dimension `n > 0` :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/sqrt{2π}` :param q: modulus `0 < q` :param m: number of LWE samples `m > 0` :param secret_distribution: distribution of secret :param beta_search: tuple (β_min, β_max, granularity) for the search space of β, default is (60,301,20) :param tau: tuple (τ_min, τ_max, granularity) for the search space of τ, default is (0,501,20) :param mitm: simulate MITM approach (√ of search space) :param reduction_cost_model: BKZ reduction cost model EXAMPLE: parameter_search(mitm = False, reduction_cost_model = est.BKZ.sieve, **example_64()) rop: 2^69.5 pre: 2^68.9 enum: 2^68.0 beta: 110 |S|: 2^40.9 prob: 0.045060 scale: 12.760 pp: 6 d: 1730 repeat: 100 tau: 170 parameter_search(mitm = True, reduction_cost_model = est.BKZ.sieve, **example_64()) rop: 2^63.4 pre: 2^63.0 enum: 2^61.5 beta: 95 |S|: 2^72.0 prob: 0.125126 scale: 12.760 pp: 11 d: 1666 repeat: 35 tau: 234 """ primald = est.partial(est.drop_and_solve, est.dual_scale, postprocess=True, decision=True) bl = primald(n, alpha, q, secret_distribution=secret_distribution, m=m, reduction_cost_model=reduction_cost_model) # we take the number of LWE samples used to be the same as in the primal attack in the LWE Estimator m = bl["m"] f = est.partial(hybrid_decoding_attack, n=n, alpha=alpha, q=q, m=m, secret_distribution=secret_distribution, reduction_cost_model=reduction_cost_model, mitm=mitm) # NOTE: we decribe our searching strategy below. To produce more accurate estimates, # change this part of the code to ensure a more granular search. As we are using # homomorphic-encryption style parameters, the running time of the code can be quite high, # justifying the below choices. # We start at beta = 60 and go up to beta_max in steps of 50 beta_max = bl["beta"] + 100 beta_search = (40, beta_max, 50) best = None for beta in range(beta_search[0], beta_search[1], beta_search[2])[::-1]: tau = 0 best_beta = None count = 3 while tau < n: if count >= 0: cost = f(beta=beta, tau=tau) if best_beta is not None: # if two consecutive estimates don't decrease, stop optimising over tau if best_beta["rop"] < cost["rop"]: count -= 1 cost["tau"] = tau if best_beta is None: best_beta = cost if RR(log(cost["rop"],2)) < RR(log(best_beta["rop"],2)): best_beta = cost if best is None: best = cost if RR(log(cost["rop"],2)) < RR(log(best["rop"],2)): best = cost tau += n//100 # now do a second, more granular search # we start at the beta which produced the lowest running time, and search ± 25 in steps of 10 tau_gap = max(n//100, 1) for beta in range(best["beta"] - 25, best["beta"] + 25, 10)[::-1]: tau = max(best["tau"] - 25,0) best_beta = None count = 3 while tau <= best["tau"] + 25: if count >= 0: cost = f(beta=beta, tau=tau) if best_beta is not None: # if two consecutive estimates don't decrease, stop optimising over tau if best_beta["rop"] < cost["rop"]: count -= 1 cost["tau"] = tau if best_beta is None: best_beta = cost if RR(log(cost["rop"],2)) < RR(log(best_beta["rop"],2)): best_beta = cost if best is None: best = cost if RR(log(cost["rop"],2)) < RR(log(best["rop"],2)): best = cost tau += tau_gap return best def get_all_security_levels(params): """ A function which gets the security levels of a collection of TFHE parameters, using the four cost models: classical, quantum, classical_conservative, and quantum_conservative :param params: a dictionary of LWE parameter sets (see concrete_params) EXAMPLE: sage: X = get_all_security_levels(concrete_LWE_params) sage: X [['LWE128_256', 126.692189756144, 117.566189756144, 98.6960000000000, 89.5700000000000], ...] """ RESULTS = [] for param in params: results = [param] x = params["{}".format(param)] n = x["n"] * x["k"] q = 2 ** 32 sd = 2 ** (x["sd"]) * q alpha = sqrt(2 * pi) * sd / RR(q) secret_distribution = (0, 1) # assume access to an infinite number of papers m = oo model = est.BKZ.sieve estimate = parameter_search(mitm = True, reduction_cost_model = est.BKZ.sieve, n = n, q = q, alpha = alpha, m = m, secret_distribution = secret_distribution) results.append(get_security_level(estimate)) RESULTS.append(results) return RESULTS