Ethereum Dapp 签名验签中的安全问题

2023 年 11 月 19 日 · 更新

因为我们最近在做 ERC4337 账户抽象相关的东西,根据 EntryPoint 校验签名的几个条件补充一下,除了 地址次数 这些维度之外还有 时间维度,如果一个有个硬分叉的链,只有旧数据没有新数据,恶意攻击者会拿着新的签名去旧数据中使用。

针对时间维度的防攻击就是加上 validUntilvalidAfter

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);
}

Comments