mirror of
https://github.com/zama-ai/concrete.git
synced 2026-02-15 07:05:09 -05:00
600 lines
20 KiB
Python
600 lines
20 KiB
Python
import estimator.estimator as est
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
|
|
# define the four cost models used for Concrete (2 classical, 2 quantum)
|
|
# note that classical and quantum are the two models used in the "HE Std"
|
|
|
|
def classical(beta, d, B):
|
|
return ZZ(2) ** RR(0.292 * beta + 16.4 + log(8 * d, 2))
|
|
|
|
|
|
def quantum(beta, d, B):
|
|
return ZZ(2) ** RR(0.265 * beta + 16.4 + log(8 * d, 2))
|
|
|
|
|
|
def classical_conservative(beta, d, B):
|
|
return ZZ(2) ** RR(0.292 * beta)
|
|
|
|
|
|
def quantum_conservative(beta, d, B):
|
|
return ZZ(2) ** RR(0.265 * beta)
|
|
|
|
# we add an enumeration model for completeness
|
|
cost_models = [classical, quantum, classical_conservative, quantum_conservative, est.BKZ.enum]
|
|
|
|
|
|
def estimate_lwe_nocrash(n, alpha, q, secret_distribution,
|
|
reduction_cost_model=est.BKZ.sieve, m=oo):
|
|
"""
|
|
A function to estimate the complexity of LWE, whilst skipping over any attacks which crash
|
|
:param n : the LWE dimension
|
|
:param alpha : the noise rate of the error
|
|
:param q : the LWE ciphertext modulus
|
|
:param secret_distribution : the LWE secret distribution
|
|
:param reduction_cost_model: the BKZ reduction cost model
|
|
:param m : the number of available LWE samples
|
|
|
|
EXAMPLE:
|
|
sage: estimate_lwe_nocrash(n = 256, q = 2**32, alpha = RR(8/2**32), secret_distribution = (0,1))
|
|
sage: 39.46
|
|
"""
|
|
|
|
# the success value denotes whether we need to re-run the estimator, in the case of a crash
|
|
success = 0
|
|
|
|
try:
|
|
# we begin by trying all four attacks (usvp, dual, dec, mitm)
|
|
estimate = est.estimate_lwe(n, alpha, q, secret_distribution=secret_distribution,
|
|
reduction_cost_model=reduction_cost_model, m=oo,
|
|
skip={"bkw", "dec", "arora-gb"})
|
|
success = 1
|
|
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
if success == 0:
|
|
try:
|
|
# dual crashes most often, so try skipping dual first
|
|
estimate = est.estimate_lwe(n, alpha, q, secret_distribution=secret_distribution,
|
|
reduction_cost_model=reduction_cost_model, m=oo,
|
|
skip={"bkw", "dec", "arora-gb", "dual"})
|
|
success = 1
|
|
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
if success == 0:
|
|
try:
|
|
# next, skip mitm
|
|
estimate = est.estimate_lwe(n, alpha, q, secret_distribution=secret_distribution,
|
|
reduction_cost_model=reduction_cost_model, m=oo,
|
|
skip={"mitm", "bkw", "dec", "arora-gb", "dual"})
|
|
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
# the output security level is just the cost of the fastest attack
|
|
security_level = get_security_level(estimate)
|
|
|
|
return security_level
|
|
|
|
|
|
|
|
def get_security_level(estimate, decimal_places = 2):
|
|
""" Function to get the security level from an LWE Estimator output,
|
|
i.e. returns only the bit-security level (without the attack params)
|
|
:param estimate: the input estimate
|
|
:param decimal_places: the number of decimal places
|
|
|
|
EXAMPLE:
|
|
sage: x = estimate_lwe(n = 256, q = 2**32, alpha = RR(8/2**32))
|
|
sage: get_security_level(x)
|
|
33.8016789754458
|
|
"""
|
|
|
|
levels = []
|
|
|
|
# use try/except to cover cases where we only consider one or two attacks
|
|
|
|
try:
|
|
levels.append(estimate["usvp"]["rop"])
|
|
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
levels.append(estimate["dec"]["rop"])
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
levels.append(estimate["dual"]["rop"])
|
|
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
levels.append(estimate["mitm"]["rop"])
|
|
|
|
except:
|
|
pass
|
|
|
|
# take the minimum attack cost (in bits)
|
|
security_level = round(log(min(levels), 2), decimal_places)
|
|
|
|
return security_level
|
|
|
|
|
|
def inequality(x, y):
|
|
""" A function which compresses the conditions
|
|
x < y and x > y into a single condition via a
|
|
multiplier
|
|
"""
|
|
if x <= y:
|
|
return 1
|
|
|
|
if x > y:
|
|
return -1
|
|
|
|
|
|
def automated_param_select_n(sd, n=None, q=2 ** 32, reduction_cost_model=est.BKZ.sieve, secret_distribution=(0, 1),
|
|
target_security=128):
|
|
""" A function used to generate the smallest value of n which allows for
|
|
target_security bits of security, for the input values of (sd,q)
|
|
:param sd: the standard deviation of the error
|
|
:param n: an initial value of n to use in optimisation, guessed if None
|
|
:param q: the LWE modulus (q = 2**32, 2**64 in TFHE)
|
|
:param reduction_cost_model: the BKZ cost model considered, BKZ.sieve is default
|
|
:param secret_distribution: the LWE secret distribution
|
|
:param target_security: the target number of bits of security, 128 is default
|
|
|
|
EXAMPLE:
|
|
sage: X = automated_param_select_n(sd = -25, q = 2**32)
|
|
sage: X
|
|
1054
|
|
"""
|
|
|
|
if n is None:
|
|
# pick some random n which gets us close (based on concrete_LWE_params)
|
|
n = sd * (-25) * (target_security/80)
|
|
|
|
sd = 2 ** sd * q
|
|
alpha = sqrt(2 * pi) * sd / RR(q)
|
|
|
|
|
|
# initial estimate, to determine if we are above or below the target security level
|
|
|
|
security_level = estimate_lwe_nocrash(n, alpha, q, secret_distribution=secret_distribution,
|
|
reduction_cost_model=reduction_cost_model, m=oo)
|
|
|
|
z = inequality(security_level, target_security)
|
|
|
|
while z * security_level < z * target_security and n > 80:
|
|
n += z * 8
|
|
alpha = sqrt(2 * pi) * sd / RR(q)
|
|
security_level = estimate_lwe_nocrash(n, alpha, q, secret_distribution=secret_distribution,
|
|
reduction_cost_model=reduction_cost_model, m=oo)
|
|
|
|
if (-1 * sd > 0):
|
|
print("target security level is unatainable")
|
|
break
|
|
|
|
# final estimate (we went too far in the above loop)
|
|
if security_level < target_security:
|
|
n -= z * 8
|
|
alpha = sqrt(2 * pi) * sd / RR(q)
|
|
security_level = estimate_lwe_nocrash(n, alpha, q, secret_distribution=secret_distribution,
|
|
reduction_cost_model=reduction_cost_model, m=oo)
|
|
|
|
print("the finalised parameters are n = {}, log2(sd) = {}, log2(q) = {}, with a security level of {}-bits".format(n,
|
|
sd,
|
|
log(q,
|
|
2),
|
|
security_level))
|
|
|
|
# final sanity check so we don't return insecure (or inf) parameters
|
|
if security_level < target_security or security_level == oo:
|
|
n = None
|
|
|
|
return n
|
|
|
|
|
|
def automated_param_select_sd(n, sd=None, q=2**32, reduction_cost_model=est.BKZ.sieve, secret_distribution=(0, 1),
|
|
target_security=128):
|
|
""" A function used to generate the smallest value of sd which allows for
|
|
target_security bits of security, for the input values of (n,q)
|
|
:param n: the LWE dimension
|
|
:param sd: an initial value of sd to use in optimisation, guessed if None
|
|
:param q: the LWE modulus (q = 2**32, 2**64 in TFHE)
|
|
:param reduction_cost_model: the BKZ cost model considered, BKZ.sieve is default
|
|
:param secret_distribution: the LWE secret distribution
|
|
:param target_security: the target number of bits of security, 128 is default
|
|
|
|
EXAMPLE
|
|
sage: X = automated_param_select_sd(n = 1054, q = 2**32)
|
|
sage: X
|
|
-26
|
|
"""
|
|
|
|
if sd is None:
|
|
# pick some random sd which gets us close (based on concrete_LWE_params)
|
|
sd = round(n * 80 / (target_security * (-25)))
|
|
|
|
# make sure sd satisfies q * sd > 1
|
|
sd = max(sd, -(log(q,2) - 2))
|
|
|
|
sd_ = (2 ** sd) * q
|
|
alpha = sqrt(2 * pi) * sd_ / RR(q)
|
|
|
|
# initial estimate, to determine if we are above or below the target security level
|
|
security_level = estimate_lwe_nocrash(n, alpha, q, secret_distribution=secret_distribution,
|
|
reduction_cost_model=reduction_cost_model, m=oo)
|
|
z = inequality(security_level, target_security)
|
|
|
|
while z * security_level < z * target_security and sd > -log(q,2):
|
|
|
|
sd += z * (0.5)
|
|
sd_ = (2 ** sd) * q
|
|
alpha = sqrt(2 * pi) * sd_ / RR(q)
|
|
security_level = estimate_lwe_nocrash(n, alpha, q, secret_distribution=secret_distribution,
|
|
reduction_cost_model=reduction_cost_model, m=oo)
|
|
|
|
## THIS IS WHERE THE PROBLEM IS, CORRECT THIS CONDITION?
|
|
if (sd > log(q, 2)):
|
|
print("target security level is unatainable")
|
|
return None
|
|
|
|
# final estimate (we went too far in the above loop)
|
|
if security_level < target_security:
|
|
sd -= z * (0.5)
|
|
sd_ = (2 ** sd) * q
|
|
alpha = sqrt(2 * pi) * sd_ / RR(q)
|
|
security_level = estimate_lwe_nocrash(n, alpha, q, secret_distribution=secret_distribution,
|
|
reduction_cost_model=reduction_cost_model, m=oo)
|
|
|
|
print("the finalised parameters are n = {}, log2(sd) = {}, log2(q) = {}, with a security level of {}-bits".format(n,
|
|
sd,
|
|
log(q,
|
|
2),
|
|
security_level))
|
|
|
|
return sd
|
|
|
|
|
|
def generate_parameter_matrix(n_range, sd=None, q=2**32, reduction_cost_model=est.BKZ.sieve,
|
|
secret_distribution=(0, 1), target_security=128):
|
|
"""
|
|
:param n_range: a tuple (n_min, n_max) giving the values of n for which to generate parameters
|
|
:param sd: the standard deviation of the LWE error
|
|
:param q: the LWE modulus (q = 2**32, 2**64 in TFHE)
|
|
:param reduction_cost_model: the BKZ cost model considered, BKZ.sieve is default
|
|
:param secret_distribution: the LWE secret distribution
|
|
:param target_security: the target number of bits of security, 128 is default
|
|
|
|
TODO: we should probably parallelise this function for speed
|
|
|
|
EXAMPLE:
|
|
sage: X = generate_parameter_matrix([788, 790])
|
|
sage: X
|
|
[(788, 4294967296, -20.0), (789, 4294967296, -20.0)]
|
|
"""
|
|
|
|
RESULTS = []
|
|
|
|
# grab min and max value/s of n, with a granularity (if given as input)
|
|
try:
|
|
(n_min, n_max, gran) = n_range
|
|
except:
|
|
(n_min, n_max) = n_range
|
|
gran = 1
|
|
|
|
sd_ = sd
|
|
|
|
for n in range(n_min, n_max + 1, gran):
|
|
sd = automated_param_select_sd(n, sd=sd_, q=q, reduction_cost_model=reduction_cost_model,
|
|
secret_distribution=secret_distribution, target_security=target_security)
|
|
sd_ = sd
|
|
RESULTS.append((n, q, sd))
|
|
|
|
return RESULTS
|
|
|
|
|
|
def generate_parameter_matrix_sd(sd_range, n=None, q=2**32, reduction_cost_model=est.BKZ.sieve,
|
|
secret_distribution=(0, 1), target_security=128):
|
|
"""
|
|
:param sd_range: a tuple (sd_min, sd_max) giving the values of sd for which to generate parameters
|
|
:param sd: the standard deviation of the LWE error
|
|
:param q: the LWE modulus (q = 2**32, 2**64 in TFHE)
|
|
:param reduction_cost_model: the BKZ cost model considered, BKZ.sieve is default
|
|
:param secret_distribution: the LWE secret distribution
|
|
:param target_security: the target number of bits of security, 128 is default
|
|
|
|
TODO: we should probably parallelise this function for speed
|
|
|
|
EXAMPLE:
|
|
sage: X = generate_parameter_matrix([788, 790])
|
|
sage: X
|
|
[(788, 4294967296, -20.0), (789, 4294967296, -20.0)]
|
|
"""
|
|
|
|
RESULTS = []
|
|
|
|
# grab min and max value/s of n
|
|
(sd_min, sd_max) = sd_range
|
|
|
|
n = n
|
|
|
|
for sd in range(sd_min, sd_max + 1):
|
|
n = automated_param_select_n(sd, n=n, q=q, reduction_cost_model=reduction_cost_model,
|
|
secret_distribution=secret_distribution, target_security=target_security)
|
|
RESULTS.append((n, q, sd))
|
|
|
|
return RESULTS
|
|
|
|
|
|
def generate_parameter_step(results, label = None, torus_sd = True):
|
|
"""
|
|
Plot results
|
|
:param results: an output of generate_parameter_matrix
|
|
|
|
returns: a step plot of chosen parameters
|
|
|
|
EXAMPLE:
|
|
X = generate_parameter_matrix([700, 790])
|
|
generate_parameter_step(X)
|
|
plt.show()
|
|
"""
|
|
|
|
N = []
|
|
SD = []
|
|
|
|
for (n, q, sd) in results:
|
|
N.append(n)
|
|
if torus_sd:
|
|
SD.append(sd)
|
|
else:
|
|
SD.append(sd + log(q,2))
|
|
|
|
plt.plot(N, SD, label = label)
|
|
plt.legend(loc = "upper right")
|
|
|
|
return plt
|
|
|
|
|
|
def generate_iso_lines(N = [256, 2048], SD = [0, 32], q = 2**32):
|
|
|
|
RESULTS = []
|
|
|
|
for n in range(N[0], N[1] + 1, 1):
|
|
for sd in range(SD[0], SD[1] + 1, 1):
|
|
sd = 2**sd
|
|
alpha = sqrt(2*pi) * sd / q
|
|
try:
|
|
est = est.estimate_lwe(n, alpha, q, secret_distribution = (0,1), reduction_cost_model = est.BKZ.sieve, skip = ("bkw", "arora-gb", "dec"))
|
|
est = get_security_level(est, 2)
|
|
except:
|
|
est = est.estimate_lwe(n, alpha, q, secret_distribution = (0,1), reduction_cost_model = est.BKZ.sieve, skip = ("bkw", "arora-gb", "dual", "dec"))
|
|
est = get_security_level(est, 2)
|
|
RESULTS.append((n, sd, est))
|
|
|
|
return RESULTS
|
|
|
|
|
|
def plot_iso_lines(results):
|
|
|
|
x1 = []
|
|
x2 = []
|
|
|
|
for z in results:
|
|
x1.append(z[0])
|
|
# use log(q)
|
|
# use -ve values to match Pascal's diagram
|
|
x2.append(z[2])
|
|
|
|
plt.plot(x1, x2)
|
|
|
|
return plt
|
|
|
|
|
|
def test_multiple_sd(n, q, secret_distribution, reduction_cost_model, split = 33, m = oo):
|
|
est = []
|
|
Y = []
|
|
for sd_ in np.linspace(0,32,split):
|
|
Y.append(sd_)
|
|
sd = (2** (-1 * sd_))* q
|
|
alpha = sqrt(2*pi) * sd / q
|
|
try:
|
|
es = est.estimate_lwe(n=512, alpha=alpha, q=q, secret_distribution=(0, 1), reduction_cost_model = reduction_cost_model,
|
|
skip=("bkw", "dec", "arora-gb"), m = m)
|
|
except:
|
|
es = est.estimate_lwe(n=512, alpha=alpha, q=q, secret_distribution=(0, 1), reduction_cost_model = reduction_cost_model,
|
|
skip=("bkw", "dec", "arora-gb", "dual"), m = m)
|
|
est.append(get_security_level(es,2))
|
|
|
|
return est, Y
|
|
|
|
|
|
## parameter curves
|
|
|
|
def get_parameter_curves_data_sd(sec_levels, sd_range, q):
|
|
|
|
Results = []
|
|
for sec in sec_levels:
|
|
try:
|
|
result_sec = generate_parameter_matrix_sd(n = None, sd_range=sd_range, q=q, reduction_cost_model=est.BKZ.sieve,
|
|
secret_distribution=(0,1), target_security=sec)
|
|
Results.append(result_sec)
|
|
except:
|
|
pass
|
|
|
|
return Results
|
|
|
|
|
|
def get_parameter_curves_data_n(sec_levels, n_range, q):
|
|
|
|
Results = []
|
|
for sec in sec_levels:
|
|
try:
|
|
result_sec = generate_parameter_matrix(n_range, sd = None, q=q, reduction_cost_model=est.BKZ.sieve,
|
|
secret_distribution=(0,1), target_security=sec)
|
|
Results.append(result_sec)
|
|
except:
|
|
pass
|
|
|
|
return Results
|
|
|
|
|
|
def interpolate_result(result, log_q):
|
|
|
|
# linear function interpolation
|
|
x = []
|
|
y = []
|
|
|
|
# 1. filter out any points which reccomend sd = -log(q) + 2
|
|
new_result= []
|
|
for res in result:
|
|
if res[2] >= - log_q + 2:
|
|
new_result.append(res)
|
|
|
|
result = new_result
|
|
for res in result:
|
|
x.append(res[0])
|
|
y.append(res[2])
|
|
|
|
|
|
(a,b) = np.polyfit(x, y, 1)
|
|
|
|
return (a,b)
|
|
|
|
|
|
def plot_interpolants(interpolants, n_range, log_q, degree = 1):
|
|
for x in interpolants:
|
|
if degree == 1:
|
|
vals = [x[0] * n + x[1] for n in range(n_range[0],n_range[1])]
|
|
elif degree == 2:
|
|
vals = [x[0] * n**2 + x[1]*n + x[2] for n in range(n_range[0],n_range[1])]
|
|
# any values which fall outside of the range and edited to give at least two bits of noise.
|
|
|
|
vvals = []
|
|
for v in vals:
|
|
if v < -log_q + 2:
|
|
vvals.append(-log_q + 2)
|
|
else:
|
|
vvals.append(v)
|
|
|
|
plt.plot(range(n_range[0], n_range[0] + len(vvals)), vvals)
|
|
|
|
return 0
|
|
|
|
|
|
## currently running
|
|
# sage: n_range = (256, 2048, 16)
|
|
# sage: sec_levels = [80 + 16*k for k in range(0,12)]
|
|
# sage: results = get_parameter_curves_data_n(sec_levels, n_range, q = 2**64)
|
|
|
|
def verify_results(results, security_level, secret_distribution = (0,1), reduction_cost_model = est.BKZ.sieve):
|
|
""" A function which verifies that a set of results match a given security level
|
|
:param results : a set of tuples of the form (n, q, sd)
|
|
:param security_level: the target security level for these params
|
|
"""
|
|
|
|
estimates = []
|
|
|
|
# 1. Grab the parameters
|
|
for (n, q, sd) in results:
|
|
if sd is not None:
|
|
sd = 2**sd
|
|
alpha = sqrt(2*pi) * sd
|
|
|
|
# 2. Test that these parameters satisfy the given security level
|
|
try:
|
|
estimate = est.estimate_lwe(n, alpha, q, secret_distribution=secret_distribution,
|
|
reduction_cost_model=reduction_cost_model, m=oo, skip = {"bkw","dec","arora-gb"})
|
|
except:
|
|
estimate = est.estimate_lwe(n, alpha, q, secret_distribution=secret_distribution,
|
|
reduction_cost_model=reduction_cost_model, m=oo,
|
|
skip={"bkw", "dec", "arora-gb", "dual"})
|
|
|
|
estimates.append(estimate)
|
|
|
|
return estimates
|
|
|
|
|
|
def verify_interpolants(interpolant, n_range, log_q, secret_distribution = (0,1), reduction_cost_model = est.BKZ.sieve):
|
|
|
|
estimates = []
|
|
q = 2**log_q
|
|
(a, b) = interpolant
|
|
|
|
for n in range(n_range[0], n_range[1]):
|
|
print(n)
|
|
# we take the max here to ensure that the "cut-off" for the minimal error is met.
|
|
sd = max(a * n + b, -log_q + 2)
|
|
sd = 2 ** sd
|
|
alpha = sqrt(2*pi) * sd
|
|
|
|
security_level = estimate_lwe_nocrash(n, alpha, q, secret_distribution=secret_distribution,
|
|
reduction_cost_model=reduction_cost_model, m=oo)
|
|
print(security_level)
|
|
if security_level == oo:
|
|
security_level = 0
|
|
estimates.append(security_level)
|
|
|
|
return estimates
|
|
|
|
def get_zama_curves():
|
|
|
|
# hardcode the parameters for now
|
|
n_range = [128, 2048, 32]
|
|
sec_levels = [80 + 16*k for k in range(0,12)]
|
|
results = get_parameter_curves_data_n(sec_levels, n_range, q = 2**64)
|
|
|
|
return results
|
|
|
|
|
|
def test_curves():
|
|
|
|
# a small hardcoded example for testing purposes
|
|
|
|
n_range = [256, 1024, 128]
|
|
sec_levels = [80, 128, 256]
|
|
results = get_parameter_curves_data_n(sec_levels, n_range, q = 2**64)
|
|
|
|
return results
|
|
|
|
def find_nalpha(l, sec_lvl):
|
|
for j in range(len(l)):
|
|
if l[j] != oo and l[j] > sec_lvl:
|
|
return j
|
|
|
|
|
|
## we start with 80/128/192/256-bits of security
|
|
|
|
## lambda = 80
|
|
## z = verify_interpolants(interps[0], (128,2048), 64)
|
|
## i = 0
|
|
## min(z[i:]) = 80.36
|
|
## so the model is sd(n) = max(-0.04047677865612648 * n + 1.1433465085639063, log_q - 2), n >= 128
|
|
|
|
|
|
## lambda = 128
|
|
## z = verify_interpolants(interps[3], (128,2048), 64)
|
|
## i = 83
|
|
## min(z[i:]) = 128.02
|
|
## so the model is sd(n) = max(-0.026374888765705498 * n + 2.012143923330495, log_q - 2), n >= 211 ( = 128 + 83)
|
|
|
|
|
|
## lambda = 192
|
|
## z = verify_interpolants(interps[7], (128,2048), 64)
|
|
## i = 304
|
|
## min(z[i:]) = 192.19
|
|
## so the model is sd(n) = max(-0.018504919354426233 * n + 2.6634073426215843, log_q - 2), n >= 432 ( = 128 + 212)
|
|
|
|
|
|
## lambda = 256
|
|
## z = verify_interpolants(interps[-1], (128,2048), 64)
|
|
## i = 653
|
|
## min(z[i:]) = 256.25
|
|
## so the model is sd(n) = max(-0.014327640360322604 * n + 2.899270827311091), log_q - 2), n >= 781 ( = 128 + 653)
|