2

Solidity中的ecrecover的应用

 2 years ago
source link: https://learnblockchain.cn/article/2701
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

有没有想过Solidity中的ecrecover命令到底是怎么回事?

这都是关于签名和密钥的...

Public Key XKCD

什么是ecrecover ?

你可能在Solidity合约中看到过ecrecover,并想知道这到底是什么。那么你遇到了EVM 预编译 ecrecover。预编译是一些提前被编译的智能合约的通用函数,所以Ethereum节点可以有效地运行这个函数。从合约的角度来看,这只是一个像操作码一样的单一命令。

看看下面的代码:

function recoverSignerFromSignature(uint8 v, bytes32 r, bytes32 s, bytes32 hash) external {
    address signer = ecrecover(hash, v, r, s);
    require(signer != address(0), "ECDSA: invalid signature");
}

基本上,大家就是这样使用它,尽管还有更多的内容。不要在生产中实际使用上述代码Patricio Palladino正确地指出了这一点。正确的方法是在本文底部的最后一个例子中。

Dont Understand Meme

那么,这一切意味着什么呢?假设你熟悉公钥密码学的基本概念,这将很容易理解。

你可能知道,每当你向以太坊网络发送一笔交易时,必须用你的私钥签署这笔交易。自然也假设以太坊节点有某种方式来验证签名是正确的。

这种验证签名的功能也同样添加到了智能合约上。有了这个功能,你可以验证更多的东西,而不仅仅是交易签名本身。事实上,你可以将任何数据传递给智能合约,对其进行散列,然后根据数据验证其签名。上面的代码中的签名是v、r和s的组合。

为什么我需要这个?

实际上,之前也有文章讨论了如何使用它的例子。这些例子包括:

从本质上讲,你可以验证一个签名数据,而这些数据不一定来自交易签署者。

我应该使用哪个签名标准?

Standards Meme

首先,我们需要决定签名的类型。虽然这对ecrecover来说这并不重要,但对签名来说,已经有几个标准可以被客户端使用以太坊密钥来签署数据:

  • eth_sign
  • personal_sign
  • EIP-712

eth_sign 是用来签署任意数据。这使得它是最强大的,最简单的(只是签署数据),但也是最危险的。这里的大问题是,你可以让用户签署一个数据,而这个实际上是交易数据。想象一下,你让用户登录到你的服务,但你让他们签署的数据实际上是一个交易,如 "发送5个ETH给攻击者"。交易毕竟只是由字节组成,人们很可能不会检查他们所签署的这串字符的实际含义。看似无害的签名,却成了窃取资金的攻击。所以一般不鼓励直接使用eth_sign。

personal_sign 后来加入来解决这个问题。该方法在任何签名数据前加上"\x19Ethereum Signed Message:\n",这意味着如果有人要签署交易数据,添加的前缀字符串会使其成为无效交易。

对于更复杂的用例,特别是在智能合约中使用时,EIP-712标准被创建。EIP-712标准随着时间的推移而有所改变,但目前MetaMask支持的最后一个版本是signTypedData_v4。或者你可以使用一个特定的库,如eip-712。EIP-712解决的主要问题是确保用户清楚地知道他们在签署什么,为哪个合约地址和网络签署,而且每个签名最多只能使用一次。简而言之,这是通过签署所有需要的配置数据(地址、chain id、版本、数据类型)的哈希值+实际数据本身来实现的。ERC20-Permit 是一个关于如何使用signTypedData_v4的好例子。

所有的函数都可以在与MetaMask交互时使用,见例子。另外,它们也可以使用eth-sig-util

所以回到问题我应该使用哪种签名标准?从合约的角度来看,使用最新的EIP-712标准!eth_sign并不安全,personal_sign主要用于实现用户登录功能。在你的合同中坚持使用EIP-712。

如何实现EIP-712

现在让我们看看如何在Solidity中实现EIP-712。大概的想法是:

  1. 计算一个域的哈希值,该值涵盖了合约地址和 chainId 的配置数据
  2. 计算结构化的数据哈希值
  3. 结合这两个哈希值,并在ecrecover 中使用它。

我个人还建议增加一个nonce和deadline值,以防止重放攻击并确保在特定时间内执行。这些不是EIP-712标准的直接组成部分,但可以很容易地添加。下面你会发现一个例子,如何实现这些,然后加上合约的本身的参数去执行它:

function executeMyFunctionFromSignature(
    uint8 v,
    bytes32 r,
    bytes32 s,
    address owner,
    uint256 myParam,
    uint256 deadline
) external {
    bytes32 eip712DomainHash = keccak256(
        abi.encode(
            keccak256(
                "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
            ),
            keccak256(bytes("MyContractName")),
            keccak256(bytes("1")),
            block.chainid,
            address(this)
        )
    );

    bytes32 hashStruct = keccak256(
        abi.encode(
            keccak256("MyFunction(address owner,uint256 myParam,uint256 nonce,uint256 deadline)"),
            owner,
            myParam,
            nonces[owner],
            deadline
        )
    );

    bytes32 hash = keccak256(abi.encodePacked("\x19\x01", eip712DomainHash, hashStruct));
    address signer = ecrecover(hash, v, r, s);
    require(signer == owner, "MyFunction: invalid signature");
    require(signer != address(0), "ECDSA: invalid signature");

    require(block.timestamp < deadline, "MyFunction: signed transaction expired");
    nonces[owner]++;

    _myFunction(owner, myParam);
}

ecrecover的安全问题+解决方案

ecrecover有几个问题,在上面的代码中没有说明,但你应该注意:

  1. 在某些情况下,ecrecover可以返回一个随机地址,而不是无效签名的0。这一点可以通过结构化数据中加入所有者地址来防止。
  2. 签名是可塑的,这意味着你可能会为同一数据创建第二个同样有效的签名。在我们的案例中,我们没有使用签名数据本身(例如,可以作为一个ID)。
  3. 如果哈希值不是在合约本身中计算的,攻击者可以构建一个看起来有效的哈希值和签名。

在实践中,我再次建议使用Openzeppelin合约。他们的ECDSA实现解决了所有这三个问题,而且他们还有一个EIP-712实现(在我看来还是一个草案,但可以使用)。这不仅更容易使用,而且他们还做了进一步的改进:

  • eip712DomainHash的缓存机制,所以只有在chainId改变时才会计算(所以通常只计算一次)
  • 如上所述,对签名的额外安全检查
  • 能够以字符串形式发送签名

上面的代码将被简化为:

import "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin-contracts/contracts/utils/cryptography/draft-EIP712.sol";

contract MyContract is EIP712 {
    function executeMyFunctionFromSignature(
        bytes memory signature,
        address owner,
        uint256 myParam,
        uint256 deadline
    ) external {
        bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
            keccak256("MyFunction(address owner,uint256 myParam,uint256 nonce,uint256 deadline)"),
            owner,
            myParam,
            nonces[owner],
            deadline
        )));

        address signer = ECDSA.recover(digest, signature);
        require(signer == owner, "MyFunction: invalid signature");
        require(signer != address(0), "ECDSA: invalid signature");

        require(block.timestamp < deadline, "MyFunction: signed transaction expired");
        nonces[owner]++;

        _myFunction(owner, myParam);
    }
}

这就是目前最新的EIP-721的第四版标准。如果你在其他合约中遇到EIP-712的实现,要注意使用的是哪个版本。

另外,最后也说明一下,调试无效的签名是非常痛苦的,因为任何数值的微小差异都会导致无效的签名,但你不知道哪些数据可能是错误的。因此,如果你遇到无效签名,一定要仔细检查你的所有输入。

另一个有趣的标准是EIP-1271。由于以太坊的智能合约背后没有私钥,所以它们不能创建那些v、r、s签名。但有了这个标准,仍然可以让合约本身创建签名,见我之前的文章的底部。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK