Выбор между 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 с полями в base64url | base64 от 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 символов JSON | 178 символов |
| Хеш для идентификации ключа | Отпечаток JWK (RFC 7638) | Отпечаток SPKI SHA-256, отпечаток сертификата |
| Кто использует | Библиотеки JOSE, jwk в WebCrypto, jwks_uri в OIDC, конвертеры COSE | OpenSSL, 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 и обратно и вычисляет оба вида идентификаторов.
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, и проверяющая сторона должна проверить цепочку, прежде чем доверять ключу из сертификата. Блок PEMBEGIN 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) и задокументируйте его.
Заметки о миграции
- Генерируйте ключи там, где они будут храниться, а затем экспортируйте открытую часть в том формате, который нужен каждому потребителю; не конвертируйте закрытые ключи из формата в формат чаще, чем необходимо.
- Конвертируйте с помощью библиотек:
node:crypto(createPublicKeyиexport),cryptographyсjwcryptoилиjoserfcв Python,joseв JavaScript, Nimbus в Java или конвертер JWK, который работает локально. - Задавайте
kid,algиuseявно, когда создаёте JWK для JWKS; если нужны стабильные идентификаторы, выводитеkidиз отпечатка JWK. - Проверяйте результат конвертации: перед публикацией подпишите данные закрытым ключом и проверьте подпись преобразованным открытым ключом.
- Синхронизируйте оба представления во время ротации: иначе JWKS и шлюз, настроенный через PEM, могут по-разному считать, какой ключ сейчас текущий.