签名滥用与重放:验签不等于业务授权
签名验证回答“这份消息是否符合指定账户的签名规则”。资产操作还要回答“谁收款、哪个合约执行、允许执行几次、是否已经取消”。即使验签正确,业务参数没有绑定也可能让攻击者使用受害者的授权。
先阅读 EIP-712、ERC-1271 与 EIP-2612,再逐项检查下面的攻击条件。
1. 重放的范围
| 缺少的约束 | 可能的结果 |
|---|---|
| 一次性 nonce 或订单消费状态 | 同一资产操作被重复执行 |
| 验签合约或业务域 | 另一个接受同样消息的应用也接受签名 |
| 链 ID 或其他网络标识 | 接受相同消息规则的其他网络可以使用签名 |
| 收款人、金额或业务动作 | 合法签名被用在不同于用户意图的执行中 |
这些结果以相关系统采用兼容的验签规则为前提,不是签名缺一个字段就必然在所有链或所有合约上可重放。
nonce 和 deadline 是应用策略,不是 EIP-712 的强制组成。一次性转账通常需要消费标记;允许反复证明身份的消息则需要另一套有效期和使用场景设计。deadline 本身只限时间,不限制时间内的使用次数。
2. ECDSA 延展性需要同时考虑 s 与 v
对于可恢复签名 (v, r, s),将 s 变成曲线阶 n - s 时,还需翻转恢复分支 v(27 与 28 互换),才能恢复同一地址。只说“翻转 s 就恢复同一地址”是不完整的。
EIP-2 限制交易签名的 high-s,但保留了 ecrecover 预编译接受 high-s 的行为。合约消息验签不能假设协议已经替自己完成 low-s 检查。
如果按 keccak256(signature) 去重,两种有效字节形式可能对应同一业务授权。应按 Owner 与 nonce、或明确的订单摘要去重,并选用检查 low-s 的库。支持其他签名格式时,同样不能把字节唯一性当作业务唯一性。
示例工程的 testHighSMalleabilityRequiresFlippedVAndIsRejected 确认原始 ecrecover 接受两种恢复形式,而固定 OpenZeppelin 5.2.0 的验签库拒绝 high-s。
3. 恢复地址、编码与执行参数
ecrecover 是 Solidity 提供的预编译调用入口,不是 EVM 操作码。失败时可返回零地址;仅检查结果是否等于未初始化的 expectedSigner,可能把两者均为零当成通过。
多个动态字段的 abi.encodePacked 有分界歧义,例如 ("a", "bc") 和 ("ab", "c") 都得到 0x616263。这里是编码不唯一,不是攻破 Keccak。使用明确 ABI 编码或 EIP-712 的动态字段哈希规则。ABI 规范
定长的 0x1901、domainSeparator 和 structHash 可以按规范 packed 拼接。不能把这一合法用法与动态业务字段拼接混为一谈。
4. 账户类型和可撤销签名
传统合约账户按 ERC-1271 查询当前策略;EOA 可用私钥签名。EIP-7702 委托 EOA 仍保留私钥,也可通过委托代码执行钱包策略。是否支持 ERC-1271 取决于代码与应用约定,不是委托的自动效果。EIP-7702
合约签名的有效性可能随钱包状态变化。链下预检查成功后,结算时仍需重验;业务取消和 nonce 消费还应由结算协议检查。
5. 假 permit 与任意 from
一种可以直接复现的缺陷:用户先给 Router 授权;Router 允许任意调用者传入 from 和收款人,却没有独立验证该业务操作。攻击者可以利用已有 allowance 从受害者转出资产。
在这样的入口里加一个可能被忽略、或只触发空 fallback 的 permit 调用,不会补上缺失的业务权限。但正确绑定身份和收款行为时,容忍被抢先提交的 permit 失败也可以安全,不能一概要求捕获异常后必须回滚。
历史安全事件需要具体合约版本、交易和原始分析才能归因;这里以可运行模型说明机制,不用未经核实的项目名、资产清单或损失金额代替证据。
6. 可运行的攻防对照
仓库 examples/ethereum-signatures 包括:
- 任意
from的漏洞夹具,复现受害者历史额度被利用。 - 身份绑定的
PermitDeposit,不能用攻击者身份消费受害者额度。 - 签入收款人的
SignedTransfer,支持 Relayer,却不能修改收款人或重放 nonce。
npm ci --ignore-scripts
forge test --match-test 'testSignedTransfer|testHighS|testArbitraryFrom|testFakePermit' -vv
测试夹具中的 UnsafeDeposit 是故意有漏洞的反例,不应部署为业务合约。关于域缓存与代理上下文,继续阅读签名验证进阶。