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兩個都須要提供,經過提供兩個不一樣的域名分隔符,這些簽名能夠相互區分。

相關文章
相關標籤/搜索