def __init__(self, n): """Initialize a secret key.""" # Public parameters self.n = n self.sigma = Params[n]["sigma"] self.sigmin = Params[n]["sigmin"] self.signature_bound = Params[n]["sig_bound"] self.sig_bytelen = Params[n]["sig_bytelen"] # Compute NTRU polynomials f, g, F, G verifying fG - gF = q mod Phi self.f, self.g, self.F, self.G = ntru_gen(n) # From f, g, F, G, compute the basis B0 of a NTRU lattice # as well as its Gram matrix and their fft's. B0 = [[self.g, neg(self.f)], [self.G, neg(self.F)]] G0 = gram(B0) self.B0_fft = [[fft(elt) for elt in row] for row in B0] G0_fft = [[fft(elt) for elt in row] for row in G0] self.T_fft = ffldl_fft(G0_fft) # Normalize Falcon tree normalize_tree(self.T_fft, self.sigma) # The public key is a polynomial such that h*f = g mod (Phi,q) self.h = div_zq(self.g, self.f)
def __init__(self, n): """Initialize a secret key.""" """Public parameters""" self.n = n self.q = q self.hash_function = hashlib.shake_256 """Private key part 1: NTRU polynomials f, g, F, G verifying fG - gF = q mod Phi""" self.f, self.g, self.F, self.G = ntru_gen(n) """Private key part 2: fft's of f, g, F, G""" self.f_fft = fft(self.f) self.g_fft = fft(self.g) self.F_fft = fft(self.F) self.G_fft = fft(self.G) """Private key part 3: from f, g, F, G, compute the basis B0 of a NTRU lattice as well as its Gram matrix and their fft's""" self.B0 = [[self.g, neg(self.f)], [self.G, neg(self.F)]] self.G0 = gram(self.B0) self.B0_fft = [[fft(elt) for elt in row] for row in self.B0] self.G0_fft = [[fft(elt) for elt in row] for row in self.G0] # self.T = ffldl(self.G0) self.T_fft = ffldl_fft(self.G0_fft) """Private key part 4: compute sigma and signature bound.""" sq_gs_norm = gs_norm(self.f, self.g, q) self.sigma = 1.28 * sqrt(sq_gs_norm) self.signature_bound = 2 * self.n * (self.sigma**2) """Private key part 5: set leaves of tree to be the standard deviations.""" normalize_tree(self.T_fft, self.sigma) """Public key: h such that h*f = g mod (Phi,q)""" self.h = div_zq(self.g, self.f)
def verify_1(n, m, sig, uid, pk, MPK): print("sig: {}".format(sig)) # parsing sig e = sig[0] z = sig[1] z1, z2, z1a, z2a = z[0], z[1], z[2], z[3] # restore e vev1 = add_zq(add_zq(z1, mul_zq(z2, MPK)), mul_zq(uid, neg(e))) vec2 = add_zq(add_zq(z1a, mul_zq(z2a, MPK)), mul_zq(pk, neg(e))) e_check = H1(n, vev1, vec2, m) #print('e from sig: {}'.format(e)) #print('e resttored: {}'.format(e_check)) return e == e_check
def sample_preimage(self, point): """ Sample a short vector s such that s[0] + s[1] * h = point. """ [[a, b], [c, d]] = self.B0_fft # We compute a vector t_fft such that: # (fft(point), fft(0)) * B0_fft = t_fft # Because fft(0) = 0 and the inverse of B has a very specific form, # we can do several optimizations. point_fft = fft(point) t0_fft = [(point_fft[i] * d[i]) / q for i in range(self.n)] t1_fft = [(-point_fft[i] * b[i]) / q for i in range(self.n)] t_fft = [t0_fft, t1_fft] # We now compute v such that: # v = z * B0 for an integral vector z # v is close to (point, 0) z_fft = ffsampling_fft(t_fft, self.T_fft, self.sigmin) v0_fft = add_fft(mul_fft(z_fft[0], a), mul_fft(z_fft[1], c)) v1_fft = add_fft(mul_fft(z_fft[0], b), mul_fft(z_fft[1], d)) v0 = [int(round(elt)) for elt in ifft(v0_fft)] v1 = [int(round(elt)) for elt in ifft(v1_fft)] # The difference s = (point, 0) - v is such that: # s is short # s[0] + s[1] * h = point s = [sub(point, v0), neg(v1)] return s
def test_ffnp(n, iterations): """Test ffnp. This functions check that: 1. the two versions (coefficient and FFT embeddings) of ffnp are consistent 2. ffnp output lattice vectors close to the targets. """ f = sign_KAT[n][0]["f"] g = sign_KAT[n][0]["g"] F = sign_KAT[n][0]["F"] G = sign_KAT[n][0]["G"] B = [[g, neg(f)], [G, neg(F)]] G0 = gram(B) G0_fft = [[fft(elt) for elt in row] for row in G0] T = ffldl(G0) T_fft = ffldl_fft(G0_fft) sqgsnorm = gs_norm(f, g, q) m = 0 for i in range(iterations): t = [[random() for i in range(n)], [random() for i in range(n)]] t_fft = [fft(elt) for elt in t] z = ffnp(t, T) z_fft = ffnp_fft(t_fft, T_fft) zb = [ifft(elt) for elt in z_fft] zb = [[round(coef) for coef in elt] for elt in zb] if z != zb: print("ffnp and ffnp_fft are not consistent") return False diff = [sub(t[0], z[0]), sub(t[1], z[1])] diffB = vecmatmul(diff, B) norm_zmc = int(round(sqnorm(diffB))) m = max(m, norm_zmc) th_bound = (n / 4.) * sqgsnorm if m > th_bound: print("Warning: ffnp does not output vectors as short as expected") return False else: return True
def sample_preimage(self, point, seed=None): """ Sample a short vector s such that s[0] + s[1] * h = point. """ [[a, b], [c, d]] = self.B0_fft # We compute a vector t_fft such that: # (fft(point), fft(0)) * B0_fft = t_fft # Because fft(0) = 0 and the inverse of B has a very specific form, # we can do several optimizations. point_fft = fft(point) t0_fft = [(point_fft[i] * d[i]) / q for i in range(self.n)] t1_fft = [(-point_fft[i] * b[i]) / q for i in range(self.n)] t_fft = [t0_fft, t1_fft] # We now compute v such that: # v = z * B0 for an integral vector z # v is close to (point, 0) if seed is None: # If no seed is defined, use urandom as the pseudo-random source. z_fft = ffsampling_fft(t_fft, self.T_fft, self.sigmin, urandom) else: # If a seed is defined, initialize a ChaCha20 PRG # that is used to generate pseudo-randomness. chacha_prng = ChaCha20(seed) z_fft = ffsampling_fft(t_fft, self.T_fft, self.sigmin, chacha_prng.randombytes) v0_fft = add_fft(mul_fft(z_fft[0], a), mul_fft(z_fft[1], c)) v1_fft = add_fft(mul_fft(z_fft[0], b), mul_fft(z_fft[1], d)) v0 = [int(round(elt)) for elt in ifft(v0_fft)] v1 = [int(round(elt)) for elt in ifft(v1_fft)] # The difference s = (point, 0) - v is such that: # s is short # s[0] + s[1] * h = point s = [sub(point, v0), neg(v1)] return s
def module_ntru_gen(d, q, m): """ Take as input system parameters, and output two "module-NTRU" matrices A and B such that: - B * A = 0 [mod q] - B has small polynomials - A is in Hermite normal form Also compute the inverse of B (over the field K = Q[x] / (x ** d + 1)). Input: d The degree of the underlying ring R = Z[x] / (x ** d + 1) q An integer m An integer Output: A A matrix in R ^ ((m + 1) x 1) B A matrix in R ^ ((m + 1) x (m + 1)) inv_B A matrix in K ^ ((m + 1) x (m + 1)) sq_gs_norm A real number, the square of the Gram-Schmidt norm of B Format: Coefficient """ if m == 1: magic_constant = [1.15] gs_slack = 1.17 elif m == 2: magic_constant = [1.07, 1.14] gs_slack = 1.17 elif m == 3: magic_constant = [1.21, 1.10, 1.06] gs_slack = 1.24 else: print("No parameters implemented yet for m = {m}".format(m=m)) return max_gs_norm = gs_slack * (q ** (1 / (m + 1))) while True: # We generate all rows of B except the last B = [[None for j in range(m + 1)] for i in range(m + 1)] for i in range(m): for j in range(m + 1): # Each coefficient B[i][j] is a polynomial sigma = magic_constant[i] * (q ** (1 / (m + 1))) # ==> ||bi~|| = gs_slack * q^(1/(m+1)) sig = sqrt(1 / (d * (m + 1 - i))) * sigma # sig = stdv. dev. des coef de bi B[i][j] = [int(round(gauss(0, sig))) for k in range(d)] # We check that the GS norm is not larger than tolerated Bp_fft = [[fft(poly) for poly in row] for row in B[:-1]] Gp = gram_fft(Bp_fft) [Lp_fft, Dp_fft] = ldl_fft(Gp) Dp = [[[0] * d for col in range(m)] for row in range(m)] for i in range(m): Dp[i][i] = ifft(Dp_fft[i][i]) prod_di = [1] + [0] * (d - 1) for i in range(m): prod_di = mul(prod_di, Dp[i][i]) last = div([q ** 2] + [0] * (d - 1), prod_di) norms = [Dp[i][i][0] for i in range(m)] + [last[0]] # If the GS norm is too large, restart if sqrt(max(norms)) > max_gs_norm: continue # Try to solve the module-NTRU equation f = submatrix(B, m, 0) f = [[neg(elt) for elt in row] for row in f] g = [B[j][0] for j in range(m)] fp = my_det(f) adjf = my_adjugate(f) gp = [0] * d for i in range(m): gp = add(gp, karamul(adjf[0][i], g[i])) try: # Compute f^(-1) mod q fp_q = [elt % q for elt in fp] inv_f = [[elt[:] for elt in row] for row in adjf] for i in range(m): for j in range(m): inv_f[i][j] = [elt % q for elt in inv_f[i][j]] inv_f[i][j] = div_zq(inv_f[i][j], fp_q, q) # Compute h = f^(-1) * g mod q and A = [1 | h] h = [None] * m for i in range(m): elt = [0] * d for j in range(m): elt = add_zq(elt, mul_zq(inv_f[i][j], g[j], q), q) h[i] = elt one = [1] + [0 for _ in range(1, d)] A = [one] + h Fp, Gp = ntru_solve(fp, gp, q) B[m][0] = Gp B[m][1] = [- coef for coef in Fp] for i in range(2, m + 1): B[m][i] = [0 for _ in range(d)] # Compute the inverse of B det_B = my_det(B) inv_B = my_adjugate(B) inv_B = [[div(poly, det_B) for poly in row] for row in inv_B] return A, B, inv_B, max(norms) # If any step failed, restart except (ZeroDivisionError, ValueError): continue