以太坊上的數字簽名

買賣虛擬貨幣

來源 | 以太坊愛好者

責編 | 晉兆雨

頭圖 | CSDN付費下載於視覺中國

密碼學簽名是區塊鏈的關鍵技術之一,可以在不暴露私鑰的前提下證明地址的所有權。該技術主要用來簽署交易(當然也可以用來簽署其他任意訊息)。本文會講解數字簽名技術在以太坊協議中的用法。

免責宣告:密碼學很難。請不要將本文的任何內容作為參考,來實現您自己的密碼學函式。雖然我們已經進行了廣泛的研究,但是文字所提供的資訊可能仍有不準確之處。本文僅用作教育用途。

什麼是密碼學簽名?

當我們討論密碼學中的簽名時,我們其實是在討論所有權、有效性和完整性證明。舉例來說,這些簽名可以用來:

證明你擁有地址的私鑰(即認證功能);

確保資訊(例如,郵件)沒有被篡改;

驗證你下載的 MyCrypto 版本是合法的。

密碼學簽名是基於數學公式的。我們擁有一個輸入訊息、一個私鑰和一個(通常情況下是秘密的)隨機數,就可以得到一串數字作為輸出值,也就是簽名。使用另一個數學公式可以進行反向計算,在不知道私鑰和隨機數的情況下進行驗證(譯者注:即驗證該簽名是否出自跟某個公鑰對應的私鑰)。這類演算法有很多,如 RSA 和 AES,但是以太坊(和比特幣)採用的都是橢圓曲線數字簽名演算法(ECDSA)。請注意,ECDSA 只是簽名演算法。與 RSA 和 AES 不同,這種演算法不能用於加密。

- 橢圓曲線的例子之一。以太坊採用的是 SECP256k1 曲線。-

透過橢圓曲線點乘演算法(elliptic curve point manipulation),我們可以使用私鑰計算出一個不可逆向計算的值(譯者注:即 “公鑰”,公鑰無法逆向計算出私鑰)。這樣一來,我們就可以建立出安全且不可篡改的簽名。能夠生成不可逆向計算的值的函式叫做 “陷門函式(trapdoor function)”:

陷門函式指的是在一個方向上易於計算,但是在缺少特殊資訊(即,陷門)的情況下很難反向計算的函式。

使用 ECDSA 簽名並驗證

ECDSA 簽名由兩個數字(整數)組成:r 和 s。以太坊還引入了額外的變數 v(恢復識別符號)。簽名可以表示成{r, s, v}。

在建立簽名時,你要先準備好一條待簽署的訊息,和用來簽署該訊息的私鑰(dₐ)。簡化後的簽名流程如下:

對待簽署訊息進行雜湊計算,得到雜湊值(e)。

生成一個安全的隨機數 k。

將 k 乘以橢圓曲線的常量 G,來計算橢圓曲線上的點(x₁, y₁)。

計算 r = x₁ mod n。如果 r 等於 0,請返回步驟 2 。

計算 s = k⁻¹(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":"Helloworld!","sig":"0x21fbf0696d5e0aa2ef41a2b4ffb623bcaf070461d61cf7251c74161f82fec3a4370854bc0a34b3ab487c1bc021cd318c734c51ae29374f2beb0e6f2dd49b4bf41c","version":"2"}

在 MyCrypto 的 “驗證訊息(Verify Message)” 一頁中,我們可以使用該簽名,並看到該訊息是由

0x76e01859d6cf4a8637350bdb81e3cef71e29b7c2 簽署的。

- MyCrypto 上的簽名驗證透過。點選此處,即可體驗。-

你可能會問:為什麼要將 address、msg 和 version 等其它資訊也包括在內?不能只驗證簽名本身嗎?好吧,不能。如果不保留其它資訊,就好像簽了一個合同,然後刪除了合同裡的所有資訊,只留下當事人的簽名。不同於交易簽名(我們之後會作更深入解釋),訊息簽名就只是簽名而已(譯者注:因此只有簽名是沒法驗證的)。

為了驗證訊息,我們需要掌握原始訊息、使用私鑰簽署訊息的地址,以及{r, s, v}簽名本身。版本號就是 MyCrypto 使用的某個版本號。舊版本的 MyCrypto 通常會加上訊息的當前日期和時間,計算其雜湊值,然後按照上述步驟簽署該訊息。後來又進行了更改,以符合 JSON-RPC 方法personal_sign 方法,因此需要指明版本號(“2”)。

簡化後的公鑰恢復流程如下:

計算訊息的雜湊值(e)。

計算橢圓曲線上的點 R = (x₁, y₁),其中 x₁ 是 r(v = 27),或 r + n(v = 28)。

計算 u₁ = -zr⁻¹ mod n 和 u₂ = sr⁻¹ mod n。

計算點 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

如果我們在 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 格式依然是最常見的。儘管如此,有一些提議非常有趣。

我先來簡單介紹下目前建立簽名所採用的方式:

"\x19EthereumSignedMessage:\n"+length(message)+message

訊息通常會預先進行雜湊計算,因此長度會固定在 32 個位元組:

"\x19EthereumSignedMessage:\n32"+Keccak256(message)

完整的訊息(包括字首)會再經歷一次雜湊計算,然後用私鑰對雜湊值簽名。這種方式適用於所有權證明,但是在其它情況下可能會出現問題。例如,如果使用者 A 簽署了一個訊息並將其傳送給合約 x,使用者 B 可以複製這個已簽署訊息併傳送給合約 Y。這就叫重放攻擊。有一些提案旨在解決這一問題,如 EIP 191 和 EIP 721。

EIP 191:簽名資料標準

EIP 191 是一個很簡單的提案:它定義了版本號和版本專有資料。格式如下所示:

0x19<1byteversion><versionspecificdata><datatosign>

顧名思義,版本專有資料(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_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);}

在下一節中,我們將詳細解釋 ecrecover。如果你想找一個簡單的 JavaScript 或 TypeScript 程式碼庫來來實現 EIP 712,請檢視這個庫:

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

如果你想詳細瞭解如何在智慧合約中實現 EIP 712,我建議你閱讀 MetaMask 的這篇文章。遺憾的是,EIP 712 規範目前還是草案,還沒有得到很多應用的支援。目前,Ledger 和 Trezor 都還沒支援 EIP 712,可能會阻礙該規範的廣泛採用。不過,Ledger 表示他們即將釋出的更新版會支援 EIP 712。

透過智慧合約來驗證簽名

訊息簽名更有趣的地方在於,我們可以使用智慧合約來驗證 ECDSA 簽名。Solidity 有一個內建函式叫做 ecrecover(這實際上是地址 0x1 上的預編譯合約),可以恢復用來簽署訊息的私鑰的地址。一個(非常)基本的合約實現如下所示:

// SPDX-License-Identifier: MITpragma 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)

原文連結:

https://medium.com/mycrypto/the-magic-of-digital-signatures-on-ethereum-98fe184dc9c7

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;