iT邦幫忙

2024 iThome 鐵人賽

DAY 17
0
Modern Web

web3 短篇集系列 第 17

合約錢包的簽章 (ERC-1271)

  • 分享至 

  • xImage
  •  

合約錢包無法簽名,因此難以證明某個合約錢包的意圖,ERC-1271 引入 isValidSignature 這個函式,讓合約錢包實作,讓別人可以靠它來驗證合約錢包的簽章。

簽章可以從兩個角度看:

  • Signer: 對一個「訊息」簽名,產生一個證明(簽章),代表 signer 與訊息是互相連結的。
  • Verifier: 驗證訊息與簽章是否來自 signer。

所謂 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 的簽章:

SignatureChecker.sol

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)。

驗證方的應用案例

  • Cow DAO 用來 placing orders 都是用 signature,因此讓 SCA 可以驗 signature 很重要。
    • Signed messages are necessary for a variety of things including placing orders on decentralized exchanges using off-chain order books, verifying that a given wallet belongs to a user
    • EIP-1271 Explained - CoW DAO
  • 中心化的登入服務,例如 Opensea 登入時會要你簽個名。

實作 isValidSignature

以下是 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,它的 isValidSignaturevalidateUserOp 實作通常會通往同一個函式(可能叫 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);
}

Reference

PS: 同時進行鐵人賽的 yahsin 關於數位簽章的文章:


上一篇
結構化的簽章 (ERC-712)
下一篇
認識 User Operation
系列文
web3 短篇集30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言