Saltar al contenido
ecdsa.com
Node.js

Node.js node:crypto

El módulo crypto integrado de Node.js: firmas ECDSA, EdDSA, RSA y poscuánticas sobre el OpenSSL que incluye cada versión de Node.js, con firmas DER, nonces aleatorios y sin política low-S por defecto.
Licencia
MIT
Versión comprobada
24.22.0 ·
Código fuente
github.com/nodejs/node
Documentación
nodejs.org/api/crypto.html

Compatibilidad de un vistazo

ECDSAEdDSACriptografía poscuántica

Curvas ECDSA
P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
Ed25519Ed448
Algoritmos poscuánticos
ML-KEMML-DSASLH-DSA
Formatos de clave pública
spki-pemspki-derx509jwksec1-uncompressedsec1-compressed
Formatos de clave privada
pkcs8-pempkcs8-derencrypted-pkcs8sec1-pemsec1-derjwkraw-scalar

node:crypto es el módulo de criptografía que se distribuye con cada versión de Node.js. Sus funciones de una sola llamada crypto.sign() y crypto.verify() y la clase KeyObject cubren ECDSA, Ed25519, Ed448, RSA y, en Node.js 24.22, también ML-DSA, SLH-DSA y ML-KEM. Todo funciona sobre el OpenSSL que incluye Node.js (3.5.8 en Node.js 24.22.0), así que la mayor parte del comportamiento coincide con el de la línea de comandos de OpenSSL y difiere del de la API WebCrypto que el mismo proceso expone como crypto.subtle.

El problema de interoperabilidad con el que más probablemente te encontrarás es la codificación de la firma. crypto.sign() devuelve DER ASN.1, mientras que WebCrypto, JWS/JWT, .NET y la mayoría de las billeteras de hardware usan la forma r‖s en bruto de ancho fijo. Una firma en la otra codificación no lanza ninguna excepción: crypto.verify() simplemente devuelve false, algo indistinguible de una firma falsificada. Establece dsaEncoding: "ieee-p1363" en el objeto de clave tanto en el lado que firma como en el que verifica, o convierte los bytes con el conversor DER ⇄ en bruto.

Otros dos valores predeterminados, menos visibles, importan cuando el otro lado es estricto. Las firmas nunca se normalizan a low-S, así que los verificadores que lo exigen, como libsecp256k1 y @noble/curves 2.x, rechazan aproximadamente la mitad. Y el nonce es aleatorio, de modo que la misma clave y el mismo mensaje dan bytes distintos en cada llamada: en las pruebas, compara los resultados de la verificación, nunca los bytes de la firma. Si un firmante llega a repetir un nonce, el comprobador de reutilización de nonces recupera la clave; por eso un nonce aleatorio necesita una fuente de aleatoriedad fiable.

Si pasas null como algoritmo, una clave EC firma en silencio con SHA-256, sea cual sea su curva; una clave P-384 falla entonces con cualquier implementación que espere SHA-384. Indica el hash en cada llamada, carga las claves una sola vez con crypto.createPublicKey() o crypto.createPrivateKey() —leen PEM, DER, JWK e incluso el punto SEC1 sin envoltorio— y conserva el KeyObject resultante en lugar de volver a analizar cadenas.

Para comprobar una firma generada en otra parte, pégala junto con la clave en el verificador de firmas; para ver qué valores predeterminados chocan con otra pila, elige ambos lados en la matriz de interoperabilidad de bibliotecas.

Datos y las comprobaciones que los respaldan

Nada de lo que sigue está copiado de la documentación. La comprobación content/libraries/verify/node-crypto/run.sh ejecutó Node.js node:crypto 24.22.0 en OpenSSL 3.5.8 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

    der

    crypto.sign() devuelve una SEQUENCE DER ASN.1, normalmente de 70 a 72 bytes en P-256. Con dsaEncoding: "ieee-p1363" en el objeto de clave, devuelve en su lugar la forma r‖s fija de 64 bytes; la opción debe establecerse tanto en el lado que firma como en el que verifica.

    Registrado por la comprobación
    fact ecdsa-signature-encoding = der
    evidence ecdsa-signature-encoding: default output parses as DER: true; ieee-p1363 output length 64
  • Low-S al firmar

    either

    Las firmas no se normalizan: de 64 firmas hechas en la comprobación, al menos una tenía s en la mitad superior del rango, como cabe esperar de un nonce aleatorio.

    Registrado por la comprobación
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: high-S seen among 64 signatures
  • High-S al verificar

    accepts-high-s

    crypto.verify() aceptó una firma cuyo s se había sustituido por n − s, en DER y en formato en bruto. Solo una implementación que exija low-S lo rechazará.

    Registrado por la comprobación
    fact ecdsa-low-s-verify = accepts-high-s
  • Nonce de ECDSA

    random

    Nonce aleatorio: firmar dos veces el mismo mensaje con la misma clave dio bytes distintos, y la clave de prueba del RFC 6979 no reprodujo la firma del RFC. Node.js no ofrece ninguna opción de ECDSA determinista.

    Registrado por la comprobación
    fact ecdsa-nonce = random
    evidence ecdsa-nonce: same input twice gives identical bytes: false; r equals RFC 6979 A.2.5: false
  • De qué calcula el hash la llamada de firma

    hashes-message

    Se calcula el hash de los datos con el algoritmo que indicas: la firma se verificó contra el SHA-256 del mensaje en una implementación independiente. No hay ninguna llamada que firme un resumen precalculado, así que pasa el propio mensaje.

    Registrado por la comprobación
    fact ecdsa-prehash = hashes-message
    evidence ecdsa-prehash: signature verifies against SHA-256(message) in @noble/curves: true
  • Hash predeterminado

    sha256

    Con null o undefined como algoritmo, una clave EC firma con SHA-256 independientemente de la curva. Indica el hash explícitamente para las claves P-384 y P-521.

    Registrado por la comprobación
    fact ecdsa-default-hash = sha256
  • Firma ECDSA mal formada

    returns-false

    Una firma r‖s en bruto donde se espera DER, DER donde se espera en bruto y datos basura sin más hacen que crypto.verify() devuelva false; nada lanza una excepción, así que un problema de formato parece una firma incorrecta.

    Registrado por la comprobación
    fact ecdsa-malformed-signature = returns-false
  • Curvas ECDSA

    P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1

    Con P-224, P-256, P-384, P-521, secp256k1 y las curvas Brainpool r1 se generaron claves y se completó un ciclo de firma y verificación. crypto.getCurves() enumera los nombres de OpenSSL, como prime256v1 para P-256.

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

    Ed25519Ed448

    Ed25519 y Ed448 firman mediante crypto.sign(null, data, key); Ed448 también acepta una opción context.

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

    ML-KEMML-DSASLH-DSA

    ML-KEM-512/768/1024 (con crypto.encapsulate() y crypto.decapsulate()), ML-DSA-44/65/87 y SLH-DSA funcionaron en Node.js 24.22.0, que incluye OpenSSL 3.5.8.

    Registrado por la comprobación
    fact pqc = ML-KEM, ML-DSA, SLH-DSA
  • Formatos de clave pública

    spki-pemspki-derx509jwksec1-uncompressedsec1-compressed

    crypto.createPublicKey() leyó SPKI en PEM y en DER, un certificado X.509 en PEM, una JWK y el punto SEC1 sin envoltorio —sin comprimir o comprimido— con format: "raw-public". Se rechazó un x‖y de 64 bytes sin el prefijo 0x04.

    Registrado por la comprobación
    fact public-key-formats = spki-pem, spki-der, x509, jwk, sec1-uncompressed, sec1-compressed
  • Formatos de clave privada

    pkcs8-pempkcs8-derencrypted-pkcs8sec1-pemsec1-derjwkraw-scalar

    crypto.createPrivateKey() leyó PKCS#8 en PEM y en DER, PKCS#8 cifrado con frase de contraseña, SEC1 EC PRIVATE KEY en PEM y en DER, una JWK con d y el escalar de 32 bytes sin envoltorio con format: "raw-private".

    Registrado por la comprobación
    fact private-key-formats = pkcs8-pem, pkcs8-der, encrypted-pkcs8, sec1-pem, sec1-der, jwk, raw-scalar

Uso

Firma y verifica con P-256 indicando el hash y la codificación de la firma en ambos lados; aquí, r‖s en bruto para una implementación WebCrypto o JOSE.
import { generateKeyPairSync, sign, verify } from "node:crypto";

const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
const message = Buffer.from("order #1042 approved");

// Name the hash and the encoding explicitly, on both sides:
// "der" for OpenSSL, Java and Go peers, "ieee-p1363" (raw r||s) for WebCrypto and JOSE.
const signature = sign("sha256", message, { key: privateKey, dsaEncoding: "ieee-p1363" });
const ok = verify("sha256", message, { key: publicKey, dsaEncoding: "ieee-p1363" }, signature);

console.log("signature bytes:", signature.length);
console.log("verified:", ok);

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.

Comando
node usage.mjs
La salida debe coincidir (expresión regular)
signature bytes: 64\nverified: true

Bibliotecas