Конвертация JWK → PEM

JWK (JSON Web Key) представляет собой JSON-структуру, описывающую криптографический ключ в стандартизированном виде. PEM (Privacy Enhanced Mail) — текстовый формат, основанный на Base64-кодировании бинарного DER-представления ключа и обёрнутый в заголовки вида -----BEGIN PUBLIC KEY-----.

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

  • JWK ориентирован на веб-экосистему, REST и JSON API
  • PEM используется в криптографических библиотеках, TLS, OpenSSL и инфраструктуре PKI

Jsrsasign предоставляет инструменты для взаимного преобразования этих форматов без необходимости ручной работы с ASN.1 структурой.


Структура JWK

RSA-ключ в формате JWK содержит набор параметров:

{
  "kty": "RSA",
  "n": "modulus_base64url",
  "e": "exponent_base64url",
  "d": "private_exponent",
  "p": "prime1",
  "q": "prime2",
  "dp": "d mod (p-1)",
  "dq": "d mod (q-1)",
  "qi": "q^-1 mod p"
}

Для публичного ключа достаточно:

  • n — модуль
  • e — публичная экспонента

Для приватного ключа присутствует полный набор CRT-параметров.

EC-ключи используют другие поля:

{
  "kty": "EC",
  "crv": "P-256",
  "x": "x_coordinate",
  "y": "y_coordinate",
  "d": "private_key"
}

PEM формат и его представление

PEM-кодирование — это Base64 DER-структуры с разделителями:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqh...
-----END PUBLIC KEY-----

Для RSA приватного ключа:

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqh...
-----END PRIVATE KEY-----

Внутренне PEM всегда сводится к ASN.1 DER структуре.


Основной механизм Jsrsasign

В библиотеке Jsrsasign ключевую роль играет объект KEYUTIL.

Он обеспечивает:

  • парсинг JWK
  • преобразование в внутренний KeyObject
  • экспорт в PEM

Конвертация JWK → PEM (RSA публичный ключ)

const jwk = {
  kty: "RSA",
  n: "base64url_modulus",
  e: "AQAB"
};

const keyObj = KEYUTIL.getKey(jwk);
const pem = KEYUTIL.getPEM(keyObj);

console.log(pem);

Результатом будет PEM публичного ключа RSA.


Конвертация JWK → PEM (RSA приватный ключ)

Если JWK содержит приватные параметры:

const jwkPrivate = {
  kty: "RSA",
  n: "base64url_modulus",
  e: "AQAB",
  d: "base64url_private_exponent",
  p: "prime1",
  q: "prime2",
  dp: "...",
  dq: "...",
  qi: "..."
};

const keyObj = KEYUTIL.getKey(jwkPrivate);
const pemPrivate = KEYUTIL.getPEM(keyObj, "PKCS8PRV");

console.log(pemPrivate);

Здесь используется формат PKCS#8 для приватного ключа.


EC ключи: JWK → PEM

Для эллиптических кривых:

const ecJwk = {
  kty: "EC",
  crv: "P-256",
  x: "base64url_x",
  y: "base64url_y"
};

const ecKey = KEYUTIL.getKey(ecJwk);
const ecPem = KEYUTIL.getPEM(ecKey);

console.log(ecPem);

Если присутствует d, ключ становится приватным.


Разбор внутреннего преобразования

Jsrsasign выполняет цепочку преобразований:

  1. JWK → внутренний объект KEYUTIL
  2. KEYUTIL → ASN.1 структура
  3. ASN.1 → DER бинарный формат
  4. DER → PEM (Base64 + заголовки)

Ключевой этап — интерпретация JWK параметров в соответствии с типом алгоритма (kty).


Обратное преобразование (для понимания структуры)

Хотя задача касается JWK → PEM, важно понимать симметрию:

const pem = `-----BEGIN PUBLIC KEY-----...`;

const keyObj = KEYUTIL.getKey(pem);
const jwk = KEYUTIL.getJWK(keyObj);

Это позволяет проверять корректность преобразования.


Особенности base64url в JWK

JWK использует base64url, отличающийся от стандартного Base64:

  • + заменяется на -
  • / заменяется на _
  • отсутствует padding =

Jsrsasign автоматически выполняет нормализацию, но при ручной обработке важно учитывать это различие.


Типичные ошибки при конвертации

1. Неполный JWK RSA ключ

Если отсутствуют p, q, d, Jsrsasign не сможет сформировать приватный PEM.

2. Неверный kty

Допустимые значения:

  • RSA
  • EC
  • oct (симметричные ключи, не конвертируются в PEM напрямую)

3. Несоответствие кривой EC

Поле crv должно быть строго поддерживаемым:

  • P-256
  • P-384
  • P-521

Использование в Node.js

const { KEYUTIL } = require("jsrsasign");

const jwk = require("./key.json");

const key = KEYUTIL.getKey(jwk);
const pem = KEYUTIL.getPEM(key);

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

При подключении через CDN:

<script src="https://cdn.jsdelivr.net/npm/jsrsasign/lib/jsrsasign-all-min.js"></script>
<script>
  const key = KEYUTIL.getKey(jwk);
  const pem = KEYUTIL.getPEM(key);
</script>

Преобразование RSA JWK через промежуточный формат

Иногда используется явное создание ключа:

const key = new RSAKey();
key.setPublic(jwk.n, jwk.e);
const pem = KEYUTIL.getPEM(key);

Такой подход полезен при частично заполненных JWK.


Работа с PKCS#1 и PKCS#8

Jsrsasign поддерживает два основных формата приватных ключей:

  • PKCS#1: RSA-specific
  • PKCS#8: универсальный контейнер
KEYUTIL.getPEM(keyObj, "PKCS1PRV");
KEYUTIL.getPEM(keyObj, "PKCS8PRV");

PKCS#8 предпочтителен для современных систем.


Валидация результата конвертации

После преобразования PEM можно проверить корректность:

const keyObj2 = KEYUTIL.getKey(pem);

Если ключ восстанавливается без ошибок, конвертация считается успешной.


Производственные нюансы

При массовой конвертации JWK важно учитывать:

  • кэширование parsed key objects
  • избегание повторного Base64 decode
  • проверку структуры перед вызовом getKey
  • различие между публичными и приватными JWK

Jsrsasign не выполняет строгую валидацию JWK по RFC на входе, поэтому ошибки часто проявляются на этапе ASN.1 генерации.


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

При конвертации JWK → PEM приватный ключ становится доступен в текстовом виде. Это требует:

  • исключения логирования PEM
  • очистки переменных после использования
  • передачи ключей только через защищённые каналы
  • ограничения области видимости объектов KEYUTIL

Преобразование ключей для JWT инфраструктуры

В системах JWT часто требуется перевод JWK из JWKS endpoint в PEM:

const jwk = jwks.keys[0];

const key = KEYUTIL.getKey(jwk);
const pem = KEYUTIL.getPEM(key);

Это позволяет использовать PEM в библиотеках, ожидающих X.509 формат.