Saltar al contenido
ecdsa.com
Node.js · WebCrypto

jose (panva)

jose, de Filip Skokan: una implementación de JOSE sin dependencias (JWS, JWE, JWK, JWKS, JWT) sobre WebCrypto que funciona sin cambios en Node.js, navegadores, Deno, Bun y entornos de ejecución edge, con EdDSA y ML-DSA, pero sin secp256k1.
Licencia
MIT
Versión comprobada
6.2.13 ·
Código fuente
github.com/panva/jose
Documentación
github.com/panva/jose/tree/main/docs

Compatibilidad de un vistazo

JOSE y JWT

Algoritmos JWS
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSAEd25519ML-DSA-44ML-DSA-65ML-DSA-87

jose cubre toda la familia JOSE —tokens firmados y cifrados, importación y exportación de JWK, conjuntos de JWK locales y remotos— con una sola API en todos los entornos de ejecución de JavaScript. Desde la versión 6 hace toda la criptografía a través de WebCrypto, así que las claves son objetos CryptoKey (se siguen aceptando instancias KeyObject de Node.js), y la biblioteca hereda la lista de curvas de WebCrypto: ES256, ES384 y ES512, EdDSA y Ed25519, y en la 6.2.13 los algoritmos ML-DSA allí donde el entorno de ejecución los ofrece. ES256K y Ed448 se rechazan con ERR_JOSE_NOT_SUPPORTED.

Sus valores predeterminados son estrictos donde importa. jwtVerify() nunca acepta alg: none —los tokens sin firmar solo se pueden leer con la clase aparte UnsecuredJWT— y cada clave está ligada a una familia de algoritmos, así que un token HS256 presentado con una clave pública EC, o un token RS256 con una clave EC, falla con un TypeError en lugar de verificarse. ES256 también rechaza una clave P-384. Pasa algorithms de todos modos: con un conjunto de claves que contiene varios tipos de clave, es lo que impide que un emisor cambie de algoritmo en silencio.

Dos valores predeterminados tiran en sentidos opuestos. clockTolerance es cero, lo que es estricto, y exp es opcional, lo que no lo es: un token sin caducidad se verifica salvo que la incluyas en requiredClaims, como hace el ejemplo. Un JWS cuya firma ECDSA llegó en DER —algo habitual cuando un token se arma a mano con un KMS en la nube o con openssl dgst— falla con el error genérico signature verification failed, sin nada que apunte a la codificación; el decodificador de JWT muestra la longitud de la firma de un vistazo y el conversor DER ⇄ en bruto la corrige.

La importación de claves es explícita: importSPKI e importPKCS8 reciben cadenas PEM —los bytes DER se rechazan con "spki" must be SPKI formatted string—, importX509 recibe un certificado PEM e importJWK, una JWK. importPKCS8 rechaza los archivos SEC1 EC PRIVATE KEY que OpenSSL escribe por defecto; conviértelos antes a PKCS#8, por ejemplo con el conversor de JWK.

Para verificar tokens de un proveedor de identidad, usa createRemoteJWKSet con el jwks_uri del proveedor: guarda las claves en caché, vuelve a descargarlas ante un kid desconocido y aplica un límite de frecuencia a esas descargas. Comprueba qué publica realmente el proveedor con el comprobador de JWKS.

Datos y las comprobaciones que los respaldan

Nada de lo que sigue está copiado de la documentación. La comprobación content/libraries/verify/jose/run.sh ejecutó jose (panva) 6.2.13 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.

  • Algoritmos JWS

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSAEd25519ML-DSA-44ML-DSA-65ML-DSA-87

    Con HS, RS, PS y ES de 256/384/512 bits, EdDSA y Ed25519, y ML-DSA-44/65/87 (en Node.js 24.22) se firmó y se verificó en la comprobación; ES256K y Ed448 lanzaron ERR_JOSE_NOT_SUPPORTED.

    Registrado por la comprobación
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, EdDSA, Ed25519, ML-DSA-44, ML-DSA-65, ML-DSA-87
    evidence jws-algorithms: ES256K: ERR_JOSE_NOT_SUPPORTED: Invalid or unsupported "alg" (Algorithm) value
    evidence jws-algorithms: Ed448: ERR_JOSE_NOT_SUPPORTED: Invalid or unsupported "alg" (Algorithm) value
  • Codificación de la firma ES256

    raw

    Las firmas ES256 de los tokens compactos son 64 bytes de r‖s en bruto, la forma que exige el RFC 7518.

    Registrado por la comprobación
    fact jws-ecdsa-encoding = raw
  • Firma DER dentro de un JWS

    rejected

    Un token cuya firma ES256 se volvió a codificar en DER falla con ERR_JWS_SIGNATURE_VERIFICATION_FAILED, el mismo error que una firma falsificada.

    Registrado por la comprobación
    fact jws-der-signature = rejected
    evidence jws-der-signature: ERR_JWS_SIGNATURE_VERIFICATION_FAILED: signature verification failed
  • Lista de algoritmos permitidos

    from-key

    Sin algorithms, se permite cualquier algoritmo que la clave pueda ejecutar; un token RS256 contra una clave EC falló con CryptoKey does not support this operation, its algorithm.name must be RSASSA-PKCS1-v1_5.

    Registrado por la comprobación
    fact jwt-algorithm-allowlist = from-key
    evidence jwt-algorithm-allowlist: RS256 token against an EC key: TypeError: CryptoKey does not support this operation, its algorithm.name must be RSASSA-PKCS1-v1_5
  • Tokens sin firma (alg none)

    rejected

    jwtVerify() rechaza alg: none incluso cuando none figura en algorithms; los tokens no protegidos solo se pueden decodificar con UnsecuredJWT.decode().

    Registrado por la comprobación
    fact jwt-alg-none = rejected
    evidence jwt-alg-none: jwtVerify: ERR_JOSE_NOT_SUPPORTED: alg none is not supported either by JOSE or your javascript runtime
    evidence jwt-alg-none: jwtVerify with algorithms ["none"]: ERR_JOSE_NOT_SUPPORTED: alg none is not supported either by JOSE or your javascript runtime
    evidence jwt-alg-none: UnsecuredJWT.decode: returned sub fact-check
  • Clave pública usada como secreto HMAC

    rejected

    Un token HS256 con el texto PEM de la clave pública como clave falla contra esa misma clave pública con CryptoKey instances must be of type "secret" for the HS256 algorithm, con o sin lista de algoritmos.

    Registrado por la comprobación
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: no list: TypeError: CryptoKey instances must be of type "secret" for the HS256 algorithm
    evidence jwt-hmac-public-key: algorithms ["HS256"]: TypeError: CryptoKey instances must be of type "secret" for the HS256 algorithm
  • Curva cotejada con alg

    enforced

    Firmar ES256 con una clave P-384 falla con its algorithm.namedCurve must be P-256.

    Registrado por la comprobación
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: TypeError: CryptoKey does not support this operation, its algorithm.namedCurve must be P-256
  • Desfase de reloj predeterminado

    0s

    Sin tolerancia por defecto: se rechazó un token que había caducado 3 segundos antes; configura clockTolerance para admitir el desfase de reloj.

    Registrado por la comprobación
    fact jwt-clock-skew = 0s
  • Claim de caducidad obligatorio

    no

    Un token sin exp se verificó; añade requiredClaims: ["exp"] para exigirlo.

    Registrado por la comprobación
    fact jwt-exp-required = no
  • Formatos de clave pública

    spki-pemx509jwk

    importSPKI (cadena PEM), importX509 (certificado PEM) e importJWK funcionaron; los bytes DER pasados a importSPKI se rechazaron. También se aceptaron directamente instancias KeyObject de Node.js.

    Registrado por la comprobación
    fact public-key-formats = spki-pem, x509, jwk
    evidence public-key-formats: spki-der: TypeError: "spki" must be SPKI formatted string
    evidence public-key-formats: KeyObject from node:crypto accepted as a key: true
  • Formatos de clave privada

    pkcs8-pemjwk

    importPKCS8 recibe una cadena PEM PKCS#8 e importJWK, una JWK con d; un PEM SEC1 EC PRIVATE KEY se rechazó con "pkcs8" must be PKCS#8 formatted string.

    Registrado por la comprobación
    fact private-key-formats = pkcs8-pem, jwk
    evidence private-key-formats: sec1-pem: TypeError: "pkcs8" must be PKCS#8 formatted string

Uso

Emite un token ES256 con un ID de clave y verifícalo después mediante un conjunto de JWK, con el algoritmo, el emisor, la audiencia y la caducidad obligatorios.
import * as jose from "jose";

const { privateKey, publicKey } = await jose.generateKeyPair("ES256");
const jwk = { ...(await jose.exportJWK(publicKey)), kid: "2026-10", alg: "ES256" };

const token = await new jose.SignJWT({ sub: "user-42" })
  .setProtectedHeader({ alg: "ES256", kid: jwk.kid })
  .setIssuer("https://auth.example.com")
  .setAudience("api")
  .setExpirationTime("10m")
  .sign(privateKey);

// The key set picks the key by kid; createRemoteJWKSet(new URL(...)) does the same over HTTPS.
const keys = jose.createLocalJWKSet({ keys: [jwk] });
const { payload } = await jose.jwtVerify(token, keys, {
  algorithms: ["ES256"],
  issuer: "https://auth.example.com",
  audience: "api",
  requiredClaims: ["exp"],
});

console.log("sub:", payload.sub);

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 jose@6.2.13
Comando
node usage.mjs
La salida debe coincidir (expresión regular)
sub: user-42

Bibliotecas