EIP-712 (一个对结构化数据的哈希标准)

翻译自: https://eips.ethereum.org/EIP...

简易大纲

对数据签名是一个已经被解决的问题若是咱们只关注那些字节字符串。遗憾的是在这个真实的世界里,咱们关心的是那些复杂的、有意义的信息。把结构化数据进行哈希处理不是件小事,错误的话会致使系统丧失安全性。html

所以,谚语“不要推出你本身的加密算法”在这里就适用了。相反,咱们须要使用一个通过同行评审的、通过充分测试的标准。这个EIP旨在成为这个标准。git

摘要

这是一个对结构化数据哈希和签名的标准,而不只仅是字节字符串。它包含:github

  1. 正确编码功能的理想框架
  2. 结构化数据和solidity中的结构体相似而且兼容的详细说明
  3. 这些结构的实例的安全哈希算法
  4. 这些实例能够被安全地包含在一组可签名消息内
  5. 领域分离的可扩展机制
  6. 新的RPC调用:eth_signTypedData
  7. 应用于EVM的优化的哈希算法

动机

这个EIP旨在提升链下消息签名对链上的可用性。咱们能够看到,由于节省gas以及减小链上交易的缘由,采用链下消息签名的需求日益增加。如今已经被签名的消息,展现给用户的是一串难以理解的16进制的字符串,附带一些组成这个消息的项目的上下文。web

eth_sign

这里咱们大体描绘了编码结构化数据,而且在用户签名时把结构化数据展现给他们确认的场景。下面就是当用户签名时,应该展示给他们的符合EIP规范的消息 的例子:算法

structedDataSign

签名以及哈希概要

签名方案由哈希算法和签名算法组成。以太坊选择的签名算法是secp256k1,哈希算法选择了keccak256,这是一个从字节串𝔹^8n^到256位字符串𝔹^256^的函数。json

一个好的哈希算法应该知足安全属性,如肯定性,第二个预映象阻抗和碰撞阻力。 当应用于字节串时, keccak256函数知足了上述条件。若是咱们想将它应用于其余集合,首先咱们须要把这个集合映射到字节串。编码函数的肯定性和单射性至关重要。若是它不知足肯定性的话,那么验证时刻的哈希可能会不一样于签名时刻的哈希,这会致使签名不正确被拒绝。若是它不是单射的,那么在集合中就会有2个不一样的元素哈希完获得相同的值,致使对一个彻底不一样的不相干的消息,签名也一样适用。数组

交易和字节串

在以太坊中,能够找到关于上述破损的解释例子。以太坊有两种消息,交易𝕋和字节串𝔹⁸ⁿ。这些分别用eth_sendTransactioneth_sign来签名。最初的编码函数encode : 𝕋∪𝔹⁸ⁿ→𝔹⁸ⁿ以下定义:安全

  • encode(t : T) = RLP_encode(t)
  • encode(b :  𝔹⁸ⁿ) = b

独立来看的话,它们都知足要求的属性,可是合在一块儿看就不知足了。若是咱们采用b = RLP_encode(t)就会产生碰撞。在Geth PR 2940中,经过修改编码函数的第二条定义,这种状况获得了缓解:bash

  • encode(b : 𝔹⁸ⁿ) = "\x19Ethereum Signed Message:\n" ‖ len(b) ‖ b 其中len(b)b中字节数的ASCII十进制编码。

这就解决了两个定义之间的冲突,由于RLP_encode(t : 𝕋)永远不会以\x19做为开头。但新的编码函数依然存在肯定性和单射性风险,仔细思考这些对咱们颇有帮助。网络

原来,上面的定义并不具备肯定性。对一个4个字节大小的字符串b来讲,用len(b) = "4"或者len(b) = "004"都是有效的。咱们能够进一步要求全部表示长度的十进制编码前面不能有0而且len("")="0"来解决这个问题。

上面的定义并非明显无碰撞阻力的。一个以"\x19Ethereum Signed Message:\n42a…"开头的字节串到底表示一个42字节大小的字符串,仍是一个以"2a"做为开头的字符串?这个问题在 Geth issue #14794中被提出来,也直接促使了trezor不使用这个标准。幸运的是这并无致使真正的碰撞由于编码后的字节串总长度提供了足够的信息来消除这个歧义。

若是忽略了len(b),肯定性和单射性就没有那么重要了。重点是,很难将任意集合映射到字节串,而不会在编码函数中引入安全问题。目前对eth_sign的设计仍然将字节串做为输入,并指望实现者提供一种编码。

任意消息

eth_sign方法会假设消息就是字节串形式的。在实践当中,咱们不会哈希这些字节串,而是这些不一样的dapp𝕄的全部不一样语义的消息。遗憾的是,这个集合并不能正式肯定。因此,咱们用类型化的命名结构集𝕊来近似表示它。这个标准正式肯定了𝕊集合并为它提供了肯定性的、单射性的编码函数。

只是编码结构体仍是不够的。好比两个不一样的dapp使用一样的结构,那么用于其中一个dapp的签名消息一样对另外一个也是有效的。这种签名是兼容的,这多是有意而为的行为,在这种状况下,只要dapps预先把重放攻击(replay attack)考虑进来就没什么问题。若是不预先考虑这些问题,那么就会存在安全问题。

解决这个问题的办法啊就是引入一个域名分隔符,一个256位的数字。这个值和签名混合,而且每一个域名的值都不同。这就让针对不一样域名的签名没法相互兼容。域名分隔符的设计中要包含Dapp的独特信息,好比dapp的名字,预期的验证者合约地址,预期的Dapp域名等。用户和用户代理可使用此信息来减轻钓鱼攻击,若是一个恶意的Dapp试图诱骗用户为另外一个Dapp的消息签名的话。

重放攻击注意点

这个标准只是关于对消息签名和验证签名。在不少实际应用中,已签名的消息被用来受权一个动做,例如token交换。使用者须要确保当应用程序看到两笔如出一辙的已签名消息时依然能够作出正确的行为,这一点十分重要。举个例子,重复的消息须要被拒绝,或者受权的行为应当是幂等的(注:一个幂等操做的特色是其任意屡次执行所产生的影响均与一次执行的影响相同)。至于这是如何实现的,要视特定应用而定,而且超出了本标准的范围。

详细说明

可签名的消息集合由交易和字节串𝕋 ∪ 𝔹⁸ⁿ扩展而来,还包含告终构化数据𝕊。可签名消息集合的最新表示就是`𝕋 ∪ 𝔹⁸ⁿ ∪ 𝕊。他们都被编码成适合哈希和签名的字节串,以下所示:

  • encode(transaction, T) = RLP_encode(transaction)
  • encode(message, 𝔹⁸ⁿ) = "\x19Ethereum Signed Message:\n" ‖ len(message) ‖ message,其中len(message)是message中字节数的非零填充的ascii十进制编码。
  • encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message),其中domainSeparatorhashStruct(message)以下定义。

这种编码知足肯定性,由于单独的组件都知足肯定性。同时编码也是单射的,由于在上面三种状况下,第一个字节永远不同。(RLP_encode(transaction))并不会以\x19做为开始。

这种编码同时也和EIP-191兼容。其中的vertion byte这里就是0x01version specific data这里就是32字节的域名分隔符domainSeparatordata to sign在这里就是hashStruct(message)

类型化的结构数据𝕊的定义

为了定义全部结构化数据的集合,咱们从定义可接受的类型开始。就像ABIv2同样,这些都和solidity的类型紧密相关。用solidity符号来解释定义就是个例证。该标准特别针对EVM,但旨在脱离与更高级别的语言的关联。例如:

struct Mail {
    address from;
    address to;
    string contents;
}

定义:一个struct类型,具备有效的标识符做为名称并包含零个或多个成员变量。成员变量由一个成员类型和一个名称组成。

定义:一个成员类型能够是一个原子类型,动态类型或者引用类型。

定义:原子类型有:bytes1bytes32uint8uint256int8int256booladdress。这些在solidiy中都有相应的定义。注意没有别名uintint;注意合约地址始终是普通的address。该标准也不支持定点数,将来版本中可能会增长新的原子类型。

定义:动态类型有bytesstring。这些在声明时和原子类型同样,可是它们在编码中的处理是不一样的。

定义:引用类型有arrays和structs。arrays能够是固定长度的,也能够是动态长度的,分别用Type[n]Type[]表示。structs是由其名称引用的其余结构体。该标准支持嵌套的struct。

定义:结构化的类型数据𝕊的集合包含全部struct类型的实例。

hashStruct的定义

hashStruct方法以下定义:

  • hashStruct(s : 𝕊) = keccak256(typeHash ‖ encodeData(s)) ,其中 typeHash = keccak256(encodeType(typeOf(s)))

注意typeHash对于给定结构类型来讲是一个常量,并不须要运行时再计算。

encodeType的定义

一个结构的类型用name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")"来编码,其中每一个成员(member)都用type ‖ " " ‖ name来表示。举个例子,上面的Mail结构体,就用Mail(address from,address to,string contents)来编码。

若是结构类型引用其余的结构体类型(而且这些结构类型又引用更多的结构类型),那么就会收集被引用的的结构类型集合,按名称排序并附加到编码中。一个编码的例子就是,Transaction(Person from,Person to,Asset tx)Asset(address token,uint256 amount)Person(address wallet,string name)

encodeData的定义

一个结构体实例的编码:enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ),也就是说,成员值的编码按照他们在类型中出现的顺序链接在一块儿,每一个编码后的成员值长度是肯定的32字节。

原子类型的值按照以下方法编码:

  • 布尔值falsevalue都分别编码成uint256类型的0或者1
  • 地址都编码成uint160类型
  • 整数(Integer)类型值都符号扩展成256位,并按大端顺序编码。
  • bytes1bytes31是从索引0开始到索引length - 1的数组,它们从自身结束到bytes32的位置都用0填充,而且按照从开始到结束的顺序编码。这对应了她们在ABI v1和v2中的编码。
  • 动态值bytesstring用他们内容的哈希值来编码。(哈希用keccak256方法)
  • 数组值的编码则是把其内容的encodedData链接起来,再对总体进行keccak256。(例如,对someType[5]进行编码,和对包含5个类型为someType的成员的结构体进行编码,是彻底同样的)。
  • 结构体值被递归编码成hashStruct(value),对于循环数据不能采用这种定义。

domainSeparator的定义

domainSeparator = hashStruct(eip712Domain)

其中eip712Domain的类型是一个名为EIP712Domain的结构体,并带有一个或多个如下字段。协议设计者只须要包含对其签名域名有意义的字段,未使用的字段不在结构体类型中。

  • string name:用户可读的签名域名的名称。例如Dapp的名称或者协议。
  • string version:签名域名的目前主版本。不一样版本的签名不兼容。
  • uint256 chainIdEIP-155中的链id。用户代理应当拒绝签名若是和目前的活跃链不匹配的话。
  • address verifyContract:验证签名的合约地址。用户代理能够作合约特定的网络钓鱼预防。
  • bytes32 salt:对协议消除歧义的加盐。这能够被用来作域名分隔符的最后的手段。

此标准的将来扩展能够添加具备新用户代理行为约束的新字段。用户代理能够自由使用提供的信息来通知/警告用户或者直接拒绝签名。

eth_signTypedData JSON RPC的详细说明

eth_signTypedData方法已经添加进了Ethereum JSON-RPC中。这个方法与`eth_sign类似。

eth_signTypedData

这个签名方法用sign(keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))计算一个以太坊特定的签名。

经过给消息加上前缀,能够将计算出的签名识别为以太坊特定的签名。这能够防止恶意DApp签署任意数据(例如交易),并使用签名来冒充受害者的状况。

注意:用来签名的地址必须解锁。

参数

  1. Address - 20字节 - 对消息签名的帐户地址
  2. TypedData - 须要被签名的类型化的结构数据。

类型化的数据是一个JSON对象,它包含类型信息,域名分割参数和消息对象。如下是一个TypedData参数的JSON-schema定义:

{
  type: 'object',
  properties: {
    types: {
      type: 'object',
      additionalProperties: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            name: {type: 'string'},
            type: {type: 'string'}
          },
          required: ['name', 'type']
        }
      }
    },
    primaryType: {type: 'string'},
    domain: {type: 'object'},
    message: {type: 'object'}
  }
}

返回值

DATA:签名。就像在eth_sign里同样,它是一个以0x开头的16进制的129字节数组。它以大端模式编码了rsv参数(黄皮书附录F)。字节0…64包含了参数r,字节64…128是参数s,最后一个字节是参数v。注意到参数v包含了链id,这在EIP-155有详细说明。

示例

请求
curl -X POST --data '{"jsonrpc":"2.0","method":"eth_signTypedData","params":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", {"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Person":[{"name":"name","type":"string"},{"name":"wallet","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person"},{"name":"contents","type":"string"}]},"primaryType":"Mail","domain":{"name":"Ether Mail","version":"1","chainId":1,"verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"from":{"name":"Cow","wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},"contents":"Hello, Bob!"}}],"id":1}'
结果
{
  "id":1,
  "jsonrpc": "2.0",
  "result": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c"
}

关于如何使用solidity中的ecrecover方法来验证用eth_signTypedData得出的签名的例子,能够在EIP712 Example.js中找到。这个合约就被部署在Ropsten和Rinkeby测试网络上。

personal_signTypedData

一样还有一个对应的personal_signTypedData方法,这个方法接受帐户的密码做为最后一个参数。

Web3 API的详细说明

Web3 version 1中新加了两个方法,和web3.eth.sign以及web3.eth.personal.sign相似。

web3.eth.signTypedData
web3.eth.signTypedData(typedData, address [, callback])

使用特定的帐户对类型化的数据签名。这个帐户须要解锁。

参数

  1. Object - 域名分割和须要签名的类型化数据。根据以上在eth_signTypedData JSON RPC调用中指定的JSON-schema进行结构化。
  2. String|Number - 用来签名数据的地址。或者是本地钱包的地址或索引:ref:web3.eth.accounts.wallet<eth_accounts_wallet>
  3. Function - (非必须)可选的回调函数,返回错误做为第一个参数,结果做为第二个参数。

注意:2.中的address参数一样能够是web3.eth.accounts.wallet <eth_accounts_wallet>中的地址或者索引。而后它会在本地用帐户的私钥进行签名。

返回值

Promise返回String - 由eth_signTypedData返回的签名

示例

有关typedData的值,参考上面的eth_signTypedData JSON-API的示例

web3.eth.signTypedData(typedData, "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826")
.then(console.log);
> "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c"
web3.eth.personal.signTypedData
web3.eth.personal.signTypedData(typedData, address, password [, callback])

web3.eth.signTypedData同样,除了要多加一个password参数。(类比web3.eth.personal.sign

理念

对于新类型,encode方法将扩展为新的状况。编码的第一个字节用来区分这些状况。出于一样的缘由,当即开始使用域名分隔符或者typeHash是不安全的。虽然很难,但能够构建出一个typeHash,这刚好也是一个合理的交易的RLP编码的前缀。

域名分割符能够防止和其余相同的结构碰撞。有可能两个Dapp具备一样的结构,好比Transfer(address from, address to, uint256 amount),但它们不该该兼容。经过引入域名分隔符,Dapp开发者能够保证不会发生签名冲突。

域名分隔符也容许相同的结构实例使用多个不一样的签名用例在一个给定的Dapp中。在以前的例子中,或许fromto两个都须要提供,经过提供两个不一样的域名分隔符,这些签名能够相互区分。

相关文章
相关标签/搜索