8358099: PEM spec updates

Reviewed-by: mullan
This commit is contained in:
Anthony Scarpino 2025-06-05 22:13:24 +00:00
parent c793de989f
commit 78158f30ae
6 changed files with 48 additions and 67 deletions

View File

@ -81,24 +81,24 @@ import java.util.Objects;
* {@link PEMRecord}. * {@link PEMRecord}.
* *
* <p> The {@linkplain #decode(String, Class)} and * <p> The {@linkplain #decode(String, Class)} and
* {@linkplain #decode(InputStream, Class)} methods take a Class parameter * {@linkplain #decode(InputStream, Class)} methods take a class parameter
* which determines the type of {@code DEREncodable} that is returned. These * which determines the type of {@code DEREncodable} that is returned. These
* methods are useful when extracting or changing the return class. * methods are useful when extracting or changing the return class.
* For example, if the PEM contains both public and private keys, the * For example, if the PEM contains both public and private keys, the
* Class parameter can specify which to return. Use * class parameter can specify which to return. Use
* {@code PrivateKey.class} to return only the private key. * {@code PrivateKey.class} to return only the private key.
* If the Class parameter is set to {@code X509EncodedKeySpec.class}, the * If the class parameter is set to {@code X509EncodedKeySpec.class}, the
* public key will be returned in that format. Any type of PEM data can be * public key will be returned in that format. Any type of PEM data can be
* decoded into a {@code PEMRecord} by specifying {@code PEMRecord.class}. * decoded into a {@code PEMRecord} by specifying {@code PEMRecord.class}.
* If the Class parameter doesn't match the PEM content, an * If the class parameter doesn't match the PEM content, a
* {@code IllegalArgumentException} will be thrown. * {@linkplain ClassCastException} will be thrown.
* *
* <p> A new {@code PEMDecoder} instance is created when configured * <p> A new {@code PEMDecoder} instance is created when configured
* with {@linkplain #withFactory(Provider)} and/or * with {@linkplain #withFactory(Provider)} and/or
* {@linkplain #withDecryption(char[])}. {@linkplain #withFactory(Provider)} * {@linkplain #withDecryption(char[])}. {@linkplain #withFactory(Provider)}
* configures the decoder to use only {@linkplain KeyFactory} and * configures the decoder to use only {@linkplain KeyFactory} and
* {@linkplain CertificateFactory} instances from the given {@code Provider}. * {@linkplain CertificateFactory} instances from the given {@code Provider}.
* {@link#withDecryption(char[])} configures the decoder to decrypt all * {@linkplain #withDecryption(char[])} configures the decoder to decrypt all
* encrypted private key PEM data using the given password. * encrypted private key PEM data using the given password.
* Configuring an instance for decryption does not prevent decoding with * Configuring an instance for decryption does not prevent decoding with
* unencrypted PEM. Any encrypted PEM that fails decryption * unencrypted PEM. Any encrypted PEM that fails decryption
@ -117,15 +117,15 @@ import java.util.Objects;
* <p> Here is an example of a {@code PEMDecoder} configured with decryption * <p> Here is an example of a {@code PEMDecoder} configured with decryption
* and a factory provider: * and a factory provider:
* {@snippet lang = java: * {@snippet lang = java:
* PEMDecoder pe = PEMDecoder.of().withDecryption(password). * PEMDecoder pd = PEMDecoder.of().withDecryption(password).
* withFactory(provider); * withFactory(provider);
* byte[] pemData = pe.decode(privKey); * byte[] pemData = pd.decode(privKey);
* } * }
* *
* @implNote An implementation may support other PEM types and * @implNote An implementation may support other PEM types and
* {@code DEREncodables}. This implementation additionally supports PEM types: * {@code DEREncodable} objects. This implementation additionally supports
* {@code X509 CERTIFICATE}, {@code X.509 CERTIFICATE}, {@code CRL}, * the following PEM types: {@code X509 CERTIFICATE},
* and {@code RSA PRIVATE KEY}. * {@code X.509 CERTIFICATE}, {@code CRL}, and {@code RSA PRIVATE KEY}.
* *
* @see PEMEncoder * @see PEMEncoder
* @see PEMRecord * @see PEMRecord
@ -179,13 +179,13 @@ public final class PEMDecoder {
return switch (pem.type()) { return switch (pem.type()) {
case Pem.PUBLIC_KEY -> { case Pem.PUBLIC_KEY -> {
X509EncodedKeySpec spec = X509EncodedKeySpec spec =
new X509EncodedKeySpec(decoder.decode(pem.pem())); new X509EncodedKeySpec(decoder.decode(pem.content()));
yield getKeyFactory( yield getKeyFactory(
KeyUtil.getAlgorithm(spec.getEncoded())). KeyUtil.getAlgorithm(spec.getEncoded())).
generatePublic(spec); generatePublic(spec);
} }
case Pem.PRIVATE_KEY -> { case Pem.PRIVATE_KEY -> {
PKCS8Key p8key = new PKCS8Key(decoder.decode(pem.pem())); PKCS8Key p8key = new PKCS8Key(decoder.decode(pem.content()));
String algo = p8key.getAlgorithm(); String algo = p8key.getAlgorithm();
KeyFactory kf = getKeyFactory(algo); KeyFactory kf = getKeyFactory(algo);
DEREncodable d = kf.generatePrivate( DEREncodable d = kf.generatePrivate(
@ -216,27 +216,27 @@ public final class PEMDecoder {
case Pem.ENCRYPTED_PRIVATE_KEY -> { case Pem.ENCRYPTED_PRIVATE_KEY -> {
if (password == null) { if (password == null) {
yield new EncryptedPrivateKeyInfo(decoder.decode( yield new EncryptedPrivateKeyInfo(decoder.decode(
pem.pem())); pem.content()));
} }
yield new EncryptedPrivateKeyInfo(decoder.decode(pem.pem())). yield new EncryptedPrivateKeyInfo(decoder.decode(pem.content())).
getKey(password.getPassword()); getKey(password.getPassword());
} }
case Pem.CERTIFICATE, Pem.X509_CERTIFICATE, case Pem.CERTIFICATE, Pem.X509_CERTIFICATE,
Pem.X_509_CERTIFICATE -> { Pem.X_509_CERTIFICATE -> {
CertificateFactory cf = getCertFactory("X509"); CertificateFactory cf = getCertFactory("X509");
yield (X509Certificate) cf.generateCertificate( yield (X509Certificate) cf.generateCertificate(
new ByteArrayInputStream(decoder.decode(pem.pem()))); new ByteArrayInputStream(decoder.decode(pem.content())));
} }
case Pem.X509_CRL, Pem.CRL -> { case Pem.X509_CRL, Pem.CRL -> {
CertificateFactory cf = getCertFactory("X509"); CertificateFactory cf = getCertFactory("X509");
yield (X509CRL) cf.generateCRL( yield (X509CRL) cf.generateCRL(
new ByteArrayInputStream(decoder.decode(pem.pem()))); new ByteArrayInputStream(decoder.decode(pem.content())));
} }
case Pem.RSA_PRIVATE_KEY -> { case Pem.RSA_PRIVATE_KEY -> {
KeyFactory kf = getKeyFactory("RSA"); KeyFactory kf = getKeyFactory("RSA");
yield kf.generatePrivate( yield kf.generatePrivate(
RSAPrivateCrtKeyImpl.getKeySpec(decoder.decode( RSAPrivateCrtKeyImpl.getKeySpec(decoder.decode(
pem.pem()))); pem.content())));
} }
default -> pem; default -> pem;
}; };
@ -271,7 +271,6 @@ public final class PEMDecoder {
*/ */
public DEREncodable decode(String str) { public DEREncodable decode(String str) {
Objects.requireNonNull(str); Objects.requireNonNull(str);
DEREncodable de;
try { try {
return decode(new ByteArrayInputStream( return decode(new ByteArrayInputStream(
str.getBytes(StandardCharsets.UTF_8))); str.getBytes(StandardCharsets.UTF_8)));
@ -483,9 +482,6 @@ public final class PEMDecoder {
* from the specified {@link Provider} to produce cryptographic objects. * from the specified {@link Provider} to produce cryptographic objects.
* Any errors using the {@code Provider} will occur during decoding. * Any errors using the {@code Provider} will occur during decoding.
* *
* <p>If {@code provider} is {@code null}, a new instance is returned with
* the default provider configuration.
*
* @param provider the factory provider * @param provider the factory provider
* @return a new PEMEncoder instance configured to the {@code Provider}. * @return a new PEMEncoder instance configured to the {@code Provider}.
* @throws NullPointerException if {@code provider} is null * @throws NullPointerException if {@code provider} is null

View File

@ -71,7 +71,7 @@ import java.util.concurrent.locks.ReentrantLock;
* OneAsymmetricKey structure using the "PRIVATE KEY" type. * OneAsymmetricKey structure using the "PRIVATE KEY" type.
* *
* <p> When encoding a {@link PEMRecord}, the API surrounds the * <p> When encoding a {@link PEMRecord}, the API surrounds the
* {@linkplain PEMRecord#pem()} with the PEM header and footer * {@linkplain PEMRecord#content()} with the PEM header and footer
* from {@linkplain PEMRecord#type()}. {@linkplain PEMRecord#leadingData()} is * from {@linkplain PEMRecord#type()}. {@linkplain PEMRecord#leadingData()} is
* not included in the encoding. {@code PEMRecord} will not perform * not included in the encoding. {@code PEMRecord} will not perform
* validity checks on the data. * validity checks on the data.
@ -108,7 +108,8 @@ import java.util.concurrent.locks.ReentrantLock;
* byte[] pemData = pe.encode(privKey); * byte[] pemData = pe.encode(privKey);
* } * }
* *
* @implNote An implementation may support other PEM types and DEREncodables. * @implNote An implementation may support other PEM types and
* {@code DEREncodable} objects.
* *
* *
* @see PEMDecoder * @see PEMDecoder
@ -287,7 +288,7 @@ public final class PEMEncoder {
} }
// If `keySpec` is non-null, then `key` hasn't been established. // If `keySpec` is non-null, then `key` hasn't been established.
// Setting a `key' prevents repeated key generations operations. // Setting a `key` prevents repeated key generation operations.
// withEncryption() is a configuration method and cannot throw an // withEncryption() is a configuration method and cannot throw an
// exception; therefore generation is delayed. // exception; therefore generation is delayed.
if (keySpec != null) { if (keySpec != null) {

View File

@ -29,7 +29,6 @@ import jdk.internal.javac.PreviewFeature;
import sun.security.util.Pem; import sun.security.util.Pem;
import java.util.Base64;
import java.util.Objects; import java.util.Objects;
/** /**
@ -39,20 +38,20 @@ import java.util.Objects;
* cryptographic object is not desired or the type has no * cryptographic object is not desired or the type has no
* {@code DEREncodable}. * {@code DEREncodable}.
* *
* <p> {@code type} and {@code pem} may not be {@code null}. * <p> {@code type} and {@code content} may not be {@code null}.
* {@code leadingData} may be null if no non-PEM data preceded PEM header * {@code leadingData} may be null if no non-PEM data preceded PEM header
* during decoding. {@code leadingData} may be useful for reading metadata * during decoding. {@code leadingData} may be useful for reading metadata
* that accompanies PEM data. * that accompanies PEM data.
* *
* <p> No validation is performed during instantiation to ensure that * <p> No validation is performed during instantiation to ensure that
* {@code type} conforms to {@code RFC 7468}, that {@code pem} is valid Base64, * {@code type} conforms to {@code RFC 7468}, that {@code content} is valid
* or that {@code pem} matches the {@code type}. {@code leadingData} is not * Base64, or that {@code content} matches the {@code type}.
* defensively copied and does not return a clone when * {@code leadingData} is not defensively copied and does not return a
* {@linkplain #leadingData()} is called. * clone when {@linkplain #leadingData()} is called.
* *
* @param type the type identifier in the PEM header without PEM syntax labels. * @param type the type identifier in the PEM header without PEM syntax labels.
* For a public key, {@code type} would be "PUBLIC KEY". * For a public key, {@code type} would be "PUBLIC KEY".
* @param pem any data between the PEM header and footer. * @param content the Base64-encoded data, excluding the PEM header and footer
* @param leadingData any non-PEM data preceding the PEM header when decoding. * @param leadingData any non-PEM data preceding the PEM header when decoding.
* *
* @spec https://www.rfc-editor.org/info/rfc7468 * @spec https://www.rfc-editor.org/info/rfc7468
@ -64,25 +63,25 @@ import java.util.Objects;
* @since 25 * @since 25
*/ */
@PreviewFeature(feature = PreviewFeature.Feature.PEM_API) @PreviewFeature(feature = PreviewFeature.Feature.PEM_API)
public record PEMRecord(String type, String pem, byte[] leadingData) public record PEMRecord(String type, String content, byte[] leadingData)
implements DEREncodable { implements DEREncodable {
/** /**
* Creates a {@code PEMRecord} instance with the given parameters. * Creates a {@code PEMRecord} instance with the given parameters.
* *
* @param type the type identifier * @param type the type identifier
* @param pem the Base64-encoded data encapsulated by the PEM header and * @param content the Base64-encoded data, excluding the PEM header and
* footer. * footer
* @param leadingData any non-PEM data read during the decoding process * @param leadingData any non-PEM data read during the decoding process
* before the PEM header. This value maybe {@code null}. * before the PEM header. This value maybe {@code null}.
* @throws IllegalArgumentException if the {@code type} is incorrectly * @throws IllegalArgumentException if {@code type} is incorrectly
* formatted. * formatted.
* @throws NullPointerException if {@code type} and/or {@code pem} are * @throws NullPointerException if {@code type} and/or {@code content} are
* {@code null}. * {@code null}.
*/ */
public PEMRecord(String type, String pem, byte[] leadingData) { public PEMRecord {
Objects.requireNonNull(type, "\"type\" cannot be null."); Objects.requireNonNull(type, "\"type\" cannot be null.");
Objects.requireNonNull(pem, "\"pem\" cannot be null."); Objects.requireNonNull(content, "\"content\" cannot be null.");
// With no validity checking on `type`, the constructor accept anything // With no validity checking on `type`, the constructor accept anything
// including lowercase. The onus is on the caller. // including lowercase. The onus is on the caller.
@ -92,37 +91,22 @@ public record PEMRecord(String type, String pem, byte[] leadingData)
"Only the PEM type identifier is allowed"); "Only the PEM type identifier is allowed");
} }
this.type = type;
this.pem = pem;
this.leadingData = leadingData;
} }
/** /**
* Creates a {@code PEMRecord} instance with a given {@code type} and * Creates a {@code PEMRecord} instance with a given {@code type} and
* {@code pem} data in String form. {@code leadingData} is set to null. * {@code content} data in String form. {@code leadingData} is set to null.
* *
* @param type the PEM type identifier * @param type the PEM type identifier
* @param pem the Base64-encoded data encapsulated by the PEM header and * @param content the Base64-encoded data, excluding the PEM header and
* footer. * footer
* @throws IllegalArgumentException if the {@code type} is incorrectly * @throws IllegalArgumentException if {@code type} is incorrectly
* formatted. * formatted.
* @throws NullPointerException if {@code type} and/or {@code pem} are * @throws NullPointerException if {@code type} and/or {@code content} are
* {@code null}. * {@code null}.
*/ */
public PEMRecord(String type, String pem) { public PEMRecord(String type, String content) {
this(type, pem, null); this(type, content, null);
}
/**
* Returns the binary encoding from the Base64 data contained in
* {@code pem}.
*
* @throws IllegalArgumentException if {@code pem} cannot be decoded.
* @return a new array of the binary encoding each time this
* method is called.
*/
public byte[] getEncoded() {
return Base64.getMimeDecoder().decode(pem);
} }
/** /**

View File

@ -565,7 +565,7 @@ public class X509Factory extends CertificateFactorySpi {
} catch (EOFException e) { } catch (EOFException e) {
return null; return null;
} }
return Base64.getDecoder().decode(rec.pem()); return Base64.getDecoder().decode(rec.content());
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new IOException(e); throw new IOException(e);
} }

View File

@ -343,7 +343,7 @@ public class Pem {
* @return PEM in a string * @return PEM in a string
*/ */
public static String pemEncoded(PEMRecord pem) { public static String pemEncoded(PEMRecord pem) {
String p = pem.pem().replaceAll("(.{64})", "$1\r\n"); String p = pem.content().replaceAll("(.{64})", "$1\r\n");
return pemEncoded(pem.type(), p); return pemEncoded(pem.type(), p);
} }
} }

View File

@ -107,8 +107,8 @@ public class PEMDecoderTest {
System.err.println("received: " + new String(rec.leadingData())); System.err.println("received: " + new String(rec.leadingData()));
throw new AssertionError("ecCSRWithData preData wrong"); throw new AssertionError("ecCSRWithData preData wrong");
} }
if (rec.pem().lastIndexOf("F") > rec.pem().length() - 5) { if (rec.content().lastIndexOf("F") > rec.content().length() - 5) {
System.err.println("received: " + rec.pem()); System.err.println("received: " + rec.content());
throw new AssertionError("ecCSRWithData: " + throw new AssertionError("ecCSRWithData: " +
"End of PEM data has an unexpected character"); "End of PEM data has an unexpected character");
} }
@ -235,10 +235,10 @@ public class PEMDecoderTest {
PEMRecord r = PEMDecoder.of().decode(entry.pem(), PEMRecord.class); PEMRecord r = PEMDecoder.of().decode(entry.pem(), PEMRecord.class);
String expected = entry.pem().split("-----")[2].replace(System.lineSeparator(), ""); String expected = entry.pem().split("-----")[2].replace(System.lineSeparator(), "");
try { try {
PEMData.checkResults(expected, r.pem()); PEMData.checkResults(expected, r.content());
} catch (AssertionError e) { } catch (AssertionError e) {
System.err.println("expected:\n" + expected); System.err.println("expected:\n" + expected);
System.err.println("received:\n" + r.pem()); System.err.println("received:\n" + r.content());
throw e; throw e;
} }