作者:Beosin安全研究专家Saya & Bryce
1. 什么是零知识证明
零知识证明(Zero-Knowledge Proof,后文简写ZKP)是一种密码学概念,它可以用来证明某个声明的真实性,而无需透露有关该声明的任何具体信息。在零知识证明中,证明者可以向验证者证明某个陈述是正确的,而验证者只会得到一个结果:要么接受该陈述的真实性,要么拒绝它,而无需了解证明的具体细节。
这个概念可以用一个简单的例子来解释。假设有两个人,一个是证明者和一个是验证者。证明者想向验证者证明自己知道一个秘密的密码,而不泄露密码本身。在传统的方式中,证明者可能会告诉验证者密码是什么,但在零知识证明中,证明者可以使用特殊的协议来向验证者证明他知道密码的正确性,而不泄露密码本身。
目前常见的零知识证明系统算法包括zk-SNARKs、zk-STARKs、BulletProofs等。
2. ZKP在区块链中的应用
在区块链技术中,ZKP有多种应用,例如提升隐私、改善可扩展性和安全性等。以下是ZKP在区块链中的一些关键应用:
1 隐私保护:
区块链是公共的,这意味着任何人都可以查看链上的所有交易。然而,有时候,用户可能希望保持他们的交易信息保密。ZKP允许用户证明他们拥有足够的资金进行交易,同时不必公开他们的资金总额。这大大增强了用户的隐私保护。例如,Zcash是一种使用零知识证明技术的加密货币,它允许用户隐藏交易的发送者、接收者和金额。
2 计算压缩与区块链扩容:
区块链的可扩展性是一个挑战,尤其是在大规模应用中。ZKP可以用于减轻节点的负担,提高整个系统的可扩展性。通过使用ZKP验证交易的有效性,节点无需查看完整的交易历史记录,从而减少了存储和处理的负担,目前应用最广泛的ZK Rollup是一种扩展性解决方案,旨在提高以太坊及其他区块链网络的吞吐量和效率。它结合了Rollup和ZKP技术的优势,提供了高性能的去中心化应用程序(DApps)扩展方案。在传统的以太坊网络上,每个交易都需要被验证和记录在区块链上,这导致了交易处理的延迟和高成本。而ZK Rollup通过将大量的交易批量处理并压缩为单个区块,ZKP则用于证明批量交易的有效性,从而确保交易的正确性和安全性。
3 身份验证:
零知识证明可以用于验证用户的身份而无需透露敏感的个人信息。例如,一个人可以使用零知识证明向网络证明他们满足某个特定的年龄要求或拥有某种特定的证书,而无需揭示他们的确切年龄或其他身份信息。
4 去中心化存储:
服务器可以向用户证明他们的数据被妥善保存,并且不泄露数据的任何内容。
总的来说,区块链的零知识证明在隐私保护、计算压缩与扩容、身份验证、去中心化存储等方面有着广泛的应用。它为区块链技术提供了更多的功能和选择,推动了区块链在不同领域的发展和应用。
3. ZKP应用中的双花攻击
zk-SNARK(Zero-Knowledge Succinct Non-Interactive Argument of Knowledge)是一种基于零知识证明的技术,可以在不泄露真实信息的情况下证明某个声明的真实性。它是一种非常高效的零知识证明技术,可以在非常短的时间内生成和验证证明,同时保护隐私和安全性,所以应用非常广泛。但是,伴随着应用的扩展,其安全性也越来越受到关注。我们在不久前就曾发现了其通用漏洞:ZKP项目中如果未正确校验verify函数中参数input的取值范围,攻击者可以伪造多个input通过校验,造成双花攻击。这种攻击影响范围非常广,涉及多个zk-SNARK算法包括:groth16、plonk等,并且solidity、js等多种开发语言均存在该漏洞。该漏洞最开始由poma在零知识证明项目Semaphore上首次发现,并给出了两笔成功实施的交易示例,具体如下图所示:
https://github.com/semaphore-protocol/semaphore/issues/16
该漏洞具体的攻击原理是,如果要在以太坊中生成和验证zk-SNARK证明,需要使用 F_p-arithmetic 有限域椭圆曲线电路,其中p值用于确定椭圆曲线有限域的范围,所以电路的input取值范围为[0,1,…,p-1]。不同的曲线拥有不同的p值:
EIP-196 中定义的BN254 曲线(也称为 ALT_BN128 曲线):
p = 21888242871839275222246405745257275088548364400416034343698204186575808495617
circom2 引入了两个新的素数,即BLS12-381曲线:
p = 52435875175126190479447740508185965837690552500527637822603658699938581184513
以及plonk2:
18446744069414584321
随后Semaphore方确认并修复了该漏洞,ZoKrates、snarkjs等zk库也同步进行了紧急修复,但Beosin安全研究员发现该问题目前并未存在一个统一的解决方案,例如Semephore协议将约束写到pairing库中并未在外层业务逻辑中显式校验数据的有效范围;而circom生成的合约代码以及Tornado.Cash则在verify函数中显式地校验SNARK_SCALAR_FIELD,这种混乱不统一的解决方式可能会对很多新的zk DApp项目方造成困扰并出现安全隐患,因此我们希望能够使用一种标准化的方式来解决这个问题。
4. ERC-1922中的双花攻击
目前以太坊具有一个zk相关的标准EIP-1922,该标准介绍了用于验证zk-SNARK的Verify合约标准接口,具体代码如下:
pragma solidity ^0.5.6;
/// @title EIP-XXXX zk-SNARK Verifier Standard
/// @dev See https://github.com/EYBlockchain/zksnark-verifier-standard
/// Note: the ERC-165 identifier for this interface is 0xXXXXXXXX.
/// ⚠️ TODO: Calculate interface identifier
interface ERC1922 /* is ERC165 */ {
/// @notice Checks the arguments of Proof, through elliptic curve
/// pairing functions.
/// @dev
/// MUST return `true` if Proof passes all checks (i.e. the Proof is
/// valid).
/// MUST return `false` if the Proof does not pass all checks (i.e. if the
/// Proof is invalid).
/// @param proof A zk-SNARK.
/// @param inputs Public inputs which accompany Proof.
/// @param verificationKeyId A unique identifier (known to this verifier
/// contract) for the Verification Key to which Proof corresponds.
/// @return result The result of the verification calculation. True
/// if Proof is valid; false otherwise.
function verify(uint256[] calldata proof, uint256[] calldata inputs, bytes32 verificationKeyId) external returns (bool result);
}
其中,零知识证明proof、inputs变量类型都为uint256[],该变量类型是目前ZKP算法中椭圆曲线运算最常用的,但是该接口中也未增加对应的安全防护,因此同样存在双花攻击的巨大安全隐患。
5. ERC-7520解决方案
Beosin根据上述问题,提出了EIP-7520防范这种安全风险,具体为以太坊生态中所有使用了zk技术的DApp项目方在compliant verifier contract 中,都必须实现该接口从而使用规范统一而又安全的方式对所有input进行有效范围校验,具体接口如下:
pragma solidity ^0.5.6;
/// @title EIP-XXXX zk-SNARK public inputs Verifier Standard
/// Note: the ERC-165 identifier for this interface is 0xXXXXXXXX.
/// ⚠️ TODO: Calculate interface identifier
interface EIP7520 /* is ERC165 & ERC1922 */ {
/// @notice Checks the arguments of Inputs are within the scalar field
/// @dev
/// MUST return `true` if Inputs passes range check (i.e. the Inputs are
/// valid).
/// MUST return `false` if the Inputs does not pass range check (i.e. if the
/// Inputs are invalid).
/// @param inputs Public inputs which accompany Proof.
/// @param p Public input which accompany the curve.
function verifyPublicInput(uint256[] inputs,uint256 p) external returns (bool result);
}
verifyPublicInput函数是这个标准的核心,涉及到的参数具体含义如下:
inputs :定义为 uint256[] 类型,代表了ZKP项目中verify函数涉及到的公共信号参数
p :定义为uint256 类型,该值对应算法中使用的椭圆曲线的p值
下面将对比实现与未实现EIP-7520接口的两种情况下,针对该该攻击的不同表现,以向各位项目方表明风险:
1 假设我们在不调用本eip接口verifyPublicInput的情况下,直接使用verify合约代码进行证明验证,具体代码如下:
function verify(uint[] memory input, Proof memory proof) internal view returns (uint) { VerifyingKey memory vk = verifyingKey(); require(input.length + 1 == vk.IC.length,"verifier-bad-input"); // Compute the linear combination vk_x Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0); for (uint i = 0; i < input.length; i++) vk_x = Pairing.addition(vk_x, Pairing.scalar_mul(vk.IC[i + 1], input[i])); vk_x = Pairing.addition(vk_x, vk.IC[0]); if (!Pairing.pairingProd4( Pairing.negate(proof.A), proof.B, vk.alfa1, vk.beta2, vk_x, vk.gamma2, proof.C, vk.delta2 )) return 1; return 0;}
原始的证明验证通过的实验结果截图:
同时,可以伪造如下4个证明同样可以通过验证,造成双花攻击:
使用其中一个伪造的证明,验证结果如下图所示:
2 如果调用了本eip中的verifyPublicInput接口,上述伪造的证明则会验证失败,部分合约代码如下,其余详细部分可以参考Reference Implementation:
function verifyx(uint[] memory inputs, Proof memory proof, bytes32 verificationKeyId,uint256 p) public returns (uint){
require(verifyPublicInput(inputs,p),"verifier-over-snark-scalar-field");
require(verify(inputs,proof,verificationKeyId),"verify fail");
return true;
}
function verifyPublicInput(uint256[] inputs,uint256 p) internal view returns (bool) {
for (uint i = 0; i < input.length; i++) {
require(input < p,"verifier-gte-snark-scalar-field");
}
return true;
}
实验结果如下图所示:
综上,可以发现如果不使用本接口对公共信号值进行取值范围的有效性校验,那么可能存在双花攻击风险。