While reading on RSA I stumbled upon Dan Boneh’s Twenty Years of Attacks on the RSA Cryptosystem 1999 paper. In there, I found a trove of applied attacks against RSA; one of which, Wiener’s, employs continued fractions approximation to break RSA efficiently (under certain conditions).

The attack was interesting enough to make me want to learn about it and spread the word.

So, today we’re going to use simple math and Python to distill Wiener’s Attack :).

Professor P

Preliminaries

Listed below are some basic math and crypto concepts. Academic fellows might want Wiener’s original paper [].

Continued Fractions

A simple continued fraction is an expression of the following form x = a 0 x=a_0 x=a0​:

x = a 0 + 1 a 1 + 1 a 2 + 1 a 3 + … x = a_0 + \frac{1}{a_1 + \frac{1}{a_2 + \frac{1}{a_3 + \ldots}}} x = a 0 ​ + a 1 ​ + a 2 ​ + a 3 ​ + … 1 ​ 1 ​ 1 ​

Where a i a_i ai​s are called quotients.

A continued fraction is abbreviated by its continued fraction expansion:

x = [ a 0 , a 1 , a 2 , a 3 , … , a n ] x = [a_0, a_1, a_2, a_3, \ldots, a_n] x = [ a 0 ​ , a 1 ​ , a 2 ​ , a 3 ​ , … , a n ​ ]

Rational numbers (Irrational numbers) have finite (infinite) continued fraction expansions.

Simple examples:

73 95 = 0 + 1 1 + 1 3 + 1 3 + 1 7 = [ 0 , 1 , 3 , 3 , 7 ] \frac{73}{95} = 0 + \frac{1}{1 + \frac{1}{3 + \frac{1}{3 + \frac{1}{7}}}} = [0, 1, 3, 3, 7] 9 5 7 3 ​ = 0 + 1 + 3 + 3 + 7 1 ​ 1 ​ 1 ​ 1 ​ = [ 0 , 1 , 3 , 3 , 7 ]

π = [ 3 , 7 , 15 , 1 , 292 , 1 , 1 , 1 , 2 , … ] \pi = [3, 7, 15, 1, 292, 1, 1, 1, 2, \ldots] π = [ 3 , 7 , 1 5 , 1 , 2 9 2 , 1 , 1 , 1 , 2 , … ]

The following algorithm calculates the continued fraction expansion of a rational number with nominator n n n and denominator d d d.

def cf_expansion ( n , d ) : e = [ ] q = n // d r = n % d e . append ( q ) while r != 0 : n , d = d , r q = n // d r = n % d e . append ( q ) return e

Rational Approximations

We know that π \pi π is an irrational number. The cool thing is that we can form succinct rational approximations of π \pi π using its continued fraction expansion.

c 0 = 3 1 = 3.0 c_0 = \frac{3}{1} = 3.0 c 0 ​ = 1 3 ​ = 3 . 0

c 1 = 3 + 1 7 = 22 7 = 3.142857 c_1 = 3 + \frac{1}{7} = \frac{22}{7} = 3.142857 c 1 ​ = 3 + 7 1 ​ = 7 2 2 ​ = 3 . 1 4 2 8 5 7

c 2 = 3 + 1 7 + 1 15 = 333 106 = 3.141509 c_2 = 3 + \frac{1}{7 + \frac{1}{15}} = \frac{333}{106} = 3.141509 c 2 ​ = 3 + 7 + 1 5 1 ​ 1 ​ = 1 0 6 3 3 3 ​ = 3 . 1 4 1 5 0 9

c 3 = 3 + 1 7 + 1 15 + 1 1 = 355 113 = 3.141593 c_3 = 3 + \frac{1}{7 + \frac{1}{15 + \frac{1}{1}}} = \frac{355}{113} = 3.141593 c 3 ​ = 3 + 7 + 1 5 + 1 1 ​ 1 ​ 1 ​ = 1 1 3 3 5 5 ​ = 3 . 1 4 1 5 9 3

… \ldots …

These rational numbers ( c i c_i ci​) are called the convergents of a continued fraction. As i i i grows c i c_i ci​ approaches π \pi π (3.141592…).

The following algorithm (taken from []) calculates the convergents of a continued fraction expansion e = [ a 0 , a 1 , … , a n ] e = [a_0, a_1,\ldots , a_n] e=[a0​,a1​,…,an​]:

def convergents ( e ) : n = [ ] d = [ ] for i in range ( len ( e ) ) : if i == 0 : ni = e [ i ] di = 1 elif i == 1 : ni = e [ i ] * e [ i - 1 ] + 1 di = e [ i ] else : ni = e [ i ] * n [ i - 1 ] + n [ i - 2 ] di = e [ i ] * d [ i - 1 ] + d [ i - 2 ] n . append ( ni ) d . append ( di ) yield ( ni , di )

RSA Review

An RSA public key consists of two integers: an exponent e e e and a modulus N N N. N N N is the product of two randomly chosen prime numbers p p p and q q q. The private key, d d d, is the decryption exponent:

d = e − 1 mod ( p − 1 ) ( q − 1 ) = e − 1 mod φ ( N ) d = e^{-1}\ \text{mod}\ (p-1)(q-1) = e^{-1}\ \text{mod}\ \varphi(N) d = e − 1 mod ( p − 1 ) ( q − 1 ) = e − 1 mod φ ( N )

Where φ ( N ) \varphi(N) φ(N) is Euler’s totient function.

That is, there exists an integer k k k such that e d − k φ ( N ) = 1 ed-k\varphi(N)=1 ed−kφ(N)=1, therefore:

φ ( N ) = e d − 1 k \varphi(N) = \frac{ed - 1}{k} φ ( N ) = k e d − 1 ​

Lets assume that the attacker has an educated guess of what k k k and d d d are. Using the formula above he can simply compute φ ( N ) \varphi(N) φ(N), but what can he do with φ ( N ) \varphi(N) φ(N)? and how can he be certain that these are the correct k k k and d d d?

Factoring N with φ ( N ) \varphi(N) φ ( N )

Recall that N = p q N=pq N=pq and φ ( N ) = ( p − 1 ) ( q − 1 ) \varphi(N) = (p-1)(q-1) φ(N)=(p−1)(q−1):

φ ( N ) = ( p − 1 ) ( q − 1 ) = N − p − q + 1 = N − p − N p + 1 \begin{aligned} \varphi(N) &= (p-1)(q-1) \\ &= N -p -q +1 \\ &= N -p -\frac{N}{p} + 1 \end{aligned} φ ( N ) ​ = ( p − 1 ) ( q − 1 ) = N − p − q + 1 = N − p − p N ​ + 1 ​

Therefore:

p 2 + p ( φ ( N ) − N − 1 ) − N = 0 p^2 +p(\varphi(N) -N -1) -N = 0 p 2 + p ( φ ( N ) − N − 1 ) − N = 0

That is, given φ ( N ) \varphi(N) φ(N) ( N N N is public knowledge) we can efficiently compute the quadratic roots p 1 p_1 p1​, p 2 p_2 p2​ and simply check if p 1 p 2 = N p_1p_2 = N p1​p2​=N to ascertain correctness.

Guessing k k k and d d d

Recall that e d − k φ ( N ) = 1 ed-k\varphi(N)=1 ed−kφ(N)=1, therefore:

∣ e φ ( N ) − k d ∣ = 1 d φ ( N ) \left| \frac{e}{\varphi(N)} - \frac{k}{d} \right| = \frac{1}{d\varphi(N)} ∣ ∣ ∣ ∣ ∣ ​ φ ( N ) e ​ − d k ​ ∣ ∣ ∣ ∣ ∣ ​ = d φ ( N ) 1 ​

Hence, k d \frac{k}{d} dk​ is a rational approximation of e φ ( N ) \frac{e}{\varphi(N)} φ(N)e​ ( 1 d φ ( N ) \frac{1}{d\varphi(N)} dφ(N)1​ is small). Assuming the attacker has e φ ( N ) \frac{e}{\varphi(N)} φ(N)e​, he may find k d \frac{k}{d} dk​ among its convergents. But… the attacker doesn’t have φ ( N ) \varphi(N) φ(N), he only has the e e e and N N N.

Could it be that under certain conditions k d \frac{k}{d} dk​ may be derived from e N \frac{e}{N} Ne​?

Wiener’s Theorem

Let N = p q N=pq N=pq with q < p < 2 q q<p<2q q<p<2q. Let d < 1 3 N 1 4 d<\frac{1}{3}N^{\frac{1}{4}} d<31​N41​. Given ⟨ e , N ⟩ \langle e,N \rangle ⟨e,N⟩ with e d = 1 mod φ ( N ) ed = 1\ \text{mod}\ \varphi(N) ed=1 mod φ(N), d d d can be efficiently recovered by searching the right k d \frac{k}{d} dk​ among the convergents of e N \frac{e}{N} Ne​.

Sanity Check

So the whole attack is as follows:

Generate a vulnerable RSA keypair (one with short private exponent ( d < 1 3 N 1 4 d<\frac{1}{3}N^{\frac{1}{4}} d < 3 1 ​ N 4 1 ​ )).

)). Find the convergents of the continued fraction expansion of e N \frac{e}{N} N e ​ .

. Iterate through the convergents d i k i \frac{d_i}{k_i} ki​di​​: Compute φ i ( N ) \varphi_i(N) φ i ​ ( N ) using k i k_i k i ​ and d i d_i d i ​ . Ascertain correctness through factoring N N N with φ i ( N ) \varphi_i(N) φ i ​ ( N ) . (Alternatively, we can do [ ] instead).



Lets see if it actually works.

Vulnerable Key Generation

We simply adapt the normal RSA key generation to fit the constraints on d d d, q q q and p p p.

import gmpy2 , random from gmpy2 import isqrt , c_div urandom = random . SystemRandom ( ) def get_prime ( size ) : while True : r = urandom . getrandbits ( size ) if gmpy2 . is_prime ( r ) : return r def test_key ( N , e , d ) : msg = ( N - 123 ) >> 7 c = pow ( msg , e , N ) return pow ( c , d , N ) == msg def create_keypair ( size ) : while True : p = get_prime ( size // 2 ) q = get_prime ( size // 2 ) if q < p < 2 * q : break N = p * q phi_N = ( p - 1 ) * ( q - 1 ) max_d = c_div ( isqrt ( isqrt ( N ) ) , 3 ) max_d_bits = max_d . bit_length ( ) - 1 while True : d = urandom . getrandbits ( max_d_bits ) try : e = int ( gmpy2 . invert ( d , phi_N ) ) except ZeroDivisionError : continue if ( e * d ) % phi_N == 1 : break assert test_key ( N , e , d ) return N , e , d , p , q

The Whole Attack Flow

import cf , sys , hashlib import vulnerable_key as vk from sympy import * def sha1 ( n ) : h = hashlib . sha1 ( ) h . update ( str ( n ) . encode ( 'utf-8' ) ) return h . hexdigest ( ) if __name__ == '__main__' : N , e , d , p , q = vk . create_keypair ( 1024 ) print ( '[+] Generated an RSA keypair with a short private exponent.' ) print ( '[+] For brevity, keypair components are crypto. hashed:' ) print ( '[+] ++ SHA1(e): ' , sha1 ( e ) ) print ( '[+] -- SHA1(d): ' , sha1 ( d ) ) print ( '[+] ++ SHA1(N): ' , sha1 ( N ) ) print ( '[+] -- SHA1(p): ' , sha1 ( p ) ) print ( '[+] -- SHA1(q): ' , sha1 ( q ) ) print ( '[+] -- SHA1(phiN): ' , sha1 ( ( p - 1 ) * ( q - 1 ) ) ) print ( '[+] ------------------' ) cf_expansion = cf . get_cf_expansion ( e , N ) convergents = cf . get_convergents ( cf_expansion ) print ( '[+] Found the continued fractions expansion convergents of e/N.' ) print ( '[+] Iterating over convergents; ' 'Testing correctness through factorization.' ) print ( '[+] ...' ) for pk , pd in convergents : if pk == 0 : continue ; possible_phi = ( e * pd - 1 ) // pk p = Symbol ( 'p' , integer = True ) roots = solve ( p ** 2 + ( possible_phi - N - 1 ) * p + N , p ) if len ( roots ) == 2 : pp , pq = roots if pp * pq == N : print ( '[+] Factored N! :) derived keypair components:' ) print ( '[+] ++ SHA1(e): ' , sha1 ( e ) ) print ( '[+] ++ SHA1(d): ' , sha1 ( pd ) ) print ( '[+] ++ SHA1(N): ' , sha1 ( N ) ) print ( '[+] ++ SHA1(p): ' , sha1 ( pp ) ) print ( '[+] ++ SHA1(q): ' , sha1 ( pq ) ) print ( '[+] ++ SHA1(phiN): ' , sha1 ( possible_phi ) ) sys . exit ( 0 ) print ( '[-] Wiener\'s Attack failed; Could not factor N' ) sys . exit ( 1 )

Executing it:

$ ./wiener.py [ + ] Generated an RSA keypair with a short private exponent. [ + ] For brevity, keypair components are crypto. hashed: [ + ] ++ SHA1 ( e ) : cf50c0f6e658fae6bd416f7cb5b99dd2764b44fa [ + ] -- SHA1 ( d ) : 1772bee24f59ea13976f03510bbc32852f02c300 [ + ] ++ SHA1 ( N ) : d2d6f603c4adf7cdc0d449ca288dd130a6741c91 [ + ] -- SHA1 ( p ) : d34f85dbc869626f7cab9c367bcbfec8aad8a6d3 [ + ] -- SHA1 ( q ) : 1e93d20bf5a79200b98441ef8b82d9f76a06df8a [ + ] -- SHA1 ( phiN ) : a5835c28d591a66e57eacdeab88a0d1d0cb3d74a [ + ] ------------------ [ + ] Found the continued fractions expansion convergents of e/N. [ + ] Iterating over convergents ; Testing correctness through factorization. [ + ] .. . [ + ] Factored N ! : ) derived keypair components: [ + ] ++ SHA1 ( e ) : cf50c0f6e658fae6bd416f7cb5b99dd2764b44fa [ + ] ++ SHA1 ( d ) : 1772bee24f59ea13976f03510bbc32852f02c300 [ + ] ++ SHA1 ( N ) : d2d6f603c4adf7cdc0d449ca288dd130a6741c91 [ + ] ++ SHA1 ( p ) : 1e93d20bf5a79200b98441ef8b82d9f76a06df8a [ + ] ++ SHA1 ( q ) : d34f85dbc869626f7cab9c367bcbfec8aad8a6d3 [ + ] ++ SHA1 ( phiN ) : a5835c28d591a66e57eacdeab88a0d1d0cb3d74a

All used code snippets are available on github.

That’s it ! :) I hope it brought some value to you.

Comments and thoughts are welcome on this tweet: