前言
📌靶场刷题遇得到很多关于验证签名的题,在这里汇总一下,消息签名的工具和方法。
1. 采用 web3.py
1.1 适用于本地测试
这是不符合当前 以太坊 规定的签名,即未加入\x19Ethereum Signed Message:\n32
,
适合用于平时在本地复现靶场的简单使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from eth_account import Account from web3 import Web3
message = "" privatekey = "" messagehash = Web3.keccak(text=message) signMessage = Account.signHash(message_hash=messagehash, private_key=privatekey)
print("message =", message) print("message's hash =",messagehash.hex()) print("v =", Web3.to_hex(signMessage.v)) print("r =", Web3.to_hex(signMessage.r)) print("s =", Web3.to_hex(signMessage.s)) print("signature =", Web3.to_hex(signMessage.signature))
|
1.2 适用于测试网
如下是遵循EIP191
协议的签名规则的代码,即加入\x19Ethereum Signed Message:\n32
。
如下这两种方法和metamask的签名结果一样。
但是如下这里个并不是按照如下的计算方式:
1 2
| bytes memory prefix = "\x19Ethereum Signed Message:\n32"; bytes32 result = keccak256(abi.encodePacked(prefix, hash));
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| from web3.auto import w3 from eth_account.messages import encode_defunct
msg = "" private_key = "" message = encode_defunct(text=msg) signed_message = w3.eth.account.sign_message(message, private_key=private_key) print("message =", msg) print("messageHash =", w3.to_hex(signed_message.messageHash)) print("r =", w3.to_hex(signed_message.r)) print("s =", w3.to_hex(signed_message.s)) print("v =", w3.to_hex(signed_message.v)) print("signature =", w3.to_hex(signed_message.signature))
|
或
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| from web3 import Web3, HTTPProvider from eth_account.messages import encode_defunct
private_key = "" rpc = 'https://rpc.ankr.com/eth' w3 = Web3(HTTPProvider(rpc))
msg = ""
message = encode_defunct(text=msg)
signed_message = w3.eth.account.sign_message(message, private_key=private_key)
print("msg =", msg) print("msgHash =", w3.to_hex(signed_message.messageHash)) print("r =", w3.to_hex(signed_message.r)) print("s =", w3.to_hex(signed_message.s)) print("v =", w3.to_hex(signed_message.v)) print("signature = ", w3.to_hex(signed_message.signature))
|
代码结果运行图:

metamask签名结果图:

2. 采用web3.js
web3.js的版本为:"version": "1.8.0"
这个方法就很牛皮了:
- 如果输入的data是string类型的:那么ta的运算结果和metamask的结果一样

- 如果输入的data是hash:那么ta的处理方式就是如下
1 2
| bytes memory prefix = "\x19Ethereum Signed Message:\n32"; bytes32 result = keccak256(abi.encodePacked(prefix, hash));
|
remix代码结果:

web3js代码结果:

代码:
1 2 3 4 5 6 7 8 9
| var Web3 = require('web3'); var web3 = new Web3(Web3.givenProvider);
let dataHash = ""; let privateKey = "" let sign = web3.eth.accounts.sign(dataHash, privateKey);
console.log(sign);
|
3. 采用 ethers.js
版本为:^6.2.3。
该脚本的签名结果和方式和metamask也是一样,遵循EIP191,不会像web3js那样,输入为hash时,不遵守
1 2
| bytes memory prefix = "\x19Ethereum Signed Message:\n32"; bytes32 result = keccak256(abi.encodePacked(prefix, hash));
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { ethers } from "ethers";
const RPC = "";
const provider = new ethers.JsonRpcProvider(RPC);
const privateKey = ""; const wallet = new ethers.Wallet(privateKey, provider);
const message = ""; const messageHash = ethers.hashMessage(message);
const signature = await wallet.signMessage(message); console.log(`message = ${messageHash}`); console.log(`signatrue = ${signature}`);
|
4. 采用ethereum.js
这是未遵循EIP191
协议的签名方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| const ethereumjsUtil = require('ethereumjs-util');
const message = 'stage1';
const privateKey = Buffer.from('0000000000000000000000000000000000000000000000000000000000000001', 'hex');
let buffer_message = Buffer.from(message); const messageHash = ethereumjsUtil.keccak256(buffer_message);
const signature = ethereumjsUtil.ecsign(messageHash, privateKey);
const formattedSignature = { v: signature.v, r: signature.r.toString('hex'), s: signature.s.toString('hex') };
console.log('Message:', message); console.log('Message Hash:', messageHash.toString('hex')); console.log('Signature:', formattedSignature);
|
5. 签名和消息还原地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol";
contract SignMessage {
using ECDSA for bytes32;
function verifyMessage(string memory message, bytes memory signature) public view returns(address, bool) { //hash the plain text message bytes32 messagehash = keccak256(bytes(message)); address signeraddress = messagehash.recover(signature); if (msg.sender==signeraddress) { //The message is authentic return (signeraddress, true); } else { //msg.sender didnt sign this message. return (signeraddress, false); } } }
|
或
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
contract SignMessage {
// @dev 从_msgHash和签名_signature中恢复signer地址 function recoverSigner(bytes32 _msgHash, bytes memory _signature) public pure returns (address){ // 检查签名长度,65是标准r,s,v签名的长度 require(_signature.length == 65, "invalid signature length"); bytes32 r; bytes32 s; uint8 v; // 目前只能用assembly (内联汇编)来从签名中获得r,s,v的值 assembly { /* 前32 bytes存储签名的长度 (动态数组存储规则) add(sig, 32) = sig的指针 + 32 等效为略过signature的前32 bytes mload(p) 载入从内存地址p起始的接下来32 bytes数据 */ // 读取长度数据后的32 bytes r := mload(add(_signature, 0x20)) // 读取之后的32 bytes s := mload(add(_signature, 0x40)) // 读取最后一个byte v := byte(0, mload(add(_signature, 0x60))) } // 使用ecrecover(全局函数):利用 msgHash 和 r,s,v 恢复 signer 地址 return ecrecover(_msgHash, v, r, s); } }
|
6. 实现同一私钥,同一消息,不同签名
6.1通过ethereum.js
实现
这需要修改ethereum.js
的源码,版本为:"version": "7.1.5"
。
修改一:node_modules\@types\secp256k1\index.d.ts
1 2
| noncefn?: ((message: Uint8Array, privateKey: Uint8Array, algo: Uint8Array | null, data: Uint8Array | null, attempt: number) => Uint8Array)
|
这里可以看到data的值被写死了,被默认写成null
,所以导致了生成的 options的值是new Uint8Array(0),这就影响了生成的签名是唯一的”错觉”。如何将这个默认值给去掉,在调用的时候传入随机的options(通过生成随机的Uint8Array
数组实现),所以将这里的代码修改为
1 2
| noncefn?: ((message: Uint8Array, privateKey: Uint8Array, algo: Uint8Array | null, data: Uint8Array, attempt: number) => Uint8Array) | undefined;
|
修改二:node_modules\ethereumjs-util\dist\signature.js
1 2
| const { signature, recid: recovery } = (0, secp256k1_1.ecdsaSign)(msgHash, privateKey);
|

可以看到这里没有传入option,即使用了源码的默认option(null),因为上一步修改了option的默认值,所以这里可以传参了,可以引入crypto
库,随机生成Uint8Array
数组,这样就可以随机生成签名,所以将这里的代码修改为:
1 2
| const crypto = require('crypto'); const { signature, recid: recovery } = (0, secp256k1_1.ecdsaSign)(msgHash, privateKey, {data:crypto.randomBytes(32)});
|
在 Node.js 中,
crypto.randomBytes() 方法是一个常见的随机数生成方法
。
综上,经过两次修改便可以实现使用同一私钥,对同一消息,进行签名可以得到不同的签名值
举例:
签名脚本,使用私钥1
,对消息stage1
进行签名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const ethereumjsUtil = require('ethereumjs-util');
const message = 'stage1';
const privateKey = Buffer.from('0000000000000000000000000000000000000000000000000000000000000001', 'hex');
let buffer_message = Buffer.from(message); const messageHash = ethereumjsUtil.keccak256(buffer_message);
const signature = ethereumjsUtil.ecsign(messageHash, privateKey);
const formattedSignature = { v: signature.v, r: signature.r.toString('hex'), s: signature.s.toString('hex') };
console.log('Message:', message); console.log('Message Hash:', messageHash.toString('hex')); console.log('Signature:', formattedSignature);
|
运行结果:

可以从结果中看到,生成的签名值不一样,到合约中验证:
1 2 3 4 5 6 7 8 9 10
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
contract Verify { // private key = 0x1 function verify(uint8 v, bytes32 r, bytes32 s) public { require(ecrecover(keccak256("stage1"), v, r, s) == 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, "who are you?"); } }
|
运行结果:

从结果中可以看到,生成的这些签名都可以通过验证。
6.2 通过加密库实现
这种方法不能准确获取signature,因为最后的v无法确定,只能猜测,但是v只能是 0x1b 或者 0x1c所以猜对的可能性为0.5,但是也可以实现生成不同签名的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| const secp256k1 = require('secp256k1'); const { randomBytes } = require('crypto');
function batchSign(numSignatures, PKey, MessageHash) { const privateKey = Buffer.from(PKey, 'hex'); const messageHash = Buffer.from(MessageHash, 'hex'); for (let i = 0; i < numSignatures; i++) { const { signature } = secp256k1.ecdsaSign(messageHash, privateKey, { data: randomBytes(32) }); signatureBytes = Buffer.from(signature) signatureHex = signatureBytes.toString('hex'); const r = signatureHex.slice(0, 64); const s = signatureHex.slice(64); console.log(`Signature ${i + 1}:`); console.log("Signature:",`0x${signatureHex}`); console.log('Signature (r):', `0x${r}`); console.log('Signature (s):', `0x${s}`); console.log('Signature (s):',"1b or 1c"); console.log('----------------------'); } }
batchSign(5, "0000000000000000000000000000000000000000000000000000000000000001", "8252a7072c69c0cdba0c0bc059898f7992314306b3f0845bbb76593da6b98311")
|