PEM files as an alternative to keystores

Sebastian Hempel

JDK 25 introduces the preview of JEP 470 – PEM encodings of cryptographic objects. The JEP introduces new classes and methods to handle keys and certificates stored in so-called PEM files. It was possible to load / store files in this format before this JEP, but the handling was far from easy. This JEP simplifies the handling of and allows programmers to easily work with the widely used PEM file format in their applications.

Java keystores with JKS and PKCS12

From the beginning, Java was able to handle different certification objects, like private keys and certificates. To persist these kinds of objects or to load objects from storage, the so-called Java keystore was used. With this format, it is possible to store multiple cryptographic objects in a single file. The store can contain public / private key pairs and certificates. Each entry of the store can have an alias / name. It is possible to protect the complete keystore with a password. Besides that, each entry can have an additional, unique password.

Depending on the content of a keystore, you can often read about a truststore. The format of a truststore is the same as a keystore. A truststore only contains certificates without any private key. The certificates in a truststore are used to verify if the system trusts a given certificate. Only certificates with a certificate chain ending in a certificate contained in a truststore are valid and trusted.

Java used to use the proprietary JKS format (Java KeyStore) for a keystore. During the lifetime of JDK8, with JEP 229, the default format of a keystore was changed to PKCS12. The reason for this was that the JKS format could not easily be extended to use new cryptographic algorithms. PKCS12, instead, is an open, extensible, and widely used format to store cryptographic keys. Java could now easily read JKS and PKCS12 keystores without any changes to the Java application itself.

Now with JEP 470 we can use another open format to handle cryptographic objects in Java.

PEM file format

PEM is a textual format for binary data. The Privacy-Enhanced Mail format defined by RFC 7468 is widely used by certification authorities (CAs) and popular software like OpenSSL to transfer cryptographic objects like private keys, public keys, and certificates.

A PEM file contains a BASE64 representation of the object. The object itself is a DER-encoded representation of the ASN.1 notation. The following examples shows an example of a PEM-encoded elliptic curve public key.

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

The header BEGIN PUBLIC KEY identifies the type of the cryptographic object. The file is terminated by the corresponding footer END PUBLIC KEY. This format can be read with basic Java code. It requires parsing the header and determining the factory used to create the object. This requires a lot of lines of code. A better approach is the use of the new method / classes introduced by JEP 470.

JEP 470 is not finalised yet and included as a preview in JDK25. To enable the API, you have to use the options —release 25 and —enable-preview when compiling the source code. When running the application, you have to use the option —enable-preview Main. For our examples, we use the source code launcher, and we have to use the option —source 25 —enable-preview Main.java.

The following code reads and decodes the elliptic curve public key given above.

void main() {
    File publicKeyFile = new File("public-key.pem");
    PEMDecoder pd = PEMDecoder.of();
    try {
        ECPublicKey key = pd.decode(new FileInputStream(publicKeyFile), ECPublicKey.class);
        IO.println(key);
    } catch (FileNotFoundException e) {
        IO.println("cannot find public-key.pem");
    } catch (IOException e) {
        IO.println("error reading public-key.pem");
    }
}

We create a FileInputStream to read the content of the PEM file. To decode the PEM file, we create an instance of the new PEMDecoder class. The static method of returns a thread-safe and reusable instance of the decoder object. We can use this object repeatedly. This code only works for PEM files that contain an EC public key. It will throw a ClassCastException for any other type of cryptographic object.

We can use pattern matching with instanceof or the switch statement to identify the type of the cryptographic object returned by the decode method.

void main() {
    File publicKeyFile = new File("public-key.pem");
    PEMDecoder pd = PEMDecoder.of();
    try {
        switch (pd.decode(new FileInputStream(publicKeyFile))) {
            case PublicKey publicKey -> IO.println(publicKey);
            default -> throw new IllegalArgumentException("unsupported type");
        }
    } catch (FileNotFoundException e) {
        IO.println("cannot find public-key.pem");
    } catch (IOException e) {
        IO.println("error reading public-key.pem");
    }
}

The decode method is able to return objects of classes that implement the new (sealed) interface DEREncodeable. This marker interface is used by the JDK to identify cryptographic APIs that support encoding and decoding of objects to a byte array in the distinguished encoding rule (DER) format.

Private keys are often secured by a passphrase. To read a PEM file with a protected private key, we create a PEMDecoder with a decryption password.

void main() {
    File privateKeyFile = new File("private-key.pem");
    PEMDecoder pd = PEMDecoder.of();
    try {
        switch (pd.withDecryption("password".toCharArray()).decode(new FileInputStream(privateKeyFile))) {
            case PrivateKey privateKey -> IO.println(privateKey.getAlgorithm());
            default -> throw new IllegalArgumentException("unsupported type");
        }
    } catch (FileNotFoundException e) {
        IO.println("cannot find private-key.pem");
    } catch (IOException e) {
        IO.println("error reading private-key.pem");
    }
}

We can specify the password to decrypt the PEM file by the withDecryption method. The decoder can still be used to decode unencrypted objects.

As an opposite to the PEMDecoder, we use instances of PEMEncoder to create PEM strings.

void main() {
    File keypairFile = new File("keypair.pem");
    try {
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
        kpg.initialize(4096);
        KeyPair kp = kpg.generateKeyPair();

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

        try (OutputStream os = new FileOutputStream(keypairFile)) {
            os.write(pem);
        } catch (IOException e) {
            IO.println("error writing keypair.pem");
        }
    } catch (NoSuchAlgorithmException e) {
        IO.println("Cannot create RSA keypair");
    }
}

We create an instance of the PEMEncoder. The encode method returns a byte array with characters encoded in ISO-8859-1. To encode the cryptographic object to a Java string, we can use the method encodeToString.

We can encode a private key with a password. For this, we create a PEMEncoder with a password using the method withEncryption. The created encoder can only be used to store private keys with the configured password.

void main() {
    File privateKeyFile = new File("private-key.pem");
    try {
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
        kpg.initialize(4096);
        KeyPair kp = kpg.generateKeyPair();

        PEMEncoder pe = PEMEncoder.of();
        byte[] pem = pe.withEncryption("password".toCharArray()).encode(kp.getPrivate());

        try (OutputStream os = new FileOutputStream(privateKeyFile)) {
            os.write(pem);
        } catch (IOException e) {
            IO.println("error writing private-key.pem");
        }
    } catch (NoSuchAlgorithmException e) {
        IO.println("Cannot create RSA keypair");
    }
}

Pros and Cons of PEM files

The use of PEM files has its pros and cons.

The PEM format is widely used in the industry. You often get your key-material in the PEM format. By directly using these files, you don’t have to implement solutions to convert PEM files to a Java keystore. For example, most clients for Let’s Encrypt save the signed certificate as a PEM file.

PEM files are also directly used by most operating systems. Cryptographic objects provided by the operating system can be used by directly reading these files.

The use of PEM files for cryptographic objects also has its downsides. In a Java keystore, the private key and the certificate / public key are stored as one entry. The key pair can be found in one place. With PEM files, you have a separate file for the private key and the certificate. You have to be sure that the two files contain the corresponding key pair. To deal with this problem, you can, for example, calculate the modulus of the private key and the public key and compare them.

Beside that the introduction of direct PEM file handling in JDK 25 is a big step to use existing key material with a java application. The new API is still in preview but will hopefully be finalised soon.

This article is part of the magazine issue Java 25 – Part 1.
You can read the complete issue with all contributions here.

Total
0
Shares
Previous Post

EclipseStore 4.0.0 Beta 1: Build Java vector database Apps

Next Post

the runtime illusion: what Java really runs

Related Posts