Перейти к содержимому
ecdsa.com

JWT или PASETO: гибкий стандарт или заданная криптография

PASETO избавляется от заголовка alg и от выбора, который стал причиной большинства уязвимостей JWT. За JWT остаётся экосистема: поставщики удостоверений, стандарты и библиотеки на любом языке.

Выбор между JWT и PASETO — это компромисс между гибкостью и устойчивостью к ошибкам использования. JWT (RFC 7519, обычно в виде JWS) позволяет токену самому назвать свой алгоритм из длинного реестра, и это открыло дорогу классическим ошибкам вроде alg: none и подмены RS256 на HS256. PASETO (Platform-Agnostic SEcurity TOkens) фиксирует криптографию для каждой версии протокола, поэтому токен v4 public всегда использует Ed25519, а токен v4 local — всегда XChaCha20 с BLAKE2b.

Коротко о главном

JWT (JWS)PASETO v4
СпецификацияRFC 7519 с RFC 7515/7518 (стандарты IETF)Спецификация сообщества (paseto-standard/paseto-spec); срок действия черновиков IETF истёк
Форматbase64url(header).base64url(claims).base64url(signature)v4.public. + base64url(сообщение ‖ подпись), необязательный .footer
Выбор алгоритмаЗаголовок alg из реестра IANAЗадаётся версией и назначением
Асимметричная подписьES256, RS256, PS256, EdDSA/Ed25519 и другиеv4.public: только Ed25519 (v3.public: ECDSA P-384)
Симметричные токеныHS256 (только MAC, утверждения читаемы) или JWEv4.local: XChaCha20 + BLAKE2b-MAC (шифрование и аутентификация)
none и подмена алгоритмаВозможны, если проверяющая сторона неправильно настроенаНевыразимы в формате
Неявные утверждения (аутентифицируются, но не передаются)НетДа (v3 и v4)
Размер примера (замер ниже)227 символов280 символов
Формат ключейJWK и JWKSPASERK (k4.public…, k4.local…)
ЭкосистемаВсе IdP, OAuth/OIDC, шлюзы, облачные сервисыБиблиотеки на многих языках; в IdP и шлюзах поддержки почти нет

Различия в устройстве

Проверяющая сторона видит в JWT заголовок, в котором назван алгоритм, и должна решить, доверять ли ему. RFC 7518 регистрирует больше десятка алгоритмов подписи и MAC плюс none, а библиотеки исторически выбирали алгоритм по заголовку, что и вызвало волну ошибок 2015 года: принимались неподписанные токены, а открытые ключи RSA использовались как секреты HMAC (см. HS256 или RS256). RFC 8725 решает эту проблему правилами настройки, которые поддерживает любая современная библиотека, но применить их должно каждое развёртывание.

PASETO заменяет согласование алгоритма версиями. Каждая версия фиксирует один примитив для каждого назначения: v4 использует Ed25519 для токенов public, а для токенов local — шифрование XChaCha20 и MAC на основе BLAKE2b в режиме с ключом; v3 использует алгоритмы NIST (ECDSA P-384, AES-256-CTR с HMAC-SHA-384) для сред, где они требуются. Проверяющую сторону, которая принимает токены v4 public, невозможно обманом заставить принять что-то другое, а ключи типизированы, поэтому ключ для токенов local нельзя использовать как открытый ключ. Кодирование перед аутентификацией (pre-authentication encoding, PAE) связывает версию, назначение, полезную нагрузку, footer и необязательные неявные утверждения (implicit assertions) — например, идентификатор арендатора, который известен обеим сторонам, но не передаётся в токене.

На практике важны ещё два различия. Симметричные токены local в PASETO зашифрованы, а симметричные токены JWT с HS256 только защищены MAC и остаются читаемыми; аналог v4.local в мире JWT — это JWE. Кроме того, у PASETO нет аналога обнаружения ключей через JWKS, так что распространение ключей остаётся на вас.

Один пример в двух форматах

Скрипт ниже (запускался на Node.js 26.10 с paseto 3.1.4 и jose 6.2.13) выпускает одни и те же утверждения с одним ключом Ed25519 в обоих форматах.

Один и тот же ключ Ed25519 и одни и те же утверждения в PASETO v4.public и в 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);
Результат
PASETO prefix: v4.public. length: 280 sub: user-42
JWT    prefix: eyJhbGciOi length: 227 sub: user-42

Здесь токен PASETO немного длиннее, потому что библиотека добавляет iat и использует метки времени в формате ISO 8601, а JWT — числовые даты; размеры подписей одинаковы. Обратите внимание на параметр algorithms: ["EdDSA"] на стороне JWT: именно эту строку PASETO делает ненужной.

Экосистема и совместимость

JWT встроен в OAuth 2.0 (токены доступа по RFC 9068), ID-токены OpenID Connect, DPoP, SD-JWT, FAPI, удостоверения рабочих нагрузок в облаке и практически в каждый API-шлюз и поставщик удостоверений. Библиотеки есть для любого языка, и их безопасность проверяют уже много лет.

Спецификацию PASETO ведёт на GitHub проект PASETO standard; срок действия более ранних черновиков IETF (Internet-Draft) истёк, и RFC они так и не стали. Библиотеки есть для многих языков, но их качество и охват версий различаются, шлюзы и поставщики удостоверений редко принимают PASETO, а стандартного механизма обнаружения нет. Поэтому выбор PASETO означает, что за издателя, проверяющие стороны и распространение ключей отвечаете вы сами.

Что когда выбирать

  • OAuth или OpenID Connect, сторонние потребители, API-шлюзы: JWT со списком разрешённых алгоритмов для каждого ключа.
  • Внутренние сервисные токены в системе на нескольких языках, которую вы контролируете: подойдёт любой; PASETO — если для каждого языка есть поддерживаемая библиотека с v4.
  • Зашифрованные токены, хранящиеся у клиентов (данные сессии в cookie): PASETO v4.local проще, чем JWE; в случае JWT используйте JWE с dir или A256KW и A256GCM.
  • Среды с требованиями FIPS: JWT с ES256 или PS256 либо PASETO v3; XChaCha20 и BLAKE2b из v4 не одобрены FIPS.
  • Команды, которые только знакомятся с безопасностью токенов: PASETO исключает целый класс ошибок конфигурации; библиотека JWT с безопасными настройками по умолчанию (например, jose, которая требует, чтобы алгоритм соответствовал типу ключа) сокращает этот разрыв.

Заметки о миграции

  1. Не пропускайте оба формата через одну проверку: по префиксу (v4.public. или eyJ) направляйте токены в разные ветки кода проверки с разными ключами.
  2. Сопоставляйте утверждения явно: PASETO резервирует iss, sub, aud, exp, nbf, iat, jti и kid, а даты записывает в формате ISO 8601; JWT использует секунды NumericDate.
  3. Замените JWKS собственным механизмом распространения ключей (строки PASERK, система управления конфигурацией или служба ключей), включая ротацию по kid в footer токена PASETO.
  4. Оставьте JWT на границах системы: многие системы выдают JWT внешним сторонам, а внутри используют PASETO.
  5. Если остаётесь на JWT, примените RFC 8725 уже сейчас: закрепите алгоритмы, проверяйте typ, iss и aud, используйте 256-битные секреты HMAC или асимметричные ключи и отклоняйте none.

Источники

Все сравнения