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}.
*
* <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
* methods are useful when extracting or changing the return class.
* 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.
* 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
* decoded into a {@code PEMRecord} by specifying {@code PEMRecord.class}.
* If the Class parameter doesn't match the PEM content, an
* {@code IllegalArgumentException} will be thrown.
* If the class parameter doesn't match the PEM content, a
* {@linkplain ClassCastException} will be thrown.
*
* <p> A new {@code PEMDecoder} instance is created when configured
* with {@linkplain #withFactory(Provider)} and/or
* {@linkplain #withDecryption(char[])}. {@linkplain #withFactory(Provider)}
* configures the decoder to use only {@linkplain KeyFactory} and
* {@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.
* Configuring an instance for decryption does not prevent decoding with
* 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
* and a factory provider:
* {@snippet lang = java:
* PEMDecoder pe = PEMDecoder.of().withDecryption(password).
* PEMDecoder pd = PEMDecoder.of().withDecryption(password).
* withFactory(provider);
* byte[] pemData = pe.decode(privKey);
* byte[] pemData = pd.decode(privKey);
* }
*
* @implNote An implementation may support other PEM types and
* {@code DEREncodables}. This implementation additionally supports PEM types:
* {@code X509 CERTIFICATE}, {@code X.509 CERTIFICATE}, {@code CRL},
* and {@code RSA PRIVATE KEY}.
* {@code DEREncodable} objects. This implementation additionally supports
* the following PEM types: {@code X509 CERTIFICATE},
* {@code X.509 CERTIFICATE}, {@code CRL}, and {@code RSA PRIVATE KEY}.
*
* @see PEMEncoder
* @see PEMRecord
@ -179,13 +179,13 @@ public final class PEMDecoder {
return switch (pem.type()) {
case Pem.PUBLIC_KEY -> {
X509EncodedKeySpec spec =
new X509EncodedKeySpec(decoder.decode(pem.pem()));
new X509EncodedKeySpec(decoder.decode(pem.content()));
yield getKeyFactory(
KeyUtil.getAlgorithm(spec.getEncoded())).
generatePublic(spec);
}
case Pem.PRIVATE_KEY -> {
PKCS8Key p8key = new PKCS8Key(decoder.decode(pem.pem()));
PKCS8Key p8key = new PKCS8Key(decoder.decode(pem.content()));
String algo = p8key.getAlgorithm();
KeyFactory kf = getKeyFactory(algo);
DEREncodable d = kf.generatePrivate(
@ -216,27 +216,27 @@ public final class PEMDecoder {
case Pem.ENCRYPTED_PRIVATE_KEY -> {
if (password == null) {
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());
}
case Pem.CERTIFICATE, Pem.X509_CERTIFICATE,
Pem.X_509_CERTIFICATE -> {
CertificateFactory cf = getCertFactory("X509");
yield (X509Certificate) cf.generateCertificate(
new ByteArrayInputStream(decoder.decode(pem.pem())));
new ByteArrayInputStream(decoder.decode(pem.content())));
}
case Pem.X509_CRL, Pem.CRL -> {
CertificateFactory cf = getCertFactory("X509");
yield (X509CRL) cf.generateCRL(
new ByteArrayInputStream(decoder.decode(pem.pem())));
new ByteArrayInputStream(decoder.decode(pem.content())));
}
case Pem.RSA_PRIVATE_KEY -> {
KeyFactory kf = getKeyFactory("RSA");
yield kf.generatePrivate(
RSAPrivateCrtKeyImpl.getKeySpec(decoder.decode(
pem.pem())));
pem.content())));
}
default -> pem;
};
@ -271,7 +271,6 @@ public final class PEMDecoder {
*/
public DEREncodable decode(String str) {
Objects.requireNonNull(str);
DEREncodable de;
try {
return decode(new ByteArrayInputStream(
str.getBytes(StandardCharsets.UTF_8)));
@ -483,9 +482,6 @@ public final class PEMDecoder {
* from the specified {@link Provider} to produce cryptographic objects.
* 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
* @return a new PEMEncoder instance configured to the {@code Provider}.
* @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.
*
* <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
* not included in the encoding. {@code PEMRecord} will not perform
* validity checks on the data.
@ -108,7 +108,8 @@ import java.util.concurrent.locks.ReentrantLock;
* 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
@ -287,7 +288,7 @@ public final class PEMEncoder {
}
// 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
// exception; therefore generation is delayed.
if (keySpec != null) {

View File

@ -29,7 +29,6 @@ import jdk.internal.javac.PreviewFeature;
import sun.security.util.Pem;
import java.util.Base64;
import java.util.Objects;
/**
@ -39,20 +38,20 @@ import java.util.Objects;
* cryptographic object is not desired or the type has no
* {@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
* during decoding. {@code leadingData} may be useful for reading metadata
* that accompanies PEM data.
*
* <p> No validation is performed during instantiation to ensure that
* {@code type} conforms to {@code RFC 7468}, that {@code pem} is valid Base64,
* or that {@code pem} matches the {@code type}. {@code leadingData} is not
* defensively copied and does not return a clone when
* {@linkplain #leadingData()} is called.
* {@code type} conforms to {@code RFC 7468}, that {@code content} is valid
* Base64, or that {@code content} matches the {@code type}.
* {@code leadingData} is not defensively copied and does not return a
* clone when {@linkplain #leadingData()} is called.
*
* @param type the type identifier in the PEM header without PEM syntax labels.
* 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.
*
* @spec https://www.rfc-editor.org/info/rfc7468
@ -64,25 +63,25 @@ import java.util.Objects;
* @since 25
*/
@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 {
/**
* Creates a {@code PEMRecord} instance with the given parameters.
*
* @param type the type identifier
* @param pem the Base64-encoded data encapsulated by the PEM header and
* footer.
* @param content the Base64-encoded data, excluding the PEM header and
* footer
* @param leadingData any non-PEM data read during the decoding process
* 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.
* @throws NullPointerException if {@code type} and/or {@code pem} are
* @throws NullPointerException if {@code type} and/or {@code content} are
* {@code null}.
*/
public PEMRecord(String type, String pem, byte[] leadingData) {
public PEMRecord {
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
// 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");
}
this.type = type;
this.pem = pem;
this.leadingData = leadingData;
}
/**
* 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 pem the Base64-encoded data encapsulated by the PEM header and
* footer.
* @throws IllegalArgumentException if the {@code type} is incorrectly
* @param content the Base64-encoded data, excluding the PEM header and
* footer
* @throws IllegalArgumentException if {@code type} is incorrectly
* formatted.
* @throws NullPointerException if {@code type} and/or {@code pem} are
* @throws NullPointerException if {@code type} and/or {@code content} are
* {@code null}.
*/
public PEMRecord(String type, String pem) {
this(type, pem, 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);
public PEMRecord(String type, String content) {
this(type, content, null);
}
/**

View File

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

View File

@ -343,7 +343,7 @@ public class Pem {
* @return PEM in a string
*/
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);
}
}

View File

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