Перейти к содержимому
ecdsa.com

JWK или PEM: ключи в JSON или в ASN.1

JWK — это ключ в виде JSON, PEM — ключ в виде DER в обёртке base64. В них одни и те же числа, и преобразование из одного формата в другой проходит без потерь, если учесть несколько деталей.
Что сравниваетсяJWK (JSON Web Key)PEM

Выбор между JWK и PEM зависит от того, куда отправляется ключ. JSON Web Key (RFC 7517) — язык JOSE, OAuth и WebCrypto, а OpenSSL, TLS-серверы, инструменты SSH, Java и большинство программ для PKI ожидают PEM-файлы (текстовую кодировку по RFC 7468 для структур DER, таких как SubjectPublicKeyInfo и PKCS #8). Криптографическое содержимое у них одинаковое, поэтому выбор сводится к инструментам и метаданным.

Коротко о главном

JWK (RFC 7517)PEM (RFC 7468)
КодировкаОбъект JSON с полями в base64urlbase64 от DER между -----BEGIN …----- и -----END …-----
Структура открытого ключаkty и поля, зависящие от типа ключа (crv, x, y, n, e)SubjectPublicKeyInfo (BEGIN PUBLIC KEY)
Закрытый ключТот же объект плюс d (и CRT-параметры для RSA)PKCS #8 (BEGIN PRIVATE KEY) или устаревшие формы PKCS #1 и SEC 1
Метаданныеkid, alg, use, key_ops, x5c, x5t#S256Нет (только метки)
Несколько ключейJWK Set: массив "keys"Несколько блоков PEM в одном файле
Размер открытого ключа P-256 (замер)126 символов JSON178 символов
Хеш для идентификации ключаОтпечаток JWK (RFC 7638)Отпечаток SPKI SHA-256, отпечаток сертификата
Кто используетБиблиотеки JOSE, jwk в WebCrypto, jwks_uri в OIDC, конвертеры COSEOpenSSL, nginx, Apache, X509EncodedKeySpec в Java, x509 в Go, cryptography в Python

Что внутри каждого формата

В JWK числа ключа записаны напрямую. Открытый ключ EC — это kty: EC, crv: P-256 и координаты x и y в виде строк base64url фиксированной длины; ключ RSA — kty: RSA с n и e; ключи Ed25519 и X25519 — kty: OKP с crv и x (RFC 8037); в закрытых ключах добавляется d. Необязательные поля привязывают ключ к назначению: alg (один алгоритм), use (sig или enc), kid и ссылки на сертификаты.

Открытый ключ в формате PEM — это DER-кодировка структуры SubjectPublicKeyInfo: идентификатор алгоритма (например, id-ecPublicKey с OID кривой P-256) и битовая строка, в которой записана точка. Закрытые ключи — это структуры PKCS #8 PrivateKeyInfo, при необходимости зашифрованные. Кроме метки, PEM не несёт никаких метаданных, поэтому алгоритм, с которым разрешено использовать ключ, нужно задавать в другом месте.

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

Преобразование проходит без потерь. Скрипт ниже (запускался на Node.js 26.10) преобразует открытый ключ P-256 из PEM в JWK и обратно и вычисляет оба вида идентификаторов.

Node.js: из PEM в JWK и обратно, отпечаток JWK и хеш SPKI
import { createHash, createPublicKey, generateKeyPairSync } from "node:crypto";

const { publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });

// PEM (SubjectPublicKeyInfo) -> JWK -> PEM again.
const pem = publicKey.export({ type: "spki", format: "pem" });
const jwk = publicKey.export({ format: "jwk" });
const roundTrip = createPublicKey({ key: jwk, format: "jwk" }).export({ type: "spki", format: "pem" });

// JWK thumbprint (RFC 7638): SHA-256 over the required members in lexicographic order.
const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
const thumbprint = createHash("sha256").update(canonical).digest("base64url");
// SPKI fingerprint: SHA-256 over the DER-encoded SubjectPublicKeyInfo.
const spki = createHash("sha256").update(publicKey.export({ type: "spki", format: "der" })).digest("base64url");

console.log("JWK members:", Object.keys(jwk).join(", "));
console.log("PEM length:", pem.length, "JWK JSON length:", JSON.stringify(jwk).length);
console.log("PEM -> JWK -> PEM identical:", pem === roundTrip);
console.log("JWK thumbprint equals SPKI hash:", thumbprint === spki);
Результат
JWK members: kty, x, y, crv
PEM length: 178 JWK JSON length: 126
PEM -> JWK -> PEM identical: true
JWK thumbprint equals SPKI hash: false

Последняя строка часто удивляет: отпечаток JWK — это хеш канонического JSON, а отпечаток SPKI — хеш DER, поэтому у одного и того же ключа два разных «отпечатка». Системы должны договориться, какой из них имеется в виду: например, cnf.jkt в DPoP — это отпечаток JWK, а при закреплении сертификатов используются хеши SPKI.

Подводные камни

  • Дополнение и длина. Значения в JWK кодируются в base64url без дополнения, а координаты EC должны сохранять полную длину (32 байта для P-256, 66 для P-521), включая ведущие нулевые байты. Самописные конвертеры, отбрасывающие нули, на P-256 и P-384 создают нерабочие ключи примерно в одном случае из 128, потому что каждая из координат x и y начинается с нулевого байта примерно в одном случае из 256; на P-521, где на старший байт приходится всего один бит, нерабочими оказываются примерно три ключа из четырёх. Node отклоняет такой JWK как некорректный, а WebCrypto выбрасывает DataError.
  • Закрытые ключи в открытом доступе. JWK с d — это закрытый ключ. Если опубликовать его в JWKS или вставить в какой-нибудь инструмент, ключ будет раскрыт; инструменты этого сайта не принимают JWK с d там, где нужен только открытый ключ.
  • Неверная метка PEM. BEGIN RSA PUBLIC KEY (PKCS #1) и BEGIN PUBLIC KEY (SPKI) — разные структуры; многие конвертеры JWK принимают только SPKI.
  • Потерянные метаданные. При преобразовании JWK в PEM теряются kid и alg; сохраните их в конфигурации, чтобы проверяющие стороны по-прежнему закрепляли алгоритм.
  • Закрытые ключи Ed25519. Параметр d в JWK — это 32-байтный seed, а «сырые» закрытые ключи в некоторых библиотеках занимают 64 байта (seed плюс открытый ключ).
  • Сертификаты — не ключи. JWK может указывать цепочку сертификатов в x5c, но её элементы — это сертификаты DER в обычном base64, а не в base64url, и проверяющая сторона должна проверить цепочку, прежде чем доверять ключу из сертификата. Блок PEM BEGIN CERTIFICATE точно так же содержит открытый ключ, но извлечь его (openssl x509 -pubkey -noout) и доверять ему — два разных шага.

Что когда выбирать

  • Ключи проверки подписи токенов, которые публикует издатель: JWK Set по адресу jwks_uri.
  • Ключи для библиотек JOSE или WebCrypto в браузерах: JWK (WebCrypto также принимает SPKI и PKCS #8 в DER).
  • Ключи и сертификаты TLS-серверов, CSR, работа с OpenSSL: PEM.
  • Код на Java, .NET и Go со стандартными классами ключей: PEM или DER (SPKI и PKCS #8), а JWK поддерживается через библиотеки JOSE.
  • Файлы конфигурации и менеджеры секретов: подходят оба; с PEM работает больше инструментов, а JWK хранит метаданные вместе с ключом.
  • Закрепление и идентификаторы ключей: выберите одно определение хеша (отпечаток JWK или SHA-256 от SPKI) и задокументируйте его.

Заметки о миграции

  1. Генерируйте ключи там, где они будут храниться, а затем экспортируйте открытую часть в том формате, который нужен каждому потребителю; не конвертируйте закрытые ключи из формата в формат чаще, чем необходимо.
  2. Конвертируйте с помощью библиотек: node:crypto (createPublicKey и export), cryptography с jwcrypto или joserfc в Python, jose в JavaScript, Nimbus в Java или конвертер JWK, который работает локально.
  3. Задавайте kid, alg и use явно, когда создаёте JWK для JWKS; если нужны стабильные идентификаторы, выводите kid из отпечатка JWK.
  4. Проверяйте результат конвертации: перед публикацией подпишите данные закрытым ключом и проверьте подпись преобразованным открытым ключом.
  5. Синхронизируйте оба представления во время ротации: иначе JWKS и шлюз, настроенный через PEM, могут по-разному считать, какой ключ сейчас текущий.

Источники

Все сравнения