Emisión y verificación de desafíos EIP-712 con Go

Un gran aspecto de la comunidad de ingenieros de Ethereum es que los ingenieros suelen estar presentes para ayudarse entre sí. En esta publicación de blog, me gustaría continuar esa tendencia, explicando un proceso por el que pasamos recientemente para construir algo aparentemente simple, pero que en sus detalles resultó en un esfuerzo de ingeniería bastante complicado, que condujo a relaciones públicas para las bases de código de Geth y Metamask.

Un poco de antecedentes: estamos trabajando activamente en un sistema de administración de identidad que tiene una función que requiere que los usuarios verifiquen el control de una dirección Ethereum. Nuestro pensamiento inicial fue usar la API eth_sign simple de Metamask, sin embargo, rápidamente nos dimos cuenta de que esta no era la ruta correcta a seguir dado que el método da como resultado un mensaje de error rojo en la interfaz de usuario de Metamask:

En su lugar, decidimos trabajar con el nuevo estándar EIP-712, que presenta al usuario una interfaz mucho más agradable y se puede verificar fácilmente en la cadena si alguna vez necesitamos esta capacidad en el futuro. Al final de esta guía, le mostraremos cómo crear el siguiente flujo:

Bastante sencillo, ¿verdad? ¡No exactamente! Como siempre, el diablo está en los detalles, y en este caso, los detalles estuvieron bastante involucrados debido a la documentación incipiente, sobre la cual esperamos mejorar a través de esta publicación.

Generación del EIP-712 en Go

Una de las principales ventajas de crear en Go es el uso de las bibliotecas integradas de Geth. Por suerte para nosotros, la compatibilidad con EIP-712 se fusionó recientemente, lo que nos permite aprovechar la biblioteca estándar para nuestros propósitos. El primer paso es generar un desafío pseudoaleatorio para que el usuario lo firme. Pasaremos el desafío al usuario para que firme mediante Metamask y luego verificaremos el resultado enviado por el usuario en el servidor.

Hay muchas cosas que están sucediendo aquí, pero vamos a desglosarlo un poco, comenzando con la definición de TypedData :

Aquí estamos definiendo los campos de las dos estructuras de datos que pasaremos en nuestro desafío a Metamask para la firma: un Challenge y un EIP712Domain . El primero está completamente definido por nosotros, el segundo sigue la estructura de separador de dominio sugerida del estándar EIP-712. El separador de dominios está diseñado para garantizar que los mensajes firmados por los clientes solo se puedan usar para dApps específicas (y sus correspondientes contratos en cadena). Dado que nuestro caso de uso particular no implica ninguna verificación en cadena, el dominio no fue tan crítico para hacerlo bien. Dicho esto, si está desarrollando una dApp y planea verificar firmas en contratos inteligentes, la estructura del dominio debe considerarse cuidadosamente para garantizar que tenga el espacio de nombres adecuado tanto para fines de seguridad como de control de versiones.

A continuación, especificamos el tipo principal. Este es simplemente el nombre de la estructura de datos que contiene la esencia de nuestro mensaje EIP-712; en nuestro caso, esta es nuestra estructura Challenge previamente definida.

¡Finalmente, llenamos los espacios en blanco !. La clave Domain establece los valores para cada uno de los campos que especificamos en nuestra definición de estructura de dominio, y la clave Message establece los valores para cada uno de los campos que especificamos en nuestra type & # x27; s ( Challenge ) definición de estructura.

En este punto, tenemos una carga útil EIP-712 completa, pero aún no hemos terminado. Necesitaremos tomar un hash de sus datos para luego verificar la firma que recibimos del usuario. Desafortunadamente, esto fue un poco más complicado de entender de lo que parecía originalmente, sin embargo, el resultado no es demasiado complicado:

Tenga en cuenta que ignoramos el segundo valor de cada una de las llamadas a HashStruct (su respuesta de error). En el código de producción, recomendamos encarecidamente que compruebe estos valores de error y los maneje en consecuencia.

Con esa advertencia fuera del camino, analicemos un poco este fragmento de código. Al firmar un mensaje EIP-712, el cliente no firma la carga útil completa; en su lugar, solo firma una versión hash keccak256 de su contenido. Afortunadamente, la biblioteca estándar de geth maneja la mayor parte de este proceso por nosotros, sin embargo, el paso final de hash de los datos y unir todo queda en nosotros.

Las dos primeras líneas manejan la codificación y el hash de nuestro dominio y estructuras de tipo primario. Esto es relativamente sencillo, simplemente llamando a HashStruct con el nombre de la estructura y un mapa de sus datos.

Una vez que hayamos codificado las dos estructuras, necesitaremos formatearlas en una cadena de bytes compatible con EIP-712. Si tiene curiosidad por saber por qué es necesario \ x19 \ x01 , no dude en sumergirse en esta sección de las especificaciones.

Ahora que tenemos la cadena de bytes, simplemente la hash y ¡voilá! Ahora tenemos todo lo que necesitamos para completar nuestro proceso de verificación. En este punto, debe serializar la estructura signerData en JSON y enviarla a su cliente para que la firme. Además, & # x27; deberá almacenar el challengeHash en una base de datos para recuperarlo más tarde una vez que el usuario haya enviado su firma para su verificación.

Firma de la carga útil con Metamask

Ahora que hemos generado la carga útil de firma en nuestro backend, supongo que ha encontrado algún medio para enviarlo al navegador de su usuario para firmar. Esta pieza del rompecabezas es un poco más sencilla, solo introduce la carga útil en Metamask y recupera la firma resultante:

Una vez más, analicemos un poco este fragmento de código:

En esta primera cláusula vamos a pasar por el baile estándar de Metamask: asegurarnos de que el usuario tenga la extensión instalada, pedir su permiso para conectarse a la API de Ethereum y recuperar una matriz de sus direcciones de billetera (las cuentas variable) en caso de éxito.

Tenga en cuenta que una implementación más completa anticiparía a los usuarios que tienen más de una cuenta disponible y le presentaría al usuario una opción para seleccionar con qué cuenta desea firmar el mensaje. Este paso debe realizarse antes de generar el mensaje, ya que querrá asegurarse de que el campo Challenge.address de nuestro mensaje EIP-712 coincida con la dirección utilizada para firmarlo.

¿Te suena familiar? Básicamente, hemos copiado todos los datos que generamos previamente en nuestro backend Go y los hemos transferido a un objeto de aspecto similar en Javascript, luego pasamos esos datos al método eth_signTypedData_v3 RPC.

Tenga en cuenta que eth_signTypedData_v3 es un poco complicado al momento de escribir esto. Para empezar, requiere que la persona que llama envíe una cadena codificada en JSON como segundo parámetro en lugar de un objeto simple de Javascript. Esto debería cambiar en versiones futuras, por lo que si está leyendo esto y recibe un error, debe verificar la documentación más actualizada y / o intentar llamarla sin codificación JSON. el objeto data . Además, notamos un error con campos de tipo cadena en la definición de estructura del mensaje: si comienza con un 0x , la biblioteca de firma de Metamask lo convertirá automáticamente en una cadena de bytes sin previo aviso, desviando su firma y resultando en un mal momento para usted. Este RP aborda el problema, pero si usted o sus usuarios tienen una versión anterior de Metamask, deben tener en cuenta este problema.

¡Un último paso! Veamos una implementación de ejemplo de onSignatureComplete :

Esta implementación es un poco una exhibición educativa siguiendo el ejemplo de Metamask en su entrada de blog de introducción. La parte más importante de esto es configurar la variable firma , ya que querrá cortar los dos primeros caracteres antes de enviarlo de vuelta a Go para su verificación.

¡Uf! Eso se puso un poco complicado, pero finalmente tenemos una firma de Metamask que enviamos a nuestro backend. El paso final en el proceso de desafío / respuesta será verificar esta firma recién horneada en nuestro backend.

Verificación de la firma en Go

De vuelta en nuestra base de código del lado del servidor, supongo que tiene algún tipo de medio para recibir la firma:

Una vez más, analicemos este fragmento:

Para empezar, vamos a convertir la firma codificada en hexadecimal (directamente desde Metamask) en un segmento de [] byte , que es el tipo de datos que necesitaremos para manipularlo correctamente. A continuación, realizamos dos verificaciones de la firma, comprobando que tenga la longitud correcta y asegurándonos de que su ID de recuperación (el último byte) esté establecido en 27 o 28. La última comprobación es para garantizar que la firma cumpla con el & quot; legado motivos & quot; especificado en la definición de la función Ecrecover dentro de Geth. Continuando con la especificación, restamos 27 del ID de recuperación para convertirlo en un 0 o 1 , otra rareza de la función Ecrecover .

¡Finalmente, aquí es donde ocurre la magia! Usamos la función Ecrecover de geth para derivar una clave pública a partir de la firma proporcionada. Si la dirección de Ethereum de esta clave pública coincide con la dirección de Ethereum de nuestro usuario, ¡estamos listos! El mensaje se ha verificado correctamente. Si la clave pública difiere, sabemos que la firma no es válida, ya sea debido a una carga útil mal formada o una clave de firma incorrecta.

Conclusión

Esperamos que esta publicación de blog lo ayude en cualquier viaje futuro que pueda realizar a través del complicado mundo de la verificación de firmas EIP-712. Este proceso fue mucho más difícil de lo que pensamos originalmente cuando nos pusimos en marcha, pero esperamos que documentar nuestro dolor ayude a ahorrar tiempo a otros.

¡Eso es todo por esta guía! Si tiene comentarios, preguntas o correcciones, no dude en comunicarse conmigo en Twitter, mi nombre de usuario es @stevenleeg.

Un agradecimiento especial a Kumavis de Metamask por pasar horas en el teléfono investigando las bases de código de Geth y Metamask para que esto funcione correctamente.

Visite el sitio web de Alpine

Síguenos en Twitter

Nada de lo contenido en este artículo debe tomarse como asesoramiento legal o de inversión.