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 - 输出结果:
v、r、s三个参数 - 最终签名:
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. 最佳实践总结
-
优先使用 OpenZeppelin 的
ECDSA库:- 提供更安全的封装
- 防止常见攻击
-
正确的重复性检查:
// 正确做法 - 检查消息哈希 bytes32 digest = keccak256(abi.encodePacked(message)); require(!usedDigests[digest], "Signature already used"); usedDigests[digest] = true; -
消息格式标准化:
- 使用以太坊签名标准格式
- 包含
\x19Ethereum Signed Message:\n前缀
-
签名验证完整流程:
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; } -
警惕签名变体:
- 考虑所有可能的签名格式
- 包括标准签名和紧凑型签名
6. 高级防护技巧
-
添加 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); } -
时间限制签名:
function verifyWithTime( address signer, bytes32 hash, bytes memory signature, uint256 expiry ) internal { require(block.timestamp <= expiry, "Signature expired"); verify(signer, hash, signature); } -
上下文绑定:
- 在签名消息中包含合约地址
- 防止跨合约重放攻击
7. 测试建议
-
测试用例应包括:
- 标准签名验证
- 紧凑型签名验证
- 签名重放尝试
- 延展性签名尝试
- 过期签名验证
-
使用多样化工具测试:
// 使用不同库生成签名进行测试 const signature1 = web3.eth.sign(message, privateKey); const signature2 = ethers.utils.splitSignature( await wallet.signMessage(message) );
8. 总结
Solidity 中的 ECDSA 签名验证看似简单,实则暗藏多种安全隐患。开发者必须:
- 深入理解 ECDSA 算法特性
- 使用经过验证的安全库
- 实施全面的重复性检查
- 考虑所有可能的签名变体
- 设计完整的签名生命周期管理
遵循这些原则,才能确保基于签名的智能合约功能既安全又可靠。