Saltar al contenido
ecdsa.com
Node.js · WebCrypto

ethers.js

ethers.js v6, la veterana biblioteca JavaScript para billeteras y contratos de Ethereum: firmas r‖s‖v de 65 bytes con v = 27/28, hash de mensajes EIP-191, nonces RFC 6979 y rechazo tajante de las firmas high-S.
Licencia
MIT
Versión comprobada
6.17.0 ·
Código fuente
github.com/ethers-io/ethers.js
Documentación
docs.ethers.org/v6/

Compatibilidad de un vistazo

ECDSAEthereum y Bitcoin

Curvas ECDSA
secp256k1
EdDSA
none
Formatos de clave pública
sec1-uncompressedsec1-compressedraw-xy

ethers cubre todo lo que una aplicación necesita para comunicarse con Ethereum —proveedores, contratos, codificación ABI, ENS—, y sus clases Wallet y SigningKey son lo que usa la mayor parte del código JavaScript para firmar. La aritmética de curvas procede de @noble/curves; ethers 6.17.0 fija su versión 1.2.0.

wallet.signMessage() implementa personal_sign: antepone al mensaje \x19Ethereum Signed Message:\n y su longitud (EIP-191), calcula el hash con keccak-256 y devuelve 65 bytes de r ‖ s ‖ v con v = 27 o 28. La comprobación contrastó el resumen con una implementación independiente de keccak y la firma con la salida RFC 6979 de @noble/curves, así que la misma clave y el mismo mensaje dan siempre los mismos bytes. Para firmar un hash que ya tienes calculado —un resumen EIP-712, el hash de una transacción—, usa SigningKey.sign(), que recibe exactamente 32 bytes y no añade ningún prefijo; cualquier otra cosa falla con invalid digest length.

Con la entrada, ethers es flexible en la estructura y estricto en la forma. verifyMessage() recuperó al firmante a partir de v = 27/28, de v = 0/1 y de la forma compacta EIP-2098 de 64 bytes, pero rechazó la firma high-S reflejada con non-canonical s; use ._s, aunque es matemáticamente válida. Las firmas producidas por OpenSSL, un KMS en la nube o módulos de hardware que no normalizan s deben convertirse a low-S (s → n − s, invertir v) antes de que ethers las acepte; el verificador de firmas de Ethereum muestra en qué forma está una firma.

La mayoría de los errores de verificación en los inicios de sesión con billetera no tienen nada que ver con la curva: un mensaje que difiere en un salto de línea o en un espacio final, un ID de cadena distinto en un dominio EIP-712, o una billetera de contrato inteligente cuyas firmas necesitan ERC-1271 en lugar de ecrecover. Inspecciona los datos tipados con el inspector de EIP-712 y los mensajes de inicio de sesión completos con el verificador de SIWE.

El ejemplo firma un mensaje de inicio de sesión, recupera la dirección y muestra la forma compacta; compara las direcciones recuperadas, nunca los bytes de la firma, porque la misma firma tiene varias codificaciones válidas.

Datos y las comprobaciones que los respaldan

Nada de lo que sigue está copiado de la documentación. La comprobación content/libraries/verify/ethers/run.sh ejecutó ethers.js 6.17.0 en Node.js 24.22.0; ethers depends on @noble/curves 1.2.0 en la imagen de contenedor node:24.22.0-bookworm-slim el 20 de octubre de 2026; la página debe coincidir con lo que imprimió, y cada dato cita esas líneas.

  • Codificación de la firma ECDSA

    rsv-65

    signMessage() devuelve 65 bytes en hexadecimal: r, s y el byte de recuperación v.

    Registrado por la comprobación
    fact ecdsa-signature-encoding = rsv-65
  • ID de recuperación v

    27-28

    v es 27 o 28 en las firmas que produce ethers.

    Registrado por la comprobación
    fact recovery-v = 27-28
  • Prefijo del mensaje

    eip-191

    hashMessage() es igual al keccak-256 de \x19Ethereum Signed Message:\n, la longitud en bytes en decimal y el mensaje, calculado de forma independiente con @noble/hashes.

    Registrado por la comprobación
    fact message-prefix = eip-191
  • Hash predeterminado

    keccak256

    Las firmas de mensajes usan keccak-256, no SHA-256.

    Registrado por la comprobación
    fact ecdsa-default-hash = keccak256
  • De qué calcula el hash la llamada de firma

    hashes-message-or-digest

    signMessage() calcula el hash del mensaje con el prefijo EIP-191; SigningKey.sign() firma un resumen de 32 bytes sin modificarlo y produjo la misma firma para ese resumen.

    Registrado por la comprobación
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: SigningKey.sign over the EIP-191 digest equals signMessage: true; a 2-byte input: invalid digest length
    evidence ecdsa-prehash: recoverAddress(digest, signature) returns the signer: true
  • Nonce de ECDSA

    rfc6979

    RFC 6979: la firma coincidió con la firma determinista de @noble/curves sobre el mismo resumen.

    Registrado por la comprobación
    fact ecdsa-nonce = rfc6979
  • Low-S al firmar

    low-s

    Ningún valor high-S entre 64 firmas de mensajes.

    Registrado por la comprobación
    fact ecdsa-low-s-sign = low-s
  • High-S al verificar

    rejects-high-s

    La firma reflejada (r, n − s) con v invertido la rechazan verifyMessage() y Signature.from() con non-canonical s; use ._s.

    Registrado por la comprobación
    fact ecdsa-low-s-verify = rejects-high-s
    evidence ecdsa-low-s-verify: verifyMessage with (r, n - s, flipped v): non-canonical s; use ._s
    evidence ecdsa-low-s-verify: Signature.from on the high-S twin: non-canonical s; use ._s
  • Formas de firma aceptadas

    v-27-28v-0-1eip-2098

    verifyMessage() recuperó al firmante a partir de firmas de 65 bytes con v = 27/28 o v = 0/1 y de la codificación compacta EIP-2098 de 64 bytes.

    Registrado por la comprobación
    fact signature-input-shapes = v-27-28, v-0-1, eip-2098
    evidence signature-input-shapes: 65-byte v 27/28: recovers the signer
    evidence signature-input-shapes: 65-byte v 0/1: recovers the signer
    evidence signature-input-shapes: 64-byte EIP-2098: recovers the signer
  • Curvas ECDSA

    secp256k1

    Solo secp256k1.

    Registrado por la comprobación
    fact ecdsa-curves = secp256k1
  • EdDSA

    none

    Sin EdDSA.

    Registrado por la comprobación
    fact eddsa = none
  • Algoritmos poscuánticos

    none

    Sin algoritmos poscuánticos.

    Registrado por la comprobación
    fact pqc = none
  • Formatos de clave pública

    sec1-uncompressedsec1-compressedraw-xy

    SigningKey.computePublicKey() convirtió puntos SEC1 entre el formato comprimido y el formato sin comprimir, y también aceptó un x‖y de 64 bytes sin el prefijo 0x04.

    Registrado por la comprobación
    fact public-key-formats = sec1-uncompressed, sec1-compressed, raw-xy
    evidence public-key-formats: 64-byte x||y: true

Uso

Firma un mensaje de inicio de sesión con la semántica de personal_sign, recupera al firmante y muestra las formas de 65 bytes y EIP-2098.
import { Signature, Wallet, verifyMessage } from "ethers";

const wallet = Wallet.createRandom();
const message = "Sign in to example.com\nNonce: 8f2c1a";

// personal_sign: EIP-191 prefix + keccak256, RFC 6979 nonce, low-S, v = 27 or 28.
const signature = await wallet.signMessage(message);
const { v, compactSerialized } = Signature.from(signature);

// Recover the address and compare it; never compare signature bytes.
const signer = verifyMessage(message, signature);

console.log("signature bytes:", (signature.length - 2) / 2, "v:", v);
console.log("signer matches:", signer === wallet.address);
console.log("EIP-2098 compact bytes:", (compactSerialized.length - 2) / 2);

El arnés de pruebas de fragmentos de código ejecuta cada ejemplo tal cual en la imagen de contenedor node:24.22.0-bookworm-slim, y la ejecución falla si la salida no coincide.

Configuración
npm init -y >/dev/null && npm install --no-audit --no-fund ethers@6.17.0
Comando
node usage.mjs
La salida debe coincidir (expresión regular)
signature bytes: 65 v: 2[78]\nsigner matches: true\nEIP-2098 compact bytes: 64

Bibliotecas