1. 引言
数字签名 用于确保真实性和完整性。正如本文所述,“一个有效的数字签名,在满足先决条件的情况下,能让接收者非常有信心地认为消息是由已知发件人创建的(真实性),并且消息在传输过程中未被篡改(完整性)。”
数字签名已广泛应用于智能合约,例如在白名单铸造和订单簿 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 代币都是非同质化的,自动化做市商(AMM)交易策略很难在 NFT 市场中使用。因此,大多数 NFT 市场,如 OpenSea、LooksRare 和 X2Y2,都采用订单簿交易模型。
订单簿交易很简单。有一个做市商,即想要以特定价格出售资产的人,和一个接手者,即想要以卖家价格购买资产的人。在这种情况下,订单匹配。在订单簿 NFT 市场中,流程是相同的。唯一的区别在于订单提供过程:NFT 市场使用数字签名进行订单验证。图 1 描述了其中一个订单簿市场(OpenSea)的整个交易流程示例。

具体来说,卖家对卖单进行签名并将其存储在 OpenSea 的服务器上。买家可以从 OpenSea 的服务器检索签名的卖单信息,然后使用签名的卖单作为参数调用 NFT 市场合约。市场合约将验证订单,以确保卖家对卖单进行了签名(因为买家发起交易)—— 以防止买家在未经卖家同意的情况下购买资产。
3. 安全事件
霍顿原则 是加密系统的格言,可以表述为“验证所要表达的内容,而非所说出的内容”,或“言行一致,签名即言”。它要求对行为进行完全和精确的签名。如果签名不完整或不准确,结果将是灾难性的。
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. 建议
遵循霍顿原则,签名你所要表达的内容,而非所说出的内容。签名应包含全面且准确的必要信息。
- 将所有需要验证的信息放入签名中。检查签名消息中的数据与运行时值的一致性(例如,签名消息中的预期用户和实际用户)。
- 要签名的消息需要确定性地编码,例如,不存在结构不同但编码结果相同的消息。



