文章探讨了BLS签名聚合中的一个关键漏洞,即共轭签名问题,该问题可能导致验证错误,并影响区块链协议中的不可抵赖性和消息绑定。通过一个具体案例分析了签名聚合被误用作批量验证时的潜在风险。
“永远不要自己实现加密算法”是普遍接受的经验法则,使用成熟、可信的实现和协议总是更好的。即使将众所周知的密码学原语用于其看似预期的目的,也出奇地容易引入细微错误和意想不到的边缘情况。
在这篇博客文章中,我们将探讨一个在生产区块链基础设施中出现的此类案例。我们将特别关注 BLS 签名及其聚合中消息绑定的歧义可能导致与协议设计者通常预期不同的行为的情况。通过以下讨论,我们将演示如何思考将密码学原语组合成更高级协议。
BLS 签名 提供了一种简单高效的方法来构建一个方案,在该方案中,多方都通过密码学方式就某个事实达成一致。给定一个私钥 sksksk 和一个适当哈希到曲线点 H(m)H(m)H(m) 的消息 mmm,BLS 签名获得为 sig=sk⋅H(m)sig = sk·H(m)sig=sk⋅H(m)。与 sksksk 相关的公钥是 pk=sk⋅gpk = sk·gpk=sk⋅g,其中 ggg 是群生成元。签名验证需要一个特殊的双线性映射,称为配对(用 eee 表示),由此可得出:e(a⋅P,Q)=e(P,a⋅Q)e(a·P, Q) = e(P, a·Q)e(a⋅P,Q)=e(P,a⋅Q)。为简单起见,我们使用泛型群表示法,不区分配对的源群 G1G_1G1 和 G2G_2G2,它们由配对友好椭圆曲线的选择决定。给定一个消息签名 sigsigsig,以及声称签名者的公钥 pkpkpk,验证签名只需检查以下等式的有效性:e(pk,H(m))=e(g,sig)e(pk, H(m)) = e(g, sig)e(pk,H(m))=e(g,sig)。我们可以通过展开定义来看到这个等式必须成立;我们有 pk=sk⋅gpk = sk·gpk=sk⋅g,所以 e(pk,H(m))=e(sk⋅g,H(m))e(pk, H(m)) = e(sk·g, H(m))e(pk,H(m))=e(sk⋅g,H(m))。使用前面提到的双线性映射 eee 的性质,我们现在可以“移动” sksksk,得到:e(g,sk⋅H(m))=e(g,sig)e(g, sk·H(m)) = e(g, sig)e(g,sk⋅H(m))=e(g,sig)。换句话说,满足此等式的签名只能由知道公钥背后秘密的一方生成。一组签名 sig1,sig2,...,signsig_1, sig_2,...,sig_nsig1,sig2,...,sign 可以通过群加法聚合为 sigagg=sig1+sig2+...+signsig_{agg} = sig_1 + sig_2 + ... + sig_nsigagg=sig1+sig2+...+sign。相应的公钥集也以相同的方式聚合为 pkagg=pk1+pk2+…+pknpk_{agg} = pk_1 + pk_2 + … + pk_npkagg=pk1+pk2+…+pkn。令人高兴的是,针对同一消息的聚合验证与单个签名的验证方式完全相同,即 e(pkagg,H(m))=e(g,sigagg)e(pk_{agg}, H(m)) = e(g, sig_{agg})e(pkagg,H(m))=e(g,sigagg),只需要两次配对操作。
理解 BLS 签名聚合的最初用例很重要,即“聚合签名对于减小证书链的大小(通过聚合链中的所有签名)和减小消息大小很有用”[ 1]。将此目标与其他涉及多个签名的方案进行比较,在签名验证方面产生了明显的对比。例如,批量验证的目的是确定一批中所有签名是否有效(不一定单独验证所有签名)。另一方面,筛选提供了较弱的保证:它只检查秘密密钥持有者是否验证了消息,但无法辨别单个签名在单独检查时是否正确验证。
尽管有这种区别,批量验证和聚合经常被混淆。这种混淆最初在 [ 2] 中指出,在同一篇论文中,作者表明 BLS 聚合签名方案满足筛选但不满足批量验证:两个单独无法验证的无效签名,但却是共轭的(即它们在聚合期间相互抵消)可以通过验证。获得共轭签名的一种方法是取两个签名 sk⋅H(m1)sk·H(m_1)sk⋅H(m1) 和 sk⋅H(m2)sk·H(m_2)sk⋅H(m2),它们分别对消息 m1m_1m1 和 m2m_2m2 有效,并用一个“错误项” A≠0∈GA ≠ 0 ∈ GA=0∈G 破坏它们,得到:sig1=sk⋅H(m1)+Asig_1 = sk·H(m_1) + Asig1=sk⋅H(m1)+A 和 sig2=sk⋅H(m2)−Asig_2 = sk·H(m_2) - Asig2=sk⋅H(m2)−A。现在,sig1sig_1sig1 和 sig2sig_2sig2 不会单独验证其各自的消息,但它们的聚合会通过验证,因为破坏项相互抵消:agg=(sk⋅H(m1)+A)+(sk⋅H(m2)−A)=sk⋅H(m1)+sk⋅H(m2)agg = (sk·H(m_1) + A) + (sk·H(m_2) - A) = sk·H(m_1) + sk·H(m_2)agg=(sk⋅H(m1)+A)+(sk⋅H(m2)−A)=sk⋅H(m1)+sk⋅H(m2)。
另一种形式的共轭签名,最早出现在 [ 3] 中,可以通过生成两个私钥 sk1=−sk2sk_1 = -sk_2sk1=−sk2 来获得;这些私钥对任何消息的签名在聚合时将产生无穷远点(即它们有效地相互抵消)。由于聚合只是曲线中的群加法,并且无穷远点充当操作的中性元素(群运算的“零元素”),这两个无效签名只是“消失”了。同样适用于两个公钥,这就是为什么在聚合公钥下的验证会成功。
这些弱点在 [ 4] 中得到了进一步的阐述,该论文也是第一个明确将它们与不可否认性联系起来的,即防止签名者以后否认他们签署了特定消息的属性。不可否认性只有在方案支持消息绑定的情况下才可能实现,消息绑定确保签名与特定消息不可分割地绑定。由于无效的共轭签名在聚合后“丢失”了,通过 BLS 聚合获取消息绑定是困难的,因此所有通过聚合验证识别单个无效签名的尝试都注定失败。
我们至少在一个 EVM 兼容区块链中发现了一次这样的尝试,我们将其称为协议 P,它设计了一种机制来识别单个无效签名,同时仍然享受 BLS 聚合的效率优势。为此,协议 P 构建了一个签名聚合树;完全聚合的签名位于根节点,每个子节点是部分签名集的聚合,其中叶子是对相同消息贡献给方案的单个签名。如果聚合签名无效,即因为其中一个签名是针对不同消息的,则递归遍历该树以确定哪个签名无效。关键是,这种遍历只在聚合签名无效时发生;如果它有效,则所有单个签名都被认为是有效的。然而,这正在将 BLS 签名聚合用于批量验证目的,由于存在共轭签名的可能性,这是不合适的。
如前所述,无法确定任何中间签名是否与其逆点相互抵消,因为验证假设签名是针对同一消息的。因此,有可能在为验证消息 msgmsgmsg 而实例化的签名集合中,通过利用共轭私钥,夹带一个针对消息 msg’!=msgmsg’ != msgmsg’!=msg 的签名,并且仍然通过验证。需要强调的是,这些共轭密钥在协议中是完全有效的,因为它们符合所有权证明的要求,前提是它们由两个串通的验证者控制。
更具体地说,假设聚合树是由 [sig1,sig2,sig3][sig_1, sig_2, sig_3][sig1,sig2,sig3] 创建的,其中 sig1sig_1sig1 和 sig2sig_2sig2 签署的是 msg′msg'msg′,而 sig3sig_3sig3 签署的是 msgmsgmsg(诚实签名)。前两个签名满足 sig1+sig2=infsig_1 + sig_2 = infsig1+sig2=inf。这将导致一个包含五个节点的树,如下所示:

我们可以将签名合并操作展开为:
Root.sig=A1.sig+L1.sigRoot.sig = A_1.sig + L_1.sigRoot.sig=A1.sig+L1.sig
=(sig2+sig3)+sig1 = (sig_2 + sig_3) + sig_1=(sig2+sig3)+sig1
=(sk2⋅H(msg′)+sk3⋅H(msg))+sk1⋅H(msg′) = (sk_2·H(msg') + sk_3·H(msg)) + sk_1·H(msg')=(sk2⋅H(msg′)+sk3⋅H(msg))+sk1⋅H(msg′)
=sk1⋅H(msg′)+sk2⋅H(msg′)+sk3⋅H(msg) = sk_1·H(msg') + sk_2·H(msg') + sk_3·H(msg)=sk1⋅H(msg′)+sk2⋅H(msg′)+sk3⋅H(msg)
=(sk1+sk2)⋅H(msg′)+sk3⋅H(msg) = (sk_1 + sk_2)·H(msg') + sk_3·H(msg)=(sk1+sk2)⋅H(msg′)+sk3⋅H(msg)
=0⋅H(msg′)+sk3⋅H(msg) = 0·H(msg') + sk_3·H(msg)=0⋅H(msg′)+sk3⋅H(msg)
=sk3⋅H(msg) = sk_3·H(msg)=sk3⋅H(msg)
=sig3 = sig_3=sig3
请注意,没有一个单独的节点是无穷远点。如前所述,“聚合树”的验证从根节点开始,因此 Root.sigRoot.sigRoot.sig 是第一个被验证的;公钥聚合展开为:
Root.key=L1.pk1+L2.pk2+L3.pk3Root.key = L_1.pk_1 + L_2.pk_2 + L_3.pk_3Root.key=L1.pk1+L2.pk2+L3.pk3
=sk1⋅g+sk2⋅g+sk3⋅g = sk_1·g + sk_2·g + sk_3·g=sk1⋅g+sk2⋅g+sk3⋅g
=(sk1+sk2+sk3)⋅g = (sk_1 + sk_2 + sk_3)·g=(sk1+sk2+sk3)⋅g
=(0+sk3)⋅g = (0 + sk_3)·g=(0+sk3)⋅g
=sk3⋅g = sk_3·g=sk3⋅g
=pk3 = pk_3=pk3
聚合验证简化为:
e(Root.key,H(msg))=e(pk3,H(msg))e(Root.key, H(msg)) = e(pk_3, H(msg))e(Root.key,H(msg))=e(pk3,H(msg))
=e(sk3⋅g,H(msg)) = e(sk_3·g, H(msg))=e(sk3⋅g,H(msg))
=e(g,sk3⋅H(msg)) = e(g, sk_3·H(msg))=e(g,sk3⋅H(msg))
=e(g,sig3) = e(g, sig_3) =e(g,sig3)
=e(g,Root.sig) = e(g, Root.sig)=e(g,Root.sig)
由于根节点的签名验证通过,无需进一步检查,验证过程报告所有签名都对同一消息有效。
该协议使用签名集合来处理区块投票,其中签名验证被延迟到达到所需的法定人数,因为投票被乐观地假定为针对同一区块。真正的批量验证或所有签名的验证在性能方面会很昂贵,因此这种优化导致聚合被误认为是验证。最终结果是,尽管系统部署了机制来检测投票是否针对冲突区块,但法定人数最终仍然计算了针对不同区块的签名。更准确地说,假设 A*、B*、C 和 D 是共识中的参与者,A* 和 B* 串通。当轮到 C 提议时,A* 和 B* 向 C 发送对区块 0x00 的投票,但他们签署的是区块 0xFF。D 向 C 发送对区块 0x00 的有效投票,即他们签署了区块 0x00。尽管 A* 和 B* 的两个签名都是针对区块 0xFF 的,但区块 0x00 仍然达到了法定人数。
虽然可以争辩说,两个串通的验证者实际上投票给了正确的区块 0x00,因为他们的无效签名仍然被计算在内,尽管它们相互抵消,但这种行为从根本上违反了消息绑定属性:签名通过密码学方式证明了被签署对象,而不是其解释或意图。在这种情况下,被签署的对象是对与达到法定人数的区块不同的区块的投票。这使得问责制和不可否认性难以实现。
所探讨的案例强调了实用密码学中的一个重要主题:原语的操作能力必须与其在某个更高级协议中的预期作用相匹配。
在 BLS 签名的情况下,共轭签名提供的反例表明,仅依赖聚合验证,尽管从性能角度来看很有吸引力,但通过消息绑定建立问责制和不可否认性变得困难,并且需要通过协议级约束来强制执行。
虽然外部控制可以解决这些特定缺陷,但依赖外部应用程序逻辑来强制执行密码学保证,会削弱通过协议设计模块化和可组合性来验证整个系统正确性的益处。
- 原文链接: certora.com/blog/bls-sig...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!