使用 Web3.js 簽章與檢驗
之前在 Solidity 智能合約 - ecrecover 簽章檢驗這篇文章,說明了智能合約如何檢驗簽章,這篇文章將說明如何使用 Web3.js 來進行簽章與檢驗。本篇文章使用 Web3.js 4.1.1 版本。
簽章
Web3.js 提供幾種鏈下簽章的函式,本篇文章介紹下面三種:
- web3.eth.sign
- web3.eth.accounts.sign
- web3.personal.sign
關於 EIP-712 相關的方法,參考使用 Web3.js 進行 EIP-712 類型結構化資料簽名。
web3.eth.sign
這是早期使用的函式,簽章不使用前綴。
1 | web3.eth.sign(message: Bytes, address: string): Promise<string> |
- message: 簽署的內容,只能是 32 bytes 長度的二進位內容。可以是 Uint8Array 或 0x 開頭的字串。
- address: 簽署人地址。
- 回傳值: 為 Promise 物件,內容為簽章。
Metamask 已經停用了這個功能,使用會在 console 看到錯誤訊息:
1 | inpage.js:1 MetaMask - RPC Error: eth_sign has been disabled. You must enable it in the advanced settings {code: -32601, message: 'eth_sign has been disabled. You must enable it in the advanced settings'} |
如果要使用要在 Metamask 中開啟設定:
[設定] => [進階] => [Eth_sign requests]
開啟輸入 I only sign what I understand
再次嘗試簽章,例如執行以下程式:
1 | const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53'; |
簽署過程可以看到簽署的內容為一串雜湊
因為這個方法不建議使用所以每次簽署都會跳警告
可以利用套件切開簽章
1 | import { fromRpcSig } from 'ethereumjs-util'; |
對應到智能合約的驗證,使用的是第一個範例無前綴的方式
1 | bytes32 hash = 0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53; |
web3.eth.accounts.sign
私鑰簽署使用這函式,會加上 EIP-191 前綴。
1 | web3.eth.accounts.sign(data: string, privateKey: Bytes): SignResult |
- data: 簽署的內容,0x 開頭為 bytes 二進位資料,或是文字內容。
- privateKey: 簽署人私鑰。可以是 Uint8Array 或 0x 開頭的字串。
- 回傳值: 簽章物件,內容參考下面例子。
加上前綴,簽署內容為
1 | “\x19Ethereum Signed Message:\n” + message.length + message |
例如我們簽署一樣內容
1 | const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53'; |
會發現簽章結果和上面不同,另外回傳的格式也不同。同時我們還發現這邊有個 bug,回傳的 r 和 s 前面沒有補 0,正確應該是:
1 | 0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f3 |
在 v4.1.1 有這個 bug,舊版的 1.x 是正常的。我們可以先拿 signature 欄位來使用。對應在智能合約使用前綴的方式來檢驗:
1 | bytes32 messageHash = 0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53; |
我有發 PR 修正個這 Bug,4.1.2 以上應該沒問題了。
或是簽署純文字內容
1 | const message = 'OK!'; |
智能合約
1 | string memory message = "OK!"; |
注意文字如果有 UTF-8 的話,在鏈上的簽署的長度要是 bytes 長度。例如:
1 | // 6 |
web3.eth.personal.sign
瀏覽器錢包簽署使用這函式,會加上 EIP-191 前綴。
1 | web3.eth.personal.sign(data: string, address: string, passphrase: string): Promise<string> |
- data: 簽署的內容,0x 開頭為 bytes 二進位資料,或是文字內容。
- address: 簽署人地址。
- passphrase: 錢包密碼,可以不用填。
- 回傳值: 為 Promise 物件,內容為簽章。
前綴規則同 web3.eth.accounts.sign
,不重複說明。
例如執行以下簽章:
1 | const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53'; |
會跳出簽章訊息如下
簽署結果和 web3.eth.accounts.sign
一樣,只是回傳的格式不同。同樣嘗試文字
1 | const message = 'OK!'; |
會跳出簽章訊息如下
但如果是 0x 開頭的資料,不過長度不是 32 Bytes
1 | // 截掉最後 1 Byte |
結果會變成一串亂碼
智能合約的檢驗也和 web3.eth.accounts.sign
一樣,不重複說明。
簽章格式
上面簽章有切開和未切開的兩種格式,切開就是照固定長度切開,前面 32 bytes 為 r,中間 32 bytes 為 s,最後 1 byte 為 v。拿上面的例子來看:
signature: 0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a356320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f81b
r: 0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a3
s: 0x56320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f8
v: 0x1b
所以要切開簽章除了使用上面提的套件之外,也可以自己簡單的處理:
1 | function splitSignature(signature) { |
檢驗簽章
除了在智能合約上的檢驗之外,我們也可以使用 Web3.js 來檢驗,可以作為後端程式登入之類的用途。可以用下面的函式:
- web3.eth.accounts.recover
- web3.eth.personal.ecRecover
web3.eth.accounts.recover
這個函式支援多種驗證方式,可以輸入切開或為切開的簽章,有前綴或沒前綴都可以。有多種輸入方式
1 | web3.eth.accounts.recover(data: string, v: string, r: string, s: string, prefixed:? boolean): string |
- data: 簽署的內容,0x 開頭為 bytes 二進位資料,或是文字內容。
- v: 0x 開頭的字串。
- r: 0x 開頭的字串。
- s: 0x 開頭的字串。
- prefixed: true 為沒有前綴,false 為使用前綴。預設 false
- 回傳值: 簽章人地址。
或是未切開的簽章
1 | web3.eth.accounts.recover(data: string, signature: string, prefixed:? boolean): string |
- data: 簽署的內容,0x 開頭為 bytes 二進位資料,或是文字內容。
- signature: 未切開的簽章,0x 開頭的字串。
- prefixed: true 為沒有前綴,false 為使用前綴。預設 false
- 回傳值: 簽章人地址。
或是物件
1 | web3.eth.accounts.recover(data: SignatureObject, _: string, prefixed:? boolean): string |
- data: 包含簽署內容和 vrs。
- _: 無作用
- prefixed: true 為沒有前綴,false 為使用前綴。預設 false
- 回傳值: 簽章人地址。
對應 web3.eth.sign 之檢驗
1 | const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53'; |
對應 web3.eth.accounts.sign 和 web3.eth.personal.sign 之檢驗
1 | const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53'; |
純文字的簽署內容
1 | const message = 'OK!'; |
web3.eth.personal.ecRecover
這個函式只支援未切開的簽章,固定使用 EIP-191 前綴。瀏覽器錢包才能用。
1 | web3.eth.personal.ecRecover(signedData: string, signature: string): Promise<string> |
- signedData: 簽署的內容,0x 開頭為 bytes 二進位資料,或是文字內容。
- signature: 未切開的簽章,0x 開頭的字串。
- 回傳值: 為 Promise 物件,內容為簽章人地址。
對應 web3.eth.accounts.sign 和 web3.eth.personal.sign 之檢驗
1 | const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53'; |
純文字的簽署內容
1 | const message = 'OK!'; |
簽名延展性 (Signature Malleability)
Web3.js 同樣有簽名延展性 (Signature Malleability) 問題,一樣可以檢查 s 的值:
1 | if (s > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0n) { |
總結
web3.eth.sign
已過時不使用。- 私鑰簽署使用
web3.eth.accounts.sign
。 - 瀏覽器錢包簽署使用
web3.personal.sign
。 - 檢驗簽章使用
web3.eth.accounts.recover
比較通用。
延伸閱讀
Solidity 智能合約 - ecrecover 簽章檢驗
Solidity 智能合約 - 簽名延展性 (Signature Malleability)
使用 Ethers.js 簽章與檢驗
使用 Web3.js 進行 EIP-712 類型結構化資料簽名