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

Flattened JSON Serialization — один из трёх форматов представления JSON Web Signature (JWS), наряду с Compact и General. Он используется в ситуациях, когда требуется передать одну подпись, но при этом сохранить структуру JSON вместо строкового представления.

Структура Flattened JWS включает:

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

Пример:

{
  "payload": "eyJzdWIiOiIxMjM0NTY3ODkwIn0",
  "protected": "eyJhbGciOiJIUzI1NiJ9",
  "signature": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}

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

Функция flattenedVerify из библиотеки jose выполняет проверку подписи Flattened JWS и извлекает полезную нагрузку. Это низкоуровневый инструмент, предоставляющий полный контроль над процессом верификации.

Основные задачи:

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

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

import { flattenedVerify } from 'jose'

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

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

Где:

  • jws — объект Flattened JWS
  • key — криптографический ключ (SecretKey, PublicKey, KeyLike)

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

1. JWS объект

Объект должен строго соответствовать Flattened JSON Serialization:

const jws = {
  payload: '...',
  protected: '...',
  signature: '...'
}

Ошибки структуры приводят к исключению.

2. Ключ (KeyLike | Uint8Array | CryptoKey)

Поддерживаются:

  • симметричные ключи (HMAC)
  • асимметричные публичные ключи (RSA, EC, OKP)

Пример:

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

или:

import { importSPKI } from 'jose'

const key = await importSPKI(spki, 'RS256')

3. Опции

await flattenedVerify(jws, key, {
  algorithms: ['RS256'],
  clockTolerance: '5s'
})

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

  • algorithms — список допустимых алгоритмов
  • clockTolerance — допустимая погрешность времени
  • typ — ожидаемый тип токена
  • issuer, audience — для дополнительной проверки payload

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

{
  payload: Uint8Array,
  protectedHeader: object
}

Расшифровка payload

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

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

Алгоритм берётся из protected заголовка:

{
  "alg": "HS256"
}

Если алгоритм не входит в разрешённый список, будет выброшено исключение.

Пример ограничения:

await flattenedVerify(jws, key, {
  algorithms: ['ES256']
})

Работа с заголовками

protectedHeader

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

Содержит:

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

Незащищённый header

Если присутствует:

"header": {
  "kid": "key1"
}

Он также участвует в процессе, но не защищён подписью.

Пример с HMAC

import { flattenedVerify } from 'jose'

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

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

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

Пример с RSA

import { flattenedVerify, importSPKI } from 'jose'

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

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

Проверка времени жизни токена

Хотя flattenedVerify не проверяет exp автоматически, это можно сделать вручную:

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

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

if (data.exp && Date.now() >= data.exp * 1000) {
  throw new Error('Token expired')
}

Проверка аудитории и издателя

await flattenedVerify(jws, key, {
  audience: 'my-app',
  issuer: 'auth-server'
})

Если значения не совпадают — ошибка.

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

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

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

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

try {
  await flattenedVerify(jws, key)
} catch (err) {
  console.error(err)
}

Безопасные практики

1. Ограничение алгоритмов

algorithms: ['RS256']

Исключает атаки с подменой алгоритма.

2. Проверка kid

Используется для выбора ключа:

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

if (protectedHeader.kid !== expectedKid) {
  throw new Error('Invalid key id')
}

3. Не доверять payload до проверки

Декодирование payload допустимо только после успешной верификации.

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

Предпочтительно для распределённых систем.

Отличие от compactVerify

Характеристика flattenedVerify compactVerify
Формат JSON строка
Подписи одна одна
Гибкость высокая ниже
Использование API, сложные сценарии простые токены

Отличие от generalVerify

  • flattenedVerify — одна подпись
  • generalVerify — несколько подписей

Расширенный пример

import { flattenedVerify, importJWK } from 'jose'

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

const key = await importJWK(jwk, 'HS256')

const { payload, protectedHeader } = await flattenedVerify(jws, key, {
  algorithms: ['HS256']
})

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

console.log(protectedHeader.alg)
console.log(decoded)

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

  • Работает асинхронно (использует WebCrypto / Node crypto)
  • Минимизирует аллокации
  • Поддерживает браузеры и Node.js

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

  • HMAC: HS256, HS384, HS512
  • RSA: RS256, PS256 и др.
  • EC: ES256, ES384
  • OKP: EdDSA

Проверка подписи: ключевые этапы

  1. Декодирование protected
  2. Проверка алгоритма
  3. Построение signing input
  4. Криптографическая проверка подписи
  5. Возврат результата

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

  • при работе с JSON API
  • при необходимости явного контроля структуры
  • при наличии дополнительных заголовков
  • при интеграции с JWS, не использующими Compact формат