Создание и разбор JWKS вручную

JWKS (JSON Web Key Set) представляет собой JSON-документ, содержащий набор криптографических ключей в формате JWK (JSON Web Key). Используется для публикации публичных ключей, применяемых при проверке JWT.

Базовая структура JWKS:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "key-1",
      "use": "sig",
      "alg": "RS256",
      "n": "modulus",
      "e": "exponent"
    }
  ]
}

Каждый элемент массива keys описывает отдельный ключ. Наиболее важные поля:

  • kty — тип ключа (RSA, EC и др.)
  • kid — уникальный идентификатор ключа
  • use — назначение (обычно sig для подписи)
  • alg — алгоритм
  • n и e — параметры RSA публичного ключа

Формирование JWK из RSA ключа через jose

В библиотеке jose генерация и экспорт ключей выполняется через WebCrypto API-обёртки.

import { generateKeyPair, exportJWK } from 'jose';

const { publicKey, privateKey } = await generateKeyPair('RS256');

const publicJwk = await exportJWK(publicKey);
const privateJwk = await exportJWK(privateKey);

publicJwk.kid = 'key-1';
publicJwk.use = 'sig';
publicJwk.alg = 'RS256';

После экспорта JWK необходимо дополнить метаданными, которые не всегда автоматически добавляются.


Ручное формирование JWKS

JWKS создаётся как контейнер для одного или нескольких JWK:

const jwks = {
  keys: [publicJwk]
};

При ротации ключей структура расширяется:

const jwks = {
  keys: [
    publicJwk_v1,
    publicJwk_v2
  ]
};

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


Извлечение ключа из JWKS вручную

При проверке JWT сначала извлекается заголовок токена:

function decodeHeader(token) {
  const [header] = token.split('.');
  return JSON.parse(Buffer.from(header, 'base64url').toString());
}

Далее извлекается kid:

const header = decodeHeader(token);
const kid = header.kid;

Поиск ключа в JWKS:

function findJwk(jwks, kid) {
  return jwks.keys.find(key => key.kid === kid);
}

Преобразование JWK в криптографический ключ

Для проверки подписи JWK должен быть импортирован в формат, который понимает jose:

import { importJWK } from 'jose';

const jwk = findJwk(jwks, kid);

const publicKey = await importJWK(jwk, 'RS256');

Проверка JWT с импортированным ключом

После получения ключа выполняется проверка токена:

import { jwtVerify } from 'jose';

const { payload } = await jwtVerify(token, publicKey);

Если подпись не совпадает, выбрасывается исключение, а если ключ с указанным kid отсутствует — процесс проверки прерывается на этапе поиска.


Полностью ручной процесс верификации JWKS

Последовательность шагов без автоматических помощников:

const header = decodeHeader(token);
const jwk = findJwk(jwks, header.kid);

if (!jwk) {
  throw new Error('Key not found');
}

const publicKey = await importJWK(jwk, 'RS256');
const { payload } = await jwtVerify(token, publicKey);

Особенности работы с несколькими ключами

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

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

Типичная схема ротации:

const jwks = {
  keys: [
    { kid: 'old-key', ... },
    { kid: 'current-key', ... }
  ]
};

Формирование собственного JWKS без jose

В случаях, когда ключи хранятся вне библиотеки:

const jwks = {
  keys: [
    {
      kty: 'RSA',
      kid: 'manual-key',
      use: 'sig',
      alg: 'RS256',
      n: '<base64url-modulus>',
      e: 'AQAB'
    }
  ]
};

Такая структура требует корректного кодирования n и e в base64url без padding.


Валидация соответствия ключа алгоритму

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

  • alg в JWT должен совпадать с alg в JWK
  • kty должен соответствовать алгоритму (RSA для RS256)
  • отсутствие проверки приводит к уязвимостям подмены алгоритма

Использование thumbprint для идентификации ключей

Помимо kid возможно использование thumbprint — криптографического отпечатка ключа:

import { calculateJwkThumbprint } from 'jose';

const thumbprint = await calculateJwkThumbprint(publicJwk);

Thumbprint позволяет детектировать изменения ключа даже при одинаковом kid.


Обработка ошибок при разборе JWKS

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

  • отсутствует kid в JWT
  • ключ не найден в наборе
  • неверная кодировка n или e
  • несовпадение алгоритма

Обработка:

try {
  const jwk = findJwk(jwks, kid);
  const key = await importJWK(jwk, 'RS256');
  await jwtVerify(token, key);
} catch (e) {
  // токен недействителен
}

Ручная реализация поиска ключа по принципу fallback

В некоторых системах допускается поиск без kid:

function findAnyKey(jwks) {
  return jwks.keys[0];
}

Такой подход используется только при отсутствии ротации ключей и считается упрощённым режимом, не подходящим для распределённых систем.