The RSA cryptosystem was invented by Ron Rivest, Adi Shamir, and Len Adleman in 1977. It is a public-key encryption system, i.e. each user has a private key and a public key.
Key Generation
To generate a key, pick two random primes of the same bitlength, and compute their product.
p = random_prime(2^512)
q = random_prime(2^512)
N = p * q
Pick an integer e
, typically 3 or 17 or 65537 which is coprime to (p-1)*(q-1)
.
e = 65537
d = inverse_mod(e,(p-1)*(q-1))
The public key is (N,e)
, the private key is (N,d)
.
The computation of d
will fail if e
is not coprime.
Encryption (textbook version)
To encrypt a message, encode it as an integer between 0 and N
.
Then compute
ciphertext = pow(message,e,N)
Decryption (textbook version)
To decrypt, compute
message = pow(ciphertext,d,N)
Attacks
The private key is easily computed from the public key (N,e)
if one can get the factors p
and q
.
At 29C3 we gave a talk FactHacks reviewing methods to factor integers without further information. Please see that webpage for scripts for the different factorization methods.
Here we focus on attacks using lattices.
Coppersmith's attack for factoring with bits of p
known
These attacks assume that we know some part of one of the
factors of N
. For example, if we know the most significant bits
of p
; more precisely, let a match p
on all but the bottom 86 bits.
p = random_prime(2^512); q = random_prime(2^512)
N = p*q
a = p - (p % 2^86)
With this value of a
, the polynomial f(x) = a + x
has a small root
r
modulo p
, where r
is the least significant 86 bits of p
. This means
that both, f(x)
and N
are divisible by p
, and of course also any
multiple or power of f(x)
.
The lattice we construct satisfies that every vector is zero mod p
.
Applying LLL we will get a small element of that lattice which hopefully
matches r
.
Here is how we set up the lattice
X = 2^86
M = matrix([[X^2, 2*X*a, a^2], [0, X, a], [0, 0, N]])
B = M.LLL()
We access the first vector of the resulting basis, interpret it as a
polynomial and check whether the constant term matches r
, i.e. that
a+r
matches p
:
Q = B[0][0]*x^2/X^2+B[0][1]*x/X+B[0][2]
a+Q.roots(ring=ZZ)[0][0] == p
This should output true.
Coppersmith's small public RSA exponent attack with partially known message
What's wrong with this RSA example?
message = Integer('squeamishossifrage',base=35)
N = random_prime(2^512)*random_prime(2^512)
c = message^3 % N
Take the cube root of the ciphertext c
:
Integer(c^(1/3)).str(base=35)
and translate to base 35 to get squeamishossifrage
.
This worked because the message was so small, that message^3
was smaller than N
, so that no reduction took place.
N = random_prime(2^150)*random_prime(2^150)
message = Integer('thepasswordfortodayisswordfish',base=35)
c = message^3 % N
int(c^(1/3))==message
The last part outputs false
, meaning that reduction mod N
did
take place. However, this is an example of a stereotyped message.
We know that it starts with thepasswordfortodayis
and probably there
is a fixed length for the password itself.
We put zeros for the unknown part and define:
a = Integer('thepasswordfortodayis000000000',base=35)
X = Integer('xxxxxxxxx',base=35)
Again we set up a basis of polynomials, this time related to the partially known message.
M = matrix([[X^3, 3*X^2*a, 3*X*a^2, a^3-c],[0,N*X^2,0,0],[0,0,N*X,0],[0,0,0,N]])
B = M.LLL()
Q = B[0][0]*x^3/X^3+B[0][1]*x^2/X^2+B[0][2]*x/X+B[0][3]
Q.roots(ring=ZZ)[0][0].str(base=35)
B
is the output of running LLL and Q
takes the first basis vector
and interprets it as a polynomial. The last line computes a root of Q
and indeed we get swordfish
after proper character encoding.
Automating the factoring with bits known attack
class partial_factoring:
def __init__(self,N,a,X):
self.N = N
self.a = a
self.X = X
self.R = ZZ['x']
x = self.R.0
self.f = x+a
# k is the multiplicity of the desired roots mod N
# kd+t-1 is the degree of the polynomial that is produced
def gen_lattice(self,t=1,k=1):
dim = k+t
A = matrix(IntegerRing(),dim,dim)
x = self.R.0
X = self.X
monomial_list = [x^i for i in range(dim)]
for i in range(k):
g = self.f(X*x)^i*self.N^(k-i)
A[i] = [g.monomial_coefficient(mon) for mon in monomial_list]
for j in range(t):
g = self.f(X*x)^k*(X*x)^j
A[k+j] = [g.monomial_coefficient(mon) for mon in monomial_list]
weights = [X^i for i in range(dim)]
def getf(M,i):
return sum(self.R(b/w)*mon for b,mon,w in zip(M[i],monomial_list,weights))
return A,getf
def solve(self,t=1,k=1):
A,getf = self.gen_lattice(t,k)
B = A.LLL()
factors = []
for r,multiplicity in getf(B,0).roots():
if r not in ZZ:
continue
if gcd(Integer(self.f(r)),self.N) != 1:
p = gcd(self.f(r),self.N)
factors.append(p)
return factors
def gen_random_modulus(nlen=1024,rlen=80):
p = random_prime(2^ceil(nlen/2),False)
q = random_prime(2^floor(nlen/2),False)
N = p*q
r = lift(mod(p,2^rlen))
a = p-r
X = 2^rlen
return (N,a,X),r
def test_factoring():
(N,a,X),r = gen_random_modulus()
u = partial_factoring(N,a,X)
if a+r in u.solve(2,1):
print "Success!"
else:
print "Failure. :("
Automating the small RSA exponent attack
class univariate_coppersmith:
# degree d polynomial f
# integer modulus N
# X is the bound on the absolute value of the root
def __init__(self, f, N, X):
self.f = f
self.N = N
self.X = X
self.R = QQ['x']
# k is the multiplicity of the desired roots mod N
# kd+t-1 is the degree of the polynomial that is produced
def gen_lattice(self,t=1,k=1):
d = self.f.degree()
dim = k*d+t
A = matrix(IntegerRing(),dim,dim)
x = self.R.0
X = self.X
monomial_list = [x^i for i in range(dim)]
for i in range(k):
for j in range(d):
g = self.f(X*x)^i*(X*x)^j*self.N^(k-i)
A[d*i+j] = [g.monomial_coefficient(mon) for mon in monomial_list]
for j in range(t):
g = self.f(X*x)^k*(X*x)^j
A[k*d+j] = [g.monomial_coefficient(mon) for mon in monomial_list]
weights = [X^i for i in range(dim)]
def getf(M,i):
return sum(self.R(b/w)*mon for b,mon,w in zip(M[i],monomial_list,weights))
return A,getf
def solve(self,t=1,k=1):
A,getf = self.gen_lattice(t,k)
B = A.LLL()
roots = []
for r,multiplicity in getf(B,0).roots():
if mod(self.f(r),self.N) == 0:
roots.append(r)
return roots
# can solve up to rlen < nlen/d
def gen_random_coppersmith(nlen=1024, rlen=150, d=3):
p = random_prime(2^floor(nlen/2),False)
q = random_prime(2^ceil(nlen/2),False)
N = p*q
a = ZZ.random_element(0,2^(N.nbits()-rlen))
r = ZZ.random_element(0,2^rlen)
m = a+r
c = lift(mod(m,N)^d)
x = ZZ['x'].0
f = (x+a)^d-c
X = 2^rlen
return (f,N,X),r
def test_coppersmith():
(f,N,X),r = gen_random_coppersmith()
u = univariate_coppersmith(f,N,X)
if r in u.solve():
print "Success!"
else:
print "Failure. :("
Version: This is version 2017.12.28 of the "RSA" web page.