1. Introdução
A assinatura digital é usada para garantir a autenticidade e integridade. Conforme descrito neste artigo, “Uma assinatura digital válida, onde os pré-requisitos são atendidos, 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 carteira de pedidos e lista de permissões em mercados de NFTs. Isso porque ajuda a economizar nos custos de transação (verificação off-chain e verificação on-chain). No entanto, o mau uso dos desenvolvedores também apresenta riscos nos mercados NFT. Neste blog, gostaríamos de falar sobre o uso indevido de assinaturas digitais no ecossistema NFT.
2. Aplicativos
A assinatura digital tem sido amplamente utilizada para a cunhagem da lista de permissões (somente os usuários com assinaturas válidas podem cunhar NFTs) em contratos NFT e mercados NFT para verificação de ordens (somente 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. Cunhagem da Lista de permissões
“NFT minting” (cunhagem de NFT) é o procedimento de criação de um NFT na blockchain. A maioria dos projetos NFT gostaria de divulgar seus produtos; eles preferem motivar os usuários por meio da lista de permissões (também chamada de pré-venda, etc.). As pessoas que ganharem os lugares na lista poderão cunhar tokens a um preço mais baixo (ou até mesmo de graça). Uma assinatura digital é usada para distinguir os cunhadores da lista de permissões e os cunhadores públicos (comuns). Abaixo está um exemplo da implementação da cunhagem da lista de permissões.
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 é da Associação NFT (que tem uma vulnerabilidade — não copie este código). A função mint_approved()
pretende implementar a cunhagem da lista de permissões: o dono do projeto assina uma mensagem de cunhagem ( info
variável) e envia a mensagem para o cunhador permitido (que pode cunhar NFTs). Então o cunhador pode invocar approved_mint
com a variável com sinal. O contrato verificará então se a mensagem é assinada pelo projeto (signer == recovered
). Em caso afirmativo, aquele que invoca a função tem permissão para cunhar NFTs (o que NÃO é seguro, pois não há verificação se quem invoca a função é a pessoa real na lista de permissões).
2.2. Verificação do pedido
A verificação de pedidos é outra aplicação da assinatura digital no ecossistema NFT. Os mercados NFT desempenham um papel essencial no ecossistema NFT, pois fornecem a funcionalidade de negociação para os NFTs. Como cada token NFT não é fungível, é difícil usar a política de negociação do criador de mercado automatizado (AMM) nos mercados NFT. Assim, a maioria dos mercados NFT, por exemplo, OpenSea, LooksRare e X2Y2, adotam o modelo de negociação de livro de pedidos.
A negociação do livro de pedidos é simples. Há um criador, também conhecido como uma pessoa que quer vender um ativo por um preço específico, e um tomador, também conhecido como uma pessoa que quer comprar o ativo ao preço do vendedor. Nesse caso, a ordem corresponde. O processo é o mesmo nos mercados NFT de livro de pedidos. A única diferença é o processo de oferta de pedidos: os mercados NFT usam assinaturas digitais para verificação de pedidos. A Figura 1 descreve um exemplo de todo o processo de negociação de um dos mercados de livros de pedidos: OpenSea.
Fig 1. Processo de negociação no 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 então invocar o contrato de mercado NFT com a ordem de venda assinada como parâmetros. O contrato de mercado validará a ordem para garantir que o vendedor assine a ordem de venda (já que o comprador inicia a transação.) — para evitar que o comprador compre 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 garantido, não o que está sendo dito” ou “garanta o que você assina e assine o que você garante”, requer a assinatura total e precisa da ação. Se a assinatura for parcial ou imprecisa, o resultado será desastroso.
3.1 Associação NFT
Relembrando o contrato NBA NFT na seção 2.1. A função verify faz uma verificação de assinatura padrão, mas está faltando um componente CRÍTICO. A verificação de assinatura apenas garante que a mensagem seja assinada pelo projeto. No entanto, não há imposição de que a pessoa que fornece a assinatura do contrato seja consistente com o cunhador da lista de permissões na mensagem assinada. Como resultado, qualquer pessoa pode usar a mesma assinatura para passar na verificação e criar NFTs.
3.2 OpenSea
Outra questão de segurança é sobre o OpenSea. No início de 2022, os pesquisadores divulgaram uma potencial vulnerabilidade do contrato de mercado OpenSea (versão: wyvern 2.2), que implementa a funcionalidade principal da negociação de NFTs.
No protocolo Wyvern, os usuários criam listagens (ofertas de venda) ou ofertas (ofertas de compra) off-chain, e as assinaturas das ofertas são verificadas on-chain. As ofertas Wyvern contêm muitos parâmetros e os parâmetros são agregados em uma única string de bytes para calcular o resumo da oferta. Em seguida, o contrato validará 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 métodos a seguir.
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 forem compostos por 2 componentes: (address, bytes
), e os parâmetros forem (0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xc098
"), os bytes agregados seriam 0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fc098
, apenasaddress+ bytes
. Parece ser fácil e claro, certo?
Agora, considere um exemplo mais complexo, a estrutura de parâmetros é (address, bytes, bytes
).
parâmetro 1 é (0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xab", "0xcdef
").
parâmetro 2 é (0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xabcd", "0xef
").
Os bytes agregados são:
parâmetro 1: 0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef
.
parâmetro 2: 0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef
.
Uau! Dois parâmetros diferentes têm o mesmo resultado agregado, o que significa que seus resumos são os MESMOS, resultando que uma assinatura pode verificar os dois parâmetros diferentes.
Isso ocorre porque há muitos componentes de comprimento variável nos parâmetros. Um invasor pode truncar parte das variáveis e anexar as partes truncadas aos seus componentes anteriores ou posteriores. Infelizmente, os contratos Wyvern têm muitos parâmetros de comprimento variável, como mostra abaixo.
......
address target;
/* HowToCall. */
AuthenticatedProxy.HowToCall howToCall;
/* Calldata. */
bytes calldata;
/* Calldata padrão de substituição, ou uma matriz de bytes vazia para nenhuma substituição. */
bytes replacementPattern;
/* Alvo de chamada estática, endereço zero para nenhuma chamada estática. */
address staticTarget;
/* Dados extras de chamada estática. */
bytes staticExtradata;
......
O impacto da vulnerabilidade é que o invasor pode (se possível) controlar as contas da vítima para executar alguns comportamentos maliciosos. Uma análise detalhada da vulnerabilidade está aqui.
Ambos os dois incidentes de segurança mencionados nesta seção violam o Princípio de Horton. Especificamente, o contrato NBA não inclui o cunhador na mensagem assinada (ou não verifica a consistência com as informações contidas na mensagem assinada com o invocador real), e o contrato Wyvern assina parâmetros sem estrutura para que o significado da ação possa ser modificado enquanto a apresentação (dizendo) dos parâmetros permanecer.
Sugestões
Siga o Princípio de Horton, assine o que você garante, não o que você diz. A assinatura deve conter informações completas e precisas necessárias.
- Coloque todas as informações a serem verificadas na assinatura. Verifique a consistência dos dados na mensagem assinada com o valor de 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, por exemplo, não existem mensagens com estruturas diferentes, mas com o mesmo resultado de codificação.
Artigo escrito por BlockSec e traduzido por Marcelo Panegali
Top comments (0)