def ldl(G): """ Compute the LDL decomposition of G. Input: G A self-adjoint matrix (i.e. G is equal to its conjugate transpose) Output: L, D The LDL decomposition of G, that is G = L * D * (L*), where: - L is lower triangular with a diagonal of 1's - D is diagonal Format: Coefficient """ deg = len(G[0][0]) dim = len(G) L = [[[0 for k in range(deg)] for j in range(dim)] for i in range(dim)] D = [[[0 for k in range(deg)] for j in range(dim)] for i in range(dim)] for i in range(dim): L[i][i] = [1] + [0 for j in range(deg - 1)] D[i][i] = G[i][i] for j in range(i): L[i][j] = G[i][j] for k in range(j): L[i][j] = sub(L[i][j], mul(mul(L[i][k], adj(L[j][k])), D[k][k])) L[i][j] = div(L[i][j], D[j][j]) D[i][i] = sub(D[i][i], mul(mul(L[i][j], adj(L[i][j])), D[j][j])) return [L, D]
def ldl(G): """Compute the LDL decomposition of G. Args: G: a Gram matrix Format: coefficient Corresponds to algorithm 14 (LDL) of Falcon's documentation, except it's in polynomial representation. """ deg = len(G[0][0]) dim = len(G) L = [[[0 for k in range(deg)] for j in range(dim)] for i in range(dim)] D = [[[0 for k in range(deg)] for j in range(dim)] for i in range(dim)] for i in range(dim): L[i][i] = [1] + [0 for j in range(deg - 1)] D[i][i] = G[i][i] for j in range(i): L[i][j] = G[i][j] for k in range(j): L[i][j] = sub(L[i][j], mul(mul(L[i][k], adj(L[j][k])), D[k][k])) L[i][j] = div(L[i][j], D[j][j]) D[i][i] = sub(D[i][i], mul(mul(L[i][j], adj(L[i][j])), D[j][j])) return [L, D]
def sample_preimage_fft(self, point): """Sample preimage.""" B = self.B0_fft c = point, [0] * self.n t_fft = self.get_coord_in_fft(c) z_fft = ffsampling_fft(t_fft, self.T_fft) v0_fft = add_fft(mul_fft(z_fft[0], B[0][0]), mul_fft(z_fft[1], B[1][0])) v1_fft = add_fft(mul_fft(z_fft[0], B[0][1]), mul_fft(z_fft[1], B[1][1])) v0 = [int(round(elt)) for elt in ifft(v0_fft)] v1 = [int(round(elt)) for elt in ifft(v1_fft)] v = v0, v1 s = [sub(c[0], v[0]), sub(c[1], v[1])] return s
def sample_preimage_fft(self, point): """ Sample preimage. Input: self The private key point An element of Z_q[x] / (x ** d + 1) Output: s A short element such that s * B = point Format: Coefficient """ d = self.d m = self.m # Compute large preimage c = [point] + [[0] * d for _ in range(m)] # Move to FFT domain c_fft = [fft(elt) for elt in c] # Compute coefficients in span(B) t_fft = vecmatmul_fft(c_fft, self.invB_fft) # Fast Fourier sampling z_fft = ffsampling_fft(t_fft, self.T_fft) # Compute short preimage s = (c - v) = (t - z) * B v_fft = vecmatmul_fft(z_fft, self.B_fft) v = [[int(round(coef)) for coef in ifft(elt)] for elt in v_fft] s = [sub(c[i], v[i]) for i in range(m + 1)] return s
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 ldl(G): """ Compute the LDL decomposition of G. Only works with 2 * 2 matrices. Args: G: a Gram matrix Format: coefficient Corresponds to algorithm 8 (LDL*) of Falcon's documentation, except it's in polynomial representation. """ deg = len(G[0][0]) dim = len(G) assert (dim == 2) assert (dim == len(G[0])) zero = [0] * deg one = [1] + [0] * (deg - 1) D00 = G[0][0][:] L10 = div(G[1][0], G[0][0]) D11 = sub(G[1][1], mul(mul(L10, adj(L10)), G[0][0])) L = [[one, zero], [L10, one]] D = [[D00, zero], [zero, D11]] return [L, D]
def ffnp(t, T): """ Compute the FFNP reduction of t, using T as auxilary information. Input: t A vector T The LDL decomposition tree of an (implicit) matrix G Output: z An integer vector such that (t - z) * B is short Format: Coefficient """ m = len(t) n = len(t[0]) z = [None] * m # General case if (n > 1): L = T[0] for i in range(m - 1, -1, -1): # t[i] is "corrected", taking into accounts the t[j], z[j] (j > i) tib = t[i][:] for j in range(m - 1, i, -1): tib = add(tib, mul(sub(t[j], z[j]), L[j][i])) # Recursive call z[i] = merge(ffnp(split(tib), T[i + 1])) return z # Bottom case: round each coefficient in parallel elif (n == 1): z[0] = [round(t[0][0])] z[1] = [round(t[1][0])] return z
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 test_ffnp(d, m, 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. """ q = q_12289 A, B, inv_B, sqr_gsnorm = module_ntru_gen(d, q, m) G0 = gram(B) G0_fft = [[fft(elt) for elt in row] for row in G0] T = ffldl(G0) T_fft = ffldl_fft(G0_fft) th_bound = (m + 1) * d * sqr_gsnorm / 4. mn = 0 for i in range(iterations): t = [[random() for coef in range(d)] for poly in range(m + 1)] 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[i], z[i]) for i in range(m + 1)] diffB = vecmatmul(diff, B) norm_zmc = int(round(sqnorm(diffB))) mn = max(mn, norm_zmc) if mn > th_bound: print("z = {z}".format(z=z)) print("t = {t}".format(t=t)) print("mn = {mn}".format(mn=mn)) print("th_bound = {th_bound}".format(th_bound=th_bound)) print("sqr_gsnorm = {sqr_gsnorm}".format(sqr_gsnorm=sqr_gsnorm)) print("Warning: the algorithm outputs vectors longer than expected") return False else: return True
def ffnp(t, T): """Compute the ffnp reduction of t, using T as auxilary information. Args: t: a vector T: a ldl decomposition tree Format: coefficient """ n = len(t[0]) z = [None, None] if (n > 1): l10, T0, T1 = T z[1] = merge(ffnp(split(t[1]), T1)) t0b = add(t[0], mul(sub(t[1], z[1]), l10)) z[0] = merge(ffnp(split(t0b), T0)) return z elif (n == 1): z[0] = [round(t[0][0])] z[1] = [round(t[1][0])] return z
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