Alert Source Discuss
Standards Track: ERC

ERC-1271: 合约的标准签名验证方法

当账户是智能合约时,验证签名的标准方法

Authors Francisco Giordano (@frangio), Matt Condon (@shrugs), Philippe Castonguay (@PhABC), Amir Bandeali (@abandeali1), Jorge Izquierdo (@izqui), Bertrand Masius (@catageek)
Created 2018-07-25

摘要

外部拥有账户(EOA)可以使用其关联的私钥对消息进行签名,但目前合约无法做到这一点。我们提出了一种标准方法,供任何合约验证代表给定合约的签名是否有效。这可以通过在签名合约上实现一个 isValidSignature(hash, signature) 函数来实现,该函数可以被调用来验证签名。

动机

现在以及将来会有许多合约想要使用签名消息来验证转移资产或其他目的的权利。为了使这些合约能够支持非外部拥有账户(即,合约所有者),我们需要一种标准机制,合约可以通过该机制表明给定的签名对其是否有效。

需要提供签名的应用程序的一个例子是具有链下订单簿的去中心化交易所,其中买/卖订单是签名消息。在这些应用程序中,EOA 对订单进行签名,表明他们希望买/卖给定的资产,并明确允许交易所智能合约通过签名完成交易。然而,当涉及到合约时,由于合约不拥有私钥,因此无法进行常规签名,因此提出了这个提案。

规范

本文档中的关键词“必须”,“不得”,“必需”,“应”,“不应”,“建议”,“可以”和“可选”应按照 RFC 2119 中的描述进行解释。

pragma solidity ^0.5.0;

contract ERC1271 {

  // bytes4(keccak256("isValidSignature(bytes32,bytes)")
  bytes4 constant internal MAGICVALUE = 0x1626ba7e;

  /**
   * @dev 应该返回提供的签名对于提供的哈希是否有效
   * @param _hash      要签名的数据的哈希值
   * @param _signature 与 _hash 关联的签名字节数组
   *
   * 当函数通过时,必须返回 bytes4 magic value 0x1626ba7e。
   * 必须不修改状态 (对于 solc < 0.5 使用 STATICCALL,对于 solc > 0.5 使用 view modifier)
   * 必须允许外部调用
   */ 
  function isValidSignature(
    bytes32 _hash, 
    bytes memory _signature)
    public
    view 
    returns (bytes4 magicValue);
}

isValidSignature 可以调用任意方法来验证给定的签名,这可能取决于上下文(例如,基于时间或基于状态),取决于EOA(例如,智能钱包内的签名者授权级别),取决于签名方案(例如,ECDSA,多重签名,BLS)等。

希望对消息进行签名的合约(例如,智能合约钱包,DAO,多重签名钱包等)应实现此函数。想要支持合约签名的应用程序,如果签名者是合约,则应调用此方法。

原理

我们认为所提出的函数的名称是合适的,考虑到 授权的 签名者为给定的数据提供正确的签名,他们的签名将被签名合约视为“有效”。因此,只有当签名者被授权代表智能钱包执行给定的操作时,签名的操作消息才有效。

为了简化从签名中分离出被签名的哈希值,提供了两个参数。为了简化起见,使用 bytes32 哈希值代替未哈希的消息,因为合约可能期望某种非标准的哈希函数,例如 EIP-712

isValidSignature() 不应能够修改状态,以防止 GasToken 铸造或类似的攻击媒介。同样,这是为了简化函数的实现表面,以实现更好的标准化并允许链下合约查询。

期望返回特定的返回值而不是布尔值,以便对签名进行更严格和更简单的验证。

向后兼容性

此 EIP 与先前关于签名验证的工作向后兼容,因为此方法特定于基于合约的签名,而不是 EOA 签名。

参考实现

签名合约的示例实现:


  /**
   * @notice 验证签名者是否为签名合约的所有者。
   */
  function isValidSignature(
    bytes32 _hash,
    bytes calldata _signature
  ) external override view returns (bytes4) {
    // Validate signatures
    if (recoverSigner(_hash, _signature) == owner) {
      return 0x1626ba7e;
    } else {
      return 0xffffffff;
    }
  }

 /**
   * @notice 恢复哈希的签名者,假设它是一个 EOA 账户
   * @dev 仅适用于 EthSign 签名
   * @param _hash       已签名消息的哈希值
   * @param _signature  签名编码为 (bytes32 r, bytes32 s, uint8 v)
   */
  function recoverSigner(
    bytes32 _hash,
    bytes memory _signature
  ) internal pure returns (address signer) {
    require(_signature.length == 65, "SignatureValidator#recoverSigner: invalid signature length");

    // Variables are not scoped in Solidity.
    uint8 v = uint8(_signature[64]);
    bytes32 r = _signature.readBytes32(0);
    bytes32 s = _signature.readBytes32(32);

    // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
    // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
    // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most
    // signatures from current libraries generate a unique signature with an s-value in the lower half order.
    //
    // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
    // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
    // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
    // these malleable signatures as well.
    //
    // Source OpenZeppelin
    // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/cryptography/ECDSA.sol

    if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
      revert("SignatureValidator#recoverSigner: invalid signature 's' value");
    }

    if (v != 27 && v != 28) {
      revert("SignatureValidator#recoverSigner: invalid signature 'v' value");
    }

    // Recover ECDSA signer
    signer = ecrecover(_hash, v, r, s);
    
    // Prevent signer from being 0x0
    require(
      signer != address(0x0),
      "SignatureValidator#recoverSigner: INVALID_SIGNER"
    );

    return signer;
  }

调用外部签名合约上的 isValidSignature() 函数的合约的示例实现;

  function callERC1271isValidSignature(
    address _addr,
    bytes32 _hash,
    bytes calldata _signature
  ) external view {
    bytes4 result = IERC1271Wallet(_addr).isValidSignature(_hash, _signature);
    require(result == 0x1626ba7e, "INVALID_SIGNATURE");
  }

安全注意事项

由于调用 isValidSignature() 函数没有 gas 限制,因此某些实现可能会消耗大量 gas。因此,在外部合约上调用此方法时,不要硬编码发送的 gas 量,因为这可能会阻止某些签名的验证。

另请注意,每个实现此方法的合约都有责任确保传递的签名确实有效,否则可能会出现灾难性的结果。

版权

保留所有版权及相关权利,通过 CC0 放弃。

Citation

Please cite this document as:

Francisco Giordano (@frangio), Matt Condon (@shrugs), Philippe Castonguay (@PhABC), Amir Bandeali (@abandeali1), Jorge Izquierdo (@izqui), Bertrand Masius (@catageek), "ERC-1271: 合约的标准签名验证方法," Ethereum Improvement Proposals, no. 1271, July 2018. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1271.