在以太坊及其构建的去中心化应用(DApp)生态中,签名验证是一项至关重要的安全机制,它确保了只有私钥的持有者才能授权特定的交易或操作,从而保障了用户资产的安全和交易的不可否认性,本文将详细介绍以太坊中验证签名的核心原理、常用方法以及实际应用中的注意事项。
签名验证的核心:椭圆曲线密码学(ECDSA)
以太坊的签名验证基础是椭圆曲线数字签名算法(ECDSA),这个过程包含三个关键角色:
- 私钥(Private Key):由用户随机生成并严格保密,用于对交易或消息进行签名。
- 公钥(Public Key):由私钥通过椭圆曲线算法派生得出,可以公开,用于验证签名的有效性。
- 地址(Address):由公钥进一步通过哈希算法(如Keccak-256)生成,是用户在以太坊网络中的身份标识。
签名验证的核心思想是:给定一个消息、一个签名和一个公钥,如何验证这个签名确实是由与该公钥对应的私钥针对该消息生成的? 如果验证通过,则意味着签名者拥有该私钥,并且消息在签名后未被篡改。
以太坊签名验证的具体步骤
在以太坊中,无论是交易签名还是个人签名(如EIP-712消息签名),验证过程都遵循相似的基本步骤:
-
获取原始消息(Message/Transaction Data):
- 对于交易,这通常是交易的所有字段(nonce, gas price, gas limit, to, value, data等)按照特定顺序和编码方式(RLP)序列化后的原始数据。
- 对于EIP-712结构化签名,这通常是经过特定编码(如
"\x19Ethereum Signed Message:\n" + message.length + message)或EIP-712规范定义的encodeData结果。
-
获取签名(Signature):
- 签名通常是一个65字节的数组,由三个部分组成:
r(32字节)、s(32字节)和v(1字节)。v用于恢复公钥的y坐标奇偶性。
- 签名通常是一个65字节的数组,由三个部分组成:
-
从签名中恢复公钥(Recover Public Key):
- 这是验证过程的核心步骤之一,利用ECDSA的数学特性,可以从消息的哈希值、签名(r, s, v)中恢复出可能的公钥,由于椭圆曲线的对称性,通常会得到两个可能的公钥,但
v值用于确定唯一正确的那个。 - 以太坊提供了
ecrecover预编译合约来实现这一功能,大多数以太坊开发库(如web3.js, ethers.js)都封装了这一底层操作。
- 这是验证过程的核心步骤之一,利用ECDSA的数学特性,可以从消息的哈希值、签名(r, s, v)中恢复出可能的公钥,由于椭圆曲线的对称性,通常会得到两个可能的公钥,但
-
从恢复的公钥生成地址:
将上一步恢复出的公钥进行Keccak-256哈希,然后取后20字节作为地址。
-
比较地址:
- 将生成的地址与签名者声称的地址(或交易中的
from地址)进行比较。 - 如果两者一致,则签名验证通过;否则,验证失败。
- 将生成的地址与签名者声称的地址(或交易中的
常用的以太坊签名验证方法
在实际开发中,我们通常不会直接调用ecrecover,而是使用成熟的库来简化操作,以下是几种主流的方法:
使用Web3.js (v1.x)
Web3.js是以太坊较早的JavaScript库,提供了签名验证功能。
const Web3 = require('web3');
const web3 = new Web3();
// 假设我们有以下数据
const message = "Hello, Ethereum!";
const signature = "0x..."; // 65字节的签名
const expectedAddress = "0x..."; // 签名者的地址
// 1. 对消息进行以太坊签名消息格式的处理
const messageHash = web3.utils.sha3("\x19Ethereum Signed Message:\n" + message.length + message);
// 或者对于EIP-712,使用特定编码
// 2. 使用ecrecover恢复地址
const recoveredAddress = web3.eth.accounts.recover(messageHash, signature);
// 3. 比较地址
console.log("Recovered Address:", recoveredAddress);
console.log("Expected Address:", expectedAddress);
if (recoveredAddress.toLowerCase() === expectedAddress.toLowerCase()) {
console.log("Signature is valid!");
} else {
console.log("Signature is invalid!");
}
使用Ethers.js (推荐)
Ethers.js是当前更推荐使用的现代、轻量级以太坊库,其API设计更友好,安全性也更高。
const { ethers } = require("ethers");
// 假设我们有以下数据
const message = "Hello, Ethereum!";
const signature = "0x..."; // 65字节的签名
const expectedAddress = "0x..."; // 签名者的地址
// 1. 直接使用ethers.utils.verifyMessage方法
// 注意:ethers会自动处理"以太坊签名消息"的前缀
const recoveredAddress = ethers.utils.verifyMessage(message, signature);
// 2. 比较地址
console.log("Recovered Address:", recoveredAddress);
console.log("Expected Address:", expectedAddress);
if (recoveredAddress.toLowerCase() === expectedAddress.toLowerCase()) {
console.log("Signature is valid!");
} else {
console.log("Signature is invalid!");
}
对于EIP-712结构化签名,Ethers.js提供了verifyTypedData方法:
const domain = {
name: 'Ether Mail',
version: '1',
chainId: 1,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC'
};
const types = {
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet
39;, type: 'address' }
]
};
const value = {
name: 'Alice',
wallet: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'
};
const typedDataSignature = "0x...";
const recoveredAddress = ethers.utils.verifyTypedData(domain, types, value, typedDataSignature);
使用Solidity智能合约内验证
在某些场景下,我们需要在智能合约中验证签名,例如实现一个只有特定地址才能调用的函数,或者允许用户通过签名授权操作。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SignatureVerifier {
function verify(
bytes32 messageHash,
bytes memory signature,
address expectedSigner
) public pure returns (bool) {
// 从签名中恢复地址
address recoveredSigner = _recoverSigner(messageHash, signature);
// 比较地址
return recoveredSigner == expectedSigner;
}
function _recoverSigner(bytes32 messageHash, bytes memory signature)
internal
pure
returns (address)
{
// 检查签名长度是否为65字节
require(signature.length == 65, "Invalid signature length");
// 提取r, s, v
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(signature, 32))
s := mload(add(signature, 64))
v := byte(0, mload(add(signature, 96)))
}
// 调整v值(以太坊中v = 27或28,ecrecover期望v = 0或1)
if (v < 27) {
v += 27;
}
require(v == 27 || v == 28, "Invalid signature v value");
// 调用ecrecover预编译合约
return ecrecover(messageHash, v, r, s);
}
// 辅助函数:对消息进行以太坊签名消息哈希
function getMessageHash(string memory message) public pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", bytes(message).length, message));
}
}
注意事项与最佳实践
- 防止重放攻击(Replay Attacks):原始的签名消息如果不包含防重放机制(如nonce、链ID、特定时间戳等),攻击者可能在其他链或同一链的不同时间点重放该签名,EIP-155通过在
v参数中加入链ID来防止跨链重放。 - 消息哈希的正确性:确保在验证时使用的是与签名时完全相同的消息哈希,对于普通文本消息,记得添加以太坊特定的前缀(
"\x19Ethereum Signed Message:\n"),对于EIP-712,确保domain和types的定义与签名时一致。 - 签名格式的规范性:确保签名是标准的65字节格式