密碼學(xué)簽名是區(qū)塊鏈系統(tǒng)中的基本模塊。使用對(duì)應(yīng)的私鑰對(duì)交易進(jìn)行簽名能夠?qū)⒔灰装l(fā)起人與特定帳戶聯(lián)系起來(lái)。如果沒(méi)有此功能,區(qū)塊鏈的記帳工
密碼學(xué)簽名是區(qū)塊鏈系統(tǒng)中的基本模塊。使用對(duì)應(yīng)的私鑰對(duì)交易進(jìn)行簽名能夠?qū)⒔灰装l(fā)起人與特定帳戶聯(lián)系起來(lái)。如果沒(méi)有此功能,區(qū)塊鏈的記帳工作將無(wú)法正常進(jìn)行。
許多在以太坊上部署的智能合約也有直接驗(yàn)證數(shù)字簽名的功能,以使得一個(gè)或多個(gè)驗(yàn)證者可以通過(guò)提交離線創(chuàng)建的簽名(甚至是由另一個(gè)智能合約生成的簽名)來(lái)授權(quán)操作。這項(xiàng)驗(yàn)證通常被用于多重簽名冷錢包或者投票合同,以便一起提交各種簽名或委托授權(quán)。
此類實(shí)現(xiàn)中的常見漏洞是簽名重放攻擊。在 Cryptonics 對(duì)一個(gè)重要項(xiàng)目的智能合約審計(jì)中,我們遇到了這個(gè)問(wèn)題的一個(gè)有趣例子。在本文中,我們將使用此示例來(lái)說(shuō)明智能合約中簽名驗(yàn)證是如何出錯(cuò)的。
與簽名驗(yàn)證相關(guān)的漏洞通常是由于誤解了底層的密碼學(xué)原理和簽名的目的而引起的。因此,在詳細(xì)了解此特定漏洞之前,我們先快速了解一下密碼學(xué)簽名的工作原理。
密碼學(xué)簽名
大多數(shù)的密碼學(xué)簽名體系都基于公私鑰對(duì)。私鑰能夠?qū)?shù)據(jù)進(jìn)行簽名,而且此簽名能夠被對(duì)應(yīng)的公鑰所驗(yàn)證。就像它的名字所暗示的一樣,一個(gè)用戶的公鑰是公開的,而私鑰則一定要保密。
對(duì)數(shù)據(jù)進(jìn)行加密簽名可實(shí)現(xiàn)兩個(gè)重要屬性:
· 數(shù)據(jù)簽名者可識(shí)別性,這是通過(guò)恢復(fù)簽名者的公鑰來(lái)實(shí)現(xiàn)的。
· 數(shù)據(jù)完整的可驗(yàn)證性,意思是簽名可以用于證明自簽名以來(lái)數(shù)據(jù)未被修改。
雖然這些是非常強(qiáng)大的屬性,但是需要重點(diǎn)注意的是簽過(guò)名的數(shù)據(jù)本身不提供額外的保障。簽名不能保證一條消息的唯一性,也不能保證簽名人就是發(fā)信人本身。當(dāng)然,加密簽名可以被用于確認(rèn)相關(guān)事實(shí),但是應(yīng)用程序也須執(zhí)行必要的檢查。我們可以在以太坊智能合約中調(diào)查以上事實(shí)。
以太坊中的簽名驗(yàn)證
以太坊和比特幣一樣,采用橢圓曲線數(shù)字簽名算法(ECDSA)和 secp256k1 曲線。智能合約可以通過(guò)系統(tǒng)方法 ecrecover 訪問(wèn)內(nèi)置的 ECDSA 簽名驗(yàn)證算法。以下示例展示了這個(gè)函數(shù)的用法:
address signer = ecrecover(msgHash, v, r, s);
這個(gè)方法的輸入?yún)?shù)是簽名值 v,r 和 s,以及簽名數(shù)據(jù)的 keccak256 哈希值。它可以校驗(yàn)數(shù)據(jù)的完整性,即確認(rèn)數(shù)字簽名與數(shù)據(jù)的哈希值相對(duì)應(yīng),并且可以從簽名中恢復(fù)簽名者的以太坊地址(以太坊地址乃是從公鑰中推導(dǎo)出來(lái)的)。
任何額外的檢查,不論是檢查簽名地址是否為正確地址,還是檢查被簽名的消息是否唯一,都必須被手動(dòng)添加進(jìn)智能合約中。
經(jīng)常有人誤解了 ecrecover 的功能,然后搞出了安全漏洞。
簽名重放漏洞
代碼示例
讓我們來(lái)看一下我們?cè)谧罱暮霞s審計(jì)中發(fā)現(xiàn)的漏洞:
function unlock(
address _to,
uint256 _amount,
uint8[] _v,
bytes32[] _r,
bytes32[] _s
)
external
{
require(_v.length >= 5);
bytes32 hashData = keccak256(_to, _amount);
for (uint i = 0; i < _v.length; i++) {
address recAddr = ecrecover(hashData, _v[i], _r[i], _s[i]);
require(_isValidator(recAddr));
}
to.transfer(_amount);
}
以上代碼是我們所審計(jì)的代碼的簡(jiǎn)化版本,為使代碼變得簡(jiǎn)短易懂,它只保留了最基礎(chǔ)的信息。但是其中的漏洞被完整地保留了下來(lái)。
被審計(jì)的合約是跨鏈橋接器的一部分,它能讓數(shù)字資產(chǎn)從一個(gè)區(qū)塊鏈轉(zhuǎn)移到另一個(gè)上。以太幣在以太坊智能合約中被鎖定之時(shí),另一條鏈上會(huì)創(chuàng)建出對(duì)應(yīng)的資產(chǎn)。當(dāng)資產(chǎn)在另一條鏈上被鎖定或銷毀時(shí), unlock 函數(shù)可以釋放先前被鎖定的以太幣。
要實(shí)現(xiàn)這個(gè)效果時(shí),跨鏈中繼者可以提交一系列的驗(yàn)證者簽名、一個(gè)解鎖的數(shù)額以及一個(gè)目標(biāo)地址。這個(gè)函數(shù)要求至少五個(gè)簽名來(lái)解鎖需要的數(shù)額并將資金傳給接收方。而內(nèi)部的 _isValidator 函數(shù)(為了簡(jiǎn)化,省略掉了具體實(shí)現(xiàn))會(huì)檢查一個(gè)地址具不具備驗(yàn)證者身份。
攻擊情景
以上代碼的問(wèn)題在于被驗(yàn)證者用 ECDSA 算法簽過(guò)名的消息中。這個(gè)消息只包含接收者的地址以及需要解鎖的數(shù)量。在這個(gè)消息中,并沒(méi)有什么內(nèi)容能防止相同的簽名被多次重復(fù)使用。想象如下的情景:
· Bob 在與以太坊連接的另一條鏈上有等價(jià)于 10ETH 的資產(chǎn)被他通過(guò)橋接器傳回了以太坊鏈上。
· Alice 是一個(gè)處理跨鏈交易的中繼者。她收集了必需的驗(yàn)證者簽名,在所連接的鏈上鎖定了相對(duì)應(yīng)的資產(chǎn)數(shù)量,并且調(diào)用 unlock 函數(shù)將 10ETH 從合約中釋放給 Bob。
· 包含一系列簽名值的交易能夠在區(qū)塊鏈上公開讀取。
· Bob 現(xiàn)在可以復(fù)制這個(gè)簽名值的序列并且自己提交一個(gè)一模一樣的解鎖函數(shù)調(diào)用請(qǐng)求。這個(gè)解鎖的操作能夠再一次成功,導(dǎo)致又一個(gè) 10ETH 被發(fā)送給Bob。
· Bob 能夠重復(fù)這個(gè)過(guò)程直到智能合約中的以太坊被耗盡。
改進(jìn)手段
以上情形被稱為簽名重放攻擊。這種攻擊能成功是由于我們無(wú)法驗(yàn)證所簽名消息的唯一性,也不知道它之前是否被用過(guò)。
一個(gè)防止此類攻擊的簡(jiǎn)單方法是在被簽名數(shù)據(jù)中包含一個(gè)消息序列號(hào)或者 nonce。以上代碼的修正版如下:
public uint256 nonce;
function unlock(
address _to,
uint256 _amount,
uint256 _nonce,
uint8[] _v,
bytes32[] _r,
bytes32[] _s
)
external
{
require(_v.length >= 5);
require(_nonce == nonce++);
bytes32 hashData = keccak256(_to, _amount, _nonce);
for (uint i = 0; i < _v.length; i++) {
address recAddr = ecrecover(hashData, _v[i], _r[i], _s[i]);
require(_isValidator(recAddr));
}
to.transfer(_amount);
}
這段代碼現(xiàn)在要求每一個(gè)成功的解鎖調(diào)用都包含一個(gè)序列號(hào)。因?yàn)橄⒅械冒粋€(gè)獨(dú)一無(wú)二的數(shù)字,所以每次成功調(diào)用所要求的簽名都是獨(dú)一無(wú)二的。這表示之前觀測(cè)到的消息對(duì)攻擊者來(lái)說(shuō)沒(méi)用了,因?yàn)橹胤艜?huì)失敗。
簽名驗(yàn)證的最佳模式
上述例子只是其中一個(gè)示例,演示了不能保證唯一型的簽名如何被重放。在大部分情景中,確保簽名能夠與每一次調(diào)用形成唯一的匹配對(duì)預(yù)防重放攻擊是非常重要的。
但是,這段代碼并不完美。它并沒(méi)有遵循簽名驗(yàn)證的最佳實(shí)踐。原因是它沒(méi)有檢查可塑性簽名,我們應(yīng)檢查作為已接受簽名一部分的 s 值是否在較低范圍內(nèi)。使用 ecrecover 函數(shù)的推薦流程可以在 Open Zeppelin 的 excellent ECDSA 庫(kù)中找到。事實(shí)上,在社區(qū)審計(jì)過(guò)的代碼,比如 Open Zeppelin 上進(jìn)行開發(fā),總是一個(gè)好主意。(作者: Stefan Beyer)