【零基础入门哈希签名架构,从Lamport到SPHINCS+】第一集 哈希签名始祖 Lamport 方案及其改进版本(Merkle,Winternitz)

第一集 哈希签名始祖 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 加密

加密的过程是:

  1. 发送信息的人(sender)对明文(massage)用某种方法进行加密(encryption)形成密文(cipher text)
  2. 发送者把密文(cipher text)发送出去
  3. 接收信息的人(receiver)也用某种方法对密文进行解密(decrypt)得到明文。

1.1.2 签名

签名的过程是:

  1. 发送信息的人(sender)对明文(massage)用某种方法进行签名(sign)形成签名(signature)
  2. 发送者把明文(massage)和签名(signature)一同发送出去
  3. 接受信息的人,用某种方式对明文进行验证(authentication),来判断这个明文是否为发送者本人发出

1.1.3 对比

加密和签名的不同点:

  1. 加密中,除了发送者以外的所有人都只能看到密文;而在签名中,所有人都可以看到明文
  2. 加密算法中,不能验证信息是由谁发出来的,谁都可以冒充发送者发送信息;签名算法中,可以验证信息是谁发出来的,任何人都无法冒充发送者来发送信息

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会把公钥公布于总,私钥自己留着。

  1. 签名(sign)是sender用私钥对明文(massage)进行“加密”(签名),得到签名(signature)
  2. 验证(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的方案,等后面提到再倒回来看。

一般情况下我们希望哈希函数满足以下三条性质:

  1. 抗原像性 preimage resistance(有些时候称为“单向性”):给出一些输出Y=H(X),找到一个令H(X)=Y的输入X是一件很耗时间的事。(要使用暴力穷举才能找到输入X,比如说X是一个1024bit的数据,你需要一个一个的去尝试,看看在2^1024这个空间内,哪一个X才能输出这个对应的Y)
  2. 抗第二原像性 Second preimage resistant :它和抗原像性有一点不同,给出一个输入X,攻击者很难找到另外一个输入X’,使得H(X)=H(X’)。
  3. 抗碰撞性 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 具体流程

  1. 生成私钥:首先随机生成3个不同的数,比如以十六进制为单位:9 A E 换算成十进制就是9 10 14这里用什么进制表示无所谓,只要是3个不同的数就可以。这3个数分别对应3个不同的bit
  2. 生成公钥:使用哈希函数H(x),对私钥进行运算,得到公钥。y=H(x),y就是公钥,F是哈希函数,x是私钥。比如说,对私钥的第一位(bit)进行哈希运算,5 = H(9),5就是第一位的公钥。我们暂时不需要知道这个哈希函数是什么,我们只需要确定,它有上面提到的三条性质。然后Sender包公钥公布于众,比如说最后得到的公钥是5D2
  3. Sender发送明文和签名,明文就是010。签名是这样的,当明文为0时,对应位置上的签名为空,当明文为1时,对应位置上的签名就是相应的私钥。因此这里的签名Signature是空A空
  4. Receiver接收到签名。开始验证,signature上的位置和massage中0的位置是对应的,这没有问题。然后再对signature中的A进行哈希运算,H(A) = D,得到的结果是D,这和公钥中对应位置的数是一样的,因此也没有问题。好了,验证完毕,receiver可以确定这个010是sender发出来的了。

2.1.1 方案的漏洞

聪明的你肯定发现了,这个方案有一个漏洞,就是攻击者可以把原本为1的bit篡改成0,因为这样只需要把原来的私钥改成即可。
然后攻击者给receiver发送000空空空来冒充成sender。

决定这个问题也很简单粗暴,就是直接把私钥的长度加一倍。这样0也有自己对应的私钥了,不再是

2.2 第一代改进

2.2.1 具体流程

  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]

  2. 生成公钥:还是使用哈希函数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]
    然后Sender包公钥公布于众

  3. Sender发送明文和签名,明文就是010。签名是这样的,当明文为0时,对应位置上的签名为 s k 0 i sk_0^i sk0i,当明文为1时,对应位置上的签名就是相应的 s k 1 i sk_1^i sk1i。因此这里的签名Signature是BA4

  4. Receiver接收到签名。开始验证,第一个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以此类推。验证完毕后,receiver可以确定这个010是sender发出来的了。

签名:

在这里插入图片描述
验证:
在这里插入图片描述
安全性:
在这里插入图片描述

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)

  1. 计算Massage中0的个数,这里Massage时010,因此0的个数为2,在3bit的Massage中0最多有3个,因此要额外用2bit的位置表示校验和,因此加上校验和(Checksum)后的Massage为 010 10。同时我们可以和容易的算出,nbit的Massage只需要在后面加上 l o g 2 n log_2n log2n bit的校验和即可。我们都知道 l o g 2 n log_2n log2n是远小于 n n n的,因此可以节省很多空间,这样密钥对的大小就可以被大大减小了。
  2. 生成私钥:因为多了2bit的校验和,因此要随机生成3+2个不同的数,密钥:9 A E 5 0
  3. 生成公钥:使用哈希函数H(x),对私钥进行运算,得到公钥。最后得到的公钥是5D267
  4. Sender发送明文和签名,明文就是010 10。签名是这样的,当明文为0时,对应位置上的签名为空,当明文为1时,对应位置上的签名就是相应的私钥。因此这里的签名Signature是空A空 5空
  5. Receiver接收到签名。开始验证,signature上的位置和massage中0的位置是对应的,这没有问题。然后再对signature中的A进行哈希运算,H(A) = D,得到的结果是D,这和公钥中对应位置的数是一样的,因此也没有问题。校验和的两位也是同理。好了,验证完毕,receiver可以确定这个010是sender发出来的了。

为什么这样做就是安全的呢?如果攻击者,把原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 2n长度的key,而现在只需要 n + l o g 2 n n+log_2 n n+log2n,大大减小了key的长度。

2.4 第三代改进 Winternitz 1979

1979年,斯坦福大学数学系的罗伯特·温特尼茨(Robert Winternitz)提出了一个进一步的实质性改进,将签名消息的大小额外减少了大约4到8倍。Winternitz的方法用时间换取了空间:减少的尺寸是以增加的计算工作量来换取的。

2.4.1 具体流程

这一代的具体流程和前面的3个版本都有挺大差别,他从另一个角度来减小签名的大小。
假设我们现在有一个32bit的massage

  1. 分块:首先将massage分块,每一块的大小一般为4bit或者8bit,这里我们使用8bit,因此massage被分成4块。每一块就是1byte。因为8bit可以表示0-255,所以我们直接用十进制来表示massage,我们的32bit的massage是 0 1 2 3写成二进制就是 0000 0000 - 0000 0001 - 0000 0010 - 0000 0011

  2. 计算校验和:这里的校验和就是每一块数据的和,0+1+2+3=6,因此这里校验和是6。在这个例子中,校验和最大为4*255=1020因此需要额外的10bit来表示校验和。加上校验和后的massage为 0000 0000 - 0000 0001 - 0000 0010 - 0000 0011 -- 00 0000 0110。最后的明文0000 0000 - 0000 0001 - 0000 0010 - 0000 0011 -- 0000 0000 - 0000 0110,这里为了方便,把校验和中的6bit变成2byte,所以现在一共是6byte。

  3. 生成私钥:生成4个私钥,每一个私钥对于原massage中的一个byte。比如说私钥为A B C D。但是一个byte有256种可能,而私钥只有一个,怎么一一对应起来呢?这就涉及到后面公钥是怎么生产的了。

  4. 生产签名:最神奇的地方来了。对于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) 。后面校验和的signature还是使用之前的签名方法。

  5. 产生公钥:公钥就是将密钥做256次哈希运算后得到的值: H 256 ( A ) ,   H 256 ( B ) ,   H 256 ( C ) ,   H 256 ( D ) H^{256}(A),\ H^{256}(B),\ H^{256}(C),\ H^{256}(D) H256(A), H256(B), H256(C), H256(D) 。后面校验和的公钥还是使用之前的方法, p k 5 ,   p k 6 pk_5,\ pk_6 pk5, pk6。Sender将公钥公布于众。

  6. Sender发送明文和签名:明文0000 0000 - 0000 0001 - 0000 0010 - 0000 0011 -- 0000 0000 - 0000 0110.签名 A ,   H ( B ) ,   H 2 ( C ) ,   H 3 ( D ) ,   s k 5 ,   s k 6 A,\ H(B),\ H^2(C),\ H^3(D),\ sk_5,\ sk_6 A, H(B), H2(C), H3(D), sk5, sk6

  7. Receiver验证:对于第一个byte: 0,把对应的签名 A A A256-0=256次哈希函数运算,得到 H 256 ( A ) H^{256}(A) H256(A) ,这与第一byte对应的公钥一致,第一byte验证完毕。对于第二个byte: 1,把对应的签名 H ( B ) H(B) H(B)256-1=255次哈希函数运算,得到 H 256 ( B ) H^{256}(B) H256(B) ,这与第二byte对应的公钥一致,第二byte验证完毕。后面的两个byte以此类推。最后的校验和还是使用之前的方法,这里不再赘述。

在这里插入图片描述

2.4.2 该方案的安全性

聪明的你肯定又发现了,只要攻击者知道sk0,那么通过Hash计算,就可以知道后面的 s k 1 ∼ s k 254 sk_1 \sim sk_{254} sk1sk254。那么攻击者是不是就可以把这个byte的0篡改成1~254呢?

但是我们上面提到校验和的时候,一直说我们用之前的方法来验证校验和。这就意味着校验和只能被改小(因为只能把1改成0),也意味着校验和是6只能变小或者不变。

假如攻击者不改变校验和。攻击者将0 1 2 3中的0修改成1,则需要在后面的3个byte中选一个减去1。但是攻击者是无法将一个byte里的数减小的,只能增大,(因为哈希函数的单向性)。

假如攻击者将校验和变小。同理,攻击者肯定需要在0 1 2 3中至少把其中的一个数变小,这也是攻击者做不到的。

综上,保证了此算法的安全性。

四、总结

从最开始的Lamport提出的方案,现在一共介绍了4种不同的签名方案,可以说是一脉相承,步步改进。现在我们来总结一下。

我们以下面的几个维度来对比以下这四种算法:

  1. 安全性:是否安全,不可被攻击
  2. 密钥对和签名长度:私钥长度、公钥长度、签名长度
  3. 签名算法花费的时间:签名时间复杂度、验证时间复杂度

我们假设massage的长度为nbit

4.1 第一版

第一版是不安全的,可以被攻击中篡改。因此我们也不讨论后面的两个维度了。

4.2 第二版

  1. 安全性:是。
  2. 私钥长度:2n,公钥长度2n,签名长度n
  3. 签名时间复杂度 O ( n ) O(n) O(n),验证时间复杂度 O ( n ) O(n) O(n)

4.3 第三版 Merkle 1990

  1. 安全性:是。
  2. 私钥长度: 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
  3. 签名时间复杂度 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

  1. 安全性:是。
  2. 私钥长度: l 1 + l o g 2 ( l 1 ∗ w ) l_1+log_2(l_1*w) l1+log2(l1w),公钥长度 l 1 + l o g 2 ( l 1 ∗ w ) l_1+log_2(l_1*w) l1+log2(l1w),签名长度 l 1 + l o g 2 ( l 1 ∗ w ) l_1+log_2(l_1*w) l1+log2(l1w)
  3. 签名时间复杂度 O ( w ) O(w) O(w),验证时间复杂度 O ( w ) O(w) O(w)

4.5 三个版本的签名大小对比图

我用python的matplotlib画了一个图,来对比一下三种方案的签名大小。

massage为1~1024bit
massage为1~40bit
画图代码:

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()
  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值