Вложенный JWT: подпись внутри шифрования

Вложенные JWT (Nested JWT) представляют собой композицию двух уровней защиты: сначала создаётся подписанный токен (JWS), затем он шифруется (JWE). В результате формируется структура, где подпись «внутри» шифрования и становится доступной только после расшифровки.

Такой подход применяется в системах с повышенными требованиями к безопасности: банковские API, обмен чувствительными данными между микросервисами, SSO-инфраструктуры, защищённые каналы передачи идентификационных данных.


В стандарте JOSE (JSON Object Signing and Encryption) вложенный JWT формируется по цепочке:

  1. Формирование JWS (подписанный токен)
  2. Использование JWS как полезной нагрузки (payload)
  3. Шифрование результата в JWE

Итоговая структура:

JWE(
  JWS(payload)
)

Это означает, что даже если злоумышленник получит токен, он увидит только зашифрованную форму. Подпись остаётся скрытой до момента расшифровки.


Разделение ответственности JWS и JWE

JWS (JSON Web Signature)

Отвечает за:

  • целостность данных
  • проверку подлинности отправителя
  • защиту от подмены payload

JWE (JSON Web Encryption)

Отвечает за:

  • конфиденциальность данных
  • скрытие содержимого токена
  • защиту от чтения третьими сторонами

Вложенная модель объединяет обе гарантии:

  • JWS защищает структуру данных
  • JWE скрывает саму структуру

Реализация через библиотеку jose

Библиотека jose является одной из наиболее полных реализаций стандарта JOSE в JavaScript и поддерживает как JWS, так и JWE, включая их композицию.

Установка:

npm install jose

Создание JWS (подпись данных)

Первый этап — формирование подписанного JWT.

import { SignJWT, importPKCS8 } from 'jose'

const privateKey = await importPKCS8(
  process.env.PRIVATE_KEY,
  'RS256'
)

const payload = {
  sub: 'user_123',
  role: 'admin',
  permissions: ['read', 'write']
}

const jws = await new SignJWT(payload)
  .setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
  .setIssuedAt()
  .setExpirationTime('2h')
  .sign(privateKey)

На этом этапе создаётся компактный JWS:

header.payload.signature

Этот токен уже защищён от подмены, но не скрыт.


Шифрование JWS в JWE

Далее полученный JWS используется как payload для шифрования.

import { EncryptJWT, importJWK } from 'jose'

const publicKey = await importJWK(
  {
    kty: 'RSA',
    e: 'AQAB',
    n: '...'
  },
  'RSA-OAEP-256'
)

const nestedJWT = await new EncryptJWT({ token: jws })
  .setProtectedHeader({
    alg: 'RSA-OAEP-256',
    enc: 'A256GCM'
  })
  .setIssuedAt()
  .setExpirationTime('2h')
  .encrypt(publicKey)

Результат — JWE компактного формата:

protectedHeader.encryptedKey.iv.ciphertext.tag

Внутри ciphertext находится JWS.


Полный цикл восстановления данных

1. Расшифровка JWE

import { jwtDecrypt } from 'jose'

const { plaintext } = await jwtDecrypt(
  nestedJWT,
  privateKey
)

const { token: extractedJWS } = JSON.parse(
  new TextDecoder().decode(plaintext)
)

После расшифровки получается исходный JWS.


2. Проверка подписи JWS

import { jwtVerify } from 'jose'

const { payload } = await jwtVerify(
  extractedJWS,
  publicKey
)

Теперь доступны проверенные данные.


Почему подпись помещают внутрь шифрования

Архитектура «подпись → шифрование» используется по нескольким причинам.

1. Сокрытие метаданных подписи

Без шифрования JWS содержит:

  • алгоритм подписи
  • идентификаторы ключей
  • структуру payload

JWE скрывает это полностью.


2. Защита от анализа трафика

Даже если подпись не может быть подделана, её наличие может раскрывать:

  • тип пользователя
  • роль
  • структуру системы

Шифрование устраняет этот риск.


3. Многоуровневая безопасность

Комбинация обеспечивает:

  • JWS: доказательство подлинности
  • JWE: конфиденциальность
  • Nested JWT: защита обеих сторон одновременно

Альтернативный порядок (JWE → JWS)

Теоретически возможна обратная схема: сначала шифрование, затем подпись. Однако она используется реже, так как:

  • подпись становится видимой без расшифровки
  • метаданные JWE могут быть изменены без нарушения подписи

Поэтому стандартной практикой считается именно:

JWS → JWE

Практическая структура nested JWT

После сериализации итоговый токен выглядит как строка:

eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0...

Внутри:

  1. JWE header
  2. encryptedKey
  3. iv
  4. ciphertext (внутри JWS)
  5. tag

Разбор типичного payload после расшифровки

После двух этапов восстановления:

{
  "sub": "user_123",
  "role": "admin",
  "permissions": ["read", "write"],
  "iat": 1710000000,
  "exp": 1710007200
}

Эти данные появляются только после:

  1. успешного дешифрования
  2. проверки подписи

Ошибки при реализации nested JWT

1. Использование слабых алгоритмов

Недопустимо:

  • HS256 в распределённых системах без контроля секретов
  • RSA1_5 вместо RSA-OAEP-256

Рекомендуемые связки:

  • подпись: RS256, ES256
  • шифрование: RSA-OAEP-256 + A256GCM

2. Преждевременное раскрытие JWS

Ошибка архитектуры — логировать или передавать JWS до шифрования.

Это уничтожает смысл вложенной модели.


3. Проверка только одного слоя

Распространённая ошибка:

  • расшифровка JWE без проверки JWS

Это приводит к ситуации, где:

  • конфиденциальность соблюдена
  • но целостность не гарантирована

Правильный порядок всегда фиксирован:

  1. decrypt JWE
  2. verify JWS

Производительность и издержки

Nested JWT увеличивает стоимость обработки:

  • двойная криптография (encrypt + sign)
  • увеличение размера токена
  • дополнительная сериализация JSON

В системах с высокой нагрузкой это может требовать:

  • кэширования ключей
  • использования EC-алгоритмов вместо RSA
  • ограничения payload

Применение в архитектуре микросервисов

Nested JWT часто используется:

  • между gateway и backend сервисами
  • в SSO (например, корпоративные identity providers)
  • в финансовых транзакциях
  • при передаче персональных данных (PII)

Типичный поток:

Client → Gateway → Auth Service → Internal Services

На каждом этапе токен может оставаться зашифрованным, но проверяемым после расшифровки.


Модель доверия

Nested JWT создаёт разделение доверия:

  • внешний уровень: невозможность прочитать данные
  • внутренний уровень: невозможность подменить данные

Это делает модель устойчивой к:

  • MITM-атакам
  • анализу трафика
  • подмене payload после расшифровки

Итоговая архитектурная схема

Payload
  ↓
JWS (подпись)
  ↓
JWE (шифрование)
  ↓
Transport
  ↓
Decrypt JWE
  ↓
Verify JWS
  ↓
Trusted payload

Особенности реализации в jose

Библиотека jose предоставляет:

  • чистую поддержку JWS/JWE
  • работу с WebCrypto API
  • поддержку RSA, EC, Octet keys
  • компактную и flattened сериализацию

Ключевая особенность — отсутствие «магии»: каждый слой реализуется явно, что важно для построения предсказуемой криптографической цепочки.