A Digital Signature Based on a Conventional Encryption Function【翻译】

翻译A Digital Signature Based on a Conventional Encryption Function,在本文中,Merkle提出了著名的Merkle hash树,它在其它方面也有不少应用,值得一读。

基于常规加密函数的数字签名

By Ralph C.Merkle
Elxsi
2334 Lundy Place
San Jose, CA 95131

摘要

本文描述了一种新的基于常规加密函数(比如DES)的数字签名,它和底层加密函数具有相同的安全性——其安全性不是建立在因式分解(factoring)的复杂性基础上,并且避免了模运算(modular arithmetic)的高计算代价。签名系统可以签署任意数量的消息,并且签名大小是消息个数的对数函数。在一个典型的系统中,签名大小可能从几百个字节到几千个字节,生成一个签名可能需要把底层加密函数执行几百到几千次。

介绍

数字签名系统已经证明[1,3,5,10],仅依赖常规加密函数(或者单向函数),都没有比较成功的为基于像因式分解[2,4]这样更加复杂的数学难题的系统提供便利性。对安全性仅基于单向函数的系统感兴趣的一个重要原因是,这类函数的存在性是可以保证的,然而因式分解的复杂性和更有效的因式分解算法依然是被广泛研究的待解问题。这并不是纯学术兴趣的问题,特别是鉴于大量的“不可攻破”的加密系统已经被相继攻破。
第二个优点是,与需要模运算的系统相比的简化计算代价: DES(Data Encryption Standard)的软件实现比模N的指数运算更快,因此一个基于DES的数字签名系统也同样有优势。这种代价的节约在硬件实现上更明显,因为DES芯片已经可以从许多制造商低价买到,并在许多已有系统中运用。当嵌入到一个已有DES芯片(或者任何硬件实现的常规加密函数)的系统中时,这种新的数字签名系统速度确实很快。
为了确保本文是完备的,我们首先简单回顾一些单向函数和一次性签名的已知结果,然后介绍一次性签名系统如何以一种新的方式来提供一个数字签名系统,它克服了阻碍这种方法被接受和使用的限制和缺陷。

单向函数

单向函数F是一个很容易计算,但难以反向求解的函数。给定x,计算y=F(x)是很容易的,而给定y,计算任何满足F(x)=y的x是困难的。
单向函数可以基于常规加密函数,因为通过明文(plain text)和密文(cipher text)来破解密钥(key)也是非常困难的。如果我们定义一个常规加密函数为:Skey (plaintext)=ciphertext,这样通过Sx (0)=y我们就定义了一个单向函数F(x)=y。也就是,我们用x做为密钥来加密一个常数,输出密文就是单向函数的输出结果。现在给定y来破解x就等同于,已知plaintext=0和ciphertext=y的情况下求解key。
单向hash函数——比如,一个接受任意大小输入(比如几千KB)并产生固定大小输出(比如64bit)的单向函数——以类似的方式,可以基于常规加密函数的重复应用。设计单向函数时需要注意:大多数方法都不能有效的抵御平方根攻击(square root attack)。比如,我们希望从112bit简化到64bit(使用DES),最明显的技术就是把112bit分解为两个56bit的块,然后重复加密一个常数。也就是,设这两个56bit的块分别为K1和K2,然后计算Sk2 (Sk1 (0))。不幸的是,使用“meet in the middle”和”square root”攻击,在2^28次计算内就可能确定出产生相同结果的新K1’和K2’。然而这种攻击也是可以避免的,重要的是要知道它们的存在,并必须防御之。
我们假设一个安全的单向hash函数,它可能是基于某些常规加密函数,并用F来标识。

签署1bit的消息

本节和下一节为不熟悉的读者简单介绍一次性签名,这两节可以跳过而不会失去连续性。

【关于一次性签名,可以参看wiki 上的介绍】

某君A可以使用下面的协议为某君B签署1bit的消息:首先,在预计算阶段,A使用F来单向加密两个值:x[1]和x[2],生成两个对应的值y[1]和y[2]。然后,A公布y[1]和y[2],但要保密x[1]和x[2]。最后,如果那1bit的消息是’1’,A通过把x[1]给B来签署它,如果是消息是’0’,则通过把x[2]给B来签署。
如果那1bit的消息是’1’,那么B知道A发送的是x[1],于是B通过计算F(x[1])=y[1]来验证。因为F和y是公开的,任何人都可以通过上面的计算验证结果。因为只有A知道x[1]和x[2],B有关x[1]的知识暗示了x[1]是A给B的,基于前面的协定,这个事实证明了A签署了消息’1’。
【译注:签署消息后,A将不能再次使用这两个x和y来签署其它的消息,因此这被称为一次性签名】

签署几个bit的消息

如果A生成了多个x和y,那么A就可以签署包含多个bit的消息。这就是Lamport-Diffie一次性签名[1]。签署一个n-bit的消息需要2n个x和2n个y。
Merkle[3]提出了该方法的一个改进,将签名大小减少到了差不多1/2。A仅为要签署消息的每bit生成一个x和一个y,而不是2个x和两个y。如果消息的某一个bit为’1’,A公布对应的x值——然而如果该bit为’0’,A不公布任何数据。因为这样B可以假装没有收到某些x,于是把消息中的某些’1’ 假装是’0’,因此A还要必须签署消息中’0’bit的个数count。现在当B假装一个’1’bit是’0’bit时,他必须还要同时增加count的值——这是不可以的。因为count只有logn个bit,因此签名大小被差不多减少了1/2——从2n减少到n+logn。
举个例子,如果我们想要签署一个8bit的消息‘0100 1110’,我们将首先计算0bit的个数(4),然后向原始的8bit消息追加一个3bit的count域(值为4),从而生成一个11bit的消息‘0100 1110 100’,我们通过公布x[2],x[5],x[6],x[7]和x[9]来签署它。B不能假装没有收到x[2],因为这样将会产生错误的消息——‘0000 1110 100’将含有5个’0’。同样的,假设没有收到x[9]将会产生错误的消息’0100 1110 000’,其count域显示消息应该没有’0’。没有这样的x组合,B可以假装没有收到它们从而编造一个合法的消息。
【译注:不是很明白啊,比如说,B可以把消息 0100 1110 100假装是 0100 1100 101,即B假装第4个1是0,并且是5个0,不就刚好可以伪装了吗?】
Winternitz[6]提出了一种改进,可以将签名大小减少到原来的几分之一。不通过计算y[1]=F(x[1])和y[2]=F(x[2])来签名1-bit的消息,A可以通过计算y[1]=F(F(F(F(x[1]))))和y[2]= F(F(F(F(x[2]))))来签名2-bit的消息。符号上,我们用上标来标记函数F的反复调用——F^3= F(F(F(x))),F^2= F(F(x)),F^1= F(x),F^0= x。如果A要签名消息m——肯定等于‘0’,‘1’,‘2’或者‘3’——A就公布F^m(x[1])和F^(3-m)(x[2])。通过计算满足F^n=y的n,B可以容易的验证A使用的函数F的次方。计算x[1]和x[2]的互补次方运算是必须的,因为B可以假装收到一个比A实际发送的更高的次方。也就是,如果A发送给B F^2(x[1]),那么B计算F^3(x[1]),然后假装称A发送的是F^3。然而这样的话,B必须计算F^0(x[2])——如果A发送的消息是‘3’的话,这个值就已经被A计算好并发给B了。因为A实际发送的是F^2(x[1]),这意味着B需要从F(x[2])计算出x[2]——显然不可以。公布x[1]和x[2]的互补次方运算其实就等同于在原Lamport-Diffie方法中公布或者x[1]和x[2]。
尽管这个例子描述了如何签名一个从0到3的消息,它也能泛化,签名从0到n-1的消息,只需要计算F^(n-1)(x[1])和F^(n-1) (x[2])序列。
上面Merkle提出的方法其实就是Winternitz一次性签名算法的特例。
因此,由Lamport-Diffie提出的原签名系统,经过Merkle和Winternitz的改进,可以用来签名任意消息,并具有优秀的安全性。签名一条消息所需的内存和计算代价也是很合适的。不幸的是签名更多的消息就需要更多的x和y,因此公共文件中需要记录更多的信息(y)。要允许A签名1000个消息,大概需要10000个y——如果系统中有1000个用户,每个人都想签名1000个消息,这将使公共文件增加到几百MB——这是很不方便的,并将严重影响系统的应用。

一次性无穷签名树

新系统的基本思想是为一次性签名使用一个无穷树。简单起见,我们假设是二叉树,树的根放在一个公共文件中,易于验证。树的每个节点都执行三个功能:(1)验证左子节点,(2)验证右子节点,(3)签名一条消息。因为树中可以有无穷个节点,因此可以签名无穷多的消息。为了执行这三个功能,每个节点都必须包含3个签名信息——‘left’签名、‘right’签名和‘message’签名。‘left’签名用来签左子节点,‘right’签名用来签右子节点,‘message’签名用来签一条用户消息。
使用下面的方式来编号节点会带来很多便利之处:
根节点被编号为1;
节点i的左子节点被编号为2i;
节点i的右子节点被编号为2i+1;

数字编号有很多便利的性质。它唯一的标识了树中的每个节点;可以很容易从根节点计算出左右子树;通过简单的除以2就可以方便的从子节点计算出根节点。注意,如果我们从节点1开始,并沿着左子节点遍历,经过的节点编号将是:1、2、4、8、16、32、64…
我们使用更多的符号约定区分x和y,它们签名树中各节点代表的不同消息——特别的,我们使用x和y的3维数组:数组x[<节点编号>, <left, right 或者message>, <一次性签名的索引>]。如果我们使用原Lamport-Diffie方法(每条消息生成128个x),那么节点i上的所有x将会是:
x[i,left, 1], x[i,left, 2], x[i,left, 3], …x[i,left, 128]
x[i,right, 1], x[i,right, 2], x[i,right, 3], …x[i,right, 128]
x[i,message, 1], x[i,message, 2], x[i,message, 3], …x[i,message, 128]

我们把节点i上所有‘left’签名的x标记为:x[i, left, *]。类似的,我们用y[i, right, *]表示节点i上的‘message’签名的y【译注:我想这里应该是y[i, message, *]才对,对应与message签名】。我们把节点i上的所有x(left, right和message)标记为x[i, *, *]。
给定一个签名,我们经常会对所有的y执行一次单向hash函数运算,因此我们使用符号F(y[i, right, *])来表示对节点i上的所有right签名执行单向hash函数F。
因此,我们的基本数据结构为两个三维的无限数组x和y,其中y是使用F函数从x计算出来的。
【译注:x[i,message, *]是待签署的用户消息的128bit签名,y[i,message, *]是x[i,message, *]执行hash计算(函数F)后的结果】
我们经常会在给定节点上计算所有y的全hash值,先将F作用于每个签名上,然后再把F作用于这三个结果上,定义HASH(i):
HASH(i) = F( F(y[i,left,*]), F(y[i,right,*]), F(y[i,message,*]) )
这就是节点i的单向全hash值。它具有一个重要的性质:如果我们已经知道了HASH(i),这时有人声称给我们发送了y[i,*,*]值,通过重新计算这些函数我们就能确认他们发送的是正确的值(或者是错误的值)。如果从接收到的数据【译注:就是发送者声称的y[i,*,*]】计算出来的HASH(i)和我们已知的相匹配,我们就能确认收到了正确的y[i,*,*]值。
在之前,A先把HASH(1)放到一个公共文件中。HASH(1)验证了A的验证树的根节点,并假设可以被任何人获得。
现在我们可以描述A用签名i签署消息m,以及B验证签名的算法了。

新的签名算法

A和B事先商定待签署的消息M,A选择节点i来签署M。【译注:自下而上的验证算法,只要B验证:根据A发送的信息计算出来的HASH(1)和从公共文件取得的HASH(1)匹配,就说明A发送的是正确签名的消息,否则是没有正确签名的】
1) A向B发送i和y[i, message, *]
2) A向B发送x[i, message, *]的相应子集,表示签名了消息M【译注:回忆Lamport-Diffie算法,A需要根据消息M的单向hash的128bit结果来选择对应的x】
3) 在校验y[i, message, *]时,B检查接收到的x[i, message, *]是否正确的签名了M【译注:通过x计算出y(F是公开的),检查是否和接收到的y一致】
4) A向B发送F(y[i,left,*])、F(y[i,right,*])和F(y[i,message,*]
5) A计算HASH(i),根据定义它等于:F( F(y[i,left,*]), F(y[i,right,*]), F(y[i,message,*]) )
6) 如果i是1,B根据从 A接收到的值计算出的HASH(1),并检查是否与从公共文件中取得的相匹配,算法结束
7) 如果i是偶数【译注:i是左子节点】
   A向B发送y[i/2,left,*]
   A向B发送正确的x[i/2,left,*]子集,表示签署了HASH(i)
   B计算HASH(i),并通过检查x和y[i/2,left,*]来检查HASH(i)是否被正确签署
【译注:x和y都是从A接收的】
8) 如果i是奇数【译注:i是右子节点】
   A向B发送y[i/2,right,*]
   A向B发送正确的x[i/2, right,*]子集,表示签署了HASH(i)
   B计算HASH(i),并通过检查x和y[i/2,right,*]来检查HASH(i)是否被正确签署
9) A和B都将i除以2,并转到步骤4
算法结束后,B将有log(i)个签名,其中一个是B真正想要的消息M的一次性签名,而其他的每一个签名验证了下一个签名的正确性和合法性——根签名的合法性可以通过公共文件证实。因此,这个一次性签名的审查轨迹从HASH(1)开始持续到HASH(i),最终结束于消息M的一次性签名。
【译注:关于上面的算法是如何有效验证多个消息的,可以参考论文《method of providing digital signatures 》,其中有一个验证8个消息的例子,本文最后会译出这个例子】
需要明确的是,这个“元系统”可以利用任何的一次性签名算法,并且对一次性签名算法的性能改进也会对该“元系统”产生相应的改进。没有特殊的理由认为当前的一次性签名系统已经很完善了,因此对一次性签名系统的进一步研究可能会获得有价值的性能改进。
还需要明确的是,二叉树是随意选择的——使用K叉树也同样简单,可能实际也在用。二叉树需要log(i)个签名,而K叉树只需要logk (i)个——K越大,签名就越小。然而使用K叉树时,HASH(i)的计算将会是:
F(
F(y[i, fisrt-sub-node, *]),
F(y[i, second-sub-node, *]),
…,
F(y[i, kth-sub-node, *]) ,
F(y[i, message, *])
)
每个节点的计算会更耗时,因为所有k个子节点的y都需要重新计算,每个节点需要更多的内存来存储签名结果。因此k不能太大——它受限于这些限制。
在增加k时最小化每个节点需要的附加验证信息,本身就是一个有趣的问题。如上所述,每个节点所需要的签名信息随着k线性增长。在作者之前的“tree-signature”方法[3, 7,P170]中,这已经被减少到log(k)。把这两个系统合并到一个系统中看起来很合适,并且可以有效的利用大的k值。尽管都使用了树,作者之前的“tree-signature”方法和本方法是很不相同的。有意思的是,Goldwasser,Micali和Rivest[8,9]也在数字签名系统中使用了类似于树的结构来达到可行的理论性质。他们的签名系统是“…based on the existence of a “claw-free” pair of[trapdoor] permutations”,通过两个大素数的乘法而构建(就像RSA算法那样)。
最后,有些读者可能认为两个三维无限数组x和y会让用户A存储起来很不方便——因此最好有个压缩机制。数组y是从数组x计算而来的,因此数组y其实不比存储。数组x是用户A随机选择的,A也可能使用一个安全的伪随机序列来生成x。特别的,A可能这样生成x[i,j,k]:先连接i,j和k,然后使用一个常规加密函数和安全密钥key来加密这个bit串:x[i,j,k] = SA’s security key(<i, j, k>)。如果我们使用DES,A的密钥只需要56bit。即使许多x都已经公开了(随着签名许多不同的消息),也不可能破解A的密钥。(<i,j,k>, x[i,j,k])就是一个plaintext-ciphertext对,定义上即使获得许多这样的对,一个传统函数的密钥也是难以破解的。
在实际使用中,A需要记录的就是一个安全密钥(可能是56bit)和一个简单计数(可能20或者30bit),以记录追踪树上签名最后一条消息的节点编号。如果计算用正确的顺序组织,生成一个签名只需要少量的内存(128个字节就足够了)。这么小的内存常用于低价高容量系统中,比如智能芯片(带内建电脑的信用卡)。

结论

本文描述了一个仅基于常规加密函数的数字签名系统。签名和验证签名算法速度快,而且只需要极少量的内存。签名大小随着待签名消息的个数呈对数增长。签名大小和所用内存可以根据计算需求来权衡。

参考文献

照例,免贴。

【全文完】

一个merkle树的验证例子

【译注:下面写一个使用merkle验证树的例子,希望能便于读者理解】
设Y=Y1Y2,…,Yn为一组待签名的消息(n个),首先定义hash函数H(i,j,Y):
H(i,i,Y) = F(Yi)                                                        ----(1)
H(i,j,Y) = F( H(i,i+j-1/2, Y), H(i+j-1/2, j, Y) )    ----(2)

F(Yi)是一个单向函数,H(i,j,Y)是Yi,Yi+1,…和Yj的单向函数,于是H(1, n, Y)可以验证从Y1到Yn的所有消息。
树的每个叶子节点代表一个待签名的消息,这个和本文描述的不一致,但更容易理解,感觉更清晰。我们取n=8,那么merkle验证树结构如下图所示:

如果我们当前要验证Y5,发送者A和接收者B将会以如下的步骤执行:
1) H(1, 8, Y)已经被验证,并且是已知的(回忆前面讲过根节点的H值是公开的)
2) 由于H(1, 8, Y) = F( H(1, 4, Y), H(5, 8, Y) ),A向B发送H(1, 4, Y)和H(5, 8, Y);然后B可以通过计算H(1, 8, Y) = F( H(1, 4, Y), H(5, 8, Y) )确认H(5, 8, Y)是正确的。
-->现在B已经确认了H(5, 8, Y)。
3) A向B发送H(5, 6, Y)和H(7, 8, Y);然后B可以通过计算H(5, 8, Y) = F( H(5, 6, Y), H(7, 8, Y))确认H(5, 6, Y)是正确的。
-->现在B已经确认了H(5, 6, Y)。
4) A向B发送H(5, 5, Y)和H(6, 6, Y);然后B可以通过计算H(5, 6, Y) = F( H(5, 5, Y), H(6, 6, Y))确认H(5, 5, Y)是正确的。
-->现在B已经确认了H(5, 5, Y)。
5) A向B发送Y5;然后B可以计算H(5, 5, Y) = F(Y5),确认无误。
-->现在B已经确认了Y5,验证结束。
在该方法下,仅需要logN的消息交互,当然上述步骤还有改进空间,但是这样更能说明验证的基本流程。
如果接下来再验证Y6的话,B已经知道了H(6,6,Y),于是只需要向A请求Y6,然后直接计算H(6, 6, Y) = F(Y6)再判断是否和前面接收到的H(6, 6, Y)相等就可以了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值