Выбор между JWS и JWE сводится к тому, какую защиту даёт токен JOSE. JSON Web Signature (RFC 7515) добавляет целостность и подлинность полезной нагрузке, которая остаётся читаемой; JSON Web Encryption (RFC 7516) добавляет конфиденциальность. Почти все JWT на практике — это JWS, и это удивляет тех, кто считает утверждения (claims) в них секретными.
Коротко о главном
| JWS | JWE | |
|---|---|---|
| Спецификация | RFC 7515 | RFC 7516 |
| Что защищает | Целостность и источник данных | Конфиденциальность (и целостность за счёт AEAD) |
| Компактная форма | header.payload.signature (3 части) | header.encrypted_key.iv.ciphertext.tag (5 частей) |
| Алгоритмы в заголовке | alg (например, ES256, RS256, HS256, Ed25519) | alg для управления ключами (ECDH-ES, ECDH-ES+A256KW, RSA-OAEP-256, A256KW, dir) и enc для содержимого (A256GCM, A128CBC-HS256) |
| Полезная нагрузка видна | Да, она только закодирована в base64url | Нет |
| Отправитель аутентифицирован | Да (подпись или MAC) | Только с симметричным ключом или аутентифицированным согласованием ключей; с одним лишь ECDH-ES или RSA-OAEP — нет |
| Размер для набора утверждений в 70 байт (замер ниже) | 186 символов (ES256) | 405 символов (ECDH-ES+A256KW с A256GCM) |
| Типичное применение | JWT: токены доступа и ID-токены, DPoP, подписанные запросы, SD-JWT | Зашифрованные ID-токены, токены, хранящиеся на стороне клиента, некоторые ответы FAPI и EUDI |
Что защищает каждый из них
JWS подписывает base64url(header) + "." + base64url(payload). Любой, у кого есть токен, может декодировать его и прочитать утверждения; именно это делает отладчик JWT. Подпись доказывает, что токен выпустил владелец ключа и что токен не изменён, а именно это и нужно API, чтобы доверять утверждениям.
JWE генерирует случайный ключ шифрования содержимого (content encryption key, CEK), шифрует полезную нагрузку алгоритмом AEAD, указанным в enc, и доставляет CEK получателю с помощью алгоритма управления ключами, указанного в alg: обёртывает CEK с помощью RSA-OAEP, обёртывает его с помощью AES Key Wrap после согласования ключей ECDH-ES, выводит его напрямую из ECDH-ES или берёт в качестве CEK заранее распределённый общий ключ (dir). Защищённый заголовок аутентифицируется как ассоциированные данные. Прочитать полезную нагрузку может только получатель.
Важная асимметрия: если управление ключами построено на открытых ключах, любой, у кого есть открытый ключ получателя, может создать корректный JWE. Успешное расшифрование ничего не говорит об отправителе. Поэтому конфиденциальные токены, которым к тому же нужно доверять, делают вложенными: подписанный JWT (JWS) помещают внутрь JWE с cty: JWT и проверяют после расшифрования.
Размеры на практике
Скрипт ниже (запускался с jose 6.2.13 на Node.js 26.10) защищает одни и те же утверждения обоими способами.
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"));
JWS parts: 3 length: 186 payload readable: true
JWE parts: 5 length: 405 decrypts: true
Основная часть накладных расходов JWE — эфемерный открытый ключ (epk) в заголовке, обёрнутый ключ, 12-байтный IV и 16-байтный тег. Вложенный токен (сначала подписанный, затем зашифрованный) получается ещё больше, потому что весь JWS становится открытым текстом JWE.
Подводные камни
- Утверждения JWS принимают за секрет. Не помещайте в токен, который только подписан, персональные данные, внутренние идентификаторы, которые вы не стали бы публиковать, или секреты: такой токен окажется в логах, хранилище браузера и обращениях в поддержку.
- Доверие неподписанному JWE. Расшифрование с ECDH-ES или RSA-OAEP доказывает лишь то, что токен зашифрован для вас.
- Слабые или устаревшие алгоритмы JWE.
RSA1_5уязвим к атакам на оракул дополнения (padding oracle),PBES2с огромным числом итераций вp2cможно использовать для атаки на отказ в обслуживании, а реализацииECDH-ES, которые принимали точки вне кривой, в 2017 году приводили к утечке закрытых ключей (RFC 8725, § 2.5). Отдавайте предпочтениеECDH-ES+A256KWилиRSA-OAEP-256сA256GCMи ограничьте число итераций PBES2, если без него не обойтись. - Токен сам выбирает алгоритм. Как и в случае JWS, получателю следует принимать только те значения
algиenc, которые он ожидает для каждого ключа. - Сжатие. Параметр
zip: DEFв JWE сжимает данные перед шифрованием, и по длине результата можно узнать кое-что об открытом тексте (RFC 8725, § 3.6); не используйте его.
Что когда выбирать
- Токены доступа, проверяемые вашими API: JWS; оставляйте в них минимум утверждений или, если утверждения нельзя показывать, используйте непрозрачные токены с интроспекцией.
- ID-токены с персональными данными, которые хранит браузер или мобильное приложение: обычно JWS; вложенный JWE, если ваша модель угроз включает клиентскую среду.
- Токены, которые проходят через недоверенных посредников (перенаправления через третьи стороны, токены в URL или QR-кодах): вложенный JWS внутри JWE.
- Сообщения между машинами по TLS со взаимной аутентификацией: часто не нужен ни тот, ни другой; TLS уже обеспечивает конфиденциальность и целостность при передаче, а JWS полезен, только если сообщение должно оставаться проверяемым после сохранения или пересылки.
- Полезная нагрузка со сквозным шифрованием для известного получателя: JWE с
ECDH-ES+A256KWиA256GCMили форматы на основе HPKE по мере их стандартизации для JOSE.
Заметки о миграции
- Классифицируйте утверждения в существующих токенах JWS: всё, что не должен видеть владелец, либо убирается из токена, либо переносится в JWE.
- Вводите вложенные токены постепенно: выдавайте JWE с
cty: JWTтем клиентам, которые умеют их расшифровывать, а внутренний JWS оставляйте без изменений, чтобы использовать прежнюю логику проверки. - Распространяйте ключи шифрования отдельно от ключей подписи: в записях JWKS следует указывать
use: encилиuse: sigи разные значенияkid. - Закрепите алгоритмы на обеих сторонах и отклоняйте
RSA1_5,PBES2без ограничения числа итераций и сжатие. - Перепроверьте логирование: когда токены станут зашифрованными, убедитесь, что расшифрованные утверждения не записываются в логи, куда сам токен никогда не попадал. Инспектор JWE показывает заголовок и структуру JWE, не расшифровывая его.
Источники
- RFC 7515, JSON Web Signature
- RFC 7516, JSON Web Encryption
- RFC 7518, § 4 и § 5, алгоритмы управления ключами и шифрования содержимого
- RFC 7519, § 11.2, порядок подписи и шифрования (вложенные JWT)
- RFC 8725, рекомендации по использованию JWT (JWT Best Current Practices)
- Документация jose (JavaScript)