智能合约中的签名验证

在智能合约的开发中,验证签名是非常常用的一种身份验证手段,那么该如何进行签名,又该如何进行签名验证呢?本篇文章将会涉及到一些密码学的知识以及 solidity 部分代码。

开始读文章之前需要了解的两个小知识点

  • encode 和 encodePackage的区别?
    • 两个都是solidity自带的打包方法,不同的是encodePackage打包结果中不会补 0,encode 打包之后会补 0,这意味着,如果使用 encodePackage 打包【“aa”,”bb”】的得到的结果与 【“a”,“abb”】打包之后的结果是一样的。这是我们不想看到的,但是encode 不会出现这种情况。所以在使用时要注意这个点,防止出现漏洞。
  • 非对称加密是什么?
    • 公开密钥密码学(英语:Public-key cryptography)也称非对称式密码学(英语:Asymmetric cryptography)是密码学的一种演算法,它需要两个密钥,一个是公开密钥,另一个是私有密钥;公钥用作加密,私钥则用作解密。使用公钥把明文加密后所得的密文,只能用相对应的私钥才能解密并得到原本的明文,最初用来加密的公钥不能用作解密。由于加密和解密需要两个不同的密钥,故被称为非对称加密。

签名验证四个步骤:

  1. 将消息签名
  2. 将消息进行哈希运算 → hash(message)
  3. 将私钥和经过哈希运算之后的消息进行签名 → sign(hash(message), private key)
  4. 恢复签名 → ecrecover(hash(message), signature)

代码运行结果分析:

Tips:文章结尾有代码

在合约部署完之后开始测试

  • 首先我们需要使用 【getMessageHash】方法对 “secret message”进行第一次 hash运算,接下来我们就可以对这个hash值进行签名了。
第一次 hash运算的结果
第一次 hash运算的结果
  • 打开浏览器的控制台按【F12】
    • 在控制台中输入 ethereum.enable();
    • 结果如图所示,想要用这个命令,你的浏览器必须装了 metamask 的插件
看钱包地址
看钱包地址
  • 然后我们定义两个变量 【account 和 hash 并对其赋值】
    • account:就是我们上一步看到的地址
    • hash:就是我们上一步通过 hash 运算得到的结果
定义变量
定义变量
  • 接下来我们就可以对 hash 签名,运行命令如下:

    ethereum.request({method: "personal_sign", params: [account, hash]})

    当我们运行这个命令后 metamask 会弹出签名请求。

签名请求
签名请求
  • 签名后的结果如图所示
签名后的结果
签名后的结果
  • 接下来验证签名是否正确
    • 首先将第一次运行的结果放到 【getEthSignedMessageHash】中得到第二次hash运算结果
  • 然后使用recover 恢复签名获取签名的地址
    • 将第二次计算的得到的hash值和浏览器控制台中签名得到的结果放到recover 中,最终会获取到签名的地址结果,我们用这个结果去和之前在浏览器中的 account 值比较。
    • 发现地址一样,签名验证通过。

以上是把一个签名到验证的过程分解,相当于拆解了验证「verify」方法,那么接下来演示一下完整的「verify」是如何运行的。

  • 我们需要将签名人的地址,签名信息,以及浏览器中计算之后的结果传进去。然后通过【verify】比对最终是否通过验证。

示例代码

// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

/*
    1. 如何对一个消息进行签名
    2. 对签名结果进行数据恢复
    3. 验证签名和签名人是否是同一个人

*/
contract VerifySig {
    /*验证函数 
        _signer: 签名人的地址
        _message: 消息的原文
        _sig:签名的结果
    */
    function verify(address _signer, string memory _message, bytes memory _sig) external pure returns (bool)
    {
        //给输入消息进行 hash 运算
        bytes32 messageHash = getMessageHash(_message);
        //再把消息结果进行 eth 消息运算
        bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);

        //判断 hash 运算之后的结果和 传进来的hash值是否是同一个地址
        return recover(ethSignedMessageHash, _sig) == _signer;
    }

    function getMessageHash(string memory _message) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(_message));
    }
    //进行两次hash 运算是因为在今天的数学界,进行一次hash运算的结果已经有可能被破解
    function getEthSignedMessageHash(bytes32 _messageHash) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(
            "\x19Ethereum Signed Message:\n32",
            _messageHash
        ));
    }

    function recover(bytes32 _ethSignedMessageHash, bytes memory _sig) 
    public pure returns (address)
    {
        //r,s,v 属于非对称加密算法里的知识,文章中有扩展
        (bytes32 r, bytes32 s, uint8 v) = _split(_sig);
        //ecrecover 是solidity自带函数,会反回还原之后的地址
        return ecrecover(_ethSignedMessageHash, v, r, s);
    }

    //分割 r,s,v变量
    function _split(bytes memory _sig) internal pure returns (bytes32 r, bytes32 s, uint8 v)
    {
        require(_sig.length == 65, "invalid signature length");
        //签名bytes是用rsv三个元素拼接的,所以第一个r占前32位,第二个s占后32位,最后一个v占最后一位
        assembly {
            r :=mload(add(_sig, 32))
            s :=mload(add(_sig, 64))
            v :=byte(0, mload(add(_sig, 96)))
        }
    }
}

在成为科学家的道路上一起成长,在赚钱的道路上并肩前行,你好我是Jack_Zhang.

Subscribe to Jack_Zhang
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.