Управление kid при смене ключей

В спецификации JOSE (JSON Object Signing and Encryption) идентификатор ключа kid является частью защищённого заголовка JWS/JWE и служит для однозначного определения ключа, которым выполняется подпись или шифрование. В контексте библиотеки jose в JavaScript этот параметр становится критическим элементом при ротации ключей, работе с JWKS (JSON Web Key Set) и валидации токенов в распределённых системах.

kid хранится внутри JWK и пробрасывается в заголовок токена:

{
  "alg": "RS256",
  "kid": "key-2026-01"
}

Его основная функция — связать токен с конкретным криптографическим ключом без необходимости перебора всех доступных ключей при проверке подписи.


Использование kid при генерации токена

В jose при создании JWT идентификатор ключа передаётся через JWK:

import { SignJWT, exportJWK } from 'jose'
import { generateKeyPair } from 'jose'

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

const jwk = await exportJWK(privateKey)
jwk.kid = 'key-2026-01'

const token = await new SignJWT({ sub: '123' })
  .setProtectedHeader({ alg: 'RS256', kid: jwk.kid })
  .setIssuedAt()
  .setExpirationTime('2h')
  .sign(privateKey)

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


Проверка токена и выбор ключа по kid

При верификации токена библиотека jose использует kid для выбора нужного ключа из набора.

Работа с JWKS endpoint

Типичный сценарий — удалённое хранилище ключей:

import { jwtVerify, createRemoteJWKSet } from 'jose'

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

const { payload } = await jwtVerify(token, JWKS)

Внутри createRemoteJWKSet происходит:

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

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


Ротация ключей и изменение kid

Ротация ключей — основная причина использования kid. При смене криптографического материала система должна поддерживать несколько активных ключей одновременно.

Типичный жизненный цикл:

  1. Создание нового ключа с новым kid
  2. Добавление его в JWKS
  3. Начало подписания новых токенов новым ключом
  4. Сохранение старого ключа до истечения срока жизни старых токенов
  5. Удаление старого ключа после полной миграции

Пример набора ключей:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "key-2025-01",
      "use": "sig"
    },
    {
      "kty": "RSA",
      "kid": "key-2026-01",
      "use": "sig"
    }
  ]
}

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

При валидации токена с изменившимся kid библиотека выполняет поиск по текущему набору ключей. Если токен подписан старым ключом, но он всё ещё присутствует в JWKS, проверка проходит корректно.

Проблема возникает в двух случаях:

  • ключ удалён раньше окончания срока жизни токена
  • kid не совпадает с реальным идентификатором в JWKS

В таких случаях возникает ошибка:

JWSInvalid: Unable to find a key matching the kid

Кеширование JWKS и влияние на kid

createRemoteJWKSet использует внутренний кеш:

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

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


Явное сопоставление ключей по kid

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

import { importJWK, jwtVerify } from 'jose'

const keys = {
  'key-2026-01': await importJWK({
    kty: 'RSA',
    n: '...',
    e: 'AQAB'
  }),
}

async function verify(token) {
  const { protectedHeader } = JSON.parse(
    Buffer.from(token.split('.')[0], 'base64').toString()
  )

  const key = keys[protectedHeader.kid]

  if (!key) throw new Error('Unknown kid')

  return jwtVerify(token, key)
}

Такой подход используется в системах без JWKS endpoint или при строгом контроле над ключами.


Типовые ошибки при работе с kid

1. Отсутствие kid в токене

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

2. Несовпадение kid в JWK и заголовке

Если ключ экспортирован с одним идентификатором, а токен подписан с другим, проверка не пройдёт.

3. Дублирование kid

В JWKS наличие двух ключей с одинаковым kid делает выбор неоднозначным и приводит к непредсказуемому поведению.

4. Преждевременное удаление ключей

Удаление старого ключа до истечения срока жизни всех токенов приводит к массовым ошибкам валидации.


Стратегии управления kid при ротации ключей

Стабильная схема идентификаторов

Часто используется формат:

<алгоритм>-<дата>
RS256-2026-01

или

key-v1, key-v2, key-v3

Перекрывающиеся окна ключей

При смене ключа оба активны одновременно:

  • новый ключ используется для подписи
  • старый — для проверки

Версионирование JWKS

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


Взаимодействие kid с алгоритмами и JWKS

kid не несёт криптографического значения. Его роль исключительно идентификационная. При этом он тесно связан с:

  • alg (алгоритм подписи)
  • kty (тип ключа)
  • use (назначение ключа)

Несоответствие между этими параметрами часто приводит к ошибкам валидации даже при корректном kid.


Поведение jose при отсутствии точного совпадения

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

  1. поиск по kid
  2. фильтрация по alg
  3. проверка всех подходящих ключей
  4. исключение неподходящих по криптографии

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


Практика безопасной ротации

Стабильная схема работы с kid в распределённой системе обычно включает:

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

Такая модель позволяет исключить массовые сбои при обновлении криптографического слоя без остановки сервиса