如何用 Solidity 验证数字签名

最近在写 Ethereum 上的智能合约,有一个功能是在智能合约上验证数字签名。本来这是一个很简单的问题,两三行代码就能搞定,但是因为 Ethereum 的文档残缺不全,这个问题很容易造成迷惑。

1. 签名生成

我们可以直接调用 Web3.js 提供的接口生成。目前,Web3.js 主要有两个版本:0.x 和 1.x。1.x 的主要优势是各个接口都返回 Promise,这样可以很方便的 async/await。

0.x 的签名接口和 1.x 的签名接口除了 address 和 dataToSign 对调了位置以外,基本上是一样的。其中,dataToSign 必须是形如“0x121a3b3456”的十六进制字符串。

然后,我们就能得到签名,签名也是一个十六进制字符串,由三个部分组成,两个 256 比特整数,一个 8 比特整数。分别记作 r, s, v。

示例代码(1.x)如下:

let rawsig = await web3.eth.sign(hexdata, account);
let sig = {
    r: "0x" + rawsig.substring(2, 66),
    s: "0x" + rawsig.substring(66, 130),
    v: web3.utils.toDecimal("0x" + rawsig.substring(130, 132))
};

到这里一切安好。

2. 签名验证

然后,我们需要在 solidity 合约上验证这个签名。solidity 提供了一个接口,叫做 ecrecovery。这个合约并不是直接验证签名,而是通过签名和待签名的数据,反算出签名的地址,然后比较这个反算出的地址和签名用的地址是否一样,如果一样,则说明签名有效。

对 v 的取值,ecrecovery 只接受 27/28 这两个数。而上面 web3 所生成的签名,v 的值是 0 和 1。因此,在调用 ecrecovery 之前,需要把 v 加上 27。官方文档中并没有提到这个问题,只记载在一个 StackOverflow 答案上 。

另外,如果直接使用,编译器仍然会报错。因为 ecrecovery 的第一个参数不是 data,而是 hash。这是因为签名算法能签名的长度是有限的,为了能签名无限长的数据,就需要用哈希“压缩”一下待签名的数据。

现在问题来了,我们不知道 web3 的签名函数是怎么进行这个哈希过程的。官方文档也没有给出任何提示。我想过调用第三方的 secp256k1 签名库,自己写哈希过程,但是出于安全考虑, web3.js 无法获得私钥。经过一番搜索,我才确定了这个签名函数用的是 keccack256 哈希。而且不仅如此,Ethereum 还在待签名的数据前加了一个前缀:

"\x19Ethereum Signed Message:\n32"

这个字符串末尾的 32 是待签名数据的长度,如果待签名数据的长度是 40,那么就应该写成:

"\x19Ethereum Signed Message:\n40"

最后,验证签名应该是这样的:

// In Web3.js (1.x)
await Contract.verify(data, sig.r, sig.s, sigv);
// In Solidity 0.4.x
bytes memory hashPrefix = "\x19Ethereum Signed Message:\n40"; //注意末尾的数字需要修改
bytes32 sigHash =  keccak256(hashPrefix, data);
address addr = ecrecover(sigHash, v+27, r, s);
// In Solidity 0.5.x
bytes memory hashPrefix = "\x19Ethereum Signed Message:\n40"; //注意末尾的数字需要修改
bytes32 sigHash =  keccak256(abi.encodePacked(hashPrefix, data));
address addr = ecrecover(sigHash, v+27, r, s);

©️ 2017-2019 奈卜拉

go back