Ethereum Dapp 签名验签中的安全问题
2023 年 11 月 19 日 · 更新
因为我们最近在做 ERC4337 账户抽象相关的东西,根据 EntryPoint 校验签名的几个条件补充一下,除了 地址、次数、链 这些维度之外还有 时间维度,如果一个有个硬分叉的链,只有旧数据没有新数据,恶意攻击者会拿着新的签名去旧数据中使用。
针对时间维度的防攻击就是加上 validUntil、validAfter
2021 年 03 月 22 日 · 更新
在一些审计报告和 Uniswap 中学到其实可以设置一个 deadline(block.timestamp)来取代下文 nonce 字段。因为使用 nonce 字段你需要记录和知道当前 nonce 到哪里了。
关键点
钱包的签名可以
- 任何地址 D 用户拿着 C 用户的签名去验证
- 任意次数 重复调用
- 任何网络(主网、测试网)跨链攻击
- 调用合约 给 A 合约的签名被拿去给 B 使用
- 任何时间 一个签名一旦签出,这个签名在任何时间都是合法签名
来使用。
举例
拿一个 Solidity 官方文档中的例子来说:
// recipient is the address that should be paid.
// amount, in wei, specifies how much ether should be sent.
// nonce can be any unique number to prevent replay attacks
// contractAddress is used to prevent cross-contract replay attacks
function signPayment(recipient, amount, nonce, contractAddress, callback) {
var hash = "0x" + abi.soliditySHA3(
["address", "uint256", "uint256", "address"],
[recipient, amount, nonce, contractAddress]
).toString("hex");
web3.eth.personal.sign(hash, web3.eth.defaultAccount, callback);
}
其实这个例子不算特别好,这是一个支付相关的签名示例,包含了下面的字段:
- recipient 收款地址,验签固定收款的钱包
延伸解释:B 对「B 支付给 A 3 ETH」这个数据进行签名,然后 C 偷盗了签名数据拿去调用,这样 ETH 也不会划转给 C 地址。 - amount 付款金额,验签固定付款金额
- nonce 唯一 ID,防止签名被多次使用
延伸解释:B 对「B 支付给 A 3 ETH」这个数据进行签名,然后 A 去使用签名多次领取,这样 B 就付给了 A 多次 ETH,如果使用了 唯一 ID,即可限制领取一次 - contractAddress 当前合约的地址(加一个合约地址可以有效防止签名被用在其他合约中)
延伸解释:B 对「B 支付给 A 3 ETH」这个数据进行签名,原本是在二手手机中介那里记的帐,A 已经领取了 ETH,结果 A 转头又去二手车中介那里说 B 应该付款 3 ETH 又收了 B 的钱。
所以通过这几个参数,我们发现
- recipient 对应 关键点里面的 「任意地址」
- nonce 对应 「任意次数」
- contractAddress 对应 「任意合约」
唯独漏了一点「任意网络」,这样的话你在测试网上面给 「向 A 转账 3 ETH」,A 可以拿到 mainnet 上面相同 contractAddress 的合约里面调用来领取 3 ETH。
所以这个例子应该改为:
// recipient is the address that should be paid.
// amount, in wei, specifies how much ether should be sent.
// nonce can be any unique number to prevent replay attacks
// chainId is used to prevent cross-contract replay attacks
// contractAddress is also used to prevent cross-contract replay attacks
function signPayment(recipient, amount, nonce, chainId, contractAddress, callback) {
var hash = "0x" + abi.soliditySHA3(
["address", "uint256", "uint256", "uint256", "address"],
[recipient, amount, nonce, chainId, contractAddress]
).toString("hex");
web3.eth.personal.sign(hash, web3.eth.defaultAccount, callback);
}