WEB3DEV Español

Delia Viloria T
Delia Viloria T

Posted on

Firma y Verificación de mensajes EIP712 off-chain y on-chain

Image description

Requisitos para el EIP712

De acuerdo al EIP712, para firmar cada mensaje necesitas proveer un Domain Separator específico a la dApp para asegurar su singularidad comparado a contratos similares. Esto envuelve definir un nombre y versión para el contrato, así como el chainId desplegado y la dirección del contrato a verificar. También puedes proveer sal para añadir singularidad. El EIP712 explica:

Es posible que dos DApps tengan una estructura idéntica como Transfer (dirección desde, dirección a, cantidad de uint256) que no debería ser compatible. Introdución un separador de dominio (domain separator) los desarrolladores de la dApp están garantizando que no pueda haber una colisión de firma.

Los siguientes parámetros son opcionales para mejorar la seguridad de tu contrato contra los ataques de repetición.

  • nombre: la dApp o el nombre del protocolo, es decir, “Polytrade”

  • versión: la versión actual de lo que la llamada estándar es un “dominio de firma”. Esta puede ser el número de la versión de tu dApp o de la plataforma. Previene firmas de una versión del dApp para trabajar con las de otros.

  • chainId: el id de la cadena EIP-155. Previene que una firma que funciona en una red, como la red de prueba, para que no trabaje en otra, como la mainnet. El keyword `block.chainid en Solidity regresa al id de la cadena actual.

  • verifyingContract: la dirección del contrato Ethereum que verificará la firma resultante. El keyword address(this) en Solidity regresa la dirección del propio contrato, el cual puede usarlo cuando se verifica la firma.

El tipo de mensaje para el separador de dominio es:

`
const domainType = {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name":"verifyingContract",
"type": "address"
}
]
}
`

Luego de definir el TypedMessage, enviaremos los valores en la misma estructura:

`
const domainData = {
name: "Polytrade",
version: "1.0",
chainId: chainId,
verifyingContract:
verifyingContractAddress
};
`

EIP2612, el cual es la extensión Permit para los tokens ERC20, permite que se apruebe firmando un mensaje siguiendo el estándar EIP712. Por lo tanto, incluye un separador de dominio junto a la estructura Permit:

`
const permitType = {
"Permit": [{
"name": "owner",
"type": "address"
},
{
"name": "spender",
"type": "address"
},
{
"name": "value",
"type": "uint256"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
}
]
}
`

Similar al Separador de Dominio, luego de definir el TypedMessage, tenemos los valores para ello:

`
const permitData: {
"owner": owner,
"spender": spender,
"value": value,
"nonce": nonce,
"deadline": deadline
}
`

Ahora que ya tenemos todos los componentes necesarios, podemos proceder a firmar los mensajes preparados y luego verificar que la dirección del dueño sea al firmante del mensaje.

1. Firmar Off-chain

a. Ethers### _signTypedData

Como se explica en los documentos puedes usar este método para firmar un mensaje usando la especificación EIP-712 y retirar la firma.

signer._signTypedData( domain , types , value )

`
signature = await signer._signTypedData(
domainData,
permitType,
permitData
);
`

b. Web3### eth_signTypedData_v4

De acuerdo a los documentos, cuando se envía los datos con la firma, deberías usar el método V3 o V4.

  • V1 está basado en una primera versión del EIP-712 al que le faltaban algunas mejoras de seguridad, que eventualmente las otras versiones tendrían, y debería quedar en desuso a favor de versiones posteriores.

  • V3 está basado en el EIP-712 excepto por los arrays y las estructuras de datos recursivos que no lo soportan.

  • V4 está basado en el EIP-712 e incluye total apoyo de los arrays y de las estructuras de datos recursivos.

`
const data = JSON.stringify({
types: {
EIP712Domain: domainType,
Permit: permitType,
},
domain: domainData,
primaryType: "Permit",
message: permitData
});

web3.currentProvider.send(
{
method: "eth_signTypedData_v4",
params: [signer, data],
from: signer
},
function(err, result) {
if (err) {
return console.error(err);
}
}
)
`

c. Metamask eth-sig-util### signTypedData

Para la función mencionada, puedes especificar la versión y proveer la clave privada y de la forma junto a los datos.

signTypedData(privateKey, data, version)

`
ethSigUtil.signTypedData(privateKey, {
data: {
types: {
EIP712Domain:domainTye,
Permit: permitType
},
domain: domainData,
primaryType: 'Permit',
message: permitData
},
"V4"
});
`

Firma Dividida

Es posible enviar la firma junto a los parámetros a un contrato inteligente para la verificación del mensaje. Sin embargo, para el EIP2612 necesitas enviar los parámetros r, s y v de la firma para la función permit. Por lo tanto, necesitamos dividir el hash para extraer estos parámetros.

splitSignature() de ethers puede hacer el trabajo:

`
const { r, s, v } = splitSignature(signature);
`

Alternativamente, puedes dividir la firma en sus componentes de la siguiente manera:

`
const r = signature.slice(0, 66);
const s = "0x" + signature.slice(66, 130);
const v = parseInt(signature.slice(130, 132), 16);
`

2. Verificando on-chain

Firmar en cadena con Solidity es sencillo. Primero, necesitamos codificar el tipo de dominio EIP712 y luego podemos obtener el hash keccak usando los siguientes pasos:

`
bytes32 internal constant _EIP_712_DOMAIN_TYPEHASH =
keccak256(
abi.encodePacked(
"EIP712Domain(",
"string name,",
"string version,",
"uint256 chainId,",
"address verifyingContract",
")"
)
);
`

Similarmente, el typehash Permit puede seguir el mismo proceso de codificación y obtener el hash keccak:

`
bytes32 internal constant _PERMIT_TYPEHASH =
keccak256(
abi.encodePacked(
"Permit(",
"address owner,",
"address spender,",
"uint256 value,",
"uint256 nonce,",
"uint256 deadline",
")"
)
);
`

Para acomodar los valores del separador de dominios cuando pasa el nombre y version y para recalcular dinámicamente el _DOMAIN_SEPARATOR en caso que hayan cambios en la dirección del contrato o del chain ID, vamos a evadir hardcodearlos. Para calcular el separador del dominio, primero obtendremos el hash del nombre* y de la **version. Subsecuentemente, vamos a hashear el dominio del tipo junto a todos los valores combinados.

`
_NAME_HASH = keccak256(bytes(name));
_VERSION_HASH = keccak256(bytes(version));
_DOMAIN_SEPARATOR = keccak256(
abi.encode(
_EIP_712_DOMAIN_TYPEHASH,
_NAME_HASH,
_VERSION_HASH,
block.chainid,
address(this)
)
);
`

Para el PERMIT, seguiremos el mismo proceso

`
bytes32 PERMIT = keccak256(
abi.encode(
_PERMIT_TYPEHASH,
owner,
spender,
value,
nonce,
deadline
)
);
`

Para obtener el compendio EIP712 para la la recuperación de la dirección pública, concatenamos los hashes de ambos _DOMAIN_SEPARATOR y PERMIT con el prefijo \x19\x01. Luego, hashearemos la concatenación resultante para obtener el compendio requerido para la dirección de la recuperación. El prefijo 0x19 está basado en el EIP191, el cual es usado para el manejo estandarizado de los datos firmados, y el 01 significa los datos estructurados del EIP712.

{0x1901}{_DOMAIN_SEPARATOR}{PERMIT}

Este ensamblaje de códigos asegura una ejecución segura, obteniendo primero el puntero de la memoria. Luego escribe el prefijo inicial de 2-byte, seguido por _DOMAIN_SEPARATOR y PERMIT. Finalmente, computa el hash del resultado concatenado y regresa el compendio resultante:

`
assembly {
let ptr := mload(0x40)
mstore(ptr, "\x19\x01")
mstore(add(ptr, 0x02), _DOMAIN_SEPARATOR)
mstore(add(ptr, 0x22), PERMIT)
digest := keccak256(ptr, 0x42)
}
`

Aquí está la versión de Solidity del código que realiza las mismas operaciones:

`
digest = keccak256(abi.encodePacked(uint16(0x1901), _DOMAIN_SEPARATOR, PERMIT));
`

Ahora que todo está preparado, podemos obtener la dirección pública usando la función ecrecover(digest, v, r, s)

`
ecrecover(digest, v, r, s);
`

Al afirmar que la equivalencia del firmante y dueño de la dirección, podemos asegurar que sean idénticos, por lo tanto nos permite aumentar la asignación del que gasta.

`
bytes32 r;
bytes32 s;
uint8 v;
if (signature.length == 64) {
bytes32 vs;
(r, vs) = abi.decode(signature, (bytes32, bytes32));
s = vs & (0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
v = uint8(uint256(vs >> 255)) + 27;
} else if (signature.length == 65) {
(r, s) = abi.decode(signature, (bytes32, bytes32));
v = uint8(signature[64]);
}
`

Puntos Claves

  • Por favor toma en cuenta que para los propósitos de la verificación off-chain, podemos utilizar los mismos pasos para generar el dominio y permitir hashes (bien sea manualmente o utilizando el ayudante TypedDataUtils.hashStruct desde la biblioteca etc-sig-util). Subsecuentemente, podemos crear un compendio desde estos hashes. Finalmente, podemos usar la función recoverAddress(digest, signature) desde la librería ethers o de la función recoverTypedSignature(data, signature, version) desde la biblioteca eth-sig-util para retirar la dirección del que firma.

  • En casos donde la estructura incluye arrays o estructuras recursivas, es necesario definir sus respectivos tipos, asegurando que estén declarados en el orden correcto.

`
messageType = {
FirstStruct: [
{ name: "owner", type: "address" },
{ name: "customer", type: "Customer[]" },
{ name: "seller", type: "Seller" },
{ name: "price", type: "uint256" },
],
Customer: [
{ name: "address", type: "address" },
{ name: "discount", type: "uint256" },
],
Seller: [
{ name: "address", type: "address" },
{ name: "price", type: "uint256" },
],
};
`

  • Toma en cuenta que ciertas medidas de seguridad deben ser tomadas en consideración cuando se verifica v, r y s en la cadena. Estas precauciones ya están siendo manejadas si usas los contratos OpenZeppelin de la implementación ECDSA

Finalmente

¡Estás listo! Con esta información, ahora puedes firmar confiadamente cualquier mensaje EIP712 y verificar que estén tanto on-chain como off-chain. Espero que esta guía, junto a los códigos simples proveídos, sean útiles para ti.

Originalmente publicado en http://github.com

Este artículo fue escrito por Zakrad y traducido por Delia Viloria T. Su original se puede leer aquí.

Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_eshttps://twitter.com/web3dev_es en Twitter.

Discussion (0)