Создание подписи с JSON-сериализацией: FlattenedSign, GeneralSign

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

  • Compact Serialization — строка из трёх частей (header.payload.signature)
  • Flattened JSON Serialization — JSON-объект с одной подписью
  • General JSON Serialization — JSON-объект с массивом подписей

Библиотека jose реализует все эти форматы. Flattened и General используются в случаях, когда требуется гибкость: дополнительные поля, несколько подписей, расширенные заголовки.


FlattenedSign: JSON с одной подписью

Формат Flattened применяется, когда подпись одна, но требуется JSON-структура вместо компактной строки.

Структура результата

{
  "payload": "base64url...",
  "protected": "base64url...",
  "header": { ... },
  "signature": "base64url..."
}
  • payload — закодированная нагрузка
  • protected — защищённый заголовок (base64url JSON)
  • header — необязательный незашищённый заголовок
  • signature — сама подпись

Создание подписи с помощью FlattenedSign

Импорт зависимостей

import { FlattenedSign } from 'jose'

Подготовка ключа

const secret = new TextEncoder().encode('super-secret-key')

Формирование подписи

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

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

Разбор API FlattenedSign

Конструктор

new FlattenedSign(payload)
  • Принимает Uint8Array
  • Строки необходимо кодировать через TextEncoder

setProtectedHeader()

.setProtectedHeader({ alg: 'HS256' })
  • Обязательное поле
  • Включается в подпись
  • Кодируется в base64url

setUnprotectedHeader() (опционально)

.setUnprotectedHeader({ kid: 'key-id-1' })
  • Не участвует в подписи
  • Используется для передачи дополнительной информации

sign()

.sign(secret)
  • Выполняет криптографическую подпись
  • Возвращает JSON-объект

Особенности FlattenedSign

  • Поддерживает только одну подпись
  • Подходит для API, где JSON предпочтительнее строки
  • Удобен для расширяемых заголовков
  • Используется в REST и межсервисных протоколах

GeneralSign: несколько подписей

Формат General JSON Serialization используется, когда требуется подписать одно сообщение несколькими ключами.

Структура результата

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

Создание подписи с помощью GeneralSign

Импорт

import { GeneralSign } from 'jose'

Подготовка ключей

const key1 = new TextEncoder().encode('secret-1')
const key2 = new TextEncoder().encode('secret-2')

Подпись

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

const jws = await new GeneralSign(payload)
  .addSignature(key1)
  .setProtectedHeader({ alg: 'HS256', kid: 'key1' })
  .addSignature(key2)
  .setProtectedHeader({ alg: 'HS256', kid: 'key2' })
  .sign()

Разбор API GeneralSign

addSignature()

.addSignature(key)
  • Добавляет новую подпись
  • Возвращает объект подписи для настройки

setProtectedHeader() для каждой подписи

.addSignature(key)
.setProtectedHeader({ alg: 'HS256' })
  • Вызывается после addSignature
  • Привязывается к текущей подписи

setUnprotectedHeader() (опционально)

.setUnprotectedHeader({ kid: 'key-id' })

sign()

.sign()
  • Выполняет подпись всех добавленных элементов
  • Возвращает общий JSON

Важные особенности GeneralSign

  • Поддерживает несколько подписей

  • Каждая подпись имеет собственные заголовки

  • Все подписи относятся к одному payload

  • Используется для:

    • мультиподписей
    • систем с несколькими доверенными сторонами
    • аудита и подтверждения

Сравнение FlattenedSign и GeneralSign

Характеристика FlattenedSign GeneralSign
Количество подписей 1 1+
Формат JSON JSON
Сложность Низкая Выше
Использование API, REST Мультиподписи
Поддержка headers Да Да

Расширенные заголовки

В обоих форматах доступны:

  • alg — алгоритм (обязательный)
  • kid — идентификатор ключа
  • typ — тип токена
  • cty — тип содержимого

Пример:

.setProtectedHeader({
  alg: 'HS256',
  typ: 'JWT',
  kid: 'main-key'
})

Кодирование payload

Payload всегда должен быть:

Uint8Array

Пример:

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

Поддерживаемые алгоритмы

Часто используемые:

  • HS256 — HMAC
  • RS256 — RSA
  • ES256 — ECDSA
  • EdDSA — Ed25519

Пример:

.setProtectedHeader({ alg: 'RS256' })

Практические сценарии

FlattenedSign

  • REST API с JSON
  • обмен токенами между сервисами
  • хранение подписанных данных в базе

GeneralSign

  • мультиподписанные документы
  • блокчейн/финансовые системы
  • распределённые системы доверия

Проверка подписей (кратко)

Flattened:

import { flattenedVerify } from 'jose'

const { payload } = await flattenedVerify(jws, key)

General:

import { generalVerify } from 'jose'

const { payload } = await generalVerify(jws, key)

Ошибки и подводные камни

  • Отсутствие alg в заголовке → ошибка
  • Неверный формат payload → TypeError
  • Использование строки вместо Uint8Array
  • Несоответствие ключа и алгоритма
  • Повторный вызов setProtectedHeader без addSignature (в GeneralSign)

Производительность и выбор формата

  • Compact — быстрее и меньше по размеру
  • Flattened — баланс удобства и читаемости
  • General — гибкость, но больше накладных расходов

Итоговая рекомендация выбора

  • Один ключ + JSON → FlattenedSign
  • Несколько подписей → GeneralSign
  • Минимальный размер → Compact (не рассматривается здесь)

Архитектурные преимущества JSON-сериализации

  • Явная структура
  • Расширяемость
  • Удобство логирования
  • Простая интеграция с API
  • Возможность частичной обработки данных

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

  • Всегда проверять alg
  • Не доверять незашищённым заголовкам
  • Использовать проверенные ключи
  • Избегать “none” алгоритма
  • Проверять целостность payload

Пример полного результата Flattened

{
  "payload": "eyJ1c2VyIjoiYWxpY2UifQ",
  "protected": "eyJhbGciOiJIUzI1NiJ9",
  "signature": "abc123..."
}

Пример полного результата General

{
  "payload": "ZGF0YQ",
  "signatures": [
    {
      "protected": "eyJhbGciOiJIUzI1NiJ9",
      "signature": "sig1"
    },
    {
      "protected": "eyJhbGciOiJIUzI1NiIsImtpZCI6IjIifQ",
      "signature": "sig2"
    }
  ]
}

Вывод по API jose

  • API построен цепочкой вызовов (builder pattern)

  • Чёткое разделение этапов:

    1. создание
    2. настройка заголовков
    3. подпись
  • Унифицированная работа с ключами

  • Строгая типизация входных данных