内容来自ANDREA CORBELLINI的椭圆曲线密码学的介绍:Elliptic Curve Cryptography: a gentle introduction
本文是椭圆曲线介绍中的第三篇:ECDH和ECDSA。
在之前的博客中已经说明了椭圆曲线是什么,并证明了椭圆曲线作为群的性质。然后我们将椭圆曲线限定到有限域中。通过这种限制,椭圆曲线中的点可以生成循环子群。后面又介绍了base point,order,cofactor这些术语的概念。
最后提出了椭圆曲线上面的离散对数问题,接下来看一下如何通过离散对数问题来构造密码协议。
全局参数
椭圆曲线算法是在有限域内的椭圆曲线中的循环子群上面运行的。因此,需要定义以下这些参数:
- 素数 p p p:定义了有限域的大小
- 参数 a a a和 b b b:椭圆曲线等式的参数
- 生成元 G G G:子群的生成元
- 阶 n n n:子群的阶
- cofactor h h h:子群的cofactor。 n h = N nh=N nh=N,其中 N N N是整个椭圆曲线的大小(阶)。
总结,全局参数为 ( p , a , b , G , n , h ) (p,a,b,G,n,h) (p,a,b,G,n,h)。
随机椭圆曲线
之前说椭圆曲线上面的离散对数问题是困难的说法其实并不严谨,因为有些特定的曲线的安全性是较弱的,可以用一些特定的算法来解决离散对数问题。比如某些曲线的 p = h n p=hn p=hn(也就是椭圆曲线的阶和有限域的大小相等),这时候就可以用Smart攻击来在多项式时间内解决离散对数问题。
那么问题就出现了,就是如果别人给我了一组椭圆曲线的参数,我要怎么确定这些参数不是精心选取的某个安全性比较弱的曲线呢?
解决方法很简单,让椭圆曲线的参数是随机数生成而非通过选取得到。我们加入一个新的参数,随机种子 S S S,对 S S S进行一个哈希运算,然后根据哈希的结果然得出 a a a和 b b b的值。因为哈希运算是单向的,所以没办法先选定 a a a, b b b,然后根据 a , b a,b a,b的值选出一个种子 S S S。这个过程是不可逆的。
通过随机种子得到a和b是容易的
而通过a和b反向得出S是基本不可能的,因为哈希函数是抗原像攻击的
通过随机种子来生成的曲线被称为可验证随机。这种通过哈希值来生成参数的方法在密码学中很常见,比如Fiat-Shamir转换。这样做可以相对保证曲线不是由某个人故意构造的。
随机椭圆曲线的生成和验证算法在ANSI X9.62标准中有描述,哈希算法是基于SHA-1的。如果对具体的算法比较好奇,可以看SECG的椭圆曲线标准当中的"Verifiably Random Curves and Base Point Generators"这一节。
作者这里给了一个python的脚本来进行教学,可以查看一下。
椭圆曲线密码学
基于椭圆曲线的密码学的公私钥基本是如下形式的:
- 私钥为一个随机数 d d d,是从 1 , . . . , n − 1 {1,...,n-1} 1,...,n−1当中随机选的,这里 n n n是子群的阶。
- 公钥为一个点 H = d G H=dG H=dG,这里 G G G是子群的生成元。
可以看到,如果知道私钥 d d d和 G G G,那么计算公钥 H H H是简单的。但如果公钥 H H H和 G G G,那么计算 d d d是很难的,因为这需要解决一个离散对数问题。
接下来这里会介绍两个基于椭圆曲线的公钥密码算法:ECDH(Elliptic curve Diffie-Hellman),是一个加密算法,ECDSA(Elliptic Curve Digital Signature Algorithm),是用于数字签名的算法。
ECDH加密
ECDH是Diffle-Hellman算法的一个变体。其实是一个密钥协商协议。代表着ECDH定义了密钥是如何在两方之间生成并交换。
ECDH具体解决的问题如下:两方(比如Alice和Bob)想要安全地交换某些信息,使得某个中间人只能窃听但不能解码得知他们的消息。这也是TLS协议的设计宗旨。
具体的步骤如下:
- 首先,Alice和Bob生成他们各自的公私钥。Alice的私钥 d A d_A dA以及公钥为 H A = d A G H_A=d_AG HA=dAG,Bob的密钥为 d B d_B dB和 H B = d B G H_B=d_BG HB=dBG。注意到Alice和Bob公用一套全局参数,比如同一个生成元 G G G和同一个椭圆曲线。
- Alice和Bob通过安全的信道来交换他们的公钥 H A , H B H_A,H_B HA,HB。中间人可能可以窃听到他们传输的 H A , H B H_A,H_B HA,HB的值,但是因为离散对数难题的存在,中间人无法通过 H A , H B H_A,H_B HA,HB计算出 d A d_A dA或 d B d_B dB的值。
- Alice计算
S
=
d
A
H
B
S=d_AH_B
S=dAHB,Bob计算
S
=
d
B
H
A
S=d_BH_A
S=dBHA,注意到两个人算出来的
S
S
S其实是一样的,因为:
S = d A H B = d A ( d B G ) = d B ( d A G ) = d B H A S=d_AH_B=d_A(d_BG)=d_B(d_AG)=d_BH_A S=dAHB=dA(dBG)=dB(dAG)=dBHA
由此,Alice和Bob就都得到了一个协商的值 S S S,而中间人是无法得到的。这里的安全性基于椭圆曲线上的Diffie-Hellman难题,形式化的定义如下:
给定三个点 P , a P , b P P,aP,bP P,aP,bP,如何计算 a b P abP abP?
或者在有限域内的原本的Diffie-Hellman难题:
给定三个数 k , k x , k y k,k^x,k^y k,kx,ky,如何计算 k x y k^{xy} kxy?
Diffie-Hellman的密钥协商思想:Alice和Bob都能轻易计算出秘密值,对于中间人来说计算出秘密值是一个困难问题。
Diffie-Hellman问题的设计宗旨有一个YouTube视频讲的比较清楚,但他是有限域内的Diffie-Hellman。
椭圆曲线内的Diffie-Hellman难题被公认为是一个“困难问题”,被认为是与离散对数难题一样难解决的,虽然学界还没有形式化的证明说这两个问题一样难。我们现在可以保证的事情就是Diffle-Hellman问题不会比离散对数难题更加难,因为如果解决了离散对数难题,那么就可以解决Diffle-Hellman问题。
注意到现在Alice和Bob有了一个共享的秘密值,所以他们就可以使用对称加密来进行数据传输了。
比如,他们可以用点 S S S的 x x x坐标作为AES算法的私钥来加密消息。这就是TLS背后的思想,区别在于在TLS是将 x x x坐标和其他一些信息窜连起来,然后计算了一次哈希,将哈希值作为了密钥。
ECDH写个实际例子
这里有另外一个python脚本模拟了ECDH的密钥交换过程,非常的简单易懂,推荐看一下。
不像是目前为止其他的例子,这里的python脚本用了SECG(“Standard for Efficient Cryptography Group”)提供标准的椭圆曲线:secp256k1
曲线。这个曲线也被用在比特币中。下面是一些全局参数:
- p = p= p= 0xffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffe fffffc2f
- a = a= a= 0
- b = b= b= 7
- x G = x_G= xG= 0x79be667e f9dcbbac 55a06295 ce870b07 029bfcdb 2dce28d9 59f2815b 16f81798
- y G = y_G= yG= 0x483ada77 26a3c465 5da4fbfc 0e1108a8 fd17b448 a6855419 9c47d08f fb10d4b8
- n = n= n= 0xffffffff ffffffff ffffffff fffffffe baaedce6 af48a03b bfd25e8c d0364141
- h = h= h= 1
这里的参数可以被替换,但要注意替换掉的参数要符合Weierstrass normal form。
临时的ECDH
有些人可能听说过ECDHE而不是ECDH。其中的"E"表示"Ephemeral"(短暂的)的意思,代表着密钥交换的结果是临时的,而不是静态的。
ECDHE被用于TLS层中,比如客户端和服务器临时生成一对公私钥,当链接建立之后。双方会对公钥进行TLS认证以及签名,然后使用ECDHE进行密钥交换。
ECDSA签名
场景如下:Alice想要用自己的私钥 d A d_A dA来对一个消息进行签名,而Bob想要用Alice的公钥 H A H_A HA来验证这个签名是否有效。除了Alice之外任何人都没有能力给出有效的签名。任何人都可以验证这个签名是否有效。
同样的,Alice和Bob要使用一样的全局参数。那么ECDSA就是达到上述目标的一种算法。
ECDSA是作用于消息的哈希值,而不是消息本身。如何选择哈希函数是使用者自己决定的,但这个哈希函数应该是一个密码学上安全的哈希函数。哈希得到的哈希值要被截断到与 n n n(子群的阶)的比特大小相同的程度,将截断后的哈希值记为 z z z。
签名算法如下:
- 从 1 , . . . , n − 1 {1,...,n-1} 1,...,n−1中选取一个随机数。
- 计算 P = k G P=kG P=kG。
- 计算一个数 r = x P m o d n r=x_P \bmod n r=xPmodn(其中 x P x_P xP指 P P P的 x x x坐标)。
- 如果 r = 0 r=0 r=0,那么就从第一步重新开始。
- 计算 s = k − 1 ( z + r d A ) m o d n s=k^{-1}(z+rd_A) \bmod n s=k−1(z+rdA)modn。
- 如果 s = 0 s=0 s=0,那么就从第一步重新开始。
签名为 ( r , s ) (r,s) (r,s)。
Alice使用自己的私钥和随机数生成对消息z的签名,Bob收到之后可以用Alice的公钥进行验证
简而言之,算法首先产生秘密 k k k。然后用 r r r将 k k k隐藏起来(基于离散对数问题)。 r r r是和消息 z z z绑定起来的,通过算式 s = k − 1 ( z + r d A ) m o d n s=k^{-1}(z+rd_A) \bmod n s=k−1(z+rdA)modn。
注意到为了计算 s s s,需要计算 k − 1 m o d n k^{-1} \bmod n k−1modn,这就要求 n n n是个素数。所以当子群不是素数阶的时候,ECDSA算法是不能用的。
验证算法
验证需要的输入为:Alice的公钥 H A H_A HA,哈希值 z z z,签名 ( r , s ) (r,s) (r,s)。
- 计算 u 1 = s − 1 z m o d n u_1 = s^{-1}z \bmod n u1=s−1zmodn。
- 计算 u 2 = s − 1 r m o d n u_2 = s^{-1}r \bmod n u2=s−1rmodn。
- 计算 P = u 1 G + u 2 H A P=u_1G+u_2H_A P=u1G+u2HA。
当且仅当 r = x P m o d n r=x_P \bmod n r=xPmodn成立时,验证通过。
算法的正确性
算法背后的逻辑可能并不是一眼能看出来的,但我们可以将目前所有的等式放在一起,那么就会清晰一点。
首先看
P
=
u
1
G
+
u
2
H
A
P=u_1G+u_2H_A
P=u1G+u2HA。因为
H
A
=
d
A
G
H_A=d_AG
HA=dAG(公钥的定义)。那么可以得知:
P
=
u
1
G
+
u
2
H
A
=
u
1
G
+
u
2
d
A
G
=
(
u
1
+
u
2
d
A
)
G
\begin{aligned} P&=u_1G+u_2H_A\\ &=u_1G+u_2d_AG\\ &=(u_1+u_2d_A)G \end{aligned}
P=u1G+u2HA=u1G+u2dAG=(u1+u2dA)G
将
u
1
,
u
2
u_1,u_2
u1,u2的值带入可得:
P
=
(
u
1
+
u
2
d
A
)
G
=
(
s
−
1
z
+
s
−
1
r
d
A
)
G
=
s
−
1
(
z
+
r
d
A
)
G
\begin{aligned} P&=(u_1+u_2d_A)G\\ &=(s^{-1}z+s^{-1}rd_A)G\\ &=s^{-1}(z+rd_A)G \end{aligned}
P=(u1+u2dA)G=(s−1z+s−1rdA)G=s−1(z+rdA)G
由于
s
=
k
−
1
(
z
+
r
d
A
)
s=k^{-1}(z+rd_A)
s=k−1(z+rdA),可以很容易推出
k
=
s
−
1
(
z
+
r
d
A
)
k=s^{-1}(z+rd_A)
k=s−1(z+rdA),将这个带入上式,就得到了:
P
=
s
−
1
(
z
+
r
d
A
)
G
=
k
G
\begin{aligned} P&=s^{-1}(z+rd_A)G\\ &=kG \end{aligned}
P=s−1(z+rdA)G=kG
所以说验证算法中得到的 P P P点是和签名算法中一致的。
ECDSA例子
同样的,作者这里也写了一个python脚本来跑ECDSA算法,总的来说还是比较简单的,因为没有涉及双线性配对,感兴趣的同学可以点进去看看。
k k k的重要性
当生成ECDSA签名的时候,保证 k k k是保密的很关键。如果我们在很多签名中使用同一个 k k k,或者说我们的随机数生成器被攻击者预测到了,那么攻击者就可以得到我们的私钥了。
索尼的PS3在之前就因为随机值 k k k翻车了。PS3游戏控制台运行游戏的话需要验证某个游戏是否由索尼给出了ECDSA签名。因此,如果我没有索尼给出的签名的话,是没办法在PS3上面发布游戏的。但问题在于,索尼的所有签名都是用同一个静态的 k k k来进行签名的。
(索尼用的随机数生成器可能是下面两个之一)
在这种情况下,我们只需要买两个索尼签名的游戏,就可以恢复出索尼的密钥了。下面是步骤,首先我们要知道两个游戏的哈希值
z
1
,
z
2
z_1,z_2
z1,z2,以及他们的签名
(
r
1
,
s
1
)
,
(
r
2
,
s
2
)
(r_1,s_1),(r_2,s_2)
(r1,s1),(r2,s2)。
- 首先,注意到 r 1 = r 2 r_1=r_2 r1=r2,因为 P = k G P=kG P=kG,而 r 1 r_1 r1和 r 2 r_2 r2都是 P P P的 x x x坐标。
- 计算 ( s 1 − s 2 ) = k − 1 ( z 1 − z 2 ) (s_1-s_2)=k^{-1}(z_1-z_2) (s1−s2)=k−1(z1−z2)。
- 两边乘 k k k: k ( s 1 − s 2 ) = ( z 1 − z 2 ) k(s_1-s_2)=(z_1-z_2) k(s1−s2)=(z1−z2)。
- 除以 ( s 1 − s 2 ) (s_1-s_2) (s1−s2): k = ( z 1 − z 2 ) ( s 1 − s 2 ) − 1 m o d n k=(z_1-z_2)(s_1-s_2)^{-1}\bmod n k=(z1−z2)(s1−s2)−1modn。
那么我们就知道了
k
k
k,通过
k
k
k很容易就能计算出私钥:
s
=
k
−
1
(
z
+
r
d
S
)
m
o
d
n
⇒
d
S
=
r
−
1
(
s
k
−
z
)
m
o
d
n
s=k^{-1}(z+rd_S) \bmod n \quad \Rightarrow \quad d_S=r^{-1}(sk-z) \bmod n
s=k−1(z+rdS)modn⇒dS=r−1(sk−z)modn
如果 k k k不是静态的,而是一个可预测的随机数的话,那也可以实现类似的攻击。