合約錢包無法簽名,因此難以證明某個合約錢包的意圖,ERC-1271 引入 isValidSignature
這個函式,讓合約錢包實作,讓別人可以靠它來驗證合約錢包的簽章。
簽章可以從兩個角度看:
所謂 Digital Signature,包含「訊息」與「簽章」,這兩個分開來就沒有意義,就好比在紙本合約底下簽名,你把合約跟簽名撕成兩半,就沒有人會相信合約內容與簽署者之間的效力。
以太坊上的帳戶 (EOA) 使用 ECDSA 演算法,具有公私鑰對可以製作電子簽章。合約帳戶 (SCA) 是一個部署到鏈上的合約,用來當加密貨幣錢包使用,因為區塊鏈是公開透明的,不可能在鏈上實作加密演算法,問題是,如何讓 SCA 的所有權人(們)證明某一個訊息和 SCA 的連結關係?
ERC-1271 建議 SCA 實作以下函式,讓應用能夠驗證「訊息」和「簽章」是被這個 SCA 核可的:
bytes4 constant internal MAGICVALUE = 0x1626ba7e;
function isValidSignature(
bytes32 _hash,
bytes memory _signature)
public
view
returns (bytes4 magicValue);
為什麼不回傳 true 而回傳 MAGICVALUE?因為怕該合約沒有實作 isValidSignature
,然後 fallback 卻回傳 true,可能混淆驗證方。
要實作 isValidSignature
非常彈性,沒做好會有可怕的安全問題,我們先來看如何驗證,Openzeppelin 提供一個圖書館可以同時驗證 EOA 和 SCA 的簽章:
library SignatureChecker {
/**
* @dev Checks if a signature is valid for a given signer and data hash. If the signer is a smart contract, the
* signature is validated against that smart contract using ERC1271, otherwise it's validated using `ECDSA.recover`.
*
* NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus
* change through time. It could return true at block N and false at block N+1 (or the opposite).
*/
function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) {
(address recovered, ECDSA.RecoverError error, ) = ECDSA.tryRecover(hash, signature);
return
(error == ECDSA.RecoverError.NoError && recovered == signer) ||
isValidERC1271SignatureNow(signer, hash, signature);
}
/**
* @dev Checks if a signature is valid for a given signer and data hash. The signature is validated
* against the signer smart contract using ERC1271.
*
* NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus
* change through time. It could return true at block N and false at block N+1 (or the opposite).
*/
function isValidERC1271SignatureNow(
address signer,
bytes32 hash,
bytes memory signature
) internal view returns (bool) {
(bool success, bytes memory result) = signer.staticcall(
abi.encodeCall(IERC1271.isValidSignature, (hash, signature))
);
return (success &&
result.length >= 32 &&
abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector));
}
}
為什麼要特別寫「Now」?它在 NOTE 說明,因為 SCA 的 isValidSignature
可能在此時對某訊息和簽章驗證通過,彼時可能同樣的訊息與簽章不會通過,因此稱 SCA 的簽章是可撤銷的 (revocable)。
以下是 ERC-1271 提供的簡單實作,它讓一個 EOA 掌管一個 SCA,因此該 EOA 的簽章即代表該 SCA 的簽章:
function isValidSignature(
bytes32 _hash,
bytes calldata _signature
) external override view returns (bytes4) {
// Validate signatures
if (recoverSigner(_hash, _signature) == owner) {
return 0x1626ba7e;
} else {
return 0xffffffff;
}
}
SCA 根據 ERC-4337 抽象帳戶實作的話,會要求一個函式 validateUserOp
,當中會實作驗證 User Operation 內的簽章是否有效,因此如果是一個遵循 ERC-1271 和 ERC-4337 的 SCA,它的 isValidSignature
和 validateUserOp
實作通常會通往同一個函式(可能叫 validateSignature)用於實作該 SCA 的驗證機制。SCA 可以實作出不同於 EOA 的 ECDSA,因此這個技術叫帳戶抽象,將錢包的驗證機制抽象化,讓 SCA 自行實作。
這篇 2024/3 寫得文章,說明上方的簡單實作可能導致重放攻擊。
重放攻擊會發生的場景來自於,假如一個 EOA 掌管多個 SCA(都是以上實作方式),那 EOA 的簽章將可以同時用於多個 SCA,文中發現如果其中一個 SCA 與 Permit2 互動,攻擊者就能偷走該簽章,並對該 EOA 所掌管的其他 SCA 進行重放攻擊。
文中提出兩個解法,對上述的簡單實作進行修改,並推薦使用 ERC-712 的包法,因為錢包管理器通常會在用戶簽名前,呈現較清楚的內容。
// Solution 1:
// Wrap incoming digest with SCA EIP712 domain
function isValidSignature(bytes32 digest, bytes calldata sig) external view returns (bytes4) {
bytes32 domainSeparator =
keccak256(
abi.encode(
_DOMAIN_SEPARATOR_TYPEHASH,
_NAME_HASH,
_VERSION_HASH,
block.chainid,
address(SCA)
)
);
bytes32 wrappedDigest = keccak256(abi.encode("\x19\x01", domainSeparator, digest));
return ECDSA.recover(wrappedDigest, sig);
}
// Solution 2:
// Wrap incoming digest with just the address of the SCA
function isValidSignature(bytes32 digest, bytes calldata sig) external view returns (bytes4) {
bytes32 wrappedDigest = keccak256(abi.encode(digest, address(SCA));
return ECDSA.recover(wrappedDigest, sig);
}
isValidSignature
。isValidSignature
PS: 同時進行鐵人賽的 yahsin 關於數位簽章的文章: