JEP 470: PEM Encodings of Cryptographic Objects (Preview)

OwnerAnthony Scarpino
TypeFeature
ScopeSE
StatusCandidate
Componentsecurity-libs / java.security
Discussionsecurity dash dev at openjdk dot org
EffortM
DurationM
Reviewed byAlan Bateman, Sean Mullan
Created2023/01/23 18:28
Updated2025/04/28 20:26
Issue8300911

Summary

Introduce an API for encoding objects that represent cryptographic keys, certificates, and certificate revocation lists into the widely-used Privacy-Enhanced Mail (PEM) transport format, and for decoding from that format back into objects. This is a preview API.

Goals

Non-Goals

Motivation

The Java Platform API has rich support for cryptographic objects such as public keys, private keys, certificates, and certificate revocation lists. Developers use these objects to sign and verify signatures, verify network connections secured by TLS, and perform other cryptographic operations.

Applications often send and receive representations of cryptographic objects, whether via user interfaces, over the network, or to and from storage devices. The Privacy-Enhanced Mail (PEM) format, defined by RFC 7468, is often used for this purpose.

This textual format was originally designed for sending cryptographic objects via e-mail, but over time it has been used and extended for other purposes. Certificate authorities issue certificate chains in the PEM format. Cryptographic libraries such as OpenSSL provide operations for generating and converting PEM-encoded cryptographic objects. Security-sensitive applications such as OpenSSH store communication keys in the PEM format. Hardware authentication devices such as Yubikeys ingest and dispense PEM-encoded cryptographic objects.

Here is an example of a PEM-encoded cryptographic object, in this case an elliptic curve public key:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/kRGOL7wCPTN4KJ2ppeSt5UYB6u
cPjjuKDtFTXbguOIFDdZ65O/8HTUqS/sVzRF+dg7H3/tkQ/36KdtuADbwQ==
-----END PUBLIC KEY-----

A PEM text contains a Base64-encoded representation of the key's binary representation surrounded by a header and footer containing the words BEGIN and END, respectively. The remaining text in the header and the footer identifies the type of the cryptographic object, in this case a PUBLIC KEY. Details of the key, such as its algorithm and content, can be obtained by parsing the Base64-encoded binary representation.

The Java Platform does not include an easy-to-use API for decoding and encoding text in the PEM format. This pain point was validated by the Java Cryptographic Extensions Survey in April 2022. While each cryptographic object provides a method to return its binary-encoded representation, and the Base64 API can be used to convert it to text, the rest of the work is left to developers:

Surely, we can do better.

Description

We introduce a new interface and two new classes in the java.security package:

This is a preview API, disabled by default

To use this API in JDK 25, you must enable preview APIs:

DER-encodable cryptographic objects

PEM is a textual format for binary data. To encode a cryptographic object into PEM text, or to decode PEM text into a cryptographic object, we need a way to convert such objects to and from binary data. Fortunately, the Java APIs for cryptographic keys, certificates, and certificate revocation lists all provide the means to convert their instances to and from byte arrays in the Distinguished Encoding Rules (DER) format. Unfortunately, these APIs are not hierarchically related, and the manner in which they expose these conversions is not uniform.

We thus introduce a new interface, [DEREncodable], to identify the cryptographic APIs that provide such conversions and whose instances can therefore be encoded to, and decoded from, the PEM format. This empty interface is sealed; its permitted classes and interfaces are AsymmetricKey, X509Certificate, X509CRL, KeyPair, EncryptedPrivateKeyInfo, PKCS8EncodedKeySpec, and X509EncodedKeySpec:

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

We make corresponding adjustments to some of the permitted classes and interfaces:

public non-sealed interface AsymmetricKey { ... }
public final class PKCS8EncodedKeySpec { ... }
public final class X509EncodedKeySpec { ... }
public final class EncryptedPrivateKeyInfo { ... }
public non-sealed abstract class X509Certificate { ... }
public non-sealed abstract class X509CRL { ... }

Encoding

The PEMEncoder class declares methods for encoding DEREncodable objects into PEM text:

public final class PEMEncoder {

    public static PEMEncoder of();

    public byte[] encode(DEREncodable so);
    public String encodeToString(DEREncodable so);

    public PEMEncoder withEncryption(char[] password);

}

To encode a DEREncodable object, first obtain a PEMEncoder instance by calling of(). The returned instance is thread-safe and reusable, so its encode methods can be used repeatedly.

There are two methods for encoding. One method returns PEM text in a byte array containing characters encoded in the ISO-8859-1 charset; for example, to encode a private key:

PEMencoder pe = PEMEncoder.of();
byte[] pem = pe.encode(privateKey);

The other encoding method returns PEM text as a string; for example, to encode a public/private key pair into a string:

String pem = pe.encodeToString(new KeyPair(publicKey, privateKey));

If you are encoding a PrivateKey then you can encrypt it via the withEncryption method, which takes a password and returns a new immutable PEMEncoder instance configured to encrypt the key with that password:

byte[] pem = pe.withEncryption(password).encodeToString(privateKey);

A PEMEncoder configured in this way can only encode PrivateKey objects. It uses a default encryption algorithm; to use non-default encryption parameters, or to encrypt with a different encryption provider, use an EncryptedPrivateKeyInfo object (see below).

Decoding

The PEMDecoder class declares methods for decoding PEM text to DEREncodable objects:

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);

 }

To decode PEM text, first obtain a PEMDecoder instance by calling of(). The returned instance is thread-safe and reusable, so its decode methods can be used repeatedly.

There are four methods for decoding; they each return a DEREncodable object. You can use pattern matching with the instanceof operator or a switch statement to identify the type of cryptographic object returned. For example, to decode PEM text that you expect to encode either a public key or a private key:

PEMDecoder pd = PEMDecoder.of();
switch (pd.decode(pem)) {
    case PublicKey publicKey -> ...;
    case PrivateKey privateKey -> ...;
    default -> throw new IllegalArgumentException(...);
}

If you know the type of the encoded cryptographic object in advance then you can pass the corresponding class to one of the decode methods that takes a Class argument, avoiding the need to pattern-match on, or else check and then cast to, the type of the method's result. For example, if you know that the type is ECPublicKey:

ECPublicKey key = pd.decode(pem, ECPublicKey.class);

In this case, if the class is incorrect then a ClassCastExeption is thrown.

If the input PEM text encodes a private key then you can decrypt it via the withEncryption method, which takes a password and returns a new PEMDecoder instance configured to decrypt the key into a PrivateKey object. A PEMDecoder configured in this way can still decode unencrypted objects. For example, to decrypt an ECPrivateKey:

ECPrivateKey eckey = pd.withDecryption(password)
                       .decode(pem, ECPrivateKey.class);

If you decode PEM text that encodes a private key, but do not provide a password, then the decode methods return an EncryptedPrivateKeyInfo instance which can be used to decrypt and produce a PrivateKey object (see below).

In some situations, you may need to use a specific cryptographic provider when decoding PEM text. The withFactory method returns a new PEMDecoder instance that uses the specified provider to produce cryptographic objects. For example, to decode a Certificate with a specific provider:

PEMDecoder d = pd.withFactory(providerFactory);
Certificate c = d.decode(pem, X509Certificate.class);

If the provider cannot produce the required type of cryptographic object then an IllegalArgumentException is thrown.

In all cases, any data preceding the PEM header in the input string or byte stream is ignored; it should be processed by the caller if needed. If the input cannot be parsed then an IllegalArgumentException is thrown. Bytes read from input streams are assumed to represent characters encoded in the ISO-8859-1 charset.

The EncryptedPrivateKeyInfo class

The existing EncryptedPrivateKeyInfo class represents an encrypted private key. To make it easier to use with the PEMEncoder and PEMDecoder classes, we have added four methods to it:

EncryptedPrivateKeyInfo {

     ...

     public static EncryptedPrivateKeyInfo
         encryptKey(PrivateKey key, char[] password);
     public static EncryptedPrivateKeyInfo
         encryptKey(PrivateKey key, char[] password,
                    String algorithm, AlgorithmParameterSpec params,
                    Provider p);

     public PrivateKey getKey(char[] password) throws InvalidKeyException;
     public PrivateKey getKey(char[] password, Provider provider)
         throws InvalidKeyException;

 }

The two new static encryptKey methods encrypt the given PrivateKey with the given password. For advanced usage, the second method allows all of the cryptographic parameters to be specified if the defaults are not sufficient. The returned EncryptedPrivateKeyInfo instance can then be passed to a PEMEncoder to encode to PEM text:

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

The new getKey methods decrypt the private key in an EncryptedPrivateKeyInfo instance. These methods take a password and, possibly, a cryptographic provider, and return a PrivateKey. They can be used when a PEMDecoder returns an EncryptedPrivateKeyInfo:

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

The default password-based encryption (PBE) algorithm used when encrypting a PrivateKey with either PEMEncoder or EncryptedPrivateKeyInfo is defined in the default security properties file. The jdk.epkcs8.defaultAlgorithm security property defines the default algorithm to be "PBEWithHmacSHA256AndAES_128". The default algorithm might change in the future, but this will not affect PEM text created today since the data encoded in that text contains the algorithm name and all other parameters necessary for decryption.

Alternatives

A PEM API is a bridge between Base64 and cryptographic objects. We rejected many other potential designs because they did not fit well with the existing cryptographic APIs. While some of the alternatives might have been adequate, we chose the proposed API for its similarity to the HexFormat API and the nested Encoder and Decoder classes of the Base64 API. We wanted to have immutability, thread safety, and distinct paths through the API for encoding and decoding.

Some of the alternatives we considered include:

Testing

Tests will include: