Saltar al contenido
ecdsa.com
Node.js

elliptic

elliptic de Fedor Indutny, la biblioteca ECDSA y EdDSA en JavaScript puro que lleva una década detrás de bundles de navegador y billeteras: es determinista pero no aplica low-S por defecto, solo admite resúmenes como entrada y tiene abierto un aviso de seguridad por nonces erróneos en P-521.
Licencia
MIT
Versión comprobada
6.6.1 ·
Código fuente
github.com/indutny/elliptic
Documentación
github.com/indutny/elliptic#readme

Compatibilidad de un vistazo

ECDSAEdDSA

Curvas ECDSA
P-224P-256P-384P-521secp256k1
EdDSA
Ed25519
Formatos de clave pública
sec1-uncompressedsec1-compressed

elliptic implementa ECDSA sobre las curvas NIST y secp256k1, además de Ed25519, en JavaScript puro sobre bn.js. Se extendió a través de crypto-browserify, de bibliotecas antiguas de Ethereum y Bitcoin y de los polyfills de Node.js que Webpack 4 inyectaba en los bundles del navegador, y por eso sigue apareciendo en los archivos de bloqueo (lockfiles). ethers v6 y viem se basan en cambio en @noble/curves, que cubre las mismas curvas con valores predeterminados más estrictos.

La API se aparta de la mayoría de las bibliotecas en tres puntos que causan errores de interoperabilidad. sign() recibe el resumen, no el mensaje, y trata como resumen cualquier secuencia de bytes que reciba: en la comprobación, firmar un mensaje en bruto dio una firma distinta de la obtenida al firmar su SHA-256. Devuelve un objeto Signature con r, s y recoveryParam, así que el formato de transmisión es el que tú serialices: toDER() para implementaciones al estilo de OpenSSL, o ambos valores rellenados a 32 bytes para r‖s en bruto. Además, s no se normaliza salvo que pases canonical: true, mientras que la verificación acepta ambas mitades.

Los nonces siguen el RFC 6979 —el vector de prueba de P-256 se reprodujo exactamente—, pero el aviso de seguridad GHSA-848j-6mx2-7j84 (CVE-2025-14505), todavía abierto para la 6.6.1, informa de que un valor intermedio se trunca cuando tiene bytes cero iniciales. La comprobación lo reprodujo: en P-521 con SHA-512, 9 de 2048 entradas fijas produjeron un nonce distinto del de RFC 6979, mientras que secp256k1 con SHA-256 y P-256 con SHA-512 no mostraron ninguna diferencia en 4096 entradas cada una. Esas firmas siguen verificándose, pero firmar el mismo mensaje con la misma clave en otra implementación de RFC 6979 produce nonces relacionados, lo que, según el aviso, puede exponer la clave. Evita elliptic para P-521 y consulta el comprobador de reutilización de nonces para entender por qué importan los nonces relacionados.

Los avisos de seguridad anteriores se corrigieron en las versiones 6.5.7 a 6.6.1; entre ellos, la aceptación de firmas codificadas en BER y un fallo que permitía extraer la clave cuando se firmaba una cadena en lugar de un hash. En la comprobación, la 6.6.1 rechazó una firma DER con un cero inicial redundante. Las claves son cadenas hexadecimales o arrays de bytes —puntos SEC1, comprimidos o sin comprimir, y escalares en bruto—, sin compatibilidad con PEM, SPKI ni JWK.

El ejemplo calcula primero el hash, pide una firma canónica low-S y muestra ambas serializaciones. Cuando una firma de elliptic falla en otra parte, compara sus dos codificaciones en el conversor DER ⇄ en bruto.

Datos y las comprobaciones que los respaldan

Nada de lo que sigue está copiado de la documentación. La comprobación content/libraries/verify/elliptic/run.sh ejecutó elliptic 6.6.1 en Node.js 24.22.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

    structured

    sign() devuelve un objeto con r, s y recoveryParam; toDER() produce DER ASN.1, y el r‖s en bruto hay que montarlo a partir de los dos valores rellenados al tamaño de la curva.

    Registrado por la comprobación
    fact ecdsa-signature-encoding = structured
    evidence ecdsa-signature-encoding: sign() returns an object with r, s and recoveryParam 0; toDER() gives 72 bytes that parse as DER: true
  • De qué calcula el hash la llamada de firma

    takes-digest

    La entrada de sign() y verify() se usa como resumen; firmar el mensaje en bruto produjo una firma distinta de la obtenida al firmar su SHA-256.

    Registrado por la comprobación
    fact ecdsa-prehash = takes-digest
  • Nonce de ECDSA

    rfc6979

    RFC 6979 con HMAC-DRBG: el vector de prueba de P-256 del apéndice A.2.5 se reprodujo exactamente, incluido su s alto (high-S).

    Registrado por la comprobación
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: r and s equal RFC 6979 A.2.5 (high s kept): true
  • Nonces que difieren del RFC 6979

    P-521/SHA-512

    En P-521 con SHA-512, 9 de 2048 entradas fijas usaron un nonce distinto del de RFC 6979 (la referencia fue @noble/curves); en secp256k1/SHA-256 y P-256/SHA-512 no apareció ninguna diferencia en 4096 entradas cada una. Esto coincide con el aviso de seguridad GHSA-848j-6mx2-7j84, abierto para todas las versiones hasta la 6.6.1.

    Registrado por la comprobación
    fact rfc6979-divergence = P-521/SHA-512
    evidence rfc6979-divergence: secp256k1 with SHA-256, 4096 fixed inputs: 0 nonces differ from RFC 6979, 0 of those signatures still verify
    evidence rfc6979-divergence: P-256 with SHA-512, 4096 fixed inputs: 0 nonces differ from RFC 6979, 0 of those signatures still verify
    evidence rfc6979-divergence: P-521 with SHA-512, 2048 fixed inputs: 9 nonces differ from RFC 6979, 9 of those signatures still verify
  • Low-S al firmar

    either

    No se normaliza por defecto: el vector de prueba del RFC volvió con su s alto (high-S). canonical: true devuelve n − s en su lugar.

    Registrado por la comprobación
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: canonical: true returns n - s: true
  • High-S al verificar

    accepts-high-s

    Se verificaron tanto la forma high-S como la forma low-S de la misma firma.

    Registrado por la comprobación
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: the RFC signature (high s) and its low-s twin both verify: true
  • Rigor del análisis DER

    rejects-non-minimal

    Una firma DER cuyo r llevaba un cero inicial redundante se rechazó con Signature without r or s, mientras que la codificación mínima de toDER() se verificó.

    Registrado por la comprobación
    fact der-strictness = rejects-non-minimal
    evidence der-strictness: the minimal DER from toDER() verifies when passed back as bytes: true
    evidence der-strictness: non-minimal INTEGER encoding: throws Signature without r or s
  • Firma ECDSA mal formada

    throws

    Un blob r‖s en bruto de 64 bytes o un array de 3 bytes pasados a verify() lanzan Signature without r or s en lugar de devolver false.

    Registrado por la comprobación
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: 64-byte raw r||s: throws Signature without r or s
    evidence ecdsa-malformed-signature: 3 bytes: throws Signature without r or s
  • Curvas ECDSA

    P-224P-256P-384P-521secp256k1

    Con P-224, P-256, P-384, P-521 y secp256k1 se firmó y se verificó; no hay curvas Brainpool.

    Registrado por la comprobación
    fact ecdsa-curves = P-224, P-256, P-384, P-521, secp256k1
  • EdDSA

    Ed25519

    Ed25519 mediante new elliptic.eddsa("ed25519").

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

    none

    Sin algoritmos poscuánticos.

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

    sec1-uncompressedsec1-compressed

    keyFromPublic() leyó puntos SEC1 en hexadecimal, comprimidos o sin comprimir, y un objeto con las coordenadas x e y en hexadecimal; un x‖y de 64 bytes sin el prefijo 0x04 se rechazó.

    Registrado por la comprobación
    fact public-key-formats = sec1-uncompressed, sec1-compressed
    evidence public-key-formats: an object of hex x and y coordinates also works: true; there is no PEM, SPKI, PKCS#8 or JWK parser
  • Formatos de clave privada

    raw-scalar

    Las claves privadas son el escalar en bruto en hexadecimal o en bytes; no hay analizador de PEM, PKCS#8 ni JWK.

    Registrado por la comprobación
    fact private-key-formats = raw-scalar

Uso

Calcula el hash del mensaje, fírmalo con una firma canónica low-S en secp256k1 y serializa la firma como DER y como r‖s en bruto.
import { createHash } from "node:crypto";
import elliptic from "elliptic";

const ec = new elliptic.ec("secp256k1");
const key = ec.genKeyPair();

// elliptic signs exactly the bytes it is given: hash the message yourself,
// and pass bytes — never a string.
const digest = createHash("sha256").update("order #1042 approved").digest();

// canonical: true keeps s in the lower half, which Bitcoin and noble verifiers require.
const signature = key.sign(digest, { canonical: true });

const der = Buffer.from(signature.toDER());
const raw = Buffer.concat([signature.r.toArrayLike(Buffer, "be", 32), signature.s.toArrayLike(Buffer, "be", 32)]);
console.log("low-S:", signature.s.cmp(ec.nh) <= 0);
console.log("verified:", key.verify(digest, der), "raw bytes:", raw.length);

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 elliptic@6.6.1
Comando
node usage.mjs
La salida debe coincidir (expresión regular)
low-S: true\nverified: true raw bytes: 64

Bibliotecas