1. Introdução
Assinatura digital é utilizada para garantir autenticidade e integridade. Como descrito neste artigo, "Uma assinatura digital válida, quando os pré-requisitos são satisfeitos, dá ao destinatário uma confiança muito alta de que a mensagem foi criada por um remetente conhecido (autenticidade), e que a mensagem não foi alterada em trânsito (integridade)."
A assinatura digital tem sido amplamente utilizada em contratos inteligentes, por exemplo, em mint de allowlist e mercados de NFT com livro de ordens. Isso porque ela ajuda a economizar custos de transação (assinatura off-chain e verificação on-chain). No entanto, o uso incorreto por parte dos desenvolvedores também introduz riscos nos mercados de NFT. Neste blog, gostaríamos de falar sobre o uso incorreto de assinaturas digitais no ecossistema de NFTs.
2. Aplicações
A assinatura digital tem sido amplamente utilizada para mint de allowlist (apenas usuários com assinaturas válidas podem criar NFTs) em contratos de NFT e mercados de NFT para verificação de ordens (apenas as ordens com assinaturas esperadas podem ser executadas). A assinatura dos dados é feita off-chain para economizar gás. A seguir, ilustraremos esses dois cenários de uso.
2.1. Mint de Allowlist
"Mint de NFT" é o procedimento de criação de um NFT na blockchain. A maioria dos projetos de NFT deseja disseminar seus produtos; eles preferem motivar os usuários por meio do mint de allowlist (também chamado de pré-venda, etc.). As pessoas que conquistam as vagas podem criar tokens a um preço menor (ou até gratuitamente). Uma assinatura digital é utilizada para distinguir os criadores da allowlist dos criadores públicos (comuns). Abaixo está um exemplo de implementação do mint de allowlist.
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 trecho de código é do Association NFT (que possui uma vulnerabilidade — não copie este código). A função mint_approved() tem a intenção de implementar o mint de allowlist: o dono do projeto assina uma mensagem de mint (variável info) e envia a mensagem ao criador autorizado (que pode criar NFTs). Em seguida, o criador pode invocar approved_mint com a variável assinada. O contrato então verificará se a mensagem foi assinada pelo projeto (signer == recovered). Em caso afirmativo, quem invocar a função terá permissão para criar NFTs (o que NÃO é seguro, pois não há verificação sobre se quem invoca a função é de fato a pessoa na allowlist).
2.2. Verificação de Ordens
A verificação de ordens é outra aplicação de assinatura digital no ecossistema de NFTs. Os mercados de NFT desempenham um papel essencial no ecossistema de NFTs, pois fornecem a funcionalidade de negociação para os NFTs. Como cada token NFT é não fungível, a política de negociação por formador de mercado automatizado (AMM) é difícil de ser utilizada nos mercados de NFT. Assim, a maioria dos mercados de NFT, como OpenSea, LooksRare e X2Y2, adota o modelo de negociação por livro de ordens.
A negociação por livro de ordens é simples. Há um criador (maker), ou seja, uma pessoa que deseja vender um ativo por um preço específico, e um tomador (taker), ou seja, uma pessoa que deseja comprar o ativo pelo preço do vendedor. Nesse caso, a ordem é correspondida. O processo é o mesmo nos mercados de NFT com livro de ordens. A única diferença é o processo de oferta de ordens: os mercados de NFT utilizam assinaturas digitais para verificação de ordens. A Figura 1 descreve um exemplo do processo completo de negociação de um dos mercados com livro de ordens: o OpenSea.

Especificamente, o vendedor assina uma ordem de venda e a armazena no servidor do OpenSea. O comprador pode recuperar as informações da ordem de venda assinada do servidor do OpenSea e, em seguida, invocar o contrato do mercado de NFT com a ordem de venda assinada como parâmetros. O contrato do mercado validará a ordem para garantir que o vendedor assinou a ordem de venda (uma vez que o comprador inicia a transação) — para evitar que o comprador adquira um ativo sem o consentimento do vendedor.
3. Incidentes de Segurança
O Princípio de Horton é uma máxima para sistemas criptográficos e pode ser expresso como "Autentique o que está sendo significado, não o que está sendo dito" ou "signifique o que você assina e assine o que você significa"; ele exige que a ação seja assinada de forma total e precisa. Se a assinatura for parcial ou imprecisa, o resultado será desastroso.
3.1 Association NFT
Relembrando o contrato de NFT da NBA na seção 2.1. A função verify realiza uma verificação de assinatura padrão, mas está faltando um componente CRÍTICO. A verificação de assinatura apenas garante que a mensagem foi assinada pelo projeto. No entanto, não há imposição de que a pessoa que fornece a assinatura ao contrato seja consistente com o criador da allowlist na mensagem assinada. Como resultado, qualquer pessoa pode usar a mesma assinatura para passar na verificação e criar NFTs.
3.2 OpenSea
Outro problema de segurança envolve o OpenSea. No início de 2022, pesquisadores divulgaram uma potencial vulnerabilidade no contrato do mercado OpenSea (versão: wyvern 2.2), que implementa a funcionalidade central de negociação de NFTs.
No protocolo Wyvern, os usuários criam listagens (ofertas de venda) ou ofertas (propostas de compra) off-chain, e as assinaturas das ofertas são verificadas on-chain. As ofertas do Wyvern contêm muitos parâmetros que são agregados em uma única string de bytes para calcular o resumo da oferta. Em seguida, o contrato valida a assinatura do resumo. O método de agregação de parâmetros simplesmente empacota os parâmetros em uma string de bytes com os seguintes 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 exemplo, se os parâmetros são compostos por 2 componentes: (address, bytes), e os parâmetros são (0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xc098"), os bytes agregados seriam 0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fc098, simplesmente address + bytes. Parece fácil e claro, certo?
Agora, considere um exemplo mais complexo: a estrutura dos parâmetros é (address, bytes, bytes).
o parâmetro 1 é
_(0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xab", "0xcdef")_.o parâmetro 2 é
_(0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xabcd", "0xef")_.
Os bytes agregados são:
parâmetro 1:
_0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef_.parâmetro 2:
_0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef_.
Surpreendente! Dois parâmetros diferentes têm o mesmo resultado agregado, o que significa que seus resumos são IGUAIS, resultando em uma assinatura que pode verificar dois parâmetros diferentes.
Isso ocorre porque há muitos componentes de comprimento variável nos parâmetros. Um atacante pode truncar parte das variáveis e anexar as partes truncadas aos seus componentes anteriores ou posteriores. Infelizmente, os contratos Wyvern possuem muitos parâmetros de comprimento variável, como mostrado abaixo.
......
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;
......
O impacto da vulnerabilidade é que o atacante poderia (se possível) controlar as contas da vítima para executar alguns comportamentos maliciosos. Uma análise detalhada da vulnerabilidade está disponível aqui.
Ambos os incidentes de segurança mencionados nesta seção violam o Princípio de Horton. Especificamente, o contrato da NBA não inclui o criador na mensagem assinada (ou não verifica a consistência das informações contidas na mensagem assinada com o invocador real), e o contrato Wyvern assina parâmetros sem estrutura definida, de modo que o significado da ação pode ser modificado enquanto a apresentação (o que é dito) dos parâmetros permanece inalterada.
4. Sugestões
Siga o Princípio de Horton: assine o que você significa, não o que você diz. A assinatura deve conter informações completas e precisas necessárias.
- Coloque todas as informações que devem ser verificadas na assinatura. Verifique a consistência dos dados na mensagem assinada com o valor em tempo de execução (por exemplo, o usuário pretendido na mensagem assinada e o usuário real).
- A mensagem a ser assinada precisa ser codificada de forma determinística, ou seja, não devem existir mensagens com estruturas diferentes que produzam o mesmo resultado de codificação.



