作者:大酱,慢雾安全团队

前言

之前的文章我们盘点了 ZKP 主流实现方案技术特点,我们提到了一些 ZKP 算法存在延展性风险,本篇我们继续从实战角度为大家展示它的攻击原理及防御方法。

漏洞简介

ZKP 的延展性攻击,是指给定敌手一个合法证明,敌手在不知道见证的条件下自己生成新的合法证明。

并不是所有的证明系统都存在延展性攻击风险,实际上这个问题目前主要存在于 Groth16 证明系统中。那么问题来了,目前已经有那么多的证明系统,为什么还要坚持使用 Groth16 呢?其实也没得选择,Groth16 生成的证明体积实在是小到极致,验证也极其之快,在计算代价十分昂贵的区块链上,使用 Groth16 似乎是最理想的选择。

延展性风险会带来哪些风险呢?我们可以想象一下如果有这么一个存款系统,它使用用户提交的 ZKP 证明来验证其身份,验证成功则可以提款。由于这个系统的验证过程是公开的,任何人都可以获取到这个证明,如果我们是用证明值本身做为取款登记,如果这个证明被获取到并进行变换,那就可被利用来多次提款。漏洞的利用需要看具体的场景,但我们可以看到延展性首先会带来双花的风险。

数学原理

理解攻击原理我们首先需要从理解算法开始,这需要有一定的密码学基础,感兴趣的同学可以自行寻找 Groth16 算法资料。这里我们直击漏洞根源:验证函数。

我们来看一下这个验证函数公式:

如果没有对这些字母一一从头介绍,我想很难看懂它在表达什么,但我觉得过多的介绍是不需要了,记好公式左边的 “A 乘 B” 就这么简单,然后我们开始施展数学魔法。咒语如下:

根据群的结合律,那么上面公式中:

这只是其中一种比较简单的构造方法,另外还有一种构造方法 [1],这里不做阐述,因为我们已经得到想要的东西了。

工程实现

有了上面的公式,就可以在工程上实现 Groth16 证明的延展。选择一个要伪造证明的对象,获取它的 proof,例如:

{  pi_a: [    '17566212007750634279332191898019870443899908963707812937725971557556988121113',    '13653824972036797689593667463260040326059024360787769597142078414930263663703',    '1'  ],  pi_b: [    [      '14906111038352923510344648516413952434168552622848767570599399834157918236589',      '15289017543994496306320102143103349779456992442925111629326024552687168229256'    ],    [      '18841235948006283310515755114762069779103481848435391875780416574913227842443',      '6835281862874020275059416795628130939104366467185014410026268177455413514889'    ],    [ '1', '0' ]  ],  pi_c: [    '21641806348662631815866837255154640732047306895903168385641666607914783128458',    '2082587994352117459125871298218148663854896572836176277773049196516560449682',    '1'  ],  protocol: 'groth16',  curve: 'bn128'}

我们看这样的一个 proof,pi_a, pi_b, pi_c 就是上面公式里描述的 A, B, C,这个证明使用的是 BN128 曲线,然后我们需要去寻找支持 BN128 曲线开发库。这里我们选择 ffjavascript,它是一个基于 Javascript 的有限域库,支持 BN128, BLS12381 曲线。

首先,我们任意构造一个域上的元素及它的逆元:

const X = F.e("123456");const invX = F.inv(X);

然后,分别相乘,核心代码如下:

const A = curve.G1.fromObject(proof.pi_a);const B = curve.G2.fromObect(proof.pi_b);new_pi_a = curve.G1.timesScalar(A, X);  //A'=x*Anew_pi_b = curve.G2.timesScalar(B, invX);  //B'=x^{-1}*B

最后,用 new_pi_a, new_pi_b 去替换原 proof,得到新的 proof:

{  pi_a: [    '6515337738552169645617263495374285821912767490069335826295120714428977813009',    '10671874016637483602721966808912960491553808325993800847672325376634242358838',    '1'  ],  pi_b: [    [      '20523135654483520737281403147507843211011765855706506084021355785019229409285',      '4032527486736971273144842057682931136787425732029780739716144011227563817375'    ],    [      '9389285843105460816015935120908213706233585149018458753845466963847282799614',      '7207137211649923819130654483456848273137049778520784010268635580504303221849'    ],    [ '1', '0' ]  ],  pi_c: [    '21641806348662631815866837255154640732047306895903168385641666607914783128458',    '2082587994352117459125871298218148663854896572836176277773049196516560449682',    '1'  ],  protocol: 'groth16',  curve: 'bn128'}

至此,我们已经成功构造出了新的证明,把 proof 放到验证函数中去验证可以发现它能通过验证。

防范

如何防范 Groth16 延展性攻击呢?可以参考这四种方法:

  • 对 proof 进行签名,验证者在验证 proof 同时也验证签名是否正确;
  • 参考 TornadoCash 在电路的公开输入中增加 nullifier 值,nullifier 确保证明对应的公开输入只能被使用一次;
  • 在电路中将证明者的身份信息(如以太坊的 msg.sender)加入到公开输入中,然后验证者就可以对提交证明的人进行身份验证;
  • 使用其它的证明系统,参考我们之前的文章

总结

Groth16 存在延展性攻击风险,可通过简单的计算伪造出新的证明,实际运用中需要注意防止出现双花攻击。

参考链接:

[1]. https://medium.com/ppio/how-to-generate-a-groth16-proof-for-forgery-9f857b0dcafd

免责声明:作为区块链信息平台,本站所发布文章仅代表作者及嘉宾个人观点,与 Web3Caff 立场无关。文章内的信息仅供参考,均不构成任何投资建议及要约,并请您遵守所在国家或地区的相关法律法规。