Создание подписи: CompactSign

В основе библиотеки jose лежит стандарт JSON Web Signature (JWS), описывающий способ создания цифровой подписи для произвольных данных. Один из наиболее распространённых форматов представления подписи — Compact Serialization, представляющий результат в виде строки из трёх частей:

BASE64URL(Protected Header) . BASE64URL(Payload) . BASE64URL(Signature)

Каждая часть кодируется с использованием Base64URL, что делает итоговую строку пригодной для передачи через HTTP-заголовки, URL и другие текстовые каналы.

Назначение CompactSign

Класс CompactSign используется для создания JWS в компактном формате. Он инкапсулирует процесс:

  • подготовки полезной нагрузки (payload)
  • формирования защищённого заголовка (protected header)
  • криптографической подписи

Это основной инструмент для генерации подписанных токенов, включая JWT.


Импорт и базовое использование

import { CompactSign } from 'jose'

Создание подписи начинается с передачи данных в виде Uint8Array:

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

Создание экземпляра CompactSign

const signer = new CompactSign(payload)

На этом этапе объект содержит только полезную нагрузку. Следующий шаг — указание защищённого заголовка.


Protected Header

Protected Header — это JSON-объект, который:

  • участвует в вычислении подписи
  • содержит метаинформацию (алгоритм, тип токена и др.)

Пример:

signer.setProtectedHeader({
  alg: 'HS256',
  typ: 'JWT'
})

Ключевые параметры:

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

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

Библиотека jose поддерживает множество алгоритмов:

Симметричные:

  • HS256
  • HS384
  • HS512

Асимметричные:

  • RS256, RS384, RS512
  • ES256, ES384, ES512
  • EdDSA

Выбор алгоритма напрямую влияет на тип используемого ключа.


Подпись с использованием симметричного ключа

const secret = encoder.encode('super-secret-key')

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

console.log(jws)

Результат — строка вида:

eyJhbGciOiJIUzI1NiJ9.<payload>.<signature>

Подпись с использованием асимметричного ключа

Для асимметричных алгоритмов используется приватный ключ:

import { generateKeyPair } from 'jose'

const { privateKey } = await generateKeyPair('RS256')

const jws = await new CompactSign(payload)
  .setProtectedHeader({ alg: 'RS256' })
  .sign(privateKey)

Внутренний процесс подписи

Процесс можно разбить на этапы:

  1. Сериализация заголовка

    • JSON → строка → Base64URL
  2. Сериализация payload

    • Uint8Array → Base64URL
  3. Формирование signing input

    BASE64URL(header) + '.' + BASE64URL(payload)
  4. Вычисление подписи

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

Особенности работы с payload

CompactSign принимает только бинарные данные (Uint8Array). Это означает:

  • строки необходимо кодировать через TextEncoder
  • объекты нужно сериализовать:
const data = { user: 'alice', admin: true }
const payload = encoder.encode(JSON.stringify(data))

Контроль над заголовками

Дополнительные параметры позволяют расширить поведение:

.setProtectedHeader({
  alg: 'RS256',
  kid: 'key-id-123',
  typ: 'JWT'
})

Важно: заголовок, установленный через setProtectedHeader, полностью защищён подписью. Любое изменение делает подпись недействительной.


Асинхронность метода sign

Метод .sign() всегда асинхронный:

const jws = await signer.sign(key)

Причины:

  • криптографические операции
  • возможная работа с WebCrypto API
  • поддержка разных сред выполнения (Node.js, браузер)

Использование в JWT

Хотя CompactSign — низкоуровневый инструмент, он может использоваться для создания JWT вручную:

const payload = encoder.encode(JSON.stringify({
  sub: '1234567890',
  name: 'John Doe',
  iat: Math.floor(Date.now() / 1000)
}))

const jwt = await new CompactSign(payload)
  .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
  .sign(secret)

Ошибки и обработка исключений

Основные причины ошибок:

  • отсутствие alg в заголовке
  • несовместимый ключ (например, публичный вместо приватного)
  • неподдерживаемый алгоритм
  • неверный формат payload

Пример обработки:

try {
  const token = await signer.sign(key)
} catch (err) {
  console.error('Ошибка подписи:', err)
}

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

Ключевые аспекты:

  • секретные ключи должны храниться безопасно
  • нельзя использовать слабые или короткие ключи
  • алгоритм должен соответствовать типу ключа
  • нельзя доверять неподписанным данным

Особенно важно:

  • избегать alg: "none"
  • проверять алгоритм на стороне верификации

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

CompactSign оптимизирован для:

  • минимального размера результата
  • быстрого формирования строки
  • эффективной работы с WebCrypto

Однако:

  • асимметричные алгоритмы медленнее
  • большие payload увеличивают время кодирования

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

Compact формат:

  • минимальный размер
  • одна подпись
  • простой парсинг

Альтернативы:

  • Flattened — поддержка дополнительных полей
  • General — множественные подписи

CompactSign используется именно тогда, когда важны компактность и совместимость.


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

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

Ограничения

  • только одна подпись
  • ограниченная гибкость структуры
  • невозможность добавления незащищённых заголовков

Итоговая схема использования

  1. Подготовка payload (Uint8Array)
  2. Создание CompactSign
  3. Установка protected header
  4. Подпись с помощью ключа
  5. Получение строки JWS
const jws = await new CompactSign(payload)
  .setProtectedHeader({ alg: 'HS256' })
  .sign(secret)

Такой подход обеспечивает строгую, стандартизированную и криптографически безопасную подпись данных в JavaScript.