安全智能合约开发(二)— NFT(市场)中数字签名的使用与规范

用数字签名保障NFT真实性:了解数字签名如何在NFT智能合约开发中提供真实性和完整性。

安全智能合约开发(二)— NFT(市场)中数字签名的使用与规范

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,都采用订单簿交易模式。

订单簿交易很简单。有一个挂单者(Maker),即希望以特定价格出售资产的人;还有一个吃单者(Taker),即希望以卖方价格购买该资产的人。在这种情况下,订单匹配。订单簿 NFT 市场的过程也相同。唯一的区别在于订单挂出过程:NFT 市场使用数字签名进行订单验证。图 1 描述了其中一个订单簿市场(OpenSea)的整个交易过程示例。

图 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. 建议

遵循霍顿原则,签名你所要表达的意思,而不是你所说的话。签名应包含全方位准确的信息。

  • 将所有需要验证的信息包含在签名中。检查签名消息中的数据与运行时值的一致性(例如,签名消息中的预期用户与实际用户)。
  • 要签名的消息需要确定性地编码,例如,不存在具有不同结构但具有相同编码结果的消息。

阅读本系列的另一篇文章

Sign up for the latest updates