Saltar al contenido
ecdsa.com
Node.js · WebCrypto

@noble/curves

La biblioteca de curvas elípticas para JavaScript de Paul Miller, auditada y sin dependencias: ECDSA en las curvas NIST, secp256k1 y Brainpool, EdDSA, Schnorr y más; desde la versión 2 es determinista, low-S y calcula el hash del mensaje por defecto.
Licencia
MIT
Versión comprobada
2.4.0 ·
Código fuente
github.com/paulmillr/noble-curves
Documentación
github.com/paulmillr/noble-curves#readme

Compatibilidad de un vistazo

ECDSAEdDSA

Curvas ECDSA
P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
Ed25519Ed25519phEd25519ctxEd448Ed448ph
Formatos de clave pública
sec1-uncompressedsec1-compressed

@noble/curves implementa la criptografía de curva elíptica en TypeScript puro, sin dependencias, y ha pasado varias auditorías. Es el motor que hay debajo de ethers v6, viem y muchas herramientas de Bitcoin y Nostr, y la respuesta habitual cuando a WebCrypto le falta una curva: abarca P-256, P-384, P-521, secp256k1 y las curvas Brainpool r1, Ed25519 y Ed448 con sus variantes pre-hash y con contexto, y las firmas Schnorr BIP-340.

Sus valores predeterminados son los estrictos. Una firma son 64 bytes de r‖s compacto, el nonce sigue el RFC 6979 —la comprobación reprodujo el vector de prueba P-256 del RFC— y s siempre se normaliza a la mitad baja. La verificación es igual de estricta: se rechazaron todas las firmas high-S que produjo node:crypto y se aceptaron todas las low-S. Las firmas de OpenSSL, Java, Go o WebCrypto son high-S aproximadamente la mitad de las veces, así que verificarlas requiere lowS: false, y una entrada DER requiere format: "der": sin esa opción, un blob DER de 70 bytes lanza un error de longitud.

La versión 2 cambió el significado del primer argumento. En 1.x, sign() y verify() recibían un hash calculado por ti; en 2.x calculan ellas mismas el hash del mensaje con la función hash de la curva, y la comprobación confirmó que la 1.9.7 solo reproduce el vector del RFC cuando recibe el SHA-256 del mensaje. Por eso, el código actualizado sin cambios calcula el hash dos veces y falla frente a todas las demás bibliotecas; pasa prehash: false donde de verdad tengas un resumen. La versión 1 tampoco normalizaba las firmas P-256, así que firmas que se verificaban en 1.x pueden fallar en 2.x.

Las claves son solo bytes en bruto: un escalar secreto de 32 bytes, y las claves públicas como puntos SEC1, comprimidos (33 bytes) por defecto y sin comprimir si se pide. SPKI, PKCS#8 y JWK no se leen, así que las claves procedentes de OpenSSL o de un JWKS hay que desempaquetarlas antes; el conversor de JWK y el laboratorio de curvas elípticas ayudan con eso. Este paquete no contiene código poscuántico; el mismo autor lo publica por separado.

El ejemplo muestra las dos opciones que hacen que noble sea interoperable con un firmante al estilo de OpenSSL —format y lowS— y la salida determinista de su propio firmante. Para comparar a mano una firma en distintas codificaciones, usa el conversor DER ⇄ en bruto; para firmas de billeteras, el verificador de firmas de Ethereum funciona con el mismo código de curvas.

Datos y las comprobaciones que los respaldan

Nada de lo que sigue está copiado de la documentación. La comprobación content/libraries/verify/noble-curves/run.sh ejecutó @noble/curves 2.4.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

    raw

    sign() devuelve 64 bytes de r‖s compacto; format: "der" devuelve DER ASN.1 en su lugar, y verify() necesita la misma opción para leerlo.

    Registrado por la comprobación
    fact ecdsa-signature-encoding = raw
    evidence ecdsa-signature-encoding: default 64 bytes; format "der" gives 71 bytes that parse as DER: true
  • Low-S al firmar

    low-s

    No apareció ningún valor high-S en 64 firmas en ninguna de las siete curvas de Weierstrass: s siempre se normaliza.

    Registrado por la comprobación
    fact ecdsa-low-s-sign = low-s
    evidence ecdsa-low-s-sign: no high-S in 64 signatures on any of the seven curves
  • High-S al verificar

    rejects-high-s

    Con las opciones predeterminadas, se rechazaron todas las firmas high-S hechas por node:crypto y se aceptaron todas las low-S; con lowS: false se verificaron las 64.

    Registrado por la comprobación
    fact ecdsa-low-s-verify = rejects-high-s
    evidence ecdsa-low-s-verify: 64 node:crypto signatures: every high-S one rejected: true; every low-S one accepted: true; all 64 accepted with lowS false: true
  • Nonce de ECDSA

    rfc6979

    RFC 6979: el vector de prueba P-256 del apéndice A.2.5 se reprodujo exactamente, con s sustituido por n − s. extraEntropy: true incorpora aleatoriedad (firmas hedged).

    Registrado por la comprobación
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: r matches RFC 6979 A.2.5: true; s equals n - s(RFC): true; extraEntropy true changes the bytes: true
  • De qué calcula el hash la llamada de firma

    hashes-message

    2.x calcula el hash del mensaje con la función hash de la curva (SHA-256 para P-256); prehash: false firma y verifica un resumen calculado por ti.

    Registrado por la comprobación
    fact ecdsa-prehash = hashes-message
  • Entrada de la firma en la versión 1

    digest

    En la 1.9.7, sign() trataba su entrada como el resumen: solo sign(sha256(m)) reprodujo el vector del RFC 6979. Actualizar desde 1.x sin cambiar las llamadas hace que el hash se calcule dos veces.

    Registrado por la comprobación
    fact v1-sign-input = digest
    evidence v1-sign-input: 1.x sign(sha256(m)) gives the RFC 6979 r: true; 1.x sign(m) does: false
  • Low-S de P-256 en la versión 1

    either

    La 1.9.7 devolvió sin cambios el valor high-S del RFC para P-256; la normalización en todas las curvas es nueva en 2.x.

    Registrado por la comprobación
    fact v1-p256-low-s-sign = either
    evidence v1-p256-low-s-sign: 1.x P-256 returned the RFC's high s unchanged: true
  • Firma ECDSA mal formada

    throws

    Una firma de longitud incorrecta —bytes DER sin format: "der" o un blob de 3 bytes— lanza expected Uint8Array of length 64; una firma bien formada pero incorrecta devuelve false.

    Registrado por la comprobación
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: DER without format: throws expected Uint8Array of length 64, got length=71
    evidence ecdsa-malformed-signature: 3 bytes: throws expected Uint8Array of length 64, got length=3
    evidence ecdsa-malformed-signature: 64 zero bytes: false
  • Curvas ECDSA

    P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1

    P-256, P-384 y P-521 desde nist.js, secp256k1 desde secp256k1.js y las curvas Brainpool r1 desde misc.js.

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

    Ed25519Ed25519phEd25519ctxEd448Ed448ph

    Con Ed25519 y sus variantes ph y ctx, Ed448 y Ed448ph se firmó y se verificó en todos los casos.

    Registrado por la comprobación
    fact eddsa = Ed25519, Ed25519ph, Ed25519ctx, Ed448, Ed448ph
  • Algoritmos poscuánticos

    none

    No hay algoritmos poscuánticos en este paquete.

    Registrado por la comprobación
    fact pqc = none
    evidence pqc: exports of the package mention ML-DSA or ML-KEM: false
  • Formatos de clave pública

    sec1-uncompressedsec1-compressed

    Las claves públicas son puntos SEC1, comprimidos o sin comprimir; un x‖y de 64 bytes y DER SPKI se rechazaron.

    Registrado por la comprobación
    fact public-key-formats = sec1-uncompressed, sec1-compressed
  • Formato de clave pública predeterminado

    sec1-compressed

    getPublicKey() devuelve el punto comprimido de 33 bytes salvo que pases false para obtener el punto sin comprimir.

    Registrado por la comprobación
    fact default-public-key = sec1-compressed
  • Formatos de clave privada

    raw-scalar

    Las claves secretas son el escalar en bruto de 32 bytes; DER PKCS#8 se rechazó.

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

    BIP-340

    schnorr en secp256k1.js implementa BIP-340: firmas de 64 bytes sobre claves públicas x-only.

    Registrado por la comprobación
    fact schnorr = BIP-340

Uso

Verifica una firma DER de node:crypto con las opciones que hacen a noble lo bastante tolerante para aceptarla y después firma de forma determinista con el propio noble.
import { generateKeyPairSync, sign } from "node:crypto";
import { p256 } from "@noble/curves/nist.js";

// A signature made elsewhere: node:crypto (OpenSSL) emits DER, with s in either half.
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
const message = new TextEncoder().encode("order #1042 approved");
const der = sign("sha256", message, privateKey);
const jwk = publicKey.export({ format: "jwk" });
const point = Uint8Array.from([4, ...Buffer.from(jwk.x, "base64url"), ...Buffer.from(jwk.y, "base64url")]);

// noble 2.x hashes the message itself and rejects high-S unless told otherwise.
console.log("verified:", p256.verify(der, message, point, { format: "der", lowS: false }));

// Its own signatures are compact r||s, RFC 6979 and low-S: the same bytes every time.
const secretKey = p256.utils.randomSecretKey();
const first = p256.sign(message, secretKey);
const second = p256.sign(message, secretKey);
console.log("deterministic:", Buffer.from(first).equals(Buffer.from(second)), first.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 @noble/curves@2.4.0
Comando
node usage.mjs
La salida debe coincidir (expresión regular)
verified: true\ndeterministic: true 64

Bibliotecas