回首近几年,我有幸经历了两个相互冲突、却又令人着迷的时代潮流变迁。第一个潮流变迁是:专家学者们耗费四十年设计的密码学,终于派上用场;从信息加密、电话安全、到加密数字货币,我们可以在生活的方方面面发现使用密码学的例子。
第二个潮流变迁是:所有密码学家已经做好准备,迎接以上美好的幻灭。
正文开始之前我得重申一下,本文所讲的不是所谓量子计算启示录(末日预言),也不是要讲 21 世纪密码学的成功。我们要谈论的是另一件未成定局的事情——密码学有史以来最简单的(也是最酷炫的)技术之一:基于散列函数的签名。
在 20 世纪 70 年代末,Leslie Lamport 发明了基于哈希函数(Hash Function,又称散列函数)的签名 ,并经过 Ralph Merkle 等人进一步改进。而后的很多年,这被视为密码学领域一滩有趣的“死水”,因为除了相应地产生冗长的(对比其他复杂方案)签名,基于哈希函数的签名好像没有什么作用。然而近几年来,这项技术似乎有了复苏的迹象。这很大程度归因于它的特性——不同于其他基于RSA或离散对数假设的签名,哈希函数签名被视为可以抵抗量子计算攻击(如 Shor's 算法)。
首先,我们进行一些背景介绍。
背景:哈希函数和签名方法
在正式介绍哈希函数签名之前,首先你得知道密码学中的哈希函数是什么。哈希函数可以接受一串字符(任意长度)作为输入,经过“消化”后,产生固定长度的输出。常见的密码学哈希运算,像是 SHA2、SHA3 或 Blake2 等,经运算会产生长度介于 256 ~ 512 位的输出。
一个函数 H(.) 要被称作“密码学”哈希函数,必须满足一些安全性的要求。这些要求有很多,不过我们主要聚焦在以下三个方面:
- 抗-原像攻击 Pre-image resistance (或俗称“单向性”):给定输出 Y=H(X),想要找到对应的输入 X 使得 H(X)=Y 是一件“极度费时”的工作。(这里当然存在许多例外,但最棒的部分在于,不论 X 属于什么分布,找到 X 的时间成本和暴力搜寻相同。)
- 抗-次原像攻击:这和前者有些微的差别。给定输入 X,对于攻击者来说,要找到另一个 X' 使得 H(X)=H(X') 是非常困难的。
- 抗-碰撞:很难找到两个输入 X1, X2,使得 H(X1)=H(X2)。要注意的是,这个假设的条件比 抗-次原像攻击还要严苛。因为攻击者可以从无垠的选择中寻找任意两个输入。
我们相信所有本文提到的哈希函数示例都能提供上述的所有特性。换言之,没有任何可行的(甚至是概念上的)方法能破解它。当然这种情况也是会变的,如果破解的方法被找到,我们当然会立即停用哈希函数(稍后会讨论关于量子计算攻击的特例)。
我们的目标是使用哈希函数构造数字签名方案,因此简要回顾数字签名这个词能带来很大的帮助。
数字签名方法源于公钥的使用,使用者(签署人)生成一对密钥:公钥和私钥。使用者自行保管私钥,并能够用私钥“签署”任何消息,从而产生相应的数字签名。任何一个持有公钥的人都能验证该消息正确性和相关签名。
从安全的角度来说,我们希望签名是不可伪造的,或是说“存在不可伪造性”。这意味着攻击者(没有私钥控制权的人)无法在某段消息上伪造你的签名。有关数字签名安全的更多定义请参阅这里。
Lamport 一次性签名
在 1979 年,一位名叫 Leslie Lamport 的数学家发明了世界上第一个基于哈希函数的签名。Lamport 发现只要使用简单的哈希函数,或是单向函数,就可以构建出非常强大的数字签名方法。
强大的前提是,用户只需要做一次签名的动作就能保证安全性!后续会做更详细的阐述。
为了更好的讨论,我们假设以下条件:一个哈希函数,它能接受 256 位的输入并产生 256 位的输出; SHA256 哈希函数就是个绝佳的示范工具;我们也需要能产生随机输入的方法。
假设我们的目标是对 256 位的消息进行签名。要得到我们需要的密钥,首先需要生成随机的 512 个位字符串,每个位字符串长度为 256 位。为了便于理解,我们将这些字串列为两个独立的表,并以符号代指:
sk0= sk10, sk20, ...,sk2560
sk1= sk11, sk21, ...,sk2561
我们以列表 (sk~0~, sk~1~) 表示用来签名的 密钥。接下来为了生成公钥,我们将随机的位字符串通过 H(.) 进行哈希运算,得到公钥如下表:
pk0= H(sk10), H(sk20), ...,H(sk2560)
pk1= H(sk11), H(sk21), ...,H(sk2561)
现在我们可以将公钥 (pk~0~,pk~1~) 公布给所有人知道。比如说,我们可以把公钥发给朋友,嵌入证书中,或是发布在 Keybase 上。
接着我们使用密钥对 256 位消息 M 进行签名。首先我们得将消息 M 重现为独立的 256 位元(Bit,又称“比特”):
M1, M2, ..., M256 ∈ {0, 1}
签名算法的其余部分非常简单。我们从消息 M 的第 1 位至第 256 位,逐一相应在密钥列表中的其中一个密钥上取出字符串。而所选密钥取决于我们要签名的消息每一位(bit)的值。
具体一点地说,对于 i = [1,256],如果第 i 位的消息位元 Mi = 0,我们会从 sk0 表中选择第 i 个字符 (ski0) ,作为我们签名的一部分;如果第 i 位的消息位元 Mi = 1,我们则从 sk1 表进行前述过程(即,如果我们要对消息 M 中的第 3 位进行签名,而该位值为 0,则使用 sk0 中的第三位,sk03,作为我们签名的一部分)。对每个消息位元完成此操作后,我们将选中的字符串连接,得到签名。
过程如图示说明,因为部分过程化简,密钥和消息长度只有 8 个 bit(位元)。要注意的是,每个色块代表的都是不同的随机 256 位字符串。
当某个用户(已经知道公钥 (pk0, pk1))收到消息 M 和签名,她能够轻易地验证这个签名。我们以 si 表示签名中第 i 个组成部分,用户能够检查相应的消息 Mi 并计算哈希值 H(si) 。如果 Mi = 0 ,则哈希值必须匹配公钥 pk0 中的元素;如果 Mi = 1 ,则哈希值必须匹配公钥 pk1 中的元素。
如果签名中的每个元素经过哈希运算后,都能找到对应的正确部分的公钥,我们就会说这个签名是有效的。以下是验证过程图示,签名中至少有一个签名元素:
如果你开始觉得 Lamport 的计划有些疯狂,你既是对的,也是错的。
首先探讨下这个数字签名方法的弊端。我们会发现, Lamport 方法的签名和密钥实在太大了,大约有数千 bits。而且更要命的是,这个方法存在严重的安全局限:每个密钥只能被用来签名一个消息,所以 Lamport 方法作为“一次性签名” 在这里被拿来举例。
这种安全局限为什么存在呢?回想一下, Lamport 签名表明了在各个消息位元上可能的两个密钥之一。假如只需要签署一条信息,这个签名方法完全没问题。然而,如果我签署了两条在每一个对应位置 i 的 bit 值都不同的消息,然后连同密钥一起发送出去,这可能导致大问题!
假设攻击者从不同的消息得到两个有效的签名,她便能够发起 “混合搭配(mix and match)”攻击,成功伪造签署第三条我从未签名过的信息。以下图示说明这个攻击过程:
这个问题的严重程度取决于你签名的消息的相异程度,以及有多少消息被攻击者给截获了。但总的来说,这肯定不是件好事。
让我们总结一下 Lamport 签名方法;它很简单、快速,但它在实际应用上还有很多不足之处。或许我们可以做一点优化?
从一次性签名到多次签名:基于默克尔树 (Merkle's tree) 的签名
Lamport 签名方法是个好的开端,但是无法用单一密钥签名多条信息,是它最大的弊端。Martin Hellman 的学生 Ralph Merkle 由此得到大量启发,他很快地想到了一个聪明的解决办法。
虽然我们不打算在这里展开解释默克尔方法的步骤,我们还是来试着理清 Ralph 的想法。
我们现在的目标是用 Lamport 签名方法签署 N 条信息。最直观的方法是,以最初的 Lamport 方法生成 N 个不同的密钥对,然后将所有公钥关联起来,集合成一个超巨大的 mega-key。(mega-key是我现编的术语。)
如果签名者继续拿着这么一把密钥集合,她就可以对 N 条不同消息进行签名,严格上来讲这也只是一把 Lamport 密钥。看起来,这样就解决了密钥重用的问题。验证者也有对应的公钥能够验证所有收到的消息。没有任何的 Lamport 密钥被使用两次。
很明显的,这种方法很糟糕,因为时间成本太高了。
具体地说,上述这种天真的方法中,为了达到要求的签名次数,签名者必须分发比普通 Lamport 公钥还要大数倍的公钥(签名者还要继续拿着同样巨大的私钥)。人们很可能会对这种结果感到不满,也会反思有没有办法避免这种负作用产生。接下来,让我们进入 Merkle 方法。
Merkle 方法希望能找到一个能签署多条不同消息的方法,同时避免公钥的成本线性激增。Merkle 方法的实现如下:
- 首先,生成 N 个独立的 Lamport 密钥,我们以 (PK1, SK1), ..., (PKN, SKN) 表示之。
- 接下来,将每一个公钥分别放到 Merkle hash tree (见下图),并计算根节点哈希值。这个根节点就会成为Merkle签名方法中的 “主公钥”。
- 签名者报关全部的 Lamport 密钥(公钥和私钥),用于签名。
关于 Merkle tree 的更多描述请点击这里。概略地说,Merkle 方法提供了一种能收集不同的值,并用一个 “根” 哈希(例子中使用的哈希函数,长度为 256 bits)代表所收集的值的方法。给出这个根哈希,就能简单“证明” 某个元素存在于这个给出的哈希树。而且这个证明的大小和叶节点数量成对数关系。
-Merkle tree,来自维基百科的解释。Lamport 公钥被放进叶节点中,然后根节点成为主公钥。-
要签名的时候,签名者从 Merkle tree 中直接选择公钥,并用对应的 Lamport 密钥签名。接着她将得到的签名结果连接 Lamport 公钥并附上“Merkle 证明”。Merkle root 可以来佐证该默克尔树中包含选中的公钥(即整个方法使用的公钥)。最后签名者将整个集合当作消息签名发送出去。
(验证者只要直接将这个“签名”分别解压为 Lamport 签名、 Lamport 公钥、 Merkle 证明,就能进行验证。验证者能够依靠拿到的 Lamport 公钥验证 Lamport 签名,并用 Merkle 证明这把公钥的确存在于 Merkle tree 中。只要满足这三个条件,验证者就能确信签名是有效的。)
这个方法的缺点是会将“签名”大小增加两倍以上。不过,现在 Merkle 方法主要的公钥只是一串简单的哈希值,使得这个方法比上面提到的原始 Lamport 方法更为简洁。
最后还有个优化部分,密码学强度的伪随机数发生器能够输出生成各式各样的密钥,同时“压缩”密钥数据本身。这使得原先庞大的位元(显然是随机的)能够转换为简短的“种子(seed)”。
很赞啦!
让签名和密钥更有效率一点
Merkle 方法使得一次性签名转变为 N 次性签名。构造这种方法仍然需要基于某些一次性签名方法,比如 Lamport 方法;但不幸的是,Lamport 方法的(带宽)成本仍相对高昂。
有两种主要的方法可以降低这些成本。第一种也是 Merkle 提出的;为了更好的解释许多强大的签名方法,我们优先说明这项技术。
回想一下 Lamport 方法,要对一条 256 位的消息进行签名,我们需要一个包含 512 个独立密钥(和公钥)位串的向量,签名本身就是 256 个密钥位串的集合。(这些数字会被需要签名的消息位元激活,位元可以是 "0" 或 "1" ,因此需要从两张不同的密钥表中提取适合的密钥元素。 )
这里引发了新的思考:如果我们不对所有的消息位元进行签名,会怎么样呢?
更详细点说,在 Lamport 方法中,我们通过输出密钥位串对一条消息的每个位元进行签名——无论它的值是什么。如果我们不要同时签名一条消息中 0 和 1 的位元,而是只签名 1 的位元,那又会如何呢?这么做能够将公钥和私钥的大小减半,因为我们可以完全丢掉整条 sk0 列。
现在我们只有单一列位串的密钥 sk1,...,sk256,对消息的每个位元 Mi = 1我们都会输出一个字符串 ski;对于消息的每个位元 Mi = 0我们都会输出......无(因为许多消息都会包含很多的 0 位元,这么做能缩减签名大小,这些 0 位元将不再带来任何成本)。
这种方法的明显缺陷是:它极度不安全,所以请不要这么做!
举例来说,假设有个攻击者观察到一条已经被签名的消息,消息开头是“1111...”。现在攻击者想要在不破坏签名的情况下,将消息编辑成“0000...”,只需要删掉这条签名中的几个组成部分即可!简言之,虽然要将 0 位元“翻转” 成 1 位元很困难,但反之要将 1 换成 0 就非常简单了。
现在有了个解决办法,而且它非常巧妙。
让我们接着瞧瞧。虽然无法避免攻击者将消息中的 1 改成 0 ,但我们能发现这些改动。只要将一个简单的“校验和(checksum)”附加到消息上,然后将消息和校验和一起签名。对于签名验证者来说,她必须验证整份签名的两个值,也需要确定收到的校验和是正确的。
我们使用的校验和非常小:它由简单的二进制整数组成,表示原始消息中的所有 0 位元数。
如果攻击者试图修改消息内容(或是校验和),使得部分 1 位元变成 0 位元,并没有手段可以阻止她。但是这种攻击会增加消息中的 0 位元数,这会使得校验和无效,验证者从而会拒绝这个签名。
当然,机智的攻击者可能还会试图混淆校验和(校验和也和消息一起被签名),增加校验和的整数值来匹配她篡改的位元数。然而最关键的是,因为校验和是二进制整数,如果要增加校验和的值,攻击者势必得将一些 0 位元转换成 1 位元。又因为校验和也被签过名,这种签名方法从源头阻止这种转换(将 0 换成 1),因此攻击者无法得逞。
(如果你继续记录下去,的确会增加被签名的“消息”的大小。在我们的例子中,一条 256 位的消息的校验和,需要额外的 8 位元及增加相应的签名成本。不过,如果这条消息包含许多 0 位元,这么做对于缩减签名大小仍然非常有效。)
Winternitz 方法:时间换取空间
上述的小窍门可以将公钥集合的大小减半,并以相同量级缩减签名的大小。这样做很棒,但称不上什么创新之举。密钥和签名仍然长达数千个位元。
如果我们能再加大力度缩减这些数字,那就更完美了。
我们采纳的最后优化手段,是由 Robert Winternitz 基于上述 Merkle 方法所提出的更进一步升级。在实际使用中,这个方法缩减了 4~8 倍的签名和公钥大小——代价是增加了签名和验证的时间。
Winternitz 的想法源自一项技术:时空权衡(time-space tradeoff)。这类解决方案使得空间需求减小,代价是增加计算时间(反之亦然)。以下几个思考能帮助我i们更好的理解 Winternitz 方法:
如果我们今天不要签名消息的每一位元(0 或 1),而是将它们视为更大的消息编码,那会怎么样呢?比如我们每 4 个位元签名一次,或是每八个位元(1 字节)签名一次?
在最初的 Lamport 方法中,我们有两列字符串作为签名密钥(和公钥)的一部分。一列是签署 0 位元用的,另一列签署 1 位元。
现在假设我们想直接对字节签名,而不是每个位元做签名。最直接的做法是将密钥(和公钥)列表从原来的两个增加到 256 个——合成一张大表,涵盖消息中每一个可能的字节。这样签名者就能从一张巨大的密钥表中选取合适的值,每次针对整个字节作签名。
不幸的是,这个主意烂透了。虽然它将签名的大小减少了 8 倍,付出代价却是将公钥和密钥大小增加 256倍 。如果这份巨大的密钥表能够用于多次签名那也就算了——但它不能。当密钥表发生重用时,这种“签整个字节版本”的 Lamport 方法也会遇到和原始 Lamport 方法一样的限制。
上面描述的种种问题,最终引出了 Winternitz 方法。
存储和分发 256 个随机密钥列成本非常高昂,如果我们只在需要签名的时候,以编程的手段生成需要的密钥呢?
Winternitz 的想法是,先替初始密钥生成一列随机种子 sk0 = (sk10, ..., sk2560) 。接着他提出用哈希函数 H() 对初始密钥的每个元素进行运算得出下一列密钥:sk1 = (sk11, ..., sk2561) = [ H(sk10), ..., H(sk2560) ],而不是直接随机生成其他列。以此类推,可以继续使用哈希函数求得下一列 sk2。
这么做的好处是我们只需要储存一列密钥,当需要签名时再使用哈希运算生成其他密钥即可。
但公钥部分怎么办呢?这就是 Winternitz 聪明的地方!
具体来说,Winternitz 提出可以对最后一列密钥再次进行哈希散列运算,生成一列公钥: pk 。(在实际应用中,我们只需要 255 列密钥,因为最后一列密钥我们可以直接视为公钥。)这个方法的优雅之处在于,任何一个给定的密钥值都能通过公钥检查;只要持续进行哈希散列运算,然后查看是否得到最终公钥即可。
完整的密钥生成过程如下:
注意:要对字节进行签名,我们只需要 255 个密钥列,而不是 256 个。因为最后一个密钥列等同于公钥。
要对消息的第一个字节进行签名,我们需要从合适的列中选取一个值。举例来说,如果是字节 “0” ,我们将从 sk0 中输出一个值作为我们的签名;如果是字节 “20” ,我们将从 sk20 中输出一个值作为我们的签名。对于最后一个字节 “255” ,我们虽然没有对应的密钥列,但也不要紧!我们可以输出空字串或是输出 pk 中的元素。
要注意,实际上我们不需要保存每一个密钥列。通过计算推演,我们能从初始列得到所需密钥。验证者只需要拿好公钥并直接进行适当次数(视消息字节数情况而定)的哈希运算,就能够验证计算结果是否等同于正确的公钥元素。
如同前面章节讨论的 Merkle 优化方法,到目前为止所描述的 Winternitz 方法也有着明显的漏洞。因为密钥列彼此关联(即 sk1 = H(sk0)),任何人看到字节“0”及其签名,都能够轻易的将消息的字节改为 “1”,然后更新签名匹配篡改(同理可类推)。事实上,无论什么字节攻击者都能够添加到消息中。如果没有检查机制,这会导致非常严重的伪造攻击隐患。
解决这个问题的办法正如前面所提到的,如果要防止攻击者修改签名,签名者必须计算原始消息字节的校验和,并对校验和也进行签名。校验和的设计使得攻击者添加任何字节,都会使校验和失效。这里不做过多讨论,详细请参阅这里。
毫无疑问,校验和正确与否是至关重要的;只要有任何一点纰漏,都会为你带来很不好的影响。如果在生产环境中部署这样的签名,会造成严重的后果。
拿下面这个有点粗糙的图说明,对一条 4 字节的消息使用 Winternitz 签名方法:
注意:示例中的消息由字节 byte 组成而不是 bit 。虽然我很确定正确地计算了校验和,但因为是手算的,有时候可能会有点小纰漏。
基于哈希函数的签名究竟有什么优势?
通篇讨论中,我们主要在讲基于哈希散列的签名如何运作的,而没有谈到为什么选择它。现在就让我们说明一下这种结构的签名特点是什么。
早期支持散列签名的观点认为,这种方法非常快速而且简单,因为这种方法只需要评估合适的哈希函数,并进行一些数据拷贝即可。纯粹从计算成本角度考虑,哈希散列签名绝对有能力和 ECDSA 、RSA 等一较高下,同时对于轻量级设备非常友好。当然,这种效率在很大程度上是以牺牲带宽为代价的。
不过(最近)关于哈希散列签名的兴起有着更复杂的原因:所有的公钥加密即将被破解。
更具体地说:即将问世的量子计算机,几乎会对所有目前使用的签名方法的安全性造成巨大的冲击,包含 RSA、ECDSA 等等。因为 Shor 算法(以及它的变体)让我们能在多项式时间内,解决离散对数和因式分解问题的方法,这使得绝大多数签名方法不再安全可靠。
大部分哈希散列签名不容易受到 Shor 算法影响。当然,我们不是说哈希散列签名能够完全抵抗量子计算攻击;对哈希运算最有效的量子攻击称作 Grover 算法,它会大大降低哈希运算的安全性 。不过这种程度的安全性影响,远小于 Shor 算法带来的影响(破解时间层级差别在平方到立方之间),因此可以简单通过增加哈希函数的运算内容和输出大小,来保障签名的安全性。像是 SHA3 系列哈希函数开发目的很明确,它能处理更大的输入,并有更好的能力对抗量子计算攻击。
至少从理论上来讲,哈希散列签名有趣之处在于它留给我们一线机会,抵御未来的量子计算攻击——或许就只能挣扎一下,谁知道呢。
未来展望
提醒一下,目前我们谈论的哈希散列签名都是“古董级”的,上述所有的哈希散列签名方法都发明于 1970 年或是早于 1980 年,这似乎不适用于今时今日。
我写完这篇文章初稿后,有许多人要我多讲讲这个领域近几年的发展。我无法给出详尽的列表,不过我能够稍微描述近几年出现的一些点子(感谢 Zooko 和 Claudio Orlandi)。
无状态签名。上述所有方法共通的一个局限在于,它们要求签名者在签名之间保持状态。对于一次性签名我们可以很直观的了解:我们必须避免重复使用任何密钥;而在 Merkle 多次签名中,我们必须记住正在使用的叶节点公钥,避免重复使用。更糟的是,Merkle 方法要求签名者先构建所有可能用上的密钥对,所以签名的数量是有限的。
在 1980 年,Oded Goldreich 指出有一种手段能够建立无需保持状态的签名 。想法如下:不预先生成所有密钥,而是生成一个简短的一次性公钥的“验证树”。每一个密钥的都可以在树的底层签署额外的一次性公钥,并以此类推。如果使用单个种子生成所有私钥,则表示完整的 Merkle 树不需要在密钥生成时存在,而可以在生成新密钥时按需求构建。每个签名都包含签名和公钥的“验证链”;从根节点开始,一直到叶节点真正用于签名的密钥对。
这项技术让我们能在非常“深”的 Merkle 树构建大量(指数级)的密钥。这允许我们构造非常多的一次性公钥,只需要我们随机地(或伪随机地)选择签名密钥,则发生密钥重用的可能性极低。当然,这是直觉想法。有关这个想法的更多优化和具体示例,请参考 Bernstein 等人的 SPHINCS 提案;SPHINCS-256 实例提供大小约为 41KB 的签名。
Picnic:后量子零知识签名(post-quantum zero-knowledge based signatures)。Picnic 是完全不同的想法,它基于一项全新的非交互式零知识证明系统(non-interactive zero-knowledge proof system) 技术,称为 ZKBoo。ZKBoo 是一种新的 ZK 证明系统,它基于一种称为“头脑中的 MPC”的技术,让证明者使用多方计算来进行自证 。这已经过于复杂,无法详细解释;但最终的结果是,人们可以继续使用哈希函数来验证复杂的语句。
简而言之,Picnic 和 ZK 证明系统提供除了哈希函数签名之外的第二种思考方向。这些签名的成本仍然很高 ——高达几百千字节,但是技术演进可以大大缩减签名的量级。
结语:老套的安全提醒
如果你稍微回想一下本文,我们费了番功夫描述一些哈希函数的安全特性。这可不只是简单展示而已!你可以看到,哈希散列签名的安全性,完全取决于我们所选择的哈希函数。
(再暗示一下,哈希散列签名不安全的地方,就是攻击者已经在设法攻破的哈希函数)
大多数讨论哈希散列签名的基础的文章,通常会在哈希函数的抗原像攻击上提出安全疑虑。以 Lamport 签名为例,我们能够很直观的理解。给定一个公钥,如果攻击者能够计算出哈希运算的输入,那她就能够轻易伪造一个有效签名。这种攻击使得签名不再安全。
不过,这是攻击者只看到了公钥,却还未看到任何一个有效签名的情况。在下面情况中,攻击者能够获得更多讯息。假设她现在有了公钥和一部分的密钥 pk01 = H(sk01) 和 sk01。如果攻击者能够找到公钥 pk01 的次原像,虽然她还不能对不同的消息进行签名伪造,但实际上她已经生成了一个新的签名。对于签名安全要求特别特别严格的场景(SUF-CMA),这就可以被视为一次有效的攻击了。因此 SUF-CMA 在抗 次原像攻击上有很高的标准。
最后一个问题出现在哈希散列签名方案的大多数实际应用中。你会注意到上面的描述假定我们正在签署 256 位消息。但在实际应用中,许多消息大于 256 位。因此,大多数人使用哈希函数 H() 前,会先散列消息 D = H(M),然后再对结果值 D 进行签名,而不是直接对消息签名。
不过这会导致最后这里要提到的攻击。因为这个做法的不可伪造能力,取决于该哈希函数的抗碰撞能力。攻击者能够找到两个不同的消息 M1 ≠ M2 ,而 H(M1) = H(M2),这就表示她找到了对两条不同消息都有效的签名。这导致了 EUF-CMA 安全性的微小缺陷。
链接: https://blog.cryptographyengineering.com/2018/04/07/hash-based-signatures-an-illustrated-primer/