{{{id=1| ################################################## # RING-LWE ATTACK (Elias, Lauter, Ozman, Stange) # ################################################## # General preparation of Sage: Create a polynomial ring and import GaussianSampler, Timer P. = PolynomialRing(RationalField(), 'y') from sage.stats.distributions.discrete_gaussian_lattice import DiscreteGaussianDistributionLatticeSampler RP = RealField(300) # this sets the precision; if it is insufficient, the implementation won't be valid from sage.doctest.util import Timer # Give the Minkowski lattice for a given ring determined by a polynomial. # Also gives a key to which are real embeddings. def cmatrix(): # returns a matrix, columns basis 1, x, x^2, x^3, ... given in the canonical embedding global N, a N. = NumberField(f) fdeg = f.degree() key = [0 for i in range(fdeg)] # 0 = real, 1 = real part of complex emb, 2 = imaginary part embs = N.embeddings(CC) M = matrix(RP,fdeg,fdeg) print "Preparing an embedding matrix: computing powers of the root." apows = [ a^j for j in range(n) ] print "Finished computing the powers of the root." i = 0 while i < n: em = embs[i] if Mod(i,20)==Mod(0,20) or Mod(i,20)==Mod(1,20): print "Embedding matrix: ", i, " rows out of ", n, " complete." if em(a).imag() == 0: key[i] = 0 for j in range(n): M[i,j] = em(apows[j]).real() i = i + 1 else: key[i] = 1 key[i+1] = 2 for j in range(n): M[i,j] = em(apows[j]).real() M[i+1,j] = (em(apows[j])*I).real() i = i + 2 return M, key # Produce a random vector from (Z/qZ)^n def random_vec(q, dim): return vector([ZZ.random_element(0,q) for i in range(dim)]) # Useful function for real numbers modulo q def modq(r,q): s = r/q t = r/q - floor(r/q) return t*q # Call sampler def call_sampler(): e = sampler().change_ring(RP) return e # Create samples using a lattice (given by latmat and its inverse), # a Gaussian sampler on that lattice, secret, prime def get_sample(latmat, latmatinv, sec, qval, keyval): e = call_sampler() # create error, in R^n dim = latmat.dimensions()[0] # detect dimension of lattice pre_a = random_vec(qval, dim) # create a uniformly randomly in terms of basis in cm a = latmat*pre_a # create a, in R^n b = vecmul_poly(a,sec,latmat,latmatinv) + e # create b, in R^n pre_b = latmatinv*b # move to basis in cm in order to reduce mod q pre_b_red = vector([modq(c,qval) for c in pre_b]) b = latmat*pre_b_red return [a, b] # Global choices: setup a field and prime, sampler. # Set to dummy values that will be altered when an attack is run q = 1 n = 1 sig = 1/sqrt(2*pi) Zq = IntegerModRing(q) R. = PolynomialRing(Zq) f = y + 1 N. = NumberField(f) S. = R.quotient(f) # This is P_q cm,key = cmatrix() cmi = cm.inverse() cm cm53 = cm.change_ring(RealField(10)) cmqq = cm53.change_ring(QQ) sampler = DiscreteGaussianDistributionLatticeSampler(cmqq.transpose(), sig) matid = matrix.identity(n) # Set the parameters for the attack def setup_params(fval,qval,sval): global q,n,sig,f,S,x,z,Zq,matid f = fval n = f.degree() matid = matrix.identity(n) q = qval Zq = IntegerModRing(q) R. = PolynomialRing(Zq) sig = sval/sqrt(2*pi) S. = R.quotient(f) print "Setting up parameters, polynomial = ", f, " and prime = ", q, " and sigma = ", sig print "Verifying properties: " print "Prime?", q.is_prime() print "Irreducible? ", f.is_irreducible() print "Value at 1 modulo q?", Mod(f.subs(y=1),q) return True # Compute the lattices in Minkowski space def prepare_matrices(polyonly): global cm, key, cmi, cmqq print "Preparing matrices." if polyonly: cm = matrix.identity(n) else: cm,key = cmatrix() print "Embedding matrix prepared." cmi = cm.inverse() print "Inverse matrix found." if polyonly: cmqq = cm.change_ring(ZZ) else: cm53 = cm.change_ring(RealField(10)) cmqq = cm53.change_ring(QQ) print "All matrices prepared." return True # Make a vector in R^n into a polynomial, given change of basis matrix and variable to use def make_poly(a,matchange,var): coeffs = matchange*a #coefficients of the polynomial are given by the change of basis matrix pol = 0 for i in range(n): pol = pol + ZZ(round(coeffs[i]))*var^i # var controls where it will live (what poly ring) return pol # Make a polynomial into a vector in Minkowski space def make_vec(fval,matchange): if fval == 0: coeffs = [0 for i in range(n)] else: coeffs = [0 for i in range(n)] colist = lift(fval).coefficients() for i in range(len(colist)): coeffs[i] = ZZ(colist[i]) return matchange*vector(coeffs) # Multiplication in the Minkowski space via moving to polynomial ring def vecmul_poly(u,v,mat,matinv): poly_u = make_poly(u,matinv,z) poly_v = make_poly(v,matinv,z) poly_prod = poly_u*poly_v return make_vec(poly_prod,mat) # Create the sampler on the lattice embedded in R^n or ZZ^n def initiate_sampler(): global sampler print "Initiating Sampler." sampler = DiscreteGaussianDistributionLatticeSampler(cmqq.transpose(), sig) print "Sampler initiated with sigma", RDF(sig) return True # Produce error vectors, just a test to see how they look def error_test(num): print "Testing the error vector production by producing ", num, " errors." errorlist = [sampler().norm().n() for _ in range(num)] meannorm = mean(errorlist) # average norm maxnorm = max(errorlist) # maximum norm print "The average error norm is ", RDF(meannorm/( sqrt(n)*sampler.sigma*sqrt(2*pi) )), " times sqrt(n)*s." maxratio = RDF(maxnorm/( sqrt(n)*sampler.sigma*sqrt(2*pi) )) print "The maximum error norm is ", maxratio, " times sqrt(n)*s." if maxratio > 1: print "~~~~~~~~~~~~~~~~~~~~~~~ ERROR ~~~~~~~~~~~~~~~~~~~~~~~~~" print "The errors do not satisfy a proven upper bound in norm." return True # Create the secret secret = 0 def create_secret(): global secret secret = cm*random_vec(q,n) return True # Produce samples samps = [] numsamps = 1 def create_samples(numsampsval): global samps, numsamps samps = [] print "Creating samples" for i in range(numsampsval): print "Creating sample number ", i samp = get_sample(cm, cmi, secret, q, key) samps.append(samp) numsamps = len(samps) print "Done creating ", numsamps, "samples." return True # Function for going down to q def go_to_q(a,matchange): pol = make_poly(a,matchange,x) #print "debug got pol:", pol pol_eval = pol.subs(x=1) #print "debug eval'd to:", pol_eval, " and then ", Zq((pol_eval)) return Zq(pol_eval) # Check to make sure moving to q preserves product -- the last two lines should be equal def sanity_check(): print "Initiating sanity check" mat = cmi pvec1 = random_vec(q,n) vec1 = cm*pvec1 pvec2 = random_vec(q,n) vec2 = cm*pvec2 vprod2 = vecmul_poly(vec1,vec2,cm,cmi) first_thing = go_to_q(vprod2,mat) second_thing = go_to_q(vec1,mat)*go_to_q(vec2,mat) if first_thing == second_thing: print "Sanity confirmed." else: print "~~~~~~~~~~~~~~~~~~~~~~~ ERROR ~~~~~~~~~~~~~~~~~~~~~~~~~" print "Sanity problem:", first_thing, " is not equal to ", second_thing, "." print "Are you sure your ring has root 1 mod q?" return True # Given a list of elements of Z/qZ, make a histogram and zero count def histoq(data): hist = [0 for i in range(10)] # empty histogram zeroct=0 # count of zeroes mod q for datum in data: e = datum if e == 0: zeroct = zeroct+1 histbit = floor(ZZ(e)*10/q) hist[histbit]=hist[histbit]+1 return [hist, zeroct] # Given a list of vectors in R^n, create a histogram of their # values in Z/qZ under make_poly, together with a zero count def histo(data,cmi): return histoq([go_to_q(datum,cmi) for datum in data]) # Create a histogram of error vectors, transported to polynomial ring def histogram_of_errors(): print "Creating a histogram of errors mod q." errs = [] for i in range(80): errs.append(sampler()) hist = histo(errs,cmi) print "The number of error vectors that are zero:", hist[1] bar_chart(hist[0], width=1).show(figsize=2) return True # Create a histogram of the a's in the samples, transported to polynomial ring def histogram_of_as(): print "Creating a histogram of sample a's mod q." a_vals = [samp[0] for samp in samps] hist = histo(a_vals,cmi) print "The number of a's that are zero:", hist[1] bar_chart(hist[0], width=1).show(figsize=2) return True # Create a histogram of errors by correct guess def histogram_of_errors_2(): print "Creating a histogram of supposed errors if sample is guessed, mod q." hist = histoq([ lift(Zq(go_to_q(sample[1],cmi) - go_to_q(sample[0],cmi)*go_to_q(secret,cmi))) for sample in samps]) print "The number of such that are zero:", hist[1] bar_chart(hist[0], width=1).show(figsize=2) return True # Create the secret mod q lift_s = 0 def secret_mod_q(): global lift_s lift_s = go_to_q(secret,cmi) print "Storing the secret mod q. The secret is ", secret, " which becomes ", lift_s return True # Algorithm 2 # reportrate controls how often it updates the status of the loop; larger = less frequently # quickflag = True will run only the secret and a few other values to give a quick idea if it works def alg2(reportrate, quickflag = False): print "Beginning algorithm 2." numsamps = len(samps) a = [ 0 for i in range(numsamps)] b = [ 0 for i in range(numsamps)] print "Moving samples to F_q." for i in range(numsamps): sample = samps[i] a[i] = go_to_q(sample[0],cmi) b[i] = go_to_q(sample[1],cmi) possibles = [] winner = [[],0] print "Samples have been moved to F_q." for i in range(2): if i == 0: print "!!!!! ROUND 1: !!!!! First, checking how many samples the secret survives (peeking ahead)." iterat = [lift_s] if i == 1: print "!!!!! ROUND 2: !!!!! Now, running the attack naively." possibles = [] if quickflag: print "We are doing it quickly (not a full test)." iterat = xrange(1000) else: iterat = xrange(q) for g in iterat: if Mod(g,reportrate) == Mod(0,reportrate): print "Currently checking residue ", g g = Zq(g) potential = True ctr = 0 while ctr < numsamps and potential: e = abs(lift(Zq(b[ctr]-g*a[ctr]))) if e > q/4 and e < 3*q/4: potential = False if ctr == winner[1]: winner[0].append(g) print "We have a new tie for longest chain:", g, " has survived ", ctr, " rounds." if ctr > winner[1]: winner = [[g],ctr] print "We have a new longest chain of samples survived:", g, " has survived ", ctr, " rounds." ctr = ctr + 1 if potential == True: print "We found a potential secret: ", g possibles.append(g) if g == lift_s: if i == 0: print "The real secret survived ", ctr, "samples." #break print "Full list of survivors of the ", numsamps, " samples:", possibles print "The real secret mod q was: ", lift_s if len(possibles) == 1 and possibles[0] == lift_s: print "Success!" return True else: print "Failure!" return False # Run a simulation. def shebang(fval,qval,sval,numsampsval,numtrials,quickflag=False,polyonly=False): global sig n = fval.degree() if polyonly: print "Welcome to the Poly-LWE Attack." else: print "Welcome to the Ring-LWE Attack." print "The attack should theoretically work if the following quantity is greater than 1." print "Quantity: ", RDF( qval/( 2*sqrt(2)*sval*n*(qval-1)^( (n-1)/2/n) ) ) timer = Timer() timer2 = Timer() timer.start() print "********** PHASE 1: SETTING UP SYSTEM " setup_params(fval,qval,sval) prepare_matrices(polyonly) if not polyonly: print "Computing the adjustment factor for s." cembs = (n - len(N.embeddings(RR)))/2 detscale = RP( ( 2^(-cembs)*sqrt(abs(f.discriminant())) )^(1/n) ) # adjust the sigma,s sval = sval*detscale sig = sig*detscale print "Adjusted s for use with this embedding, result is ", sval initiate_sampler() print "The sampler has been created with sigma = ", RDF(sampler.sigma) print "Sampled vectors will have expected norm ", RDF(sqrt(n)*sampler.sigma) error_test(5) print "Time for Phase 1: ", timer.stop() timer.start() count_successes = 0 timer2.start() for trialnum in range(numtrials): print "*~*~*~*~*~*~*~*~*~*~*~*~* TRIAL NUMBER ", trialnum, "*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~" print "********** PHASE 2: CREATE SECRET AND SAMPLES" create_secret() create_samples(numsampsval) sanity_check() print "Time for Phase 2: ", timer.stop() timer.start() print "********** PHASE 3: HISTOGRAMS" histogram_of_errors() print "The histogram of errors (above) should be clustered at edges for success." histogram_of_as() print "The histogram of a's (above) should be fairly uniform." histogram_of_errors_2() print "The histogram of sample errors (above) should be clustered at edges for success." print "Time for Phase 3: ", timer.stop() timer.start() print "********** PHASE 4: ATTACK ALGORITHM" secret_mod_q() result = alg2(100000,quickflag) print "Result of Algorithm 2:", result print "Time for Phase 4: ", timer.stop() if result == True: count_successes = count_successes + 1 print "*~*~*~*~*~*~*~*~*~*~*~*~* ", count_successes, " out of ", trialnum+1, " successes so far. *~*~*~*~*~*" totaltime = timer2.stop() print "Total time for ", trialnum+1, "trials was ", totaltime return count_successes /// Preparing an embedding matrix: computing powers of the root. Finished computing the powers of the root. Embedding matrix: 0 rows out of 1 complete. }}} {{{id=9| n= 2^4 q = next_prime(2^10-2) s = 3.192 shebang(y^n+q-1,q,s,40,1, quickflag=False, polyonly=True) /// }}} {{{id=10| /// }}} {{{id=11| /// }}} {{{id=3| /// }}} {{{id=5| /// }}} {{{id=7| /// }}} {{{id=8| /// }}}