作者: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;
}
實驗結果如下圖所示:
綜上,可以發現如果不使用本接口對公共信號值進行取值範圍的有效性校驗,那么可能存在雙花攻擊風險。
鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播信息之目的,不構成任何投資建議,如有侵權行為,請第一時間聯絡我們修改或刪除,多謝。