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

Формат General JSON Serialization для JWS предназначен для случаев, когда одно и то же сообщение подписывается несколькими сторонами или несколькими ключами. В отличие от компактной сериализации (Compact JWS), где присутствует только одна подпись, здесь структура позволяет хранить массив подписей.

Структура General JWS:

{
  "payload": "base64url-encoded payload",
  "signatures": [
    {
      "protected": "base64url-encoded protected header",
      "header": { "unprotected": "header" },
      "signature": "base64url-encoded signature"
    }
  ]
}

Ключевые элементы:

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

Назначение функции generalVerify

Функция generalVerify из библиотеки jose выполняет проверку всех подписей, содержащихся в General JWS. Она возвращает результат первой успешно проверенной подписи или выбрасывает исключение, если ни одна подпись не прошла валидацию.

Импорт:

import { generalVerify } from 'jose'

Сигнатура функции

await generalVerify(jws, key, options)

Параметры:

  • jws — объект General JWS или строка JSON
  • key — ключ или функция получения ключа
  • options — дополнительные параметры валидации

Базовый пример верификации

import { generalVerify } from 'jose'

const jws = {
  payload: 'SGVsbG8gd29ybGQ',
  signatures: [
    {
      protected: 'eyJhbGciOiJIUzI1NiJ9',
      signature: '...'
    }
  ]
}

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

const { payload, protectedHeader } = await generalVerify(jws, secret)

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

Результат:

  • payloadUint8Array с декодированным содержимым
  • protectedHeader — объект заголовка

Работа с несколькими подписями

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

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

generalVerify:

  • перебирает подписи по порядку
  • пытается проверить каждую
  • возвращает результат при первой успешной проверке

Если ни одна подпись не валидна:

try {
  await generalVerify(jws, key)
} catch (err) {
  console.error('Все подписи недействительны')
}

Использование функции выбора ключа

Часто подписи могут быть созданы разными ключами. В этом случае используется функция-резолвер:

const keyResolver = async (protectedHeader, jws) => {
  if (protectedHeader.kid === 'key1') {
    return key1
  }
  if (protectedHeader.kid === 'key2') {
    return key2
  }
  throw new Error('Unknown key')
}

await generalVerify(jws, keyResolver)

Аргументы функции:

  • protectedHeader — заголовок подписи
  • jws — полный объект JWS

Валидация алгоритма

Ограничение допустимых алгоритмов:

await generalVerify(jws, key, {
  algorithms: ['HS256']
})

Если алгоритм подписи не входит в список — выбрасывается ошибка.

Проверка критических заголовков

Заголовки crit требуют явного указания:

await generalVerify(jws, key, {
  crit: ['exp']
})

Если критический заголовок присутствует, но не обработан — верификация завершится ошибкой.

Работа с полезной нагрузкой

Payload возвращается в виде Uint8Array. Преобразование:

const text = new TextDecoder().decode(payload)

Если payload содержит JSON:

const data = JSON.parse(text)

Верификация без декодирования payload

Возможна работа с бинарными данными без преобразования:

const { payload } = await generalVerify(jws, key)
// payload остаётся Uint8Array

Проверка нескольких ключей

При отсутствии kid или явной привязки:

const keys = [key1, key2, key3]

const resolver = async () => keys

await generalVerify(jws, resolver)

Библиотека переберёт ключи для каждой подписи.

Обработка ошибок

Типичные ошибки:

  • JWSInvalid — некорректная структура
  • JWSSignatureVerificationFailed — подпись не прошла проверку
  • JOSENotSupported — неподдерживаемый алгоритм

Пример:

try {
  await generalVerify(jws, key)
} catch (err) {
  if (err.code === 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') {
    // обработка ошибки подписи
  }
}

Верификация с удалённым JWK Set

Подключение удалённого набора ключей:

import { createRemoteJWKSet } from 'jose'

const JWKS = createRemoteJWKSet(new URL('https://example.com/.well-known/jwks.json'))

await generalVerify(jws, JWKS)

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

  • ключ автоматически выбирается по kid
  • поддерживается кэширование
  • обновление ключей происходит по мере необходимости

Сравнение с compactVerify

Характеристика generalVerify compactVerify
Поддержка подписей Множественные Одна
Формат JSON Строка
Гибкость Высокая Ограниченная
Использование Сложные сценарии Простые сценарии

Практические сценарии применения

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

Оптимизация производительности

При большом числе подписей:

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

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

  • ограничивать список допустимых алгоритмов
  • проверять kid и источник ключей
  • избегать доверия к незашифрованным заголовкам (header)
  • использовать только защищённые заголовки (protected)

Расширенные параметры options

await generalVerify(jws, key, {
  algorithms: ['RS256'],
  crit: ['b64'],
  complete: true
})
  • complete — возвращает дополнительные данные о подписи

Возвращаемое значение

{
  payload: Uint8Array,
  protectedHeader: object
}

При использовании complete: true:

{
  payload,
  protectedHeader,
  signature
}

Итерация по всем подписям вручную

Если требуется проверить каждую подпись:

for (const sig of jws.signatures) {
  try {
    await generalVerify(
      { payload: jws.payload, signatures: [sig] },
      key
    )
    console.log('Подпись валидна')
  } catch {}
}

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

  • строгая проверка RFC 7515
  • отказ при любых несоответствиях
  • безопасная работа с алгоритмами
  • отсутствие «тихих» ошибок

Частые ошибки при использовании

  • передача строки вместо объекта без JSON.parse
  • неправильный ключ
  • отсутствие поддержки алгоритма
  • игнорирование kid

Рекомендации

  • всегда использовать protected заголовки
  • не доверять незашифрованным данным
  • использовать функцию-резолвер для гибкости
  • ограничивать алгоритмы
  • логировать ошибки верификации