Преобразование между форматами PEM и JWK

PEM и JWK представляют собой два принципиально разных способа представления криптографических ключей. PEM ориентирован на текстовое кодирование ключевых материалов в формате Base64 с обрамлением BEGIN/END блоками, тогда как JWK описывает ключ как структурированный JSON-объект с явным указанием параметров алгоритма, типа ключа и его компонентов.

В библиотеке Jose преобразование между этими форматами реализовано через набор функций импорта и экспорта ключей, работающих поверх стандарта WebCrypto API. Внутренне Jose оперирует объектами CryptoKey, а преобразование PEM ↔︎ JWK является этапом сериализации и десериализации ключевого материала.


PEM (Privacy-Enhanced Mail) используется для представления ключей в виде текстовых блоков:

  • PKCS#8 — приватные ключи
  • SPKI (X.509 SubjectPublicKeyInfo) — публичные ключи
  • RSA PRIVATE KEY / PUBLIC KEY (PKCS#1) — устаревшие форматы, но до сих пор встречаются

Пример PEM публичного ключа:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----

Jose не работает напрямую со строками PEM в криптооперациях. PEM сначала преобразуется в CryptoKey.


JWK как структурированное представление ключа

JWK (JSON Web Key) описывает ключ в виде JSON:

{
  "kty": "RSA",
  "n": "...",
  "e": "AQAB",
  "alg": "RS256",
  "ext": true,
  "key_ops": ["verify"]
}

Ключевые особенности JWK:

  • Явное описание алгоритма (alg)
  • Чёткое разделение параметров RSA, EC, OKP
  • Возможность хранения в JSON API и конфигурациях
  • Удобство для передачи между сервисами

Импорт PEM в Jose (PEM → CryptoKey → JWK)

Импорт публичного ключа (SPKI)

В Jose используется importSPKI:

import { importSPKI } from 'jose'

const pemPublicKey = `
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqh...
-----END PUBLIC KEY-----
`

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

После импорта ключ становится объектом CryptoKey, пригодным для проверки JWT или других операций.


Импорт приватного ключа (PKCS#8)

import { importPKCS8 } from 'jose'

const pemPrivateKey = `
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqh...
-----END PRIVATE KEY-----
`

const privateKey = await importPKCS8(pemPrivateKey, 'RS256')

Экспорт в JWK формат

После получения CryptoKey Jose позволяет получить JWK-представление.

Экспорт публичного ключа в JWK

import { exportJWK } from 'jose'

const jwk = await exportJWK(publicKey)

console.log(jwk)

Результат будет содержать параметры, зависящие от типа ключа:

RSA:

{
  "kty": "RSA",
  "n": "...",
  "e": "AQAB"
}

EC:

{
  "kty": "EC",
  "crv": "P-256",
  "x": "...",
  "y": "..."
}

Экспорт приватного ключа в JWK

Если CryptoKey содержит приватную часть:

const jwkPrivate = await exportJWK(privateKey)

В этом случае JWK дополнительно включает параметр:

{
  "d": "..."
}

Обратное преобразование JWK → CryptoKey

Импорт JWK в Jose

import { importJWK } from 'jose'

const jwk = {
  kty: 'RSA',
  n: '...',
  e: 'AQAB'
}

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

Если ключ содержит приватные параметры (d, p, q и др.), он будет импортирован как приватный.


Полный цикл преобразования PEM → JWK

Типовой сценарий включает два этапа:

1. PEM → CryptoKey

import { importSPKI, importPKCS8 } from 'jose'

const publicKey = await importSPKI(pemPublic, 'RS256')
const privateKey = await importPKCS8(pemPrivate, 'RS256')

2. CryptoKey → JWK

import { exportJWK } from 'jose'

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

Полный цикл JWK → PEM (через CryptoKey)

Прямого экспорта в PEM в Jose нет, но преобразование выполняется через промежуточный слой.

1. JWK → CryptoKey

import { importJWK } from 'jose'

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

2. CryptoKey → PEM

Для PEM используется экспорт в SPKI/PKCS#8:

import { exportSPKI, exportPKCS8 } from 'jose'

const pemPublic = await exportSPKI(key)
const pemPrivate = await exportPKCS8(key)

Экспорт PEM из CryptoKey

Публичный ключ (SPKI)

import { exportSPKI } from 'jose'

const pem = await exportSPKI(publicKey)

Приватный ключ (PKCS#8)

import { exportPKCS8 } from 'jose'

const pem = await exportPKCS8(privateKey)

Особенности алгоритмов при конвертации

Jose строго связывает ключ с алгоритмом при импорте:

  • RS256 → RSA SHA-256
  • ES256 → P-256 (ECDSA)
  • EdDSA → Ed25519 / Ed448

Ошибка в указании алгоритма приводит к невозможности импорта ключа.


Работа с RSA ключами

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

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

Приватные дополнения:

  • d, p, q, dp, dq, qi

При экспорте из PEM в JWK Jose автоматически восстанавливает полную структуру, если ключ содержит приватную часть.


Работа с EC ключами

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

  • crv — кривая (P-256, P-384, P-521)
  • x, y — координаты точки
  • d — приватный компонент

Пример:

{
  kty: "EC",
  crv: "P-256",
  x: "...",
  y: "...",
  d: "..."
}

Потери информации при преобразовании

При переходе между PEM и JWK важно учитывать:

  • PEM хранит структуру ASN.1
  • JWK хранит разложенные параметры

Возможные ограничения:

  • Некорректные PKCS#1 ключи могут требовать предварительной нормализации
  • Некоторые HSM-экспортированные ключи не содержат приватных частей
  • Потеря metadata (например, комментариев PEM)

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

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

Invalid key format

Причина: ключ RSA импортируется с ES256.


Повреждённый PEM

Failed to import key

Причина: отсутствие BEGIN/END блоков или некорректный Base64.


Попытка экспортировать неподдерживаемый ключ

Некоторые CryptoKey могут быть non-extractable:

extractable: false

В этом случае exportJWK завершится ошибкой.


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

Типовой поток в системах JWT:

  1. PEM загружается из файла или переменной окружения
  2. Конвертируется в CryptoKey
  3. Используется для подписи или проверки токенов
  4. При необходимости экспортируется в JWK для JWKS endpoint

JWKS и связь с JWK

JWK часто используется внутри JWKS (JSON Web Key Set):

{
  "keys": [
    {
      "kty": "RSA",
      "n": "...",
      "e": "AQAB",
      "kid": "key-1"
    }
  ]
}

Jose позволяет легко генерировать такие структуры через exportJWK.


Особенности безопасности при конвертации

  • Приватные JWK нельзя безопасно логировать
  • PEM должен храниться в защищённом хранилище
  • При экспорте важно контролировать extractable
  • JWK часто используется в публичных endpoints, PEM — нет

Использование key ID (kid)

При преобразовании ключей в JWK рекомендуется добавлять kid:

const jwk = await exportJWK(publicKey)
jwk.kid = '2026-rotation-key-1'

Это обеспечивает корректную ротацию ключей в системах аутентификации.