1. 簡介
數位簽章 用於確保真實性與完整性。如 這篇文章 所述,「在滿足先決條件的情況下,有效的數位簽章能讓接收者非常有信心地確認訊息是由已知發送者所建立(真實性),且訊息在傳輸過程中未被篡改(完整性)」。
數位簽章已廣泛應用於智慧合約中,例如允許名單(Allowlist)鑄造和訂單簿(Order-book)NFT 市場。這是因為它有助於節省交易成本(鏈下簽署與鏈上驗證)。然而,開發者的誤用也在 NFT 市場中帶來了風險。在本部落格中,我們將探討 NFT 生態系統中數位簽章的誤用情況。
2. 應用
數位簽章已廣泛用於 NFT 合約中的允許名單鑄造(僅擁有有效簽章的使用者才能鑄造 NFT),以及 NFT 市場中的訂單驗證(僅能執行具有預期簽章的訂單)。資料的簽署過程在鏈下進行以節省 Gas 費用。以下我們將說明這兩種使用場景。
2.1. 允許名單鑄造
「NFT 鑄造」是區塊鏈上建立 NFT 的過程。大多數 NFT 專案希望傳播其產品;他們傾向於透過允許名單鑄造(也稱為預售等)來激勵使用者。贏得名額的人可以用較低的價格(甚至免費)鑄造代幣。數位簽章用於區分允許名單鑄造者與公開(普通)鑄造者。以下是允許名單鑄造實現的一個範例。
function mint_approved(
vData memory info,
uint256 number_of_items_requested,
uint16 _batchNumber
) external {
...
require(verify(info), "Unauthorised access secret");
...
}
function verify(vData memory info) public view returns (bool) {
require(info.from != address(0), "INVALID_SIGNER");
bytes memory cat =
abi.encode(
info.from,
info.start,
info.end,
info.eth_price,
info.dust_price,
info.max_mint,
info.mint_free
);
bytes32 hash = keccak256(cat);
require(info.signature.length == 65, "Invalid signature length");
bytes32 sigR;
bytes32 sigS;
uint8 sigV;
bytes memory signature = info.signature; assembly {
sigR := mload(add(signature, 0x20))
sigS := mload(add(signature, 0x40))
sigV := byte(0, mload(add(signature, 0x60)))
} bytes32 data =
keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
);
address recovered = ecrecover(data, sigV, sigR, sigS);
return signer == recovered;
}
此程式碼片段來自 Association NFT(該合約存在 漏洞 —— 請勿複製此程式碼)。mint_approved() 函數旨在實現允許名單鑄造:專案所有者簽署鑄造訊息(info 變數)並將訊息發送給允許的鑄造者(可以鑄造 NFT 的人)。然後,鑄造者可以使用已簽署的變數呼叫 approved_mint。合約隨後會驗證該訊息是否由專案方簽署(signer == recovered)。如果是,呼叫函數的人就被允許鑄造 NFT(這是不安全的,因為沒有驗證呼叫函數的人是否為名單上的實際人員)。
2.2. 訂單驗證
訂單驗證是數位簽章在 NFT 生態系統中的另一項應用。NFT 市場在 NFT 生態系統中扮演重要角色,因為它們為 NFT 提供了交易功能。由於每個 NFT 代幣都是不可替代的(Non-fungible),自動造市商(AMM)的交易策略很難應用於 NFT 市場。因此,大多數 NFT 市場,例如 OpenSea、LooksRare 和 X2Y2,都採用訂單簿交易模型。
訂單簿交易很簡單。有一位掛單者(Maker),即想要以特定價格出售資產的人;以及一位吃單者(Taker),即想要以賣方價格購買資產的人。在這種情況下,訂單匹配。在訂單簿 NFT 市場中過程也是一樣的。唯一的區別在於訂單報價的過程:NFT 市場使用數位簽章進行訂單驗證。圖 1 描述了其中一個訂單簿市場 OpenSea 的整個交易流程範例。

具體而言,賣方簽署一份出售訂單並將其儲存在 OpenSea 的伺服器上。買方可以從 OpenSea 的伺服器獲取簽署後的出售訂單資訊,然後將簽署後的訂單作為參數呼叫 NFT 市場合約。市場合約將驗證訂單,以確保賣方確實簽署了該訂單(因為交易是由買方發起的)——藉此防止買方在未經賣方同意的情況下購買資產。
3. 安全事件
霍頓原則(Horton Principle) 是密碼學系統的一條準則,可以表述為「驗證你的意圖,而不是你的聲明」或「言行一致,簽署你所意圖代表的內容」,它要求完全且精確地對操作進行簽署。如果簽章是不完整或不準確的,結果將是災難性的。
3.1 Association NFT
回顧第 2.1 節中的 NBA NFT 合約。該 verify 函數執行了標準的簽章驗證,但缺少了一個關鍵組件。簽章驗證僅確保訊息由專案方簽署,然而,並沒有強制要求將簽章提供給合約的人與簽署訊息中允許名單內的鑄造者一致。因此,任何人都可以使用相同的簽章通過驗證並鑄造 NFT。
3.2 OpenSea
另一個安全問題與 OpenSea 有關。在 2022 年初,研究人員揭露了 OpenSea 市場合約(版本:wyvern 2.2,實作了 NFT 交易的核心功能)的一個潛在漏洞。
在 Wyvern 協議中,使用者在鏈下建立掛單(出售報價)或出價(購買報價),並在鏈上驗證報價的簽章。Wyvern 報價包含許多參數,這些參數被聚合在一起形成一個位元組字串,以計算報價的摘要。隨後,合約會驗證該摘要的簽章。參數聚合方法僅使用以下方法將參數打包到位元組字串中:
index = ArrayUtils.unsafeWriteAddress(index, order.target);
index = ArrayUtils.unsafeWriteUint8(index, uint8(order.howToCall));
index = ArrayUtils.unsafeWriteBytes(index, order.calldata);
index = ArrayUtils.unsafeWriteBytes(index, order.replacementPattern);
index = ArrayUtils.unsafeWriteAddress(index, order.staticTarget);
index = ArrayUtils.unsafeWriteBytes(index, order.staticExtradata);
index = ArrayUtils.unsafeWriteAddress(index, order.paymentToken);
例如,如果參數由 2 個組件組成:(address, bytes),且參數為 (0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xc098"),聚合後的位元組將是 0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fc098,即 address + bytes。看起來簡單明瞭,對吧?
現在,考慮一個更複雜的例子,參數結構為 (address, bytes, bytes)。
參數 1 為
_(0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xab", "0xcdef")_。參數 2 為
_(0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xabcd", "0xef")_。
聚合後的位元組分別為:
參數 1:
_0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef_。參數 2:
_0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef_。
哇!兩個不同的參數具有相同的聚合結果,這意味著它們的摘要是相同的,導致一個簽章可以驗證兩個不同的參數。
這是因為參數中包含許多可變長度的組件。攻擊者可以截斷部分變數,並將截斷的部分附加到其前一個或後一個組件上。不幸的是,Wyvern 合約具有許多可變長度參數,如下所示。
......
address target;
/* HowToCall. */
AuthenticatedProxy.HowToCall howToCall;
/* Calldata. */
bytes calldata;
/* Calldata replacement pattern, or an empty byte array for no replacement. */
bytes replacementPattern;
/* Static call target, zero-address for no static call. */
address staticTarget;
/* Static call extra data. */
bytes staticExtradata;
......
該漏洞的影響是攻擊者可能(如果條件允許)控制受害者的帳戶來執行某些惡意行為。有關該漏洞的詳細分析請見 此處。
本節提到的兩個安全事件都違反了霍頓原則。具體而言,NBA 合約未將鑄造者包含在簽署的訊息中(或未檢查簽署訊息中包含的資訊與實際呼叫者的一致性),而 Wyvern 合約簽署了結構不明確的參數,導致在參數的呈現(即內容)不變的情況下,操作的含義可能會被修改。
4. 建議
遵循霍頓原則:簽署你所意圖代表的內容,而不是你所說的內容。簽章應包含所需且全面、準確的資訊。
- 將所有需要驗證的資訊放入簽章中。檢查簽署訊息中的資料與執行時數值的一致性(例如,簽署訊息中的預期使用者與實際使用者)。
- 需要簽署的訊息必須以確定性的方式進行編碼,例如,不存在結構不同但編碼結果相同的訊息。



