ERC-7739: 智能账户的可读类型化签名
一种防御性的重新哈希方案,可防止智能账户之间的签名重放,并保留签名内容的可读性
Authors | vectorized (@vectorized), Sihoon Lee (@push0ebp), Francisco Giordano (@frangio), Hadrien Croubois (@Amxx), Ernesto García (@ernestognw), Im Juno (@junomonster), howydev (@howydev), Atarpara (@Atarpara), 0xcuriousapple (@0xcuriousapple) |
---|---|
Created | 2024-05-28 |
Discussion Link | https://ethereum-magicians.org/t/erc-7739-readable-typed-signatures-for-smart-accounts/20513 |
Requires | EIP-191, EIP-712, EIP-1271, EIP-5267 |
Table of Contents
摘要
本提案定义了一个标准,用于防止当多个智能账户由单个外部拥有账户 (EOA) 拥有时,签名在这些账户之间的重放。这是通过针对 ERC-1271 验证的防御性重新哈希方案实现的,该方案使用特定的嵌套 EIP-712 类型化结构,从而在钱包客户端签名请求期间保留签名内容的可读性。
动机
智能账户可以使用 ERC-1271 通过 isValidSignature
函数验证签名。
如下所示的直接实现容易受到签名重放攻击。
/// @dev 此实现不安全。
function isValidSignature(
bytes32 hash,
bytes calldata signature
) external override view returns (bytes4) {
uint8 v = uint8(signature[64]);
(bytes32 r, bytes32 s) = abi.decode(signature, (bytes32, bytes32));
// 拒绝可延展的签名。
require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0);
address signer = ecrecover(hash, v, r, s);
// 拒绝失败的恢复。
require(signer != address(0));
// `owner` 是一个存储变量,包含智能账户的所有者。
if (signer == owner) {
return 0x1626ba7e;
} else {
return 0xffffffff;
}
}
当多个智能账户由单个 EOA 拥有时,如果 hash
不包括智能账户地址,则相同的签名可以在智能账户之间重放。
不幸的是,许多流行的应用程序(例如 Permit2)都是这种情况。因此,许多智能账户实现都执行某种形式的防御性重新哈希。首先,智能账户从以下内容中计算出一个最终哈希:(1)哈希,(2)其自身的地址,(3)链 ID。然后,智能账户根据签名验证最终哈希。可以使用 EIP-712 实现防御性重新哈希,但直接的实现会使签名内容不透明。
此标准提供了一种防御性重新哈希方案,该方案使签名内容在所有支持 EIP-712 的钱包客户端中可见。它旨在最大限度地减少采用摩擦。即使钱包客户端或应用程序前端未更新,用户仍然可以注入客户端 JavaScript 来启用防御性重新哈希。
规范
本文档中的关键词“必须 (MUST)”,“不得 (MUST NOT)”,“必需 (REQUIRED)”,“应 (SHALL)”,“不应 (SHALL NOT)”,“应该 (SHOULD)”,“不应该 (SHOULD NOT)”,“推荐 (RECOMMENDED)”,“不推荐 (NOT RECOMMENDED)”,“可以 (MAY)”,和“可选 (OPTIONAL)”按照 RFC 2119 和 RFC 8174 中的描述进行解释。
概述
以下依赖是必需的 (REQUIRED):
-
EIP-712 类型化结构化数据哈希和签名。
在内部提供相关的类型化数据哈希逻辑,这是构造最终哈希所必需的。 -
ERC-1271 合约的标准签名验证方法。
提供isValidSignature(bytes32 hash, bytes calldata signature)
函数。 -
ERC-5267 EIP-712 域的检索。
提供eip712Domain()
函数,该函数是计算最终哈希所必需的。
本标准定义了 ERC-1271 的 isValidSignature
函数的行为,该函数包括两个工作流程:(1)TypedDataSign
工作流程,(2)PersonalSign
工作流程。
TypedDataSign
工作流程
TypedDataSign
工作流程处理 hash
最初使用 EIP-712 计算的情况。
TypedDataSign
最终哈希
TypedDataSign
工作流程的最终哈希定义为:
keccak256(\x19\x01 ‖ APP_DOMAIN_SEPARATOR ‖
hashStruct(TypedDataSign({
contents: hashStruct(originalStruct),
name: eip712Domain().name,
version: eip712Domain().version,
chainId: eip712Domain().chainId,
verifyingContract: eip712Domain().verifyingContract,
salt: eip712Domain().salt
}))
)
其中 ‖
表示字节的连接运算符。
在 Solidity 中,这可以写成:
finalTypedDataSignHash =
keccak256(
abi.encodePacked(
hex"1901",
// 应用程序特定的域分隔符。通过 `signature` 传递。
bytes32(APP_DOMAIN_SEPARATOR),
keccak256(
abi.encode(
// 使用 `contentsType` 动态计算,该 `contentsType` 通过 `signature` 传递。
typedDataSignTypehash,
// 这是 `contents` 结构哈希,通过 `signature` 传递。
bytes32(hashStruct(originalStruct)),
// `eip712Domain()` 来自 ERC-5267。
keccak256(bytes(eip712Domain().name)),
keccak256(bytes(eip712Domain().version)),
uint256(eip712Domain().chainId),
uint256(uint160(eip712Domain().verifyingContract)),
bytes32(eip712Domain().salt)
)
)
)
);
其中 typedDataSignTypehash
是:
typedDataSignTypehash =
keccak256(
abi.encodePacked(
"TypedDataSign(",
contentsName, " contents,",
"string name,",
"string version,",
"uint256 chainId,",
"address verifyingContract,",
"bytes32 salt"
")",
contentsType
)
);
如果 contentsType
是 "Mail(address from,address to,string message)"
,则 contentsName
将是 "Mail"
。
contentsName
是 contentsType
的子字符串,直到(不包括)第一个 "("
实例:
在 Solidity 中,这可以写成:
// `slice(string memory subject, uint256 start, uint256 end)`
// 返回从 `start` 到 `end`(不包括)切片的 `subject` 的副本。
// `start` 和 `end` 是字节偏移量。
//
// `indexOf(string memory subject, string memory search)`
// 返回从左到右在 `subject` 中第一次出现 `search` 的字节索引,
// 如果未找到 `search`,则返回 `2**256 - 1`。
contentsName =
LibString.slice(
contentsType,
0, // 起始字节索引。
LibString.indexOf(contentsType, "(") // 结束字节索引(不包括)。
);
为了完整起见,提供了 LibString
Solidity 库的副本。
为了安全起见,如果以下任何一项为真,建议 (RECOMMENDED) 将签名视为无效:
contentsName
是空字符串(即bytes(contentsName).length == 0
)。contentsName
以以下任何字节开头abcdefghijklmnopqrstuvwxyz(
。contentsName
包含以下任何字节, )\x00
。
TypedDataSign
签名
传递到 isValidSignature
中的 signature
将更改为:
originalSignature ‖ APP_DOMAIN_SEPARATOR ‖ contents ‖ contentsDescription ‖ uint16(contentsDescription.length)
其中:
contents
是原始结构的 bytes32 结构哈希。contentsDescription
是:contentsType
(隐式模式),
其中contentsType
以contentsName
开头。contentsType ‖ contentsName
(显式模式),
其中contentsType
不一定以contentsName
开头。
在 Solidity 中,这可以写成:
signature =
abi.encodePacked(
bytes(originalSignature),
bytes32(APP_DOMAIN_SEPARATOR),
bytes32(contents),
bytes(contentsDescription),
uint16(contentsDescription.length)
);
附加的 APP_DOMAIN_SEPARATOR
和 contents
结构哈希将用于验证通过 isValidSignature
传入的 hash
是否确实正确,通过:
hash == keccak256(
abi.encodePacked(
hex"1901",
bytes32(APP_DOMAIN_SEPARATOR),
bytes32(contents)
)
)
如果 hash
与重建的哈希不匹配,则 hash
和 signature
在 TypedDataSign
工作流程下无效。
PersonalSign
工作流程
此 PersonalSign
工作流程处理 hash
最初使用 EIP-191 计算的情况。
PersonalSign
最终哈希
PersonalSign
工作流程的最终哈希定义为:
keccak256(\x19\x01 ‖ ACCOUNT_DOMAIN_SEPARATOR ‖
hashStruct(PersonalSign({
prefixed: keccak256(bytes(\x19Ethereum Signed Message:\n ‖
base10(bytes(someString).length) ‖ someString))
}))
)
其中 ‖
表示字节的连接运算符。
在 Solidity 中,这可以写成:
finalPersonalSignHash =
keccak256(
abi.encodePacked(
hex"1901",
// 智能账户域分隔符。
// 可以通过来自 ERC-5267 的 `eip712Domain()` 计算。
bytes32(ACCOUNT_DOMAIN_SEPARATOR),
keccak256(
abi.encode(
// `PERSONAL_SIGN_TYPEHASH`。
keccak256("PersonalSign(bytes prefixed)"),
// `hash` 来自 `isValidSignature(hash, signature)`
hash
)
)
)
);
这里,hash
在应用程序合约中计算并传递到 isValidSignature
中。
智能账户不需要知道如何计算 hash
。为了完整起见,这是它的计算方式:
hash =
abi.encodePacked(
"\x19Ethereum Signed Message:\n",
// `toString` 返回 uint256 的 base10 表示形式。
LibString.toString(someString.length),
// 这是要签名的原始消息。
someString
);
PersonalSign
签名
PersonalSign
工作流程不需要将其他数据附加到传递到 isValidSignature
中的 signature
。
支持检测
智能账户应该 (SHOULD) 为 isValidSignature(0x7739773977397739773977397739773977397739773977397739773977397739, "")
返回 bytes4(0x77390001)
,以指示支持此标准。
如果此标准得到更新,则幻数 bytes4(0x77390001)
可以 (MAY) 递增。
签名验证工作流程推导
由于 isValidSignature
签名函数签名未更改,因此实现必须能够推导出验证签名所需的工作流程类型。
如果签名包含正确的数据以重建 hash
,则 isValidSignature
函数必须执行 TypedDataSign
工作流程。
否则,isValidSignature
函数必须执行 PersonalSign
工作流程。
在 Solidity 中,可以这样编写检查:
// 如果这是真的,则表示 `signature` 包含
// 正确的 `APP_DOMAIN_SEPARATOR` 和 `contents`,
// 并且必须执行 `TypedDataSign` 工作流程。
// 否则,必须执行 `PersonalSign` 工作流程。
hash == keccak256(
abi.encodePacked(
hex"1901",
bytes32(APP_DOMAIN_SEPARATOR),
bytes32(contents)
)
)
有条件地跳过防御性重新哈希
如果以下任何一项为真,则智能账户可以 (MAY) 跳过防御性重新哈希工作流程:
isValidSignature
被链下调用。- 传递到
isValidSignature
中的hash
已经包含了智能账户的地址。
由于许多开发人员可能不会更新他们的应用程序以支持嵌套的 EIP-712 工作流程,因此智能账户实现应该 (SHOULD) 尝试通过在安全的情况下跳过防御性重新哈希来适应。
原理
TypedDataSign
结构
typedDataSignTypehash
必须在链上动态构造。这是为了强制签名内容在签名请求中可见,要求 contents
必须是用户定义的类型。
eip712Domain
的字段被展平到 TypedDataSign
结构中,而不是作为 EIP712Domain
类型的字段包含,以避免与验证合约的域类型发生冲突(如果不同)。
ERC-5267 中的 bytes1 fields
位图和 uint256[] extensions
数组已被省略。区分不存在的字段与零字段(例如 bytes32(0)
)不会为链上防御性重新哈希提供额外的安全优势。extensions
参数是用于链下信令的 EIP 编号列表。
隐式和显式模式的 contentsDescription
当 contents
结构包含嵌套类型时,EIP-712 字典排序可能会导致 contentsName
未精确定位在 contentsType
的开头。因此,我们需要显式模式。
使用 isValidSignature
进行支持检测
为了更容易在模块化智能账户中实现,我们已决定利用 isValidSignature
方法返回幻数,而不是定义新函数。
拒绝以任何小写 7 位 ASCII 字符开头的 contentsName
此建议是为了保持标准语言不可知且面向未来。原子类型(如 uint256
)在其他语言中可能有不同的名称(例如 u256
)。
向后兼容性
检测先前的草案
在先前的草案中,我们指定了一个 supportsNestedTypedDataSign()
函数用于支持检测,该函数返回 bytes4(0xd620c85a)
。
参考实现
它包括安全、灵活性、开发人员体验和用户体验所需的相关的补充功能。
参考实现并非有意设计的极简。这是为了避免重蹈 ERC-1271 的覆辙,在 ERC-1271 中,极简的参考实现被错误地认为可以安全地用于生产。
安全考虑
拒绝无效的 contentsName
当前 eth_signTypedData
的主要实现不会清理自定义类型的名称。
网络钓鱼网站可以制作带有控制字符的 contentsName
,以跳出 PersonalSign
类型编码,从而导致钱包客户端要求用户签署不透明的哈希。
要求链上清理 contentsName
将阻止此网络钓鱼攻击媒介。
不可能链接这种类型的多个签名者
对于 ERC-1271 签名,使用此方法作为重放保护的帐户不能有使用相同方法的签名者。这是因为签名定义了一个 TypedDataSign
结构类型,其成员具有正在签名的消息的类型,并且如果正在签名的消息是另一个 TypedDataSign
结构,则生成的 EIP-712 消息将在其主体中包含两个具有不兼容内容的单独的 TypedDataSign
类型,这是无法在 EIP-712 请求中表示的。
版权
在 CC0 下放弃版权及相关权利。
Citation
Please cite this document as:
vectorized (@vectorized), Sihoon Lee (@push0ebp), Francisco Giordano (@frangio), Hadrien Croubois (@Amxx), Ernesto García (@ernestognw), Im Juno (@junomonster), howydev (@howydev), Atarpara (@Atarpara), 0xcuriousapple (@0xcuriousapple), "ERC-7739: 智能账户的可读类型化签名 [DRAFT]," Ethereum Improvement Proposals, no. 7739, May 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7739.