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