1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
| import random import time import logging from sage.crypto.util import random_blum_prime
def generate_gifp_instance(modulus_bit_length, alpha, gamma, beta1, beta2, seed=None, max_attempts=10): """ Generate two RSA instances with the same modulus length and a shared bit. :param modulus_bit_length: The bit length of the modulus. :param alpha: The ratio of the bit length of the larger prime to the modulus bit length. :param gamma: The ratio of the bit length of the shared bit to the modulus bit length. :param beta1: The ratio of the bit length of the smaller prime in the first RSA instance to the modulus bit length. :param beta2: The ratio of the bit length of the smaller prime in the second RSA instance to the modulus bit length. :param seed: The seed for the random number generator. If not provided, the current time's microsecond is used. :param max_attempts: The maximum number of attempts to generate RSA instances. :return: A list of the first RSA instance's primes, a list of the second RSA instance's primes, the shared bit, and the seed. """ N1 = N2 = attempts = 0 while attempts < max_attempts: if seed is None: seed = int(time.time() * 1e6) set_random_seed(seed) if beta2<beta1: tmp=beta2 beta2=beta1 beta1=tmp else: beta1=beta1 beta2=beta2 share_bit_length = int(modulus_bit_length * gamma) beta1_bit_length = int(modulus_bit_length * beta1) beta2_bit_length = int(modulus_bit_length * beta2) share_bit = ZZ(randint(2**(share_bit_length - 1)+1, 2**share_bit_length - 1)) p_bit_length = int(modulus_bit_length * (1-alpha)) q_bit_length = int(modulus_bit_length * alpha) MSB4p1 = ZZ(randint(2**int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*gamma-modulus_bit_length*beta1-1)+1, 2**int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*gamma-modulus_bit_length*beta1) - 1)) MSB4p2 = ZZ(randint(2**int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*gamma-modulus_bit_length*beta2-1)+1, 2**int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*gamma-modulus_bit_length*beta2) - 1)) p1 = random_blum_prime(MSB4p1*2**int(modulus_bit_length*gamma+modulus_bit_length*beta1) + share_bit* 2**(beta1_bit_length)+2**(beta1_bit_length - 1), MSB4p1*2**int(modulus_bit_length*gamma+modulus_bit_length*beta1) + share_bit* 2**(beta1_bit_length)+2**beta1_bit_length - 1) p2 = random_blum_prime(MSB4p2*2**int(modulus_bit_length*gamma+modulus_bit_length*beta2) + share_bit* 2**(beta2_bit_length)+2**(beta2_bit_length - 1), MSB4p2*2**int(modulus_bit_length*gamma+modulus_bit_length*beta2) + share_bit* 2**(beta2_bit_length)+2**beta2_bit_length - 1) q1 = random_blum_prime(2**(q_bit_length - 1), 2**q_bit_length - 1) q2 = random_blum_prime(2**(q_bit_length - 1), 2**q_bit_length - 1) N1 = p1 * q1 N2 = p2 * q2 desired_solution = (int(bin(p1)[2+int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*beta1):],2)* 2**int(beta2*modulus_bit_length-beta1*modulus_bit_length)-int(bin(p2)[2+int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*beta2):],2),int(bin(MSB4p1)[2:],2)-int(bin(MSB4p2)[2:],2),q2) if N1.nbits() != modulus_bit_length or N2.nbits() != modulus_bit_length: attempts += 1 seed = int(time.time() * 1e6) print(f"Regenerated seed: {seed}") else: print(f"Geenerated gifp instance successfully with seed: {seed}") print(f'share_bit: {bin(share_bit)[2:]}') print(f'p1: {p1}') print(f'MSB4p1: {bin(MSB4p1)[2:]}') print(f'share_bit4p1: {bin(p1)[2+int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*gamma-modulus_bit_length*beta1):2+int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*beta1)]}') print(f'LSB4p1: {bin(p1)[2+int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*beta1):]}') print(f'q1: {q1}') print(f'N1: {N1}') print(f'p2: {p2}') print(f'MSB4p2: {bin(MSB4p2)[2:]}') print(f'share_bit4p2: {bin(p2)[2+int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*gamma-modulus_bit_length*beta2):2+int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*beta2)]}') print(f'LSB4p2: {bin(p2)[2+int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*beta2):]}') print(f'q2: {q2}') print(f'N2: {N2}') print(f"The desired solution is (x0, y0, z0, w0) = {(int(bin(p1)[2+int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*beta1):],2)* 2**int(beta2*modulus_bit_length-beta1*modulus_bit_length)-int(bin(p2)[2+int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*beta2):],2),int(bin(MSB4p1)[2:],2)-int(bin(MSB4p2)[2:],2),q2,p2)}") return [p1, q1, N1], [p2, q2, N2], share_bit, desired_solution logging.warning(f"Failed to generate RSA instances after {max_attempts} attempts.") return None
def create_lattice(pr, shifts, bounds, order="invlex", sort_shifts_reverse=False, sort_monomials_reverse=False): """ Creates a lattice from a list of shift polynomials. :param pr: the polynomial ring :param shifts: the shifts :param bounds: the bounds :param order: the order to sort the shifts/monomials by :param sort_shifts_reverse: set to true to sort the shifts in reverse order :param sort_monomials_reverse: set to true to sort the monomials in reverse order :return: a tuple of lattice and list of monomials """ print(f"Creating a lattice with {len(shifts)} shifts (order = {order}, sort_shifts_reverse = {sort_shifts_reverse}, sort_monomials_reverse = {sort_monomials_reverse})...") if pr.ngens() > 1: pr_ = pr.change_ring(ZZ, order=order) shifts = [pr_(shift) for shift in shifts]
monomials = set() for shift in shifts: monomials.update(shift.monomials())
shifts.sort(reverse=sort_shifts_reverse) monomials = sorted(monomials, reverse=sort_monomials_reverse) L = matrix(ZZ, len(shifts), len(monomials)) for row, shift in enumerate(shifts): for col, monomial in enumerate(monomials): L[row, col] = shift.monomial_coefficient(monomial) * monomial(*bounds)
monomials = [pr(monomial) for monomial in monomials] return L, monomials
def log_lattice(L): """ Logs a lattice. :param L: the lattice """ for row in range(L.nrows()): r = "" for col in range(L.ncols()): if L[row, col] == 0: r += "_ " else: r += "X " print(r) def reduce_lattice(L, delta=0.8): """ Reduces a lattice basis using a lattice reduction algorithm (currently LLL). :param L: the lattice basis :param delta: the delta parameter for LLL (default: 0.8) :return: the reduced basis """ print(f"Reducing a {L.nrows()} x {L.ncols()} lattice...") return L.LLL(delta)
def reconstruct_polynomials(B, f, modulus, monomials, bounds): """ Reconstructs polynomials from the lattice basis in the monomials. :param B: the lattice basis :param f: the original polynomial (if set to None, polynomials will not be divided by f if possible) :param modulus: the original modulus :param monomials: the monomials :param bounds: the bounds :return: a list of polynomials """ print(f"Reconstructing polynomials (divide_original = {f is not None}, modulus_bound = {modulus is not None})...") print(f"Reconstructing polynomials with bounds: {bounds}") polynomials = [] for row in range(B.nrows()): norm_squared = 0 ww = 0 polynomial = 0 for col, monomial in enumerate(monomials): if B[row, col] == 0: continue norm_squared += B[row, col] ** 2 ww += 1 assert B[row, col] % monomial(*bounds) == 0 polynomial += B[row, col] * monomial // monomial(*bounds)
if modulus is not None and norm_squared * ww >= modulus ** 2: print(f"Row {row} is too large, ignoring...") continue
if f is not None and polynomial % f == 0: print(f"Original polynomial divides reconstructed polynomial at row {row}, dividing...") polynomial //= f
if polynomial.is_constant(): print(f"Polynomial at row {row} is constant, ignoring...") continue
polynomials.append(polynomial)
print(f"Reconstructed {len(polynomials)} polynomials") return polynomials
def find_roots_univariate(x, polynomial): """ Returns a generator generating all roots of a univariate polynomial in an unknown. :param x: the unknown :param polynomial: the polynomial :return: a generator generating dicts of (x: root) entries """ if polynomial.is_constant(): return
for root in polynomial.roots(multiplicities=False): if root != 0: yield {x: int(root)}
def find_roots_groebner(pr, unknown_modular, polynomials,bounds): """ Returns a generator generating all roots of a polynomial in some unknowns. Uses Groebner bases to find the roots. :param N1: the modulus of the first RSA instance :param N2: the modulus of the second RSA instance :param pr: the polynomial ring :param desired_solution: the desired root (x0, y0, z0, w0), just used for debug :param unknown_modular: unknown modular M^m*N1^t, just used for debug :param polynomials: the reconstructed polynomials :param bounds: the bounds :return: a generator generating dicts of (x0: x0root, x1: x1root, ...) entries """ gens = pr.gens() s = Sequence(polynomials, pr.change_ring(QQ, order="lex")) for ii in range(len(s)): print(f"Polynomials in Sequence: {s[ii]}")
while len(s) > 0: start_time_gb=time.time() G = s.groebner_basis() print(f"Sequence length: {len(s)}, Groebner basis length: {len(G)}") if len(G) == len(gens): end_time_gb=time.time() print(f'The time taken by gb is {end_time_gb-start_time_gb}') print(f"Found Groebner basis with length {len(gens)}, trying to find roots...") roots = {} for ii in range(len(G)): print(f"Groebner basis {G[ii]}") for polynomial in G: vars = polynomial.variables() if len(vars) == 1: for root in find_roots_univariate(vars[0], polynomial.univariate_polynomial()): roots |= root
if len(roots) == pr.ngens(): yield roots return
return else: s.pop()
def eliminate_N2(f, modular): pr = ZZ["x", "y", "z"] x, y, z= pr.gens() tmp_poly=0 for mono in f.monomials(): if f.monomial_coefficient(mono)%modular==0: tmp_poly+=mono*f.monomial_coefficient(mono) else: tmp_poly+=mono*(f.monomial_coefficient(mono)% modular) return tmp_poly
def modular_gifp(unknown_modular, f, M, N1, N2, m, t, X, Y, Z): """ Computes Generalized Implicit Factorization Problem. More information: Feng, Yansong and Nitaj, Abderrahmane and Pan, Yanbin, "Generalized Implicit Factorization Problem" :param desired_solution: the desired root (x0, y0, z0, w0), just used for debug :param unknown_modular: unknown modular M^m*N1^t, just used for debug :param f: the polynomial :param M: 2^(|\beta1-\beta2|*n) :param N1: the modulus of the first RSA instance :param N2: the modulus of the second RSA instance :param m: the number of shifts :param s: a parameter :param t: a parameter :param X: the bound for x :param Y: the bound for y :param Z: the bound for z :param W: the bound for w :return: a generator generating small roots of the polynomial """ f = f.change_ring(ZZ)
pr = ZZ["x", "y", "z"] x, y, z = pr.gens()
print("Generating shifts...")
shifts = []
modular = M^m*N1^t
print(f'modular is {modular}') N2_inverse = inverse_mod(N2, modular) print(f'N2^{-1}: {N2_inverse}') for ii in range(m + 1): for jj in range(m - ii + 1): g = (y*z) ** jj * f ** (ii) * M ** (m - ii) * N1 ** max(t - ii, 0) print(f'Initial polynomial shift: {g}') shifts.append(g) print(f'Add shift: {g}')
print('Generating lattice for gifp') L, monomials = create_lattice(pr, shifts, [X, Y, Z]) log_lattice(L) print('Reduce the lattice') start_time_LLL=time.time() L = reduce_lattice(L) end_time_LLL=time.time() print(f'The time taken by LLL is {end_time_LLL-start_time_LLL}') log_lattice(L)
polynomials = reconstruct_polynomials(L, f, unknown_modular, monomials, [X, Y, Z]) for poly in polynomials: print(f'Polynomials after reconstructing from short vectors: {poly}')
for roots in find_roots_groebner(pr, unknown_modular, polynomials,[X, Y, Z]): yield roots[x], roots[y], roots[z]
def attack_gifp_instance(modulus_bit_length, alpha, gamma, beta1, beta2, m=1, seed=None, t=None): """ Attack an GIFP instance with parameters :param modulus_bit_length: the bit length of the modulus :param alpha, gamma, beta1, beta2: parameters for the GIFP instance :param m: a given parameter for controlling the lattice dimension (default: 3) :param seed: The seed for the random number generator. If not provided, the current time's microsecond is used. :param t: a parameter :return: 1 if attack succeeds else 0 """ if beta2<beta1: tmp=beta2 beta2=beta1 beta1=tmp else: beta1=beta1 beta2=beta2 x, y, z = ZZ["x", "y", "z"].gens() N1 = 11195108435418195710792588075406654238662413452040893604269481198631380853864777816171135346615239847585274781942826320773907414919521767450698159152141823148113043170072260905000812966959448737906045653134710039763987977024660093279241536270954380974093998238962759705207561900626656220185252467266349413165950122829268464816965028949121220409917771866453266522778041491886000765870296070557269360794230165147639201703312790976341766891628037850902489808393224528144341687117276366107884626925409318998153959791998809250576701129098030933612584038842347204032289231076557168670724255156010233010888918002630018693299 N2 = 15100254697650550107773880032815145863356657719287915742500114525591753087962467826081728465512892164117836132237310655696249972190691781679185814089899954980129273157108546566607320409558512492474972517904901612694329245705071789171594962844907667870548108438624866788136327638175865250706483350097727472981522495023856155253124778291684107340441685908190131143526592231859940556416271923298043631447630144435617140894108480182678930181019645093766210388896642127572162172851596331016756329494450522133805279328640942549500999876562756779916153474958590607156569686953857510763692124165713467629066731049974996526071 N1_list = [926811193643434749652052036902780747582254462279473779] print('Generating polynimial f') f = x*z + 2**(int(beta2*modulus_bit_length)+int(gamma*modulus_bit_length))*y*z+N2 print(f'Polynomial f: {f}') X = Integer(2 ** int(beta2*modulus_bit_length)) Y = Integer(2 ** int(modulus_bit_length*1-modulus_bit_length*alpha-modulus_bit_length*gamma-modulus_bit_length*beta1)) Z = Integer(2 ** int(alpha * modulus_bit_length)) M = Integer(2 ** int(modulus_bit_length*beta2-modulus_bit_length*beta1)) print(f'M = {M}') t = round((1 - sqrt(alpha)) * m) if t is None else t unknown_modular = M ** m * N1 ** t print(f"Trying m = {m}, t = {t}...") gb_tmp = [] for x0, y0, z0 in modular_gifp(unknown_modular, f, M, N1, N2, m, t, X, Y, Z): v = int(f(x0, y0, z0)) if v == 0: p2 = N2/z0 q2 = z0 p1 = N2/z0+x0+y0*(2**int(gamma*modulus_bit_length+beta2*modulus_bit_length)) q1 = N1/p1 if p1 is not None and q1 is not None and p2 is not None and q2 is not None : print(f"Succeeded!") print(f"Found p1 = {p1}") print(f"Found q1 = {q1}") print(f"Found p2 = {p2}") print(f"Found q2 = {q2}") return 1 else: print(f"Failed!") return 0
modulus_bit_length, alpha, gamma, beta1, beta2, m = 2048, 0.125, 0.43359375, 0.31640625, 0, 6 result = attack_gifp_instance(modulus_bit_length, alpha, gamma, beta1, beta2, m) print(result)
|