ECDSA Signature Vulns in Solidity
字数 1586 2025-08-29 08:30:12

Solidity 中 ECDSA 签名漏洞全面解析

1. 以太坊签名机制基础

1.1 账户与签名机制

在以太坊中:

  • 用户账户由公私钥对生成
  • 私钥用于对交易签名
  • 公钥前20字节作为账户地址
  • 签名机制是整个区块链的基石

1.2 ECDSA 签名过程

以太坊使用 ECDSA (椭圆曲线数字签名算法):

  • 输入参数:私钥 private_key 和消息哈希 message_hash
  • 输出结果:vrs 三个参数
  • 最终签名:r(32字节) + s(32字节) + v(1字节) 拼接成65字节的 signature

2. 合约中的签名验证

2.1 签名恢复方法

合约中常用的两种签名恢复方法:

方法一:ecrecover (EVM预编译函数)

address signer = ecrecover(messageHash, v, r, s);

方法二:ECDSA.recover (OpenZeppelin封装)

address signer = ECDSA.recover(messageHash, signature);

2.2 两种方法的区别

OpenZeppelin的 ECDSA.recover

  • ecrecover 进行了封装
  • 增加了预先检查
  • 防止ECDSA签名延展性攻击
  • 提供更丰富的功能支持
  • 但需注意紧凑型签名攻击风险

3. 常见签名验证场景

3.1 授权特权操作

  • 通过验证单个用户签名
  • 允许调用者执行特权操作

3.2 用户份额累加

  • 通过验证多个用户签名
  • 累加用户份额(类似投票机制)

4. ECDSA 签名漏洞类型及实例

4.1 签名重复性检验不完善

漏洞特征

  • 仅检查相邻签名是否相同
  • 允许不同用户交替重复签名

实例分析

// 错误实现 - 仅检查相邻签名
function changeBridgeSettings(bytes memory signature) public {
    require(signature != lastSigner, "Duplicate signature");
    lastSigner = signature;
    // 其他逻辑...
}

攻击方式

  • 两个不同用户交替重复签名
  • 可无限累加份额

修复方案

  • 应记录所有已用签名
  • 或记录消息哈希而非签名

4.2 ECDSA签名延展性攻击

漏洞原理

  • 对于同一 r,存在两个不同的 s 满足要求
  • 可根据一个 s 计算另一个:s' = n - s (n为曲线阶数)

实例分析

// 使用 ecrecover 易受攻击
address signer = ecrecover(messageHash, v, r, s);

修复方案

  • 使用 OpenZeppelin 的 ECDSA.recover
  • 该函数内部会检查并拒绝延展性签名

4.3 紧凑型签名攻击

背景知识

  • ERC-2098 引入紧凑型签名
  • 签名长度从65字节缩短到64字节
  • 初衷是优化EVM空间使用

漏洞特征

  • 合约仅存储原始签名进行重复性检查
  • 攻击者可生成紧凑型等效签名绕过检查

修复方案

  • 应存储和检查消息哈希 (digest) 而非签名
  • 修改 nullifiers 存储结构:
mapping(bytes32 => bool) private usedDigests; // 存储用过的消息哈希

4.4 不遵守规范的无限签名

漏洞原理

  • RFC-6979 规定应使用确定性 k 进行签名
  • ecrecover 不检查 k 是否符合标准
  • 攻击者可生成无数个有效签名

防护措施

  • 严格检查消息哈希的唯一性
  • 绝不依赖签名本身的唯一性

5. 最佳实践总结

  1. 优先使用 OpenZeppelin 的 ECDSA

    • 提供更安全的封装
    • 防止常见攻击
  2. 正确的重复性检查

    // 正确做法 - 检查消息哈希
    bytes32 digest = keccak256(abi.encodePacked(message));
    require(!usedDigests[digest], "Signature already used");
    usedDigests[digest] = true;
    
  3. 消息格式标准化

    • 使用以太坊签名标准格式
    • 包含 \x19Ethereum Signed Message:\n 前缀
  4. 签名验证完整流程

    function verify(
        address signer,
        bytes32 hash,
        bytes memory signature
    ) internal {
        require(signer == ECDSA.recover(hash, signature), "Invalid signature");
        require(!usedHashes[hash], "Signature already used");
        usedHashes[hash] = true;
    }
    
  5. 警惕签名变体

    • 考虑所有可能的签名格式
    • 包括标准签名和紧凑型签名

6. 高级防护技巧

  1. 添加 nonce 机制

    mapping(address => uint256) public nonces;
    
    function verifyWithNonce(
        address signer,
        bytes32 hash,
        bytes memory signature,
        uint256 nonce
    ) internal {
        require(nonce == nonces[signer], "Invalid nonce");
        nonces[signer]++;
        verify(signer, hash, signature);
    }
    
  2. 时间限制签名

    function verifyWithTime(
        address signer,
        bytes32 hash,
        bytes memory signature,
        uint256 expiry
    ) internal {
        require(block.timestamp <= expiry, "Signature expired");
        verify(signer, hash, signature);
    }
    
  3. 上下文绑定

    • 在签名消息中包含合约地址
    • 防止跨合约重放攻击

7. 测试建议

  1. 测试用例应包括

    • 标准签名验证
    • 紧凑型签名验证
    • 签名重放尝试
    • 延展性签名尝试
    • 过期签名验证
  2. 使用多样化工具测试

    // 使用不同库生成签名进行测试
    const signature1 = web3.eth.sign(message, privateKey);
    const signature2 = ethers.utils.splitSignature(
        await wallet.signMessage(message)
    );
    

8. 总结

Solidity 中的 ECDSA 签名验证看似简单,实则暗藏多种安全隐患。开发者必须:

  1. 深入理解 ECDSA 算法特性
  2. 使用经过验证的安全库
  3. 实施全面的重复性检查
  4. 考虑所有可能的签名变体
  5. 设计完整的签名生命周期管理

遵循这些原则,才能确保基于签名的智能合约功能既安全又可靠。

Solidity 中 ECDSA 签名漏洞全面解析 1. 以太坊签名机制基础 1.1 账户与签名机制 在以太坊中: 用户账户由公私钥对生成 私钥用于对交易签名 公钥前20字节作为账户地址 签名机制是整个区块链的基石 1.2 ECDSA 签名过程 以太坊使用 ECDSA (椭圆曲线数字签名算法): 输入参数:私钥 private_key 和消息哈希 message_hash 输出结果: v 、 r 、 s 三个参数 最终签名: r(32字节) + s(32字节) + v(1字节) 拼接成65字节的 signature 2. 合约中的签名验证 2.1 签名恢复方法 合约中常用的两种签名恢复方法: 方法一: ecrecover (EVM预编译函数) 方法二: ECDSA.recover (OpenZeppelin封装) 2.2 两种方法的区别 OpenZeppelin的 ECDSA.recover : 对 ecrecover 进行了封装 增加了预先检查 防止ECDSA签名延展性攻击 提供更丰富的功能支持 但需注意紧凑型签名攻击风险 3. 常见签名验证场景 3.1 授权特权操作 通过验证单个用户签名 允许调用者执行特权操作 3.2 用户份额累加 通过验证多个用户签名 累加用户份额(类似投票机制) 4. ECDSA 签名漏洞类型及实例 4.1 签名重复性检验不完善 漏洞特征 : 仅检查相邻签名是否相同 允许不同用户交替重复签名 实例分析 : 攻击方式 : 两个不同用户交替重复签名 可无限累加份额 修复方案 : 应记录所有已用签名 或记录消息哈希而非签名 4.2 ECDSA签名延展性攻击 漏洞原理 : 对于同一 r ,存在两个不同的 s 满足要求 可根据一个 s 计算另一个: s' = n - s (n为曲线阶数) 实例分析 : 修复方案 : 使用 OpenZeppelin 的 ECDSA.recover 该函数内部会检查并拒绝延展性签名 4.3 紧凑型签名攻击 背景知识 : ERC-2098 引入紧凑型签名 签名长度从65字节缩短到64字节 初衷是优化EVM空间使用 漏洞特征 : 合约仅存储原始签名进行重复性检查 攻击者可生成紧凑型等效签名绕过检查 修复方案 : 应存储和检查消息哈希 ( digest ) 而非签名 修改 nullifiers 存储结构: 4.4 不遵守规范的无限签名 漏洞原理 : RFC-6979 规定应使用确定性 k 进行签名 但 ecrecover 不检查 k 是否符合标准 攻击者可生成无数个有效签名 防护措施 : 严格检查消息哈希的唯一性 绝不依赖签名本身的唯一性 5. 最佳实践总结 优先使用 OpenZeppelin 的 ECDSA 库 : 提供更安全的封装 防止常见攻击 正确的重复性检查 : 消息格式标准化 : 使用以太坊签名标准格式 包含 \x19Ethereum Signed Message:\n 前缀 签名验证完整流程 : 警惕签名变体 : 考虑所有可能的签名格式 包括标准签名和紧凑型签名 6. 高级防护技巧 添加 nonce 机制 : 时间限制签名 : 上下文绑定 : 在签名消息中包含合约地址 防止跨合约重放攻击 7. 测试建议 测试用例应包括 : 标准签名验证 紧凑型签名验证 签名重放尝试 延展性签名尝试 过期签名验证 使用多样化工具测试 : 8. 总结 Solidity 中的 ECDSA 签名验证看似简单,实则暗藏多种安全隐患。开发者必须: 深入理解 ECDSA 算法特性 使用经过验证的安全库 实施全面的重复性检查 考虑所有可能的签名变体 设计完整的签名生命周期管理 遵循这些原则,才能确保基于签名的智能合约功能既安全又可靠。