# RSA 總整理 RSA 是一個著名的非對稱式加密系統,其安全性建立在大數分解的難度上 目前 RSA 僅能透過旁通道攻擊(Side-Channel Attack) --- <center> <strong><font size=5> RSA 基本內容 </font></strong> </center> </br> 接下來我們需要先介紹 RSA 所需的數學知識 #### 尤拉函數 Eular Function 我們會以 $\phi$ (念作 phi) 表示尤拉函數 定義 $\phi(p)$ 為在所有的正整數當中,有多少 $< p$ 的數字與 $p$ 互質的數字 在這個定義之下,如果說 $p$ 是一個質數的話,那麼所有 $< p$ 的正整數也必定與之互質,因此此時的 $\phi(p) = p-1$ 這也就是費馬小定理 在 RSA 當中,我們會取兩個質數 $p, q$,並計算 $n = p \times q$ 此時的 $\phi(n) = \phi(p) \times \phi(q) = (p-1) \times (q-1)$ #### 模反元素 Modular Inverse 一個整數 $a$ 在模(mod) $n$ 的模反元素是指能滿足以下式子的整數 $b$ $$ a^{-1} \equiv b\ (mod\ n) $$ 或是表示成 $$ ab \equiv 1\ (mod\ n) $$ :::info 就如同要找到一個數 $a$ 的乘法反元素,等同找到一個數字 $b$ 滿足 $$ ab = 1 $$ 此時的 $b = \frac{1}{a}$,也就是我們常見的倒數 ::: 在 RSA 當中,我們會去找某個數字 $e$ 在模(mod) $\phi(n)$ 下的模反元素 $d$ $$ \begin{align*} & e^{-1} \equiv d\ (mod\ \phi(n))\\ \rightarrow \ & ed \equiv 1\ (mod\ \phi(n))\\ \rightarrow \ & ed\ mod\ \phi(n) = 1 \end{align*} $$ :::info $$ e^{-1} \neq \frac{1}{e} $$ 由於是在模底下運作,這個是不成立的! ::: 這裡還有一個重要的性質 $$ gcd(e, \phi(n)) = 1 $$ 因為唯有這個條件成立,才會存在模反元素 $d$ :::success 如果不知道為什麼,不妨舉幾個數字來試試看,例如 - $e = 2$ 且 $\phi(n) = 4$ - $e = 6$ 且 $\phi(n) = 8$ 會發現到無論如何都不會找到解,這裡先不寫證明 ::: 在 python 當中要求模反元素,可以使用 python 的 modules 輕鬆達成 - gmpy2 ```python= >>> from gmpy2 import invert >>> invert(3, 7) mpz(5) ``` - PyCryptodome ```python= >>> from Crypto.Util.number import inverse >>> inverse(3, 7) 5 ``` --- ### RSA Encryption 在 RSA 當中有幾個常用的符號 | 符號 | 意義 | | ---- | -------- | | $m$ | 明文 | | $c$ | 密文 | | $e$ | 加密指數 | | $d$ | 解密指數 | | $n$ | 模數 | RSA 加密的流程如下: 1. 找兩個質數 $p, q$,並令 $n = p \times q$ 需要符合以下條件才能保證其安全性 - $n > m$ - $2q > p > q$ 2. 選一個加密指數 $e$,使得 $gcd(e, \phi(n)) = 1$ 3. $c = m^e\ mod\ n$ ### RSA Decryption RSA 解密流程如下 1. 嘗試將 $n$ 分解出兩質數 $p, q$ 2. 計算 $\phi(n) = (p-1) \times (q-1)$ 3. 找出解密指數 $d$ 使得 $ed \equiv 1\ (mod\ \phi(n))$ 4. $m = c^d\ mod\ n$ 第一步的分解可以透過下列的工具協助 - [factordb](http://www.factordb.com/) - [alpetron](https://www.alpertron.com.ar/ECM.HTM) - yafu - SageMath :::success <center> <strong>驗證 RSA 解密的正確性</strong> </center> > 證明 $m = c^d \% n$ 已知 $m < n$ 要證明 $m = c^d \% n$ 等同於證明 $c^d \% n - m = 0$ </br> $$ \begin{align*} c^d \% n - m &= 0\\ &= (m^e \ \% \ n)^d \ \% \ n - m \quad \quad \quad (c = m^e \ \% \ n)\\ &= m^{ed} \ \% \ n - m\\ &= m^{k(p-1)(q-1)+1} \ \% \ n - m \quad \quad (ed \equiv 1\ mod\ \phi(n) \Rightarrow ed = k\phi(n) + 1, k \in \mathbb{R})\\ &= m(m^{k(p-1)(q-1)} - 1) \ \% \ n \end{align*} $$ </br> 根據費馬小定理可知 $a^{p-1} \equiv 1\ (mod\ p) \quad$ ($p$ 為質數) $$ \begin{align*} &\Rightarrow (m^{k(p-1)})^{q-1} \equiv 1\ (mod\ q)\\ &\Rightarrow (m^{k(q-1)})^{p-1} \equiv 1\ (mod\ p)\\ &\Rightarrow m^{k(p-1)(q-1)} \equiv 1\ \ (mod\ pq)\\ &\Rightarrow m^{k(p-1)(q-1)} \equiv 1\ \ (mod\ n) \end{align*} $$ 因此 $m(m^{k(p-1)(q-1)} - 1) \% n\\ = m(1 - 1) \% n = 0 \quad \blacksquare$ ::: ### Convert 要做 RSA encryption 運算必須要是一串數字 做完 RSA decryption 後會得到的是一串數字 因此我們須要有可以將字串跟數字之間做轉換的工具,以下使用 pyCryptodome - 字串轉為數字 ```python= >>> from Crypto.Util.number import long_to_bytes >>> long_to_bytes(1567694910997846802713134265455563233695200125) b'FLAG{long_to_bytes}' ``` - 數字轉為字串 ```python= >>> from Crypto.Util.number import bytes_to_long >>> bytes_to_long(b'FLAG{bytes_to_long}') 1567694910997644772753309158996360910989059965 ``` --- ### 範例題目 ``` n = 641038552669922004650640356715638051098436533497923620822471 e = 65537 c = 537756663490573782815200170116248828935623440674192611059116 ``` 透過 RSA decryption 的公式 $m = c^d\ mod(n)$ 可以知道我們還缺 $d$ ,而 $d$ 為 $e$ 在模 $\phi(n)$ 下的模反元素 先求 $\phi(n)$ 因為這題的 $n$ 還不大,可以透過上述的工具先做分解,可以得到 - $p = 670839939121655241517135431571$ - $q = 955576010440355093873649387901$ 則 $\phi(n) = (p-1) \times (q-1)$ $d$ 就可以透過 $e, \phi(n)$ 計算出來 **sol.py** ```python= #!/usr/bin/python3 from Crypto.Util.number import * n = 641038552669922004650640356715638051098436533497923620822471 e = 65537 c = 537756663490573782815200170116248828935623440674192611059116 p = 670839939121655241517135431571 q = 955576010440355093873649387901 phi = (q - 1) * (p - 1) d = inverse(e , phi) print(long_to_bytes(pow(c , d , n))) ``` --- <center> <strong><font size=5> RSA Side-Channels </font></strong> </center> ## RSA Little e 當 $n$ 過大且 $e$ 過小使得 $m^{e} < n$,此時只要對 $c$ 開 $e$ 次方根就可以得到 $m$ 先看一下 RSA encryption $$ c = m^e \% n $$ 如果 $m^e < n$,就意味著 $\% n$ 這個操作根本沒做 也就是說 $c = m^e$,倒過來說 $$ m = \sqrt[e]{c} $$ --- ### 範例題目 ``` n = 5489682192316422625434821656898117285064026279334711820823698107520079986854633408581065889926979914843916643565952889381426707291132831900665471049142135375628581051708437366655536056332623163988398065066438737021804605433412757434719710964824708642384163317493038636229777958794272501125366402189914988842955471879476018505743333 e = 5 c = 8695825648499005816846784896009806853240301482143432878264036921549622676046441329661491555770611113772567007417487958975920139817671636465013430373715241659378827911749003801125730213937874981663934259422055079597638258783093392939657595833683452469465273839501 ``` 可以使用 Python 開多次方根的工具 ```python= >>> from gmpy2 import iroot >>> iroot(27, 3) (mpz(3), True) ``` 輸出的結果第一個值會告訴我們結果,第二個值會告訴我們是否可以完全開 $n$ 次方號 如果我們只要取數字的話 ```python= >>> from gmpy2 import iroot >>> iroot(27, 3)[0] mpz(3) ``` **sol.py** ```python= #!/usr/bin/python3 from gmpy2 import * from Crypto.Util.number import * n = 5489682192316422625434821656898117285064026279334711820823698107520079986854633408581065889926979914843916643565952889381426707291132831900665471049142135375628581051708437366655536056332623163988398065066438737021804605433412757434719710964824708642384163317493038636229777958794272501125366402189914988842955471879476018505743333 e = 5 c = 8695825648499005816846784896009806853240301482143432878264036921549622676046441329661491555770611113772567007417487958975920139817671636465013430373715241659378827911749003801125730213937874981663934259422055079597638258783093392939657595833683452469465273839501 flag = iroot(c , 5)[0] print(iroot(c , 5)) print(long_to_bytes(flag).decode()) ``` --- ## RSA TwinPrime 當 $p$ 和 $p+2$ 皆為質數時稱作孿生質數 已知 $$ n_1 = p \times q \quad n_2 = (p+2) \times (q+2) $$ 其中 $p, p+2$ 以及 $q, q+2$ 皆為孿生質數 那麼 $$ n_1 = p \times q\\ n_2 = (p+2) \times (q+2) = pq + 2p + 2q + 4\\ $$ 可以藉此得到 $\phi(n_1)$ 以及 $\phi(n_2)$ $$ \begin{align*} \phi(n_1) &= (p-1) \times (q-1) = pq - (p+q) + 1\\ \phi(n_2) &= (p+1) \times (q+1) = pq + (p+q) + 1 \end{align*} $$ 而 $p+q$ $$ \frac{n_2 - n_1 - 4}{2} = p + q $$ 因此,在不分解 $n_1, n_2$ 的前提下,我們就可以得到 $\phi(n_1)$ 以及 $\phi(n_2)$,並藉此進一步得到 $d$,然後解出原文 $m$ --- ### 範例題目 ``` n1 = 3593874939884522738360081188589590942537982121904663270426462920730452822866381549106050173717138442489161338601666460445680189833995562392260726330939659090382885163937026571902869123413995198480029534611435826740336455815556935256575605610959114838074076799488081282449 n2 = 3593874939884522738360081188589590942537982121904663270426462920730452822866381549106050173717138442489161338601666460449137123359408777399863055172836432363160566421893035214503585639512339952271274564675355484330533094284391934282566872891805164209109744997389364339953 e = 65537 c2 = 1452263324646172405378479278086175738198155482064889627046654734720235493221282450178456894139596530399913842093175193746761395688114601838969755236143605806012997830096042176122993514497200279356143322897735756185292885577845775294136832076476786425175672990487855382256 ``` 根據前面的敘述,可以分得到 $$ \begin{align*} \phi(n_1) &= (p-1) \times (q-1) = pq - (p+q) + 1\\ \phi(n_2) &= (p+1) \times (q+1) = pq + (p+q) + 1\\ p + q &= \frac{n_2 - n_1 - 4}{2} \end{align*} $$ 有了 $\phi$ 以及 $e$ 就可以得到 $d$,也就可以解密了 **sol.py** ```python= #!/usr/bin/python from Crypto.Util.number import long_to_bytes , inverse n1 = 3593874939884522738360081188589590942537982121904663270426462920730452822866381549106050173717138442489161338601666460445680189833995562392260726330939659090382885163937026571902869123413995198480029534611435826740336455815556935256575605610959114838074076799488081282449 n2 = 3593874939884522738360081188589590942537982121904663270426462920730452822866381549106050173717138442489161338601666460449137123359408777399863055172836432363160566421893035214503585639512339952271274564675355484330533094284391934282566872891805164209109744997389364339953 e = 65537 c2 = 1452263324646172405378479278086175738198155482064889627046654734720235493221282450178456894139596530399913842093175193746761395688114601838969755236143605806012997830096042176122993514497200279356143322897735756185292885577845775294136832076476786425175672990487855382256 pq = n1 p_q = (n2 - n1 - 4) // 2 phi1 = pq - p_q + 1 phi2 = pq + p_q + 1 d1 = inverse(e , phi1) d2 = inverse(e , phi2) c1 = pow(c2 , d2 , n2) flag = pow(c1 , d1 , n1) print(long_to_bytes(flag)) ``` --- ## Wiener Attack Wiener Attack 是在特定情況下提供我們猜測 $k, d$ 的方式($k$ 稍後說明) 透過猜測 $k, d$ 得到 $p, q$,因此得到 $\phi(n)$ 以及明文 接下來我們需要先介紹 Wiener Attack 所需的數學知識 ### 連分數 Continued fraction 在數學當中連分數以下列方式表達: $$ x = a_0 + \frac{1}{a_1 + \frac{1}{a_2 + \frac{1}{a_3 + \frac{1}{\ddots}}}} \quad a_i \in \mathbb{Z}^{+} \texttt{ for all } i $$ 並且也可以用 list 的方式表達,我們會稱呼為 **continued fraction expansion** $$ x = [a_0, a_1, a_2, a_3, \dots, a_n] $$ 並且可以保證所有的**有理數**都有有限的連分數展開式 以 $\frac{73}{95}$ 舉例來說 $$ \begin{align*} & \frac{73}{95}\\ = &\ 0 + \frac{1}{\frac{95}{73}}\\ = &\ 0 + \frac{1}{1 + \frac{1}{\frac{73}{22}}}\\ = &\ 0 + \frac{1}{1 + \frac{1}{3 + \frac{1}{\frac{22}{7}}}}\\ = &\ 0 + \frac{1}{1 + \frac{1}{3 + \frac{1}{3 + \frac{1}{7}}}}\\ = &\ [0, 1, 3, 3, 7] \end{align*} $$ 從範例當中可以發現,我們會不斷的透過倒數以及商數使得連分數不斷擴張,直到商數為 $0$ 結束 ```python= # Continued Fraction Expansion of Rationals 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 ``` 同時也會有一個數列 $[c_0, c_1, c_2, \dots, c_n]$ 稱作 **convergents of the continued fraction expasion** $$ \begin{align*} c_0 = 0 &= \frac{0}{1}\\ c_1 = 0 + \frac{1}{1} &= \frac{1}{1}\\ c_2 = 0 + \frac{1}{1 + \frac{1}{3}} &= \frac{3}{4}\\ c_3 = 0 + \frac{1}{1 + \frac{1}{3 + \frac{1}{3}}} &= \frac{10}{13}\\ c_4 = 0 + \frac{1}{1 + \frac{1}{3 + \frac{1}{3 + \frac{1}{7}}}} &= \frac{73}{95} \end{align*} $$ 也就是一個不斷逼近於 $\frac{73}{95}$ 的數列 同樣的,對於一個小數也可以寫成連分數的型態 以 $2.875$ 舉例來說 $$ \begin{align*} & 2.875\\ = &\ 2 + 0.875\\ = &\ 2 + \frac{1}{\frac{1}{0.875}}\\ = &\ 2 + \frac{1}{1 + \frac{1}{\frac{1}{0.14285714285714}}}\\ = &\ 2 + \frac{1}{1 + \frac{1}{7 + \frac{1}{\frac{1}{0.00000000000014}}}}\\ = &\ 2 + \frac{1}{1 + \frac{1}{7}} \end{align*} $$ ```python= def convergents(e): n = [] # Nominators d = [] # Denominators 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: # i > 1 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) ``` --- 接下來是關於 Wiener Attack 的觀察 ### 觀察一 $$ \begin{align*} \phi(n) &= (p-1)(q-1)\\ &= pq - p - q + 1\\ &= n - p - q + 1\\ &= n - p - \frac{n}{p} + 1 \end{align*} $$ 接下來同乘以 $p$ 並移項 $$ \begin{align*} &p\phi(n) = pn - p^2 - n + p\\ \Rightarrow\ &p^2 + p(\phi(n) - n - 1) + n = 0 \end{align*} $$ 從觀察一可以發現,只要有 $\phi(n)$ 就可以透過解一元二次方程式的根得到 $p, q$ ### 觀察二 我們知道 $ed \equiv 1 \ mod\ \phi(n)$ 所以 $ed = k\phi(n) + 1 \texttt{ for some }k$ $$ \phi(n) = \frac{ed-1}{k} $$ 從觀察二可以發現,只要知道 $k, d$ 就可以解出 $\phi(n)$ --- ### Wiener Attack 從上面的兩個觀察當中可以得知,只要解出 $k, d$ 就可以解出 $p, q$,也就可以解密 而 Wiener Attack 其實就是提供我們一種猜 $k, d$ 的方式 回顧觀察二並將式子同除以 $d\phi(n)$ 做整理 $$ \begin{align*} ed - k\phi(n) &= 1\\ \frac{e}{\phi(n)} - \frac{k}{d} &= \frac{1}{d\phi(n)} \end{align*} $$ 由於 $d\phi(n)$ 很大,可以將 $\frac{1}{d\phi(n)}$ 視為 $0$ 整理式子後會發現 $$ \frac{k}{d} = \frac{e}{\phi(n)} $$ 而 $\phi(n) \approx n$ $$ \frac{k}{d} \approx \frac{e}{n} $$ 得到這個結論後,我們就可以透過尋找 $\frac{e}{n}$ 的 **convergents of the continued fraction expansion** 猜看看 $k, d$ 每猜一次就帶回去求 $p, q$,然後檢查 $pq = n$ 是否成立,不是就再找下一個 ### 限制 - $d < \frac{1}{3}n^{\frac{1}{4}}$ - $q < p < 2p$ ### 時間複雜度 知道 $k, d$ 之後回推 $\phi(n)$ 只需要 $O(1)$ $$ \phi(n) = \frac{ed}{k} $$ 知道 $\phi(n)$ 之後解方程式的根也只需要 $O(1)$ $$ p, q = \frac{-(\phi(n) - n - 1) \pm \sqrt{(\phi(n) - n - 1)^2 - 4n}}{2} $$ 而計算 continued fraction expansion 實際上就是在做輾轉相除法 所以嘗試所有的 convergents 需要 $O(log(min(e, n)))$ 並且 $e < \phi(n) < n$,因此時間複雜度計為 $O(log(e))$ 總時間複雜度為 $O(log(e))$ --- ### 範例題目 **task.py** ```python= #!/usr/bin/python3 from random import SystemRandom from gmpy2 import isqrt, c_div , invert from Crypto.Util.number import getPrime , bytes_to_long urandom = SystemRandom() def create_keypair(size): while True: p = getPrime(size // 2) q = getPrime(size // 2) if q < p < 2 * q: break n = p * q phi = (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(invert(d, phi)) if (e * d) % phi == 1: break except: continue return n , e , d , p , q n , e , d , p , q = create_keypair(1024) from secret import flag m = bytes_to_long(flag) c = pow(m , e , n) a = """n = {} e = {} c = {}""".format(n , e , c) with open("output.py" , "w") as f: f.write(a) ``` **output.py** ```python= n = 68126437388977012491646508485989748986441131328587518477687317430610358060299259889270096202942172667026765693121007244799277906816721129879871839668534245685223273320285357819059314305041568152938631311930694380458197864770594205779250872221180313165818499006934672425382344026926216633834392646021142886769 e = 15950795120525802915045576886792784828953195706868419082039089065630551280007920240748167126298220976040006667069839541710102987502293373955109113066554922904845606595280968695858510227233594594345712943776826231898942926115077969092158946886802684295287362659572633661590452939636520740933192957991470727487 c = 64084228016799140236089362630474537548271913453940735254127977713219774643382144415063040580056793784702386932065587548669067894646547290107939535980687043376839097247039277684116912998816751608587498829246494557174907620772811870282348713591834440433602622206371153329743437983172303794473772691521200966081 ``` 根據前面的推論,先將 convergents of the continued fraction expasion 算出來 接著對於每一組 convergents 去猜測 $k, d$ 最後求出 $\phi(n), p, q$ 並確認 $pq = n$ 是否成立即可 **sol.py** ```python= #!/usr/bin/python3 from Crypto.Util.number import long_to_bytes from gmpy2 import iroot n = 68126437388977012491646508485989748986441131328587518477687317430610358060299259889270096202942172667026765693121007244799277906816721129879871839668534245685223273320285357819059314305041568152938631311930694380458197864770594205779250872221180313165818499006934672425382344026926216633834392646021142886769 e = 15950795120525802915045576886792784828953195706868419082039089065630551280007920240748167126298220976040006667069839541710102987502293373955109113066554922904845606595280968695858510227233594594345712943776826231898942926115077969092158946886802684295287362659572633661590452939636520740933192957991470727487 c = 64084228016799140236089362630474537548271913453940735254127977713219774643382144415063040580056793784702386932065587548669067894646547290107939535980687043376839097247039277684116912998816751608587498829246494557174907620772811870282348713591834440433602622206371153329743437983172303794473772691521200966081 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 def convergents(e): n = [] # Nominators d = [] # Denominators 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: # i > 1 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) def solve(phi, n): b = (phi - n - 1) D = iroot(pow(phi - n - 1, 2)-4*n, 2)[0] return ((-b + D) // 2, (-b - D) // 2) exp_lst = cf_expansion(e, n) for i in convergents(exp_lst): k, d = i if(k == 0): continue phi = e*d//k p, q = solve(phi, n) if(p*q == n): print(long_to_bytes(pow(c, d, n))) break ``` --- ### RSA Side-Channel(待補充內容) 這邊我們只詳細介紹了 little e 以及 twin prime ,不過還有其他有趣的部份可以繼續研究 - $d$ 過小 - [Boneh-Durfee Attack](https://eprint.iacr.org/2016/353) - 同 $n$ 不同 $e$ - [Common Modulus](https://eprint.iacr.org/2009/037.pdf) - 同 $e$ 不同 $n$ - [Håstad's Broadcast](https://en.wikipedia.org/wiki/Coppersmith's_attack) ###### tags: `Crypto`