Fork of the official github repository of the framework Leaky-LWE-Estimator, a Sage Toolkit to attack and estimate the hardness of LWE with Side Information. https://github.com/lducas/leaky-LWE-Estimator
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DBDD.sage 14KB

  1. from fpylll import *
  2. from fpylll.algorithms.bkz2 import BKZReduction
  3. load("../framework/load_strategies.sage")
  4. load("../framework/DBDD_generic.sage")
  5. load("../framework/proba_utils.sage")
  6. # Restoration Type
  7. SUBSTITUTION = 0
  8. QMODULUS_SUBSTITUTION = 1
  9. class DBDD(DBDD_generic):
  10. """
  11. This class defines all the elements defining a DBDD instance with all
  12. the basis computations
  13. """
  14. def __init__(self, B, S, mu, u=None, verbosity=1, float_type="ld", **kwargs):
  15. """constructor that builds a DBDD instance from a lattice, mean, sigma
  16. and a target
  17. ;min_dim: Number of coordinates to find to consider the problem solved
  18. :B: Basis of the lattice
  19. :S: The Covariance matrix (Sigma) of the uSVP solution
  20. :mu: The expectation of the uSVP solution
  21. :u: The unique vector to be found (optinal, for verification purposes)
  22. :fp_type: Floating point type to use in FPLLL ("d, ld, dd, qd")
  23. """
  24. self.verbosity = verbosity
  25. self.B = B # The lattice Basis
  26. self.D = kwargs.get('D', None) # The dual Basis (only B or D is active)
  27. assert self.D.T * self.B == identity_matrix(B.nrows())
  28. self._dim = B.nrows()
  29. #self._keep_basis = False
  30. self.S = S
  31. self.PP = 0 * S # Span of the projections so far (orthonormal)
  32. self.mu = mu
  33. self.u = u
  34. self.u_original = u
  35. self.expected_length = RR(sqrt(self.S.trace()) + 1)
  36. self.projections = 0
  37. self.save = {"save": None}
  38. self.float_type = float_type
  39. self.estimate_attack(silent=True)
  40. self.Pi = identity_matrix(self._dim) # Reduction matrix
  41. self.Gamma = identity_matrix(self._dim) # Substitution matrix
  42. self._restoration_instructions = []
  43. def dim(self):
  44. return self._dim
  45. def S_diag(self):
  46. return [self.S[i, i] for i in range(self.S.nrows())]
  47. @without_dependencies
  48. def volumes(self):
  49. if self.B is not None:
  50. Bvol = logdet(self.B * self.B.T) / 2
  51. else:
  52. Bvol = -logdet(self.D * self.D.T) / 2
  53. S = self.S + self.mu.T * self.mu
  54. Svol = degen_logdet(S)
  55. dvol = Bvol - Svol / 2.
  56. return (Bvol, Svol, dvol)
  57. @without_dependencies
  58. def test_primitive_dual(self, V, action):
  59. if self.B is None:
  60. self.B = dual_basis(self.D)
  61. W = V * self.B.T
  62. den = lcm([x.denominator() for x in W[0]])
  63. num = gcd([x for x in W[0] * den])
  64. assert den == 1
  65. if num == 1:
  66. return True
  67. if action == "warn":
  68. logging("non-primitive (factor %d)." %
  69. num, style="WARNING", newline=False)
  70. return True
  71. elif action == "reject":
  72. raise RejectedHint("non-primitive (factor %d)." % num)
  73. raise InvalidHint("non-primitive (factor %d)." % num)
  74. def get_reduced_hint_vector(self, V):
  75. V = V * self.Gamma
  76. if V == 0:
  77. raise RejectedHint("Redundant hint")
  78. return V
  79. def add_restoration_instruction(self, type, Gamma, **data):
  80. self.Gamma *= Gamma
  81. last_instruction = self._restoration_instructions[-1] if len(self._restoration_instructions)>0 else (None, None, None)
  82. if type==SUBSTITUTION and last_instruction is not None and last_instruction[0]==SUBSTITUTION:
  83. self._restoration_instructions[-1] = (SUBSTITUTION, last_instruction[1]*Gamma, {})
  84. else:
  85. self._restoration_instructions.append((type, Gamma, data))
  86. #@not_after_projections
  87. #@hint_integration_wrapper(force=True, requires=["dual"], invalidates=["primal"])
  88. #def reduce_dimension(self, Gamma, normalization_matrix=None):
  89. # if normalization_matrix is None:
  90. # normalization_matrix = (Gamma.T * Gamma).inverse()
  91. # normalized_Gamma = Gamma*normalization_matrix
  92. #
  93. # self.D = self.D * Gamma
  94. # self.mu = self.mu * normalized_Gamma
  95. # self.S = normalized_Gamma.T * self.S * normalized_Gamma
  96. # self.PP = 0 * self.S
  97. #
  98. # #self.Pi *= normalized_Gamma
  99. # self.add_restoration_instruction(SUBSTITUTION, Gamma)
  100. @not_after_projections
  101. @hint_integration_wrapper(force=True, requires=["dual"],
  102. invalidates=["primal"])
  103. def integrate_perfect_hint(self, v, l, reduction=False):
  104. V = concatenate(v, -l)
  105. V = self.get_reduced_hint_vector(V)
  106. VS = V * self.S
  107. den = scal(VS * V.T)
  108. if den == 0:
  109. raise NotImplementedError('Normally, useless condition')
  110. #raise RejectedHint("Redundant hint")
  111. self.D = lattice_orthogonal_section(self.D, V)
  112. self._dim -= 1
  113. num = self.mu * V.T
  114. self.mu -= (num / den) * VS
  115. num = VS.T * VS
  116. self.S -= num / den
  117. ## Dimension Reduction
  118. Gamma, data = build_standard_substitution_matrix(V, output_data=True)
  119. normalization_matrix = data['normalization_matrix']
  120. normalized_Gamma = Gamma*normalization_matrix
  121. self.D = self.D * Gamma
  122. self.mu = self.mu * normalized_Gamma
  123. self.S = normalized_Gamma.T * self.S * normalized_Gamma
  124. self.PP = 0 * self.S
  125. self.Pi = normalized_Gamma.T * self.Pi
  126. self.add_restoration_instruction(SUBSTITUTION, Gamma)
  127. @not_after_projections
  128. @hint_integration_wrapper(force=True, requires=["dual"], invalidates=["primal"])
  129. def integrate_modular_hint(self, v, l, k, smooth=True):
  130. V = concatenate(v, -l)
  131. V = self.get_reduced_hint_vector(V)
  132. VS = V * self.S
  133. den = scal(VS * V.T)
  134. if den == 0:
  135. raise NotImplementedError('Normally, useless condition')
  136. #raise RejectedHint("Redundant hint")
  137. if not smooth:
  138. raise NotImplementedError()
  139. self.D = lattice_modular_intersection(self.D, V, k)
  140. @not_after_projections
  141. @hint_integration_wrapper(force=True, requires=["dual"], invalidates=["primal"])
  142. def integrate_q_modular_hint(self, v, l, q):
  143. V = concatenate(v, -l)
  144. V = self.get_reduced_hint_vector(V)
  145. if V == 0:
  146. raise RejectedHint("Redundant hint")
  147. _, pivot = V.nonzero_positions()[0]
  148. V = V * int(mod(V[0,pivot],q)**(-1)) % q
  149. V = V.apply_map(lambda x: recenter(x,q))
  150. W = q * canonical_vec(self._dim, pivot)
  151. Gamma = build_standard_substitution_matrix(V, pivot=pivot)
  152. assert scal(V * W.T)/q == 1, f'<V, W> = {scal(V * W.T)/q} != 1'
  153. self.D = lattice_modular_intersection(self.D, V, q)
  154. # So, V/q is a dual vector, and we hope it is a primitive one
  155. ## Let build the reduction matrix \Pi
  156. #B_ = block_matrix(QQ, [[Gamma.T], [W]])
  157. #Pi = dual_basis(B_)[:self._dim-1]
  158. dim = self._dim
  159. Pi = block_matrix(QQ, [
  160. [identity_matrix(pivot), zero_matrix(pivot,1), zero_matrix(pivot, dim-pivot-1)],
  161. [zero_matrix(dim-pivot-1, pivot), zero_matrix(dim-pivot-1,1), identity_matrix(dim-pivot-1)],
  162. ])
  163. assert Pi.dimensions() == (dim-1, dim), 'Error in the dimension of \Pi'
  164. assert Pi * Gamma == identity_matrix(dim-1), 'Error in the calculation of \Pi: \Pi\Gamma != I'
  165. assert Pi * W.T == 0, 'Error in the calculation of \Pi: \Pi W != 0'
  166. self._dim -= 1
  167. # Even if we have a formulae for the primal basis, it is incorrect because we don't apply "lattice_modular_intersection" on self.B
  168. if self.D is not None:
  169. self.D = self.D * Gamma
  170. self.mu = self.mu * Pi.T
  171. self.S = Pi * self.S * Pi.T
  172. self.PP = 0 * self.S
  173. self.Pi = Pi * self.Pi
  174. self.add_restoration_instruction(QMODULUS_SUBSTITUTION, Gamma, w=W, v=V, q=q)
  175. @not_after_projections
  176. @hint_integration_wrapper(force=True)
  177. def integrate_approx_hint(self, v, l, variance, aposteriori=False):
  178. if variance < 0:
  179. raise InvalidHint("variance must be non-negative !")
  180. if variance == 0:
  181. raise InvalidHint("variance=0 : must use perfect hint !")
  182. if not aposteriori:
  183. V = concatenate(v, -l)
  184. V = self.get_reduced_hint_vector(V)
  185. VS = V * self.S
  186. d = scal(VS * V.T)
  187. center = scal(self.mu * V.T)
  188. coeff = (- center / (variance + d))
  189. self.mu += coeff * VS
  190. self.S -= (1 / (variance + d) * VS.T) * VS
  191. else:
  192. V = concatenate(v, 0)
  193. V = self.get_reduced_hint_vector(V)
  194. VS = V * self.S
  195. # test if eigenvector
  196. #if not scal(VS * V.T)**2 == scal(VS * VS.T) * scal(V * V.T):
  197. # raise RejectedHint("Not an eigenvector of Σ,")
  198. if not scal(VS * VS.T):
  199. raise RejectedHint("0-Eigenvector of Σ forbidden,")
  200. # New formulae
  201. den = scal(VS * V.T)
  202. self.mu += ((l - scal(self.mu * V.T)) / den) * VS
  203. self.S += (((variance - den) / den**2) * VS.T ) * VS
  204. @not_after_projections
  205. @hint_integration_wrapper()
  206. def integrate_approx_hint_fulldim(self, center,
  207. covariance, aposteriori=False):
  208. # Using http://www.cs.columbia.edu/~liulp/pdf/linear_normal_dist.pdf
  209. # with A = Id
  210. if not aposteriori:
  211. d = self.S.nrows() - 1
  212. if self.S.rank() != d or covariance.rank() != d:
  213. raise InvalidHint("Covariances not full dimensional")
  214. zero = vec(d * [0])
  215. F = (self.S + block4(covariance, zero.T, zero, vec([1]))).inverse()
  216. F[-1, -1] = 0
  217. C = concatenate(center, 1)
  218. self.mu += ((C - self.mu) * F) * self.S
  219. self.S -= self.S * F * self.S
  220. else:
  221. raise NotImplementedError()
  222. @hint_integration_wrapper(force=False,
  223. requires=["primal"],
  224. invalidates=["dual"])
  225. def integrate_short_vector_hint(self, v):
  226. V = concatenate(v, 0)
  227. if V.dimensions()[1] > self._dim:
  228. V = V * self.Pi.T # Reduction
  229. assert V.dimensions()[1] == self._dim
  230. V -= V * self.PP
  231. if scal((V * self.S) * V.T) == 0:
  232. raise InvalidHint("Projects to 0")
  233. self.projections += 1
  234. PV = identity_matrix(V.ncols()) - projection_matrix(V)
  235. try:
  236. self.B = lattice_project_against(self.B, V)
  237. self._dim -= 1
  238. except ValueError:
  239. raise InvalidHint("Not in Λ")
  240. self.mu = self.mu * PV
  241. self.u = self.u * (self.Pi.T * PV * self.Gamma.T)
  242. self.S = PV.T * self.S * PV
  243. self.PP += V.T * (V / scal(V * V.T))
  244. @without_dependencies
  245. def attack(self, beta_max=None, beta_pre=None, randomize=False, tours=1):
  246. """
  247. Run the lattice reduction to solve the DBDD instance.
  248. Return the (blocksize, solution) of a succesful attack,
  249. or (None, None) on failure
  250. """
  251. self.logging(" Running the Attack ", style="HEADER")
  252. if self.B is None:
  253. self.B = dual_basis(self.D)
  254. # Apply adequate distortion
  255. denom = lcm([x.denominator() for x in self.B.list()])
  256. B = self.B
  257. d = B.nrows()
  258. S = self.S + self.mu.T * self.mu
  259. L, Linv = square_root_inverse_degen(S, self.B)
  260. M = B * Linv
  261. # Make the matrix Integral
  262. denom = lcm([x.denominator() for x in M.list()])
  263. M = matrix(ZZ, M * denom)
  264. # Build the BKZ object
  265. G = None
  266. try:
  267. G = GSO.Mat(IntegerMatrix.from_matrix(M), float_type=self.float_type)
  268. except ValueError as e:
  269. G = GSO.Mat(IntegerMatrix.from_matrix(M))
  270. bkz = BKZReduction(G)
  271. if randomize:
  272. bkz.lll_obj()
  273. bkz.randomize_block(0, d, density=d / 4)
  274. bkz.lll_obj()
  275. u_den = lcm([x.denominator() for x in self.u.list()])
  276. if beta_pre is not None:
  277. self.logging("\rRunning BKZ-%d (until convergence)" %
  278. beta_pre, newline=False)
  279. bkz.lll_obj()
  280. par = BKZ.Param(block_size=beta_pre, strategies=strategies)
  281. bkz(par)
  282. bkz.lll_obj()
  283. else:
  284. beta_pre = 2
  285. # Run BKZ tours with progressively increasing blocksizes
  286. for beta in range(beta_pre, B.nrows() + 1):
  287. self.logging("\rRunning BKZ-%d" % beta, newline=False)
  288. if beta_max is not None:
  289. if beta > beta_max:
  290. self.logging("Failure ... (reached beta_max)",
  291. style="SUCCESS")
  292. self.logging("")
  293. return None, None
  294. if beta == 2:
  295. bkz.lll_obj()
  296. else:
  297. par = BKZ.Param(block_size=beta,
  298. strategies=strategies, max_loops=tours)
  299. bkz(par)
  300. bkz.lll_obj()
  301. # Recover the tentative solution,
  302. # undo distorition, scaling, and test it
  303. v = vec(bkz.A[0])
  304. v = u_den * v * L / denom
  305. solution = matrix(ZZ, v.apply_map(round)) / u_den
  306. self.reduced_solution = solution
  307. for instruc_type, Gamma, data in self._restoration_instructions[::-1]:
  308. solution = solution * Gamma.T
  309. if instruc_type == SUBSTITUTION:
  310. pass
  311. elif instruc_type == QMODULUS_SUBSTITUTION:
  312. solution += data['w'] * round(solution[0].inner_product(data['v'][0])/data['q'])
  313. else:
  314. solution += data['w'] * data['func'](solution)
  315. if not self.check_solution(solution):
  316. continue
  317. self.logging("Success !", style="SUCCESS")
  318. self.logging("")
  319. return beta, solution
  320. self.logging("Failure ...", style="FAILURE")
  321. self.logging("")
  322. return None, None