Верификация компактного JWS: compactVerify

Компактное представление JSON Web Signature (JWS) состоит из трёх частей, разделённых точками:

BASE64URL(Protected Header) . BASE64URL(Payload) . BASE64URL(Signature)
  • Protected Header — содержит алгоритм подписи (alg) и дополнительные параметры
  • Payload — полезная нагрузка (например, JWT)
  • Signature — криптографическая подпись

Библиотека jose предоставляет метод compactVerify для проверки корректности такой структуры и подлинности подписи.


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

Для работы с compactVerify необходимо импортировать функцию:

import { compactVerify } from 'jose'

Минимальный пример верификации:

const { payload, protectedHeader } = await compactVerify(jws, key)

Где:

  • jws — строка в компактном формате
  • key — ключ для проверки подписи (секретный или публичный)

Поддерживаемые типы ключей

Метод compactVerify принимает различные типы ключей:

  • Uint8Array (для симметричных алгоритмов, например HS256)
  • CryptoKey (Web Crypto API)
  • KeyObject (Node.js)
  • JWK (JSON Web Key)

Пример симметричного ключа:

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

Пример асимметричного ключа:

import { importSPKI } from 'jose'

const publicKey = await importSPKI(spkiPem, 'RS256')

Результат выполнения

compactVerify возвращает объект:

{
  payload: Uint8Array,
  protectedHeader: { alg: string, ... }
}
  • payload — бинарное представление полезной нагрузки
  • protectedHeader — объект с декодированным заголовком

Для преобразования payload в строку:

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

Пример полной верификации

import { compactVerify } from 'jose'

const jws = 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiam9obiJ9.xxxxxx'
const secret = new TextEncoder().encode('secret')

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

console.log(protectedHeader.alg) // HS256
console.log(new TextDecoder().decode(payload)) // {"user":"john"}

Проверка алгоритма

compactVerify не ограничивает автоматически допустимые алгоритмы. Проверка значения alg должна выполняться явно:

if (protectedHeader.alg !== 'HS256') {
  throw new Error('Недопустимый алгоритм')
}

Это критично для предотвращения атак подмены алгоритма.


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

Метод выбрасывает исключения в следующих случаях:

  • Некорректный формат JWS
  • Ошибка декодирования Base64URL
  • Неверная подпись
  • Несовпадение ключа и алгоритма

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

try {
  await compactVerify(jws, key)
} catch (err) {
  console.error('Ошибка верификации:', err)
}

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

Хотя compactVerify может использоваться для проверки JWT, он не валидирует стандартные поля (exp, nbf, iss). Для этого применяется jwtVerify.

Тем не менее, базовая проверка возможна:

const { payload } = await compactVerify(token, key)
const claims = JSON.parse(new TextDecoder().decode(payload))

Верификация с использованием JWK

const jwk = {
  kty: 'oct',
  k: 'c2VjcmV0'
}

const { payload } = await compactVerify(jws, jwk)

Асинхронные источники ключей

compactVerify поддерживает функцию-резолвер ключа:

const getKey = async (protectedHeader) => {
  if (protectedHeader.alg === 'RS256') {
    return publicKey
  }
  throw new Error('Unknown alg')
}

await compactVerify(jws, getKey)

Это удобно при работе с JWKS или динамическими ключами.


Работа с JWKS (JSON Web Key Set)

Пример получения ключа из удалённого источника:

import { createRemoteJWKSet } from 'jose'

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

const { payload } = await compactVerify(jws, JWKS)

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


Проверка дополнительных параметров заголовка

Protected Header может содержать:

  • kid — идентификатор ключа
  • typ — тип токена
  • cty — тип содержимого

Пример:

if (!protectedHeader.kid) {
  throw new Error('Отсутствует kid')
}

Безопасность и лучшие практики

1. Явная проверка алгоритма

Никогда не доверять значению alg без проверки.

2. Использование строгих ключей

  • HS256 → только секретные ключи
  • RS256 / ES256 → только публичные ключи

3. Ограничение источников ключей

При использовании удалённых JWKS:

  • кэширование
  • проверка домена
  • защита от SSRF

4. Проверка структуры payload

const data = JSON.parse(decoded)

if (typeof data.user !== 'string') {
  throw new Error('Invalid payload')
}

Отличие от других методов библиотеки

Метод Назначение
compactVerify Проверка JWS (низкий уровень)
jwtVerify Проверка JWT + валидация claims
generalVerify Проверка JWS в общем JSON формате

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

compactVerify поддерживает все алгоритмы, реализованные в jose:

  • HMAC: HS256, HS384, HS512
  • RSA: RS256, RS384, RS512
  • ECDSA: ES256, ES384, ES512
  • EdDSA

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


Работа в разных средах

Библиотека jose универсальна:

  • Node.js (через crypto)
  • Браузер (через Web Crypto API)
  • Deno, Bun

compactVerify автоматически использует доступные криптографические примитивы.


Пример с асимметричной подписью

import { compactVerify, importSPKI } from 'jose'

const publicKey = await importSPKI(spkiPem, 'RS256')

const { payload } = await compactVerify(jws, publicKey)

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

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

Факторы, влияющие на скорость:

  • алгоритм (HMAC быстрее RSA/ECDSA)
  • размер payload
  • тип ключа
  • использование удалённых JWKS

Для высоконагруженных систем рекомендуется:

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

Расширенные сценарии

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

const keys = [key1, key2]

const getKey = async () => {
  for (const key of keys) {
    try {
      await compactVerify(jws, key)
      return key
    } catch {}
  }
  throw new Error('No valid key')
}

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

  • Использование неподходящего ключа (например, публичного вместо секретного)
  • Отсутствие проверки alg
  • Игнорирование ошибок верификации
  • Неправильное декодирование payload
  • Доверие данным без дополнительной валидации

Минимальный шаблон безопасной верификации

const { payload, protectedHeader } = await compactVerify(jws, key)

if (protectedHeader.alg !== 'HS256') {
  throw new Error('Invalid alg')
}

const data = JSON.parse(new TextDecoder().decode(payload))

if (!data.user) {
  throw new Error('Invalid payload')
}

Такой подход обеспечивает базовый уровень безопасности при работе с компактными JWS.