老鱼 avatar

以太坊研究系列【签名和验证】

chaimyu

Published: 15 Nov 2018 › Updated: 15 Nov 2018以太坊研究系列【签名和验证】

以太坊研究系列【签名和验证】

前面研究GUSD的Custodian合约时,需要进行离线签名,以前都是对交易进行签名,没有单独对数据进行签名,这次一起来看看怎么对数据签名和验证。

geth签名验证

personal.sign

> a0
"0x54b865714068f5f03574ace39a1f3279c4e83e2c"

> personal.sign("My name is Chaim!", a0, "123456")
Error: invalid argument 0: json: cannot unmarshal hex string without 0x prefix into Go value of type hexutil.Bytes

可以看到sign不能直接签名字符串,需要签名0x开头的数据,需要先把数据进行hash

> web3.sha3("My name is Chaim!")
"0xc891c508f8b48c9a9b15b417180549195d8a5911b592b68e14ae61d277c015c8"

> personal.sign("0xc891c508f8b48c9a9b15b417180549195d8a5911b592b68e14ae61d277c015c8", a0, "123456")
"0xbe5f614e12201b96c3020a1fcdcf998215efb4861cc66b0163b8c295890c46a074eda3dc68bc06a1eca7bc3cf2461b8a1c5cfd9ca9f4a1ef8b84d574eebe3d091c"

personal.ecRecover

> personal.ecRecover("0xc891c508f8b48c9a9b15b417180549195d8a5911b592b68e14ae61d277c015c8", "0xbe5f614e12201b96c3020a1fcdcf998215efb4861cc66b0163b8c295890c46a074eda3dc68bc06a1eca7bc3cf2461b8a1c5cfd9ca9f4a1ef8b84d574eebe3d091c")
"0x54b865714068f5f03574ace39a1f3279c4e83e2c"

> personal.ecRecover("0xd891c508f8b48c9a9b15b417180549195d8a5911b592b68e14ae61d277c015c8", "0xbe5f614e12201b96c3020a1fcdcf998215efb4861cc66b0163b8c295890c46a074eda3dc68bc06a1eca7bc3cf2461b8a1c5cfd9ca9f4a1ef8b84d574eebe3d091c")
"0xde8f08dda362e8373b98dea069c905ae853ce632"

从上面看到,如果签名正确可得到私钥对应的地址,如果签名不对(0xc改成0xd)也能得到一个地址,但是地址是错误的。


web3js签名验证

js签名

const Web3 = require("web3");

var web3 = new Web3(new Web3.providers.WebsocketProvider('ws://127.0.0.1:8546'));
web3.eth.getAccounts(function(error, result){
    var a0 = result[0];
    console.log("account:" + a0);
    var sha3Msg = web3.utils.sha3("My name is Chaim!");
    console.log("sha3:" + sha3Msg);

    var signedData = web3.eth.sign(sha3Msg, a0).then(resp => {
        console.log("signed::" + resp);
    });
});

实际上在web3js中不要求要签名的数据是编码的,直接可以签名字符串数据。

执行结果:

Chaim:web3eth Chaim$ node sign.js
account:0x54b865714068f5F03574ACe39a1F3279C4E83E2c
sha3:0xc891c508f8b48c9a9b15b417180549195d8a5911b592b68e14ae61d277c015c8
signed::0xbe5f614e12201b96c3020a1fcdcf998215efb4861cc66b0163b8c295890c46a074eda3dc68bc06a1eca7bc3cf2461b8a1c5cfd9ca9f4a1ef8b84d574eebe3d091c

js验证签名

const Web3 = require("web3");

var web3 = new Web3(new Web3.providers.WebsocketProvider('ws://127.0.0.1:8546'));
web3.eth.personal.ecRecover("0xc891c508f8b48c9a9b15b417180549195d8a5911b592b68e14ae61d277c015c8", "0xbe5f614e12201b96c3020a1fcdcf998215efb4861cc66b0163b8c295890c46a074eda3dc68bc06a1eca7bc3cf2461b8a1c5cfd9ca9f4a1ef8b84d574eebe3d091c").then(resp => {
    console.log("addr:" + resp)});

同样ecRecover传入的带有签名的数据也可以是字符串,使用web3.utils.utf8ToHex()转化为16进制字符串。

执行结果:

addr:0x54b865714068f5f03574ace39a1f3279c4e83e2c

solidity验证

geth生成签名

> sha3msg = web3.sha3("My name is Chaim!")
"0xc891c508f8b48c9a9b15b417180549195d8a5911b592b68e14ae61d277c015c8"
> sig = web3.eth.sign(a0, sha3msg)
"0xbe5f614e12201b96c3020a1fcdcf998215efb4861cc66b0163b8c295890c46a074eda3dc68bc06a1eca7bc3cf2461b8a1c5cfd9ca9f4a1ef8b84d574eebe3d091c"
> r = sig.slice(0, 66)
"0xbe5f614e12201b96c3020a1fcdcf998215efb4861cc66b0163b8c295890c46a0"
> s = '0x' + sig.slice(66, 130)
"0x74eda3dc68bc06a1eca7bc3cf2461b8a1c5cfd9ca9f4a1ef8b84d574eebe3d09"
> v = '0x' + sig.slice(130, 132)
"0x1c"
> v = web3.toDecimal(v)
28

用solidity来验证签名使用ecrevocer()函数,需要传入r、s、v值。

v值應為27 or 28,如果v = 0或1的話,需加上27。

solidity contract验证

pragma solidity ^0.4.21;

contract test {
function verify(bytes32 _message, uint8 _v, bytes32 _r, bytes32 _s) public constant returns (address) {
   bytes memory prefix = "\x19Ethereum Signed Message:\n32";
   bytes32 prefixedHash = sha3(prefix, _message);
   address signer = ecrecover(prefixedHash, _v, _r, _s);
   return signer;
   }
}

Remix上调试,正常返回地址,如下:

发布合约,用上面geth生成的v、r、s和已签名数据调用合约函数:

abi_verify = [...]
addr_verify = "0xedfc0b97e3162063f1e7a9780a3705abca4a9a60"
contract_verify = eth.contract(abi_verify).at(addr_verify)

> contract_verify.verify.call("0xc891c508f8b48c9a9b15b417180549195d8a5911b592b68e14ae61d277c015c8",28,"0xbe5f614e12201b96c3020a1fcdcf998215efb4861cc66b0163b8c295890c46a0","0x74eda3dc68bc06a1eca7bc3cf2461b8a1c5cfd9ca9f4a1ef8b84d574eebe3d09")
"0x54b865714068f5f03574ace39a1f3279c4e83e2c"

调用合约返回了正确的签名地址。

此处注意,如果不加前缀"\x19Ethereum Signed Message:\n32"解出的地址是错误的,原因在这,摘录原文如下:

The sign method calculates an Ethereum specific signature with:
sign(keccack256("\x19Ethereum Signed Message:\n" + len(message) + message))).
By adding a prefix to the message makes the calculated signature recognisable as an Ethereum specific signature. This prevents misuse where a malicious DApp can sign arbitrary data (e.g. transaction) and use the signature to impersonate the victim.

Python库secp256k1签名验证

用geth签名带上了不需要的前缀,得找些工具或代码来做签名工作。

secp256k1这个python库我在python3.6下安装失败了,但在python2.7下是成功的。

签名需要私钥,先想办法把geth中的地址的私钥找出来,方法在这

eb4e675d4adee4fa60cf82a681a7d192d7bd3ece7938e7e63ea06a5259eb0d0c

用私钥签名:

python -m secp256k1 signrec -k eb4e675d4adee4fa60cf82a681a7d192d7bd3ece7938e7e63ea06a5259eb0d0c -m 0xc891c508f8b48c9a9b15b417180549195d8a5911b592b68e14ae61d277c015c8

d39b1a3b363e15691e0ae7120e650bf3c0d632621e1016c9fa0aaf1dc5d38b632d396c9f968dd1ea91867adbd4b2751ba00affd24cd2f1cac0ac1d17919bbb5d 1

从签名中解出公钥:

python -m secp256k1 recpub -s d39b1a3b363e15691e0ae7120e650bf3c0d632621e1016c9fa0aaf1dc5d38b632d396c9f968dd1ea91867adbd4b2751ba00affd24cd2f1cac0ac1d17919bbb5d -i 1 -m 0xc891c508f8b48c9a9b15b417180549195d8a5911b592b68e14ae61d277c015c8

Public key: 0366840aef641387cd9ed6ce575e08720be48d9767a0d1fe0ba398250e47be91f4

更多使用secp256k1见secp256k1-py 安装以及命令行操作


js库签名验证(elliptic)

let elliptic = require('elliptic');
let sha3 = require('js-sha3');
let ec = new elliptic.ec('secp256k1');

// let keyPair = ec.genKeyPair();
let keyPair = ec.keyFromPrivate("eb4e675d4adee4fa60cf82a681a7d192d7bd3ece7938e7e63ea06a5259eb0d0c");
let privKey = keyPair.getPrivate("hex");
let pubKey = keyPair.getPublic();
console.log(`Private key: ${privKey}`);
console.log("Public key :", pubKey.encode("hex").substr(2));
console.log("Public key (compressed):",
    pubKey.encodeCompressed("hex"));

console.log();

let msg = 'My name is Chaim!';
let msgHash = sha3.keccak256(msg);
let signature = ec.sign(msgHash, privKey, "hex", {canonical: true});
console.log(`Msg: ${msg}`);
console.log(`Msg hash: ${msgHash}`);
console.log("Signature:", signature);

console.log();

let hexToDecimal = (x) => ec.keyFromPrivate(x, "hex").getPrivate().toString(10);
let pubKeyRecovered = ec.recoverPubKey(
    hexToDecimal(msgHash), signature, signature.recoveryParam, "hex");
console.log("Recovered pubKey:", pubKeyRecovered.encodeCompressed("hex"));

let validSig = ec.verify(msgHash, signature, pubKeyRecovered);
console.log("Signature valid?", validSig);

执行结果如下:

Chaim:web3eth Chaim$ node secp256k1.js 
Private key: eb4e675d4adee4fa60cf82a681a7d192d7bd3ece7938e7e63ea06a5259eb0d0c
Public key : 66840aef641387cd9ed6ce575e08720be48d9767a0d1fe0ba398250e47be91f45e8b5bb23f5256994a6fb5de254520109b4e633eefb2e7322c106dea589129d1
Public key (compressed): 0366840aef641387cd9ed6ce575e08720be48d9767a0d1fe0ba398250e47be91f4

Msg: My name is Chaim!
Msg hash: c891c508f8b48c9a9b15b417180549195d8a5911b592b68e14ae61d277c015c8
Signature: Signature {
  r: <BN: 506f4440e1185666fd2520143103ea2760a902271f8a113f02a16e14df48d671>,
  s: <BN: 727257238e0d6537ca8bdcc1e9fb98bbccb715d7b0888431a917827d06d29847>,
  recoveryParam: 1 }

Recovered pubKey: 0366840aef641387cd9ed6ce575e08720be48d9767a0d1fe0ba398250e47be91f4
Signature valid? true

重新发布一个不带前缀"\x19Ethereum Signed Message:\n32"的合约,再用合约里方法验证看看:

abi_verify2 = [...]
addr_verify2 = "0xa141e2d6435f6730c7b81b0c5e048d1fac5df6da"
contract_verify2 = eth.contract(abi_verify2).at(addr_verify2)

> contract_verify2.verify.call("0xc891c508f8b48c9a9b15b417180549195d8a5911b592b68e14ae61d277c015c8",28,"0x506f4440e1185666fd2520143103ea2760a902271f8a113f02a16e14df48d671","0x727257238e0d6537ca8bdcc1e9fb98bbccb715d7b0888431a917827d06d29847")
"0x54b865714068f5f03574ace39a1f3279c4e83e2c"

可以看到,解出了正确的地址!


js库签名二(ethereumjs-util)

签名

let ethUtil = require('ethereumjs-util');

let hash2 = new Buffer(msgHash, 'hex');
let prikey2 = new Buffer(privKey, 'hex');
const rsv = ethUtil.ecsign(hash2, prikey2);
console.log(rsv);
console.log("r: 0x" + rsv.r.toString('hex'));
console.log("s: 0x" + rsv.s.toString('hex'));
console.log("v: " + rsv.v);

参考

http://me.tryblockchain.org/web3js-sign-ecrecover-decode.html

https://medium.com/taipei-ethereum-meetup/%E7%94%A8ecrecover%E4%BE%86%E9%A9%97%E7%B0%BD%E5%90%8D-694fa8ae3638

https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign

https://ethereum.stackexchange.com/questions/15364/ecrecover-from-geth-and-web3-eth-sign

https://ethereum.stackexchange.com/questions/49299/how-to-call-this-solidity-function-with-web3js-when-executing-the-contract

http://cw.hubwiz.com/card/c/web3.js-1.0/1/6/4/

https://www.jianshu.com/p/9269acbce4fd

https://solidity.readthedocs.io/en/develop/abi-spec.html

keythereum
https://github.com/ethereumjs/keythereum

椭圆曲线(ECDSA)
https://blog.csdn.net/teaspring/article/details/77834360

https://github.com/emn178/js-sha3

https://gist.github.com/nakov/1dcbe26988e18f7a4d013b65d8803ffc

Leave 以太坊研究系列【签名和验证】 to:

Written by

One day sitting in the well looked at the sky, blowing the wind uphole, jumping out of the well, dancing in the wind!

Read more #ethereum posts


Best Posts From 老鱼

We have not curated any of chaimyu's posts yet. But you can encourage our curation team to review posts by visiting them regularly and by referring other readers. Because we give priority to frequently read content.

More Posts From 老鱼