作者: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/// 註: the ERC-165 identifier for this interface is 0xXXXXXXXXXXX. /// ⚠️ TODO: Calculate interface identifierinterface ERC1922 /* is ERC165 */ { /// @notice Checks the arguments of Proof, through elliptic curve /// pairing functions. /// @dev /// MUST return `true ///`true if Proof passes all checks (ie the Proof is /// valid). /// MUST return `false` if the Proof does not pass all checks (ie 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 identifierinterface EIP7520 / is /// @notice Checks the arguments of Inputs are within the scalar field /// @dev /// MUST return `true` if Inputs passes range check (ie the Inputs are /// valid). /// MUST return ` false` if the Inputs does not pass range check (ie if the /// Inputs are invalid). /// @param inputs Public inputs which accompany Proof. /// @param p Public input which accompany the curve. function verifyPublicput(unction 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(vkICk.[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,proverify)(inification,verar-field ),”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 ;}
實驗結果如下圖所示:
綜上,可以發現如果不使用本介面對公共訊號值進行取值範圍的有效性校驗,那麼可能存在雙花攻擊風險。