Saltar al contenido
ecdsa.com

JWS frente a JWE: tokens JOSE firmados o cifrados

Un JWS demuestra quién creó una carga útil, pero cualquiera puede leerla. Un JWE oculta la carga útil a todos salvo a su destinatario, pero, con algoritmos de clave pública, no dice nada de quién la creó.

JWS frente a JWE es la cuestión de qué protege un token JOSE. Un JSON Web Signature (RFC 7515) añade integridad y autenticidad a una carga útil que sigue siendo legible; un JSON Web Encryption (RFC 7516) añade confidencialidad. Casi todos los JWT que te encuentras son JWS, lo que sorprende a quien espera que los claims sean secretos.

De un vistazo

JWSJWE
EspecificaciónRFC 7515RFC 7516
ProtegeIntegridad y origenConfidencialidad (e integridad mediante el AEAD)
Forma compactaheader.payload.signature (3 partes)header.encrypted_key.iv.ciphertext.tag (5 partes)
Algoritmos del encabezadoalg (por ejemplo, ES256, RS256, HS256, Ed25519)alg para la gestión de claves (ECDH-ES, ECDH-ES+A256KW, RSA-OAEP-256, A256KW, dir) más enc para el contenido (A256GCM, A128CBC-HS256)
Carga útil visibleSí, solo en base64urlNo
Remitente autenticadoSí (firma o MAC)Solo con gestión de claves simétrica o con acuerdo de claves autenticado; no con ECDH-ES ni con RSA-OAEP por sí solos
Tamaño para un conjunto de claims de 70 bytes (medido más abajo)186 caracteres (ES256)405 caracteres (ECDH-ES+A256KW con A256GCM)
Usos típicosTokens JWT de acceso y de ID, DPoP, solicitudes firmadas, SD-JWTTokens de ID cifrados, tokens almacenados en el cliente, algunas respuestas de FAPI y EUDI

Qué protege cada uno

Un JWS firma base64url(header) + "." + base64url(payload). Cualquiera que tenga el token puede decodificarlo y leer los claims; el decodificador de JWT hace exactamente eso. La firma demuestra que el token procede del titular de la clave y que nadie lo ha modificado, que es lo que necesita una API para confiar en los claims.

Un JWE genera una clave de cifrado de contenido (CEK) aleatoria, cifra la carga útil con el AEAD que indica enc y entrega la CEK al destinatario con el algoritmo de gestión de claves que indica alg: envuelta con RSA-OAEP, envuelta con AES Key Wrap tras un acuerdo de claves ECDH-ES, derivada directamente de ECDH-ES o compartida de antemano (dir). El encabezado protegido se autentica como datos asociados. Solo el destinatario puede leer la carga útil.

La asimetría importante: si la gestión de claves usa criptografía de clave pública, cualquiera que tenga la clave pública del destinatario puede crear un JWE válido. Que el descifrado funcione no demuestra nada sobre el remitente. Por eso los tokens sensibles en los que además hay que confiar se anidan: un JWT firmado (JWS) dentro de un JWE con cty: JWT, que se verifica después del descifrado.

Tamaños en la práctica

El script siguiente, ejecutado con jose 6.2.13 en Node.js 26.10, protege los mismos claims de las dos formas.

jose: los mismos claims como JWS y como JWE
import { CompactEncrypt, CompactSign, compactDecrypt, compactVerify, generateKeyPair } from "jose";

const claims = new TextEncoder().encode(JSON.stringify({ sub: "user-42", email: "alice@example.com", plan: "pro" }));

// JWS: integrity and origin; the payload is only base64url-encoded.
const signer = await generateKeyPair("ES256");
const jws = await new CompactSign(claims).setProtectedHeader({ alg: "ES256" }).sign(signer.privateKey);
await compactVerify(jws, signer.publicKey);

// JWE: confidentiality for the holder of the recipient's private key.
const recipient = await generateKeyPair("ECDH-ES+A256KW");
const jwe = await new CompactEncrypt(claims).setProtectedHeader({ alg: "ECDH-ES+A256KW", enc: "A256GCM" }).encrypt(recipient.publicKey);
const { plaintext } = await compactDecrypt(jwe, recipient.privateKey);

console.log("JWS parts:", jws.split(".").length, "length:", jws.length, "payload readable:", Buffer.from(jws.split(".")[1], "base64url").toString().includes("alice"));
console.log("JWE parts:", jwe.split(".").length, "length:", jwe.length, "decrypts:", new TextDecoder().decode(plaintext).includes("alice"));
Salida
JWS parts: 3 length: 186 payload readable: true
JWE parts: 5 length: 405 decrypts: true

La mayor parte de la sobrecarga del JWE se debe a la clave pública efímera (epk) del encabezado, la clave envuelta, el IV de 12 bytes y la etiqueta de 16 bytes. Un token anidado, firmado y después cifrado, crece todavía más, porque el JWS entero pasa a ser el texto en claro del JWE.

Trampas habituales

  • Tratar como secretos los claims de un JWS. No pongas datos personales, identificadores internos que no publicarías ni secretos en un token que solo está firmado: acabará en logs, en el almacenamiento del navegador y en tickets de soporte.
  • Confiar en un JWE sin firmar. El descifrado con ECDH-ES o RSA-OAEP solo demuestra que el token se cifró para ti.
  • Algoritmos JWE débiles u obsoletos. RSA1_5 es vulnerable a ataques de oráculo de relleno (padding oracle), PBES2 con un número de iteraciones p2c enorme puede usarse para una denegación de servicio, y las implementaciones de ECDH-ES que aceptaban puntos fuera de la curva filtraron claves privadas en 2017 (RFC 8725 §2.5). Prefiere ECDH-ES+A256KW o RSA-OAEP-256 con A256GCM, y limita las iteraciones de PBES2 si tienes que admitirlo.
  • Algoritmo elegido por el token. Igual que con JWS, el destinatario debería aceptar solo los valores de alg y enc que espera para cada clave.
  • Compresión. El zip: DEF de JWE comprime antes de cifrar, lo que puede filtrar información sobre el texto en claro a través de la longitud (RFC 8725 §3.6); evítalo.

Cuándo elegir cada uno

  • Tokens de acceso que comprueban tus API: JWS; reduce los claims al mínimo o usa tokens opacos con introspección si no deben ser legibles.
  • Tokens de ID con datos personales, almacenados por un navegador o una aplicación móvil: lo habitual es JWS; un JWE anidado si tu modelo de amenazas incluye el entorno del cliente.
  • Tokens que pasan por intermediarios que no son de confianza (redirecciones a través de terceros, tokens incrustados en URL o en códigos QR): JWS anidado dentro de un JWE.
  • Mensajes entre máquinas sobre TLS con autenticación mutua: a menudo ninguno de los dos; TLS ya aporta confidencialidad e integridad en tránsito, y un JWS solo aporta valor si el mensaje debe seguir siendo verificable después de almacenarse o reenviarse.
  • Cargas útiles cifradas de extremo a extremo para un destinatario conocido: JWE con ECDH-ES+A256KW y A256GCM, o formatos basados en HPKE a medida que se estandaricen para JOSE.

Notas de migración

  1. Clasifica los claims de los tokens JWS existentes: todo lo que no deba ser visible para el titular sale del token o pasa a un JWE.
  2. Introduce los tokens anidados de forma gradual: emite JWE con cty: JWT para los clientes que puedan descifrarlos y deja sin cambios el JWS interior para reutilizar la lógica de verificación.
  3. Distribuye las claves de cifrado por separado de las claves de firma: las entradas del JWKS deberían tener use: enc o use: sig y valores de kid distintos.
  4. Fija los algoritmos en ambos extremos y rechaza RSA1_5, PBES2 sin límite de iteraciones y la compresión.
  5. Vuelve a revisar los logs: una vez cifrados los tokens, asegúrate de que los claims descifrados no se registran en lugares donde el token nunca estuvo. El inspector de JWE muestra el encabezado y la estructura de un JWE sin descifrarlo.

Referencias

Todas las comparaciones