首页 > 元宇宙 > 硬核 | 技术详解密码学签名在以太坊上应用
以太坊爱好者  

硬核 | 技术详解密码学签名在以太坊上应用

摘要:密码学签名是区块链关键技术之一,不仅可以用来发送交易,还可以与去中心化交易所、多签合约和其它智能合约交互。原文标题:《干货 | 以太坊上的数字签名》 撰文:Ma

密码学签名是区块链关键技术之一,不仅可以用来发送交易,还可以与去中心化交易所、多签合约和其它智能合约交互。

原文标题:《干货 | 以太坊上的数字签名》
撰文:Maarten Zuidhoorn
翻译 & 校对:闵敏 & 阿剑

密码学签名是区块链的关键技术之一,可以在不暴露私钥的前提下证明地址的所有权。该技术主要用来签署交易(当然也可以用来签署其他任意消息)。本文会讲解数字签名技术在以太坊协议中的用法。

干货 | 以太坊上的数字签名

什么是密码学签名?

当我们讨论密码学中的签名时,我们其实是在讨论所有权、有效性和完整性证明。举例来说,这些签名可以用来:

  • 证明你拥有地址的私钥(即认证功能);
  • 确保信息(例如,邮件)没有被篡改;
  • 验证你下载的 MyCrypto 版本是合法的。

密码学签名是基于数学公式的。我们拥有一个输入消息、一个私钥和一个(通常情况下是秘密的)随机数,就可以得到一串数字作为输出值,也就是签名。使用另一个数学公式可以进行反向计算,在不知道私钥和随机数的情况下进行验证(译者注:即验证该签名是否出自跟某个公钥对应的私钥)。这类算法有很多,如 RSA 和 AES,但是以太坊(和比特币)采用的都是椭圆曲线数字签名算法(ECDSA)。请注意,ECDSA 只是签名算法。与 RSA 和 AES 不同,这种算法不能用于加密。

干货 | 以太坊上的数字签名椭圆曲线的例子之一。以太坊采用的是 SECP256k1 曲线

通过椭圆曲线点乘算法(elliptic curve point manipulation),我们可以使用私钥计算出一个不可逆向计算的值(译者注:即 「公钥」,公钥无法逆向计算出私钥)。这样一来,我们就可以创建出安全且不可篡改的签名。能够生成不可逆向计算的值的函数叫做 「陷门函数(trapdoor function)」:

陷门函数指的是在一个方向上易于计算,但是在缺少特殊信息(即,陷门)的情况下很难反向计算的函数。

使用 ECDSA 签名并验证

ECDSA 签名由两个数字(整数)组成:rs。以太坊还引入了额外的变量 v(恢复标识符)。签名可以表示成 {r, s, v}

在创建签名时,你要先准备好一条待签署的消息,和用来签署该消息的私钥(d?)。简化后的签名流程如下:

  1. 对待签署消息进行哈希计算,得到哈希值(e)。
  2. 生成一个安全的随机数 k
  3. k 乘以椭圆曲线的常量 G,来计算椭圆曲线上的点(x?, y?)。
  4. 计算 r = x? mod n。如果 r 等于 0,请返回步骤 2 。
  5. 计算 s = k?1(e + rd?) mod n。如果 s 等于 0,请返回步骤 2。

在以太坊上,通常使用 Keccak256("\x19Ethereum Signed Message:\n32" + Keccak256(message)) 来计算哈希值。这样可以确保该签名不能在以太坊之外使用。

由于 k 是随机值,我们每次得到的签名都不一样。如果 k 的随机程度不够高,或者随机值被泄漏,就有可能使用两个不同的签名计算出私钥【「fault attack」】。但是,如果你在 MyCrypto 内签署同一条消息,每次得到的输出值都相同,那么如何确保其安全性?这些确定性签名均采用 RFC 6979 标准。该标准描述了如何基于私钥和消息(或哈希值)来生成安全的 k 值。

{r, s, v} 签名可以组成一个长达 65 字节的序列:r 有 32 个字节,s 有 32 个字节,v 有一个字节。如果我们将该签名编码成一个十六进制的字符串,我们最后会得到一个 130 个字符长的字符串。大多数钱包和界面都会使用这个字符串。以 MyCrypto 为例,一个完整的签名如下图所示:

{
  "address": "0x76e01859d6cf4a8637350bdb81e3cef71e29b7c2",
  "msg": "Hello world!",
  "sig": "0x21fbf0696d5e0aa2ef41a2b4ffb623bcaf070461d61cf7251c74161f82fec3a4370854bc0a34b3ab487c1bc021cd318c734c51ae29374f2beb0e6f2dd49b4bf41c",
  "version": "2"
}

在 MyCrypto 的 「验证消息(Verify Message)」 一页中,我们可以使用该签名,并看到该消息是由 0x76e01859d6cf4a8637350bdb81e3cef71e29b7c2 签署的。

干货 | 以太坊上的数字签名MyCrypto 上的签名验证通过。点击此处,即可体验

你可能会问:为什么要将 addressmsgversion 等其它信息也包括在内?不能只验证签名本身吗?好吧,不能。如果不保留其它信息,就好像签了一个合同,然后删除了合同里的所有信息,只留下当事人的签名。不同于交易签名(我们之后会作更深入解释),消息签名就只是签名而已(译者注:因此只有签名是没法验证的)。

为了验证消息,我们需要掌握原始消息、使用私钥签署消息的地址,以及 {r, s, v} 签名本身。版本号就是 MyCrypto 使用的某个版本号。旧版本的 MyCrypto 通常会加上消息的当前日期和时间,计算其哈希值,然后按照上述步骤签署该消息。后来又进行了更改,以符合 JSON-RPC 方法 personal_sign 方法,因此需要指明版本号(「2」)。

简化后的公钥恢复流程如下:

  • 计算消息的哈希值(e)。
  • 计算椭圆曲线上的点 R = (x?, y?),其中 x? 是 rv = 27),或 r + nv = 28)。
  • 计算 u? = -zr?1 mod nu? = sr?1 mod n
  • 计算点 Q? = (x?, y?) = u? × G + u? × R

Q? 是地址用来签名的私钥所对应的公钥。我们可以通过公钥计算出一个地址,并检查该地址是否与已提供地址相符。如果相符,则签名有效。

恢复标识符(「v」)

v 是签名的最后一个字节,而且不是 27 (0x1b) 就是 28 (0x1c)。恢复标识符非常重要,因为我们使用的是椭圆曲线算法,仅凭 rs 可计算出曲线上的多个点,因此会恢复出两个不同的公钥(及其对应地址)。v 会告诉我们应该使用这些点中的哪一个。

在大多数实现中,v 在内部只是 0 或 1,而 27 是在签署比特币消息时加上的任意数。以太坊也接受了这一点。

从 EIP-155 开始,我们还使用链 ID 来计算 v 值。这可以防止跨链重放攻击:以太坊上签署的交易无法在以太坊经典上使用,反之亦然。目前,恢复标识符只用来签署交易而非消息。

签署交易

目前为止,我们主要讨论了针对消息的签名。就像消息一样,交易在发送前也需要签名。如果你使用 Ledger 和 Trezor 之类的硬件钱包,签名过程会在硬件内部发生。如果使用私钥(或 keysotre 文件、助记词),可以直接在 MyCrypto 上完成签名。签署交易所使用的方法与签署消息非常相似,只不过交易的编码方式略有不同。

要签署的交易先用 RLP 编码方式编码,包含了所有交易参数(nonce、gas price、gas limit、to、value、data)和签名(v, r, s)。签过名的交易如下所示:

0xf86c0a8502540be400825208944bbeeb066ed09b7aed07bf39eee0460dfa261520880de0b6b3a7640000801ca0f3ae52c1ef3300f44df0bcfd1341c232ed6134672b16e35699ae3f5fe2493379a023d23d2955a239dd6f61c4e8b2678d174356ff424eac53da53e17706c43ef871

如果我们在 MyCrypto 的已签名交易广播页面上输入该交易,我们就会看到所有交易参数:

干货 | 以太坊上的数字签名MyCrypto 的已签名交易广播页面上的交易参数概览

签过名的交易的第一组字节包含 RLP 编码后的交易参数,最后一组字节包含签名 {r, s, v}。我们可以通过以下方式对签名交易进行编码:

  • 交易参数:RLP(nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0)
  • 使用 Keccak256 算法来计算经过 RLP 编码的未签署交易的哈希值。
  • 按照上文讲述的步骤,通过 ECDSA 算法,使用私钥签署哈希值。
  • 对已签名的交易进行编码:RLP(nonce, gasPrice, gasLimit, to, value, data, v, r, s)

将经过 RLP 编码的交易数据解码后,我们又可以得到原始交易参数和签名。

请注意,链 ID 是被编码到签名的 v 参数中的,因此我们不会将链 ID 本身包含在最终的签名交易数据中。我们也不会提供任何发送方地址,因为地址可以通过签名恢复。这就是以太坊网络内部用来验证交易的方式。

签名消息的标准化

关于如何为签 名消息定义标准结构,人们提出了很多种提议。目前为止,还没有一个提议最终确定下来。最初由 Geth 实现的 personal_sign 格式依然是最常见的。尽管如此,有一些提议非常有趣。

我先来简单介绍下目前创建签名所采用的方式:

    "\x19Ethereum Signed Message:\n" + length(message) + message

消息通常会预先进行哈希计算,因此长度会固定在 32 个字节:

    "\x19Ethereum Signed Message:\n32" + Keccak256(message)

完整的消息(包括前缀)会再经历一次哈希计算,然后用私钥对哈希值签名。这种方式适用于所有权证明,但是在其它情况下可能会出现问题。例如,如果用户 A 签署了一个消息并将其发送给合约 x,用户 B 可以复制这个已签署消息并发送给合约 Y。这就叫重放攻击。有一些提案旨在解决这一问题,如 EIP 191 和 EIP 721。

EIP 191:签名数据标准

EIP 191 是一个很简单的提案:它定义了版本号和版本专有数据。格式如下所示:

 0x19 <1 byte version>  

顾名思义,版本专有数据(version specific data)取决于我们所使用的版本。目前,EIP 191 有三个版本:

  • 0x00:带有 「目标验证者(intended validator)」 的数据。如果是合约,可以是合约地址。
  • 0x01:结构化数据,如 EIP-712 中定义的那样。关于这点,之后会给出详细解释。
  • 0x45:常规的签过名的消息,如 personal_sign 的当前行为。

如果我们指定目标验证者(如,合约地址),该合约可以使用自己的地址来重新计算哈希值。将已签署消息提交到不同的合约实例是行不通的,因为后者无法验证签名。

由于 0x19 已经被选为固定的字节前缀,签名消息无法成为经过 RLP 编码的签名交易,因为后者永远不会以 0x19 开头。

EIP 712:基于以太坊的类型化结构化数据哈希和签名

请不要将 EIP 712 与非同质化代币标准 ERC 721 搞混了。EIP 712 是一个关于 「类型化」 已签署数据的提案。通过人类可读的方式将数据呈现出来,这样可以降低数据的验证难度。

干货 | 以太坊上的数字签名通过 MetaMask 签署消息。左边是旧版已签署消息界面(使用的是 personal_sign,右边是新版界面(使用的是 EIP-712)

EIP-712 定义了一种新的方法来代替 personal_signeth_signTypedData(最新版用的是 eth_signTypedData_v4)。如果使用这种方法,我们必须指定所有属性(例如,toamountnonce)及其各自的类型(如,addressuint256uint256),还有该应用的一些基本信息,称为域(domain)。

域包含应用名称、版本、链 ID、你正在交互的合约和盐值(salt)等信息。合约应该验证这些信息,从而确保同一个签名不能在不同的应用上使用。这样可以解决上文提到的重放攻击问题。

上图所示消息的具体定义如下:

 {
  types: {
    EIP712Domain: [
      { name: 'name', type: 'string' },
      { name: 'version', type: 'string' },
      { name: 'chainId', type: 'uint256' },
      { name: 'verifyingContract', type: 'address' },
      { name: 'salt', type: 'bytes32' }
    ],
    Transaction: [
      { name: 'to', type: 'address' },
      { name: 'amount', type: 'uint256' },
      { name: 'nonce', type: 'uint256' }
    ]
  },
  domain: {
    name: 'MyCrypto',
    version: '1.0.0',
    chainId: 1,
    verifyingContract: '0x098D8b363933D742476DDd594c4A5a5F1a62326a',
    salt: '0x76e22a8ee58573472b9d7b73c41ee29160bc2759195434c1bc1201ae4769afd7'
  },
  primaryType: 'Transaction',
  message: {
    to: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520',
    amount: 1000000,
    nonce: 0
  }
}

如你所见,这个消息在 MetaMask 上是可见的,我们可以确认我们正在签署的消息就是我们想要执行的。EIP 712 实行 EIP 191,因此数据将以 0x1901 开头:0x19 是前缀,0x01 是版本字节,表示这是一个 EIP 712 签名。

通过 Solidity,我们可以为 Transaction 类型定义一个 struct,并编写一个函数来对交易进行哈希计算:

struct Transaction {
  address payable to;
  uint256 amount;
  uint256 nonce;
}

function hashTransaction(Transaction calldata transaction) public view returns (bytes32) {
  return keccak256(
    abi.encodePacked(
      byte(0x19),
      byte(0x01),
      DOMAIN_SEPARATOR,
      TRANSACTION_TYPE,
      keccak256(
        abi.encode(
          transaction.to,
          transaction.amount,
          transaction.nonce
        )
      )
    )
  );
}

上述交易的数据如下所示:

0x1901fb502c9363785a728bf2d9a150ff634e6c6eda4a88196262e490b191d5067cceee82daae26b730caeb3f79c5c62cd998926589b40140538f456915af319370899015d824eda913cd3bfc2991811b955516332ff2ef14fe0da1b3bc4c0f424929

上述数据由 EIP-191 字节、哈希域分隔符、哈希后的 Transaction 类型和 Transaction 输入组成。该数据会再经过一次哈希计算,并进行签署。然后,我们可以使用 ecrecover 来验证智能合约中的签名:

function verify (address signer, Transaction calldata transaction, bytes32 r, bytes32 s, uint8 v) public returns (bool) {
  return signer == ecrecover(hashTransaction(transaction), v, r, s);
}   

在下一节中,我们将详细解释 ecrecover。如果你想找一个简单的 JavaScript 或 TypeScript 代码库来来实现 EIP 712,请查看 这个库。

如果你想详细了解如何在智能合约中实现 EIP 712,我建议你阅读 MetaMask 的这篇文章。遗憾的是,EIP 712 规范目前还是草案,还没有得到很多应用的支持。目前,Ledger 和 Trezor 都还没支持 EIP 712,可能会阻碍该规范的广泛采用。不过,Ledger 表示他们即将发布的更新版会支持 EIP 712。

通过智能合约来验证签名

消息签名更有趣的地方在于,我们可以使用智能合约来验证 ECDSA 签名。Solidity 有一个内置函数叫做 ecrecover(这实际上是地址 0x1 上的预编译合约),可以恢复用来签署消息的私钥的地址。一个(非常)基本的合约实现如下所示:

// SPDX-License-Identifier: MIT
pragma solidity 0.7.0;

contract SignatureVerifier {
  /**
   * @notice Recovers the address for an ECDSA signature and message hash, note that the hash is automatically prefixed with "\x19Ethereum Signed Message:\n32"
   * @return address The address that was used to sign the message
   */
  function recoverAddress (bytes32 hash, uint8 v, bytes32 r, bytes32 s) public pure returns (address) {
    bytes memory prefix = "\x19Ethereum Signed Message:\n32";
    bytes32 prefixedHash = keccak256(abi.encodePacked(prefix, hash));

    return ecrecover(prefixedHash, v, r, s);
  }

  /**
   * @notice Checks if the recovered address from an ECDSA signature is equal to the address `signer` provided.
   * @return valid Whether the provided address matches with the signature
   */
  function isValid (address signer, bytes32 hash, uint8 v, bytes32 r, bytes32 s) external pure returns (bool) {
    return recoverAddress(hash, v, r, s) == signer;
  }
}

该合约仅用于验证签名,本身没有任何用处,因为签名验证也可以在没有智能合约的情况下完成。

这种方式的用处在于,用户可以通过免信任方式向智能合约发送某些指令,而无需发送交易。例如,用户可以签署一条消息:「请从我的地址向该地址发送 1 个以太币。」 智能合约可以使用 EIP-712 和 / 或 EIP-1077 标准来验证签名者并执行该指令。智能合约中的签名验证可用于以下应用:

  • 多签合约(如 Gnosis Safe);
  • 去中心化交易所;
  • 元交易和 gas 中继者(如 Gas Station Network)。

但是,如果你想通过正在使用的智能合约钱包签署消息怎么办?我们显然不能让钱包智能合约访问私钥对吧。ERC 1271 提议了一个标准,可以让智能合约验证其它智能合约的签名。其规范非常简单:

pragma solidity ^0.7.0;

contract ERC1271 {
  bytes4 constant internal MAGICVALUE = 0x1626ba7e;

  function isValidSignature(
    bytes32_hash, 
    bytes memory_signature
  ) public view returns (bytes4 magicValue);
}

合约必须实现 isValidSignature 函数,该函数可以像上述合约那样运行任意函数。如果签名确实是与合约对应的,则函数返回 MAGICVALUE。这样一来,只要是实现了 ERC 1271 的合约,任何合约都可以验证其签名。从内部来说,实现 ERC 1271 的合约可以让多名用户签署同一个消息(例如,在多签合约的情况下),并将哈希值存储在内部。然后,该合约可以验证提供给 isValidSignature 函数的哈希值是否在内部签署,且签名是否对合约所有者之一有效。

总结

对于区块链和去中心化来说,签名非常重要。签名不仅可以用来发送交易,还可以用来与去中心化交易所、多签合约和其它智能合约进行交互。目前还没有明确的消息签名标准,进一步采用 EIP 712 规范有助于生态系统改善用户体验,并为消息签名制定标准。

免责声明:密码学很难。请不要将本文的任何内容作为参考,来实现您自己的密码学函数。虽然我们已经进行了广泛的研究,但是文本所提供的信息可能仍有不准确之处。本文仅用作教育用途。

参考文献和相关文章

  • Ethereum: A Secure Decentralised Genralised Transaction Ledger (Yellowpaper)

  • EIP-155: Simple replay attack protection

  • EIP-191: Signed Data Standard

  • EIP-712: Ethereum typed structured data hashing and signing

  • ERC-1271: Standard Signature Validation Method for Contracts

  • RFC6979: Deterministic Usage of the Digital Signature Algorithm (DSA) and Elliptic Curve Digital Signature Algorithm (ECDSA)

免责声明
世链财经作为开放的信息发布平台,所有资讯仅代表作者个人观点,与世链财经无关。如文章、图片、音频或视频出现侵权、违规及其他不当言论,请提供相关材料,发送到:2785592653@qq.com。
风险提示:本站所提供的资讯不代表任何投资暗示。投资有风险,入市须谨慎。
世链粉丝群:提供最新热点新闻,空投糖果、红包等福利,微信:juu3644。