Solidity 智能合約 - ecrecover 簽章檢驗
簡介
智能合約中的 ecrecover
函式,可以用來驗證錢包的的簽章。檢驗方可以在沒有簽章者私鑰的情況下,確認簽署者的身份。常用來作為鏈下授權,再由第三方上鏈使用或是單純的身份驗證。
函式
1 | function ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address) |
- hash: 是被簽章訊息的雜湊
- v: 簽章的 v 值
- r: 簽章的 r 值
- s: 簽章的 s 值
- 回傳: 簽章者地址
範例
例如下面的範例
1 | contract EcrecoverTest { |
結果會回傳簽署者錢包地址 0xDD980c315dFA75682F04381E98ea38BD2A151540
,表示他簽署了 0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53
這個訊息。
正常使用時,是用參數傳入而不是寫死,並檢驗簽章者,例如:
1 | function verify(address owner, bytes32 hash, uint8 v, bytes32 r, bytes32 s) external { |
不過直接把 hash 傳入不能看出他授權的內容,所以實際上會是一些原始資料,並在鏈上進行雜湊:
1 | function permit(address owner, uint value, uint8 v, bytes32 r, bytes32 s) external { |
EIP-191
除了上面的方式,在 EIP-191 定義了在使用簽章時,要加上一段前綴:
1 | "\x19Ethereum Signed Message:\n" + 訊息 bytes 長度 + 訊息內容 |
uint 是 32 bytes,所以上面的例子可以改為:
1 | bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", value)); |
這是因為原來簽章方式,有可能產生有效的交易簽章。
重放 (Replay)
意思是指同樣的簽章被重複使用,在沒有正確處理簽章的使用範圍和有效狀態,可能被利用來做為一種攻擊的手段。上面的例子可以看出沒有處理,所以可以重複使用簽章來動用資產,一般我們可以加上一個 nonce 來判斷此簽章是否已經使用過:
1 | contract EcrecoverTest { |
nonce 也可以是在鏈上每個使用者有的一個遞增數值。除了 nonce 之外,一般還會加上一個有效時間。不過即使加上了 nonce 和有效時間,仍然存在簽章重複使用的可能,在不同的合約如果使用和上面相同驗證方式,相同的簽章會可以重複使用,這時候可以加上合約地址限制使用的範圍。在做了上述的限制和處理後,最後還是有一個情況可能發生重放,就是在不同的鏈假設剛好有相同的合約地址,雖然機率很低。最後我們可以加上鏈 ID 和授權對象做限制得到以下範例:
1 | function permit(address owner, address spender, uint value, uint nonce, uint deadline, uint8 v, bytes32 r, bytes32 s) external { |
雜湊簽章訊息
由於前綴需要輸入訊息長度,要去計算動態長度的訊息,或許處理會有點麻煩,所以也有一個方式是簽署的訊息是先進行過一次雜湊:
1 | bytes32 messageHash = abi.encodePacked(block.chainid, address(this), spender, value, nonce, deadline); |
這後後面就可以固定寫 32。
簽章擷取
簽章格式除了已經擷取好的 vrs 之外,有時也會有沒有擷取的
1 | 0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a356320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f81b |
在鏈上可以用以下方式擷取出 vrs
1 | function splitSignature(bytes memory signature) internal pure returns (uint8 v, bytes32 r, bytes32 s) { |
不過並不建議在鏈上擷取,會造成 Gas 的上升,應該在鏈下先進行擷取。
取消簽章
由於簽章一但簽署送出後,在失效之前持有者都有權限執行相關的操作,可能會造成簽章者的損失。例如常見的使用簽章來掛單報價,假設一個 NFT 持有者簽署了以 1 ETH 賣出,有效一個月。後來預期價格上漲,持有者在網頁上取消掛單,但如果鏈上沒有取消簽章的機制,一個月內知道簽章的人仍能以 1 ETH 買入,即便市價漲到 10 ETH。之前 OpenSea 就曾發生相關事件。
為了掛單時節省成本和提升效率,使用鏈下簽章的報價機制,反而造成上述的問題。要解決這個問題,就必須使用鏈上取消簽章的機制,將上面的範例簡單改為讓 nonce 無效:
1 | contract EcrecoverTest { |
使用套件
我們也可以使用 OpenZeppelin MessageHashUtils 套件 來處理簽署訊息的前綴。我們可以利用下面的多載方法自動加上 EIP-191 前綴:
1 | function toEthSignedMessageHash(bytes32 messageHash) internal pure returns (bytes32 digest); |
固定長度雜湊簽章訊息
1 | import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; |
動態長度的內容:
1 | // uint value = ...; |
延伸閱讀
Solidity 智能合約 - 簽名延展性 (Signature Malleability)
使用 Web3.js 簽章與檢驗
使用 Ethers.js 簽章與檢驗
Solidity 智能合約 - EIP-712 類型結構化資料雜湊和簽名