之前在 Solidity 智能合約 - ecrecover 簽章檢驗這篇文章,說明了智能合約如何檢驗簽章,這篇文章將說明如何使用 Ethers.js 來進行簽章與檢驗。本篇文章使用 Ethers.js 6.7.1 版本。

Signer

後面內容要使用 Signer 物件,這邊先簡單說明如何取得。

私鑰 signer

1
2
3
4
5
import { JsonRpcProvider, Wallet } from "ethers";

const privateKey = '0x...'
const provider = new JsonRpcProvider(url);
const signer = new Wallet(privateKey, provider);

瀏覽器錢包 signer

1
2
3
4
import { BrowserProvider } from "ethers";

const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();

簽章

相較於 Web3.js 有很多種方法, Ethers.js 有下面同步和非同步的版本:

  • signer.signMessage
  • signer.signMessageSync

關於 EIP-712 相關的方法,參考使用 Ethers.js 進行 EIP-712 類型結構化資料簽名

signer.signMessage

固定使用 EIP-191 前綴

1
signer.signMessage(message: string | Uint8Array): Promise<string>
  • message: 簽署的內容,如果是字串則視為純文字,二進位資料使用 Uint8Array。
  • 回傳值: 為 Promise 物件,內容為簽章。

加上前綴,簽署內容為

1
“\x19Ethereum Signed Message:\n” + message.length + message

例如我們簽署二進位內容

1
2
3
4
5
6
7
8
9
10
import { getBytes } from "ethers";

const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53';
const messageHashBytes = getBytes(message);

// v5 使用 arrayify
// const messageHashBytes = ethers.utils.arrayify(message);

// 0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f30029ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a1c
const signature = await signer.signMessage(messageHashBytes);

在 Metamask 會跳出簽章訊息如下
簽署

可以用下面函式切開簽章

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Signature } from "ethers";

// {
// _type: 'signature',
// networkV: null,
// r: '0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f3',
// s: '0x0029ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a',
// v: 28
// }
Signature.from(signature).toJSON();

// v5 使用 splitSignature
// ethers.utils.splitSignature(signature);

對應在智能合約使用前綴的方式來檢驗:

1
2
3
bytes32 messageHash = 0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53;
bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash));
return ecrecover(hash , v, r, s);

或是簽署純文字內容

1
2
3
const message = 'OK!';
// 0x7dff4feac0d0bf31d5b11ef3793bf9d00e7b9028eaaa9737f5079685ff435c9c7a22e79b7dab0b0d37524a0f9621d98b76fbc77b6402f87d142746eee61edcb21c
const signature = await signer.signMessage(message);

會跳出簽章訊息如下
簽署純文字

智能合約

1
2
3
string memory message = "OK!";
bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n3", message));
return ecrecover(hash , v, r, s);

注意文字如果有 UTF-8 的話,在鏈上的簽署的長度要是 bytes 長度。例如:

1
2
// 6
new Blob(['中文']).size

signer.signMessageSync

signMessage 的同步版本,只能在 Node.js 使用

1
signer.signMessageSync(message: string | Uint8Array): string
  • message: 簽署的內容,如果是字串則視為純文字,二進位資料使用 Uint8Array。
  • 回傳值: 簽章。

可以直接取得簽章

1
const signature = signer.signMessageSync(message);

其他和 signMessage 一樣,不重複說明。

簽章格式

上面簽章有切開和未切開的兩種格式,切開就是照固定長度切開,前面 32 bytes 為 r,中間 32 bytes 為 s,最後 1 byte 為 v。拿上面的例子來看:

signature: 0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a356320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f81b

r: 0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a3
s: 0x56320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f8
v: 0x1b

所以要切開簽章除了使用上面提的方法之外,也可以自己簡單的處理:

1
2
3
4
5
6
7
function splitSignature(signature) {
return {
r: `0x${signature.substring(2, 66)}`,
s: `0x${signature.substring(66, 130)}`,
v: `0x${signature.substring(130, 132)}`
}
}

檢驗簽章

除了在智能合約上的檢驗之外,我們也可以使用 Ethers.js 來檢驗,可以作為後端程式登入之類的用途。可以用下面的函式:

  1. verifyMessage
  2. recoverAddress

verifyMessage

固定使用 EIP-191 前綴,簽書內容可輸入純文字或二進位資料。

1
verifyMessage(message: Uint8Array | string, sig: SignatureLike): string
  • message: 簽署的內容,如果是字串則視為純文字,二進位資料使用 Uint8Array。
  • sig: 簽章,可以是未切開的字串,或者是包含 vrs 的物件。
  • 回傳值: 簽署人地址。

二進位內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { getBytes, verifyMessage } from "ethers";

const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53';
const messageHashBytes = getBytes(message);
const signature = '0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f30029ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a1c';

// or vrs
// const signature = {
// r: '0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f3',
// s: '0x0029ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a',
// v: 28
// };

// 0xDD980c315dFA75682F04381E98ea38BD2A151540
const address = verifyMessage(messageHashBytes, signature);

純文字內容

1
2
const message = 'OK!';
const address = verifyMessage(message, signature);

recoverAddress

固定使用 EIP-191 前綴,簽書內容只能輸入二進位資料。

1
recoverAddress(digest: Uint8Array | string, signature: SignatureLike): string
  • digest: 簽署的內容,字串必須是 0x 開頭的字串或使用 Uint8Array。
  • sig: 簽章,可以是為切開的字串,或者是包含 vrs 的物件。
  • 回傳值: 簽署人地址。

0x 開頭的字串可以直接丟入,用 getBytes 轉換也可以。

1
2
3
4
import { recoverAddress } from "ethers";

const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53';
const address = recoverAddress(message, signature);

簽名延展性 (Signature Malleability)

Ethers.js 有處理簽名延展性 (Signature Malleability) 問題,篡改過的簽章會出現 INVALID_ARGUMENT 的錯誤。

總結

  1. 簽署使用 signMessage 比較通用。
  2. 檢驗簽章純文字可使用 verifyMessage
  3. 檢驗簽章二進位內容可使用 recoverAddress

延伸閱讀

Solidity 智能合約 - ecrecover 簽章檢驗
Solidity 智能合約 - 簽名延展性 (Signature Malleability)
使用 Web3.js 簽章與檢驗
使用 Ethers.js 進行 EIP-712 類型結構化資料簽名