mirror of
https://github.com/zama-ai/concrete.git
synced 2026-02-15 15:15:06 -05:00
3465 lines
108 KiB
Python
3465 lines
108 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
Cost estimates for solving LWE.
|
||
|
||
.. moduleauthor:: Martin R. Albrecht <martinralbrecht@googlemail.com>
|
||
|
||
# Supported Secret Distributions #
|
||
|
||
The following distributions for the secret are supported:
|
||
|
||
- ``"normal"`` : normal form instances, i.e. the secret follows the noise distribution (alias: ``True``)
|
||
- ``alpha``: where α a positive real number, (discrete) Gaussian distribution with parameter α
|
||
- ``"uniform"`` : uniform mod q (alias: ``False``)
|
||
- ``(a,b)`` : uniform in the interval ``[a,…,b]``
|
||
- ``((a,b), h)`` : exactly ``h`` components are ``∈ [a,…,b]∖\\{0\\}``, all other components are zero
|
||
|
||
"""
|
||
|
||
|
||
# Imports
|
||
from collections import OrderedDict
|
||
from functools import partial
|
||
from sage.arith.srange import srange
|
||
from sage.calculus.var import var
|
||
from sage.functions.log import exp, log
|
||
from sage.functions.other import ceil, sqrt, floor, binomial
|
||
from sage.all import erf
|
||
from sage.interfaces.magma import magma
|
||
from sage.misc.all import cached_function, round, prod
|
||
from sage.numerical.optimize import find_root
|
||
from sage.rings.all import QQ, RR, ZZ, RealField, PowerSeriesRing, RDF
|
||
from sage.rings.infinity import PlusInfinity
|
||
from sage.structure.element import parent
|
||
from sage.symbolic.all import pi, e
|
||
from scipy.optimize import newton
|
||
import logging
|
||
import sage.crypto.lwe
|
||
|
||
oo = PlusInfinity()
|
||
|
||
|
||
class Logging:
|
||
"""
|
||
Control level of detail being printed.
|
||
"""
|
||
|
||
plain_logger = logging.StreamHandler()
|
||
plain_logger.setFormatter(logging.Formatter("%(message)s"))
|
||
|
||
detail_logger = logging.StreamHandler()
|
||
detail_logger.setFormatter(logging.Formatter("%(levelname)s:%(name)s: %(message)s"))
|
||
|
||
logging.getLogger("estimator").handlers = [plain_logger]
|
||
logging.getLogger("estimator").setLevel(logging.INFO)
|
||
|
||
loggers = ("binsearch", "repeat", "guess")
|
||
|
||
for logger in loggers:
|
||
logging.getLogger(logger).handlers = [detail_logger]
|
||
logging.getLogger(logger).setLevel(logging.INFO)
|
||
|
||
CRITICAL = logging.CRITICAL
|
||
ERROR = logging.ERROR
|
||
WARNING = logging.WARNING
|
||
INFO = logging.INFO
|
||
DEBUG = logging.DEBUG
|
||
NOTSET = logging.NOTSET
|
||
|
||
@staticmethod
|
||
def set_level(lvl, loggers=None):
|
||
"""Set logging level
|
||
|
||
:param lvl: one of `CRITICAL`, `ERROR`, `WARNING`, `INFO`, `DEBUG`, `NOTSET`
|
||
:param loggers: one of `Logging.loggers`, if `None` all loggers are used.
|
||
|
||
"""
|
||
if loggers is None:
|
||
loggers = Logging.loggers
|
||
|
||
for logger in loggers:
|
||
logging.getLogger(logger).setLevel(lvl)
|
||
|
||
|
||
# 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.
|
||
"""
|
||
|
||
pass
|
||
|
||
|
||
# Binary Search #
|
||
|
||
|
||
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, predictate=lambda x, best: extract(x) <= extract(best), *arg, **kwds)
|
||
|
||
|
||
def binary_search(f, start, stop, param, predicate=lambda x, best: x <= best, *arg, **kwds):
|
||
"""
|
||
Searches for the best value in the interval [start,stop] depending on the given predicate.
|
||
|
||
: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 predicate: comparison is performed by evaluating ``predicate(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 not in D:
|
||
kwds[param] = b
|
||
D[b] = f(*arg, **kwds)
|
||
if b == start:
|
||
best = D[start]
|
||
break
|
||
if not predicate(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]
|
||
logging.getLogger("binsearch").debug(u"%4d, %s" % (b, best))
|
||
if b - 1 not in D:
|
||
kwds[param] = b - 1
|
||
D[b - 1] = f(*arg, **kwds)
|
||
if predicate(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 predicate(D[b + 1], best):
|
||
break
|
||
else:
|
||
start = b
|
||
b = ceil((stop + b) / 2)
|
||
direction = 1
|
||
return best
|
||
|
||
|
||
class Param:
|
||
"""
|
||
Namespace for processing LWE parameter sets.
|
||
"""
|
||
|
||
@staticmethod
|
||
def Regev(n, m=None, dict=False):
|
||
"""
|
||
:param n: LWE dimension `n > 0`
|
||
:param m: number of LWE samples `m > 0`
|
||
:param dict: return a dictionary
|
||
|
||
"""
|
||
if dict:
|
||
return Param.dict(sage.crypto.lwe.Regev(n=n, m=m))
|
||
else:
|
||
return Param.tuple(sage.crypto.lwe.Regev(n=n, m=m))
|
||
|
||
@staticmethod
|
||
def LindnerPeikert(n, m=None, dict=False):
|
||
"""
|
||
:param n: LWE dimension `n > 0`
|
||
:param m: number of LWE samples `m > 0`
|
||
:param dict: return a dictionary
|
||
|
||
"""
|
||
if dict:
|
||
return Param.dict(sage.crypto.lwe.LindnerPeikert(n=n, m=m))
|
||
else:
|
||
return Param.tuple(sage.crypto.lwe.LindnerPeikert(n=n, m=m))
|
||
|
||
@staticmethod
|
||
def tuple(lwe):
|
||
"""
|
||
Return (n, α, q) given an LWE instance object.
|
||
|
||
:param lwe: LWE object
|
||
:returns: (n, α, q)
|
||
|
||
"""
|
||
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
|
||
|
||
@staticmethod
|
||
def 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 = Param.tuple(lwe)
|
||
m = lwe.m if lwe.m else oo
|
||
return {"n": n, "alpha": alpha, "q": q, "m": m}
|
||
|
||
@staticmethod
|
||
def preprocess(n, alpha, q, success_probability=None, prec=None, m=oo):
|
||
"""
|
||
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 m is not None and m < 1:
|
||
raise InsufficientSamplesError(u"m=%d < 1" % m)
|
||
|
||
if prec is None:
|
||
try:
|
||
prec = alpha.prec()
|
||
except AttributeError:
|
||
pass
|
||
try:
|
||
prec = max(success_probability.prec(), prec if prec else 0)
|
||
except AttributeError:
|
||
pass
|
||
|
||
if prec is None or prec < 128:
|
||
prec = 128
|
||
RR = RealField(prec)
|
||
n, alpha, q = ZZ(n), RR(alpha), ZZ(q)
|
||
|
||
if m is not oo:
|
||
m = ZZ(m)
|
||
|
||
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
|
||
|
||
|
||
# Error Parameter Conversions
|
||
|
||
|
||
def stddevf(sigma):
|
||
"""
|
||
Gaussian width parameter σ → standard deviation
|
||
|
||
:param sigma: Gaussian width parameter σ
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import stddevf
|
||
sage: stddevf(64.0)
|
||
25.532...
|
||
|
||
sage: stddevf(64)
|
||
25.532...
|
||
|
||
sage: stddevf(RealField(256)(64)).prec()
|
||
256
|
||
|
||
"""
|
||
|
||
try:
|
||
prec = parent(sigma).prec()
|
||
except AttributeError:
|
||
prec = 0
|
||
if prec > 0:
|
||
FF = parent(sigma)
|
||
else:
|
||
FF = RR
|
||
return FF(sigma) / FF(sqrt(2 * pi))
|
||
|
||
|
||
def sigmaf(stddev):
|
||
"""
|
||
standard deviation → Gaussian width parameter σ
|
||
|
||
:param sigma: standard deviation
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import stddevf, sigmaf
|
||
sage: n = 64.0
|
||
sage: sigmaf(stddevf(n))
|
||
64.000...
|
||
|
||
sage: sigmaf(RealField(128)(1.0))
|
||
2.5066282746310005024157652848110452530
|
||
sage: sigmaf(1.0)
|
||
2.50662827463100
|
||
sage: sigmaf(1)
|
||
2.50662827463100
|
||
sage: sigmaf(1r)
|
||
2.50662827463100
|
||
|
||
"""
|
||
RR = parent(stddev)
|
||
# check that we got ourselves a real number type
|
||
try:
|
||
if (abs(RR(0.5) - 0.5) > 0.001):
|
||
RR = RealField(53) # hardcode something
|
||
except TypeError:
|
||
RR = RealField(53) # hardcode something
|
||
return RR(sqrt(2 * pi)) * stddev
|
||
|
||
|
||
def alphaf(sigma, q, sigma_is_stddev=False):
|
||
"""
|
||
Gaussian width σ, modulus q → noise rate α
|
||
|
||
:param sigma: Gaussian width parameter (or standard deviation if ``sigma_is_stddev`` is set)
|
||
:param q: modulus `0 < q`
|
||
:param sigma_is_stddev: if set then `sigma` is interpreted as the standard deviation
|
||
|
||
:returns: α = σ/q or σ·sqrt(2π)/q depending on `sigma_is_stddev`
|
||
|
||
"""
|
||
if sigma_is_stddev is False:
|
||
return RR(sigma / q)
|
||
else:
|
||
return RR(sigmaf(sigma) / q)
|
||
|
||
|
||
class Cost:
|
||
"""
|
||
Algorithms costs.
|
||
"""
|
||
|
||
def __init__(self, data=None, **kwds):
|
||
"""
|
||
|
||
:param data: we call ``OrderedDict(data)``
|
||
|
||
"""
|
||
if data is None:
|
||
self.data = OrderedDict()
|
||
else:
|
||
self.data = OrderedDict(data)
|
||
|
||
for k, v in kwds.items():
|
||
self.data[k] = v
|
||
|
||
def str(self, keyword_width=None, newline=None, round_bound=2048, compact=False, unicode=True):
|
||
"""
|
||
|
||
:param keyword_width: keys are printed with this width
|
||
:param newline: insert a newline
|
||
:param round_bound: values beyond this bound are represented as powers of two
|
||
:param compact: do not add extra whitespace to align entries
|
||
:param unicode: use unicode to shorten representation
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import Cost
|
||
sage: s = Cost({"delta_0":5, "bar":2})
|
||
sage: print(s)
|
||
delta_0: 5, bar: 2
|
||
|
||
sage: s = Cost([(u"delta_0", 5), ("bar",2)])
|
||
sage: print(s)
|
||
delta_0: 5, bar: 2
|
||
|
||
"""
|
||
if unicode:
|
||
unicode_replacements = {"delta_0": u"δ_0", "beta": u"β", "epsilon": u"ε"}
|
||
else:
|
||
unicode_replacements = {}
|
||
|
||
format_strings = {
|
||
u"beta": u"%s: %4d",
|
||
u"d": u"%s: %4d",
|
||
"b": "%s: %3d",
|
||
"t1": "%s: %3d",
|
||
"t2": "%s: %3d",
|
||
"l": "%s: %3d",
|
||
"ncod": "%s: %3d",
|
||
"ntop": "%s: %3d",
|
||
"ntest": "%s: %3d",
|
||
}
|
||
|
||
d = self.data
|
||
s = []
|
||
for k in d:
|
||
v = d[k]
|
||
kk = unicode_replacements.get(k, k)
|
||
if keyword_width:
|
||
fmt = u"%%%ds" % keyword_width
|
||
kk = fmt % kk
|
||
if not newline and k in format_strings:
|
||
s.append(format_strings[k] % (kk, v))
|
||
elif ZZ(1) / round_bound < v < round_bound or v == 0 or ZZ(-1) / round_bound > v > -round_bound:
|
||
try:
|
||
if compact:
|
||
s.append(u"%s: %d" % (kk, ZZ(v)))
|
||
else:
|
||
s.append(u"%s: %8d" % (kk, ZZ(v)))
|
||
except TypeError:
|
||
if v < 2.0 and v >= 0.0:
|
||
if compact:
|
||
s.append(u"%s: %.6f" % (kk, v))
|
||
else:
|
||
s.append(u"%s: %8.6f" % (kk, v))
|
||
else:
|
||
if compact:
|
||
s.append(u"%s: %.3f" % (kk, v))
|
||
else:
|
||
s.append(u"%s: %8.3f" % (kk, v))
|
||
else:
|
||
t = u"%s" % (u"≈" if unicode else "") + u"%s2^%.1f" % ("-" if v < 0 else "", log(abs(v), 2).n())
|
||
if compact:
|
||
s.append(u"%s: %s" % (kk, t))
|
||
else:
|
||
s.append(u"%s: %8s" % (kk, t))
|
||
if not newline:
|
||
if compact:
|
||
return u", ".join(s)
|
||
else:
|
||
return u", ".join(s)
|
||
else:
|
||
return u"\n".join(s)
|
||
|
||
def reorder(self, first):
|
||
"""
|
||
Return a new ordered dict from the key:value pairs in dictinonary but reordered such that the
|
||
``first`` keys come first.
|
||
|
||
:param dictionary: input dictionary
|
||
:param first: keys which should come first (in order)
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import Cost
|
||
sage: d = Cost([("a",1),("b",2),("c",3)]); d
|
||
a: 1
|
||
b: 2
|
||
c: 3
|
||
|
||
sage: d.reorder( ["b","c","a"])
|
||
b: 2
|
||
c: 3
|
||
a: 1
|
||
"""
|
||
keys = list(self.data)
|
||
for key in first:
|
||
keys.pop(keys.index(key))
|
||
keys = list(first) + keys
|
||
r = OrderedDict()
|
||
for key in keys:
|
||
r[key] = self.data[key]
|
||
return Cost(r)
|
||
|
||
def filter(self, keys):
|
||
"""
|
||
Return new ordered dictinonary from dictionary restricted to the keys.
|
||
|
||
:param dictionary: input dictionary
|
||
:param keys: keys which should be copied (ordered)
|
||
"""
|
||
r = OrderedDict()
|
||
for key in keys:
|
||
r[key] = self.data[key]
|
||
return Cost(r)
|
||
|
||
def repeat(self, times, select=None, lll=None):
|
||
u"""
|
||
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 select: toggle which fields ought to be repeated and which shouldn't
|
||
:param lll: if set amplify lattice reduction times assuming the LLL algorithm suffices and costs ``lll``
|
||
: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: from estimator import Param, dual
|
||
sage: n, alpha, q = Param.Regev(128)
|
||
|
||
sage: dual(n, alpha, q).repeat(2^10)
|
||
rop: 2^91.1
|
||
m: 2^18.6
|
||
red: 2^91.1
|
||
delta_0: 1.008631
|
||
beta: 115
|
||
d: 380
|
||
|v|: 688.951
|
||
repeat: 2^27.0
|
||
epsilon: 0.007812
|
||
|
||
sage: dual(n, alpha, q).repeat(1)
|
||
rop: 2^81.1
|
||
m: 380
|
||
red: 2^81.1
|
||
delta_0: 1.008631
|
||
beta: 115
|
||
d: 380
|
||
|v|: 688.951
|
||
repeat: 2^17.0
|
||
epsilon: 0.007812
|
||
|
||
"""
|
||
# TODO review this list
|
||
do_repeat = {
|
||
u"rop": True,
|
||
u"red": True,
|
||
u"babai": True,
|
||
u"babai_op": True,
|
||
u"epsilon": False,
|
||
u"mem": False,
|
||
u"delta_0": False,
|
||
u"beta": False,
|
||
u"k": False,
|
||
u"D_reg": False,
|
||
u"t": False,
|
||
u"m": True,
|
||
u"d": False,
|
||
u"|v|": False,
|
||
u"amplify": False,
|
||
u"repeat": False, # we deal with it below
|
||
u"c": False,
|
||
u"b": False,
|
||
u"t1": False,
|
||
u"t2": False,
|
||
u"l": False,
|
||
u"ncod": False,
|
||
u"ntop": False,
|
||
u"ntest": False,
|
||
}
|
||
|
||
if lll and self["red"] != self["rop"]:
|
||
raise ValueError("Amplification via LLL was requested but 'red' != 'rop'")
|
||
|
||
if select is not None:
|
||
for key in select:
|
||
do_repeat[key] = select[key]
|
||
|
||
ret = OrderedDict()
|
||
for key in self.data:
|
||
try:
|
||
if do_repeat[key]:
|
||
if lll and key in ("red", "rop"):
|
||
ret[key] = self[key] + times * lll
|
||
else:
|
||
ret[key] = times * self[key]
|
||
else:
|
||
ret[key] = self.data[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 Cost(ret)
|
||
|
||
def __rmul__(self, times):
|
||
return self.repeat(times)
|
||
|
||
def combine(self, 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 = Cost()
|
||
else:
|
||
cost = base
|
||
for key in self.data:
|
||
cost[key] = self.data[key]
|
||
for key in right:
|
||
cost[key] = right.data[key]
|
||
return Cost(cost)
|
||
|
||
def __add__(self, other):
|
||
return self.combine(self, other)
|
||
|
||
def __getitem__(self, key):
|
||
return self.data[key]
|
||
|
||
def __setitem__(self, key, value):
|
||
self.data[key] = value
|
||
|
||
def __iter__(self):
|
||
return iter(self.data)
|
||
|
||
def values(self):
|
||
return self.data.values()
|
||
|
||
def __str__(self):
|
||
return self.str(unicode=False, compact=True)
|
||
|
||
def __repr__(self):
|
||
return self.str(unicode=False, newline=True, keyword_width=12)
|
||
|
||
def __unicode__(self):
|
||
return self.str(unicode=True)
|
||
|
||
|
||
class SDis:
|
||
"""
|
||
Distributions of Secrets.
|
||
"""
|
||
|
||
@staticmethod
|
||
def is_sparse(secret_distribution):
|
||
"""Return true if the secret distribution is sparse
|
||
|
||
:param secret_distribution: distribution of secret, see module level documentation for details
|
||
|
||
EXAMPLES::
|
||
|
||
sage: from estimator import SDis
|
||
sage: SDis.is_sparse(True)
|
||
False
|
||
|
||
sage: SDis.is_sparse(0.1)
|
||
False
|
||
|
||
sage: SDis.is_sparse(0.2)
|
||
False
|
||
|
||
sage: SDis.is_sparse(((-1, 1), 64))
|
||
True
|
||
|
||
sage: SDis.is_sparse(((-3, 3), 64))
|
||
True
|
||
|
||
sage: SDis.is_sparse((-3, 3))
|
||
False
|
||
|
||
"""
|
||
try:
|
||
(a, b), h = secret_distribution
|
||
# TODO we could check h against n but then this function would have to depend on n
|
||
return True
|
||
except (TypeError, ValueError):
|
||
return False
|
||
|
||
@staticmethod
|
||
def is_small(secret_distribution, alpha=None):
|
||
"""Return true if the secret distribution is small
|
||
|
||
:param secret_distribution: distribution of secret, see module level documentation for details
|
||
:param alpha: optional, used for comparing with `secret_distribution`
|
||
|
||
EXAMPLES::
|
||
|
||
sage: from estimator import SDis
|
||
sage: SDis.is_small(False)
|
||
False
|
||
|
||
sage: SDis.is_small(True)
|
||
True
|
||
|
||
sage: SDis.is_small("normal")
|
||
True
|
||
|
||
sage: SDis.is_small(1)
|
||
True
|
||
|
||
sage: SDis.is_small(1.2)
|
||
True
|
||
|
||
sage: SDis.is_small(((-1, 1), 64))
|
||
True
|
||
|
||
sage: SDis.is_small(((-3, 3), 64))
|
||
True
|
||
|
||
sage: SDis.is_small((-3, 3))
|
||
True
|
||
|
||
sage: SDis.is_small((0, 1))
|
||
True
|
||
|
||
"""
|
||
|
||
if secret_distribution == "normal" or secret_distribution is True:
|
||
return True
|
||
try:
|
||
if float(secret_distribution) > 0:
|
||
if alpha is not None:
|
||
if secret_distribution <= alpha:
|
||
return True
|
||
else:
|
||
raise NotImplementedError("secret size %f > error size %f"%(secret_distribution, alpha))
|
||
return True
|
||
except TypeError:
|
||
pass
|
||
try:
|
||
(a, b), h = secret_distribution
|
||
return True
|
||
except (TypeError, ValueError):
|
||
try:
|
||
(a, b) = secret_distribution
|
||
return True
|
||
except (TypeError, ValueError):
|
||
return False
|
||
|
||
@staticmethod
|
||
def bounds(secret_distribution):
|
||
"""Return bounds of secret distribution
|
||
|
||
:param secret_distribution: distribution of secret, see module level documentation for details
|
||
|
||
EXAMPLES::
|
||
|
||
sage: from estimator import SDis
|
||
sage: SDis.bounds(False)
|
||
Traceback (most recent call last):
|
||
...
|
||
ValueError: Cannot extract bounds for secret.
|
||
|
||
sage: SDis.bounds(True)
|
||
Traceback (most recent call last):
|
||
...
|
||
ValueError: Cannot extract bounds for secret.
|
||
|
||
sage: SDis.bounds(0.1)
|
||
Traceback (most recent call last):
|
||
...
|
||
ValueError: Cannot extract bounds for secret.
|
||
|
||
sage: SDis.bounds(0.12)
|
||
Traceback (most recent call last):
|
||
...
|
||
ValueError: Cannot extract bounds for secret.
|
||
|
||
sage: SDis.bounds(((-1, 1), 64))
|
||
(-1, 1)
|
||
|
||
sage: SDis.bounds(((-3, 3), 64))
|
||
(-3, 3)
|
||
|
||
sage: SDis.bounds((-3, 3))
|
||
(-3, 3)
|
||
|
||
"""
|
||
try:
|
||
(a, b) = secret_distribution
|
||
try:
|
||
(a, b), _ = (a, b) # noqa
|
||
except (TypeError, ValueError):
|
||
pass
|
||
return a, b
|
||
except (TypeError, ValueError):
|
||
raise ValueError("Cannot extract bounds for secret.")
|
||
|
||
@staticmethod
|
||
def is_bounded_uniform(secret_distribution):
|
||
"""Return true if the secret is bounded uniform (sparse or not).
|
||
|
||
:param secret_distribution: distribution of secret, see module level documentation for
|
||
details
|
||
|
||
EXAMPLES::
|
||
|
||
sage: from estimator import SDis
|
||
sage: SDis.is_bounded_uniform(False)
|
||
False
|
||
|
||
sage: SDis.is_bounded_uniform(True)
|
||
False
|
||
|
||
sage: SDis.is_bounded_uniform(0.1)
|
||
False
|
||
|
||
sage: SDis.is_bounded_uniform(0.12)
|
||
False
|
||
|
||
sage: SDis.is_bounded_uniform(((-1, 1), 64))
|
||
True
|
||
|
||
sage: SDis.is_bounded_uniform(((-3, 3), 64))
|
||
True
|
||
|
||
sage: SDis.is_bounded_uniform((-3, 3))
|
||
True
|
||
|
||
.. note :: This function requires the bounds to be of opposite sign, as scaling code does
|
||
not handle the other case.
|
||
|
||
"""
|
||
|
||
try:
|
||
# next will fail if not bounded_uniform
|
||
a, b = SDis.bounds(secret_distribution)
|
||
|
||
# check bounds are around 0, otherwise not implemented
|
||
if a <= 0 and 0 <= b:
|
||
return True
|
||
except (TypeError, ValueError):
|
||
pass
|
||
|
||
return False
|
||
|
||
@staticmethod
|
||
def is_ternary(secret_distribution):
|
||
"""Return true if the secret is ternary (sparse or not)
|
||
|
||
:param secret_distribution: distribution of secret, see module level documentation for details
|
||
|
||
EXAMPLES::
|
||
|
||
sage: from estimator import SDis
|
||
sage: SDis.is_ternary(False)
|
||
False
|
||
|
||
sage: SDis.is_ternary(True)
|
||
False
|
||
|
||
sage: SDis.is_ternary(0.1)
|
||
False
|
||
|
||
sage: SDis.is_ternary(0.12)
|
||
False
|
||
|
||
sage: SDis.is_ternary(((-1, 1), 64))
|
||
True
|
||
|
||
sage: SDis.is_ternary((-1, 1))
|
||
True
|
||
|
||
sage: SDis.is_ternary(((-3, 3), 64))
|
||
False
|
||
|
||
sage: SDis.is_ternary((-3, 3))
|
||
False
|
||
|
||
"""
|
||
|
||
if SDis.is_bounded_uniform(secret_distribution):
|
||
a, b = SDis.bounds(secret_distribution)
|
||
if a == -1 and b == 1:
|
||
return True
|
||
|
||
return False
|
||
|
||
@staticmethod
|
||
def is_binary(secret_distribution):
|
||
"""Return true if the secret is binary (sparse or not)
|
||
|
||
:param secret_distribution: distribution of secret, see module level documentation for details
|
||
|
||
EXAMPLES::
|
||
|
||
sage: from estimator import SDis
|
||
sage: SDis.is_binary(False)
|
||
False
|
||
|
||
sage: SDis.is_binary(True)
|
||
False
|
||
|
||
sage: SDis.is_binary(0.1)
|
||
False
|
||
|
||
sage: SDis.is_binary(0.12)
|
||
False
|
||
|
||
sage: SDis.is_binary(((-1, 1), 64))
|
||
False
|
||
|
||
sage: SDis.is_binary((-1, 1))
|
||
False
|
||
|
||
sage: SDis.is_binary(((0, 1), 64))
|
||
True
|
||
|
||
sage: SDis.is_binary((0, 1))
|
||
True
|
||
|
||
"""
|
||
|
||
if SDis.is_bounded_uniform(secret_distribution):
|
||
a, b = SDis.bounds(secret_distribution)
|
||
if a == 0 and b == 1:
|
||
return True
|
||
|
||
return False
|
||
|
||
@staticmethod
|
||
def nonzero(secret_distribution, n):
|
||
"""Return number of non-zero elements or ``None``
|
||
|
||
:param secret_distribution: distribution of secret, see module level documentation for details
|
||
:param n: LWE dimension `n > 0`
|
||
|
||
"""
|
||
try:
|
||
(a, b) = secret_distribution
|
||
try:
|
||
(a, b), h = (a, b) # noqa
|
||
return h
|
||
except (TypeError, ValueError):
|
||
if n is None:
|
||
raise ValueError("Parameter n is required for sparse secrets.")
|
||
B = ZZ(b - a + 1)
|
||
h = round((B - 1) / B * n)
|
||
return h
|
||
except (TypeError, ValueError):
|
||
raise ValueError("Cannot extract `h`.")
|
||
|
||
@staticmethod
|
||
def mean(secret_distribution, q=None, n=None):
|
||
"""
|
||
Mean of the secret per component.
|
||
|
||
:param secret_distribution: distribution of secret, see module level documentation for details
|
||
:param n: only used for sparse secrets
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import SDis
|
||
sage: SDis.mean(True)
|
||
0
|
||
|
||
sage: SDis.mean(False, q=10)
|
||
0
|
||
|
||
sage: SDis.mean(0.1)
|
||
0
|
||
|
||
sage: SDis.mean(0.12)
|
||
0
|
||
|
||
sage: SDis.mean(((-3,3)))
|
||
0
|
||
|
||
sage: SDis.mean(((-3,3),64), n=256)
|
||
0
|
||
|
||
sage: SDis.mean(((-3,2)))
|
||
-1/2
|
||
|
||
sage: SDis.mean(((-3,2),64), n=256)
|
||
-3/20
|
||
|
||
"""
|
||
if not SDis.is_small(secret_distribution):
|
||
# uniform distribution variance
|
||
if q is None:
|
||
raise ValueError("Parameter q is required for uniform secret.")
|
||
a = -floor(q / 2)
|
||
b = floor(q / 2)
|
||
return (a + b) / ZZ(2)
|
||
else:
|
||
try:
|
||
a, b = SDis.bounds(secret_distribution)
|
||
|
||
try:
|
||
(a, b), h = secret_distribution
|
||
if n is None:
|
||
raise ValueError("Parameter n is required for sparse secrets.")
|
||
return h / ZZ(n) * (b * (b + 1) - a * (a - 1)) / (2 * (b - a))
|
||
except (TypeError, ValueError):
|
||
return (a + b) / ZZ(2)
|
||
except ValueError:
|
||
# small with no bounds, it's normal
|
||
return ZZ(0)
|
||
|
||
@staticmethod
|
||
def variance(secret_distribution, alpha=None, q=None, n=None):
|
||
"""
|
||
Variance of the secret per component.
|
||
|
||
:param secret_distribution: distribution of secret, see module level documentation for details
|
||
:param alpha: only used for normal form LWE
|
||
:param q: only used for normal form LWE
|
||
:param n: only used for sparse secrets
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import SDis, alphaf
|
||
sage: SDis.variance(True, 8./2^15, 2^15).sqrt().n()
|
||
3.19...
|
||
|
||
sage: from estimator import SDis
|
||
sage: SDis.variance("normal", 8./2^15, 2^15).sqrt().n()
|
||
3.19...
|
||
|
||
sage: SDis.variance(alphaf(1.0, 100, True), q=100)
|
||
1.00000000000000
|
||
|
||
sage: SDis.variance(alphaf(1.2, 100, True), q=100)
|
||
1.44000000000000
|
||
|
||
sage: SDis.variance((-3,3), 8./2^15, 2^15)
|
||
4
|
||
|
||
sage: SDis.variance(((-3,3),64), 8./2^15, 2^15, n=256)
|
||
7/6
|
||
|
||
sage: SDis.variance((-3,2))
|
||
35/12
|
||
|
||
sage: SDis.variance(((-3,2),64), n=256)
|
||
371/400
|
||
|
||
sage: SDis.variance((-1,1), 8./2^15, 2^15)
|
||
2/3
|
||
|
||
sage: SDis.variance(((-1,1),64), 8./2^15, 2^15, n=256)
|
||
1/4
|
||
|
||
.. note :: This function assumes that the bounds are of opposite sign, and that the
|
||
distribution is centred around zero.
|
||
"""
|
||
if not SDis.is_small(secret_distribution):
|
||
# uniform distribution variance
|
||
a = -floor(q / 2)
|
||
b = floor(q / 2)
|
||
n = b - a + 1
|
||
return (n ** 2 - 1) / ZZ(12)
|
||
else:
|
||
try:
|
||
a, b = SDis.bounds(secret_distribution)
|
||
except ValueError:
|
||
# small with no bounds, it's normal
|
||
if isinstance(secret_distribution, bool) or secret_distribution == "normal":
|
||
return stddevf(alpha * q) ** 2
|
||
else:
|
||
return RR(stddevf(secret_distribution * q))**2
|
||
try:
|
||
(a, b), h = secret_distribution
|
||
except (TypeError, ValueError):
|
||
return ((b - a + 1) ** 2 - 1) / ZZ(12)
|
||
if n is None:
|
||
raise ValueError("Parameter n is required for sparse secrets.")
|
||
if not (a <= 0 and 0 <= b):
|
||
raise ValueError("a <= 0 and 0 <= b is required for uniform bounded secrets.")
|
||
# E(x^2), using https://en.wikipedia.org/wiki/Square_pyramidal_number
|
||
tt = (h / ZZ(n)) * (2 * b ** 3 + 3 * b ** 2 + b - 2 * a ** 3 + 3 * a ** 2 - a) / (ZZ(6) * (b - a))
|
||
# Var(x) = E(x^2) - E(x)^2
|
||
return tt - SDis.mean(secret_distribution, n=n) ** 2
|
||
|
||
|
||
def switch_modulus(f, n, alpha, q, secret_distribution, *args, **kwds):
|
||
"""
|
||
|
||
:param f: run f
|
||
: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 secret_distribution: distribution of secret, see module level documentation for details
|
||
|
||
|
||
"""
|
||
length = SDis.nonzero(secret_distribution, n)
|
||
s_var = SDis.variance(secret_distribution, alpha, q, n=n)
|
||
|
||
p = RR(ceil(sqrt(2 * pi * s_var * 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_var * length * 2 / ZZ(12)) / alpha))
|
||
beta = RR(sqrt(2) * alpha)
|
||
return f(n, beta, p, secret_distribution, *args, **kwds)
|
||
|
||
|
||
# Repetition
|
||
|
||
|
||
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: targeted success probability < 1
|
||
:param success_probability: targeted success probability < 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
|
||
"""
|
||
if target_success_probability < success_probability:
|
||
return ZZ(1)
|
||
if success_probability == 0.0:
|
||
return oo
|
||
|
||
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))),
|
||
)
|
||
prec = min(prec, 2048)
|
||
RR = RealField(prec)
|
||
|
||
success_probability = RR(success_probability)
|
||
target_success_probability = RR(target_success_probability)
|
||
|
||
try:
|
||
if majority:
|
||
eps = success_probability / 2
|
||
return ceil(2 * log(2 - 2 * target_success_probability) / log(1 - 4 * eps ** 2))
|
||
else:
|
||
# target_success_probability = 1 - (1-success_probability)^trials
|
||
return ceil(log(1 - target_success_probability) / log(1 - success_probability))
|
||
except ValueError:
|
||
return oo
|
||
|
||
|
||
def amplify_sigma(target_advantage, sigma, q):
|
||
"""
|
||
Amplify distinguishing advantage for a given σ and q
|
||
|
||
:param target_advantage:
|
||
:param sigma: (lists of) Gaussian width parameters
|
||
:param q: modulus `0 < q`
|
||
|
||
"""
|
||
try:
|
||
sigma = sum(sigma_ ** 2 for sigma_ in sigma).sqrt()
|
||
except TypeError:
|
||
pass
|
||
RR = parent(sigma)
|
||
advantage = RR(exp(-RR(pi) * (RR(sigma / q) ** 2)))
|
||
# return target_advantage/advantage**2 # old
|
||
return amplify(target_advantage, advantage, majority=True)
|
||
|
||
|
||
def rinse_and_repeat(
|
||
f,
|
||
n,
|
||
alpha,
|
||
q,
|
||
success_probability=0.99,
|
||
m=oo,
|
||
optimisation_target=u"red",
|
||
decision=True,
|
||
repeat_select=None,
|
||
*args,
|
||
**kwds
|
||
):
|
||
"""Find best trade-off between success probability and running time.
|
||
|
||
:param f: a function returning a cost estimate
|
||
: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 success_probability: targeted success probability < 1
|
||
:param optimisation_target: which value to minimise
|
||
:param decision: set if `f` solves a decision problem, unset for search problems
|
||
:param repeat_select: passed through to ``cost_repeat`` as parameter ``select``
|
||
:param samples: the number of available samples
|
||
|
||
"""
|
||
n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability)
|
||
RR = parent(alpha)
|
||
|
||
best = None
|
||
step_size = 32
|
||
i = floor(-log(success_probability, 2))
|
||
has_solution = False
|
||
while True:
|
||
# RR() fixes the precision issue, but we still infinite loop via line 1218
|
||
prob = RR(min(2 ** RR(-i), success_probability))
|
||
try:
|
||
current = f(n, alpha, q, success_probability=prob, m=m, *args, **kwds)
|
||
repeat = amplify(success_probability, prob, majority=decision)
|
||
# TODO does the following hold for all algorithms?
|
||
current = current.repeat(repeat, select=repeat_select)
|
||
has_solution = True
|
||
except (OutOfBoundsError, InsufficientSamplesError):
|
||
key = list(best)[0] if best is not None else optimisation_target
|
||
current = Cost()
|
||
current[key] = PlusInfinity()
|
||
current["epsilon"] = ZZ(2) ** -i
|
||
|
||
logging.getLogger("repeat").debug(current)
|
||
|
||
key = list(current)[0]
|
||
if best is None:
|
||
if current[key] is not PlusInfinity():
|
||
best = current
|
||
i += step_size
|
||
|
||
if i > 8192: # somewhat arbitrary constant
|
||
raise RuntimeError("Looks like we are stuck in an infinite loop.")
|
||
|
||
continue
|
||
|
||
if key not in best or current[key] < best[key]:
|
||
best = current
|
||
i += step_size
|
||
else:
|
||
# we go back
|
||
i = -log(best["epsilon"], 2) - 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
|
||
|
||
|
||
class BKZ:
|
||
"""
|
||
Cost estimates for BKZ.
|
||
"""
|
||
|
||
@staticmethod
|
||
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)))
|
||
|
||
@staticmethod
|
||
def _beta_secant(delta):
|
||
"""
|
||
Estimate required blocksize `k` for a given root-hermite factor δ based on [PhD:Chen13]_
|
||
|
||
:param delta: root-hermite factor
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import BKZ
|
||
sage: 50 == BKZ._beta_secant(1.0121)
|
||
True
|
||
sage: 100 == BKZ._beta_secant(1.0093)
|
||
True
|
||
sage: BKZ._beta_secant(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(BKZ._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 beta method wouldn't do this. For
|
||
# consistency, call the old beta method, i.e. consider this try as "failed".
|
||
raise RuntimeError("k < 40")
|
||
return k
|
||
except (RuntimeError, TypeError):
|
||
# if something fails, use old beta method
|
||
k = BKZ._beta_simple(delta)
|
||
return k
|
||
|
||
@staticmethod
|
||
def _beta_find_root(delta):
|
||
"""
|
||
|
||
TESTS::
|
||
|
||
sage: from estimator import betaf, delta_0f
|
||
sage: betaf(delta_0f(500))
|
||
500
|
||
|
||
"""
|
||
# handle k < 40 separately
|
||
k = ZZ(40)
|
||
if delta_0f(k) < delta:
|
||
return k
|
||
|
||
try:
|
||
k = find_root(lambda k: RR(BKZ._delta_0f(k) - delta), 40, 2 ** 16, maxiter=500)
|
||
k = ceil(k - 1e-8)
|
||
except RuntimeError:
|
||
# finding root failed; reasons:
|
||
# 1. maxiter not sufficient
|
||
# 2. no root in given interval
|
||
k = BKZ._beta_simple(delta)
|
||
return k
|
||
|
||
@staticmethod
|
||
def _beta_simple(delta):
|
||
"""
|
||
Estimate required blocksize `k` for a given root-hermite factor δ based on [PhD:Chen13]_
|
||
|
||
:param delta: root-hermite factor
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import betaf
|
||
sage: 50 == betaf(1.0121)
|
||
True
|
||
sage: 100 == betaf(1.0093)
|
||
True
|
||
sage: betaf(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
|
||
|
||
@staticmethod
|
||
def svp_repeat(beta, d):
|
||
"""Return number of SVP calls in BKZ-β
|
||
|
||
:param beta: block size
|
||
:param d: dimension
|
||
|
||
.. note :: loosely based on experiments in [PhD:Chen13]
|
||
|
||
"""
|
||
return 8 * d
|
||
|
||
@staticmethod
|
||
@cached_function
|
||
def GSA(n, q, delta, m):
|
||
"""
|
||
Compute the Gram-Schmidt lengths based on the GSA.
|
||
|
||
:param n: LWE dimension `n > 0`
|
||
:param q: modulus `0 < q`
|
||
:param delta: root-Hermite factor
|
||
:param m: lattice dimension
|
||
|
||
.. [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 range(m)]
|
||
b = [log_q - b[-1 - i] for i in range(m)]
|
||
b = list(map(lambda x: x.exp(), b))
|
||
return b
|
||
|
||
# BKZ Estimates
|
||
|
||
@staticmethod
|
||
def LLL(d, B=None):
|
||
"""
|
||
Runtime estimation for LLL algorithm
|
||
|
||
:param d: lattice dimension
|
||
:param B: bit-size of entries
|
||
|
||
.. [CheNgu11] Chen, Y., & Nguyen, P. Q. (2011). BKZ 2.0: better lattice security
|
||
estimates. In D. H. Lee, & X. Wang, ASIACRYPT~2011 (pp. 1–20). : Springer,
|
||
Heidelberg.
|
||
"""
|
||
if B:
|
||
return d ** 3 * B ** 2
|
||
else:
|
||
return d ** 3 # ignoring B for backward compatibility
|
||
|
||
@staticmethod
|
||
def LinPei11(beta, d, B=None):
|
||
"""
|
||
Runtime estimation assuming the Lindner-Peikert model in elementary operations.
|
||
|
||
.. [LinPei11] Lindner, R., & Peikert, C. (2011). Better key sizes (and attacks) for LWE-based
|
||
encryption. In A. Kiayias, CT-RSA~2011 (pp. 319–339). : Springer, Heidelberg.
|
||
|
||
:param beta: block size
|
||
:param d: lattice dimension
|
||
:param B: bit-size of entries
|
||
|
||
"""
|
||
delta_0 = delta_0f(beta)
|
||
return BKZ.LLL(d, B) + ZZ(2) ** RR(1.8 / log(delta_0, 2) - 110 + log(2.3 * 10 ** 9, 2))
|
||
|
||
@staticmethod
|
||
def _BDGL16_small(beta, d, B=None):
|
||
u"""
|
||
Runtime estimation given `β` and assuming sieving is used to realise the SVP oracle for small dimensions.
|
||
|
||
:param beta: block size
|
||
:param d: lattice dimension
|
||
:param B: bit-size of entries
|
||
|
||
.. [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 BKZ.LLL(d, B) + ZZ(2) ** RR(0.292 * beta + 16.4 + log(BKZ.svp_repeat(beta, d), 2))
|
||
|
||
@staticmethod
|
||
def _BDGL16_asymptotic(beta, d, B=None):
|
||
u"""
|
||
Runtime estimation given `β` and assuming sieving is used to realise the SVP oracle.
|
||
|
||
:param beta: block size
|
||
:param d: lattice dimension
|
||
:param B: bit-size of entries
|
||
|
||
.. [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).
|
||
|
||
"""
|
||
# TODO we simply pick the same additive constant 16.4 as for the experimental result in [BDGL16]
|
||
return BKZ.LLL(d, B) + ZZ(2) ** RR(0.292 * beta + 16.4 + log(BKZ.svp_repeat(beta, d), 2))
|
||
|
||
@staticmethod
|
||
def BDGL16(beta, d, B=None):
|
||
u"""
|
||
Runtime estimation given `β` and assuming sieving is used to realise the SVP oracle.
|
||
|
||
:param beta: block size
|
||
:param d: lattice dimension
|
||
:param B: bit-size of entries
|
||
|
||
.. [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 BKZ._BDGL16_asymptotic(beta, d, B)
|
||
|
||
@staticmethod
|
||
def LaaMosPol14(beta, d, B=None):
|
||
u"""
|
||
Runtime estimation for quantum sieving.
|
||
|
||
:param beta: block size
|
||
:param d: lattice dimension
|
||
:param B: bit-size of entries
|
||
|
||
.. [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.
|
||
|
||
.. [Laarhoven15] Laarhoven, T. (2015). Search problems in cryptography: from
|
||
fingerprinting to lattice sieving (Doctoral dissertation). Eindhoven University of
|
||
Technology. http://repository.tue.nl/837539
|
||
"""
|
||
return BKZ.LLL(d, B) + ZZ(2) ** RR((0.265 * beta + 16.4 + log(BKZ.svp_repeat(beta, d), 2)))
|
||
|
||
@staticmethod
|
||
def CheNgu12(beta, d, B=None):
|
||
u"""
|
||
Runtime estimation given `β` and assuming [CheNgu12]_ estimates are correct.
|
||
|
||
:param beta: block size
|
||
:param d: lattice dimension
|
||
:param B: bit-size of entries
|
||
|
||
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 = list(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
|
||
|
||
The estimation
|
||
|
||
2^(0.270188776350190*beta*log(beta) - 1.0192050451318417*beta + 16.10253135200765)
|
||
|
||
is of the number of enumeration nodes, hence we need to multiply by the number of
|
||
cycles to process one node. This cost per node is typically estimated as 100 [FPLLL].
|
||
|
||
.. [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
|
||
|
||
.. [FPLLL] The FPLLL development team. fplll, a lattice reduction library. 2016.
|
||
Available at https://github.com/fplll/fplll
|
||
"""
|
||
# TODO replace these by fplll timings
|
||
repeat = BKZ.svp_repeat(beta, d)
|
||
cost = RR(0.270188776350190 * beta * log(beta) - 1.0192050451318417 * beta + 16.10253135200765 + log(100, 2))
|
||
return BKZ.LLL(d, B) + repeat * ZZ(2) ** cost
|
||
|
||
@staticmethod
|
||
def ABFKSW20(beta, d, B=None):
|
||
"""
|
||
Enumeration cost according to [ABFKSW20]_.
|
||
|
||
:param beta: block size
|
||
:param d: lattice dimension
|
||
:param B: bit-size of entries
|
||
|
||
.. [ABFKSW20] Martin R. Albrecht, Shi Bai, Pierre-Alain Fouque, Paul Kirchner, Damien
|
||
Stehlé and Weiqiang Wen. Faster Enumeration-based Lattice Reduction: Root Hermite
|
||
Factor $k^{1/(2k)}$ in Time $k^{k/8 + o(k)}$. CRYPTO 2020.
|
||
"""
|
||
if 1.5 * beta >= d or beta <= 92: # 1.5β is a bit arbitrary, β≤92 is the crossover point
|
||
cost = RR(0.1839 * beta * log(beta, 2) - 0.995 * beta + 16.25 + log(100, 2))
|
||
else:
|
||
cost = RR(0.125 * beta * log(beta, 2) - 0.547 * beta + 10.4 + log(100, 2))
|
||
repeat = BKZ.svp_repeat(beta, d)
|
||
|
||
return BKZ.LLL(d, B) + repeat * ZZ(2) ** cost
|
||
|
||
@staticmethod
|
||
def ADPS16(beta, d, B=None, mode="classical"):
|
||
u"""
|
||
Runtime estimation from [ADPS16]_.
|
||
|
||
:param beta: block size
|
||
:param n: LWE dimension `n > 0`
|
||
:param B: bit-size of entries
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import BKZ, Param, dual, partial
|
||
sage: cost_model = partial(BKZ.ADPS16, mode="paranoid")
|
||
sage: dual(*Param.LindnerPeikert(128), reduction_cost_model=cost_model)
|
||
rop: 2^37.3
|
||
m: 346
|
||
red: 2^37.3
|
||
delta_0: 1.008209
|
||
beta: 127
|
||
d: 346
|
||
|v|: 284.363
|
||
repeat: 2000
|
||
epsilon: 0.062500
|
||
|
||
.. [ADPS16] Edem Alkim, Léo Ducas, Thomas Pöppelmann, & Peter Schwabe (2016).
|
||
Post-quantum key exchange - A New Hope. In T. Holz, & S. Savage, 25th USENIX
|
||
Security Symposium, USENIX Security 16 (pp. 327–343). USENIX Association.
|
||
"""
|
||
|
||
if mode not in ("classical", "quantum", "paranoid"):
|
||
raise ValueError("Mode '%s' not understood" % mode)
|
||
|
||
c = {
|
||
"classical": 0.2920,
|
||
"quantum": 0.2650, # paper writes 0.262 but this isn't right, see above
|
||
"paranoid": 0.2075,
|
||
}
|
||
|
||
c = c[mode]
|
||
|
||
return ZZ(2) ** RR(c * beta)
|
||
|
||
sieve = BDGL16
|
||
qsieve = LaaMosPol14
|
||
lp = LinPei11
|
||
enum = ABFKSW20
|
||
|
||
|
||
def delta_0f(beta):
|
||
"""
|
||
Compute root-Hermite factor `δ_0` from block size `β`.
|
||
"""
|
||
beta = ZZ(round(beta))
|
||
return BKZ._delta_0f(beta)
|
||
|
||
|
||
def betaf(delta):
|
||
"""
|
||
Compute block size `β` from root-Hermite factor `δ_0`.
|
||
"""
|
||
# TODO: decide for one strategy (secant, find_root, old) and its error handling
|
||
k = BKZ._beta_find_root(delta)
|
||
return k
|
||
|
||
|
||
reduction_default_cost = BKZ.enum
|
||
|
||
|
||
def lattice_reduction_cost(cost_model, delta_0, d, B=None):
|
||
"""
|
||
Return cost dictionary for returning vector of norm` δ_0^d Vol(Λ)^{1/d}` using provided lattice
|
||
reduction algorithm.
|
||
|
||
:param lattice_reduction_estimate:
|
||
:param delta_0: root-Hermite factor `δ_0 > 1`
|
||
:param d: lattice dimension
|
||
:param B: bit-size of entries
|
||
|
||
"""
|
||
beta = betaf(delta_0)
|
||
cost = cost_model(beta, d, B)
|
||
return Cost([("rop", cost), ("red", cost), ("delta_0", delta_0), ("beta", beta)])
|
||
|
||
|
||
def lattice_reduction_opt_m(n, q, delta):
|
||
"""
|
||
Return the (heuristically) optimal lattice dimension `m`
|
||
|
||
:param n: LWE dimension `n > 0`
|
||
:param q: modulus `0 < q`
|
||
:param delta: root Hermite factor `δ_0`
|
||
|
||
"""
|
||
# round can go wrong if the input is not a floating point number
|
||
return ZZ(round(sqrt(n * log(q, 2) / log(delta, 2)).n()))
|
||
|
||
|
||
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)
|
||
|
||
a = func(*args, reduction_cost_model=BKZ.sieve, **kwds)
|
||
b = func(*args, reduction_cost_model=BKZ.enum, **kwds)
|
||
if a["red"] <= b["red"]:
|
||
return a
|
||
else:
|
||
return b
|
||
|
||
return wrapper
|
||
|
||
|
||
# Combinatorial Algorithms for Sparse/Sparse Secrets
|
||
|
||
|
||
def guess_and_solve(f, n, alpha, q, secret_distribution, success_probability=0.99, **kwds):
|
||
"""Guess components of the secret.
|
||
|
||
:param f:
|
||
: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 secret_distribution: distribution of secret, see module level documentation for details
|
||
:param success_probability: targeted success probability < 1
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import guess_and_solve, dual_scale, partial
|
||
sage: q = next_prime(2^30)
|
||
sage: n, alpha = 512, 8/q
|
||
sage: dualg = partial(guess_and_solve, dual_scale)
|
||
sage: dualg(n, alpha, q, secret_distribution=((-1,1), 64))
|
||
rop: 2^63.9
|
||
m: 530
|
||
red: 2^63.9
|
||
delta_0: 1.008803
|
||
beta: 111
|
||
repeat: 2^21.6
|
||
d: 1042
|
||
c: 9.027
|
||
k: 0
|
||
|
||
"""
|
||
|
||
n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability)
|
||
RR = parent(alpha)
|
||
|
||
a, b = SDis.bounds(secret_distribution)
|
||
h = SDis.nonzero(secret_distribution, n)
|
||
|
||
size = b - a + 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:
|
||
current = f(
|
||
n - i,
|
||
alpha,
|
||
q,
|
||
secret_distribution=secret_distribution,
|
||
success_probability=max(1 - 1 / RR(2) ** 80, success_probability),
|
||
**kwds
|
||
)
|
||
except (OutOfBoundsError, RuntimeError, InsufficientSamplesError):
|
||
i += abs(step_size)
|
||
fail_attempts += 1
|
||
if fail_attempts > max_fail_attempts:
|
||
break
|
||
continue
|
||
if h is None or i < h:
|
||
repeat = size ** i
|
||
else:
|
||
# TODO: this is too pessimistic
|
||
repeat = (size) ** h * binomial(i, h)
|
||
current["k"] = i
|
||
current = current.repeat(repeat, select={"m": False})
|
||
|
||
logging.getLogger("binsearch").debug(current)
|
||
|
||
key = list(current)[0]
|
||
if best is None:
|
||
best = current
|
||
i += step_size
|
||
else:
|
||
if best[key] > 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
|
||
|
||
|
||
def success_probability_drop(n, h, k, fail=0, rotations=False):
|
||
"""
|
||
Probability that `k` randomly sampled components have ``fail`` non-zero components amongst
|
||
them.
|
||
|
||
:param n: LWE dimension `n > 0`
|
||
: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
|
||
:param rotations: consider rotations of the basis to exploit ring structure (NTRU only)
|
||
"""
|
||
|
||
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
|
||
prob_drop = binomial(K, k) * binomial(N - K, n - k) / binomial(N, n)
|
||
if rotations:
|
||
return 1 - (1 - prob_drop) ** N
|
||
else:
|
||
return prob_drop
|
||
|
||
|
||
def drop_and_solve(
|
||
f,
|
||
n,
|
||
alpha,
|
||
q,
|
||
secret_distribution=True,
|
||
success_probability=0.99,
|
||
postprocess=True,
|
||
decision=True,
|
||
rotations=False,
|
||
**kwds
|
||
):
|
||
"""
|
||
Solve instances of dimension ``n-k`` with increasing ``k`` using ``f`` and pick parameters such
|
||
that cost is minimised.
|
||
|
||
:param f: attack estimate function
|
||
: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 secret_distribution: distribution of secret, see module level documentation for details
|
||
:param success_probability: targeted success probability < 1
|
||
:param postprocess: check against shifted distributions
|
||
:param decision: the underlying algorithm solves the decision version or not
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import drop_and_solve, dual_scale, primal_usvp, partial
|
||
sage: q = next_prime(2^30)
|
||
sage: n, alpha = 512, 8/q
|
||
sage: primald = partial(drop_and_solve, primal_usvp)
|
||
sage: duald = partial(drop_and_solve, dual_scale)
|
||
|
||
sage: duald(n, alpha, q, secret_distribution=((-1,1), 64))
|
||
rop: 2^55.9
|
||
m: 478
|
||
red: 2^55.3
|
||
delta_0: 1.009807
|
||
beta: 89
|
||
repeat: 2^14.2
|
||
d: 920
|
||
c: 8.387
|
||
k: 70
|
||
postprocess: 8
|
||
|
||
sage: duald(n, alpha, q, secret_distribution=((-3,3), 64))
|
||
rop: 2^59.8
|
||
m: 517
|
||
red: 2^59.7
|
||
delta_0: 1.009403
|
||
beta: 97
|
||
repeat: 2^18.2
|
||
d: 969
|
||
c: 3.926
|
||
k: 60
|
||
postprocess: 6
|
||
|
||
sage: kwds = {"use_lll":False, "postprocess":False}
|
||
sage: duald(n, alpha, q, secret_distribution=((-1,1), 64), **kwds)
|
||
rop: 2^69.7
|
||
m: 521
|
||
red: 2^69.7
|
||
delta_0: 1.008953
|
||
beta: 107
|
||
repeat: 257
|
||
d: 1033
|
||
c: 9.027
|
||
k: 0
|
||
postprocess: 0
|
||
|
||
sage: duald(n, alpha, q, secret_distribution=((-3,3), 64), **kwds)
|
||
rop: 2^74.2
|
||
m: 560
|
||
red: 2^74.2
|
||
delta_0: 1.008668
|
||
beta: 114
|
||
repeat: 524.610
|
||
d: 1068
|
||
c: 4.162
|
||
k: 4
|
||
postprocess: 0
|
||
|
||
sage: duald(n, alpha, q, secret_distribution=((-3,3), 64), rotations=True, **kwds)
|
||
Traceback (most recent call last):
|
||
...
|
||
ValueError: Rotations are only support as part of the primal-usvp attack on NTRU.
|
||
|
||
sage: primald(n, alpha, q, secret_distribution=((-3,3), 64), rotations=True, **kwds)
|
||
rop: 2^51.5
|
||
red: 2^51.5
|
||
delta_0: 1.010046
|
||
beta: 84
|
||
d: 913
|
||
m: 444
|
||
repeat: 1.509286
|
||
k: 44
|
||
postprocess: 0
|
||
|
||
sage: primald(n, alpha, q, secret_distribution=((-3,3), 64), rotations=False, **kwds)
|
||
rop: 2^58.2
|
||
red: 2^58.2
|
||
delta_0: 1.009350
|
||
beta: 98
|
||
d: 1001
|
||
m: 492
|
||
repeat: 1.708828
|
||
k: 4
|
||
postprocess: 0
|
||
|
||
This function is based on:
|
||
|
||
.. [Albrecht17] Albrecht, M. R. (2017). On dual lattice attacks against small-secret LWE and
|
||
parameter choices in helib and SEAL. In J. Coron, & J. B. Nielsen, EUROCRYPT 2017, Part II
|
||
(pp. 103–129).
|
||
|
||
|
||
"""
|
||
|
||
if rotations and f.__name__ != "primal_usvp":
|
||
raise ValueError("Rotations are only support as part of the primal-usvp attack on NTRU.")
|
||
|
||
n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability)
|
||
|
||
best = None
|
||
|
||
if not decision and postprocess:
|
||
raise ValueError("Postprocessing is only defined for the dual attack which solves the decision version.")
|
||
|
||
# too small a step size leads to an early abort, too large a step
|
||
# size means stepping over target
|
||
step_size = int(n // 32)
|
||
|
||
if not SDis.is_bounded_uniform(secret_distribution):
|
||
raise NotImplementedError("Only bounded uniform secrets are currently supported.")
|
||
|
||
a, b = SDis.bounds(secret_distribution)
|
||
assert SDis.is_bounded_uniform(secret_distribution)
|
||
h = SDis.nonzero(secret_distribution, n)
|
||
|
||
k = ZZ(0)
|
||
|
||
while (n - h) > k:
|
||
probability = success_probability_drop(n, h, k, rotations=rotations)
|
||
|
||
# increase precision until the probability is meaningful
|
||
while success_probability ** probability == 1:
|
||
success_probability = RealField(64 + success_probability.prec())(success_probability)
|
||
|
||
current = f(
|
||
n - k,
|
||
alpha,
|
||
q,
|
||
success_probability=success_probability ** probability if decision else success_probability,
|
||
secret_distribution=secret_distribution,
|
||
**kwds
|
||
)
|
||
|
||
cost_lat = list(current.values())[0]
|
||
cost_post = 0
|
||
if postprocess:
|
||
repeat = current["repeat"]
|
||
dim = current["d"]
|
||
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) * (b - a) ** 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, rotations=rotations)
|
||
|
||
current["rop"] = cost_lat + cost_post
|
||
current = current.repeat(1 / probability, select={"m": False})
|
||
current["k"] = k
|
||
current["postprocess"] = postprocess
|
||
current = current.reorder(["rop"])
|
||
|
||
logging.getLogger("guess").debug(current)
|
||
|
||
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
|
||
|
||
|
||
# Primal Attack (uSVP)
|
||
|
||
|
||
def _primal_scale_factor(secret_distribution, alpha=None, q=None, n=None):
|
||
"""
|
||
Scale factor for primal attack, in the style of [BaiGal14]. In the case of non-centered secret
|
||
distributions, it first appropriately rebalances the lattice bases to maximise the scaling.
|
||
|
||
:param secret_distribution: distribution of secret, see module level documentation for details
|
||
:param alpha: noise rate `0 ≤ α < 1`, noise has standard deviation `αq/\\sqrt{2π}`
|
||
:param q: modulus `0 < q`
|
||
:param n: only used for sparse secrets
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import _primal_scale_factor, alphaf
|
||
sage: _primal_scale_factor(True, 8./2^15, 2^15)
|
||
1.000000000...
|
||
|
||
sage: _primal_scale_factor(alphaf(sqrt(2), 2^13, True), \
|
||
alpha=alphaf(sqrt(16/3), 2^13, True), q=2^13)
|
||
1.63299316185545
|
||
|
||
sage: _primal_scale_factor((-1,1), alpha=8./2^15, q=2^15)
|
||
3.908820095...
|
||
|
||
sage: _primal_scale_factor(((-1,1), 64), alpha=8./2^15, q=2^15, n=256)
|
||
6.383076486...
|
||
|
||
sage: _primal_scale_factor((-3,3), alpha=8./2^15, q=2^15)
|
||
1.595769121...
|
||
|
||
sage: _primal_scale_factor(((-3,3), 64), alpha=8./2^15, q=2^15, n=256)
|
||
2.954790254...
|
||
|
||
sage: _primal_scale_factor((-3,2), alpha=8./2^15, q=2^15)
|
||
1.868773442...
|
||
|
||
sage: _primal_scale_factor(((-3,2), 64), alpha=8./2^15, q=2^15, n=256)
|
||
3.313928192...
|
||
|
||
sage: _primal_scale_factor((0, 1), alpha=sqrt(2*pi)/2^15, q=2^15)
|
||
2.000000000...
|
||
|
||
sage: _primal_scale_factor((0, 1), alpha=sqrt(2*pi)/2^15/2, q=2^15)
|
||
1.000000000...
|
||
|
||
sage: _primal_scale_factor((0, 1), alpha=sqrt(2*pi)/2^15/1.99, q=2^15) > 1
|
||
True
|
||
|
||
.. [BaiGal14] Bai, S., & Galbraith, S. D. (2014). Lattice decoding attacks on binary
|
||
LWE. In W. Susilo, & Y. Mu, ACISP 14 (pp. 322–337). : Springer, Heidelberg.
|
||
"""
|
||
|
||
# For small/sparse secret use Bai and Galbraith's scaled embedding
|
||
# NOTE: We assume a <= 0 <= b
|
||
|
||
scale = RR(1)
|
||
if SDis.is_small(secret_distribution, alpha=alpha):
|
||
# target same same shortest vector length as in Kannan's embedding of same dimension
|
||
stddev = stddevf(alpha * q)
|
||
var_s = SDis.variance(secret_distribution, alpha, q, n=n)
|
||
if stddev ** 2 > var_s:
|
||
# only scale if the error is sampled wider than the secret
|
||
scale = stddev / RR(sqrt(var_s))
|
||
|
||
return scale
|
||
|
||
|
||
def _primal_usvp(
|
||
block_size,
|
||
n,
|
||
alpha,
|
||
q,
|
||
scale=1,
|
||
m=oo,
|
||
success_probability=0.99,
|
||
kannan_coeff=None,
|
||
d=None,
|
||
reduction_cost_model=reduction_default_cost,
|
||
):
|
||
"""
|
||
Estimate cost of solving LWE using primal attack (uSVP version)
|
||
|
||
: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 scale: The identity part of the lattice basis is scaled by this constant.
|
||
:param m: number of available LWE samples `m > 0`
|
||
:param d: dimension for the attack d <= m + 1 (`None' for optimized choice)
|
||
:parap kannan_coeff: Coeff for Kannan's embedding (`None' to set it kannan_coeff=stddev,
|
||
which is optimal at least when Distrib(secret) = Distrib(Error).)
|
||
:param success_probability: targeted success probability < 1
|
||
:param reduction_cost_model: cost model for lattice reduction
|
||
|
||
.. note:: This is the low-level function, in most cases you will want to call ``primal_usvp``
|
||
|
||
"""
|
||
|
||
n, alpha, q = Param.preprocess(n, alpha, q)
|
||
RR = alpha.parent()
|
||
q = RR(q)
|
||
delta_0 = delta_0f(block_size)
|
||
stddev = stddevf(alpha * q)
|
||
block_size = RR(block_size)
|
||
|
||
scale = RR(scale)
|
||
|
||
m = min(2 * ceil(sqrt(n * log(q) / log(delta_0))), m)
|
||
|
||
if kannan_coeff is None:
|
||
kannan_coeff = stddev
|
||
|
||
def log_b_star(d):
|
||
return delta_0.log() * (2 * block_size - d) + (kannan_coeff.log() + n * scale.log() + (d - n - 1) * q.log()) / d
|
||
|
||
C = (stddev ** 2 * (block_size - 1) + kannan_coeff ** 2).log() / 2
|
||
|
||
# find the smallest d \in [n,m] s.t. a*d^2 + b*d + c >= 0
|
||
# if no such d exists, return the upper bound m
|
||
def solve_for_d(n, m, a, b, c):
|
||
if a*n*n + b*n + c >= 0: # trivial case
|
||
return n
|
||
|
||
# solve for ad^2 + bd + c == 0
|
||
|
||
disc = b*b - 4*a*c # the discriminant
|
||
if disc < 0: # no solution, return m
|
||
return m
|
||
|
||
# compute the two solutions
|
||
d1 = (-b + sqrt(disc))/(2*a)
|
||
d2 = (-b - sqrt(disc))/(2*a)
|
||
if a > 0: # the only possible solution is ceiling(d2)
|
||
return min(m, ceil(d2))
|
||
|
||
# the case a<=0:
|
||
|
||
# if n is to the left of d1 then the first solution is ceil(d1)
|
||
if n <= d1:
|
||
return min(m, ceil(d1))
|
||
|
||
# otherwise, n must be larger than d2 (since an^2+bn+c<0) so no solution
|
||
return m
|
||
|
||
# we have m samples → the largest permissible dimension d is m+1
|
||
if d is None:
|
||
# for d in range(n, m + 2):
|
||
# if log_b_star(d) - C >= 0:
|
||
# break
|
||
aa = -delta_0.log()
|
||
bb = delta_0.log()*2*block_size + q.log() - C
|
||
cc = kannan_coeff.log() + n * scale.log() - (n + 1) * q.log()
|
||
# assert d == solve_for_d(n, m+1, aa, bb, cc)
|
||
d = solve_for_d(n, m+1, aa, bb, cc)
|
||
|
||
assert d <= m + 1
|
||
|
||
def ineq(d):
|
||
lhs = sqrt(stddev ** 2 * (block_size - 1) + kannan_coeff ** 2)
|
||
rhs = delta_0 ** (2 * block_size - d) * (kannan_coeff * scale ** n * q ** (d - n - 1)) ** (ZZ(1) / d)
|
||
return lhs <= rhs
|
||
|
||
ret = lattice_reduction_cost(reduction_cost_model, delta_0, d)
|
||
if not ineq(d):
|
||
ret["rop"] = oo
|
||
ret["red"] = oo
|
||
|
||
ret["d"] = d
|
||
ret["delta_0"] = delta_0
|
||
|
||
return ret
|
||
|
||
|
||
def primal_usvp(
|
||
n,
|
||
alpha,
|
||
q,
|
||
secret_distribution=True,
|
||
m=oo,
|
||
success_probability=0.99,
|
||
kannan_coeff=None,
|
||
d=None,
|
||
reduction_cost_model=reduction_default_cost,
|
||
**kwds
|
||
):
|
||
u"""
|
||
Estimate cost of solving LWE using primal attack (uSVP version)
|
||
|
||
: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 secret_distribution: distribution of secret, see module level documentation for details
|
||
:param m: number of LWE samples `m > 0`
|
||
:param success_probability: targeted success probability < 1
|
||
:param reduction_cost_model: cost model for lattice reduction
|
||
|
||
EXAMPLES::
|
||
|
||
sage: from estimator import primal_usvp, Param, BKZ
|
||
sage: n, alpha, q = Param.Regev(256)
|
||
|
||
sage: primal_usvp(n, alpha, q)
|
||
rop: 2^145.5
|
||
red: 2^145.5
|
||
delta_0: 1.005374
|
||
beta: 256
|
||
d: 694
|
||
m: 437
|
||
|
||
sage: primal_usvp(n, alpha, q, secret_distribution=True, m=n)
|
||
rop: 2^186.9
|
||
red: 2^186.9
|
||
delta_0: 1.004638
|
||
beta: 320
|
||
d: 513
|
||
m: 256
|
||
|
||
sage: primal_usvp(n, alpha, q, secret_distribution=False, m=2*n)
|
||
rop: 2^186.9
|
||
red: 2^186.9
|
||
delta_0: 1.004638
|
||
beta: 320
|
||
d: 513
|
||
m: 512
|
||
|
||
sage: primal_usvp(n, alpha, q, reduction_cost_model=BKZ.sieve)
|
||
rop: 2^103.6
|
||
red: 2^103.6
|
||
delta_0: 1.005374
|
||
beta: 256
|
||
d: 694
|
||
m: 437
|
||
|
||
sage: primal_usvp(n, alpha, q)
|
||
rop: 2^145.5
|
||
red: 2^145.5
|
||
delta_0: 1.005374
|
||
beta: 256
|
||
d: 694
|
||
m: 437
|
||
|
||
sage: primal_usvp(n, alpha, q, secret_distribution=(-1,1), m=n)
|
||
rop: 2^84.7
|
||
red: 2^84.7
|
||
delta_0: 1.007345
|
||
beta: 154
|
||
d: 513
|
||
m: 256
|
||
|
||
sage: primal_usvp(n, alpha, q, secret_distribution=((-1,1), 64))
|
||
rop: 2^77.1
|
||
red: 2^77.1
|
||
delta_0: 1.007754
|
||
beta: 140
|
||
d: 477
|
||
m: 220
|
||
|
||
.. [USENIX:ADPS16] Alkim, E., Léo Ducas, Thomas Pöppelmann, & Schwabe, P. (2015).
|
||
Post-quantum key exchange - a new hope.
|
||
|
||
.. [BaiGal14] Bai, S., & Galbraith, S. D. (2014). Lattice decoding attacks on binary
|
||
LWE. In W. Susilo, & Y. Mu, ACISP 14 (pp. 322–337). : Springer, Heidelberg.
|
||
|
||
"""
|
||
|
||
if m < 1:
|
||
raise InsufficientSamplesError("m=%d < 1" % m)
|
||
|
||
n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability)
|
||
|
||
# allow for a larger embedding lattice dimension, using Bai and Galbraith's
|
||
if SDis.is_small(secret_distribution, alpha=alpha):
|
||
m += n
|
||
|
||
scale = _primal_scale_factor(secret_distribution, alpha, q, n)
|
||
|
||
kwds = {
|
||
"n": n,
|
||
"alpha": alpha,
|
||
"q": q,
|
||
"kannan_coeff": kannan_coeff,
|
||
"d": d,
|
||
"reduction_cost_model": reduction_cost_model,
|
||
"m": m,
|
||
"scale": scale,
|
||
}
|
||
|
||
cost = binary_search(
|
||
_primal_usvp,
|
||
start=40,
|
||
stop=2 * n,
|
||
param="block_size",
|
||
predicate=lambda x, best: x["red"] <= best["red"],
|
||
**kwds
|
||
)
|
||
|
||
for block_size in range(32, cost["beta"] + 1)[::-1]:
|
||
t = _primal_usvp(block_size=block_size, **kwds)
|
||
if t["red"] == oo:
|
||
break
|
||
cost = t
|
||
|
||
if SDis.is_small(secret_distribution, alpha=alpha):
|
||
cost["m"] = cost["d"] - n - 1
|
||
else:
|
||
cost["m"] = cost["d"] - 1
|
||
|
||
return cost
|
||
|
||
|
||
# Primal Attack (Enumeration)
|
||
|
||
|
||
def enumeration_cost(n, alpha, q, success_probability, delta_0, m, clocks_per_enum=2 ** 15.1):
|
||
"""
|
||
Estimates the cost of performing enumeration.
|
||
|
||
: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 success_probability: target success probability
|
||
:param delta_0: root-Hermite factor `δ_0 > 1`
|
||
:param m: number of LWE samples `m > 0`
|
||
:param clocks_per_enum: the log of the number of clock cycles needed per enumeration
|
||
"""
|
||
target_success_probability = success_probability
|
||
|
||
try:
|
||
alpha.is_NaN()
|
||
RR = alpha.parent()
|
||
|
||
except AttributeError:
|
||
RR = RealField(128)
|
||
|
||
step = RDF(1)
|
||
|
||
B = BKZ.GSA(n, q, delta_0, m)
|
||
|
||
d = [RDF(1)] * m
|
||
bd = [d[i] * B[i] for i in range(m)]
|
||
scaling_factor = RDF(sqrt(pi) / (2 * alpha * q))
|
||
probs_bd = [RDF((bd[i] * scaling_factor)).erf() for i in range(m)]
|
||
success_probability = prod(probs_bd)
|
||
|
||
if RR(success_probability).is_NaN() or success_probability == 0.0:
|
||
# try in higher precision
|
||
step = RR(step)
|
||
d = [RR(1)] * m
|
||
bd = [d[i] * B[i] for i in range(m)]
|
||
scaling_factor = RR(sqrt(pi) / (2 * alpha * q))
|
||
probs_bd = [RR((bd[i] * scaling_factor)).erf() for i in range(m)]
|
||
success_probability = prod(probs_bd)
|
||
|
||
if success_probability.is_NaN():
|
||
return OrderedDict([("babai", oo), ("babai_op", oo)])
|
||
|
||
if success_probability <= 0:
|
||
# TODO this seems like the wrong except to raise
|
||
raise InsufficientSamplesError("success probability <= 0.")
|
||
|
||
bd = map(list, zip(bd, range(len(bd))))
|
||
bd = sorted(bd)
|
||
|
||
import bisect
|
||
|
||
last_success_probability = success_probability
|
||
|
||
while success_probability < RDF(target_success_probability):
|
||
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 Cost([("babai", oo), ("babai_op", oo)])
|
||
|
||
r = Cost([("babai", RR(prod(d))), ("babai_op", RR(prod(d)) * clocks_per_enum)])
|
||
|
||
return r
|
||
|
||
|
||
def _primal_decode(
|
||
n,
|
||
alpha,
|
||
q,
|
||
secret_distribution=True,
|
||
m=oo,
|
||
success_probability=0.99,
|
||
reduction_cost_model=reduction_default_cost,
|
||
clocks_per_enum=2 ** 15.1,
|
||
):
|
||
"""
|
||
Decoding attack as described in [LinPei11]_.
|
||
|
||
: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 success_probability: targeted success probability < 1
|
||
:param clocks_per_enum: the number of enumerations computed per clock cycle
|
||
|
||
EXAMPLE:
|
||
|
||
sage: from estimator import Param, primal_decode
|
||
sage: n, alpha, q = Param.Regev(256)
|
||
|
||
sage: primal_decode(n, alpha, q)
|
||
rop: 2^156.5
|
||
m: 464
|
||
red: 2^156.5
|
||
delta_0: 1.005446
|
||
beta: 251
|
||
d: 720
|
||
babai: 2^141.8
|
||
babai_op: 2^156.9
|
||
repeat: 2^14.2
|
||
epsilon: 2^-12.0
|
||
|
||
sage: primal_decode(n, alpha, q, secret_distribution=(-1,1), m=n)
|
||
rop: 2^194.6
|
||
m: 256
|
||
red: 2^194.6
|
||
delta_0: 1.005069
|
||
beta: 280
|
||
d: 512
|
||
babai: 2^179.6
|
||
babai_op: 2^194.7
|
||
repeat: 2^34.2
|
||
epsilon: 2^-32.0
|
||
|
||
sage: primal_decode(n, alpha, q, secret_distribution=((-1,1), 64))
|
||
rop: 2^156.5
|
||
m: 464
|
||
red: 2^156.5
|
||
delta_0: 1.005446
|
||
beta: 251
|
||
d: 720
|
||
babai: 2^141.8
|
||
babai_op: 2^156.9
|
||
repeat: 2^14.2
|
||
epsilon: 2^-12.0
|
||
|
||
.. [LinPei11] Lindner, R., & Peikert, C. (2011). Better key sizes (and attacks) for
|
||
LWE-based encryption. In A. Kiayias, CT-RSA~2011 (pp. 319–339). : Springer, Heidelberg.
|
||
"""
|
||
|
||
n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability)
|
||
|
||
if m < 1:
|
||
raise InsufficientSamplesError("m=%d < 1" % m)
|
||
|
||
if SDis.is_small(secret_distribution, alpha=alpha):
|
||
m_ = m + n
|
||
else:
|
||
m_ = m
|
||
|
||
RR = alpha.parent()
|
||
|
||
delta_0m1 = (
|
||
_dual(n, alpha, q, success_probability=success_probability, secret_distribution=secret_distribution, m=m)[
|
||
"delta_0"
|
||
]
|
||
- 1
|
||
)
|
||
|
||
step = RR(1.05)
|
||
direction = -1
|
||
|
||
def combine(enum, bkz):
|
||
current = Cost()
|
||
current["rop"] = enum["babai_op"] + bkz["red"]
|
||
|
||
for key in bkz:
|
||
current[key] = bkz[key]
|
||
for key in enum:
|
||
current[key] = enum[key]
|
||
current[u"m"] = m_ - n if SDis.is_small(secret_distribution, alpha=alpha) else m_
|
||
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)
|
||
m_ = min(m_optimal, m_)
|
||
bkz = lattice_reduction_cost(reduction_cost_model, delta_0, m_, B=log(q, 2))
|
||
bkz["d"] = m_
|
||
|
||
enum = enumeration_cost(n, alpha, q, success_probability, delta_0, m_, clocks_per_enum=clocks_per_enum)
|
||
current = combine(enum, bkz).reorder(["rop", "m"])
|
||
|
||
# if lattice reduction is cheaper than enumration, make it more expensive
|
||
if current["red"] < current["babai_op"]:
|
||
prev_direction = direction
|
||
direction = -1
|
||
if direction != prev_direction:
|
||
step = 1 + RR(step - 1) / 2
|
||
delta_0m1 /= step
|
||
elif current["red"] > current["babai_op"]:
|
||
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
|
||
|
||
|
||
primal_decode = partial(rinse_and_repeat, _primal_decode, decision=False, repeat_select={"m": False})
|
||
|
||
|
||
# Dual Attack
|
||
|
||
|
||
def _dual_scale_factor(secret_distribution, alpha=None, q=None, n=None, c=None):
|
||
"""
|
||
Scale factor for dual attack. Assumes the secret vector has been "re-balanced", as to
|
||
be distributed around 0.
|
||
|
||
:param secret_distribution: distribution of secret, see module level documentation for details
|
||
:param alpha: noise rate `0 ≤ α < 1`, noise has standard deviation `αq/\\sqrt{2π}`
|
||
:param q: modulus `0 < q`
|
||
:param n: only used for sparse secrets
|
||
:param c: explicit constant `c`
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import _dual_scale_factor, alphaf
|
||
sage: _dual_scale_factor(True, 8./2^15, 2^15)
|
||
(1.00000000000000, 3.19153824321146)
|
||
|
||
sage: _dual_scale_factor(alphaf(sqrt(2), 2^13, True), \
|
||
alpha=alphaf(sqrt(16/3), 2^13, True), q=2^13)
|
||
(1.63299316185545, 1.41421356237309)
|
||
|
||
sage: _dual_scale_factor((-1,1), alpha=8./2^15, q=2^15)
|
||
(3.90882009522336, 0.816496580927726)
|
||
|
||
sage: _dual_scale_factor(((-1,1), 64), alpha=8./2^15, q=2^15, n=256)
|
||
(6.38307648642292, 0.500000000000000)
|
||
|
||
sage: _dual_scale_factor((-3,3), alpha=8./2^15, q=2^15)
|
||
(1.59576912160573, 2.00000000000000)
|
||
|
||
sage: _dual_scale_factor(((-3,3), 64), alpha=8./2^15, q=2^15, n=256)
|
||
(2.95479025475795, 1.08012344973464)
|
||
|
||
sage: _dual_scale_factor((-3,2), alpha=8./2^15, q=2^15)
|
||
(1.86877344262086, 1.70782512765993)
|
||
|
||
sage: _dual_scale_factor(((-3,2), 64), alpha=8./2^15, q=2^15, n=256)
|
||
(3.31392819210159, 0.963068014212911)
|
||
|
||
.. note :: This function assumes that the bounds are of opposite sign, and that the
|
||
distribution is centred around zero.
|
||
"""
|
||
stddev = RR(stddevf(alpha * q))
|
||
# Calculate scaling in the case of bounded_uniform secret distribution
|
||
# NOTE: We assume a <= 0 <= b
|
||
if SDis.is_small(secret_distribution, alpha=alpha):
|
||
stddev_s = SDis.variance(secret_distribution, alpha=alpha, q=q, n=n).sqrt()
|
||
if c is None:
|
||
# |<v,s>| = |<w,e>| → c * \sqrt{n} * \sqrt{σ_{s_i}^2 + E(s_i)^2} == \sqrt{m} * σ
|
||
# TODO: we are assuming n == m here! The coefficient formula for general m
|
||
# is the one below * sqrt(m/n)
|
||
c = RR(stddev / stddev_s)
|
||
else:
|
||
stddev_s = stddev
|
||
c = RR(1)
|
||
|
||
stddev_s = RR(stddev_s)
|
||
|
||
return c, stddev_s
|
||
|
||
|
||
def _dual(
|
||
n, alpha, q, secret_distribution=True, m=oo, success_probability=0.99, reduction_cost_model=reduction_default_cost
|
||
):
|
||
"""
|
||
Estimate cost of solving LWE using dual attack as described in [MicReg09]_
|
||
|
||
: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 secret_distribution: distribution of secret, see module level documentation for details
|
||
:param m: number of LWE samples `m > 0`
|
||
:param success_probability: targeted success probability < 1
|
||
:param reduction_cost_model: cost model for lattice reduction
|
||
|
||
.. [MicReg09] Micciancio, D., & Regev, O. (2009). Lattice-based cryptography. In D. J.
|
||
Bernstein, J. Buchmann, & E. Dahmen (Eds.), Post-Quantum Cryptography (pp. 147–191).
|
||
Berlin, Heidelberg, New York: Springer, Heidelberg.
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import Param, BKZ, dual
|
||
sage: n, alpha, q = Param.Regev(256)
|
||
|
||
sage: dual(n, alpha, q)
|
||
rop: 2^197.2
|
||
m: 751
|
||
red: 2^197.2
|
||
delta_0: 1.005048
|
||
beta: 282
|
||
d: 751
|
||
|v|: 1923.968
|
||
repeat: 2^35.0
|
||
epsilon: 2^-16.0
|
||
|
||
sage: dual(n, alpha, q, secret_distribution=True, m=n)
|
||
rop: 2^255.2
|
||
m: 512
|
||
red: 2^255.2
|
||
delta_0: 1.004627
|
||
beta: 322
|
||
d: 512
|
||
|v|: 2^11.4
|
||
repeat: 2^67.0
|
||
epsilon: 2^-32.0
|
||
|
||
sage: dual(n, alpha, q, secret_distribution=False, m=2*n)
|
||
rop: 2^255.2
|
||
m: 512
|
||
red: 2^255.2
|
||
delta_0: 1.004627
|
||
beta: 322
|
||
d: 512
|
||
|v|: 2^11.4
|
||
repeat: 2^67.0
|
||
epsilon: 2^-32.0
|
||
|
||
sage: dual(n, alpha, q, reduction_cost_model=BKZ.sieve)
|
||
rop: 2^142.9
|
||
m: 787
|
||
red: 2^142.9
|
||
delta_0: 1.004595
|
||
beta: 325
|
||
d: 787
|
||
|v|: 1360.451
|
||
repeat: 2^19.0
|
||
epsilon: 0.003906
|
||
|
||
.. note :: this is the standard dual attack, for the small secret variant see
|
||
``dual_scale``
|
||
|
||
"""
|
||
|
||
n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability)
|
||
|
||
if m < 1:
|
||
raise InsufficientSamplesError("m=%d < 1" % m)
|
||
|
||
RR = parent(alpha)
|
||
f = lambda eps: RR(sqrt(log(1 / eps) / pi)) # noqa
|
||
|
||
if SDis.is_small(secret_distribution, alpha=alpha):
|
||
m = m + n
|
||
log_delta_0 = log(f(success_probability) / alpha, 2) ** 2 / (4 * n * log(q, 2))
|
||
delta_0 = min(RR(2) ** log_delta_0, RR(1.02190)) # at most LLL
|
||
m_optimal = lattice_reduction_opt_m(n, q, delta_0)
|
||
if m > m_optimal:
|
||
m = m_optimal
|
||
else:
|
||
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
|
||
# TODO delta_0f(m) is HKZ reduction, and thus it is identical 1, i.e. meaningless.
|
||
# A better check here would be good.
|
||
if delta_0 < delta_0f(m):
|
||
raise OutOfBoundsError("delta_0 = %f < %f" % (delta_0, delta_0f(m)))
|
||
|
||
ret = lattice_reduction_cost(reduction_cost_model, delta_0, m, B=log(q, 2))
|
||
ret[u"m"] = m
|
||
ret[u"d"] = m
|
||
ret[u"|v|"] = RR(delta_0 ** m * q ** (n / m))
|
||
return ret.reorder(["rop", "m"])
|
||
|
||
|
||
dual = partial(rinse_and_repeat, _dual, repeat_select={"m": False})
|
||
|
||
|
||
def dual_scale(
|
||
n,
|
||
alpha,
|
||
q,
|
||
secret_distribution,
|
||
m=oo,
|
||
success_probability=0.99,
|
||
reduction_cost_model=reduction_default_cost,
|
||
c=None,
|
||
use_lll=True,
|
||
):
|
||
"""
|
||
Estimate cost of solving LWE by finding small `(y,x/c)` such that `y ⋅ A ≡ c ⋅ x \bmod q` as
|
||
described in [EC:Abrecht17]_
|
||
|
||
: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 secret_distribution: distribution of secret, see module level documentation for details
|
||
:param m: number of LWE samples `m > 0`
|
||
:param success_probability: targeted success probability < 1
|
||
:param reduction_cost_model: cost model for lattice reduction
|
||
:param c: explicit constant `c`
|
||
:param use_lll: use LLL calls to produce more small vectors
|
||
|
||
EXAMPLES::
|
||
|
||
sage: from estimator import Param, dual_scale
|
||
|
||
sage: dual_scale(*Param.Regev(256), secret_distribution=(-1,1))
|
||
rop: 2^101.8
|
||
m: 288
|
||
red: 2^101.8
|
||
delta_0: 1.006648
|
||
beta: 183
|
||
repeat: 2^65.4
|
||
d: 544
|
||
c: 31.271
|
||
|
||
sage: dual_scale(*Param.Regev(256), secret_distribution=(-1,1), m=200)
|
||
rop: 2^106.0
|
||
m: 200
|
||
red: 2^106.0
|
||
delta_0: 1.006443
|
||
beta: 192
|
||
repeat: 2^68.0
|
||
d: 456
|
||
c: 31.271
|
||
|
||
sage: dual_scale(*Param.Regev(256), secret_distribution=((-1,1), 64))
|
||
rop: 2^93.7
|
||
m: 258
|
||
red: 2^93.7
|
||
delta_0: 1.006948
|
||
beta: 170
|
||
repeat: 2^56.0
|
||
d: 514
|
||
c: 51.065
|
||
|
||
.. [Albrecht17] Albrecht, M. R. (2017). On dual lattice attacks against small-secret LWE and
|
||
parameter choices in helib and SEAL. In J. Coron, & J. B. Nielsen, EUROCRYPT} 2017, Part {II
|
||
(pp. 103–129).
|
||
"""
|
||
|
||
n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability)
|
||
RR = parent(alpha)
|
||
|
||
# stddev of the error
|
||
stddev = RR(stddevf(alpha * q))
|
||
|
||
m_max = m
|
||
|
||
if not SDis.is_small(secret_distribution, alpha=alpha):
|
||
m_max -= n # We transform to normal form, spending n samples
|
||
|
||
c, stddev_s = _dual_scale_factor(secret_distribution, alpha=alpha, q=q, n=n, c=c)
|
||
|
||
best = dual(n=n, alpha=alpha, q=q, m=m, reduction_cost_model=reduction_cost_model)
|
||
delta_0 = best["delta_0"]
|
||
|
||
if use_lll:
|
||
rerand_scale = 2
|
||
else:
|
||
rerand_scale = 1
|
||
|
||
while True:
|
||
m_optimal = lattice_reduction_opt_m(n, q / c, delta_0)
|
||
d = ZZ(min(m_optimal, m_max + n))
|
||
m = d - n
|
||
|
||
# the vector found will have norm v
|
||
v = rerand_scale * delta_0 ** d * (q / c) ** (n / d)
|
||
|
||
# each component has stddev v_
|
||
v_ = v / RR(sqrt(d))
|
||
|
||
# we split our vector in two parts.
|
||
v_r = sigmaf(RR(stddev * sqrt(m) * v_)) # 1. v_r is multiplied with the error stddev (dimension m-n)
|
||
v_l = sigmaf(RR(c * stddev_s * sqrt(n) * v_)) # 2. v_l is the rounding noise (dimension n)
|
||
|
||
ret = lattice_reduction_cost(reduction_cost_model, delta_0, d, B=log(q, 2))
|
||
|
||
repeat = max(amplify_sigma(success_probability, (v_r, v_l), q), RR(1))
|
||
if use_lll:
|
||
lll = BKZ.LLL(d, log(q, 2))
|
||
else:
|
||
lll = None
|
||
ret = ret.repeat(times=repeat, lll=lll)
|
||
|
||
ret[u"m"] = m
|
||
ret[u"repeat"] = repeat
|
||
ret[u"d"] = d
|
||
ret[u"c"] = c
|
||
|
||
ret = ret.reorder(["rop", "m"])
|
||
logging.getLogger("repeat").debug(ret)
|
||
|
||
if best is None:
|
||
best = ret
|
||
|
||
if ret["rop"] > best["rop"]:
|
||
break
|
||
|
||
best = ret
|
||
delta_0 = delta_0 + RR(0.00005)
|
||
|
||
return best
|
||
|
||
|
||
# Combinatorial
|
||
|
||
|
||
def mitm(n, alpha, q, secret_distribution=True, m=oo, success_probability=0.99):
|
||
"""
|
||
Meet-in-the-Middle attack as described in [AlbPlaSco15]_
|
||
|
||
: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 secret_distribution: distribution of secret, see module level documentation for details
|
||
:param m: number of LWE samples `m > 0`
|
||
:param success_probability: targeted success probability < 1
|
||
|
||
.. [AlbPlaSco15] Albrecht, M. R., Player, R., & Scott, S. (2015). On the concrete hardness of
|
||
Learning with Errors. Journal of Mathematical Cryptology, 9(3), 169–203.
|
||
|
||
"""
|
||
n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability)
|
||
ret = Cost()
|
||
RR = alpha.parent()
|
||
|
||
if not SDis.is_small(secret_distribution, alpha=alpha):
|
||
m = m - n
|
||
secret_distribution = True
|
||
ret["m"] = n
|
||
else:
|
||
ret["m"] = 0
|
||
|
||
if m < 1:
|
||
raise InsufficientSamplesError("m=%d < 1" % m)
|
||
|
||
t = ceil(2 * sqrt(log(n)))
|
||
try:
|
||
a, b = SDis.bounds(secret_distribution)
|
||
size = b - a + 1
|
||
except ValueError:
|
||
size = (2 * t * alpha * q) + 1
|
||
|
||
m_required = ceil((log((2 * n) / ((size ** (n / 2)) - 1))) / (log(size / q)))
|
||
if m is not None and m < m_required:
|
||
raise InsufficientSamplesError("m=%d < %d (required)" % (m, m_required))
|
||
else:
|
||
m = m_required
|
||
|
||
if (3 * t * alpha) * m > 1 - 1 / (2 * n):
|
||
raise ValueError("Cannot find m to satisfy constraints (noise too big).")
|
||
|
||
ret["rop"] = RR(size ** (n / 2) * 2 * n * m)
|
||
ret["mem"] = RR(size ** (n / 2) * m)
|
||
ret["m"] += m
|
||
|
||
return ret.reorder(["rop", "m", "mem"])
|
||
|
||
|
||
cfft = 1 # convolutions mod q
|
||
|
||
|
||
def _bkw_coded(n, alpha, q, secret_distribution=True, m=oo, success_probability=0.99, t2=0, b=2, ntest=None): # noqa
|
||
"""
|
||
Coded-BKW.
|
||
|
||
: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 t2: number of coded BKW steps (≥ 0)
|
||
:param b: table size (≥ 1)
|
||
:param success_probability: targeted success probability < 1
|
||
:param ntest: optional parameter ntest
|
||
|
||
"""
|
||
n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability)
|
||
sigma = stddevf(alpha * q) # [C:GuoJohSta15] use σ = standard deviation
|
||
RR = alpha.parent()
|
||
|
||
cost = Cost()
|
||
|
||
# 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 # noqa
|
||
cost["l"] = l
|
||
|
||
try:
|
||
secret_bounds = SDis.bounds(secret_distribution)
|
||
d = secret_bounds[1] - secret_bounds[0] + 1
|
||
except ValueError:
|
||
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): # noqa
|
||
"""
|
||
If the parameter `ntest` is not provided, we use this function to estimate it.
|
||
|
||
:param n: LWE dimension `n > 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("ntest")
|
||
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:
|
||
start = max(ZZ(round(find_root(ntop, 2, n - t1 * b + 1, rtol=0.1))) - 1, 2)
|
||
except RuntimeError:
|
||
start = 2
|
||
ntest_min = 1
|
||
for ntest in range(start, n - t1 * b + 1):
|
||
if abs(ntop(ntest=ntest).n()) < abs(ntop(ntest=ntest_min).n()):
|
||
ntest_min = ntest
|
||
else:
|
||
break
|
||
return ZZ(ntest_min)
|
||
|
||
# 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
|
||
s_var = SDis.variance(secret_distribution, alpha, q, n=n)
|
||
coding_variance = s_var * sigma_set ** 2 * ntot
|
||
sigma_final = RR(sqrt(2 ** (t1 + t2) * sigma ** 2 + coding_variance))
|
||
# cost[u"σ_final"] = RR(sigma_final)
|
||
|
||
M = amplify_sigma(success_probability, sigmaf(sigma_final), q)
|
||
if M is oo:
|
||
cost["rop"] = oo
|
||
cost["m"] = oo
|
||
return cost
|
||
m = (t1 + t2) * (q ** b - 1) / 2 + M
|
||
cost["m"] = RR(m)
|
||
|
||
if not SDis.is_small(secret_distribution, alpha=alpha):
|
||
# Equation (7)
|
||
n_ = n - t1 * b
|
||
C0 = (m - n_) * (n + 1) * ceil(n_ / (b - 1))
|
||
assert C0 >= 0
|
||
# cost["C0(gauss)"] = RR(C0)
|
||
else:
|
||
C0 = 0
|
||
|
||
# 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(1, t2 + 1)])
|
||
C2 = RR(C2_)
|
||
for i in range(1, 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["mem"] = (t1 + t2) * q ** b
|
||
|
||
cost = cost.reorder(["rop", "m", "mem", "b", "t1", "t2"])
|
||
|
||
return cost
|
||
|
||
|
||
def bkw_coded(n, alpha, q, secret_distribution=True, m=oo, success_probability=0.99, ntest=None):
|
||
"""
|
||
Coded-BKW as described in [C:GuoJohSta15]
|
||
|
||
: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 success_probability: targeted success probability < 1
|
||
:param samples: the number of available samples
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import Param, bkw_coded
|
||
sage: n, alpha, q = Param.Regev(64)
|
||
sage: bkw_coded(n, alpha, q)
|
||
rop: 2^48.8
|
||
m: 2^39.6
|
||
mem: 2^39.6
|
||
b: 3
|
||
t1: 2
|
||
t2: 10
|
||
l: 2
|
||
ncod: 53
|
||
ntop: 0
|
||
ntest: 6
|
||
|
||
.. [GuoJohSta15] Guo, Q., Johansson, T., & Stankovski, P. (2015). Coded-BKW: solving LWE using lattice
|
||
codes. In R. Gennaro, & M. J. B. Robshaw, CRYPTO~2015, Part~I (pp. 23–42). :
|
||
Springer, Heidelberg.
|
||
|
||
"""
|
||
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
|
||
r = binary_search(
|
||
_bkw_coded,
|
||
2,
|
||
min(n // b, ceil(2 * log(q, 2))),
|
||
"t2",
|
||
predicate=lambda x, best: x["rop"] <= best["rop"] and (best["m"] > m or x["m"] <= m),
|
||
b=b,
|
||
n=n,
|
||
alpha=alpha,
|
||
q=q,
|
||
secret_distribution=secret_distribution,
|
||
success_probability=success_probability,
|
||
m=m,
|
||
ntest=ntest,
|
||
)
|
||
return r
|
||
|
||
best = binary_search(
|
||
_run, 2, 3 * bstart, "b", predicate=lambda x, best: x["rop"] <= best["rop"] and (best["m"] > m or x["m"] <= m)
|
||
)
|
||
# binary search cannot fail. It just outputs some X with X["oracle"]>m.
|
||
if best["m"] > m:
|
||
raise InsufficientSamplesError("m=%d < %d (required)" % (m, best["m"]))
|
||
return best
|
||
|
||
|
||
# Algebraic
|
||
|
||
|
||
@cached_function
|
||
def have_magma():
|
||
try:
|
||
magma(1)
|
||
return True
|
||
except TypeError:
|
||
return False
|
||
|
||
|
||
def gb_cost(n, D, omega=2, prec=None):
|
||
"""
|
||
Estimate the complexity of computing a Gröbner basis.
|
||
|
||
:param n: number of variables `n > 0`
|
||
:param D: tuple of `(d,m)` pairs where `m` is number polynomials and `d` is a degree
|
||
:param omega: linear algebra exponent, i.e. matrix-multiplication costs `O(n^ω)` operations.
|
||
:param prec: compute power series up to this precision (default: `2n`)
|
||
|
||
"""
|
||
prec = 2*n if prec is None else prec
|
||
|
||
if have_magma():
|
||
R = magma.PowerSeriesRing(QQ, prec)
|
||
z = R.gen(1)
|
||
coeff = lambda f, d: f.Coefficient(d) # noqa
|
||
s = 1
|
||
else:
|
||
R = PowerSeriesRing(QQ, "z", prec)
|
||
z = R.gen()
|
||
z = z.add_bigoh(prec)
|
||
coeff = lambda f, d: f[d] # noqa
|
||
s = R(1)
|
||
s = s.add_bigoh(prec)
|
||
|
||
for d, m in D:
|
||
s *= (1 - z ** d) ** m
|
||
s /= (1 - z) ** n
|
||
|
||
retval = Cost([("rop", oo), ("Dreg", oo)])
|
||
|
||
for dreg in range(prec):
|
||
if coeff(s, dreg) < 0:
|
||
break
|
||
else:
|
||
return retval
|
||
|
||
retval["Dreg"] = dreg
|
||
retval["rop"] = binomial(n + dreg, dreg) ** omega
|
||
retval["mem"] = binomial(n + dreg, dreg) ** 2
|
||
|
||
return retval
|
||
|
||
|
||
def arora_gb(n, alpha, q, secret_distribution=True, m=oo, success_probability=0.99, omega=2):
|
||
"""
|
||
Arora-GB as described in [AroGe11,ACFP14]_
|
||
|
||
: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 secret_distribution: distribution of secret, see module level documentation for details
|
||
:param m: number of LWE samples `m > 0`
|
||
:param success_probability: targeted success probability < 1
|
||
:param omega: linear algebra constant
|
||
|
||
.. [ACFP14] Albrecht, M. R., Cid, C., Jean-Charles Faugère, & Perret, L. (2014).
|
||
Algebraic algorithms for LWE.
|
||
|
||
.. [AroGe11] Arora, S., & Ge, R. (2011). New algorithms for learning in presence of
|
||
errors. In L. Aceto, M. Henzinger, & J. Sgall, ICALP 2011, Part~I (pp. 403–415). :
|
||
Springer, Heidelberg.
|
||
"""
|
||
|
||
n, alpha, q, success_probability = Param.preprocess(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 {"rop": oo}
|
||
|
||
if SDis.is_small(secret_distribution, alpha=alpha):
|
||
try:
|
||
a, b = SDis.bounds(secret_distribution)
|
||
d2 = b - a + 1
|
||
except ValueError:
|
||
d2 = 2 * ceil(3 * stddev) + 1
|
||
d2 = [(d2, n)]
|
||
else:
|
||
d2 = []
|
||
|
||
ps_single = lambda C: RR(1 - (2 / (C * RR(sqrt(2 * pi))) * exp(-(C ** 2) / 2))) # noqa
|
||
|
||
m_req = floor(exp(RR(0.75) * n))
|
||
d = n
|
||
t = ZZ(floor((d - 1) / 2))
|
||
C = t / stddev
|
||
pred = gb_cost(n, [(d, m_req)] + d2, omega)
|
||
pred["t"] = t
|
||
pred["m"] = m_req
|
||
pred = pred.reorder(["rop", "m", "Dreg", "t"])
|
||
|
||
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_req = log(success_probability, 2) / log(ps_single(C), 2)
|
||
if m_req < n:
|
||
continue
|
||
m_req = floor(m_req)
|
||
|
||
current = gb_cost(n, [(d, m_req)] + d2, omega)
|
||
|
||
if current["Dreg"] is None:
|
||
continue
|
||
|
||
current["t"] = t
|
||
current["m"] = m_req
|
||
|
||
current = current.reorder(["rop", "m", "Dreg", "t"])
|
||
|
||
logging.getLogger("repeat").debug(current)
|
||
|
||
if best is None:
|
||
best = current
|
||
else:
|
||
if best["rop"] > current["rop"] and current["m"] <= m:
|
||
best = current
|
||
stuck = 0
|
||
else:
|
||
stuck += 1
|
||
if stuck >= 5:
|
||
break
|
||
return best
|
||
|
||
|
||
# Toplevel function
|
||
|
||
|
||
def estimate_lwe( # noqa
|
||
n,
|
||
alpha=None,
|
||
q=None,
|
||
secret_distribution=True,
|
||
m=oo,
|
||
reduction_cost_model=reduction_default_cost,
|
||
skip=("mitm", "arora-gb", "bkw"),
|
||
):
|
||
"""
|
||
Highlevel-function for estimating security of LWE parameter sets
|
||
|
||
: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, see module level documentation for details
|
||
:param reduction_cost_model: use this cost model for lattice reduction
|
||
:param skip: skip these algorithms
|
||
|
||
EXAMPLE::
|
||
|
||
sage: from estimator import estimate_lwe, Param, BKZ, alphaf
|
||
sage: d = estimate_lwe(*Param.Regev(128))
|
||
usvp: rop: ≈2^57.3, red: ≈2^57.3, δ_0: 1.009214, β: 101, d: 349, m: 220
|
||
dec: rop: ≈2^61.9, m: 229, red: ≈2^61.9, δ_0: 1.009595, β: 93, d: 357, ...
|
||
dual: rop: ≈2^81.1, m: 380, red: ≈2^81.1, δ_0: 1.008631, β: 115, d: 380, ...
|
||
|
||
sage: d = estimate_lwe(**Param.LindnerPeikert(256, dict=True))
|
||
usvp: rop: ≈2^127.8, red: ≈2^127.8, δ_0: 1.005788, β: 228, d: 588, m: 331
|
||
dec: rop: ≈2^137.3, m: 337, red: ≈2^137.3, δ_0: 1.005976, β: 217, d: 593, ...
|
||
dual: rop: ≈2^166.0, m: 368, red: ≈2^166.0, δ_0: 1.005479, β: 249, repeat: ...
|
||
|
||
sage: d = estimate_lwe(*Param.LindnerPeikert(256), secret_distribution=(-1,1))
|
||
Warning: the LWE secret is assumed to have Hamming weight 171.
|
||
usvp: rop: ≈2^98.0, red: ≈2^98.0, δ_0: 1.006744, β: 178, d: 499, m: 242, ...
|
||
dec: rop: ≈2^137.3, m: 337, red: ≈2^137.3, δ_0: 1.005976, β: 217, d: 593, ...
|
||
dual: rop: ≈2^108.4, m: 270, red: ≈2^108.3, δ_0: 1.006390, β: 195, repeat: ...
|
||
|
||
sage: d = estimate_lwe(*Param.LindnerPeikert(256), secret_distribution=(-1,1), reduction_cost_model=BKZ.sieve)
|
||
Warning: the LWE secret is assumed to have Hamming weight 171.
|
||
usvp: rop: ≈2^80.3, red: ≈2^80.3, δ_0: 1.006744, β: 178, d: 499, m: 242, ...
|
||
dec: rop: ≈2^111.8, m: 369, red: ≈2^111.8, δ_0: 1.005423, β: 253, d: 625, ...
|
||
dual: rop: ≈2^90.6, m: 284, red: ≈2^90.6, δ_0: 1.006065, β: 212, ...
|
||
|
||
sage: d = estimate_lwe(n=100, alpha=8/2^20, q=2^20, skip="arora-gb")
|
||
mitm: rop: ≈2^329.2, m: 23, mem: ≈2^321.5
|
||
usvp: rop: ≈2^32.4, red: ≈2^32.4, δ_0: 1.013310, β: 40, d: 141, m: 40
|
||
dec: rop: ≈2^34.0, m: 156, red: ≈2^34.0, δ_0: 1.021398, β: 40, d: 256, ...
|
||
dual: rop: ≈2^35.5, m: 311, red: ≈2^35.5, δ_0: 1.014423, β: 40, d: 311, ...
|
||
bkw: rop: ≈2^53.6, m: ≈2^43.5, mem: ≈2^44.5, b: 2, t1: 5, t2: 18, l: 1, ...
|
||
|
||
sage: d = estimate_lwe(n=100, secret_distribution=alphaf(1, 2^20, True), alpha=8/2^20, q=2^20)
|
||
usvp: rop: ≈2^32.2, red: ≈2^32.2, δ_0: 1.013310, β: 40, d: 127, m: 26
|
||
dec: rop: ≈2^34.0, m: 156, red: ≈2^34.0, δ_0: 1.021398, β: 40, d: ...
|
||
dual: rop: ≈2^35.5, m: 311, red: ≈2^35.5, δ_0: 1.014423, β: 40, d: ...
|
||
|
||
|
||
"""
|
||
|
||
algorithms = OrderedDict()
|
||
|
||
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]
|
||
|
||
if "mitm" not in skip:
|
||
algorithms["mitm"] = mitm
|
||
|
||
if "usvp" not in skip:
|
||
if SDis.is_bounded_uniform(secret_distribution):
|
||
algorithms["usvp"] = partial(
|
||
drop_and_solve,
|
||
primal_usvp,
|
||
reduction_cost_model=reduction_cost_model,
|
||
postprocess=False,
|
||
decision=False,
|
||
)
|
||
else:
|
||
algorithms["usvp"] = partial(primal_usvp, reduction_cost_model=reduction_cost_model)
|
||
|
||
if "dec" not in skip:
|
||
if SDis.is_sparse(secret_distribution) and SDis.is_ternary(secret_distribution):
|
||
algorithms["dec"] = partial(
|
||
drop_and_solve,
|
||
primal_decode,
|
||
reduction_cost_model=reduction_cost_model,
|
||
postprocess=False,
|
||
decision=False,
|
||
)
|
||
else:
|
||
algorithms["dec"] = partial(primal_decode, reduction_cost_model=reduction_cost_model)
|
||
|
||
if "dual" not in skip:
|
||
if SDis.is_bounded_uniform(secret_distribution):
|
||
algorithms["dual"] = partial(
|
||
drop_and_solve, dual_scale, reduction_cost_model=reduction_cost_model, postprocess=True
|
||
)
|
||
elif SDis.is_small(secret_distribution, alpha=alpha):
|
||
algorithms["dual"] = partial(dual_scale, reduction_cost_model=reduction_cost_model)
|
||
else:
|
||
algorithms["dual"] = partial(dual, reduction_cost_model=reduction_cost_model)
|
||
|
||
if "bkw" not in skip:
|
||
algorithms["bkw"] = bkw_coded
|
||
|
||
if "arora-gb" not in skip:
|
||
if SDis.is_sparse(secret_distribution) and SDis.is_small(secret_distribution, alpha=alpha):
|
||
algorithms["arora-gb"] = partial(drop_and_solve, arora_gb)
|
||
elif SDis.is_small(secret_distribution, alpha=alpha):
|
||
algorithms["arora-gb"] = partial(switch_modulus, arora_gb)
|
||
else:
|
||
algorithms["arora-gb"] = arora_gb
|
||
|
||
alg_width = max(len(key) for key in set(algorithms).difference(skip))
|
||
cost_kwds = {"compact": False}
|
||
|
||
logger = logging.getLogger("estimator")
|
||
|
||
if SDis.is_bounded_uniform(secret_distribution) and not SDis.is_sparse(secret_distribution):
|
||
h = SDis.nonzero(secret_distribution, n)
|
||
logger.info("Warning: the LWE secret is assumed to have Hamming weight %s." % h)
|
||
|
||
results = OrderedDict()
|
||
for alg in algorithms:
|
||
algf = algorithms[alg]
|
||
try:
|
||
tmp = algf(n, alpha, q, secret_distribution=secret_distribution, m=m)
|
||
results[alg] = tmp
|
||
logger.info("%s: %s" % (("%%%ds" % alg_width) % alg, results[alg].str(**cost_kwds)))
|
||
except InsufficientSamplesError as e:
|
||
logger.info(
|
||
"%s: %s" % (("%%%ds" % alg_width) % alg, "insufficient samples to run this algorithm: '%s'" % str(e))
|
||
)
|
||
|
||
return results
|