openjdk.ruOpenJDK на русском

JEP 542: PEM Encodings of Cryptographic Objects

PEM-кодирование криптографических объектов

ОтветственныйAnthony Scarpino
ТипFeature
ОбластьSE
СтатусCompleted
Выпуск28
Компонентsecurity-libs / java.security
Обсуждениеsecurity dash dev at openjdk dot org
ТрудоёмкостьS
ДлительностьS
Связан сJEP 538: PEM Encodings of Cryptographic Objects (Third Preview)
РецензентыSean Mullan
ОдобренSean Mullan
Создан2026/06/11 18:21
Обновлён2026/09/09 17:12
Задача8386511

Аннотация

Добавить API для кодирования объектов, которые представляют криптографические ключи, сертификаты и списки отзыва сертификатов, в широко используемый транспортный формат Privacy-Enhanced Mail (PEM), а также для декодирования из этого формата обратно в объекты.

История

PEM API впервые появился в статусе Preview (предварительная версия) в JEP 470 (JDK 25), вторая версия Preview с небольшими изменениями вышла в JEP 524 (JDK 26), а третья версия Preview с небольшими изменениями — в JEP 538 (JDK 27). Здесь мы предлагаем окончательно утвердить API без дальнейших изменений.

Цели

  • Простота использования — определить лаконичный 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. Аппаратные устройства аутентификации, такие как YubiKey, принимают и выдают криптографические объекты в кодировке 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:

  • Интерфейс BinaryEncodable реализуют классы API платформы Java, представляющие криптографические объекты с ключевым или сертификатным материалом, который можно закодировать в двоичный вид.

  • Классы PEMEncoder и PEMDecoder предназначены для кодирования в формат PEM и декодирования из него. Экземпляры этих классов неизменяемы и допускают повторное использование, то есть не сохраняют информацию о ранее закодированных или декодированных криптографических объектах.

  • Класс PEM, реализующий BinaryEncodable, предназначен для кодирования и декодирования текста PEM, представляющего криптографические объекты, для которых в платформе Java нет API.

Криптографические объекты, кодируемые в двоичный вид

PEM — текстовый формат для двоичных данных. Чтобы закодировать криптографический объект в текст PEM или декодировать текст PEM в криптографический объект, нужен способ преобразовывать такие объекты в двоичные данные и обратно. К счастью, все Java API для криптографических ключей, сертификатов и списков отзыва сертификатов предоставляют средства для преобразования своих экземпляров в стандартизованные двоичные кодировки и обратно. К сожалению, эти API не связаны иерархически, и способ, которым они предоставляют эти преобразования, неединообразен.

Поэтому мы добавляем новый интерфейс BinaryEncodable, с помощью которого классы PEMEncoder и PEMDecoder могут единообразно обрабатывать криптографические объекты, имеющие двоичное представление. Этот пустой интерфейс является sealed-интерфейсом; его видимые разрешённые классы и интерфейсы — AsymmetricKey, X509Certificate, X509CRL, KeyPair, EncryptedPrivateKeyInfo, PKCS8EncodedKeySpec, X509EncodedKeySpec и PEM:

public sealed interface BinaryEncodable
    permits AsymmetricKey, KeyPair,
            PKCS8EncodedKeySpec, X509EncodedKeySpec,
            EncryptedPrivateKeyInfo, X509Certificate, X509CRL, PEM
{ }

У интерфейса BinaryEncodable есть ещё один разрешённый класс, который не является публичным и поэтому не показан в спецификации. Из-за существования этого непубличного разрешённого класса код приложения, который использует Pattern Matching (сопоставление с образцом) в выражении или операторе switch по значению BinaryEncodable, должен содержать либо ветку default, либо ветку case BinaryEncodable. Так код приложения не завершится неожиданным MatchException, если в будущем выпуске мы добавим в интерфейс BinaryEncodable новые разрешённые классы или интерфейсы.

Мы вносим соответствующие изменения в некоторые из разрешённых классов и интерфейсов:

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 объявляет методы для кодирования объектов BinaryEncodable в текст PEM:

public final class PEMEncoder {

    public static PEMEncoder of();

    public byte[] encode(BinaryEncodable be);
    public String encodeToString(BinaryEncodable be);

    public PEMEncoder withEncryption(char[] password);

}

Чтобы закодировать объект BinaryEncodable, сначала получите экземпляр 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. Он использует алгоритм шифрования по умолчанию и выбрасывает CryptoException, если происходит ошибка шифрования. Чтобы использовать параметры шифрования, отличные от параметров по умолчанию, или шифровать с помощью другого провайдера шифрования, используйте объект EncryptedPrivateKeyInfo (см. ниже).

Декодирование

Класс PEMDecoder объявляет методы для декодирования текста PEM в объекты BinaryEncodable:

public final class PEMDecoder {

     public static PEMDecoder of();

     public BinaryEncodable decode(String str);
     public BinaryEncodable decode(InputStream is) throws IOException;
     public <S extends BinaryEncodable> S decode(String string, Class<S> cl);
     public <S extends BinaryEncodable> S decode(InputStream is, Class<S> cl)
         throws IOException;

     public PEMDecoder withDecryption(char[] password);
     public PEMDecoder withFactoriesOf(Provider provider);

 }

Чтобы декодировать текст PEM, сначала получите экземпляр PEMDecoder, вызвав of(). Возвращённый экземпляр потокобезопасен и допускает повторное использование, поэтому его методы декодирования можно вызывать многократно.

Есть четыре метода декодирования; каждый из них возвращает объект BinaryEncodable. Чтобы определить тип возвращённого криптографического объекта, можно использовать 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 с помощью фабрики ключей или сертификатов от конкретного криптографического провайдера. Метод withFactoriesOf возвращает новый PEMDecoder, настроенный на использование фабрик указанного провайдера для создания криптографических объектов. Например, чтобы декодировать Certificate с помощью фабрики сертификатов определённого провайдера:

PEMDecoder d = pd.withFactoriesOf(provider);
Certificate c = d.decode(pem, X509Certificate.class);

Если провайдер не может создать криптографический объект нужного типа, выбрасывается IllegalArgumentException.

При декодировании текста PEM в криптографический объект любые данные, предшествующие заголовку PEM во входной строке или потоке байтов, игнорируются. Если эти данные нужны, их можно получить, декодировав текст в объект PEM.

Если входные данные PEM невозможно разобрать, выбрасывается IllegalArgumentException. Если происходит ошибка расшифровки, выбрасывается CryptoException. Байты, прочитанные из входных потоков, интерпретируются в кодировке ISO-8859-1.

Класс PEM

Класс PEM реализует BinaryEncodable. Его экземпляры могут хранить данные PEM любого типа. Поэтому с его помощью можно кодировать и декодировать тексты PEM, представляющие криптографические объекты, для которых в платформе Java нет API, например запросы на сертификацию PKCS#10.

public class PEM implements BinaryEncodable
{
    public PEM(String type, String base64Content);
    public PEM(String type, String base64Content, byte[] leadingData);
    public PEM(String type, byte[] base64Content);
    public PEM(String type, byte[] base64Content, byte[] leadingData);
    String type();           // Cryptographic object type, from the header text
                             // (e.g., "PRIVATE KEY")
    byte[] content();        // Base64-encoded PEM content
    byte[] leadingData();    // Any content preceding the PEM header
    byte[] decode();         // Decode Base64 content
}

Экземпляр PEMDecoder декодирует текст PEM в объект PEM, если для PEM-типа этого текста в платформе Java нет API:

BinaryEncodable d = PEMDecoder.of().decode(pem);
if (d instanceof PEM pr) {
    throw new IllegalArgumentException("Unhandled PEM type: " + pr.type()
                                       + "; data: " + pr.content());
}

Если вам нужен доступ к данным, предшествующим тексту PEM, или вы хотите обрабатывать содержимое текста самостоятельно, можно явно запросить PEM при декодировании:

PEM pr = PEMDecoder.of().decode(pem, PEM.class);

Экземпляр PEMEncoder кодирует объект PEM в текст PEM, не проверяя его содержимое.

Класс EncryptedPrivateKeyInfo

Существующий класс EncryptedPrivateKeyInfo представляет зашифрованный закрытый ключ. Чтобы его было проще использовать с классами PEMEncoder и PEMDecoder, мы добавили в него семь методов.

EncryptedPrivateKeyInfo {

     ...
     // encrypt methods
     public static EncryptedPrivateKeyInfo
         encrypt(BinaryEncodable key, char[] password);
     public static EncryptedPrivateKeyInfo
         encrypt(BinaryEncodable key, char[] password,
                    String algorithm, AlgorithmParameterSpec params,
                    Provider provider);
     public static EncryptedPrivateKeyInfo 
         encrypt(BinaryEncodable key, Key encKey,
                    String algorithm, AlgorithmParameterSpec params,
                    Provider provider, SecureRandom random);

     // getKey methods
     public PrivateKey getKey(char[] password) 
         throws NoSuchAlgorithmException, InvalidKeyException;
     public PrivateKey getKey(Key decryptKey)
         throws NoSuchAlgorithmException, InvalidKeyException;

     // getKeyPair methods
     public KeyPair getKeyPair(char[] password) 
         throws NoSuchAlgorithmException, InvalidKeyException;
     public KeyPair getKeyPair(Key decryptKey)
         throws NoSuchAlgorithmException, InvalidKeyException;

 }

Три новых статических метода encrypt шифруют заданный BinaryEncodable заданным паролем. BinaryEncodable должен быть PrivateKey, KeyPair или PKCS8EncodedKeySpec. Для более сложных случаев второй и третий методы encrypt позволяют указать дополнительные криптографические параметры, если параметров по умолчанию недостаточно. Возвращённый экземпляр EncryptedPrivateKeyInfo затем можно передать в PEMEncoder, чтобы закодировать его в текст PEM:

var epki = EncryptedPrivateKeyInfo.encrypt(privateKey, password);
byte[] pem = PEMEncoder.of().encode(epki);

Новые методы getKey расшифровывают закрытый ключ в экземпляре EncryptedPrivateKeyInfo, принимая либо пароль, либо Key, и возвращают PrivateKey:

EncryptedPrivateKeyInfo epki = PEMDecoder.of().decode(pem, EncryptedPrivateKeyInfo.class);
PrivateKey key = epki.getKey(password);

Новые методы getKeyPair расшифровывают экземпляр EncryptedPrivateKeyInfo в KeyPair, если кодировка содержит и открытый, и закрытый ключ. Если открытого ключа нет, выбрасывается IllegalArgumentException.

Алгоритм шифрования на основе пароля (PBE) по умолчанию, который используется при шифровании BinaryEncodable с помощью PEMEncoder или EncryptedPrivateKeyInfo, задан в файле свойств безопасности по умолчанию. Свойство безопасности jdk.epkcs8.defaultAlgorithm задаёт алгоритм по умолчанию: «PBEWithHmacSHA256AndAES_128». В будущем алгоритм по умолчанию может измениться, но это не затронет текст PEM, созданный до этого момента, поскольку закодированные в тексте данные содержат имя алгоритма и все остальные параметры, необходимые для расшифровки.

Класс CryptoException

Новый класс CryptoException представляет общую криптографическую ошибку. Он предназначен для неустранимых сбоев, связанных с GeneralSecurityException, в тех контекстах, где проверяемые исключения нежелательны.

Альтернативы

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 — API CertificateFactory уже поддерживает декодирование данных сертификатов и списков отзыва сертификатов в формате 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, где часть методов относится к конкретному формату, сбивали бы с толку.

Тестирование

Тесты будут включать:

  • проверку того, что все поддерживаемые классы BinaryEncodable могут кодировать и декодировать текст PEM;
  • проверку того, что криптографические объекты RSA, EC, ML-KEM и EdDSA можно кодировать и декодировать;
  • чтение текста PEM, созданного сторонними приложениями, и наоборот; и
  • негативное тестирование с некорректным текстом PEM.