第一集 哈希签名始祖 Lamport 方案及其改进版本(Merkle,Winternitz)
第二集 哈希签名树 Merkle Tree Authentication
第三集 HORS Hash to Obtain Random Subset 2002、DBS算法 2008
第四集 GMSS、XMSS、Multi Tree XMSS
第五集 SPHINCS 2015 SPHINCS+ 2019(完结撒花)
一、什么是数字签名
在以前,我们写信的时候,都会通过在信封上盖上自己的印章或者是签上自己的名字,来证明这封信是出自自己之手。
在计算机时代,我们在传输信息的时候,怎么证明一条信息(massage)是自己写的呢?
这时候就要用到“数字签名”了。
这里有一篇关于加密和签名的文章,我觉得讲的挺不错的,有图有具体例子,通俗易懂。一文彻底搞懂加密、数字签名和数字证书,看不懂你打我!
1.1 加密和签名
加密和签名是两个不同的概念。
1.1.1 加密
加密的过程是:
- 发送信息的人(sender)对明文(massage)用某种方法进行加密(encryption)形成密文(cipher text)
- 发送者把密文(cipher text)发送出去
- 接收信息的人(receiver)也用某种方法对密文进行解密(decrypt)得到明文。
1.1.2 签名
签名的过程是:
- 发送信息的人(sender)对明文(massage)用某种方法进行签名(sign)形成签名(signature)
- 发送者把明文(massage)和签名(signature)一同发送出去
- 接受信息的人,用某种方式对明文进行验证(authentication),来判断这个明文是否为发送者本人发出
1.1.3 对比
加密和签名的不同点:
- 加密中,除了发送者以外的所有人都只能看到密文;而在签名中,所有人都可以看到明文
- 加密算法中,不能验证信息是由谁发出来的,谁都可以冒充发送者发送信息;签名算法中,可以验证信息是谁发出来的,任何人都无法冒充发送者来发送信息
1.2 对称和非对称加密
对称加密:在上面的加密算法中,加密和解密(encryption and decrypt)中提到的“某种方法”是同一个方法。比如说,加密是将26个字母整体右移一位,解密就是将26个字母整体左移一位。明文:hello word,密文:ifmm hpse(h的下一个字母是i),解密就是每个字母再左移回去,就会得到hello word
非对称加密:就是加密和解密(encryption and decrypt)中提到的“某种方法”是不是同一个方法。具体怎么操作,在 1.2.1 非对称加密流程 会提到。
1. 2 公钥和密钥
在常见的非对称加密和签名算法中,加密和解密(encryption and decrypt)签名和验证(sign and authentication)这四个步骤中提到的“某种方法”就用到了公钥和私钥。
1.2.1 非对称加密流程
在receiver接收信息之前,会先生成一对钥匙,包含了公钥和私钥。receiver会把公钥公布于总,私钥自己留着。
加密(encryption)是sender用公钥进行加密
解密(decrypt)是receiver用私钥进行解密
由于只有receiver自己有私钥,因此,除了sender以外,只有receiver能知道明文。
这里最经典的例子就是RSA算法。至于为什么RSA用不同的方法(公钥和私钥)可以还原得到相同的结果,这就涉及到数学上的“同余”这个概念了。
我自己对RSA简单的理解就是“不同数对同一个数求余数,余数是可以相同的”,30除以7余2,51除以7也是余2,这里的30和51可以看作是公钥和私钥,而这里的余数2就是明文。实际的计算会比这个复杂,具体怎么操作和数学的证明感兴趣可以自己去搜索,这里给出一个文章的链接:RSA算法
1.2.2 签名流程
在sender发送信息之前,会先生成一对钥匙,包含了公钥和私钥。sender会把公钥公布于总,私钥自己留着。
- 签名(sign)是sender用私钥对明文(massage)进行“加密”(签名),得到签名(signature)
- 验证(authentication)是receiver用公钥对签名(signature)进行“解密”,得到明文,然后对比这个解密出来的明文和sender发送过来的明文是否一致。如果一致,说明明文就是sender发送过来的。
由于只有sender自己有私钥,因此,只有sender能知道对明文进行签名。
1.3 OTS - One Time Signature
在我们今天要讲的Lamport提出的Hash签名方案中,整个流程和上面 1.2.2 签名流程 中介绍的有所不同,Lamport是直接把私钥当作Signature连同Massage一起发送出去的。这就导致你的一个密钥对只能签名一次,因此这种方案叫做“One Time Scheme”简称OTS
具体是怎么回事,后面会详细介绍。
二、哈希函数的性质
这里可以先跳过,直接看 三、Lamport的方案,等后面提到再倒回来看。
一般情况下我们希望哈希函数满足以下三条性质:
- 抗原像性 preimage resistance(有些时候称为“单向性”):给出一些输出Y=H(X),找到一个令H(X)=Y的输入X是一件很耗时间的事。(要使用暴力穷举才能找到输入X,比如说X是一个1024bit的数据,你需要一个一个的去尝试,看看在2^1024这个空间内,哪一个X才能输出这个对应的Y)
- 抗第二原像性 Second preimage resistant :它和抗原像性有一点不同,给出一个输入X,攻击者很难找到另外一个输入X’,使得H(X)=H(X’)。
- 抗碰撞性 Collision resistant :很难找到两个值X1,X2,使得H(X1)=H(X2)。另外需要注意的是,因为攻击者可以自由选择任意两条消息,所以这一点其实是比抗第二原像性更严格的要求。
因为哈希函数是单向的,因此也被称为One Way Function
三、Lamport的方案
Lamport在1979年的一篇论文《Constructing Digital Signatures from One Way Function》提出了Hash签名方案。
我们直接举例讲解Lamport方案。
假设现在我们有一个3bit
的massage需要签名,massage内容为: 010
2.1 原始版本 Lamport 1979
2.1.1 具体流程
- 生成私钥:首先随机生成
3
个不同的数,比如以十六进制为单位:9 A E
换算成十进制就是9 10 14
这里用什么进制表示无所谓,只要是3个不同的数就可以。这3个数分别对应3个不同的bit - 生成公钥:使用哈希函数H(x),对私钥进行运算,得到公钥。y=H(x),y就是公钥,F是哈希函数,x是私钥。比如说,对私钥的第一位(bit)进行哈希运算,5 = H(9),5就是第一位的公钥。我们暂时不需要知道这个哈希函数是什么,我们只需要确定,它有上面提到的三条性质。然后签名者把公钥公布于众,比如说最后得到的公钥是
5D2
- 签名者发送明文和签名,明文就是
010
。签名是这样的,当明文为0
时,对应位置上的签名为空,当明文为1
时,对应位置上的签名就是相应的私钥。因此这里的签名Signature是空A空
- 验签者接收到签名。开始验证,signature上
空
的位置和massage中0
的位置是对应的,这没有问题。然后再对signature中的A
进行哈希运算,H(A) = D,得到的结果是D
,这和公钥中对应位置的数是一样的,因此也没有问题。好了,验证完毕,验签者可以确定这个010
是签名者发出来的了。
2.1.1 方案的漏洞
聪明的你肯定发现了,这个方案有一个漏洞,就是攻击者可以把原本为1
的bit篡改成0
,因为这样只需要把原来的私钥改成空
即可。
然后攻击者给验签者发送000
和空空空
来冒充成签名者。
决定这个问题也很简单粗暴,就是直接把私钥的长度加一倍。这样0
也有自己对应的私钥了,不再是空
了
2.2 第一代改进
2.2.1 具体流程
-
生成私钥:首先随机生成
3*2
个不同的数,我们现在称私钥(Secret Key)为 S K SK SK 。这里有6位的私钥,为了方便我们定义:
S K = ( s k 0 , s k 1 ) SK=(sk_0, sk_1) SK=(sk0,sk1)
s k 0 = s k 0 1 , s k 0 2 , s k 0 3 sk_0=sk_0^1, sk_0^2, sk_0^3 sk0=sk01,sk02,sk03
s k 1 = s k 1 1 , s k 1 2 , s k 1 3 sk_1=sk_1^1, sk_1^2, sk_1^3 sk1=sk11,sk12,sk13
为了与上面的原始版本对比, s k 1 sk_1 sk1我们还是使用上面的3个密钥,有:
s k 1 = 9 , A , E sk_1=9, A, E sk1=9,A,E
即 s k 1 1 = 9 , s k 1 2 = A , s k 1 3 = E sk_1^1=9, sk_1^2=A, sk_1^3=E sk11=9,sk12=A,sk13=E
我们随机生成sk0
s k 0 = B , C , 4 sk_0 = B, C, 4 sk0=B,C,4
因此密钥为
S K = [ B , C , 4 9 , A , E ] SK=\begin{bmatrix} B, C, 4\\ 9, A, E\end{bmatrix} SK=[B,C,49,A,E] -
生成公钥:还是使用哈希函数H(x),对私钥进行运算,得到公钥。我们现在称公钥(Public Ksy)为 P K PK PK
有: P K = H ( S K ) , P K = ( p k 0 , p k 1 ) PK = H(SK), PK=(pk_0, pk_1) PK=H(SK),PK=(pk0,pk1)
具体来讲:
p k 0 i = H ( s k 0 i ) , p k 1 i = H ( s k 1 i ) , i ∈ { 1 , 2 , 3 } pk_0^i=H(sk_0^i), pk_1^i=H(sk_1^i), \ i \in\{1,2,3\} pk0i=H(sk0i),pk1i=H(sk1i), i∈{1,2,3}
为了与上面的原始版本对比, p k 1 pk_1 pk1我们还是使用上面的3个公钥,有:
s k 1 = 5 , D , 2 sk_1=5,D,2 sk1=5,D,2
我们随机生成pk0
p k 0 = F , 3 , 6 pk_0 = F, 3, 6 pk0=F,3,6
因此公钥为
P K = [ F , 3 , 6 5 , D , 2 ] PK=\begin{bmatrix} F, 3, 6 \\ 5,D,2 \end{bmatrix} PK=[F,3,65,D,2]
然后签名者把公钥公布于众 -
签名者发送明文和签名,明文就是
010
。签名是这样的,当明文为0
时,对应位置上的签名为 s k 0 i sk_0^i sk0i,当明文为1
时,对应位置上的签名就是相应的 s k 1 i sk_1^i sk1i。因此这里的签名Signature是BA4
-
验签者接收到签名。开始验证,第一个bit是0,因此将Signature的第一个位
B
做哈希运算H(B)=F,得到的结果是F
,这和 p k 0 1 pk_0^1 pk01对应,这没有问题。然后第二bit是1
,对应的signature是A
,对其进行哈希运算,H(A) = D,得到的结果是D
,这和公钥中对应位置的数 p k 1 2 pk_1^2 pk12是一样的,因此也没有问题。第三个bit以此类推。验证完毕后,验签者可以确定这个010
是签名者发出来的了。
签名:
验证:
安全性:
2.2.2 该方案的缺点
最明显的缺点就是,私钥和公钥都很大,是Massage的bit数的两倍。当Massage很大的时候,签名和公钥就变得很大。
2.3 第二代改进 Merkle 1990
该方案是1990年Merkle在《A CERTIFIED DIGITAL SIGNATURE》中的第四节“4. An Improved One Time Signature”中提出的方案。
2.3.1 具体流程
这一代的具体流程和原始版本是一样的,不同的地方是,在整个Massage后面,加上了一个数,用于校验Massage中0
的个数,这个数被称为“校验和”(Checksum)
- 计算Massage中
0
的个数,这里Massage时010
,因此0
的个数为2,在3bit的Massage中0
最多有3个,因此要额外用2bit的位置表示校验和,因此加上校验和(Checksum)后的Massage为010 10
。同时我们可以和容易的算出,n
bit的Massage只需要在后面加上 l o g 2 n log_2n log2n bit的校验和即可。我们都知道 l o g 2 n log_2n log2n是远小于 n n n的,因此可以节省很多空间,这样密钥对的大小就可以被大大减小了。 - 生成私钥:因为多了2bit的校验和,因此要随机生成
3+2
个不同的数,密钥:9 A E 5 0
- 生成公钥:使用哈希函数H(x),对私钥进行运算,得到公钥。最后得到的公钥是
5D267
- 签名者发送明文和签名,明文就是
010 10
。签名是这样的,当明文为0
时,对应位置上的签名为空,当明文为1
时,对应位置上的签名就是相应的私钥。因此这里的签名Signature是空A空 5空
- 验签者接收到签名。开始验证,signature上
空
的位置和massage中0
的位置是对应的,这没有问题。然后再对signature中的A
进行哈希运算,H(A) = D,得到的结果是D
,这和公钥中对应位置的数是一样的,因此也没有问题。校验和的两位也是同理。好了,验证完毕,验签者可以确定这个010
是签名者发出来的了。
为什么这样做就是安全的呢?如果攻击者,把原Massage中的1
修改成0
,校验和也一起改,这样不行吗?
答案是不行的。首先我们先想想,校验和我们可以改成什么?我们只能把原本为1
的改成0
,通过这个操作,你只能把校验和减小,而不能变大。由于校验和代表的是原Massage中0的个数,也就是说通过改变校验和,我们只能把原Massage中0的个数减小,但是我们只能把1
的改成0
,也是增加0的个数。
是不是很巧妙?所以通过这样死锁,保证了这种方案安全性。
2.3.2 该方案的总结
该方案是1990年Merkle在《A CERTIFIED DIGITAL SIGNATURE》中的第四节“4. An Improved One Time Signature”中提到对OTS的改进。对于一个长度为n的massage,之前需要使用 2 ∗ n 2*n 2∗n长度的key,而现在只需要 n + l o g 2 n n+log_2 n n+log2n,大大减小了key的长度。
2.4 第三代改进 Winternitz 1979 WOTS
1979年,斯坦福大学数学系的罗伯特·温特尼茨(Robert Winternitz)提出了一个进一步的实质性改进,将签名消息的大小额外减少了大约4到8倍。Winternitz的方法用时间换取了空间:减少的尺寸是以增加的计算工作量来换取的。
这种方法被称为WOTS(Winternitz One Time Signature)。
2.4.1 具体流程
这一代的具体流程和前面的3个版本都有挺大差别,他从另一个角度来减小签名的大小。
WOTS只有两个初始参数:
w
,
n
w,n
w,n,其中
n
n
n 就是message的长度(bit),而
w
w
w是WOTS 链条的长度,也就是每一块长度是
log
2
(
w
)
b
i
t
s
\log_2(w) bits
log2(w)bits
由上面这两个参数可以推导出以下参数:
l
1
l_1
l1 message分块之后的块数(链条数),
l
2
l_2
l2 校验和分块之后的块数(链条数),
l
e
n
=
l
1
+
l
2
len=l_1+l_2
len=l1+l2整个WOTS的分块的块数(链条数)
上面陌生的术语在下面都会有解释,这里先给出这些参数的定义。
假设我们现在有一个32bit
的massage,即
n
=
32
n=32
n=32,假设massage写成二进制是 0000 0000 - 0000 0001 - 0000 0010 - 0000 0011
-
消息分块:首先将massage分块,每一块的大小一般为
4bit
或者8bit
,这里我们使用8bit
,因此massage被分成 l 1 = 4 l_1=4 l1=4块。每一块就是1byte=8bits
。因为8bit可以表示0-255,所以WOTS链条的长度就是 w = 256 w=256 w=256。用十六进制来表示massage,我们的32bit的massage被分块后是0x00 0x01 0x02 0x03
-
计算校验和:这里的校验和的定义是 l 1 ∗ ( w − 1 ) l_1*(w-1) l1∗(w−1) 减去每一块数据的和,即
4*255 - 0+1+2+3=1014
,因此这里校验和是1014
。在这个例子中,校验和最大为4*255=1020
因此需要额外的10bit
来表示校验和。加上校验和后的massage为0000 0000 - 0000 0001 - 0000 0010 - 0000 0011 -- 11 1111 0110
。这里为了方便(计算机中的最小单位一般是byte),把校验和中的10bit变成2byte(有6个bits空出来没用),所以现在一共是4+2=6byte。最后的签名为0000 0000 - 0000 0001 - 0000 0010 - 0000 0011 -- 0000 0011 - 1111 0110
, -
校验和分块:我们的校验和是
0000 0011 - 1111 0110
变成了2byte之后,直接就分成了 l 2 = 2 l_2=2 l2=2块 。现在整个WOTS一共有 l e n = 4 + 2 len=4+2 len=4+2 块。 -
生成私钥:生成
len=6
个私钥,每一个私钥对于massage和校验和中的一个块(1byte)。假设私钥为A B C D E F
。但是一个byte有256种可能,而私钥只有一个,怎么一一对应起来呢?这就涉及到后面公钥是怎么生产的了。 -
生产签名:最神奇的地方来了。对于massage的第一个byte,是
0
,因此第一个signature就是对应位置上的A
。如果第一个byte是1
,则signature是对应位置上的A
做一次哈希运算 H ( A ) H(A) H(A);如果第一个byte是2
,则signature是对应位置上的A
做两次哈希运算 H ( H ( A ) ) = H 2 ( A ) H(H(A))=H^2(A) H(H(A))=H2(A);以此类推;如果如果第一个byte是255
,则signature是对应位置上的A
做255次哈希运算 H 255 ( A ) H^{255}(A) H255(A)。根据这个规则,最后得到的4个byte的signature是 A , H ( B ) , H 2 ( C ) , H 3 ( D ) A,\ H(B),\ H^2(C),\ H^3(D) A, H(B), H2(C), H3(D) 。后面校验和同理 H 3 ( E ) , H 246 ( F ) H^3(E),\ H^{246}(F) H3(E), H246(F) 。 -
产生公钥:公钥就是将密钥做 w − 1 = 255 w-1=255 w−1=255次哈希运算后得到的值: H 255 ( A ) , H 255 ( B ) , H 255 ( C ) , H 255 ( D ) , H 255 ( E ) H 255 ( F ) H^{255}(A),\ H^{255}(B),\ H^{255}(C),\ H^{255}(D),\ H^{255}(E)\ H^{255}(F) H255(A), H255(B), H255(C), H255(D), H255(E) H255(F) 。签名者将公钥公布于众。对于每一块,都有256个值 【 A , H 1 ( A ) , ⋯ , H 255 ( A ) 】 【A, H^1(A), \cdots , H^{255}(A)】 【A,H1(A),⋯,H255(A)】每一个值都是由前一个值hash得到的,hash函数就这样像把这256个值用链子串起来,形成了一条像是链子一样的结构,而对应的签名就是从这条链上的某个位置取出来的一个值。这样的结构就被称为WOTS链条。每一个块对应了一条链。
-
签名者发送明文和签名:明文
0000 0000 - 0000 0001 - 0000 0010 - 0000 0011
.签名 A , H ( B ) , H 2 ( C ) , H 3 ( D ) , H 3 ( E ) , H 246 ( F ) A,\ H(B),\ H^2(C),\ H^3(D),H^3(E),\ H^{246}(F) A, H(B), H2(C), H3(D),H3(E), H246(F) -
验签者验证:对于第一个byte:
0
,把对应的签名 A A A 做255-0=256
次哈希函数运算,得到 H 255 ( A ) H^{255}(A) H255(A) ,这与第一byte对应的公钥一致,第一byte验证完毕。对于第二个byte:1
,把对应的签名 H ( B ) H(B) H(B) 做255-1=254
次哈希函数运算,得到 H 255 ( B ) H^{255}(B) H255(B) ,这与第二byte对应的公钥一致,第二byte验证完毕。后面的两个byte以此类推。最后的校验和还是使用之前的方法,这里不再赘述。
2.4.2 该方案的安全性
聪明的你肯定又发现了,只要攻击者知道sk0,那么通过Hash计算,就可以知道后面的
s
k
1
∼
s
k
254
sk_1 \sim sk_{254}
sk1∼sk254。那么攻击者是不是就可以把这个byte的0
篡改成1~254
呢?
这时候校验和就发挥它的作用了。
还是以上面的痢疾,攻击者将消息0 1 2 3
中的0
修改成1
,这里整体的和增加了1,根据检验和的定义,检验和就会减小1,校验和就从0000 0011 - 1111 0110
变成了 0000 0011 - 1111 0101
,可以看到,校验和最后一块从246变成了245,但是攻击者无法知道
H
245
(
F
)
H^{245}(F)
H245(F)的值(因为哈希函数的单向性)。这样就和之前的校验和一样,形成了一个死锁结构。
综上,保证了此算法的安全性。
四、总结
从最开始的Lamport提出的方案,现在一共介绍了4种不同的签名方案,可以说是一脉相承,步步改进。现在我们来总结一下。
我们以下面的几个维度来对比以下这四种算法:
- 安全性:是否安全,不可被攻击
- 密钥对和签名长度:私钥长度、公钥长度、签名长度
- 签名算法花费的时间:签名时间复杂度、验证时间复杂度
我们假设massage的长度为n
bit
4.1 第一版
第一版是不安全的,可以被攻击中篡改。因此我们也不讨论后面的两个维度了。
4.2 第二版
- 安全性:是。
- 私钥长度:
2n
,公钥长度2n
,签名长度n
- 签名时间复杂度 O ( n ) O(n) O(n),验证时间复杂度 O ( n ) O(n) O(n)
4.3 第三版 Merkle 1990
- 安全性:是。
- 私钥长度: n + l o g 2 n n+log_2n n+log2n,公钥长度 n + l o g 2 n n+log_2n n+log2n,签名长度 n + l o g 2 n n+log_2n n+log2n
- 签名时间复杂度 O ( n ) O(n) O(n),验证时间复杂度 O ( n ) O(n) O(n)
4.4 第四版 Winternitz 1979
第四版比较特殊,因为要分块。
假设我们每一块能代表的数为w
。这里的w
也被称为Winternitz系数,每一块就是
l
o
g
2
w
log_2w
log2w bit
因此块数为
l
1
=
n
/
l
o
g
2
w
l_1=n/log_2w
l1=n/log2w
- 安全性:是。
- 私钥长度: l 1 + l o g 2 ( l 1 ∗ w ) l_1+log_2(l_1*w) l1+log2(l1∗w),公钥长度 l 1 + l o g 2 ( l 1 ∗ w ) l_1+log_2(l_1*w) l1+log2(l1∗w),签名长度 l 1 + l o g 2 ( l 1 ∗ w ) l_1+log_2(l_1*w) l1+log2(l1∗w)
- 签名时间复杂度 O ( w ) O(w) O(w),验证时间复杂度 O ( w ) O(w) O(w)
4.5 三个版本的签名大小对比图
我用python的matplotlib画了一个图,来对比一下三种方案的签名大小。
画图代码:
import matplotlib.pyplot as plt
import numpy as np
n = np.array([i for i in range(1, 1024)])
y1 = 2*n
y2 = n+np.log2(n)
w = 16
l1 = n / np.log2(w)
y3 = l1 + np.log2(l1 * w)
w = 256
l1 = n / np.log2(w)
y4 = l1 + np.log2(l1 * w)
plt.figure(figsize=(15,10))
plt.plot(n, y1, label = 'Lamport')
plt.plot(n, y2, label = 'Merkle')
plt.plot(n, y3, label = 'Winternitz w=16')
plt.plot(n, y4, label = 'Winternitz w=256')
plt.legend(fontsize=15)
plt.grid()
plt.ylabel('signature size', fontsize=20)
plt.xlabel('massage size', fontsize=20)
# plt.xlim(1,40)
# plt.ylim(0,80)
plt.show()