Hi Neil,

Thanks for taking the time to provide some feedback.

On 3/24/26 4:37 AM, Neil Madden wrote:
> Hi,
>
> I'd like to provide some feedback on the 2nd preview of PEM support
> (JEP-524) in the recently released OpenJDK 26.
>
> Firstly, thanks for adding this support, it's very welcome as someone who
> works with PEM files on a pretty much daily basis. However, I think the
> functionality and API still need some work.
>
> From a security point of view, I am hesitant about more support for
> password-based encryption in 2026. It's just not secure and shouldn't be
> encouraged. For example, OpenSSL 3.6.1 27 Jan 2026 (Library: OpenSSL 3.6.1
> 27 Jan 2026) on my machine will happily accept completely insecure
> passwords and then sets the default iterations to just 2048. The
> implementation in the JDK appears to default to just 4096 iterations for
> similarly insecure passwords ('changeme'). Many openssl examples on the
> internet still suggest using -des3 to encrypt the key. But really, any
> kind of password-based encryption is woefully inadequate to protect
> private key material as I wrote about on https://
> neilmadden.blog/2023/01/09/on-pbkdf2-iterations/ after the LastPass
> breach. 4096 iterations is completely useless for protecting any kind of
> long- term secret like a private key unless the password is already
> cryptographically-strong (in which case you don't need iterated hashing).

This is outside the PEM API’s control; those values are determined by the
crypto provider and the user’s input. The PEM API is designed for simpler use
cases. For more advanced scenarios, the JEP introduces new
EncryptedPrivateKeyInfo.encrypt() methods that let callers provide a Key and
explicitly configure the encryption parameters.

> Support for _reading_ encrypted PEMs seems reasonable, as they are widely
> used, but perhaps don't support writing them? Or if you must, then
> perhaps only support an API that automatically generates a strong random
> password (and then sets the iterations to 1) and don't support a user-
> supplied password at all.

Not supporting encryption or password-based encoding would be a significant functional limitation. EncryptedPrivateKeyInfo.encrypt() addresses this by allowing callers to provide a key and/or specify alternative encryption parameters.

>
> By the way, while testing this I discovered that the implementation
> doesn't support PKCS#1 encrypted private keys, which was the default for
> OpenSSL 1 for many years. It's reasonable to only support PKCS#8-format
> encrypted keys in 2026, but that should be documented, as there are still
> lots of keys in that format. Furthermore, the implementation currently
> fails to even parse keys in this format:
>
> Exception in thread "main" java.io.IOException: Incomplete footer
>    at java.base/sun.security.util.Pem.readPEM(Pem.java:271)
>    at java.base/sun.security.util.Pem.readPEM(Pem.java:334)
>    at java.base/java.security.PEMDecoder.decode(PEMDecoder.java:423)
>
>  From a file that starts:
>
> -----BEGIN RSA PRIVATE KEY-----
> Proc-Type: 4,ENCRYPTED
> DEK-Info: DES-EDE3-CBC,ECBE98FA344F0C87
>
> ...
>
> (The hyphens in the DEK-Info field are causing the issue here. Having
> written comprehensive PEM decoders in the past, these unstructured blocks
> are a pain to skip correctly).

Unencrypted PKCS#1 RSA decoding is provided for basic compatibility, but there
is no current plan to support encrypted PKCS#1. That should be documented more
clearly.

I fixed the decoder so it can generate a PEM object for encrypted PKCS#1 RSA
PEM text when decode() is called with PEM.class.

> OK, so on to general comments about the API.
>
> Overall, I find the API a bit non-intuitive, non-discoverable, and takes a
> bit of guesswork. For example, it appears that reading a KeyPair requires
> that the key is in PKCS#8 format with an embedded public key. Simply
> concatenating the PEM-format private and public keys (as is common with
> PEMs) doesn't work: you have to read the components separately. You then
> have to know what order they are in within the file, or else resort to a
> generic decode() and switch statement (which feels a bit ugly and
> unnecessary). In my experience, PEM files that contain more than one
> object are rarely in a consistent order.

Each PEM block is treated as independent. When reading from an InputStream, there’s no guarantee of order or of how many PEM blocks the file contains. There may be a mix of public keys, private keys, and certificates, interspersed with metadata. If the developer knows the order and types in the PEM stream, they can use the decode method that takes a class. If they don’t know the format, the generic decode method with a switch statement addresses that situation. A public API needs to be flexible enough to support different usage patterns.

RFC 5958 defines the OneAsymmetricKey structure, which allows public and private keys to be in the same PEM. It’s the only way the API can guarantee that the keys were intended to be paired.

If the developer wants to concatenate a public and private key, they can encode each separately.

> This need to use a switch statement, coupled with the fact that
> DEREncodable is a sealed type also seems like it creates the potential for
> breaking changes in the future. If I create an exhaustive switch statement
> to parse a generic PEM format, then any addition of a new type of
> DEREncodable object will break my code - e.g. if you ever decide to add a
> PKCS1EncodedKeySpec or perhaps a post-quantum-related new type of object.
> I would vote for making the interface non-sealed. (Use an abstract class
> if you want to not allow external implementations).

I understand that sealing can present challenges for third-party classes. Sealing this interface was an architectural decision to prevent types that don’t support PEM from being incorrectly cast or returned.

If the JDK adds new class types in the future, the DEREncodable interface can be updated to permit those additional classes/interfaces.

> (Also, if you're going to have a fixed hierarchy, why not have
> decodePrivateKey() etc methods rather than taking a .class?)

That requires the developer to know the PEM type being decoded at compile time. If that’s the case, the class can be specified in the existing decode() method. Having general decoding methods and specific ones, like decodePrivateKey(), would be redundant.

A top-level API goal was to minimize applications having to parse PEM text.

> I think there are some obvious (to me, as a long-time JCA user)
> alternative designs which are not considered in the JEP. Even if you
> reject them, it would be good to have the rationale considered and
> documented:
>
> 1. An obvious API that comes to mind is the KeyStore API. Given that a PEM
> file can contain multiple types of cryptographic object, this would seem
> somewhat natural. The KeyStore implementation could then automatically
> iterate through the objects and work out which ones belong together,
> reconstruct certificate chains correctly, put things in the correct
> order, and so on. Certainly for configuring TLS key/truststores this seems
> by far the most natural interface. And there is already precedent in that
> API for supporting password-based encryption (with all the same
> drawbacks), so the security model fits naturally.

I don't see this as an alternative PEM API design. A KeyStore implementation would be a separate feature that could use a the PEM API. Additionally not every PEM type can be used with the KeyStore API.

> 2. For encoding, many of the DEREncodable classes already support a
> .getEncoded() method. There is precedent in the JDK for a
> .getEncoded(String format) option too - e.g. AlgorithmParameters. A
> key.getEncoded("PEM") or even key.getEncoded("PEM", password) seem natural
> and easily-discoverable alternatives. They would also seem (to me) simple
> to provide default implementations of too (e.g. in X509Key for public
> keys).

We explored similar designs during the process. Distributing PEM encoding across existing classes has some advantages, but decoding doesn’t fit that model. It would still need a separate decoder and rely on a shared interface to tie the various types together. That split felt inconsistent and disjointed.

> 3. Likewise, although the JEP dismisses adding a new PEMEncodedKeySpec for
> use with KeyFactory, there is precedent in the JDK for handling PEMs
> already via existing factory classes. Notably the X.509 CertificateFactory
> already transparently handles inputs in PEM format, as well as DER
> encoded. Could KeyFactory not also do this? Perhaps transparently in the
> KeyFactory class itself so that it is done for all provider
> implementations.

EncodedKeySpecs only describe the kind of data being carried, so they didn’t add much value here. CertificateFactory decodes a single type, but KeyFactory may need to decode several: a public key, a private key, an encrypted private key, or a key pair. If each of those requires a separate decode method, we end up with the same issue as many existing APIs: either the type must be known at compile time, or the application has to parse the PEM header itself.

Also, some PEM text may not map cleanly to an existing Java cryptographic object or either factory. Those would require some sort of miscellaneous “Factory”, which would not result in a clean design.

> 4. Finally, if the desire is to have separate encoder/decoder APIs, why
> not PEMInputStream/PEMOutputStream? I don't really understand the desire
> to have separate encoder/decoder classes for this, it just makes the API
> more obtuse.> Compare:>
> try (var in = new PEMDecoder(new FileInputStream(...)) {
>     var priv = in.read(PrivateKey.class); // or .readPrivateKey()
> }
>
> vs
>
> try (var in = new FileInputStream(...)) {
>      var decoder = PEMDecoder.of(); // nit: "of" what?
>      var priv = decoder.decode(in, PrivateKey.class);
> }>
> I hope this feedback helps.
>
> Best wishes,
>
> Neil Madden
>

Much of the your API feedback would require the application be responsible for parsing the PEM header. The goal of this API is to make PEM encoding and decoding of cryptographic objects straightforward, and having the API handle the header supports that goal.

Concerns about DEREncodable switch statements also miss the possibility of heterogeneous inputs. Wherever the PEM header is interpreted, there will be some conditional logic to use the correct decoding path. If the input is homogeneous and the type is known, no switch is needed and the call stays simple, PEMDecoder.of().decode(stream, PrivateKey.class).

Thanks for the feedback,

Tony

Reply via email to