/ 币圈行情

以太坊验证签名方法详解,从原理到实践

发布时间:2026-01-24 18:47:45

在以太坊及其构建的去中心化应用(Dapp生态中,签名验证是一项至关重要的安全机制,它确保了只有私钥的持有者才能授权特定的交易或操作,从而保障了用户资产的安全和交易的不可否认性,本文将详细介绍以太坊中验证签名的核心原理、常用方法以及实际应用中的注意事项。

签名验证的核心:椭圆曲线密码学(ECDSA)

以太坊的签名验证基础是椭圆曲线数字签名算法(ECDSA),这个过程包含三个关键角色:

  1. 私钥(Private Key):由用户随机生成并严格保密,用于对交易或消息进行签名。
  2. 公钥(Public Key):由私钥通过椭圆曲线算法派生得出,可以公开,用于验证签名的有效性。
  3. 地址(Address):由公钥进一步通过哈希算法(如Keccak-256)生成,是用户在以太坊网络中的身份标识。

签名验证的核心思想是:给定一个消息、一个签名和一个公钥,如何验证这个签名确实是由与该公钥对应的私钥针对该消息生成的? 如果验证通过,则意味着签名者拥有该私钥,并且消息在签名后未被篡改。

以太坊签名验证的具体步骤

在以太坊中,无论是交易签名还是个人签名(如EIP-712消息签名),验证过程都遵循相似的基本步骤:

  1. 获取原始消息(Message/Transaction Data)

    • 对于交易,这通常是交易的所有字段(nonce, gas price, gas limit, to, value, data等)按照特定顺序和编码方式(RLP)序列化后的原始数据。
    • 对于EIP-712结构化签名,这通常是经过特定编码(如"\x19Ethereum Signed Message:\n" message.length message)或EIP-712规范定义的encodeData结果。
  2. 获取签名(Signature)

    • 签名通常是一个65字节的数组,由三个部分组成:r(32字节)、s(32字节)和v(1字节)。v用于恢复公钥的y坐标奇偶性。
  3. 从签名中恢复公钥(Recover Public Key)

    • 这是验证过程的核心步骤之一,利用ECDSA的数学特性,可以从消息的哈希值、签名(r, s, v)中恢复出可能的公钥,由于椭圆曲线的对称性,通常会得到两个可能的公钥,但v值用于确定唯一正确的那个。
    • 以太坊提供了ecrecover预编译合约来实现这一功能,大多数以太坊开发库(如web3.js, ethers.js)都封装了这一底层操作。
  4. 从恢复的公钥生成地址

    将上一步恢复出的公钥进行Keccak-256哈希,然后取后20字节作为地址。

  5. 比较地址

    • 将生成的地址与签名者声称的地址(或交易中的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', 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));
    }
}

注意事项与最佳实践

  1. 防止重放攻击(Replay Attacks):原始的签名消息如果不包含防重放机制(如nonce、链ID、特定时间戳等),攻击者可能在其他链或同一链的不同时间点重放该签名,EIP-155通过在v参数中加入链ID来防止跨链重放。
  2. 消息哈希的正确性:确保在验证时使用的是与签名时完全相同的消息哈希,对于普通文本消息,记得添加以太坊特定的前缀("\x19Ethereum Signed Message:\n"),对于EIP-712,确保domain和types的定义与签名时一致。
  3. 签名格式的规范性:确保签名是标准的65字节格式

免责声明:本文为转载,非本网原创内容,不代表本网观点。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。

如有疑问请发送邮件至:bangqikeconnect@gmail.com