Back to Blog

Desarrollo Seguro de Contratos Inteligentes (2) — Cómo Usar Firmas Digitales Correctamente en NFT (Mercados)

August 12, 2022
8 min read

1. Introducción

La firma digital se utiliza para garantizar la autenticidad e integridad. Como se describe en este artículo, "Una firma digital válida, cuando se cumplen los requisitos previos, otorga al destinatario una confianza muy alta en que el mensaje fue creado por un remitente conocido (autenticidad), y que el mensaje no fue alterado en tránsito (integridad)."

La firma digital se ha utilizado ampliamente en los contratos inteligentes, por ejemplo, en la acuñación por lista de permitidos y en los mercados de NFT con libro de órdenes. Esto se debe a que ayuda a reducir los costos de transacción (firma fuera de la cadena y verificación en la cadena). Sin embargo, el mal uso por parte de los desarrolladores también introduce riesgos en los mercados de NFT. En este blog, nos gustaría hablar sobre el mal uso de las firmas digitales en el ecosistema NFT.

2. Aplicaciones

La firma digital se ha utilizado ampliamente para la acuñación por lista de permitidos (solo los usuarios con firmas válidas pueden acuñar NFTs) en contratos NFT y mercados NFT para la verificación de órdenes (solo las órdenes con las firmas esperadas pueden ejecutarse). La firma de los datos se realiza fuera de la cadena para ahorrar gas. A continuación, ilustraremos estos dos escenarios de uso.

2.1. Acuñación por Lista de Permitidos

La "acuñación de NFT" es el procedimiento de crear un NFT en la cadena de bloques. La mayoría de los proyectos NFT desean difundir sus productos; prefieren motivar a los usuarios mediante la acuñación por lista de permitidos (también llamada preventa, etc.). Las personas que consiguen los cupos pueden acuñar tokens a un precio más bajo (incluso de forma gratuita). Se utiliza una firma digital para distinguir a los acuñadores de la lista de permitidos de los acuñadores públicos (ordinarios). A continuación se muestra un ejemplo de la implementación de la acuñación por lista de permitidos.

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;
    }

Este fragmento de código proviene del Association NFT (que tiene una vulnerabilidad — no copie este código). La función mint_approved() pretende implementar la acuñación por lista de permitidos: el propietario del proyecto firma un mensaje de acuñación (variable info) y envía el mensaje al acuñador autorizado (quien puede acuñar NFTs). Luego, el acuñador puede invocar approved_mint con la variable firmada. El contrato verificará si el mensaje fue firmado por el proyecto (signer == recovered). De ser así, quien invoca la función tiene permitido acuñar NFTs (lo cual NO es seguro ya que no hay verificación de si quien invoca la función es la persona real en la lista de permitidos).

2.2. Verificación de Órdenes

La verificación de órdenes es otra aplicación de la firma digital en el ecosistema NFT. Los mercados de NFT desempeñan un papel esencial en el ecosistema NFT ya que proporcionan la funcionalidad de intercambio para los NFTs. Dado que cada token NFT no es fungible, la política de comercio del creador de mercado automatizado (AMM) es difícil de utilizar en los mercados de NFT. Por ello, la mayoría de los mercados de NFT, como OpenSea, LooksRare y X2Y2, adoptan el modelo de comercio con libro de órdenes.

El comercio con libro de órdenes es simple. Hay un creador, es decir, una persona que desea vender un activo a un precio específico, y un tomador, es decir, una persona que desea comprar el activo al precio del vendedor. En este caso, la orden coincide. El proceso es el mismo en los mercados de NFT con libro de órdenes. La única diferencia es el proceso de oferta de órdenes: los mercados de NFT utilizan firmas digitales para la verificación de órdenes. La Figura 1 describe un ejemplo de todo el proceso de comercio de uno de los mercados con libro de órdenes: OpenSea.

**Fig 1. Proceso de comercio en OpenSea**
Fig 1. Proceso de comercio en OpenSea

Específicamente, el vendedor firma una orden de venta y la almacena en el servidor de OpenSea. El comprador puede recuperar la información de la orden de venta firmada del servidor de OpenSea y luego invocar el contrato del mercado NFT con la orden de venta firmada como parámetros. El contrato del mercado validará la orden para asegurarse de que el vendedor firmó la orden de venta (ya que el comprador inicia la transacción), con el fin de evitar que el comprador adquiera un activo sin el consentimiento del vendedor.

3. Incidentes de Seguridad

El Principio de Horton es una máxima para los sistemas criptográficos y puede expresarse como "Autenticar lo que se quiere decir, no lo que se dice" o "quiere decir lo que firmas y firma lo que quieres decir"; requiere firmar la acción de manera total y precisa. Si la firma es parcial o inexacta, el resultado será desastroso.

3.1 Association NFT

Recordando el contrato NFT de la NBA en la sección 2.1. La función verify realiza una verificación de firma estándar, pero le falta un componente CRÍTICO. La verificación de la firma solo garantiza que el mensaje fue firmado por el proyecto. Sin embargo, no existe ninguna comprobación de que la persona que proporciona la firma al contrato sea consistente con el acuñador de la lista de permitidos en el mensaje firmado. Como resultado, cualquiera puede usar la misma firma para pasar la verificación y acuñar NFTs.

3.2 OpenSea

Otro problema de seguridad está relacionado con OpenSea. A principios de 2022, los investigadores revelaron una vulnerabilidad potencial del contrato del mercado OpenSea (versión: wyvern 2.2), que implementa la funcionalidad principal del comercio de NFTs.

En el protocolo Wyvern, los usuarios crean listados (ofertas de venta) u ofertas (ofertas de compra) fuera de la cadena, y las firmas de las ofertas se verifican en la cadena. Las ofertas de Wyvern contienen muchos parámetros que se agregan en una única cadena de bytes para calcular el resumen de la oferta. Luego, el contrato validará la firma del resumen. El método de agregación de parámetros simplemente empaqueta los parámetros en una cadena de bytes con los siguientes métodos.

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);

Por ejemplo, si los parámetros se componen de 2 componentes: (address, bytes), y los parámetros son (0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xc098"), los bytes agregados serían 0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fc098, simplemente address + bytes. Parece fácil y claro, ¿verdad?

Ahora, consideremos un ejemplo más complejo; la estructura de los parámetros es (address, bytes, bytes).

el parámetro 1 es _(0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xab", "0xcdef")_.

el parámetro 2 es _(0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xabcd", "0xef")_.

Los bytes agregados son:

parámetro 1: _0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef_.

parámetro 2: _0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef_.

¡Dos parámetros diferentes tienen el mismo resultado agregado, lo que significa que sus resúmenes son IGUALES, resultando en que una sola firma podría verificar los dos parámetros diferentes!

Esto se debe a que hay muchos componentes de longitud variable en los parámetros. Un atacante puede truncar parte de las variables y adjuntar las partes truncadas a sus componentes anteriores o posteriores. Desafortunadamente, los contratos de Wyvern tienen muchos parámetros de longitud variable como se muestra a continuación.

......
    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;
    ......

El impacto de la vulnerabilidad es que un atacante podría (si es posible) controlar las cuentas de la víctima para ejecutar algunos comportamientos maliciosos. Un análisis detallado de la vulnerabilidad se encuentra aquí.

Ambos incidentes de seguridad mencionados en esta sección violan el Principio de Horton. Específicamente, el contrato de la NBA no incluye al acuñador en el mensaje firmado (o no verifica la consistencia de la información contenida en el mensaje firmado con el invocador real), y el contrato de Wyvern firma parámetros sin estructura, de modo que el significado de la acción puede modificarse mientras que la presentación (lo que se dice) de los parámetros permanece igual.

4. Sugerencias

Siga el Principio de Horton, firme lo que quiere decir, no lo que dice. La firma debe contener información completa y precisa.

  • Incluya toda la información que debe verificarse en la firma. Compruebe la consistencia de los datos en el mensaje firmado con el valor en tiempo de ejecución (por ejemplo, el usuario previsto en el mensaje firmado y el usuario real).
  • El mensaje a firmar debe codificarse de manera determinista, es decir, no deben existir mensajes con diferentes estructuras que tengan el mismo resultado de codificación.

Lea el Otro Artículo de Esta Serie