Работа с сертификатами X.509

X.509-сертификаты используются как стандартный способ распространения и валидации публичных ключей в инфраструктуре открытых ключей. В контексте JOSE (JSON Object Signing and Encryption), который включает JWS, JWE и JWT, сертификаты X.509 выполняют функцию доверенного контейнера для публичного ключа и метаданных о владельце ключа.

В библиотеке Jose X.509 применяется в нескольких ключевых сценариях:

  • импорт публичных ключей из сертификатов;
  • извлечение ключей из цепочек доверия;
  • использование заголовка x5c в JWT/JWS;
  • проверка подписи через сертификат вместо прямого JWKS;
  • построение доверенной цепочки сертификатов.

Форматы представления сертификатов

Перед использованием в Jose важно понимать, в каком виде сертификат поступает в систему.

PEM (Base64 с заголовками)

Наиболее распространённый формат:

-----BEGIN CERTIFICATE-----
MIID...
-----END CERTIFICATE-----

Используется для передачи через конфигурации, переменные окружения, файлы.

DER (бинарный формат)

Чистое бинарное представление сертификата X.509. Обычно используется внутри систем и при работе с низкоуровневыми API.

SPKI и PKCS#8

Хотя это не X.509 напрямую, они тесно связаны:

  • SPKI — формат публичного ключа
  • PKCS#8 — формат приватного ключа

Jose работает с ними через отдельные функции импорта.


Импорт X.509 сертификатов в Jose

Основная функция для работы с сертификатами:

import { importX509 } from 'jose'

Импорт публичного ключа из сертификата

import { importX509 } from 'jose'

const pem = `
-----BEGIN CERTIFICATE-----
MIID... (certificate data)
-----END CERTIFICATE-----
`

const publicKey = await importX509(pem, 'RS256')

Здесь:

  • pem — строка сертификата;
  • RS256 — алгоритм, для которого будет использоваться ключ.

Функция извлекает публичный ключ из X.509 и приводит его к формату CryptoKey (Web Crypto API).


Работа с SPKI как альтернативой X.509

Во многих случаях проще использовать SPKI:

import { importSPKI } from 'jose'

const spki = `-----BEGIN PUBLIC KEY-----
MIIB...
-----END PUBLIC KEY-----`

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

Различие:

  • X.509 содержит метаданные сертификата;
  • SPKI содержит только публичный ключ.

Заголовок x5c в JWS и JWT

Одним из ключевых механизмов интеграции X.509 в JOSE является заголовок x5c.

Структура x5c

x5c — это массив сертификатов в формате Base64 DER без PEM-обёртки:

{
  "alg": "RS256",
  "x5c": [
    "MIID...base64cert...",
    "MIIC...intermediate..."
  ]
}

Первый элемент — сертификат подписывающего ключа.


Проверка JWT с использованием x5c

Jose позволяет проверять JWT, извлекая ключ прямо из сертификата.

Пример проверки

import { jwtVerify, importX509 } from 'jose'

async function verifyToken(token) {
  const { payload, protectedHeader } = await jwtVerify(token, async (header) => {
    const cert = Buffer.from(header.x5c[0], 'base64').toString('utf-8')

    return await importX509(cert, header.alg)
  })

  return payload
}

Логика работы

  1. Из JWT извлекается заголовок.
  2. Из x5c берётся сертификат.
  3. Сертификат декодируется из Base64.
  4. Из сертификата извлекается публичный ключ.
  5. Ключ используется для проверки подписи.

Работа с цепочкой сертификатов

X.509 поддерживает цепочки доверия:

  • leaf certificate (конечный сертификат)
  • intermediate CA
  • root CA

В x5c цепочка представляется массивом:

"x5c": [
  "leaf-cert",
  "intermediate-cert",
  "root-cert"
]

Jose не выполняет полноценную валидацию цепочки как PKI-система, но позволяет:

  • извлечь ключ из leaf-сертификата;
  • передать цепочку в сторонние валидаторы;
  • интегрироваться с внешними trust store.

Извлечение информации о сертификате

Хотя Jose не является полноценной библиотекой для работы с X.509 метаданными, можно комбинировать его с Web Crypto API.

Пример получения thumbprint:

import { calculateJwkThumbprint } from 'jose'

const thumbprint = await calculateJwkThumbprint(publicKey)

Используется для:

  • идентификации ключа;
  • сопоставления с JWKS;
  • кэширования доверенных ключей.

Использование X.509 в архитектуре OIDC

В системах OpenID Connect часто применяется схема:

  • Identity Provider подписывает JWT;
  • публичный ключ распространяется через x5c или JWKS;
  • клиент проверяет токен через Jose.

Типичная схема:

  1. Получение JWT от IdP.
  2. Извлечение x5c.
  3. Импорт сертификата через importX509.
  4. Проверка подписи через jwtVerify.

Валидация и доверие к сертификатам

Критически важный момент: Jose не выполняет проверку доверия к сертификату.

Это означает:

  • отсутствие проверки CA;
  • отсутствие проверки срока действия;
  • отсутствие проверки отзыва (CRL/OCSP).

Все эти проверки должны быть реализованы отдельно.

Рекомендуемая схема безопасности

  • проверка цепочки через Node.js crypto или внешние библиотеки;
  • проверка notBefore и notAfter;
  • проверка отпечатка сертификата;
  • сопоставление с whitelist доверенных CA.

Ошибки при работе с X.509

Неверный формат PEM

Частая проблема:

Error: failed to parse certificate

Причина:

  • отсутствуют заголовки PEM;
  • нарушена Base64-структура;
  • лишние пробелы.

Несоответствие алгоритма

importX509(cert, 'ES256')

Если сертификат RSA, а указан ES256, возникнет ошибка несоответствия ключа.


Отсутствие x5c

Некоторые JWT содержат только kid:

{
  "kid": "abc123"
}

В этом случае требуется JWKS, а не X.509.


Работа в Node.js и браузере

Jose полностью поддерживает Web Crypto API, поэтому X.509 работает в обеих средах.

Node.js

Требуется:

import { webcrypto } from 'crypto'
globalThis.crypto = webcrypto

Браузер

Поддержка встроена, дополнительных настроек не требуется.


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

import { jwtVerify, importX509 } from 'jose'

const token = 'eyJ...'

const result = await jwtVerify(token, async (header) => {
  const certPem =
    '-----BEGIN CERTIFICATE-----\n' +
    header.x5c[0] +
    '\n-----END CERTIFICATE-----'

  return await importX509(certPem, header.alg)
})

console.log(result.payload)

Использование X.509 в JWS (подпись сообщений)

Помимо JWT, X.509 применяется и в JWS:

import { importX509, SignJWT } from 'jose'

Сценарий:

  • сертификат используется для получения публичного ключа;
  • приватный ключ хранится отдельно;
  • подпись выполняется через SignJWT.

Взаимодействие с JWKS и x5c

Сравнение подходов:

  • JWKS — JSON набор ключей;
  • x5c — сертификатная модель;
  • SPKI — прямой ключ.

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

  • JWKS как основной источник;
  • x5c как fallback или bootstrap доверия.

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

Импорт X.509 — относительно дорогая операция.

Рекомендуется:

  • кешировать результат importX509;
  • кешировать по thumbprint;
  • избегать повторного парсинга сертификатов.

Пример:

const cache = new Map()

async function getKey(cert, alg) {
  const key = cache.get(cert)
  if (key) return key

  const imported = await importX509(cert, alg)
  cache.set(cert, imported)

  return imported
}

Типичные сценарии применения

  • проверка токенов OAuth2/OpenID Connect;
  • интеграция с корпоративными PKI;
  • взаимодействие с внешними IdP;
  • банковские и финтех-системы;
  • межсервисная аутентификация.

Ограничения подхода X.509 в Jose

  • отсутствует полноценная PKI-валидация;
  • нет встроенной проверки цепочек доверия;
  • нет OCSP/CRL проверки;
  • ограниченность работы только с публичными ключами.

Эти аспекты требуют внешней инфраструктуры безопасности.