EXP6 RSA攻击
💡 参考实验:
实验四 RSA中公开的模数N_rsa 模数_chuxuezheerer的博客-CSDN博客
Stanford_Cryptography-I/factoring_challenge.py at main · galadd/Stanford_Cryptography-I
一、RSA陷门置换函数
- 有两个任意的奇素数p和q,,其欧拉函数值 φ ( n ) = ( p − 1 ) ( q − 1 ) \varphi \left( n \right) =\left( p-1 \right) \left( q-1 \right) φ(n)=(p−1)(q−1)
- 随机选一整数e(公钥), 1 ≤ e ≤ φ ( n ) , ( φ ( n ) , e ) = 1 (互素) 1\le e\le \varphi \left( n \right) ,\left( \varphi \left( n \right) ,e \right) =1\text{(互素)} 1≤e≤φ(n),(φ(n),e)=1(互素),在模 φ ( n ) \varphi \left(n \right) φ(n)下,e存在逆元: d e m o d φ ( n ) = 1 ⇒ d = e − 1 m o d φ ( n ) de\ mod\ \varphi \left( n \right) =1 \Rightarrow d=e^{-1}\ mod\ \varphi \left( n \right) de mod φ(n)=1⇒d=e−1 mod φ(n) ,即可计算出私钥d。
- n,e都可公开,私钥为d,销毁p,q不可泄露。
- 加密变换: m → c = m e m o d n m\rightarrow c=m^e\ mod\ n m→c=me mod n
- 解密变换: m = c d m o d n m=c^d\ mod\ n m=cd mod n
- 公钥加密,私钥解密
二、实验原理及运行结果
在 RSA 的密钥生成阶段,不同用户应该独立的生成模 N,即选取大素数 p 和 q相乘得到 N。但是某个项目中 RSA 具体实现过程如下:(1)首先随机选取一个随机数 R 然后选取距离 R 最近的素数 p,以及另外一个相近的素数 q。(2)p和 q 相乘得到 N。由于 p 和 q 为相近的两个素数,在此情况下存在对 N 的高效分解方法,导致方案不安全。
假设 N 的因子 p 和 q 满足:⭐ ∣ p − q ∣ < 2 N 1 4 |p-q|<2N^{\frac{1}{4}} ∣p−q∣<2N41,那么 N 的分解可由如下分析得出:
令
A
=
p
+
q
2
A=\frac{p+q}{2}
A=2p+q,那么根据 p 和 q 之间的关系,能够得到
A
−
N
<
1
A - \sqrt{N}<1
A−N<1。因此可以根据
A
=
c
e
i
l
(
s
q
r
t
(
N
)
)
A=ceil(sqrt(N))
A=ceil(sqrt(N))来计算得到A,其中ceil是取整函数。如下所示是
p
,
q
,
N
,
p
+
q
2
p,q,\sqrt{N},\frac{p+q}{2}
p,q,N,2p+q之间的关系:
由于
N
=
p
q
=
(
A
−
x
)
(
A
+
x
)
=
A
2
−
x
2
N=pq=(A-x)(A+x)=A^2-x^2
N=pq=(A−x)(A+x)=A2−x2,因此有
x
=
A
2
−
N
x=\sqrt{A^2-N}
x=A2−N。当求得 x 以后,容易根据
p
=
A
+
x
,
q
=
A
−
x
p=A+x,q=A-x
p=A+x,q=A−x得到 N 的分解。
分解挑战#1
如下的整数 N 是两个素数 p,q 的乘积,且满足 ∣ p − q ∣ < 2 N 1 4 |p-q|<2N^{\frac{1}{4}} ∣p−q∣<2N41,请分解整数N,并给出其十进制结果。
N = 17976931348623159077293051907890247336179769789423065727343008115 7732675805505620686985379449212982959585501387537164015710139858647833778606925583497541085196591615128057575940752635007475935288710823649949940771895617054361149474865046711015101563940680527540071584560878577663743040086340742855278549092581
由上方的数学关系即可得到x,p,q。
🥺 使用math.sqrt()开平方的结果:
math.sqrt()函数只能处理float类型的值,因此当N超过其可处理的最大值时,会引发该错误。使用print(sys.float_info.max)查看Python中float类型可以表示的最大值为:1.7976931348623157e+308,N远大于该值,所以不能用math.sqrt()函数开根号。
😜 解决方法:
使用gmpy2中的函数解决。gmpy2是Python的一个第三方库,提供了高精度计算功能,包括大整数运算、大浮点数运算、素数测试和素数生成等。gmpy2底层使用GNU Multiple Precision Arithmetic Library (GMP)实现高精度计算,因此具有很高的效率和精度。
使用gmpy2库可以方便地处理超出Python内置类型范围的数值,例如非常大的整数、超出标准浮点数表示范围的浮点数等。此外,gmpy2还提供了一些高级的数学函数,例如椭圆曲线加密、计算Jacobi符号、计算模反演等。
在使用gmpy2库进行高精度计算时,常常需要将Python内置类型的整数转换为gmpy2库中的整数类型,以便利用gmpy2库提供的高精度运算功能。gmpy2.mpz()
函数用于将Python内置类型的整数转换为gmpy2库中的整数类型mpz(多精度整数)。
但gmpy2中没有提供向上取整函数。
常用的开根号函数:
- gmpy2.isqrt(x):返回一个整数y,满足y^2 <= x < (y+1)^2,即x的平方根的最大整数。
- gmpy2.sqrt(x):返回一个浮点数y,满足y^2 ≈ x。
- gmpy2.c_sqrt(x):返回一个复数z,满足z^2 ≈ x。
- gmpy2.sqrtrem(x):返回一个元组(y, r),其中y是整数类型的平方根,r是余数,满足x = y^2 + r。
- gmpy2.isqrt_rem(x)函数返回一个元组(q, r),其中q是整数类型的平方根,r是余数,满足x = q^2 + r,且r < 2q + 1。
gmpy2.sqrtrem(x)函数使用了高精度浮点数的计算方法,因此在计算小数部分时会有一定的误差。由于gmpy2库支持高精度计算,因此这个误差相对较小,通常可以满足实际需求。
gmpy2.isqrt_rem(x)函数使用了二分查找的方法来计算平方根和余数,因此计算结果准确无误。由于该函数不需要进行高精度浮点数的计算,因此在计算速度上相对较快,特别适合处理大整数的开方运算。
因此,当需要精确计算一个数的平方根和余数时,可以选择gmpy2.isqrt_rem()函数;当对精度要求不高,但需要高效计算一个数的平方根和余数时,可以选择gmpy2.sqrtrem()函数。
def task1(N): # |p-q|<2N^{1/4}
N = gmpy2.mpz(N) # 转换为gmpy2中的整数类型
A, rem = gmpy2.isqrt_rem(N) # 结果和余数
if rem > 0:
A += 1
x = gmpy2.isqrt(A**2 - N)
p = A - x
q = A + x
if gmpy2.mul(p, q) == N:
print("分解正确!")
else:
print("分解有误。")
return p, q
print("分解挑战1的结果如下:")
task1_p, task1_q = task1(179769313486231590772930519078902473361797697894230657273430081157732675805505620686985379449212982959585501387537164015710139858647833778606925583497541085196591615128057575940752635007475935288710823649949940771895617054361149474865046711015101563940680527540071584560878577663743040086340742855278549092581)
print("p:", task1_p)
print("q:", task1_q)
运行结果:
分解正确!
分解挑战1的结果如下:
p:13407807929942597099574024998205846127479365820592393377723561443721764030073778560980348930557750569660049234002192590823085163940025485114449475265364281
q: 13407807929942597099574024998205846127479365820592393377723561443721764030073662768891111614362326998675040546094339320838419523375986027530441562135724301
分解挑战#2
如下的整数 N 是两个素数 p,q 的乘积,且满足 ∣ p − q ∣ < 2 11 N 1 4 |p-q|<2^{11}N^{\frac{1}{4}} ∣p−q∣<211N41,请分解整数N,并给出其十进制结果。
N=648455842808071669662824265346772278726343720706976263060439070378797308618081116462714015276061417569195587321840254520655424906719892428844841839353281972988531310511738648965962582821502504990264452100885281673303711142296421027840289307657458645233683357077834689715838646088239640236866252211790085787877
def task2(N): # |p-q|<2^{11}N^{1/4}
N = gmpy2.mpz(N)
A, rem = gmpy2.isqrt_rem(N) # 结果和余数。A的最小值为ceil(sqrt(N))
floor = A # floor(√N)
if rem > 0:
A += 1
p = q = 0
while A < 2**19 + floor:
x, rem = gmpy2.isqrt_rem(A**2-N)
if rem == 0: # 能够开方没有余数
p, q = A-x, A+x
if gmpy2.mul(p, q) == N:
print("分解正确!")
break
A += 1
if p == 0 and q == 0:
print("分解错误!")
return p, q
print("分解挑战2的结果如下:")
task2_p, task2_q = task2(648455842808071669662824265346772278726343720706976263060439070378797308618081116462714015276061417569195587321840254520655424906719892428844841839353281972988531310511738648965962582821502504990264452100885281673303711142296421027840289307657458645233683357077834689715838646088239640236866252211790085787877)
print("p:", task2_p)
print("q:", task2_q)
运行结果:
分解挑战2的结果如下:
分解正确!
p: 25464796146996183438008816563973942229341454268524157846328581927885777969985222835143851073249573454107384461557193173304497244814071505790566593206419759
q: 25464796146996183438008816563973942229341454268524157846328581927885777970106398054491246526970814167632563509541784734741871379856682354747718346471375403
解密挑战#3
如下所示是使用分解挑战 1 中的模数 N 对一段消息进行加密得到的密文。其中加密指数 e=65537。明文消息使用 ASCII 进行编码后使用 PKCS v1.5 进行消息填充,需要指出的是,消息中使用’0x00’对消息和随机填充进行分割,而不是’0xFF’。请在实验报告中给出下列密文的解密结果。
22096451867410381776306561134883418017410069787892831071731839143676135600120538004282329650473509424343946219751512256465839967942889460764542040581564748988013734864120452325229320176487916666402997509188729971690526083222067771600019329260870009579993724077458967773697817571267229951148662959627934791540
❓ RSA分块加密和填充
RSA加密长度限制问题:
明文长度需要小于密钥长度,而密文长度则等于密钥长度。因此当加密内容长度大于密钥长度时,有效的RSA加解密就需要对内容进行分段并且填充。填充后每组数据的长度与RSA公钥等长。同时可以避免Textbook RSA攻击,即若m较小时直接求e次方根得到m。
实际应用中的RSA公钥加密方法:
常见的填充方式:
假设现有RSA算法密钥长度为1024bit,则三种填充模式的填充效果如下所述。
- RSA_NO_PADDING填充模式
当用户选择RSA_NO_PADDING填充模式时,如果明文不够128字节,加密的时候会在明文前面填充若干数据0,直至达到128字节。
解密后的明文也会包括前面填充的零,用户需要注意把解密后的字段前向填充的零去掉,才是真正的明文。凌科芯安系列芯片支持RSA算法的芯片目前就是时钟这种填充方式。如果填充规则不符合要求,建议用户使用芯片加密前,先自行填充。- RSA_PKCS1_PADDING填充模式
如果明文不够128字节,加密的时候会在明文中随机填充一些数据,所以会导致对同样的明文每次加密后的结果都不一样。
对加密后的密文,用户使用相同的填充方式都能解密。解密后的明文也就是之前加密的明文。
例如:EB = 00 || BT || PS || 00 || D ,其中D为消息,BT为The block type块类型,PS为The padding string填充字符串。
BT(The block type块类型):
BT=00 or 01 (私钥运算时)
BT=02 (公钥运算时)
PS(The padding string填充字符串):
BT=00时,PS由00组成;
BT=01时,PS由FF组成;
BT=02时,PS由伪随机生成,且非零,以增加填充的随机性和安全性;
则PS长度为Len(EB) - 3 - Len(D),最少是8字节。
由填充形式可得明文数据D即为第二个”0x00”之后的数据。- RSA_PKCS1_OAEP_PADDING填充模式
RSA_PKCS1_OAEP_PADDING填充模式是PKCS#1推出的新填充方式,安全性最高,和前面RSA_PKCS1_PADDING的区别就是加密前的编码方式不一样。
最后将加密后的结果拼接在一起,形成最终的密文。- PKCS v1.5和RSA_PKCS1_PADDING的关系
PKCS#1 v1.5是RSA加密填充方案的一种,RSA_PKCS1_PADDING是OpenSSL库中对PKCS#1 v1.5填充方案的实现方式之一。在OpenSSL中,使用RSA_PKCS1_PADDING填充模式时,会对原始明文数据进行PKCS#1 v1.5填充,然后再进行RSA加密,生成最终的密文。
🙈 RSA加密字符串:
可以将字符串视为一个字节数组,并将其视为一个数据块来进行加密。通常需要将字符串转换为字节数组,并将字节数组填充到指定的长度,然后对填充后的数据块进行RSA加密。
def task3(p, q, c, e): # RSA解密
N = gmpy2.mul(p, q)
fi = (p - 1) * (q - 1)
c = gmpy2.mpz(c)
e = gmpy2.mpz(e)
d = gmpy2.invert(e, fi) # gmpy2库中的一个数论函数,用于计算在模b意义下a的逆元。也就是求一个整数x,使得 (a * x) % b = 1。
# m = c ^ d mod n
m = gmpy2.powmod(c, d, N) # gmpy2库中的一个数论函数,用于计算在模modulus意义下的幂运算。也就是计算 (base**exponent) % modulus 的值。
# 使用快速幂算法来计算幂运算,因此对于大数幂运算,它的效率比简单的幂运算更高。
# 去除填充→EB = 00 || BT || PS || 00 || D
m = gmpy2.digits(m, 16).split("00")[1] # digits(x, base)用于将整数n表示为指定进制的数字字符串
# .split()以字符串'00'为分隔符将该字符串分割成若干段,并选择其中第一个分段即原始数据D
return bytes.fromhex(m) # 以字节形式返回解密得到的明文
运行结果: