PlonK中的Frozen Heart漏洞
在本博客系列的第一部分中,我们披露了破坏多个零知识证明系统实现健全性的关键漏洞。这类被我们称为Frozen Heart的漏洞,是由Fiat-Shamir转换的不安全实现引起的,允许恶意用户伪造随机语句的证明。在第二部分和第三部分中,我们演示了如何利用两个特定证明系统中的Frozen Heart漏洞:Girault的知识证明和Bulletproofs。在本部分中,我们将探讨PlonK中的Frozen Heart漏洞。
零知识证明与Fiat-Shamir转换
本文假设您对零知识证明有一定的了解。如果您想了解更多相关信息,可以参考Matt Green的入门文章等有用的博客文章和视频。要了解更多关于Fiat-Shamir转换的信息,请查看我写的详细解释博客文章。您还可以查看ZKDocs获取关于这两个主题的更多信息。
PlonK
PlonK证明系统是近年来开发的最新ZK-SNARK方案之一。SNARK本质上是一类专门的零知识证明系统,具有非常高效的证明大小和验证成本。有关SNARK的更多信息,我强烈推荐阅读Zcash的博客系列。
大多数SNARK需要可信设置仪式。在这个仪式中,一组参与方生成以特定方式结构化的一组值。重要的是,该组中没有人可以知道这组值的基础秘密(否则证明系统可能被破坏)。这个仪式并不理想,因为这些证明系统的用户必须信任某些组诚实地安全地生成这些值,否则整个证明系统就不安全。
较旧的SNARK系统需要为每个程序多次执行此仪式。PlonK的通用和可更新可信设置要好得多;只需要一个可信设置仪式就可以证明多个程序的语句。
在下一节中,我将逐步介绍完整的PlonK协议,以演示Frozen Heart漏洞的工作原理。然而,PlonK相当复杂,因此本节可能难以理解。如果您想了解更多关于PlonK工作原理的信息,我强烈推荐阅读Vitalik Buterin的博客文章并观看David Wong的YouTube系列。
PlonK中的Frozen Heart漏洞
与Bulletproofs一样,PlonK包含多个Fiat-Shamir转换。这些转换中的每一个,如果实现不正确,都可能包含Frozen Heart漏洞。我将专注于一个似乎是最常见的特定漏洞。这是影响Dusk Network的plonk、Iden3的SnarkJS和ConsenSys的gnark的漏洞。
与大多数SNARK一样,PlonK用于证明某个计算已正确执行。在一个称为算术化的过程中,计算以证明系统可以解释的格式表示:多项式和算术电路。
我们可以将计算的输入视为电路中的输入线。对于SNARK,有公共和私有输入。私有输入是电路中只有证明者看到的线。公共输入是剩余的线,由证明者和验证者共同看到。
其他公共值包括用于生成和验证证明的可信设置仪式生成的值。此外,由于证明者和验证者需要就待证明的计算或程序达成一致,程序的电路表示在双方之间公开共享。
在本系列的第一部分中,我们介绍了安全实现Fiat-Shamir转换的经验法则:Fiat-Shamir哈希计算必须包括来自零知识证明语句的所有公共值以及在证明中计算的所有公共值(即所有随机“承诺”值)。这意味着公共输入、来自可信设置仪式的值、程序的电路以及证明本身中计算的所有公共值都必须包含在PlonK的Fiat-Shamir转换中。影响Dusk Network、Iden3和ConsenSys代码库的Frozen Heart漏洞源于它们未能在这些计算中包含公共输入。让我们仔细看看。
PlonK协议概述
PlonK自首次发布以来经历了一些修订,因此许多PlonK实现并非基于Cryptology ePrint存档中的最新版本。在本节中,我将专注于2020年3月发布的这个版本,因为这(或类似版本)似乎是SnarkJS实现所基于的。注意:当协议实现不正确时,此漏洞仍然适用于论文的所有版本,但对于不同版本,如何利用此问题的确切细节会有所不同。
(以上段落于2022年6月8日更新。)
在深入协议细节之前,我将从高层次描述它。为了生成证明,证明者首先构建一系列对代表计算的各种多项式的承诺(具体来说,它们是对应于程序电路的约束)。在承诺这些值之后,证明者构建这些多项式的开放证明,验证者可以使用椭圆曲线配对进行验证。这种多项式承诺和开放是使用Kate多项式承诺方案完成的。该方案很复杂,但它是PlonK协议的核心部分,理解它是理解PlonK工作原理的关键。查看此博客文章以获取更多细节。
从证明者的角度来看,协议总共有五轮,每轮计算一个新的Fiat-Shamir挑战。以下是PlonK协议的所有公共和私有输入:
PlonK证明系统的公共和私有输入(来源)
公共预处理输入包含来自可信设置仪式的值(x[1]1, …, xn+2[1]1值,其中x[1]1 = g1x且g1是椭圆曲线群的生成器)和程序的电路表示;这些由证明者和验证者共享。剩余的值是程序电路的输入线。公共输入是公共线,证明者的输入既是公共输入又是证明者的私有线。有了这些,我们可以开始协议的第一轮,其中证明者计算盲线值。
PlonK协议中证明者的第1轮(来源)
证明者首先计算三个多项式(a、b和c),然后通过使用来自可信设置仪式的值在点x处评估它们。基础x值未知(假设可信设置仪式已正确执行),因此证明者正在生成对这些多项式的承诺而不泄露任何信息。证明者在第2轮中对置换多项式执行相同的操作:
PlonK协议中证明者的第2轮(来源)
我们不会详细描述这一轮,因为它特别复杂且与此漏洞无关。总之,这一轮负责强制执行“复制约束”,确保分配给所有线的值是一致的(即,它防止证明者在一个门的输出是另一个门的输入时分配不一致的值)。从那里,证明者在第3轮中计算商多项式。这个商多项式对于Frozen Heart漏洞至关重要,我们很快就会看到。
PlonK协议中证明者的第3轮(来源)
在第3轮之后,证明者然后使用Fiat-Shamir技术计算评估挑战zeta。然后使用zeta评估到目前为止构建的所有多项式。这个zeta挑战值是我们将在Frozen Heart漏洞中目标的值。如您所见,论文作者使用术语“转录本”,他们在论文前面解释为预处理输入、公共输入以及沿途生成的证明值。
(以上段落于2022年4月29日更新。)
PlonK协议中证明者的第4轮(来源)
最后,在第5轮中,证明者然后生成开放证明多项式并返回最终证明。
PlonK协议中证明者的第5轮(来源)
为了验证此证明,验证者执行以下12个步骤:
PlonK协议中的验证者(来源)
在高层次上,验证者首先验证证明格式良好(步骤1-4),计算一系列值(步骤5-11),然后通过使用单个椭圆曲线配对操作验证它们(步骤12)。此检查本质上验证了构建的多项式的可除性,并且只有在多项式按预期结构化时才会通过(当然,除非存在Frozen Heart漏洞)。
利用PlonK中的Frozen Heart漏洞
回想一下,所讨论的Frozen Heart漏洞源于未能在任何Fiat-Shamir挑战计算(重要的是zeta)中包含程序电路的公共输入。在高层次上,恶意证明者可以通过选择恶意的公共输入值(将取决于挑战值如zeta)来利用此漏洞,以欺骗验证者重建在步骤8中通过最终验证检查的t_bar。让我们看看细节。
回想一下,在第1轮中,证明者基于证明者的公共和私有线值生成三个多项式(a、b和c)。如果我们是试图伪造证明的恶意证明者,那么我们实际上没有这些线值(因为我们实际上没有进行任何计算)。因此,在第1轮中,我们将生成完全随机的多项式a’、b’和c’,然后输出它们的评估,[a’]1、[b’]1和[c’]1。
我们的随机a、b和c多项式将破坏在第2轮中计算的多项式,因为该多项式强制执行“复制约束”,这意味着线值是一致的。对于完全随机的多项式,基本上保证这些约束不会成立。幸运的是,我们可以通过将第2轮多项式归零并输出[0]1来完全跳过这些检查。注意:如果实现检查无穷远点,验证者可能会拒绝此证明。如果是这种情况,您可以在生成a’、b’和c’时巧妙一些,以便复制约束成立,但如何工作的细节有点棘手。由于SnarkJS实现在此归零多项式上不返回错误,我将继续使用此方法。
现在,对于第3轮,请记住我们没有正确计算多项式t所需的线值。相反,我们将生成一个随机多项式t’并输出[t’lo]1、[t’mid]1和[t’hi]1。
在第4轮中,我们将计算挑战zeta和所有评估,就像我们之前所做的那样,除了我们现在使用a’、b’、c’和t’。然后我们将使用这些评估以预期的方式构建r多项式,然后在zeta处评估r并输出所有评估。注意,计算的每个评估都与我们在前几轮中承诺的多项式一致。
在第5轮中,我们将按预期执行计算,但将a、b、c和t替换为a’、b’、c’和t’。这是协议的结束,但我们还没有完成。最后一步是计算将欺骗验证者的公共输入值。
欺骗验证者实际上归结为此开放证明多项式(我们可以忽略另一个开放证明多项式,因为我们在第2轮中将多项式z归零):
开放证明多项式的计算(来源)
由于我们在第4轮中的评估对应于在第5轮中使用的多项式,上面大括号内的多项式在zeta处评估时将评估为零;因此,它可被(X - zeta)整除,并且我们可以计算Wzeta。现在,我们需要确保我们在证明中计算的值以相同的方式由验证者重新计算。在步骤9-11中,验证者以与证明者计算它们相同的结构重建值。因此,所有这些值应该是有效的。
这给我们留下了在步骤8中要解决的最后一个问题,其中验证者计算t_bar。因为我们输出了t’_bar,一个在zeta处评估的随机多项式,而不是证明者预期计算的t_bar,验证者的t_bar将不匹配我们的输出,并且不会通过验证。然而,验证者使用公共输入来计算t_bar,这些公共输入未包含在任何挑战的Fiat-Shamir转换中(即zeta)。因此,我们可以调整我们的公共输入,以便它们强制验证者在步骤8中计算t’_bar。
为此,我们首先将我们在第4轮中计算的t’_bar代入步骤8中等式的左侧。然后,我们为此等式求解PI(zeta),一个公共输入乘以拉格朗日多项式的和。由于它是一个和,有多种组合可以工作。最简单的方法是将除第一个之外的所有公共输入归零,并求解导致我们求解的PI(zeta)值的线值。我们现在构建了我们的公共输入,将欺骗验证者计算t’_bar。这之所以有效,是因为这些公共输入值不用于计算zeta,因此我们在必须决定这些公共输入值之前就知道zeta。
现在,验证者将重建我们在第4轮中计算的相同t’_bar值,并且最终配对检查将通过——我们成功伪造了证明。
需要明确的是,此漏洞仅通过不正确的实现引入;这不是PlonK论文本身的漏洞。
(本节于2022年4月29日更新。)
Frozen Heart对PlonK的影响
让我们退后一步评估此漏洞的严重性。首先,让我们回想一下我们正在证明什么。使用PlonK,证明者向验证者证明他已正确执行特定(商定)程序,并且他给验证者的输出是正确的。在上一节中,我们通过使用程序电路的完全随机线值伪造了证明。验证者接受此计算证明为有效,即使证明者没有正确计算电路的任何线值(即,他实际上没有运行程序)。值得重申的是,本文仅关注一种Frozen Heart漏洞。如果其他Fiat-Shamir转换不正确,可能对此证明系统进行几种类似的攻击。
这是否在实践中可利用取决于验证者如何处理公共输入值。具体来说,只有当验证者接受来自证明者的任意公共输入(而不是事先商定,例如)时,这才可利用。如果我们查看SnarkJS存储库中的示例代码,我们可以看到公共输入(publicSignals)由证明者输出(使用fullProve函数)并被验证者盲目接受(此示例适用于Groth16,但PlonK API的工作方式相同)。通常,此漏洞的可利用性取决于实现。
使用SnarkJS的示例代码片段(来源)
您可以想象,在大多数应用程序中,这种类型的证明伪造非常严重。PlonK证明系统有效地零保证程序已正确执行。请记住,我们的示例使用了随机线值,但这不是要求。更复杂的攻击者可以选择更巧妙的线值(例如,通过选择有利于攻击者的程序输出)并仍然执行此相同攻击。
这是我们关于Frozen Heart漏洞系列的最后一篇文章。我希望到现在为止,我已经清楚地表明这些问题非常严重,并且不幸的是普遍存在。我们的希望是,这一系列文章能更好地提高对这些问题的认识。如果您还没有,请查看ZKDocs以获取更多指导,如果您希望添加更多内容,请与我们联系!
如果您喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News
页面内容 近期文章 使用Deptective调查您的依赖项 系好安全带,Buttercup,AIxCC的评分回合正在进行中! 使您的智能合约超越私钥风险成熟 Go解析器中意外的安全隐患 我们审查首批DKLs23库的收获 来自Silence Laboratories的23个库 © 2025 Trail of Bits。 使用Hugo和Mainroad主题生成。