以太坊签名验签原理全面解析 您所在的位置:网站首页 钱包私钥碰撞器 以太坊签名验签原理全面解析

以太坊签名验签原理全面解析

2023-12-22 13:04| 来源: 网络整理| 查看: 265

目录

签名三大作用

签名基础

不可逆计算

陷门函数

ECDSA 签名过程

ECDSA 验签过程

恢复标识符(“v”)

签署交易

关键词解读

EIP 191

EIP 712

ERC 1271

RLP编码

Keccak256

Fault attack

重放攻击

RFC 6979 标准

web3.eth.accounts.recover验签

参考

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

1.证明你拥有地址的私钥(即认证功能); 2.确保信息(例如,邮件)没有被篡改; 3.验证你下载的 文件是合法有效的。

签名基础

原理:基于数学公式

输入:一个输入消息、一个私钥和一个(通常情况下是秘密的)随机数,就可以得到一串数字作为输出值,也就是签名。

输出:使用另一个数学公式可以进行反向计算,在不知道私钥和随机数的情况下进行验证(译者注:即验证该签名是否出自跟某个公钥对应的私钥)。

这类算法有很多,如 RSA 和 AES,但是以太坊(和比特币)采用的都是椭圆曲线数字签名算法(ECDSA)。请注意,ECDSA 只是签名算法。与 RSA 和 AES 不同,这种算法不能用于加密。以太坊采用的是 secp256k1曲线。

签名方案由哈希算法和签名算法组成。以太坊选择的签名算法是secp256k1,哈希算法选择了keccak256,这是一个从字节串。

不可逆计算 通过椭圆曲线点乘算法(elliptic curve point manipulation),我们可以使用私钥计算出一个不可逆向计算的值(译者注:即 “公钥”,公钥无法逆向计算出私钥)。这样一来,我们就可以创建出安全且不可篡改的签名。

陷门函数 能够生成不可逆向计算的值的函数叫做 “陷门函数(trapdoor function)”。

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

ECDSA 签名过程 ECDSA 签名由两个数字(整数)组成:r 和 s。以太坊还引入了额外的变量 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(e + rd) mod n。如果 s 等于 0,请返回步骤 2。

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

以太坊具体关键代码如下:

func signHash(data []byte) []byte { msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) return crypto.Keccak256([]byte(msg)) }

由于 k 是随机值,我们每次得到的签名都不一样。如果 k 的随机程度不够高,或者随机值被泄漏,就有可能使用两个不同的签名计算出私钥,进而导致著名的Fault attack。但是,如果你在 MyCrypto 内签署同一条消息,每次得到的输出值都相同,那么如何确保其安全性?这些确定性签名均采用 RFC 6979 标准。

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

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

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

为什么要将 address、msg 和 version 等其它信息也包括在内?不能只验证签名本身吗?不能。如果不保留其它信息,就好像签了一个合同,然后删除了合同里的所有信息,只留下当事人的签名。因为只有签名是没法验证的。下个章节会有讲到?

ECDSA 验签过程 为了验证消息,我们需要掌握原始消息、使用私钥签署消息的地址,以及 {r, s, v} 签名本身。版本号就是使用的某个版本号。以符合 JSON-RPC 方法personal_sign 方法,因此需要指明版本号(“2”)。

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

1.计算消息的哈希值(e)。 2.计算椭圆曲线上的点 R = (x, y),其中 x 是 r(v = 27),或 r + n(v = 28)。 3.计算 u = -zr mod n 和 u = sr mod n。 4.计算点 Q = (x, y) = u × G + u × R。

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

恢复标识符(“v”) v 是签名的最后一个字节,而且不是 27 (0x1b) 就是 28 (0x1c)。恢复标识符非常重要,因为我们使用的是椭圆曲线算法,仅凭r 和 s 可计算出曲线上的多个点,因此会恢复出两个不同的公钥(及其对应地址)。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

签过名的交易的第一组字节包含 RLP 编码后的交易参数,最后一组字节包含签名 {r, s, v}。

我们可以通过以下方式对签名交易进行编码:

1.交易参数:RLP(nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0)。 2.使用 Keccak256 算法来计算经过 RLP 编码的未签署交易的哈希值。 3.按照上文讲述的步骤,通过 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)

完整的消息(包括前缀)会再经历一次哈希计算,然后用私钥对哈希值签名。这种方式适用于所有权证明,但是在其它情况下可能会出现问题。

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

0x19

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

1.0x00:带有 “目标验证者(intended validator)” 的数据。如果是合约,可以是合约地址。 2.0x01:结构化数据,如 EIP-712 中定义的那样。 3.0x45:常规的签过名的消息,如 personal_sign 的当前行为。如果我们指定目标验证者(如,合约地址),该合约可以使用自己的地址来重新计算哈希值。将已签署消息提交到不同的合约实例是行不通的,因为后者无法验证签名。

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

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

通过 MetaMask 签署消息。左边是旧版已签署消息界面(使用的是 personal_sign,右边是新版界面(使用的是 EIP-712)。

EIP-712 定义了一种新的方法来代替 personal_sign:eth_signTypedData(最新版用的是 eth_signTypedData_v4)。如果使用这种方法,我们必须指定所有属性(例如,to、amount 和 nonce)及其各自的类型(如,address、uint256 和 uint256),还有该应用的一些基本信息,称为域(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); }

ERC 1271 如果你想通过正在使用的智能合约钱包签署消息怎么办?我们显然不能让钱包智能合约访问私钥对吧。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 函数的哈希值是否在内部签署,且签名是否对合约所有者之一有效。

RLP编码 RLP(Recursive Length Prefix,递归长度前缀)是一种序列化编码算法,用于编码任意的嵌套结构的二进制数据。RLP序列化方法因为简单、短小等诸多优点,现如今已经成为以太坊中数据序列化/反序列化的主要方法,区块、交易等数据结构在持久化时会先经过RLP编码后再存储到数据库中。

Keccak256 keccak256算法则可以将任意长度的输入压缩成64位16进制的数,且哈希碰撞的概率近乎为0。

Fault attack ECDSA签名算法的安全性是比较依赖于安全的随机数生成算法的,如果随机数算法存在问题,使用了相同的k进行签名,那么攻击者是可以根据签名信息恢复私钥的Fault attack,历史上也出过几次这样的事故,比如10年索尼的PS3私钥遭破解以及12年受java某随机数生成库的影响造成的比特币被盗事件,关于这部分内容我也写过相关的分析,可以参见利用随机数冲突的ECDSA签名恢复以太坊私钥,所以说ECDSA签名在设计上还是存在一些问题的, 这也激励了新的EdDSA算法的出现。

重放攻击 如果用户 A 签署了一个消息并将其发送给合约 x,用户 B 可以复制这个已签署消息并发送给合约 Y。这就叫重放攻击。有一些提案旨在解决这一问题,如 EIP 191 和 EIP 721。

RFC 6979 标准 该标准描述了如何基于私钥和消息(或哈希值)来生成安全的 k 值。

随机系列谈的童鞋现在应该明白k值对于比特币的重要性(不仅仅是比特币,对于整个椭圆曲线家族来说都是如此)了吧?暴漏k值(签名)相当于暴漏私钥,因此: k值必须是保密且唯一的,并不一定必须随机!

由于历史上发生过太多次伪随机数失败案例,有人想出了一种用“确定性”方式来产生k值的方法,同样保证了“保密”且“唯一”,最后成为一个编号为6979的规范,即:RFC6979。

为了确定性的产生保密且唯一的k值,我们先试着写出这么一个简单的公式:

k = SHA256(d + HASH(m));

其中,d是私钥,m是消息,我们一般会对消息的HASH进行签名,因此这里是HASH(m)。

好了,满足我们的需求其实只需要这么个简单的公式就足够了,因为参数里有私钥d,就保证了“保密”,再加上消息m,保证了“唯一”,这也是“确定性”的算法,只要SHA256是安全的,此算法就是安全的,很完美。

如果仅仅是针对比特币而言,这个公式已经很好了,但考虑到RFC6979面向的是密码学(不仅仅是比特币)的统一规范,要考虑更多的复杂情况(更多曲线、更多参数、更多算法等),因此,实际上的RFC6979要比上述公式复杂得多,代码实现起来也要多得多。

算法可以复杂,代码可以很长,但原理都一样,要用私钥来保证“保密”,要用消息来保证“唯一”,再使用确定的、不可逆的方法来进行运算,最终计算出来的k值就是安全的。

RFC6979算法的完整实现,Java语言可参考bitherj项目所依赖的SpongyCastle中HMacDSAKCalculator类,Objective-C语言可参考bitheri项目。

web3.eth.accounts.recover验签 recover

web3.eth.accounts.recover(signatureObject); web3.eth.accounts.recover(message, signature [, preFixed]); web3.eth.accounts.recover(message, v, r, s [, preFixed]);

Recovers the Ethereum address which was used to sign the given data.

Parameters 1.message|signatureObject - String|Object: Either signed message or hash, or the signature object as following values:

messageHash - String: The hash of the given message already prefixed with “\x19Ethereum Signed Message:\n” + message.length + message.r - String: First 32 bytes of the signatures - String: Next 32 bytes of the signaturev - String: Recovery value + 27

2.signature - String: The raw RLP encoded signature, OR parameter 2-4 as v, r, s values.

3.preFixed - Boolean (optional, default: false): If the last parameter is true, the given message will NOT automatically be prefixed with “\x19Ethereum Signed Message:\n” + message.length + message, and assumed to be already prefixed.

Returns

String: The Ethereum address used to sign this data.

Example

web3.eth.accounts.recover({ messageHash: '0x1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655', v: '0x1c', r: '0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd', s: '0x6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029' }) > "0x2c7536E3605D9C16a7a3D7b1898e529396a65c23" // message, signature web3.eth.accounts.recover('Some data', '0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c'); > "0x2c7536E3605D9C16a7a3D7b1898e529396a65c23" // message, v, r, s web3.eth.accounts.recover('Some data', '0x1c', '0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd', '0x6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029'); > "0x2c7536E3605D9C16a7a3D7b1898e529396a65c23"

参考 https://www.anquanke.com/post/id/167018

http://tools.ietf.org/html/rfc6979

https://github.com/Mrtenz/eip-712

https://github.com/bither/

https://web3js.readthedocs.io/en/v1.2.11/web3-eth-accounts.html#recover



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有