之前的文章介紹了 ecrecover 的用法,不過內建的 ecrecover 存在簽名延展性 (Signature Malleability) 的問題。這篇文章將說明這個問題,以及如何解決。

問題

拿之前文章的例子

1
2
3
4
5
6
7
function test() external view returns(address) {
bytes32 hash = 0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53;
uint8 v = 0x1b;
bytes32 r = 0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a3;
bytes32 s = 0x56320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f8;
return ecrecover(hash , v, r, s);
}

上面的結果會得到簽署人為 0xDD980c315dFA75682F04381E98ea38BD2A151540。我們把簽章做以下竄改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function test() external view returns(address) {
bytes32 hash = 0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53;
uint8 v = 0x1b;
bytes32 r = 0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a3;
bytes32 s = 0x56320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f8;

// 進行竄改
// v: 0x1c
// s: 0xa9cdf41a092863b2f1aa7c29465d1aef0c35eaeb4f584b067debc225f5538e49
v = v == 0x1b ? 0x1c : 0x1b;
s = bytes32(uint(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141) - uint(s));

return ecrecover(hash , v, r, s);
}

結果會發現,仍然能成功回傳相同的簽署者。所以在這個情況下簽章可能會被重複使用。例如修改之前文章的範例,以 s 作為檢驗簽章是否使用過:

1
2
3
4
5
6
7
8
9
10
11
12
contract EcrecoverTest {
mapping(uint => bool) usedSignatures;

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
require(deadline >= block.timestamp, "signature expired");
require(!usedSignatures[s], "used signature");
bytes32 messageHash = abi.encodePacked(block.chainid, address(this), spender, value, deadline);
bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash));
require(owner == ecrecover(hash , v, r, s), "invalid signature");
usedSignatures[s] = true;
}
}

上面的程式會造成一個簽章可以被使用兩次。

原因

以太坊使用的橢圓曲線函數 ECDSA 圖型大概長這樣
ECDSA

數學不是很了解,不過大概可以理解為,在鏡像的地方可以找到另一個解,所以可以很容易修改部分資料達成。

解決方案

驗證簽章範圍

可以去驗證簽章的數值範圍

1
require(uint(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, "invalid s");

使用套件

或者引用 OpenZeppelin ECDSA 套件

1
2
3
4
5
6
7
8
9
10
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract EcrecoverTest {
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
// ...
// 替換掉 ecrecover
require(owner == ECDSA.recover(hash , v, r, s), "invalid signature");
// ...
}
}

延伸閱讀

Solidity 智能合約 - ecrecover 簽章檢驗