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

in ethereum •  6 years ago  (edited)

前面研究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

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Hello! Your post has been resteemed and upvoted by @ilovecoding because we love coding! Keep up good work! Consider upvoting this comment to support the @ilovecoding and increase your future rewards! ^_^ Steem On!

Reply !stop to disable the comment. Thanks!