2021SC@SDUSC
本文我会讲解如何使用椭圆曲线secp256k1通过私钥生成公钥、进行数字签名和签名验证,同时说明在签名过程中要注意的随机数k。
secp256k1的参数
前面提到,我们要合理选取椭圆曲线的两个参数a和b的取值,生成元P点的选取,以及选一个足够大的素数p,让椭圆曲线这个群足够大,并让生成元P经过多次几何加法运算生成的循环子群足够大,大到可以遍历椭圆曲线群内全部的点。在secp256k1的标准定义中,生成元是G,所以在下文中我们改用G描述。
在secp256k1中,定义a=0,b=7,也就是说在secp256k1中椭圆曲线方程就是
y
2
=
x
3
+
7
y^2 = x^3 + 7
y2=x3+7 ,当然在有限域中我们应该写作
y
2
=
(
x
3
+
7
)
m
o
d
p
y^2 =( x^3 + 7 )\,mod\,p
y2=(x3+7)modp。
有了这两个参数,我们就可以在实数域中唯一确定椭圆曲线的长相了,大概就是下图这样:
但是想在有限域中看到这个图像的样子却有些困难,因为在有限域中除了需要知道a和b的值外我们还需要一个素数p,为了让有限域足够大,素数又选的很大,在secp256k1中,素数p值为:
这个数大概是10的77次方数量级的,可以想象在一个长宽均为这么大的正方形里面有密密麻麻的满足y^2 =( x^3 + 7 ) mod p 的点,这些点关于 y=p/2 对称。这么说或许脑海中已经有个雏形了,那么我们还比较感兴趣的是这个图像里面到底有多少个点(包含一个无穷远点),也就是这个椭圆曲线的秩是多少。答案是:
又是一个天文数字,但我们的担心又来了:如果我在上面选择一个生成元,经过标量乘法运算,构成的循环子群很小该怎么办,因为上文中我举出过一个例子,群的秩大不代表群上一个点生成的循环子群就大。但我们找到了一个生成元G,它的坐标是:
对这样的一个点,我们不停地用它自己加它自己,也就是G+G+……+G直到最后加完的结果得到G的坐标,也就是又回到了G本身,形成一个循环群,或者我们可以表述为满足nG=G的n是多大,答案是这个n和椭圆曲线的秩一样大。也就是说这一整个椭圆曲线群就是一个循环群,它的生成元就是G,这个循环群里面一共有n个点。可以看出来这个a和b的取值,生成元G点的选取,以及足够大的素数p的选取都是非常巧妙的。
上文中椭圆曲线参数数据来自: https://en.bitcoin.it/wiki/Secp256k1.
ECDSA椭圆曲线数字签名算法
公钥生成
首先在说签名之前先来说如何根据私钥生成公钥。其中私钥就是{1,2,3……,n-1}中的随机的一个值,这个n就是上文椭圆曲线的秩n,可见私钥可以选择的空间还是非常大的。那么公钥就是通过私钥生成的,生成的方法就是标量乘法,用nG得到的椭圆曲线上的一个点就是公钥。由于这个循环群遍布群的所有点,因此私钥和公钥其实就是一一对应的,唯一的私钥确定公钥,公钥又同时对应一个私钥。
同时我们可以看到,根据私钥生成公钥比较简单,只需要计算出标量乘法的结果就行了,换句话说,对G点做“私钥”次几何加法得到的就是公钥了,但我们没必要真一个个加,可以G+G得2G,然后2G+2G得4G……用指数方式滚雪球算,这样的话做的次数最多就是
l
o
g
2
2
256
log_22^{256}
log22256 ,也就是几百次的几何加法,这对计算机来说还是非常容易完成的;但是根据公钥找到私钥确是非常困难的(这个求解困难性问题称之为椭圆曲线上的离散对数问题ECDLP),因为{1,2,3……,n-1}中任何一个数都可能是私钥,我们只有几何加法和标量乘法,也没人告诉我们反过来怎么做,更没法用滚雪球指数的方法算,因为滚一个雪球可能就少试了一大堆私钥的可能取值。唯一可行的办法就是穷举攻击:把n代入一个个试,直到结果恰好等于公钥为止。或许试n个数也没那么难,但这个n这么大:
私钥求公钥,我们进行几百次几何加法运算就可以,但根据公钥求私钥要进行n次几何加法运算,这个次数是
2
256
2^{256}
2256 数量级的,可想而知这其中的难度跨度有多大。
对于这种数或许都不会特别敏感,只有告诉我们计算机破解这个结果到底要花多少年我们才有个更加直观的的感受。下图是《密码编码学与网络安全》中的一组数据:
可以看到128位的密钥都要540000000年,更何况secp256k1中n的二进制表示是256位,破解所花的年数必然是个天文数字。因此这个私钥生成公钥还是挺安全的。
最后我们给私钥和公钥给出符号表示:
H
A
=
d
A
G
H_A=d_AG
HA=dAG
其中
d
A
d_A
dA就是私钥,
H
A
H_A
HA就是公钥,其中:
私钥就是一个整数,用二进制表示的话有256位;
G就是那个有固定xy坐标的椭圆曲线循环群生成元;
H
A
H_A
HA就是有限域椭圆曲线中的某一个点,它具有横纵坐标。
签名
签名就是使用私钥对要签名的文件进行签名。所以这里要签名的一方需要准备两个量:发送方本人的私钥和要签名的文件。签名要经过六步,下面开始:
第一步,在{1,2,3……,n-1}中取一个随机数k。这里的n就是椭圆曲线的秩,也就是椭圆曲线中点的个数,因此随机数k就是一个二进制256位的数字。
第二步,计算 P = k G P=kG P=kG其中G是子群的生成元。这里得到的P点实际上就是椭圆曲线上的某一个点,它具有横纵坐标。
第三步,计算 r = x P m o d n r=x_P\, mod \, n r=xPmodn
其中
x
P
x_P
xP是P点的横坐标,P点的横坐标最大为素数p(当然也可能取不到最大),也就是椭圆曲线横纵坐标能达到的最大值,前面提到p的值为
而n为
因此P点的横坐标xP很有可能会比n大,因此需要进行一个取模运算得到一个不大于n的二进制256位数r。
第四步,计算 z = H a s h ( m e s s a g e ) z=Hash(message) z=Hash(message)
Hash就是一个函数,它可以将任意长度的比特串生成一个定长的比特串,并且将比特串即便只改变一位得到的哈希结果也会天差地别(雪崩效应),并且哈希具有不可逆性,即我可以将一个比特串哈希,但很难给定一个哈希结果说出来那个哈希前的比特串(除非这个对应关系你记录过,用数学的方法难以求解)。区块链比特币中常使用哈希函数有SHA-256,一个任意长度的比特串被SHA-256哈希后的结果就是是256位的定长比特串。
第五步,计算 s = k − 1 ( z + r d A ) m o d n s=k^{-1}(z+rd_A) \,mod \,n s=k−1(z+rdA)modn
其中
k
−
1
k^{-1}
k−1就是模n下k的乘法逆元(满足该乘法逆元与k的乘积对n取模结果为一,用扩展欧几里得算法来求),z和r就是第四、三步分别介绍的,
d
A
d_A
dA就是私钥。经过有限域
G
F
(
n
)
GF(n)
GF(n)加法模运算和乘法模运算运算结果就是s。
第六步,生成数字签名(r,s)。数字签名就是r和s摆在一起组合成的512位(r和s各256位)比特串(这个过程在编译原理中称之为连接)。这样就得到了数字签名。
签名验证
签名验证就是验证签名是否正确,防止有人伪造签名,如果签名验证通过,那么至少可以证明发出这个文件的确实就是正确的发送方而不是什么别的人。
在说验签之前先需要梳理一下目前接收方的已知量,因为签名是发送方本人签名,而验签是所有人都可以验证,所以有些东西是只有发送方知道的,有些东西可以让任何人都知道。目前我们有签名生成过程中的一些中间量如r、s、z,其中r和s构成了数字签名,z是接收方对接收到的数据取哈希结果,在区块链比特币中通常称z为哈希数字摘要,这三个量对接收方都是已知的。此外还要椭圆曲线的两个参数a和b,大素数p,循环群生成元G,椭圆曲线群的秩n,secp256k1是个公开的算法,我都可以在网上找到这些参数,那这些参数也就是公开的,接收方肯定也会知道。此外还有公钥 H A H_A HA也是公开的,在“公钥生成”部分我也说过根据公钥要花几亿年才能求出私钥。此外还有一个随机数k,下一个目录会讲。
验签的过程一共有四步:
第一步,使用如下公式求出
u
1
u_1
u1和
u
2
u_2
u2,其中
s
−
1
s^{-1}
s−1就是s对模n的乘法逆元,z和r都是接收方已知量,因此我们容易得到
u
1
u_1
u1和
u
2
u_2
u2。
u
1
=
s
−
1
z
m
o
d
n
u
2
=
s
−
1
r
m
o
d
n
u_1=s^{-1}z\,mod\,n\\u_2=s^{-1}r\,mod\,n
u1=s−1zmodnu2=s−1rmodn
第二步,计算Q。由于
G
G
G和
H
A
H_A
HA都是椭圆曲线上的点
u
1
u_1
u1和
u
2
u_2
u2都是256位比特串,因此这就相当于对
G
G
G和
H
A
H_A
HA进行了标量乘法运算,在“公钥生成”部分提到最多不超过200多步就可以算出标量乘法的结果。计算出新的两个点
u
1
G
u_1G
u1G和
u
2
H
A
u_2H_A
u2HA进行几何加法运算,在上一篇博客中详细阐述了计算的方法,得到的结果是Q。
Q
=
u
1
G
+
u
2
H
A
Q=u_1G+u_2H_A
Q=u1G+u2HA
第三步,计算
r
Q
r_Q
rQ。由于Q是一个点,它必然有横坐标值
x
Q
x_Q
xQ,对n取模后得到一个256位比特串
r
Q
r_Q
rQ。
r
Q
=
x
Q
m
o
d
n
r_Q=x_Q\,mod\,n
rQ=xQmodn
第四步,验证rQ是否与r相等,如果相等,则签名正确。
r
Q
=
r
是否成立
r_Q=r\,是否成立
rQ=r是否成立
在这整个过程中我们可以看到,我们没有用到任何接收方不该知道的量就验证了签名的正确性,而且计算过程并不复杂,也不需要大的算量,计算机可以很容易地完成。
k
最后你可能会发现一个问题,签名过程中用到了一个随机数k,在验证签名的时候我没有用到它。那么这个k是不是可以随意公开或者消息签名时多次使用呢?答案是k既不能泄露,也不能相同。历史上也有许多使用相同k导致泄密的前车之鉴。
首先先说为什么k不能泄露。
首先我们有这样一个式子:
s
=
k
−
1
(
z
+
r
d
A
)
m
o
d
n
s=k^{-1}(z+rd_A)\,mod\,n
s=k−1(z+rdA)modn
在这个式子里面,r和s在签名中是公开的,z是文件哈希值也是公开的。如果我们移移项,就会有个很大的发现:
d
A
=
r
−
1
(
s
k
−
z
)
m
o
d
n
d_A=r^{-1}(sk-z)\,mod\,n
dA=r−1(sk−z)modn
这个式子就等于告诉别人,如果k泄露,私钥 d A d_A dA就可以很容易地求出来,私钥就泄露了,私钥泄露你发送的信息就都泄露了。
然后再来说说为什么k不能重复使用。
首先一般人肯定有个疑惑,一个人甚至一个公司要发送那么多文件,我怎么知道他是不是用了重复的k,如果他只有几份文件用了相同的k我也很难辨别找出来。这个问题我下面会回答。
首先假设两个签名用了一样的k,但这两个签名签在了不同的文件上,可以令两个签名分别为(r1,s1)和(r2,s2),签名的文件哈希分别为z1和z2。a,b,p,G,n都是公开的,哪个文件签名都用。公钥HA由私钥生成,一个人发的文件私钥一样公钥肯定也一样。
对签名过程生成r的时候是这么生成的:
P
=
k
G
r
=
x
P
m
o
d
n
P=kG\\r=x_P\,mod\,n
P=kGr=xPmodn
在这个过程中很容易可以看出来,G固定如果k一样,那么P一定一样,P一样那么它的横坐标
x
P
x_P
xP肯定定值,n固定那么r肯定也一样,那么前面的r1和r2肯定会一样。这样就可以回答之前的问题了,判断是不是用了重复的k很简单,只要看签名的前256位(也就是r部分)是不是完全一样就行了,如果完全一样那必然就是用了一样的k。
接下来首先对这个数字签名s部分生成的式子:
s
=
k
−
1
(
z
+
r
d
A
)
m
o
d
n
s=k^{-1}(z+rd_A)\,mod\,n
s=k−1(z+rdA)modn
如果是同一个人发送的数据必然使用相同的私钥dA,如果使用相同的k那么r1=r2,那么
r
d
A
r d_A
rdA这一项就相同。如果代入s1 z1 s2 z2不难构造下面的式子:
(
s
1
−
s
2
)
m
o
d
n
=
k
−
1
(
z
1
−
z
2
)
m
o
d
n
k
(
s
1
−
s
2
)
m
o
d
n
=
(
z
1
−
z
2
)
m
o
d
n
k
=
(
z
1
−
z
2
)
(
s
1
−
s
2
)
−
1
m
o
d
n
(s_1-s_2)\,mod\,n=k^{-1}(z_1-z_2)\,mod\,n\\k(s_1-s_2)\,mod\,n=(z_1-z_2)\,mod\,n\\k=(z_1-z_2)(s_1-s_2)^{-1}\,mod\,n
(s1−s2)modn=k−1(z1−z2)modnk(s1−s2)modn=(z1−z2)modnk=(z1−z2)(s1−s2)−1modn
通过这三个式子可以看到,如果两份文件r的一样我们推出来k一样,那么我们完全可以使用两个文件的z1 z2 s1 s2来计算得到k,计算得到k就又回到k泄露问题了,上面也提到泄露了k就是泄露了私钥,因此k也绝对不能重复使用。
不过好在上面我们推导关键步骤还是基于相同私钥,也就说两个人碰巧用了相同的k(不过这概率还是很低,毕竟k取值是{1,2,3……,n-1}中的一个,而n又无比巨大),由于使用不同的私钥因而还是无法攻破他们各自的私钥,因此只要是一个私钥就别用相同的k就行了。
公钥恢复
签名者的公钥可以在数字签名中进行恢复,下面说恢复的原理:
首先在签名过程中有如下三个式子:
u
1
=
s
−
1
z
m
o
d
n
u
2
=
s
−
1
r
m
o
d
n
Q
=
u
1
G
+
u
2
H
A
u_1=s^{-1}z\,mod\,n\\u_2=s^{-1}r\,mod\,n\\Q=u_1G+u_2H_A
u1=s−1zmodnu2=s−1rmodnQ=u1G+u2HA
将这三个式子联立可得:
Q
=
(
z
G
−
r
H
A
)
s
−
1
Q = (zG - rH_A)s^{-1 }
Q=(zG−rHA)s−1
且进一步推得:
Q
=
(
z
G
−
r
H
A
)
s
−
1
⇒
H
A
=
(
s
Q
−
z
G
)
r
−
1
Q = (zG - rH_A)s^{-1 } \Rightarrow H_A = (sQ - zG)r^{-1}
Q=(zG−rHA)s−1⇒HA=(sQ−zG)r−1
在签名验证时有一步要验证
r
r
r与
r
Q
r_Q
rQ是否相等,这里如果数字签名就是私钥和公钥的所属方,那么
r
r
r与
r
Q
r_Q
rQ必然相等,由
r
Q
=
x
Q
m
o
d
n
r_Q=x_Q\,mod\,n
rQ=xQmodn,则
r
r
r实际上就是Q点的横坐标。在数字签名的前半部分得到
r
r
r,将其作为横坐标放入椭圆曲线方程中就可以得到Q点的纵坐标,有了Q点的横纵坐标就可以得到公钥
H
A
H_A
HA,就在数字签名中恢复出了签名者的公钥。
也就是说公钥恢复的实际步骤为:
先将数字签名的 r r r部分代入到椭圆曲线方程中得到一个纵坐标,将 r r r作为横坐标,在方程中得到的纵坐标,组合成一个点 Q Q Q。
计算 H A = ( s Q − z G ) r − 1 H_A = (sQ - zG)r^{-1} HA=(sQ−zG)r−1得到公钥。
ECDH椭圆曲线Diffie–Hellman秘钥交换
ECDH是一种加密方法,用于生成通信双方的共享密钥。
首先通信双方A和B分别在本地生成私钥,然后私钥生成公钥: H A = d A G H B = d B G H_A=d_AG\\H_B=d_BG HA=dAGHB=dBG这时候双方都就有了自己的私钥和公钥,接下来生成共享私钥,A用自己的私钥与B的公钥进行标量乘法运算,同时B用自己的私钥与A的公钥进行标量乘法运算,即 S = d A H B S = d B H A S=d_AH_B\\S=d_BH_A S=dAHBS=dBHA可以发现S其实是相同的: S = d A d B G S=d_Ad_BG S=dAdBG然后A和B就可以用这个共享密钥用对称加密的方式对消息加解密了。