Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8300911

PEM Encodings of Cryptographic Objects (Preview)

XMLWordPrintable

    • Icon: JEP JEP
    • Resolution: Unresolved
    • Icon: P4 P4
    • None
    • security-libs
    • None
    • Anthony Scarpino
    • Feature
    • Open
    • SE
    • security dash dev at openjdk dot org
    • M
    • M

      Summary

      Introduce an API for encoding and decoding the Privacy-Enhanced Mail (PEM) format. The PEM format is used for storing and sending cryptographic keys, certificates, and certificate revocation list. This is a preview API.

      Goals

      • Ease of use - Define a concise API that converts between PEM text and key, certificate, and certificate revocation list objects.
      • Standards - Support conversions between PEM data and cryptographic objects that are represented in the following binary formats: PKCS#8 (private keys), X.509 (certificates, CRLs, and public keys), and PKCS#8 v2.0 (OneAsymmetricKey and Encrypted private keys).

      Non-Goals

      • It is not a goal to support types in RFC 7468 that cannot be represented as cryptographic Java objects.

      Motivation

      The Java API has rich support for cryptographic objects, including public keys, private Keys, certificates, and certificate revocation lists. Developers use these objects to sign and verify signatures, verify TLS connections, and perform other cryptographic operations.

      Some applications may only load cryptographic objects at startup or from a local keystore, but other applications may import objects from a user, the network, or a device. This requires a common way to store and communicate cryptographic objects in diverse environments. Per RFC 7468, PEM defines a frequently used textual format for cryptographic objects.

      This textual format was originally designed to send cryptographic objects via e-mail, but over time has been used and extended to different contexts. Certificate Authorities issue certificate chains in PEM. Microservices may use PEM for key and/or certificate stores when replicating multiple server instances that require pre-configured cryptographic objects. Cryptographic libraries, like OpenSSL, support cryptographic object generation and format conversion. Key Management applications may initialize and update cryptographic objects with PEM.

      As the following example shows, PEM consists of a <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">Base64</code> API representation of the cryptographic object’s binary-encoded representation surrounded by a header and footer containing "BEGIN" and "END", respectively. The type of the header and footer identify the type of the cryptographic object, "PUBLIC KEY", and are book-ended with five dashes. Details about the key, such as the algorithm, can be obtained by parsing the binary-encoded representation. Below is an example of an Elliptic Curve(EC) public key:

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

      The Java Platform does not include an easy-to-use API for decoding and encoding this format. This pain point was expressed in the Java Cryptographic Extensions Survey in April 2022. While each cryptographic object provides a method to return its binary-encoded representation and there is a Base64 API to textualize it, the Java API leaves the rest of the work 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 one new interface and two new classes:

      • A DEREncodeable interface that will be implemented by Java API classes that contain binary-encodable key and certificate material.
      • PEMEncoder and PEMDecoder classes for encoding to and decoding from the PEM format. These APIs are immutable and reusable and do not keep state from the previously used security object.

      This is a preview API, disabled by default

      To use this new API in JDK 24, you must enable preview features:

      DEREncodable

      PEM is a textual format for binary data. Thus to encode a cryptographic object into PEM text, or 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 therefore 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 interface is sealed; its permitted classes and interfaces are: <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">AsymmetricKey</code>, <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">X509Certificate</code>, <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">X509CRL</code>, <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">KeyPair</code>, <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">EncryptedPrivateKeyInfo</code>, <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">PKCS8EncodedKeySpec</code>, and <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">X509EncodedKeySpec</code>.

      package java.security;
      
      public sealed interface DEREncodable permits AsymmetricKey, KeyPair, X509CRL,
          X509Certificate, PKCS8EncodedKeySpec, X509EncodedKeySpec,
          EncryptedPrivateKeyInfo {}

      We make corresponding adjustments to the permitted classes and interfaces:

      public non-sealed interface AsymmetricKey ...
      public non-sealed abstract class X509Certificate ...
      public non-sealed abstract class X509CRL ...
      public final class EncryptedPrivateKeyInfo ...
      public final class PKCS8EncodedKeySpec ...
      public final class X509EncodedKeySpec ...

      Encoding

      The PEMEncoder class defines methods for encoding DEREncodable objects to PEM text:

      package java.security;
      
      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, get a PEMEncoder instance by calling of(). This instance is reusable and thread-safe, allowing encode methods to be used repeatedly. There are two methods to complete the encoding process. The first is encode(DEREncodable) that returns PEM text in a byte[]. The other is encodeToString(DEREncodable) that returns PEM text as a String. Return values use StandardCharsets.ISO-8859-1.

      If the DEREncodable used is a <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">PrivateKey</code>, there is an option to encrypt. The withEncryption(char[] password) method returns a new immutable PEMEncoder instance, configured with encryption using a default algorithm. Only PrivateKey objects can be encoded by an encrypted PEMEncoder. To use non-default encryption parameters or encrypt with a different encryption <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">Provider</code>, encode with an <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">EncryptedPrivateKeyInfo</code> object. See the EncryptedPrivateKeyInfo section below for more details.

      Here are examples using the PEMEncoder class:

      Encoding a PrivateKey into PEM text:

      PEMencoder p = PEMEncoder.of();
      var pemData = p.encode(privKey);

      Encoding a PrivateKey into PEM text with encryption:

      String pemString = p.withEncryption(password).encodeToString(privKey);

      Encoding both a public and private key into the same PEM text:

      byte[] pemData = p.encode(new KeyPair(publicKey, privateKey));

      Decoding

      The PEMDecoder subclass defines methods for decoding PEM text to a DEREncodable:

       package java.security;
      
       public final class PEMDecoder {
           public static PEMDecoder of();
           public PEMDecoder withDecryption(char[] password);
           public PEMDecoder withFactory(Provider provider);
           public DEREncodable decode(String str);
           public DEREncodable decode(InputStream is) throws IOException;
           public <S extends DEREncodable> S decode(String string,
               Class<S> sClass);
           public <S extends DEREncodable> S decode(InputStream is,
               Class<S> sClass) throws IOException;
       }

      To decode PEM text, get a PEMDecoder instance by calling of(). This instance is reusable and thread-safe, allowing decode methods to be used repeatedly. There are four methods to complete the decoding process. They each return a DEREncodable for which the caller can use pattern matching when processing the result. If you know the class type being decoded, the two decode methods that take a Class<S> argument can be used to specify the returned object's class. If the class does not match the PEM type, an <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">ClassCastExeption</code> is thrown. When passing input data, the application is responsible for processing data ahead of the PEM header as it will be ignored. All input data into these methods will use StandardCharsets.ISO-8859-1. Any <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">IOException</code> from the InputStream will be thrown and parsing errors will be thrown as <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">IllegalArgumentException</code>.

      withDecryption(char[] password) allows for decrypting encrypted private key PEM text. It returns a new immutable PEMDecoder configured with the given password. When this configured instance is used on encrypted private key PEM text, the decode methods will return a PrivateKey object. The other PEM types can be decoded by this configured instance, as decryption is not relevant. On non-decryption configured instances, the decode methods return an EncryptedPrivateKeyInfo object for encrypted private keys, which can then be used to decrypt and generate the PrivateKey object (see EncryptedPrivateKeyInfo below for more details).

      In some situations a DEREncodable may need to be generated with a particular cryptographic provider. The withFactory(Provider) method returns a new PEMDecoder instance that uses the specified provider to generate the cryptographic object. If the provider does not support the data being decoded, an IllegalArgumentException is thrown.

      Here are some examples:

      Decoding a <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">PublicKey</code> from PEM text:

      PEMDecoder pd = PEMDecoder.of();
      PublicKey key = pd.decode(pemData, PublicKey.class);

      Using pattern matching when decoding PEM text of an unknown type:

      switch (pd.decode(pemData)) {
          case PublicKey pubkey -> ...
          case PrivateKey privkey -> ...
          ...
      }

      Decoding an encrypted ECPrivateKey from PEM text:

      ECPrivateKey eckey = pd.withDecryption(password).
          decode(pemData, ECPrivateKey.class);

      Decoding a Certificate with a specific Factory provider:

      PEMDecoder d = pd.withFactory(providerF);
      Certificate c = d.decode(pemData, X509Certificate.class);

      EncryptedPrivateKeyInfo

      The EncryptedPrivateKeyInfo class represents an encrypted private key. We have enhanced this class with additional methods for situations in which more encryption parameters are needed:

       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 encryptKey() methods encrypt the given PrivateKey with the given password. For advanced usage, the second method allows all the cryptographic parameters to be specified if the defaults are not sufficient. The returned EncryptedPrivateKeyInfo object can then be passed to PEMEncoder.encode() to encode to PEM text:

      ekpi = EncryptedPrivateKeyInfo.encryptKey(privkey, password);
      byte[] pemData = PEMEncoder.of().encode(epki);

      The getKey() methods are used for decrypting an initialized EncryptedPrivateKeyInfo, such as when PEMDecoder.decode() has returned an EncryptedPrivateKeyInfo. The methods will return a PrivateKey with a given password and an optional cryptographic provider:

      EncryptedPrivateKeyInfo epki = PEMDecoder.of().decode(pemData);
      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 jdk/conf/security/java.security 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 texts created today since the binary data encoded in those text contains the name of the algorithm and other parameters necessary for decryption.

      Alternatives

      A PEM API is a bridge between Base64 and cryptographic objects. Many other potential API designs were rejected over how they fit with the security APIs. Retrofitting existing APIs can be awkward when all the pieces do not fit in the right classes. While a few of the rejected alternatives could have sufficiently delivered the feature, we chose the proposed API for its similarity to HexFormat and Base64's Encoder/Decoder classes. There were strong requirements to have a distinct encode and decode paths through the API, immutability, thread-safety, and conversion between PEM text and cryptographic objects in an easy-to-use way. Some of those design alternatives are documented below:

      Extend the EncodedKeySpec API

      The EncodedKeySpec API encapsulates binary encoded key data for <code class="prettyprint" data-shared-secret="1735236212559-0.2792157962694374">KeyFactory</code> instances and other security classes. A new PEMEncodedKeySpec subclass could type identify encapsulated PEM text, while providing encoding and decoding operations between PEM text and the appropriate private or public key EncodedKeySpec.

      This design had a few deficiencies. First, A new PEMEncodedKeySpec would be used as a translation class which is not the purpose of EncodedKeySpec. Second, EncodedKeySpec is Key-centric and cannot support encoded certificates or CRLs to PEM text. Finally, a new EncodedKeySpec subclass would add compatibility risks and ease of use issues with existing third-party cryptographic providers.

      Enhance the CertificateFactory and KeyFactory APIs

      Key and certificate factory classes are used to convert and generate their respective objects. The CertificateFactory API already supports decoding of PEM certificate and CRL data. Adding encoding to CertificateFactory and KeyFactory would be consistent with the existing design, but CertificateFactory makes the design look easy as there is one industry standard encoding for certificates. KeyFactory has different key encoding formats to support and cryptographic providers may support a subset of asymmetric keys. PEM adds an extra layer of complexity if each provider is responsible for its conversion; as well as, handling encrypted private keys. This makes KeyFactory a difficult solution.

      Static method usage

      Static methods are good for immutability and thread-safety, but for PEM, encrypted private keys present a usability issue. Conversion of an encrypted private key is a two-stage operation, while other types of cryptographic objects is a single-stage operation. The encryption/decryption stage could require solutions such as a different static method for decryption parameters or use of EncryptedPrivateKeyInfo. This does not produce a consistent operational flow with all objects.

      Having encoders or decoders storing configuration state, like a password, gives the application consistency of a usable result, a PrivateKey or PEM text.

      Intermediate PEM Object API

      We could introduce a wrapper class whose instances would wrap a key, a certificate, a certificate revocation list, or some PEM text. This object could either contain methods to encode/decode or a separate API would perform operations on it. While it 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 operations. Having distinct encoding and decoding paths from given data provides a more guided user experience.

      Single Class API

      A single PEM class could performs both encoding and decoding, but similar to the Static Method API and Intermediate PEM Object issues, it lacks distinct operational paths. Separating the encoding and decoding into their own classes results in an API that’s easier to use because each class presents the developer with 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 used internally by individual providers for importing and exporting cryptographic objects. Adding services for textual and binary formats could be useful beyond PEM.

      After examination, this alternative creates a lot of infrastructure and little additional service. Binary formats as a service creates potential compatibility risks for existing providers and API usage becomes complicated.

      Supporting PEM or other textual formats are complicated for providers, as they have format-specific requirement. A general purpose textual interface to the provider or a format-specific interface for each textual format are not reasonable solutions.

      General Purpose Encoder & Decoding API

      We looked at a generic Java security API that could be used with many textual format, instead of a PEM-specific API. We rejected this because textual format do not all have to same features. Some support both different keys, certificate chains, compression, and other options. Many format in one API, where some methods were format-specific would be confusing for developers. A simple single format API is an easy to use approach.

      Testing

      Tests will include:

      • Verifying all supported DEREncodable classes can encode and decode PEM text.
      • Verifying RSA, EC, and EdDSA security objects can be encoded and decoded.
      • Reading PEM text generated from third-party applications, and vice-versa.
      • Negative testing with bad PEM text.

            mr Mark Reinhold
            ascarpino Anthony Scarpino
            Anthony Scarpino Anthony Scarpino
            Alan Bateman, Sean Mullan
            Votes:
            0 Vote for this issue
            Watchers:
            8 Start watching this issue

              Created:
              Updated: