JSON-сериализация JWS: Flattened и General

Стандарт JWS (JSON Web Signature) определяет несколько способов представления подписи:

  • Compact Serialization — строка из трёх частей

  • JSON Serialization:

    • Flattened
    • General

JSON-форматы используются, когда требуется гибкость: несколько подписей, расширенные заголовки, структурированные данные.

Библиотека jose в JavaScript предоставляет удобные API для работы с обоими JSON-представлениями.


Flattened JSON Serialization

Структура

Flattened-формат применяется, когда используется одна подпись. Он представляет JWS в виде JSON-объекта:

{
  "payload": "...",
  "protected": "...",
  "header": { ... },
  "signature": "..."
}

Поля:

  • payload — полезная нагрузка (Base64URL)
  • protected — защищённый заголовок (Base64URL JSON)
  • header — необязательный незашифрованный заголовок
  • signature — подпись

Создание Flattened JWS

В jose используется класс FlattenedSign:

import { FlattenedSign } from 'jose'

const payload = new TextEncoder().encode('example data')

const jws = await new FlattenedSign(payload)
  .setProtectedHeader({ alg: 'HS256' })
  .sign(secretKey)

console.log(jws)

Результат:

{
  "payload": "ZXhhbXBsZSBkYXRh",
  "protected": "eyJhbGciOiJIUzI1NiJ9",
  "signature": "..."
}

Добавление незашифрованного заголовка

const jws = await new FlattenedSign(payload)
  .setProtectedHeader({ alg: 'HS256' })
  .setUnprotectedHeader({ kid: 'key-id-1' })
  .sign(secretKey)

Это добавит поле header.


Верификация Flattened JWS

import { flattenedVerify } from 'jose'

const { payload, protectedHeader } = await flattenedVerify(jws, secretKey)

console.log(new TextDecoder().decode(payload))
console.log(protectedHeader)

General JSON Serialization

Структура

General-формат поддерживает несколько подписей:

{
  "payload": "...",
  "signatures": [
    {
      "protected": "...",
      "header": { ... },
      "signature": "..."
    },
    {
      "protected": "...",
      "signature": "..."
    }
  ]
}

Особенности

  • Один payload
  • Несколько подписей
  • Каждая подпись может иметь свои заголовки
  • Удобно для сценариев с несколькими подписантами

Создание General JWS

Используется класс GeneralSign:

import { GeneralSign } from 'jose'

const payload = new TextEncoder().encode('multi-signature data')

const signer = new GeneralSign(payload)

signer.addSignature(key1).setProtectedHeader({ alg: 'HS256' })
signer.addSignature(key2).setProtectedHeader({ alg: 'HS512' })

const jws = await signer.sign()

console.log(jws)

Результат:

{
  "payload": "...",
  "signatures": [
    {
      "protected": "...",
      "signature": "..."
    },
    {
      "protected": "...",
      "signature": "..."
    }
  ]
}

Добавление заголовков для конкретной подписи

signer
  .addSignature(key1)
  .setProtectedHeader({ alg: 'HS256' })
  .setUnprotectedHeader({ kid: 'key1' })

Каждая подпись настраивается отдельно.


Верификация General JWS

Для проверки используется generalVerify:

import { generalVerify } from 'jose'

const { payload, signatures } = await generalVerify(jws, keyResolver)

keyResolver

Так как подписей может быть несколько, используется функция:

const keyResolver = async (protectedHeader, jws) => {
  if (protectedHeader.alg === 'HS256') return key1
  if (protectedHeader.alg === 'HS512') return key2
}

Сравнение Flattened и General

Характеристика Flattened General
Количество подписей 1 Несколько
Структура Простая Сложная
Использование Обычные сценарии Мультиподписи
API FlattenedSign GeneralSign

Когда использовать Flattened

  • Одна подпись
  • Простая структура
  • REST API
  • JWT-аналоги в JSON-формате

Когда использовать General

  • Несколько подписантов
  • Разные алгоритмы подписи
  • Проверка доверия от нескольких сторон
  • Системы с распределённой ответственностью

Работа с payload

Payload всегда передаётся как Uint8Array:

const payload = new TextEncoder().encode(JSON.stringify({ user: 'admin' }))

Detached Payload

В JSON-сериализации можно исключить payload:

const jws = await new FlattenedSign(payload)
  .setProtectedHeader({ alg: 'HS256', b64: false, crit: ['b64'] })
  .sign(secretKey)

Payload передаётся отдельно при проверке.


Безопасность

  • Проверка alg обязательна
  • Не доверять header без верификации
  • Использовать строгий keyResolver
  • Не смешивать ключи разных типов

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

  • Flattened быстрее за счёт простоты
  • General требует обработки массива подписей
  • Проверка всех подписей может быть затратной

Практический сценарий: несколько подписей

const signer = new GeneralSign(payload)

signer.addSignature(serviceKey).setProtectedHeader({ alg: 'HS256' })
signer.addSignature(adminKey).setProtectedHeader({ alg: 'HS512' })

const jws = await signer.sign()

Проверка:

const result = await generalVerify(jws, keyResolver)

Ошибки и отладка

Частые проблемы:

  • Неверный alg
  • Несоответствие ключа
  • Повреждённый Base64URL
  • Отсутствие crit параметров при необходимости

Выбор формата

  • Flattened — стандарт по умолчанию
  • General — специализированный инструмент для сложных сценариев

Взаимодействие с другими системами

JSON-сериализация удобна для:

  • Web APIs
  • Microservices
  • Систем с аудитом подписей
  • Криптографических протоколов

Внутреннее устройство в jose

Библиотека:

  • Автоматически кодирует payload
  • Управляет Base64URL
  • Проверяет корректность заголовков
  • Обрабатывает криптографию через WebCrypto / Node.js crypto

Расширенные возможности

  • Поддержка разных алгоритмов в одном JWS
  • Комбинирование protected и unprotected заголовков
  • Detached payload
  • Кастомные критические параметры (crit)

Минимальный пример Flattened

const jws = await new FlattenedSign(
  new TextEncoder().encode('data')
)
  .setProtectedHeader({ alg: 'HS256' })
  .sign(key)

Минимальный пример General

const jws = await new GeneralSign(
  new TextEncoder().encode('data')
)
  .addSignature(key)
  .setProtectedHeader({ alg: 'HS256' })
  .sign()

Итоговое понимание структуры

  • Flattened — объект с одной подписью
  • General — массив подписей
  • Payload общий
  • Заголовки могут различаться
  • Подписи независимы друг от друга

Такой подход делает JWS универсальным инструментом для построения защищённых распределённых систем.