JEP 470: PEM Encodings of Cryptographic Objects (Preview)
Owner | Anthony Scarpino |
Type | Feature |
Scope | SE |
Status | Candidate |
Component | security-libs / java.security |
Discussion | security dash dev at openjdk dot org |
Effort | M |
Duration | M |
Reviewed by | Alan Bateman, Sean Mullan |
Created | 2023/01/23 18:28 |
Updated | 2025/04/28 20:26 |
Issue | 8300911 |
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
-
Ease of use — Define a concise API that converts between PEM text and objects representing keys, certificates, and certificate revocation lists.
-
Support standards — Support conversions between PEM text and cryptographic objects that have standard representations in the binary formats PKCS#8 (for private keys), X.509 (public keys, certificates, and certificate revocation lists), and PKCS#8 v2.0 (encrypted private keys and asymmetric keys).
Non-Goals
- It is not a goal to support PEM encodings for which no corresponding standard Java API exists.
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:
-
Encoding a public key is straightforward, if tedious.
-
Decoding a PEM-encoded key requires careful parsing of the source PEM text, determining the factory to use to create the key object, and determining the key's algorithm.
-
Encrypting and decrypting a private key requires over a dozen lines of code.
Surely, we can do better.
Description
We introduce a new interface and two new classes in the java.security
package:
-
The
DEREncodeable
interface, implemented by Java API classes that contain binary-encodable key or certificate material, and -
PEMEncoder
andPEMDecoder
classes, for encoding to and decoding from the PEM format. Instances of these classes are immutable and reusable, i.e., they do not retain information from the previously encoded or decoded cryptographic object.
This is a preview API, disabled by default
To use this API in JDK 25, you must enable preview APIs:
-
Compile the program with
javac --release 25 --enable-preview Main.java
and run it withjava --enable-preview Main
; or, -
When using the source code launcher, run the program with
java --enable-preview Main.java
; or, -
When using jshell, start it with
jshell --enable-preview
.
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:
-
Extend the
EncodedKeySpec
API — This API encapsulates binary-encoded key data forKeyFactory
instances and other cryptographic classes. A newPEMEncodedKeySpec
subclass could type-identify encapsulated PEM text while providing encoding and decoding operations between PEM text and the appropriate private or public keyEncodedKeySpec
.This design had a few deficiencies. First, the
PEMEncodedKeySpec
class would be used for translation, which is not the purpose of its superclass,EncodedKeySpec
. Second,EncodedKeySpec
is key-centric and thus cannot support the encoding of certificates or certificate revocation lists to PEM text. Finally, a newEncodedKeySpec
subclass would raise compatibility risks and ease-of-use issues with existing third-party cryptographic providers. -
Enhance the
CertificateFactory
andKeyFactory
APIs — TheCertificateFactory
API already supports the decoding of PEM certificate and certificate revocation list data, so adding encoding methods toCertificateFactory
andKeyFactory
would be consistent with the existing design.CertificateFactory
makes this approach look easy, since there is one industry standard encoding for certificates.KeyFactory
, by contrast, would have to support different encoding formats. To make matters worse, provider ofKeyFactory
instances need not support all known types of asymmetric keys. Provider maintainers, moreover, may balk at having to be responsible for PEM encoding and also for handling encrypted private keys. This makes enhancingKeyFactory
a difficult solution. -
Static methods — Static methods are good for immutability and thread safety, but encrypted private keys present a usability issue. Conversions of encrypted private keys require a password, while conversions of other types of cryptographic objects do not. Thus, with static methods, for encrypted private keys we would need unpleasant solutions such as additional overloaded methods that take encryption parameters, or the mandatory use of
EncryptedPrivateKeyInfo
instances. Having encoders and decoders store the encryption password results in a better user experience. -
Intermediate PEM object API — We could introduce a wrapper class whose instances would contain a key, a certificate, a certificate revocation list, or some PEM text. This class could either declare encoding and decoding methods, or a separate API could perform operations on its instances.
While this approach would provide an independent representation of PEM text, too much flexibility is a negative. A class whose instances can wrap both cryptographic objects and PEM text could be confusing, since these are fundamentally two different kinds of things. Having distinct encoding and decoding paths from given data provides a more guided user experience.
-
Single-class API — A single PEM class could perform both encoding and decoding, but, as with the static-method and intermediate-PEM-object approaches, it would lack distinct operational paths for encoding and decoding. Separating encoding and decoding into their own classes results in an API that is easier to use since each class presents just the operations needed.
-
Create cryptographic provider service support for encodings — We considered enabling cryptographic providers to support services for converting between textual and binary representations of cryptographic objects. Binary formats are already used internally by providers for importing and exporting cryptographic objects, and adding conversion services could be useful beyond PEM.
This approach would, however, require a lot of infrastructure but result in little additional service. At the same time, it would put compatibility at risk for existing providers and complicate the use of the API.
-
Introduce a general-purpose cryptographic encoding and decoding API — We considered a generic API that could be used with many textual formats. We rejected this because these formats do not all have the same features, differing in their support for keys, certificate chains, compression, and other options. Many formats in one API, where some methods are format-specific, would be confusing.
Testing
Tests will include:
-
Verifying that all supported
DEREncodable
classes can encode and decode PEM text. -
Verifying that RSA, EC, and EdDSA cryptographic objects can be encoded and decoded.
-
Reading PEM text generated by third-party applications, and vice versa.
-
Negative testing with bad PEM text.