Saltar al contenido
ecdsa.com

JWT frente a PASETO: estándar flexible o criptografía fija

PASETO elimina el encabezado alg y las opciones que causaron la mayoría de las vulnerabilidades de JWT. JWT conserva el ecosistema: proveedores de identidad, estándares y bibliotecas en todos los lenguajes.

JWT frente a PASETO es un compromiso entre flexibilidad y resistencia al mal uso. JWT (RFC 7519, normalmente un JWS) deja que el token indique su algoritmo, elegido de un registro extenso, lo que hizo posibles errores clásicos como alg: none y la confusión de RS256 con HS256. PASETO (Platform-Agnostic SEcurity TOkens) fija la criptografía en cada versión del protocolo, de modo que un token v4 public siempre es Ed25519 y un token v4 local siempre es XChaCha20 con BLAKE2b.

De un vistazo

JWT (JWS)PASETO v4
EspecificaciónRFC 7519 con RFC 7515/7518 (estándares del IETF)Especificación comunitaria (paseto-standard/paseto-spec); los borradores del IETF caducaron
Formatobase64url(header).base64url(claims).base64url(signature)v4.public. + base64url(mensaje ‖ firma), .footer opcional
Selección del algoritmoEncabezado alg del registro de la IANAFijado por la versión y el propósito
Firma con clave públicaES256, RS256, PS256, EdDSA/Ed25519 y másv4.public: solo Ed25519 (v3.public: ECDSA P-384)
Tokens simétricosHS256 (solo MAC, claims legibles) o JWEv4.local: XChaCha20 + BLAKE2b-MAC (cifrado y autenticado)
none y confusión de algoritmosPosibles si el verificador está mal configuradoNo se pueden expresar
Aserción implícita (autenticada, no transmitida)NoSí (v3 y v4)
Tamaño de ejemplo (medido más abajo)227 caracteres280 caracteres
Formato de claveJWK y JWKSPASERK (k4.public…, k4.local…)
EcosistemaTodos los IdP, OAuth/OIDC, pasarelas, servicios en la nubeBibliotecas en muchos lenguajes; pocos IdP o pasarelas lo admiten

Diferencias de diseño

Un verificador de JWT ve un encabezado que indica el algoritmo y debe decidir si confía en él. El RFC 7518 registra más de una docena de algoritmos de firma y de MAC, además de none, y las bibliotecas solían decidir qué verificación aplicar según el encabezado, lo que produjo la oleada de errores de 2015: se aceptaban tokens sin firmar y se usaban claves públicas RSA como secretos HMAC (consulta HS256 frente a RS256). El RFC 8725 lo corrige con reglas de configuración que todas las bibliotecas modernas admiten, pero cada despliegue debe aplicarlas.

PASETO sustituye la negociación por versiones. Cada versión fija una primitiva por propósito: v4 usa Ed25519 para los tokens public y cifrado XChaCha20 con un MAC BLAKE2b con clave para los tokens local; v3 usa algoritmos del NIST (ECDSA P-384, AES-256-CTR con HMAC-SHA-384) para los entornos que los necesitan. A un verificador configurado para tokens v4 public no se le puede engañar para que acepte otra cosa, y las claves tienen tipo, de modo que una clave local no puede usarse como clave pública. La codificación previa a la autenticación (PAE, pre-authentication encoding) vincula la versión, el propósito, la carga útil, el pie (footer) y las aserciones implícitas opcionales, como un identificador de inquilino que ambas partes conocen pero que el token no lleva.

Hay otras dos diferencias que importan en la práctica. Los tokens simétricos local de PASETO están cifrados, mientras que los tokens simétricos HS256 de JWT solo llevan un MAC y son legibles; el equivalente JWT de v4.local es un JWE. Además, PASETO no tiene nada equivalente al descubrimiento de claves mediante JWKS, así que la distribución de claves corre de tu cuenta.

Comparación directa

El script siguiente, ejecutado en Node.js 26.10 con paseto 3.1.4 y jose 6.2.13, emite los mismos claims con una misma clave Ed25519 en los dos formatos.

La misma clave Ed25519 y los mismos claims como PASETO v4.public y como JWT
import { generateKeyPairSync } from "node:crypto";
import { SignJWT, jwtVerify } from "jose";
import { V4 } from "paseto";

const claims = { sub: "user-42", scope: "orders:read" };
const { privateKey, publicKey } = generateKeyPairSync("ed25519");

// PASETO v4.public: Ed25519 is implied by the version; no alg header exists.
const paseto = await V4.sign(claims, privateKey, { expiresIn: "15m", issuer: "https://auth.example.com" });
const pasetoClaims = await V4.verify(paseto, publicKey, { issuer: "https://auth.example.com" });

// JWT with the same key: the verifier must pin the algorithm itself.
const jwt = await new SignJWT(claims).setProtectedHeader({ alg: "EdDSA" }).setIssuer("https://auth.example.com").setExpirationTime("15m").sign(privateKey);
const { payload } = await jwtVerify(jwt, publicKey, { algorithms: ["EdDSA"], issuer: "https://auth.example.com" });

console.log("PASETO prefix:", paseto.slice(0, 10), "length:", paseto.length, "sub:", pasetoClaims.sub);
console.log("JWT    prefix:", jwt.slice(0, 10), "length:", jwt.length, "sub:", payload.sub);
Salida
PASETO prefix: v4.public. length: 280 sub: user-42
JWT    prefix: eyJhbGciOi length: 227 sub: user-42

Aquí el PASETO es algo más largo porque la biblioteca añade iat y usa marcas de tiempo ISO 8601, mientras que el JWT usa fechas numéricas; las firmas tienen el mismo tamaño. Fíjate en la opción algorithms: ["EdDSA"] del lado JWT: esa única línea es lo que PASETO hace innecesario.

Ecosistema e interoperabilidad

JWT está integrado en OAuth 2.0 (tokens de acceso del RFC 9068), los tokens de ID de OpenID Connect, DPoP, SD-JWT, FAPI, la identidad de cargas de trabajo en la nube y prácticamente todas las pasarelas de API y todos los proveedores de identidad. Hay bibliotecas para todos los lenguajes, con años de revisiones de seguridad.

La especificación de PASETO la mantiene en GitHub el proyecto PASETO standard; los borradores anteriores del IETF (Internet-Drafts) caducaron sin llegar a ser RFC. Hay bibliotecas para muchos lenguajes, pero su calidad y las versiones que cubren varían, las pasarelas y los proveedores de identidad rara vez aceptan PASETO y no existe un mecanismo de descubrimiento estándar. Por tanto, elegir PASETO significa hacerse cargo del emisor, de los verificadores y de la distribución de claves.

Cuándo elegir cada uno

  • OAuth u OpenID Connect, consumidores externos, pasarelas de API: JWT, con una lista de algoritmos permitidos para cada clave.
  • Tokens entre servicios internos en un sistema políglota que controlas: cualquiera de los dos; PASETO si todos los lenguajes tienen una biblioteca v4 mantenida.
  • Tokens cifrados que guardan los clientes (datos de sesión en una cookie): PASETO v4.local es más sencillo que JWE; con JWT, usa JWE con dir o A256KW y A256GCM.
  • Entornos sujetos a FIPS: JWT con ES256 o PS256, o PASETO v3; XChaCha20 y BLAKE2b, que usa v4, no están aprobados por FIPS.
  • Equipos con poca experiencia en seguridad de tokens: PASETO elimina toda una clase de errores de configuración; una biblioteca JWT con valores por defecto seguros (como jose, que exige que los algoritmos correspondan a los tipos de clave) reduce la diferencia.

Notas de migración

  1. No mezcles formatos detrás de un mismo verificador: enruta por prefijo (v4.public. frente a eyJ) hacia rutas de código de verificación separadas, con claves separadas.
  2. Haz explícita la correspondencia de los claims: PASETO reserva iss, sub, aud, exp, nbf, iat, jti y kid, con fechas ISO 8601; JWT usa segundos NumericDate.
  3. Sustituye JWKS por tu propia distribución de claves (cadenas PASERK, gestión de la configuración o un servicio de claves), incluida la rotación por kid en el pie de PASETO.
  4. Mantén JWT en los bordes: muchos sistemas emiten JWT para partes externas y PASETO internamente.
  5. Si te quedas con JWT, aplica ya el RFC 8725: fija los algoritmos, valida typ, iss y aud, usa secretos HMAC de 256 bits o claves asimétricas y rechaza none.

Referencias

Todas las comparaciones