Verificando assinatura hashing com Keccak256

porMatheusem28/05/2022

Nesse artigo iremos abordar sobre como verificar uma assinatura hashing através da função keccak256.

Hashing com Keccak256

O keccack256 é uma função nativa do Solidity que calcula o hash de entrada Keccak-256.

Alguns casos de uso são:

  • Criar um ID exclusivo determinístico de uma entrada
  • Esquema de confirmação-revelação
  • Assinatura criptografada compacta (assinando o hash em vez de uma entrada maior)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract HashFunction {
function hash(
string memory _text,
uint _num,
address _addr
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_text, _num, _addr));
}
// Exemplo de colisão de hash
// A colisão de hash pode ocorrer quando você passa mais de um tipo de dados dinâmico
// para abi.encodePacked. Nesse caso, você deve usar abi.encode.
function collision(string memory _text, string memory _anotherText)
public
pure
returns (bytes32)
{
// encodePacked(AAA, BBB) -> AAABBB
// encodePacked(AA, ABBB) -> AAABBB
return keccak256(abi.encodePacked(_text, _anotherText));
}
}
contract GuessTheMagicWord {
bytes32 public answer =
0x60298f78cc0b47170ba79c10aa3851d7648bd96f2f8e46a19dbc777c36fb0c00;
// Palavra mágica é "Solidity"
function guess(string memory _word) public view returns (bool) {
return keccak256(abi.encodePacked(_word)) == answer;
}
}

Verificando uma assinatura

As mensagens podem ser assinadas fora da blockchain e depois verificadas dentro da blockchain usando um contrato inteligente.

Aqui um exemplo de verificação da assinatura assinada fora do contrato e verificado na chamada de uma função do contrato.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
/* Verificação de assinatura
Como assinar e verificar
# Assinatura
1. Crie uma mensagem para assinar
2. Faça o hash da mensagem
3. Assine o hash (fora da blockchain, mantenha sua chave privada em segredo)
# Verificar
1. Recrie o hash da mensagem original
2. Recupere o signatário da assinatura e do hash
3. Compare o signatário recuperado com o signatário reivindicado
*/
contract VerifySignature {
/* 1. Desbloqueie sua conta MetaMask
ethereum.enable()
*/
/* 2. Obtém a mensagem com o código hash de assinatura
getMessageHash(
0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C,
123,
"coffee and donuts",
1
)
hash = "0xcf36ac4f97dc10d91fc2cbb20d718e94a8cbfe0f82eaedc6a4aa38946fb797cd"
*/
function getMessageHash(
address _to,
uint _amount,
string memory _message,
uint _nonce
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_to, _amount, _message, _nonce));
}
/* 3. Assina o contrato com o hash
# usando um navegador
account = "copie e cole a conta do signatário aqui"
ethereum.request({ method: "personal_sign", params: [account, hash]}).then(console.log)
# usando a web3
web3.personal.sign(hash, web3.eth.defaultAccount, console.log)
A assinatura será diferente para contas diferentes
0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
*/
function getEthSignedMessageHash(bytes32 _messageHash)
public
pure
returns (bytes32)
{
/*
A assinatura é produzida assinando um hash keccak256 com o seguinte formato:
"\x19Ethereum Signed Message\n" + len(msg) + msg
*/
return
keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)
);
}
/* 4. Verifica a assinatura
signer = 0xB273216C05A8c0D4F0a4Dd0d7Bae1D2EfFE636dd
to = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C
amount = 123
message = "coffee and donuts"
nonce = 1
signature =
0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
*/
function verify(
address _signer,
address _to,
uint _amount,
string memory _message,
uint _nonce,
bytes memory signature
) public pure returns (bool) {
bytes32 messageHash = getMessageHash(_to, _amount, _message, _nonce);
bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);
return recoverSigner(ethSignedMessageHash, signature) == _signer;
}
function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature)
public
pure
returns (address)
{
(bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);
return ecrecover(_ethSignedMessageHash, v, r, s);
}
function splitSignature(bytes memory sig)
public
pure
returns (
bytes32 r,
bytes32 s,
uint8 v
)
{
require(sig.length == 65, "Tamanho de assinatura inválido");
assembly {
/*
Os primeiros 32 bytes armazenam o comprimento da assinatura
add(sig, 32) = ponteiro da assinatura + 32
efetivamente, pula os primeiros 32 bytes da assinatura
mload(p) carrega os próximos 32 bytes começando no endereço de memória p na memória
*/
// primeiros 32 bytes, após o prefixo de comprimento
r := mload(add(sig, 32))
// dos 32 até os 64 bytes
s := mload(add(sig, 64))
// últimos 32 byte (primeiro byte dos próximos 32 bytes)
v := byte(0, mload(add(sig, 96)))
}
// implicitly return (r, s, v)
}
}

Aqui um exemplo de implementação de uma assinatura sendo feita fora do contrato utilizando javascript.

const { expect } = require("chai")
const { ethers } = require("hardhat")
describe("VerifySignature", function () {
it("Check signature", async function () {
const accounts = await ethers.getSigners(2)
const VerifySignature = await ethers.getContractFactory("VerifySignature")
const contract = await VerifySignature.deploy()
await contract.deployed()
// const PRIV_KEY = "0x..." - Private Key da MetaMask (nunca revele para ninguém essa key)
// const signer = new ethers.Wallet(PRIV_KEY)
const signer = accounts[0]
const to = accounts[1].address
const amount = 999
const message = "Hello"
const nonce = 123
const hash = await contract.getMessageHash(to, amount, message, nonce)
const sig = await signer.signMessage(ethers.utils.arrayify(hash))
const ethHash = await contract.getEthSignedMessageHash(hash)
console.log("signatário ", signer.address)
console.log("signatário recuperado", await contract.recoverSigner(ethHash, sig))
// Mensagem com assinatura correta irá retornar true
expect(
await contract.verify(signer.address, to, amount, message, nonce, sig)
).to.equal(true)
// Mensagem com assinatura incorreta irá retornar false
expect(
await contract.verify(signer.address, to, amount + 1, message, nonce, sig)
).to.equal(false)
})
})

Para quais casos de uso isso serviria?

  • realizar transações de jogos NFT
  • criação e transação de itens NFT
  • criação de um marketplace
  • sistema de airdrops e staking

Entre várias outras utilidades.


Testar no Remix