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

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.

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

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.

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

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

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.

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

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.

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

Reply via email to