POSTS2017

RSA 简介

rsa jupyterlite 简易原理

主要算法简介

RSA 的原理是在于能够找到三个很大的正整数 \(e,d,n\) 使得对于任何 \(0 \le m \lt n\):

\[ (m^e)^d \equiv m \pmod{n} \]

其中公钥为 \((n,e)\),可以发送给任何人;私钥为 \((n,d)\),只能由所有者掌握。RSA 支持 4 种操作:加密/解密,签名/验证签名。

加密

首先用一些编码手段把需要加密的信息转换成整数 \(m\),用公钥中的两个参数计算

\[ c = m^e \pmod{n} \]

其中 \(c\) 就是加密后的信息了。

解密

由私钥所有者计算

\[ \begin{align} c^d \pmod{n} &= (m^e \pmod{n})^d \pmod{n} \\ &= (m^e)^d \pmod{n} \\ &= m \pmod{n} \end{align} \]

就能还原原来的信息了(乘积与其余数乘积同余,见同余命题 a

签名

  1. 首先对要签名的信息作一个 message digest,常用的如 MD5 和 SHA 系列;
  2. 然后把这个 digest 转换成整数 \(m\) 使得 \(1 \le m \lt n\);
  3. 最后用私钥 \((n,d)\) 计算 \(s = m^d \pmod{n}\)

验签

  1. 用公钥 \((n,e)\) 计算 \(v = s^e \pmod{n}\)
  2. 把 \(v\) 转换回 digest
  3. 独立对要签名的信息再作一次 digest
  4. 对比这两个 digest,如果相同的话,那这个签名就是有效的

Note

可以看到加解密和签名验签刚好是两个方向:前者是公钥加密发送给所有者解密,后者是用私钥签名发送给其他人用公钥验签;所以这就是为何一般不建议使用相同的 key 同时用作加密和签名

Using the same key for encryption and signing

Given that the underlying mathematics is the same for encryption and signing, only in reverse, if an attacker can convince a key holder to sign an unformatted encrypted messageusing the same key then she gets the original.

公私钥生成

生成 \(e,d,n\) 的算法大致如下:

  1. 生成两个大的随机质数 \(p\) 和 \(q\)
  2. 计算 \(n = pq\),\(n\) 是公钥的一部分
  3. 计算 \(n\) 的欧拉函数 \(\varphi(n) = (p - 1)(q - 1)\)
  4. 选择公钥幂 \(1 < e < \varphi(n)\),且 \(e\) 和 \(\varphi(n)\) 互质
  5. 计算私钥幂 \(d\) 使得 \(ed \equiv 1 \pmod{\varphi(n)}\)
  6. \((e, n)\) 是公钥,其余 \(p,q,d,\varphi(n)\) 这些都需要保密

通过这样算法生成出来的 \(e,d,n\) 即可满足最开始的要求,证明可看下面

注意 \(e\) 和 \(n\) 是公开的,而通过 \(\varphi(n)\) 和 \(e\) 则可以算出 \(d\),故其实 RSA 的安全性即来自于 从 \(n\) 难以计算出 \(\varphi(n) = (p - 1)(q - 1) = (n + 1) - (p + q)\), 这是因为大数质数分解目前没有有效算法, 也就是从 \(n\) 难以分解出 \(p,q\),也就难以算出 \(\varphi(n)\) 了

实现细节

更多细节有兴趣可以看一下下面参考的链接,稍微摘录一些:

  • 计算 \(y = x^e \pmod{n}\) (所谓 modular exponentiation)的复杂度大约为 \(O(k^3)\),其中 \(k\) 是 \(n\) 的长度(例如 1024 bits),且当 \(e\) 的二进制表示中 1 的数量越多则越慢。
  • 公钥幂 \(e\) 一般选取固定的:例如 \[ \begin{split} 3 = 2^{2^0}+1 \\ 5 = 2^{2^1}+1 \\ 17 = 2^{2^2}+1 \\ 257 = 2^{2^3}+1 \\ 65537 = 2^{2^4}+1 \end{split} \] 这5个是前五个 fermat number:\(F_x = 2^{2^x}+1\) ,恰好都是素数,但 \(F_5\) 以及后面的 fermat number 不是素数。显然这些数字的二进制表示只有两个 1,如上述原因,在加密的过程中能节省计算时间。实际应用中一般都是使用 65537 ,因为小的 \(e\) 不太安全。
  • 多个素数相乘结果作为模的一个好处是可以更快速的计算 \(m = c^d \pmod{n}\)
    • 使用中国剩余定理(Chinese Remainder Theorem,CRT)可以把这个计算转换成对 \(p,q\) 的 modular exponentiation 计算,而 \(p,q\) 的长度都只有 \(n\) 的一半,所以复杂度会降低到原来的四分之一:\(O(2(\cfrac{k}{2})^3) = O(\cfrac{k^3}{4})\)

存储格式

如上所述,RSA 的公私钥实际上就是些数字,但我们平时实际使用的是 .pem 格式(或 .der)的 key 。

.pem 格式头部和尾部的有一些标识符(-----BEGIN XXXX-----)这些是为了让 parser 可以马上知道这个文件包含的是公钥私钥,或是证书等。

中间的数据其实是 base64 编码过后的 DER(Distinguished Encoding Rules) 编码的 ASN.1(Abstract Syntax Notation One) 数据。

不妨写个程序看一下(dec.py):

import sys
from base64 import b64decode
from pyasn1.codec.der.decoder import decode as der_decode
from pyasn1_modules import rfc2437

def read_key_pem(src):
    if isinstance(src, bytes):
        src = src.decode('ascii')
    data = ''.join((line.strip() for line in src.split('\n') if not line.startswith('-----')))
    return der_decode(b64decode(data), asn1Spec=rfc2437.RSAPrivateKey())[0]

obj = read_key_pem(open(sys.argv[1]).read())
print(obj.prettyPrint())

生成一个 rsa private key 来跑一下:

$ openssl genrsa -out pri.pem 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
...........................+++++
............+++++
e is 65537 (0x010001)
$ python ./dec.py pri.pem
RSAPrivateKey:
 version=0
 modulus=30078218357432552470030675464869....
 publicExponent=65537
 privateExponent=5666198999662210....
 prime1=17622717115310961559542629....
 ...

可以看到实际上存储的就是上面所述的各种数字,完整的定义是在 PKCS#1 标准中:

RSAPrivateKey ::= SEQUENCE {
  version           Version,
  modulus           INTEGER,  -- n
  publicExponent    INTEGER,  -- e
  privateExponent   INTEGER,  -- d
  prime1            INTEGER,  -- p
  prime2            INTEGER,  -- q
  exponent1         INTEGER,  -- d mod (p-1)
  exponent2         INTEGER,  -- d mod (q-1)
  coefficient       INTEGER,  -- (inverse of q) mod p
  otherPrimeInfos   OtherPrimeInfos OPTIONAL
}

其中 modulus/publicExponent/privateExponent 即模/公钥幂/私钥幂,可以看到实际中的 RSA 私钥也是包含 publicExponent 的; 所以这就是为什么 openssl 工具中能从私钥中导出公钥的原因。

更新 2022-09-10: 现在也可以用 jupyterlite 来跑上述程序啦~

相关数学

同余命题

仅列出一些会用到同余式命题

a \(ab \equiv r_ar_b \pmod{n}\),其中 \(r_a = a \pmod{n}\),\(r_b = b \pmod{n}\)

Proof

设 \(a = k_an + r_a\),\(b = k_bn + r_b\),则 \(ab = k_ak_bn^2 + k_ar_bn + k_br_an + r_ar_b\), 则有 \(ab \equiv r_ar_b \pmod{n}\)

b 若 \(a\) 和 \(n\) 互质,且 \(ab \equiv ac \pmod{n}\),则有 \(b \equiv c \pmod{n}\)

Proof
  1. 由于 \(ab \equiv ac \pmod{n}\),故有 \(ab = kn + r\) 和 \(ac = qn + r\)
  2. 两式相减得 \(a(b - c) = (k - q)n\)
  3. 由于 \(a\) 和 \(n\) 互质,则 \(n\) 必然是 \(b - c\) 的因子,即 \(b - c \equiv 0 \pmod{n}\),即 \(b \equiv c \pmod{n}\)

c 若 \(a\) 和 \(b\) 均与 \(n\) 互质,则 \(ab \pmod{n}\) 也与 \(n\) 互质

Proof

令 \(ab = kn + r\),若余数 \(r\) 与 \(n\) 不互质,则存在一个 \(n\) 和 \(r\) 的公共因子 \(x\) 且 \(x > 1\), 于是有 \(ab = (kn_x + r_x)x\),但无论 \(a\) 还是 \(b\) 都跟 \(n\) 互质,不可能有 \(x\) 作为因子,矛盾

d 若 \(a \equiv b \pmod{p}\) 且 \(a \equiv b \pmod{q}\),又 \(p\) 和 \(q\) 互质, 则有 \(a \equiv b \pmod{pq}\)

Proof

由于 \(a - b\) 能同时被 \(p\) 和 \(q\) 整除,所以也能被 \(lcm(p, q)\) (最小公倍数)整除,又因为 \(p\) 和 \(q\) 互质, 故 \(lcm(p, q) = pq\)

Euler’s totient function

欧拉函数
\(\varphi(n)\) 定义为所有小于等于 \(n\) 且和 \(n\) 互质的自然数总数

例如

\[\begin{eqnarray} \varphi(1) &= |\{1\}| &= 1 \\ \varphi(2) &= |\{1\}| &= 1 \\ \varphi(3) &= |\{1, 2\}| &= 2 \\ \varphi(4) &= |\{1, 3\}| &= 2 \\ \varphi(5) &= |\{1, 2, 3, 4\}| &= 4 \\ \varphi(6) &= |\{1, 5\}| &= 2 \\ \varphi(7) &= |\{1, 2, 3, 4, 5, 6\}| &= 6 \\ \varphi(8) &= |\{1, 3, 5, 7\}| &= 4 \\ \varphi(9) &= |\{1, 2, 4, 5, 7, 8\}| &= 6 \\ \end{eqnarray}\]

特别地

若 \(n\) 是质数,则 \(\varphi(n) = n - 1\)

因为除了 \(n\) 自己外,其他小于它的数均与之互质

若 \(n = pq\) 且 \(p,q\) 均为质数,则 \(\varphi(n) = (p - 1)(q - 1)\)

这是因为少于等于 \(n\) 且跟 \(n\) 互质的只能是:

  • \(p\) 的倍数,即 \(p, 2p, ...., qp\) 共 \(q\) 个
  • 或者是 \(q\) 的倍数,即 \(q, 2q, ...., pq\) 共 \(p\) 个

于是 \(\varphi(n) = n - q - p + 1 = (p - 1)(q - 1)\)(\(pq\) 多减了一次所以加 1)

Euler’s theorem

欧拉定理

若 \(a\) 和 \(n\) 互质,有 \[a^{\varphi(n)} \equiv 1 \pmod{n}\]

Proof

假设所有小于等于 \(n\) 且和 \(n\) 互质的自然数集合为 \(S_n = \{s_1, s_2, ..., s_{\varphi(n)}\}\),令 \(a\) 为任意与 \(n\) 互质的自然数, 再令 \(r_i = as_i \pmod{n}\),则所有 \(r_i\) 组成集合 \(R_n = \{r_1, r_2, ..., r_{\varphi(n)}\}\),下面证明实际上 \(S_n = R_n\):

  • 任一 \(r_i\) 也与 \(n\) 互质,因此 \(r_i \in S_n\) ;这是根据同余命题 c 得出的,因为 \(a\) 和 \(s_i\) 均与 \(n\) 互质
  • 若 \(i \neq j\),则 \(as_i \not\equiv as_j \pmod{n}\),因此 \(r_i \neq r_j\),否则若 \(as_i \equiv as_j \pmod{n}\),又 \(a\) 和 \(n\) 互质, 根据同余命题 b 推出 \(s_i \equiv s_j \pmod{n}\) 矛盾

因此 \(R_n\) 中也有 \(\varphi(n)\) 个各不相同又都是 \(S_n\) 中的元素,于是只能有 \(S_n = R_n\) 了

若把这两个集合中的所有元素相乘就有

\[ (ar_1 \pmod{n})(ar_2 \pmod{n})...(ar_{\varphi(n)} \pmod{n}) = r_1r_2...r_{\varphi(n)} \]

取模且根据同余命题 a

\[ a^{\varphi(n)}r_1r_2...r_{\varphi(n)} \equiv r_1r_2...r_{\varphi(n)} \pmod{n} \]

使用同余命题 b 约掉 \(r_1r_2...r_{\varphi(n)}\),因为这些都与 \(n\) 互质

\[ a^{\varphi(n)} \equiv 1 \pmod{n} \]

RSA 正确性证明

若 \(p,q\) 均为质数,\(n = pq\),又 \(ed \equiv 1 \pmod{\varphi(n)}\),则对于任意 \(0 \le m \lt n\), 有 \(m^{ed} \equiv m \pmod{n}\)

Proof

\(ed \equiv 1 \pmod{\varphi(n)}\) 即存在 \(k \ge 0\) 使得 \(ed = k\varphi(n) + 1 = k(p - 1)(q - 1) + 1\)

  • 若 \(m = 0\),显然

  • 若 \(m > 0\) 且与 \(n\) 互质,则由上面欧拉定理可得 \(m^{\varphi(n)} \equiv 1 \pmod{n}\),于是有

    \[ m^{ed} = m^{k\varphi(n) + 1} = m \cdot (m^{\varphi(n)})^k \equiv m \cdot (1)^k \equiv m \pmod{n} \]

  • 若 \(m > 0\) 且不与 \(n\) 互质,则 \(m\) 要么是 \(p\) 的倍数,要么是 \(q\) 的倍数,但不能同时为 \(p\) 和 \(q\) 的倍数, 因为这两个质数的最小公倍数即 \(n\),而 \(m < n\)

    不妨假设是 \(p\) 的倍数,且不是 \(q\) 的倍数

    1. \(m\) 是 \(p\) 倍数则 \[ m^{ed} \equiv 0 \equiv m \pmod{p} \]

    2. \(m\) 不是 \(q\) 倍数故与之互质,再次由欧拉定理得出 \(m^{\varphi(q)} = m^{q - 1} \equiv 1 \pmod{q}\),于是

      \[ m^{ed} = m \cdot (m^{(q - 1)})^{k(p - 1)} \equiv m \cdot (1)^{k(p - 1)} \equiv m \pmod{q} \]

    由同余命题 d 就能得出 \(m^{ed} \equiv m \pmod{pq}\)

参考