JEP 524: PEM Encodings of Cryptographic Objects (Second Preview)
PEM-кодирование криптографических объектов (вторая версия Preview (предварительная версия))
| Ответственный | Anthony Scarpino |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 26 |
| Компонент | security-libs / java.security |
| Обсуждение | security dash dev at openjdk dot org |
| Трудоёмкость | S |
| Длительность | S |
| Связан с | JEP 470: PEM Encodings of Cryptographic Objects (Preview) |
| JEP 538: PEM Encodings of Cryptographic Objects (Third Preview) | |
| Рецензенты | Sean Mullan |
| Одобрен | Sean Mullan |
| Создан | 2025/06/25 21:32 |
| Обновлён | 2026/02/18 01:53 |
| Задача | 8360563 |
Аннотация
Добавить API для кодирования объектов, представляющих криптографические ключи, сертификаты и списки отзыва сертификатов, в широко распространённый транспортный формат Privacy-Enhanced Mail (PEM), а также для декодирования из этого формата обратно в объекты. Это API в статусе Preview.
История
PEM API был предложен как возможность в статусе Preview в JEP 470 и выпущен в JDK 25. Здесь мы предлагаем вторую версию Preview, чтобы оставить время для дополнительных отзывов и получить больше опыта работы с этой возможностью.
Изменения по сравнению с первой версией Preview:
-
Класс
PEMRecordтеперь называетсяPEMи теперь содержит методdecode(), который возвращает декодированное содержимое в Base64. -
Методы
encryptKeyклассаEncryptedPrivateKeyInfoтеперь называютсяencryptи теперь принимают объектыDEREncodableвместо объектовPrivateKey, что позволяет шифровать объектыKeyPairиPKCS8EncodedKeySpec. -
В класс
EncryptedPrivateKeyInfoдобавлены методыgetKeyPair, которые расшифровывают текст в кодировке PKCS#8, содержащийPublicKey. -
Исключения, которые выбрасывают методы
getKeyклассаEncryptedPrivateKeyInfo, теперь согласованы с исключениями соседних методовgetKeySpec. -
Классы
PEMEncoderиPEMDecoderтеперь поддерживают шифрование и расшифровку объектовKeyPairиPKCS8EncodedKeySpec.
Цели
-
Простота использования — определить лаконичный API для преобразования между текстом PEM и объектами, представляющими ключи, сертификаты и списки отзыва сертификатов.
-
Поддержка стандартов — поддержать преобразование между текстом PEM и криптографическими объектами, у которых есть стандартные представления в двоичных форматах PKCS#8 (для закрытых ключей), X.509 (открытые ключи, сертификаты и списки отзыва сертификатов) и PKCS#8 v2.0 (зашифрованные закрытые ключи и асимметричные ключи).
Мотивация
API платформы Java обладает обширной поддержкой криптографических объектов, таких как открытые ключи, закрытые ключи, сертификаты и списки отзыва сертификатов. Разработчики используют эти объекты, чтобы создавать и проверять подписи, проверять сетевые соединения, защищённые TLS, и выполнять другие криптографические операции.
Приложения часто отправляют и получают представления криптографических объектов — через пользовательские интерфейсы, по сети или при записи на устройства хранения и чтении с них. Для этого часто используется формат Privacy-Enhanced Mail (PEM), определённый в RFC 7468.
Изначально этот текстовый формат был разработан для отправки криптографических объектов по электронной почте, но со временем его стали использовать и расширять для других целей. Удостоверяющие центры выдают цепочки сертификатов в формате PEM. Криптографические библиотеки, такие как OpenSSL, предоставляют операции для генерации и преобразования криптографических объектов в кодировке PEM. Приложения, критичные к безопасности, такие как OpenSSH, хранят ключи для связи в формате PEM. Аппаратные устройства аутентификации, такие как Yubikeys, принимают и выдают криптографические объекты в кодировке PEM.
Вот пример криптографического объекта в кодировке PEM, в данном случае открытого ключа на эллиптической кривой:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/kRGOL7wCPTN4KJ2ppeSt5UYB6u
cPjjuKDtFTXbguOIFDdZ65O/8HTUqS/sVzRF+dg7H3/tkQ/36KdtuADbwQ==
-----END PUBLIC KEY-----
Текст PEM содержит двоичное представление ключа в кодировке Base64, окружённое заголовком и концевиком, которые содержат слова BEGIN и END соответственно. Остальной текст в заголовке и концевике указывает тип криптографического объекта, в данном случае PUBLIC KEY. Подробности о ключе, такие как его алгоритм и содержимое, можно получить, разобрав двоичное представление в кодировке Base64.
В платформе Java нет простого в использовании API для декодирования и кодирования текста в формате PEM. Эту проблему подтвердил опрос Java Cryptographic Extensions Survey в апреле 2022 года. Хотя каждый криптографический объект предоставляет метод, возвращающий его двоичное представление, а для преобразования его в текст можно использовать API Base64, остальная работа ложится на разработчиков:
-
Кодирование открытого ключа выполняется просто, хотя и утомительно.
-
Декодирование ключа в кодировке PEM требует аккуратного разбора исходного текста PEM, определения фабрики, с помощью которой создаётся объект ключа, и определения алгоритма ключа.
-
Шифрование и расшифровка закрытого ключа требуют более десятка строк кода.
Безусловно, мы можем сделать лучше.
Описание
Мы добавляем новый интерфейс и три новых класса в пакет java.security:
-
Интерфейс
DEREncodableреализуют классы API платформы Java, которые представляют криптографические объекты с ключевым материалом или материалом сертификата, допускающим двоичное кодирование. -
Классы
PEMEncoderиPEMDecoderпредназначены для кодирования в формат PEM и декодирования из него. Экземпляры этих классов неизменяемы и допускают повторное использование, т. е. они не сохраняют информацию о ранее закодированном или декодированном криптографическом объекте. -
Класс
PEM, реализующийDEREncodable, предназначен для кодирования и декодирования текста PEM, представляющего криптографические объекты, для которых в платформе Java нет API.
Это API в статусе Preview, по умолчанию отключённый
Чтобы использовать этот API в JDK 26, нужно включить Preview-API:
-
Скомпилируйте программу с
javac --release 26 --enable-preview Main.javaи запускайте её сjava --enable-preview Main; или -
При использовании средства запуска исходного кода запускайте программу с
java --enable-preview Main.java; или -
При использовании jshell запускайте его с
jshell --enable-preview.
Криптографические объекты, допускающие кодирование в DER
PEM — текстовый формат для двоичных данных. Чтобы закодировать криптографический объект в текст PEM или декодировать текст PEM в криптографический объект, нужен способ преобразовывать такие объекты в двоичные данные и обратно. К счастью, API Java для криптографических ключей, сертификатов и списков отзыва сертификатов позволяют преобразовывать свои экземпляры в массивы байтов в формате Distinguished Encoding Rules (DER) и обратно. К сожалению, эти API не связаны иерархически, и способ, которым они предоставляют эти преобразования, неодинаков.
Поэтому мы вводим новый интерфейс DEREncodable, который обозначает криптографические API, предоставляющие такие преобразования, чьи экземпляры, следовательно, можно кодировать в формат PEM и декодировать из него. Этот пустой интерфейс объявлен как sealed; его разрешённые классы и интерфейсы — AsymmetricKey, X509Certificate, X509CRL, KeyPair, EncryptedPrivateKeyInfo, PKCS8EncodedKeySpec, X509EncodedKeySpec и PEM:
public sealed interface DEREncodable
permits AsymmetricKey, KeyPair,
PKCS8EncodedKeySpec, X509EncodedKeySpec,
EncryptedPrivateKeyInfo, X509Certificate, X509CRL, PEM
{ }
Мы вносим соответствующие изменения в некоторые из разрешённых классов и интерфейсов:
public non-sealed interface AsymmetricKey { ... }
public non-sealed class PKCS8EncodedKeySpec { ... }
public non-sealed class X509EncodedKeySpec { ... }
public non-sealed class EncryptedPrivateKeyInfo { ... }
public non-sealed abstract class X509Certificate { ... }
public non-sealed abstract class X509CRL { ... }
Кодирование
Класс PEMEncoder объявляет методы для кодирования объектов DEREncodable в текст PEM:
public final class PEMEncoder {
public static PEMEncoder of();
public byte[] encode(DEREncodable so);
public String encodeToString(DEREncodable so);
public PEMEncoder withEncryption(char[] password);
}
Чтобы закодировать объект DEREncodable, сначала получите экземпляр PEMEncoder, вызвав of(). Возвращённый экземпляр потокобезопасен и допускает повторное использование, поэтому его методы кодирования можно вызывать многократно.
Есть два метода кодирования. Один метод возвращает текст PEM в виде массива байтов, содержащего символы в кодировке ISO-8859-1; например, чтобы закодировать закрытый ключ:
PEMEncoder pe = PEMEncoder.of();
byte[] pem = pe.encode(privateKey);
Другой метод кодирования возвращает текст PEM в виде строки; например, чтобы закодировать пару открытого и закрытого ключей в строку:
String pem = pe.encodeToString(new KeyPair(publicKey, privateKey));
Если вы кодируете PrivateKey, то можете зашифровать его с помощью метода withEncryption, который принимает пароль и возвращает новый неизменяемый экземпляр PEMEncoder, настроенный на шифрование ключа этим паролем:
String pem = pe.withEncryption(password).encodeToString(privateKey);
PEMEncoder, настроенный таким образом, может кодировать объекты PrivateKey, KeyPair и PKCS8EncodedKeySpec. Он использует алгоритм шифрования по умолчанию; чтобы задать параметры шифрования, отличные от параметров по умолчанию, или шифровать с помощью другого провайдера, используйте объект EncryptedPrivateKeyInfo (см. ниже).
Декодирование
Класс PEMDecoder объявляет методы для декодирования текста PEM в объекты DEREncodable:
public final class PEMDecoder {
public static PEMDecoder of();
public DEREncodable decode(String str);
public DEREncodable decode(InputStream is) throws IOException;
public <S extends DEREncodable> S decode(String string, Class<S> cl);
public <S extends DEREncodable> S decode(InputStream is, Class<S> cl)
throws IOException;
public PEMDecoder withDecryption(char[] password);
public PEMDecoder withFactory(Provider provider);
}
Чтобы декодировать текст PEM, сначала получите экземпляр PEMDecoder, вызвав of(). Возвращённый экземпляр потокобезопасен и допускает повторное использование, поэтому его методы декодирования можно вызывать многократно.
Есть четыре метода декодирования; каждый из них возвращает объект DEREncodable. Чтобы определить тип возвращённого криптографического объекта, можно использовать Pattern Matching (сопоставление с образцом) с оператором instanceof или оператором switch. Например, чтобы декодировать текст PEM, который, как вы ожидаете, кодирует либо открытый, либо закрытый ключ:
PEMDecoder pd = PEMDecoder.of();
switch (pd.decode(pem)) {
case PublicKey publicKey -> ...;
case PrivateKey privateKey -> ...;
default -> throw new IllegalArgumentException(...);
}
Если тип закодированного криптографического объекта известен заранее, можно передать соответствующий класс одному из методов decode, принимающих аргумент Class, и тогда не придётся применять Pattern Matching к типу результата метода или проверять этот тип и затем выполнять приведение к нему. Например, если известно, что тип — ECPublicKey:
ECPublicKey key = pd.decode(pem, ECPublicKey.class);
Если в этих методах указан неверный класс, выбрасывается ClassCastException.
Если входной текст PEM кодирует зашифрованный закрытый ключ, его можно расшифровать с помощью метода withDecryption, который принимает пароль и возвращает новый экземпляр PEMDecoder, настроенный на расшифровку ключа в объект PrivateKey. Настроенный таким образом PEMDecoder по-прежнему может декодировать незашифрованные объекты. Например, чтобы расшифровать ECPrivateKey:
ECPrivateKey eckey = pd.withDecryption(password)
.decode(pem, ECPrivateKey.class);
Если вы декодируете текст PEM, кодирующий закрытый ключ, но не указываете пароль, методы decode возвращают экземпляр EncryptedPrivateKeyInfo, с помощью которого можно расшифровать ключ и получить объект PrivateKey (см. ниже).
В некоторых ситуациях при декодировании текста PEM может понадобиться определённый криптографический провайдер. Метод withFactory возвращает новый экземпляр PEMDecoder, который использует указанного провайдера для создания криптографических объектов. Например, чтобы декодировать Certificate с помощью определённого провайдера:
PEMDecoder d = pd.withFactory(providerFactory);
Certificate c = d.decode(pem, X509Certificate.class);
Если провайдер не может создать криптографический объект нужного типа, выбрасывается IllegalArgumentException.
При декодировании текста PEM в криптографический объект любые данные, предшествующие заголовку PEM во входной строке или потоке байтов, игнорируются. Если эти данные нужны, их можно получить, декодировав текст в объект PEM.
Если входные данные PEM не удаётся разобрать, выбрасывается IllegalArgumentException. Предполагается, что байты, прочитанные из входных потоков, представляют символы в кодировке ISO-8859-1.
Класс PEM
Класс PEM реализует DEREncodable. Его экземпляры могут хранить данные PEM любого типа. Поэтому с его помощью можно кодировать и декодировать тексты PEM, представляющие криптографические объекты, для которых в платформе Java нет API, например запросы на сертификацию PKCS#10.
public record PEM(String type, String content, byte[] leadingData)
implements DEREncodable
{
public PEM(String type, String content);
public PEM(String type, String content, byte[] leadingData);
String type(); // Cryptographic object type, from the header text
// (e.g., "PRIVATE KEY")
String content(); // Base64-encoded PEM content
byte[] leadingData(); // Any content preceding the PEM header
byte[] decode(); // Decode Base64 content
}
Экземпляр PEMDecoder декодирует текст PEM в объект PEM, если для типа PEM этого текста в платформе Java нет API:
DEREncodable d = PEMDecoder.of().decode(pem);
if (d instanceof PEM pr) {
throw new IllegalArgumentException("Unhandled PEM type: " + pr.type()
+ "; data: " + pr.content());
}
PEM pr = PEMDecoder.of().decode(pem, PEM.class);
Экземпляр PEMEncoder кодирует объект PEM в текст PEM, не проверяя его содержимое.
Класс EncryptedPrivateKeyInfo
Существующий класс EncryptedPrivateKeyInfo представляет зашифрованный закрытый ключ. Чтобы его было проще использовать с классами PEMEncoder и PEMDecoder, мы добавили в него семь методов:
EncryptedPrivateKeyInfo {
...
public static EncryptedPrivateKeyInfo
encrypt(DEREncodable key, char[] password);
public static EncryptedPrivateKeyInfo
encrypt(DEREncodable key, char[] password,
String algorithm, AlgorithmParameterSpec params,
Provider provider);
public static EncryptedPrivateKeyInfo
encrypt(DEREncodable key, Key encKey,
String algorithm, AlgorithmParameterSpec params,
Provider provider, SecureRandom random);
public PrivateKey getKey(char[] password)
throws NoSuchAlgorithmException, InvalidKeyException;
public PrivateKey getKey(char[] password, Provider provider)
throws NoSuchAlgorithmException, InvalidKeyException;
public KeyPair getKeyPair(char password)
throws NoSuchAlgorithmException, InvalidKeyException;
public KeyPair getKeyPair(char[] password, Provider provider)
throws NoSuchAlgorithmException, InvalidKeyException;
}
Три новых статических метода encrypt шифруют заданный DEREncodable заданным паролем. DEREncodable должен быть PrivateKey, KeyPair или PKCS8EncodedKeySpec. Для более сложных случаев второй и третий методы encrypt позволяют указать дополнительные криптографические параметры, если параметров по умолчанию недостаточно. Возвращённый экземпляр EncryptedPrivateKeyInfo затем можно передать в PEMEncoder, чтобы закодировать его в текст PEM:
var epki = EncryptedPrivateKeyInfo.encryptKey(privateKey, password);
byte[] pem = PEMEncoder.of().encode(epki);
Новые методы getKey расшифровывают закрытый ключ в экземпляре EncryptedPrivateKeyInfo. Эти методы принимают пароль и, при необходимости, криптографический провайдер и возвращают PrivateKey. Их можно использовать, когда PEMDecoder возвращает EncryptedPrivateKeyInfo:
EncryptedPrivateKeyInfo epki = PEMDecoder.of().decode(pem);
PrivateKey key = epki.getKey(password);
Новые методы getKeyPair расшифровывают экземпляр EncryptedPrivateKeyInfo в KeyPair, если кодировка содержит и открытый, и закрытый ключ. Если открытого ключа нет, выбрасывается IllegalArgumentException.
Алгоритм шифрования на основе пароля (PBE), который используется по умолчанию при шифровании DEREncodable с помощью PEMEncoder или EncryptedPrivateKeyInfo, задан в файле свойств безопасности по умолчанию. Свойство безопасности jdk.epkcs8.defaultAlgorithm задаёт алгоритм по умолчанию «PBEWithHmacSHA256AndAES_128». В будущем алгоритм по умолчанию может измениться, но это не затронет текст PEM, созданный сегодня, поскольку закодированные в этом тексте данные содержат название алгоритма и все остальные параметры, необходимые для расшифровки.
Альтернативы
PEM API — это мост между Base64 и криптографическими объектами. Мы отклонили многие другие возможные варианты дизайна, потому что они плохо сочетались с существующими криптографическими API. Хотя некоторые из альтернатив могли бы подойти, мы выбрали предлагаемый API за его сходство с API HexFormat и вложенными классами Encoder и Decoder API Base64. Мы хотели получить неизменяемость, потокобезопасность и раздельные пути в API для кодирования и декодирования.
Среди рассмотренных нами альтернатив:
-
Расширить API
EncodedKeySpec— этот API инкапсулирует двоично-закодированные данные ключа для экземпляровKeyFactoryи других криптографических классов. Новый подклассPEMEncodedKeySpecмог бы обозначать типом инкапсулированный текст PEM и при этом предоставлять операции кодирования и декодирования между текстом PEM и соответствующим закрытым или открытым ключомEncodedKeySpec.У этого дизайна было несколько недостатков. Во-первых, класс
PEMEncodedKeySpecиспользовался бы для преобразования, а это не является назначением его суперклассаEncodedKeySpec. Во-вторых,EncodedKeySpecориентирован на ключи и поэтому не может поддерживать кодирование сертификатов или списков отзыва сертификатов в текст PEM. Наконец, новый подклассEncodedKeySpecсоздал бы риски совместимости и проблемы удобства использования с существующими сторонними криптографическими провайдерами. -
Расширить API
CertificateFactoryиKeyFactory— APICertificateFactoryуже поддерживает декодирование данных сертификатов и списков отзыва сертификатов в формате PEM, поэтому добавление методов кодирования вCertificateFactoryиKeyFactoryсоответствовало бы существующему дизайну.Благодаря
CertificateFactoryэтот подход выглядит простым, поскольку для сертификатов существует одна отраслевая стандартная кодировка.KeyFactory, напротив, пришлось бы поддерживать разные форматы кодирования. Хуже того, провайдеры экземпляровKeyFactoryне обязаны поддерживать все известные типы асимметричных ключей. Кроме того, сопровождающие провайдеров могут не захотеть отвечать за кодирование PEM, а также за обработку зашифрованных закрытых ключей. Поэтому расширениеKeyFactory— трудное решение. -
Статические методы — статические методы хороши для неизменяемости и потокобезопасности, но зашифрованные закрытые ключи создают проблему удобства использования. Для преобразования зашифрованных закрытых ключей нужен пароль, а для преобразования других типов криптографических объектов — нет. Поэтому со статическими методами для зашифрованных закрытых ключей нам понадобились бы неудобные решения, например дополнительные перегруженные методы, принимающие параметры шифрования, или обязательное использование экземпляров
EncryptedPrivateKeyInfo. Если кодировщики и декодировщики хранят пароль шифрования, пользоваться API удобнее. -
API промежуточного объекта PEM — мы могли бы ввести класс-обёртку, экземпляры которого содержали бы ключ, сертификат, список отзыва сертификатов или какой-либо текст PEM. Этот класс мог бы либо объявлять методы кодирования и декодирования, либо операции над его экземплярами выполнял бы отдельный API.
Хотя такой подход дал бы независимое представление текста PEM, чрезмерная гибкость — это минус. Класс, экземпляры которого могут оборачивать и криптографические объекты, и текст PEM, мог бы сбивать с толку, поскольку это принципиально разные виды сущностей. Раздельные пути кодирования и декодирования от заданных данных лучше направляют пользователя.
-
API из одного класса — один класс PEM мог бы выполнять и кодирование, и декодирование, но, как и в подходах со статическими методами и промежуточным объектом PEM, у него не было бы раздельных путей операций для кодирования и декодирования. Если разделить кодирование и декодирование по отдельным классам, API проще использовать, поскольку каждый класс предоставляет только нужные операции.
-
Добавить поддержку кодировок в сервисы криптографических провайдеров — мы рассматривали возможность дать криптографическим провайдерам поддержку сервисов для преобразования между текстовым и двоичным представлениями криптографических объектов. Двоичные форматы уже используются провайдерами внутри для импорта и экспорта криптографических объектов, и добавление сервисов преобразования могло бы быть полезно не только для PEM.
Однако такой подход потребовал бы большой инфраструктуры, а дополнительной пользы дал бы мало. Кроме того, он создал бы риски совместимости для существующих провайдеров и усложнил бы использование API.
-
Ввести универсальный API криптографического кодирования и декодирования — мы рассматривали обобщённый API, который можно было бы использовать со многими текстовыми форматами. Мы отказались от него, потому что возможности у этих форматов разные: они по-разному поддерживают ключи, цепочки сертификатов, сжатие и другие параметры. Множество форматов в одном API, где часть методов относится к конкретному формату, сбивали бы с толку.
Тестирование
Тесты будут включать:
- проверку того, что все поддерживаемые классы
DEREncodableмогут кодировать и декодировать текст PEM; - проверку того, что криптографические объекты RSA, EC, ML-KEM и EdDSA можно кодировать и декодировать;
- чтение текста PEM, созданного сторонними приложениями, и наоборот; и
- негативное тестирование с некорректным текстом PEM.