Recovering RSA-512 Key Pair with Distributed CADO-NFS

Available in 中文

Intro#

RSA is currently the most widely used public-key cryptosystem in the world. In public-key cryptography, there are two kinds of keys: private keys and public keys. Private keys are generated by some algorithm and need to be kept secret by the operator. Public keys are directly derived from private keys and can be published publicly anywhere. A public key together with the private key which it’s derived from is called a key pair.

The key size of a key pair in RSA refers to the expected length (in bits) of the generated RSA modulus. It determines the overall security of the key pair. As of August 2023, the recommended least key size is 2048 bits. Unfortunately, some developers don’t take best practices for security consideration and are still using 512-bit RSA keys.

This article demonstrates recovery of the private key (PKCS#1 / PKCS#8 format) from an already known RSA-512 public key by factorizing RSA modulus with CADO-NFS.

Disclaimer: This article is intended for educational purposes only. The author does not condone or encourage any illegal activities. The information provided in this article is for informational purposes only and should not be used for any illegal activities. The author is not responsible for any misuse of the information provided in this article.

Review on RSA#

Formalization#

  • Choose two primes $p, q$ and $N:=pq$ is the modulus for the key pair.

  • Compute $\lambda(N)=\lambda(pq)=\operatorname{lcm}(p-1,q-1)$.

    Here $\lambda(n)$ is Carmichael’s totient function, defined as $\eqref{carmichael}$.

    $$ \begin{equation} \lambda(n):=\min\{m>0: \forall a \in{}\mathbf{R}^*_n, a^m\equiv 1\pmod n\}\label{carmichael} \end{equation} $$

    where $\mathbf{R}^*_{n}{}:= \{i\in{}\mathbf{R}_n: \gcd(n, i) = 1\}$ is the reduced residue system modulo $n$.

    Many textbooks are instead using Euler’s totient function $\varphi(n)$. This does not affect the correctness.

  • Choose a public exponent $e\neq 1$ so that $e\in{}\mathbf{R}^*_{\lambda(n)}$, namely $e$ is coprime to $\lambda(n)$.

  • Compute the private exponent $d:\equiv e^{-1}\pmod{\lambda(n)}$.

  • For end-to-end encryption: the encryptor $E(m):\equiv m^e\pmod N$ and the decryptor $D(m):\equiv m^d\pmod N$.

  • For signing purposes: the signer $\DeclareMathOperator*{\concat}{\Vert} S(m):=D( \operatorname{hash}(m))$

I am not giving any proof of correctness here since it’s a bit off-topic, but you can see the proof written by Stein in his book Elementary Number Theory: Primes, Congruences, and Secrets here.

The formalization here is quite pedantic, there are much more restrictions in real life implementations, like the moderate distance between $p,q$, requirement on $e>2$ and the usage of fixed value $e:=65537$ in common libraries.

Components in Real Life#

The tuple $(p, q, e)$ can uniquely determines an RSA key pair, this is quite obvious from the steps above. In theory a private key COULD be $(p, q, d)$ and the derived public key is thus $(N, e)=(pq, e)$. It’s also feasible to take $(p, q, e)$ as the private key tuple.

From mathematical context, the form of components is not unique but they are equivalent. So what stuff are actually stored in modern private key files? More specifically, what are the real components stored in PKCS#1 / PKCS#8? In fact, modern private key formats store components more than necessary, for space–time tradeoff.

Private Key Components#

Let’s generate a random key and inspect its components via OpenSSL:

 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
$ openssl genrsa 512 | tee sample.key | openssl pkey -noout -text
Private-Key: (512 bit, 2 primes)
modulus:
    00:e8:b1:ea:27:bc:3a:a6:9f:53:48:24:9c:5a:9d:
    9e:64:af:12:3e:ae:db:46:f0:4d:99:55:61:fd:c2:
    c8:45:35:25:05:e6:7b:37:c0:cc:92:7a:45:c0:5b:
    01:a3:4c:4e:87:5b:70:48:62:05:2a:6e:c2:3a:6a:
    f0:07:92:61:ad
publicExponent: 65537 (0x10001)
privateExponent:
    00:ca:ec:5c:86:ca:5f:5b:6f:15:f1:93:fb:c6:0a:
    2e:57:1f:55:89:d4:72:e5:2a:2a:81:cd:33:e7:58:
    35:ce:23:20:af:2c:e6:fd:39:4e:2f:a6:be:23:23:
    7f:e3:d2:f1:81:98:ef:48:91:a3:94:68:04:c6:a6:
    d3:49:df:a8:01
prime1:
    00:f6:a8:8b:b4:cb:45:0f:d8:b9:6b:de:32:31:87:
    c1:98:05:6d:d0:07:3d:c0:c0:8d:3d:5c:c1:1a:3a:
    64:7d:45
prime2:
    00:f1:81:fc:ee:f1:5b:01:58:0c:0a:92:e2:fe:84:
    17:f2:9d:f9:bb:e9:6a:4d:fd:77:56:37:32:76:68:
    54:15:49
exponent1:
    00:c2:d9:14:3b:c6:d3:90:98:18:a9:c0:86:10:20:
    78:eb:19:66:cd:0d:db:57:78:04:5a:f3:f9:cb:39:
    53:86:ad
exponent2:
    32:ff:5e:f8:d9:f0:7c:ad:2a:05:c5:94:b7:68:0f:
    62:85:27:6a:1d:d4:fb:d9:0d:2d:20:84:c0:6a:31:
    6f:39
coefficient:
    00:bf:63:63:7b:71:73:ab:59:66:7d:2e:6f:eb:8a:
    e3:88:a1:10:cb:db:5f:fb:d4:83:31:52:2d:97:6c:
    38:29:50

By default, openssl genrsa will generate keys in PKCS#8 format. This private key contains:

  • modulus corresponds to $N:=pq$
  • publicExponent corresponds to $e$, OpenSSL always uses fixed value $e:=65537$ when generating new keys
  • privateExponent corresponds to $d:\equiv e^{-1}\pmod{\lambda(n)}$
  • prime1 and prime2 correspond to $p$ and $q$

But who’s giving exponent1, exponent2, coefficient?

Sped-up Exponentiation#

The point of these three unknown components is to make the decryptor

$$ D(m):\equiv m^d\pmod N $$

and the signer $S(m)$ faster by Chinese remainder theorem.

In general this is actually an algorithm to compute modulus exponentiation for big numbers. Since the multiplication for big ones is not trivial operation (Karatsuba, FFT etc.) thus cannot be treated as $O(1)$, it is commonly implemented by some library.

Computing $m^d\pmod N$ is equivalent to solving $\eqref{crt-1}$, which is trivially solvable,

$$ \begin{equation} x\equiv m^d\pmod{pq}\label{crt-1} \end{equation} $$

Since $p,q$ are primes, they are obviously coprime i.e. $\gcd(p,q)=1$. Justified by CRT, equation $\eqref{crt-1}$ can be rewritten equivalently into a system $\eqref{crt-2}$,

$$ \begin{equation} \left\{ \begin{aligned} x&\equiv m^d\pmod{p} \\ x&\equiv m^d\pmod{q} \end{aligned} \right.\label{crt-2} \end{equation} $$

Now the modulus $N$ has been halved (in digits) into $p,q$, hence the exponential part $d$ is able to be reduced.

For a prime, its Carmichael’s $\lambda$ is equal to Euler’s $\varphi$, which states

$$ \lambda(p)=\varphi(p)=p-1,\lambda(q)=\varphi(q)=q-1 $$

Let $d_p:\equiv d\pmod{\lambda(p)}$ and $d_q:\equiv d\pmod{\lambda(q)}$, this states $k_p,k_q\in\mathbb{Z}$ exist so that

$$ d=k_p\lambda(p)+d_p=k_q\lambda(q)+d_q, $$

By applying Euler’s theorem, the exponential part is reduced to $\eqref{crt-3}$

$$ \begin{equation} \left\{ \begin{aligned} x&\equiv m^d\equiv m^{k_p\lambda(p)+d_p}\equiv m^{d_p}\pmod{p} \\ x&\equiv m^d\equiv m^{k_q\lambda(q)+d_q}\equiv m^{d_q}\pmod{q} \end{aligned} \right.\label{crt-3} \end{equation} $$

The smaller exponents $d_p$ and $d_q$ are called exponent1 and exponent2 in the component list above, they are precomputed in the private key.

By applying CRT algorithm here, the solution is $\eqref{crt-4}$

$$ \begin{equation} x\equiv m^{d_q} + q^*(m^{d_p}-m^{d_q})q \pmod{pq}\label{crt-4} \end{equation} $$

where $q^*:\equiv q^{-1} \pmod p$, and is called coefficient in the component list above.

In conclusion, those extra components are generally called CRT components:

  • exponent1 (paired with prime1 $p$) is $d_p:\equiv d\pmod{\lambda(p)}$, where $\lambda(p)=p-1$
  • exponent2 (paired with prime2 $q$) is $d_q:\equiv d\pmod{\lambda(q)}$, where $\lambda(q)=q-1$
  • coefficient is $q^*:\equiv q^{-1} \pmod p$

Public Key Components#

Derive the public key from previously generated private key, and inspect its components:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ openssl rsa -in sample.key -pubout | openssl pkey -pubin -noout -text
writing RSA key
Public-Key: (512 bit)
Modulus:
    00:e8:b1:ea:27:bc:3a:a6:9f:53:48:24:9c:5a:9d:
    9e:64:af:12:3e:ae:db:46:f0:4d:99:55:61:fd:c2:
    c8:45:35:25:05:e6:7b:37:c0:cc:92:7a:45:c0:5b:
    01:a3:4c:4e:87:5b:70:48:62:05:2a:6e:c2:3a:6a:
    f0:07:92:61:ad
Exponent: 65537 (0x10001)

Public keys are simpler, and the list shows exactly the tuple $(N,e)$.

Recover Key Pair#

The trapdoor in RSA ensures that an attacker cannot recover the private key easily from a public key, unless they do know the factorization of the modulus i.e. $N=pq$. Looking up the factorization on factordb is always a good idea, but the successful cases are rare in real life.

If someone is using RSA-512, the attacker can spend less than a week to factorize the modulus stored in the public key with CADO-NFS software suite, which implements NFS algorithm.

Distributed Factorization#

CADO-NFS supports a central server for distributed computation. Here are some additional tips:

  • Use same Linux distro OR same Docker image on the server and clients, if distributed

    Otherwise, you probably need to struggle with glibc version compatibility issues.

  • Pay attention to the output after CADO-NFS server starts up, which will contain a command for restoring progress, this is quite useful if the server accidentally exits.

Prerequisites#

  • Aside from common toolchains, cmake and libgmp3-dev are required to build CADO-NFS.

  • Download CADO-NFS source tarball, extract and enter the directory.

  • Run make -j8. (adjust the job number to fit your workload)

  • ./cado-nfs.py and ./cado-nfs-client.py are ready to use.

  • Get modulus and public exponent from public key pub.pem, some digits are redacted.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    $ openssl pkey -pubin -in pub.pem -noout -text
    Public-Key: (512 bit)
    Modulus:
        00:bc:dd:6e:bc:57:ae:a2:be:7d:ad:70:8f:71:0a:
        80:86:3b:c5:18:f7:ab:4d:5e:bd:a5:64:b2:65:d2:
        7c:d9:4c:5e:e3:c3:bc:33:b7:60:30:98:bd:6d:c4:
        **:**:**:**:**:**:**:**:**:**:**:**:**:**:**:
        **:**:**:**:**
    Exponent: 65537 (0x10001)
    $ openssl rsa -pubin -in public.pem -noout -modulus 
    Modulus=BCDD6EBC57AEA2BE7DAD708F710A80863BC518F7AB4D5EBDA564B265D27CD94C5EE3C3BC33B7603098BD6DC4****************REDACTED****************
    

    You just need to convert the hexadecimal form into decimal number, for example evaluate it directly in Python

    1
    2
    
    $ python -c "print(0xBCDD6EBC57AEA2BE7DAD708F710A80863BC518F7AB4D5EBDA564B265D27CD94C5EE3C3BC33B7603098BD6DC4****************REDACTED****************)"
    9891661152720289911993780388856560399924301310754393756699908896661301299157403390541217712185535804***********************REDACTED***********************
    

Server#

1
2
3
4
./cado-nfs.py 9891661152720289911993780388856560399924301310754393756699908896661301299157403390541217712185535804***********************REDACTED*********************** \
              server.ssl=no server.address=0.0.0.0 server.port=52600 server.whitelist=0.0.0.0/0 \
              tasks.workdir=$HOME/nfs-work \
              --server

Clients#

1
./cado-nfs-client.py --server=http://server:52600 --override -t 8

Change server to your server address and modify --override -t 8 to override the thread number on different clients.

Result#

The final output will be just two primes, as shown below.

95488332303787905388806934005632272891519544646395*************************** 103590259815731432651796987187054092306262180182970***************************

Factorization Result

Construct Private Key#

To construct a private key with CRT components easily, I recommend to use rsatool.py.

You’ll need to clone the repository and run pip install . in its directory first.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ python rsatool.py -n 9891661152720289911993780388856560399924301310754393756699908896661301299157403390541217712185535804***********************REDACTED*********************** \
                    -p 95488332303787905388806934005632272891519544646395*************************** \
                    -q 103590259815731432651796987187054092306262180182970*************************** \
                    -e 65537 \
                    -f PEM -o recovered.key
Using (p, q) to calculate RSA paramaters

n =
bcdd6ebc57aea2be7dad708f710a80863bc518f7ab4d5ebda564b265d27cd94c5ee3c3bc33b76030
********************redacted********************

e = 65537 (0x10001)

d =
5d9b5a611b0c41392ccd23b6d48e21f965f26057909dbe513c825631454bffde921bc51a076cc3a5
********************redacted********************

p =
d31c7b17003dec91c9709546264c76c94670a4a9********redacted********

q =
e506026099230459a9a042fac22b258da1c43647********redacted********

Saving PEM as recovered.key

Now the private key is recovered and ready to use. This tool creates keys in PKCS#1 format. To convert it to PKCS#8 format, simply run

1
$ openssl pkey -in recovered.key