diff --git a/src/java.base/share/classes/java/security/AsymmetricKey.java b/src/java.base/share/classes/java/security/AsymmetricKey.java index e96aeb4d84c71..d37afe9bfea89 100644 --- a/src/java.base/share/classes/java/security/AsymmetricKey.java +++ b/src/java.base/share/classes/java/security/AsymmetricKey.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,7 +34,7 @@ * * @since 22 */ -public interface AsymmetricKey extends Key { +public non-sealed interface AsymmetricKey extends Key, DEREncodable { /** * Returns the parameters associated with this key. * The parameters are optional and may be either diff --git a/src/java.base/share/classes/java/security/DEREncodable.java b/src/java.base/share/classes/java/security/DEREncodable.java new file mode 100644 index 0000000000000..63c6a73ee52b5 --- /dev/null +++ b/src/java.base/share/classes/java/security/DEREncodable.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.security; + +import jdk.internal.javac.PreviewFeature; + +import javax.crypto.EncryptedPrivateKeyInfo; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * This interface is implemented by security API classes that contain + * binary-encodable key or certificate material. + * These APIs or their subclasses typically provide methods to convert + * their instances to and from byte arrays in the Distinguished + * Encoding Rules (DER) format. + * + * @see AsymmetricKey + * @see KeyPair + * @see PKCS8EncodedKeySpec + * @see X509EncodedKeySpec + * @see EncryptedPrivateKeyInfo + * @see X509Certificate + * @see X509CRL + * @see PEMRecord + * + * @since 25 + */ + +@PreviewFeature(feature = PreviewFeature.Feature.PEM_API) +public sealed interface DEREncodable permits AsymmetricKey, KeyPair, + PKCS8EncodedKeySpec, X509EncodedKeySpec, EncryptedPrivateKeyInfo, + X509Certificate, X509CRL, PEMRecord { +} diff --git a/src/java.base/share/classes/java/security/KeyPair.java b/src/java.base/share/classes/java/security/KeyPair.java index cc648a677dd5a..39c98501fea9d 100644 --- a/src/java.base/share/classes/java/security/KeyPair.java +++ b/src/java.base/share/classes/java/security/KeyPair.java @@ -37,7 +37,7 @@ * @since 1.1 */ -public final class KeyPair implements java.io.Serializable { +public final class KeyPair implements java.io.Serializable, DEREncodable { @java.io.Serial private static final long serialVersionUID = -7565189502268009837L; diff --git a/src/java.base/share/classes/java/security/PEMDecoder.java b/src/java.base/share/classes/java/security/PEMDecoder.java new file mode 100644 index 0000000000000..eeb6decdf4078 --- /dev/null +++ b/src/java.base/share/classes/java/security/PEMDecoder.java @@ -0,0 +1,512 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.security; + +import jdk.internal.javac.PreviewFeature; + +import sun.security.pkcs.PKCS8Key; +import sun.security.rsa.RSAPrivateCrtKeyImpl; +import sun.security.util.KeyUtil; +import sun.security.util.Pem; + +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.spec.PBEKeySpec; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.security.cert.*; +import java.security.spec.*; +import java.util.Base64; +import java.util.Objects; + +/** + * {@code PEMDecoder} implements a decoder for Privacy-Enhanced Mail (PEM) data. + * PEM is a textual encoding used to store and transfer security + * objects, such as asymmetric keys, certificates, and certificate revocation + * lists (CRLs). It is defined in RFC 1421 and RFC 7468. PEM consists of a + * Base64-formatted binary encoding enclosed by a type-identifying header + * and footer. + * + *

The {@linkplain #decode(String)} and {@linkplain #decode(InputStream)} + * methods return an instance of a class that matches the data + * type and implements {@link DEREncodable}. + * + *

The following lists the supported PEM types and the {@code DEREncodable} + * types that each are decoded as: + *

+ * + *

The {@code PublicKey} and {@code PrivateKey} types, an algorithm specific + * subclass is returned if the underlying algorithm is supported. For example an + * ECPublicKey and ECPrivateKey for Elliptic Curve keys. + * + *

If the PEM type does not have a corresponding class, + * {@code decode(String)} and {@code decode(InputStream)} will return a + * {@link PEMRecord}. + * + *

The {@linkplain #decode(String, Class)} and + * {@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 + * {@code PrivateKey.class} to return only the private key. + * 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. + * + *

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 + * 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 + * will throw a {@link RuntimeException}. When an encrypted private key PEM is + * used with a decoder not configured for decryption, an + * {@link EncryptedPrivateKeyInfo} object is returned. + * + *

This class is immutable and thread-safe. + * + *

Here is an example of decoding a {@code PrivateKey} object: + * {@snippet lang = java: + * PEMDecoder pd = PEMDecoder.of(); + * PrivateKey priKey = pd.decode(priKeyPEM, PrivateKey.class); + * } + * + *

Here is an example of a {@code PEMDecoder} configured with decryption + * and a factory provider: + * {@snippet lang = java: + * PEMDecoder pe = PEMDecoder.of().withDecryption(password). + * withFactory(provider); + * byte[] pemData = pe.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}. + * + * @see PEMEncoder + * @see PEMRecord + * @see EncryptedPrivateKeyInfo + * + * @spec https://www.rfc-editor.org/info/rfc1421 + * RFC 1421: Privacy Enhancement for Internet Electronic Mail + * @spec https://www.rfc-editor.org/info/rfc7468 + * RFC 7468: Textual Encodings of PKIX, PKCS, and CMS Structures + * + * @since 25 + */ + +@PreviewFeature(feature = PreviewFeature.Feature.PEM_API) +public final class PEMDecoder { + private final Provider factory; + private final PBEKeySpec password; + + // Singleton instance for PEMDecoder + private final static PEMDecoder PEM_DECODER = new PEMDecoder(null, null); + + /** + * Creates an instance with a specific KeyFactory and/or password. + * @param withFactory KeyFactory provider + * @param withPassword char[] password for EncryptedPrivateKeyInfo + * decryption + */ + private PEMDecoder(Provider withFactory, PBEKeySpec withPassword) { + password = withPassword; + factory = withFactory; + } + + /** + * Returns an instance of {@code PEMDecoder}. + * + * @return a {@code PEMDecoder} instance + */ + public static PEMDecoder of() { + return PEM_DECODER; + } + + /** + * After the header, footer, and base64 have been separated, identify the + * header and footer and proceed with decoding the base64 for the + * appropriate type. + */ + private DEREncodable decode(PEMRecord pem) { + Base64.Decoder decoder = Base64.getMimeDecoder(); + + try { + return switch (pem.type()) { + case Pem.PUBLIC_KEY -> { + X509EncodedKeySpec spec = + new X509EncodedKeySpec(decoder.decode(pem.pem())); + yield getKeyFactory( + KeyUtil.getAlgorithm(spec.getEncoded())). + generatePublic(spec); + } + case Pem.PRIVATE_KEY -> { + PKCS8Key p8key = new PKCS8Key(decoder.decode(pem.pem())); + String algo = p8key.getAlgorithm(); + KeyFactory kf = getKeyFactory(algo); + DEREncodable d = kf.generatePrivate( + new PKCS8EncodedKeySpec(p8key.getEncoded(), algo)); + + // Look for a public key inside the pkcs8 encoding. + if (p8key.getPubKeyEncoded() != null) { + // Check if this is a OneAsymmetricKey encoding + X509EncodedKeySpec spec = new X509EncodedKeySpec( + p8key.getPubKeyEncoded(), algo); + yield new KeyPair(getKeyFactory(algo). + generatePublic(spec), (PrivateKey) d); + + } else if (d instanceof PKCS8Key p8 && + p8.getPubKeyEncoded() != null) { + // If the KeyFactory decoded an algorithm-specific + // encodings, look for the public key again. This + // happens with EC and SEC1-v2 encoding + X509EncodedKeySpec spec = new X509EncodedKeySpec( + p8.getPubKeyEncoded(), algo); + yield new KeyPair(getKeyFactory(algo). + generatePublic(spec), p8); + } else { + // No public key, return the private key. + yield d; + } + } + case Pem.ENCRYPTED_PRIVATE_KEY -> { + if (password == null) { + yield new EncryptedPrivateKeyInfo(decoder.decode( + pem.pem())); + } + yield new EncryptedPrivateKeyInfo(decoder.decode(pem.pem())). + 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()))); + } + case Pem.X509_CRL, Pem.CRL -> { + CertificateFactory cf = getCertFactory("X509"); + yield (X509CRL) cf.generateCRL( + new ByteArrayInputStream(decoder.decode(pem.pem()))); + } + case Pem.RSA_PRIVATE_KEY -> { + KeyFactory kf = getKeyFactory("RSA"); + yield kf.generatePrivate( + RSAPrivateCrtKeyImpl.getKeySpec(decoder.decode( + pem.pem()))); + } + default -> pem; + }; + } catch (GeneralSecurityException | IOException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Decodes and returns a {@link DEREncodable} from the given {@code String}. + * + *

This method reads the {@code String} until PEM data is found + * or the end of the {@code String} is reached. If no PEM data is found, + * an {@code IllegalArgumentException} is thrown. + * + *

This method returns a Java API cryptographic object, + * such as a {@code PrivateKey}, if the PEM type is supported. + * Any non-PEM data preceding the PEM header is ignored by the decoder. + * Otherwise, a {@link PEMRecord} will be returned containing + * the type identifier and Base64-encoded data. + * Any non-PEM data preceding the PEM header will be stored in + * {@code leadingData}. + * + *

Input consumed by this method is read in as + * {@link java.nio.charset.StandardCharsets#UTF_8 UTF-8}. + * + * @param str a String containing PEM data + * @return a {@code DEREncodable} + * @throws IllegalArgumentException on error in decoding or no PEM data + * found + * @throws NullPointerException when {@code str} is null + */ + public DEREncodable decode(String str) { + Objects.requireNonNull(str); + DEREncodable de; + try { + return decode(new ByteArrayInputStream( + str.getBytes(StandardCharsets.UTF_8))); + } catch (IOException e) { + // With all data contained in the String, there are no IO ops. + throw new IllegalArgumentException(e); + } + } + + /** + * Decodes and returns a {@link DEREncodable} from the given + * {@code InputStream}. + * + *

This method reads from the {@code InputStream} until the end of + * the PEM footer or the end of the stream. If an I/O error occurs, + * the read position in the stream may become inconsistent. + * It is recommended to perform no further decoding operations + * on the {@code InputStream}. + * + *

This method returns a Java API cryptographic object, + * such as a {@code PrivateKey}, if the PEM type is supported. + * Any non-PEM data preceding the PEM header is ignored by the decoder. + * Otherwise, a {@link PEMRecord} will be returned containing + * the type identifier and Base64-encoded data. + * Any non-PEM data preceding the PEM header will be stored in + * {@code leadingData}. + * + *

If no PEM data is found, an {@code IllegalArgumentException} is + * thrown. + * + * @param is InputStream containing PEM data + * @return a {@code DEREncodable} + * @throws IOException on IO or PEM syntax error where the + * {@code InputStream} did not complete decoding. + * @throws EOFException at the end of the {@code InputStream} + * @throws IllegalArgumentException on error in decoding + * @throws NullPointerException when {@code is} is null + */ + public DEREncodable decode(InputStream is) throws IOException { + Objects.requireNonNull(is); + PEMRecord pem = Pem.readPEM(is); + return decode(pem); + } + + /** + * Decodes and returns a {@code DEREncodable} of the specified class from + * the given PEM string. {@code tClass} must extend {@link DEREncodable} + * and be an appropriate class for the PEM type. + * + *

This method reads the {@code String} until PEM data is found + * or the end of the {@code String} is reached. If no PEM data is found, + * an {@code IllegalArgumentException} is thrown. + * + *

If the class parameter is {@code PEMRecord.class}, + * a {@linkplain PEMRecord} is returned containing the + * type identifier and Base64 encoding. Any non-PEM data preceding + * the PEM header will be stored in {@code leadingData}. Other + * class parameters will not return preceding non-PEM data. + * + *

Input consumed by this method is read in as + * {@link java.nio.charset.StandardCharsets#UTF_8 UTF-8}. + * + * @param Class type parameter that extends {@code DEREncodable} + * @param str the String containing PEM data + * @param tClass the returned object class that implements + * {@code DEREncodable} + * @return a {@code DEREncodable} specified by {@code tClass} + * @throws IllegalArgumentException on error in decoding or no PEM data + * found + * @throws ClassCastException if {@code tClass} is invalid for the PEM type + * @throws NullPointerException when any input values are null + */ + public S decode(String str, Class tClass) { + Objects.requireNonNull(str); + try { + return decode(new ByteArrayInputStream( + str.getBytes(StandardCharsets.UTF_8)), tClass); + } catch (IOException e) { + // With all data contained in the String, there are no IO ops. + throw new IllegalArgumentException(e); + } + } + + /** + * Decodes and returns the specified class for the given + * {@link InputStream}. The class must extend {@link DEREncodable} and be + * an appropriate class for the PEM type. + * + *

This method reads from the {@code InputStream} until the end of + * the PEM footer or the end of the stream. If an I/O error occurs, + * the read position in the stream may become inconsistent. + * It is recommended to perform no further decoding operations + * on the {@code InputStream}. + * + *

If the class parameter is {@code PEMRecord.class}, + * a {@linkplain PEMRecord} is returned containing the + * type identifier and Base64 encoding. Any non-PEM data preceding + * the PEM header will be stored in {@code leadingData}. Other + * class parameters will not return preceding non-PEM data. + * + *

If no PEM data is found, an {@code IllegalArgumentException} is + * thrown. + * + * @param Class type parameter that extends {@code DEREncodable}. + * @param is an InputStream containing PEM data + * @param tClass the returned object class that implements + * {@code DEREncodable}. + * @return a {@code DEREncodable} typecast to {@code tClass} + * @throws IOException on IO or PEM syntax error where the + * {@code InputStream} did not complete decoding. + * @throws EOFException at the end of the {@code InputStream} + * @throws IllegalArgumentException on error in decoding + * @throws ClassCastException if {@code tClass} is invalid for the PEM type + * @throws NullPointerException when any input values are null + * + * @see #decode(InputStream) + * @see #decode(String, Class) + */ + public S decode(InputStream is, Class tClass) + throws IOException { + Objects.requireNonNull(is); + Objects.requireNonNull(tClass); + PEMRecord pem = Pem.readPEM(is); + + if (tClass.isAssignableFrom(PEMRecord.class)) { + return tClass.cast(pem); + } + DEREncodable so = decode(pem); + + /* + * If the object is a KeyPair, check if the tClass is set to class + * specific to a private or public key. Because PKCS8v2 can be a + * KeyPair, it is possible for someone to assume all their PEM private + * keys are only PrivateKey and not KeyPair. + */ + if (so instanceof KeyPair kp) { + if ((PrivateKey.class).isAssignableFrom(tClass) || + (PKCS8EncodedKeySpec.class).isAssignableFrom(tClass)) { + so = kp.getPrivate(); + } + if ((PublicKey.class).isAssignableFrom(tClass) || + (X509EncodedKeySpec.class).isAssignableFrom(tClass)) { + so = kp.getPublic(); + } + } + + /* + * KeySpec use getKeySpec after the Key has been generated. Even though + * returning a binary encoding after the Base64 decoding is ok when the + * user wants PKCS8EncodedKeySpec, generating the key verifies the + * binary encoding and allows the KeyFactory to use the provider's + * KeySpec() + */ + + if ((EncodedKeySpec.class).isAssignableFrom(tClass) && + so instanceof Key key) { + try { + // unchecked suppressed as we know tClass comes from KeySpec + // KeyType not relevant here. We just want KeyFactory + if ((PKCS8EncodedKeySpec.class).isAssignableFrom(tClass)) { + so = getKeyFactory(key.getAlgorithm()). + getKeySpec(key, PKCS8EncodedKeySpec.class); + } else if ((X509EncodedKeySpec.class).isAssignableFrom(tClass)) { + so = getKeyFactory(key.getAlgorithm()) + .getKeySpec(key, X509EncodedKeySpec.class); + } else { + throw new IllegalArgumentException("Invalid KeySpec."); + } + } catch (InvalidKeySpecException e) { + throw new IllegalArgumentException("Invalid KeySpec " + + "specified (" + tClass.getName() +") for key (" + + key.getClass().getName() +")", e); + } + } + + return tClass.cast(so); + } + + private KeyFactory getKeyFactory(String algorithm) { + if (algorithm == null || algorithm.isEmpty()) { + throw new IllegalArgumentException("No algorithm found in " + + "the encoding"); + } + try { + if (factory == null) { + return KeyFactory.getInstance(algorithm); + } + return KeyFactory.getInstance(algorithm, factory); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException(e); + } + } + + // Convenience method to avoid provider getInstance checks clutter + private CertificateFactory getCertFactory(String algorithm) { + try { + if (factory == null) { + return CertificateFactory.getInstance(algorithm); + } + return CertificateFactory.getInstance(algorithm, factory); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Returns a copy of this {@code PEMDecoder} instance that uses + * {@link KeyFactory} and {@link CertificateFactory} implementations + * from the specified {@link Provider} to produce cryptographic objects. + * Any errors using the {@code Provider} will occur during decoding. + * + *

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 + */ + public PEMDecoder withFactory(Provider provider) { + Objects.requireNonNull(provider); + return new PEMDecoder(provider, password); + } + + /** + * Returns a copy of this {@code PEMDecoder} that decodes and decrypts + * encrypted private keys using the specified password. + * Non-encrypted PEM can still be decoded from this instance. + * + * @param password the password to decrypt encrypted PEM data. This array + * is cloned and stored in the new instance. + * @return a new PEMEncoder instance configured for decryption + * @throws NullPointerException if {@code password} is null + */ + public PEMDecoder withDecryption(char[] password) { + Objects.requireNonNull(password); + return new PEMDecoder(factory, new PBEKeySpec(password)); + } +} diff --git a/src/java.base/share/classes/java/security/PEMEncoder.java b/src/java.base/share/classes/java/security/PEMEncoder.java new file mode 100644 index 0000000000000..d54650833305c --- /dev/null +++ b/src/java.base/share/classes/java/security/PEMEncoder.java @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.security; + +import jdk.internal.javac.PreviewFeature; +import sun.security.pkcs.PKCS8Key; +import sun.security.util.DerOutputStream; +import sun.security.util.DerValue; +import sun.security.util.Pem; +import sun.security.x509.AlgorithmId; + +import javax.crypto.*; +import javax.crypto.spec.PBEKeySpec; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.cert.*; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +/** + * {@code PEMEncoder} implements an encoder for Privacy-Enhanced Mail (PEM) + * data. PEM is a textual encoding used to store and transfer security + * objects, such as asymmetric keys, certificates, and certificate revocation + * lists (CRL). It is defined in RFC 1421 and RFC 7468. PEM consists of a + * Base64-formatted binary encoding enclosed by a type-identifying header + * and footer. + * + *

Encoding may be performed on Java API cryptographic objects that + * implement {@link DEREncodable}. The {@link #encode(DEREncodable)} + * and {@link #encodeToString(DEREncodable)} methods encode a DEREncodable + * into PEM and return the data in a byte array or String. + * + *

Private keys can be encrypted and encoded by configuring a + * {@code PEMEncoder} with the {@linkplain #withEncryption(char[])} method, + * which takes a password and returns a new {@code PEMEncoder} instance + * configured to encrypt the key with that password. Alternatively, a + * private key encrypted as an {@code EncryptedKeyInfo} object can be encoded + * directly to PEM by passing it to the {@code encode} or + * {@code encodeToString} methods. + * + *

PKCS #8 2.0 defines the ASN.1 OneAsymmetricKey structure, which may + * contain both private and public keys. + * {@link KeyPair} objects passed to the {@code encode} or + * {@code encodeToString} methods are encoded as a + * OneAsymmetricKey structure using the "PRIVATE KEY" type. + * + *

When encoding a {@link PEMRecord}, the API surrounds the + * {@linkplain PEMRecord#pem()} 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. + * + *

The following lists the supported {@code DEREncodable} classes and + * the PEM types that each are encoded as: + * + *

+ * + *

This class is immutable and thread-safe. + * + *

Here is an example of encoding a {@code PrivateKey} object: + * {@snippet lang = java: + * PEMEncoder pe = PEMEncoder.of(); + * byte[] pemData = pe.encode(privKey); + * } + * + *

Here is an example that encrypts and encodes a private key using the + * specified password: + * {@snippet lang = java: + * PEMEncoder pe = PEMEncoder.of().withEncryption(password); + * byte[] pemData = pe.encode(privKey); + * } + * + * @implNote An implementation may support other PEM types and DEREncodables. + * + * + * @see PEMDecoder + * @see PEMRecord + * @see EncryptedPrivateKeyInfo + * + * @spec https://www.rfc-editor.org/info/rfc1421 + * RFC 1421: Privacy Enhancement for Internet Electronic Mail + * @spec https://www.rfc-editor.org/info/rfc7468 + * RFC 7468: Textual Encodings of PKIX, PKCS, and CMS Structures + * + * @since 25 + */ +@PreviewFeature(feature = PreviewFeature.Feature.PEM_API) +public final class PEMEncoder { + + // Singleton instance of PEMEncoder + private static final PEMEncoder PEM_ENCODER = new PEMEncoder(null); + + // Stores the password for an encrypted encoder that isn't setup yet. + private PBEKeySpec keySpec; + // Stores the key after the encoder is ready to encrypt. The prevents + // repeated SecretKeyFactory calls if the encoder is used on multiple keys. + private SecretKey key; + // Makes SecretKeyFactory generation thread-safe. + private final ReentrantLock lock; + + /** + * Instantiate a {@code PEMEncoder} for Encrypted Private Keys. + * + * @param pbe contains the password spec used for encryption. + */ + private PEMEncoder(PBEKeySpec pbe) { + keySpec = pbe; + key = null; + lock = new ReentrantLock(); + } + + /** + * Returns an instance of {@code PEMEncoder}. + * + * @return a {@code PEMEncoder} + */ + public static PEMEncoder of() { + return PEM_ENCODER; + } + + /** + * Encodes the specified {@code DEREncodable} and returns a PEM encoded + * string. + * + * @param de the {@code DEREncodable} to be encoded + * @return a {@code String} containing the PEM encoded data + * @throws IllegalArgumentException if the {@code DEREncodable} cannot be + * encoded + * @throws NullPointerException if {@code de} is {@code null} + * @see #withEncryption(char[]) + */ + public String encodeToString(DEREncodable de) { + Objects.requireNonNull(de); + return switch (de) { + case PublicKey pu -> buildKey(null, pu.getEncoded()); + case PrivateKey pr -> buildKey(pr.getEncoded(), null); + case KeyPair kp -> { + if (kp.getPublic() == null) { + throw new IllegalArgumentException("KeyPair does not " + + "contain PublicKey."); + } + if (kp.getPrivate() == null) { + throw new IllegalArgumentException("KeyPair does not " + + "contain PrivateKey."); + } + yield buildKey(kp.getPrivate().getEncoded(), + kp.getPublic().getEncoded()); + } + case X509EncodedKeySpec x -> + buildKey(null, x.getEncoded()); + case PKCS8EncodedKeySpec p -> + buildKey(p.getEncoded(), null); + case EncryptedPrivateKeyInfo epki -> { + try { + yield Pem.pemEncoded(Pem.ENCRYPTED_PRIVATE_KEY, + epki.getEncoded()); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + case X509Certificate c -> { + try { + if (isEncrypted()) { + throw new IllegalArgumentException("Certificates " + + "cannot be encrypted"); + } + yield Pem.pemEncoded(Pem.CERTIFICATE, c.getEncoded()); + } catch (CertificateEncodingException e) { + throw new IllegalArgumentException(e); + } + } + case X509CRL crl -> { + try { + if (isEncrypted()) { + throw new IllegalArgumentException("CRLs cannot be " + + "encrypted"); + } + yield Pem.pemEncoded(Pem.X509_CRL, crl.getEncoded()); + } catch (CRLException e) { + throw new IllegalArgumentException(e); + } + } + case PEMRecord rec -> { + if (isEncrypted()) { + throw new IllegalArgumentException("PEMRecord cannot be " + + "encrypted"); + } + yield Pem.pemEncoded(rec); + } + + default -> throw new IllegalArgumentException("PEM does not " + + "support " + de.getClass().getCanonicalName()); + }; + } + + /** + * Encodes the specified {@code DEREncodable} and returns the PEM encoding + * in a byte array. + * + * @param de the {@code DEREncodable} to be encoded + * @return a PEM encoded byte array + * @throws IllegalArgumentException if the {@code DEREncodable} cannot be + * encoded + * @throws NullPointerException if {@code de} is {@code null} + * @see #withEncryption(char[]) + */ + public byte[] encode(DEREncodable de) { + return encodeToString(de).getBytes(StandardCharsets.ISO_8859_1); + } + + /** + * Returns a new {@code PEMEncoder} instance configured for encryption + * with the default algorithm and a given password. + * + *

Only {@link PrivateKey} objects can be encrypted with this newly + * configured instance. Encoding other {@link DEREncodable} objects will + * throw an {@code IllegalArgumentException}. + * + * @implNote + * The default password-based encryption algorithm is defined + * by the {@code jdk.epkcs8.defaultAlgorithm} security property and + * uses the default encryption parameters of the provider that is selected. + * For greater flexibility with encryption options and parameters, use + * {@link EncryptedPrivateKeyInfo#encryptKey(PrivateKey, Key, + * String, AlgorithmParameterSpec, Provider, SecureRandom)} and use the + * returned object with {@link #encode(DEREncodable)}. + * + * @param password the encryption password. The array is cloned and + * stored in the new instance. + * @return a new {@code PEMEncoder} instance configured for encryption + * @throws NullPointerException when password is {@code null} + */ + public PEMEncoder withEncryption(char[] password) { + // PBEKeySpec clones the password + Objects.requireNonNull(password, "password cannot be null."); + return new PEMEncoder(new PBEKeySpec(password)); + } + + /** + * Build PEM encoding. + */ + private String buildKey(byte[] privateBytes, byte[] publicBytes) { + DerOutputStream out = new DerOutputStream(); + Cipher cipher; + + if (privateBytes == null && publicBytes == null) { + throw new IllegalArgumentException("No encoded data given by the " + + "DEREncodable."); + } + + // If `keySpec` is non-null, then `key` hasn't been established. + // Setting a `key' prevents repeated key generations operations. + // withEncryption() is a configuration method and cannot throw an + // exception; therefore generation is delayed. + if (keySpec != null) { + // For thread safety + lock.lock(); + if (key == null) { + try { + key = SecretKeyFactory.getInstance(Pem.DEFAULT_ALGO). + generateSecret(keySpec); + keySpec.clearPassword(); + keySpec = null; + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Security property " + + "\"jdk.epkcs8.defaultAlgorithm\" may not specify a " + + "valid algorithm. Operation cannot be performed.", e); + } finally { + lock.unlock(); + } + } else { + lock.unlock(); + } + } + + // If `key` is non-null, this is an encoder ready to encrypt. + if (key != null) { + if (privateBytes == null || publicBytes != null) { + throw new IllegalArgumentException("Can only encrypt a " + + "PrivateKey."); + } + + try { + cipher = Cipher.getInstance(Pem.DEFAULT_ALGO); + cipher.init(Cipher.ENCRYPT_MODE, key); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Security property " + + "\"jdk.epkcs8.defaultAlgorithm\" may not specify a " + + "valid algorithm. Operation cannot be performed.", e); + } + + try { + new AlgorithmId(Pem.getPBEID(Pem.DEFAULT_ALGO), + cipher.getParameters()).encode(out); + out.putOctetString(cipher.doFinal(privateBytes)); + return Pem.pemEncoded(Pem.ENCRYPTED_PRIVATE_KEY, + DerValue.wrap(DerValue.tag_Sequence, out).toByteArray()); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException(e); + } + } + + // X509 only + if (publicBytes != null && privateBytes == null) { + if (publicBytes.length == 0) { + throw new IllegalArgumentException("No public key encoding " + + "given by the DEREncodable."); + } + + return Pem.pemEncoded(Pem.PUBLIC_KEY, publicBytes); + } + + // PKCS8 only + if (publicBytes == null && privateBytes != null) { + if (privateBytes.length == 0) { + throw new IllegalArgumentException("No private key encoding " + + "given by the DEREncodable."); + } + + return Pem.pemEncoded(Pem.PRIVATE_KEY, privateBytes); + } + + // OneAsymmetricKey + if (privateBytes.length == 0) { + throw new IllegalArgumentException("No private key encoding " + + "given by the DEREncodable."); + } + + if (publicBytes.length == 0) { + throw new IllegalArgumentException("No public key encoding " + + "given by the DEREncodable."); + } + try { + return Pem.pemEncoded(Pem.PRIVATE_KEY, + PKCS8Key.getEncoded(publicBytes, privateBytes)); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + private boolean isEncrypted() { + return (key != null || keySpec != null); + } +} diff --git a/src/java.base/share/classes/java/security/PEMRecord.java b/src/java.base/share/classes/java/security/PEMRecord.java new file mode 100644 index 0000000000000..dfe951a09637f --- /dev/null +++ b/src/java.base/share/classes/java/security/PEMRecord.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.security; + +import jdk.internal.javac.PreviewFeature; + +import sun.security.util.Pem; + +import java.util.Base64; +import java.util.Objects; + +/** + * {@code PEMRecord} is a {@link DEREncodable} that represents Privacy-Enhanced + * Mail (PEM) data by its type and Base64 form. {@link PEMDecoder} and + * {@link PEMEncoder} use {@code PEMRecord} when representing the data as a + * cryptographic object is not desired or the type has no + * {@code DEREncodable}. + * + *

{@code type} and {@code pem} 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. + * + *

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. + * + * @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 leadingData any non-PEM data preceding the PEM header when decoding. + * + * @spec https://www.rfc-editor.org/info/rfc7468 + * RFC 7468: Textual Encodings of PKIX, PKCS, and CMS Structures + * + * @see PEMDecoder + * @see PEMEncoder + * + * @since 25 + */ +@PreviewFeature(feature = PreviewFeature.Feature.PEM_API) +public record PEMRecord(String type, String pem, 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 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 + * formatted. + * @throws NullPointerException if {@code type} and/or {@code pem} are + * {@code null}. + */ + public PEMRecord(String type, String pem, byte[] leadingData) { + Objects.requireNonNull(type, "\"type\" cannot be null."); + Objects.requireNonNull(pem, "\"pem\" cannot be null."); + + // With no validity checking on `type`, the constructor accept anything + // including lowercase. The onus is on the caller. + if (type.startsWith("-") || type.startsWith("BEGIN ") || + type.startsWith("END ")) { + throw new IllegalArgumentException("PEM syntax labels found. " + + "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. + * + * @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 + * formatted. + * @throws NullPointerException if {@code type} and/or {@code pem} 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); + } + + /** + * Returns the type and Base64 encoding in PEM format. {@code leadingData} + * is not returned by this method. + */ + @Override + public String toString() { + return Pem.pemEncoded(this); + } +} diff --git a/src/java.base/share/classes/java/security/cert/X509CRL.java b/src/java.base/share/classes/java/security/cert/X509CRL.java index 111de1daf0e43..d19618f81ed41 100644 --- a/src/java.base/share/classes/java/security/cert/X509CRL.java +++ b/src/java.base/share/classes/java/security/cert/X509CRL.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -107,7 +107,7 @@ * @see X509Extension */ -public abstract class X509CRL extends CRL implements X509Extension { +public abstract non-sealed class X509CRL extends CRL implements X509Extension, DEREncodable { private transient X500Principal issuerPrincipal; diff --git a/src/java.base/share/classes/java/security/cert/X509Certificate.java b/src/java.base/share/classes/java/security/cert/X509Certificate.java index 4e579a75b1c0d..fe4a472dead1a 100644 --- a/src/java.base/share/classes/java/security/cert/X509Certificate.java +++ b/src/java.base/share/classes/java/security/cert/X509Certificate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -107,8 +107,8 @@ * @see X509Extension */ -public abstract class X509Certificate extends Certificate -implements X509Extension { +public abstract non-sealed class X509Certificate extends Certificate + implements X509Extension, DEREncodable { @java.io.Serial private static final long serialVersionUID = -2491127588187038216L; diff --git a/src/java.base/share/classes/java/security/spec/PKCS8EncodedKeySpec.java b/src/java.base/share/classes/java/security/spec/PKCS8EncodedKeySpec.java index af7f135d7c43f..917b15e06b660 100644 --- a/src/java.base/share/classes/java/security/spec/PKCS8EncodedKeySpec.java +++ b/src/java.base/share/classes/java/security/spec/PKCS8EncodedKeySpec.java @@ -25,25 +25,35 @@ package java.security.spec; +import java.security.DEREncodable; + /** * This class represents the ASN.1 encoding of a private key, - * encoded according to the ASN.1 type {@code PrivateKeyInfo}. - * The {@code PrivateKeyInfo} syntax is defined in the PKCS#8 standard + * encoded according to the ASN.1 type {@code OneAsymmetricKey}. + * The {@code OneAsymmetricKey} syntax is defined in the PKCS#8 standard * as follows: * *

- * PrivateKeyInfo ::= SEQUENCE {
+ * OneAsymmetricKey ::= SEQUENCE {
  *   version Version,
  *   privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
  *   privateKey PrivateKey,
- *   attributes [0] IMPLICIT Attributes OPTIONAL }
+ *   attributes       [0] Attributes OPTIONAL,
+ *   ...,
+ *   [[2: publicKey  [1] PublicKey OPTIONAL ]],
+ *   ...
+ * }
+ *
+ * PrivateKeyInfo ::= OneAsymmetricKey
  *
- * Version ::= INTEGER
+ * Version ::= INTEGER { v1(0), v2(1) }
  *
  * PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
  *
  * PrivateKey ::= OCTET STRING
  *
+ * PublicKey ::= BIT STRING
+ *
  * Attributes ::= SET OF Attribute
  * 
* @@ -56,11 +66,14 @@ * @see EncodedKeySpec * @see X509EncodedKeySpec * + * @spec https://www.rfc-editor.org/info/rfc5958 + * RFC 5958: Asymmetric Key Packages + * * @since 1.2 */ -public class PKCS8EncodedKeySpec extends EncodedKeySpec { - +public non-sealed class PKCS8EncodedKeySpec extends EncodedKeySpec implements + DEREncodable { /** * Creates a new {@code PKCS8EncodedKeySpec} with the given encoded key. * diff --git a/src/java.base/share/classes/java/security/spec/X509EncodedKeySpec.java b/src/java.base/share/classes/java/security/spec/X509EncodedKeySpec.java index a405104cd074e..e8b6e5996769f 100644 --- a/src/java.base/share/classes/java/security/spec/X509EncodedKeySpec.java +++ b/src/java.base/share/classes/java/security/spec/X509EncodedKeySpec.java @@ -25,6 +25,8 @@ package java.security.spec; +import java.security.DEREncodable; + /** * This class represents the ASN.1 encoding of a public key, * encoded according to the ASN.1 type {@code SubjectPublicKeyInfo}. @@ -49,8 +51,8 @@ * @since 1.2 */ -public class X509EncodedKeySpec extends EncodedKeySpec { - +public non-sealed class X509EncodedKeySpec extends EncodedKeySpec implements + DEREncodable { /** * Creates a new {@code X509EncodedKeySpec} with the given encoded key. * diff --git a/src/java.base/share/classes/javax/crypto/EncryptedPrivateKeyInfo.java b/src/java.base/share/classes/javax/crypto/EncryptedPrivateKeyInfo.java index 1e4e769a83ee0..90316d7437e04 100644 --- a/src/java.base/share/classes/javax/crypto/EncryptedPrivateKeyInfo.java +++ b/src/java.base/share/classes/javax/crypto/EncryptedPrivateKeyInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,13 +25,18 @@ package javax.crypto; -import java.io.*; +import jdk.internal.javac.PreviewFeature; + +import sun.security.jca.JCAUtil; +import sun.security.pkcs.PKCS8Key; +import sun.security.util.*; +import sun.security.x509.AlgorithmId; + +import javax.crypto.spec.PBEKeySpec; +import java.io.IOException; import java.security.*; import java.security.spec.*; -import sun.security.x509.AlgorithmId; -import sun.security.util.DerValue; -import sun.security.util.DerInputStream; -import sun.security.util.DerOutputStream; +import java.util.Objects; /** * This class implements the {@code EncryptedPrivateKeyInfo} type @@ -55,14 +60,14 @@ * @since 1.4 */ -public class EncryptedPrivateKeyInfo { +public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { // The "encryptionAlgorithm" is stored in either the algid or // the params field. Precisely, if this object is created by // {@link #EncryptedPrivateKeyInfo(AlgorithmParameters, byte[])} // with an uninitialized AlgorithmParameters, the AlgorithmParameters // object is stored in the params field and algid is set to null. - // In all other cases, algid is non null and params is null. + // In all other cases, algid is non-null and params is null. private final AlgorithmId algid; private final AlgorithmParameters params; @@ -73,19 +78,15 @@ public class EncryptedPrivateKeyInfo { private final byte[] encoded; /** - * Constructs (i.e., parses) an {@code EncryptedPrivateKeyInfo} from - * its ASN.1 encoding. + * Constructs an {@code EncryptedPrivateKeyInfo} from a given encrypted + * PKCS#8 ASN.1 encoding. * @param encoded the ASN.1 encoding of this object. The contents of * the array are copied to protect against subsequent modification. - * @exception NullPointerException if the {@code encoded} is - * {@code null}. - * @exception IOException if error occurs when parsing the ASN.1 encoding. + * @throws NullPointerException if {@code encoded} is {@code null}. + * @throws IOException if error occurs when parsing the ASN.1 encoding. */ public EncryptedPrivateKeyInfo(byte[] encoded) throws IOException { - if (encoded == null) { - throw new NullPointerException("the encoded parameter " + - "must be non-null"); - } + Objects.requireNonNull(encoded); this.encoded = encoded.clone(); DerValue val = DerValue.wrap(this.encoded); @@ -201,7 +202,7 @@ public EncryptedPrivateKeyInfo(AlgorithmParameters algParams, tmp = null; } - // one and only one is non null + // one and only one is non-null this.algid = tmp; this.params = this.algid != null ? null : algParams; @@ -219,6 +220,17 @@ public EncryptedPrivateKeyInfo(AlgorithmParameters algParams, this.encoded = null; } + /** + * Create an EncryptedPrivateKeyInfo object from the given components + */ + private EncryptedPrivateKeyInfo(byte[] encoded, byte[] eData, + AlgorithmId id, AlgorithmParameters p) { + this.encoded = encoded; + encryptedData = eData; + algid = id; + params = p; + } + /** * Returns the encryption algorithm. *

Note: Standard name is returned instead of the specified one @@ -308,6 +320,242 @@ private PKCS8EncodedKeySpec getKeySpecImpl(Key decryptKey, } } + /** + * Creates and encrypts an {@code EncryptedPrivateKeyInfo} from a given + * {@code PrivateKey}. A valid password-based encryption (PBE) algorithm + * and password must be specified. + * + *

The PBE algorithm string format details can be found in the + * + * Cipher section of the Java Security Standard Algorithm Names + * Specification. + * + * @param key the {@code PrivateKey} to be encrypted + * @param password the password used in the PBE encryption. This array + * will be cloned before being used. + * @param algorithm the PBE encryption algorithm. The default algorithm + * will be used if {@code null}. However, {@code null} is + * not allowed when {@code params} is non-null. + * @param params the {@code AlgorithmParameterSpec} to be used with + * encryption. The provider default will be used if + * {@code null}. + * @param provider the {@code Provider} will be used for PBE + * {@link SecretKeyFactory} generation and {@link Cipher} + * encryption operations. The default provider list will be + * used if {@code null}. + * @return an {@code EncryptedPrivateKeyInfo} + * @throws IllegalArgumentException on initialization errors based on the + * arguments passed to the method + * @throws RuntimeException on an encryption error + * @throws NullPointerException if the key or password are {@code null}. If + * {@code params} is non-null when {@code algorithm} is {@code null}. + * + * @implNote The {@code jdk.epkcs8.defaultAlgorithm} Security Property + * defines the default encryption algorithm and the + * {@code AlgorithmParameterSpec} are the provider's algorithm defaults. + * + * @since 25 + */ + @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) + public static EncryptedPrivateKeyInfo encryptKey(PrivateKey key, + char[] password, String algorithm, AlgorithmParameterSpec params, + Provider provider) { + + SecretKey skey; + Objects.requireNonNull(key, "key cannot be null"); + Objects.requireNonNull(password, "password cannot be null."); + PBEKeySpec keySpec = new PBEKeySpec(password); + if (algorithm == null) { + if (params != null) { + throw new NullPointerException("algorithm must be specified" + + " if params is non-null."); + } + algorithm = Pem.DEFAULT_ALGO; + } + + try { + SecretKeyFactory factory; + if (provider == null) { + factory = SecretKeyFactory.getInstance(algorithm); + } else { + factory = SecretKeyFactory.getInstance(algorithm, provider); + } + skey = factory.generateSecret(keySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new IllegalArgumentException(e); + } + return encryptKeyImpl(key, algorithm, skey, params, provider, null); + } + + /** + * Creates and encrypts an {@code EncryptedPrivateKeyInfo} from a given + * {@code PrivateKey} and password. Default algorithm and parameters are + * used. + * + * @param key the {@code PrivateKey} to be encrypted + * @param password the password used in the PBE encryption. This array + * will be cloned before being used. + * @return an {@code EncryptedPrivateKeyInfo} + * @throws IllegalArgumentException on initialization errors based on the + * arguments passed to the method + * @throws RuntimeException on an encryption error + * @throws NullPointerException when the {@code key} or {@code password} + * is {@code null} + * + * @implNote The {@code jdk.epkcs8.defaultAlgorithm} Security Property + * defines the default encryption algorithm and the + * {@code AlgorithmParameterSpec} are the provider's algorithm defaults. + * + * @since 25 + */ + @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) + public static EncryptedPrivateKeyInfo encryptKey(PrivateKey key, + char[] password) { + return encryptKey(key, password, Pem.DEFAULT_ALGO, null, null); + } + + /** + * Creates and encrypts an {@code EncryptedPrivateKeyInfo} from the given + * {@link PrivateKey} using the {@code encKey} and given parameters. + * + * @param key the {@code PrivateKey} to be encrypted + * @param encKey the password-based encryption (PBE) {@code Key} used to + * encrypt {@code key}. + * @param algorithm the PBE encryption algorithm. The default algorithm is + * will be used if {@code null}; however, {@code null} is + * not allowed when {@code params} is non-null. + * @param params the {@code AlgorithmParameterSpec} to be used with + * encryption. The provider list default will be used if + * {@code null}. + * @param random the {@code SecureRandom} instance used during + * encryption. The default will be used if {@code null}. + * @param provider the {@code Provider} is used for {@link Cipher} + * encryption operation. The default provider list will be + * used if {@code null}. + * @return an {@code EncryptedPrivateKeyInfo} + * @throws IllegalArgumentException on initialization errors based on the + * arguments passed to the method + * @throws RuntimeException on an encryption error + * @throws NullPointerException if the {@code key} or {@code encKey} are + * {@code null}. If {@code params} is non-null, {@code algorithm} cannot be + * {@code null}. + * + * @implNote The {@code jdk.epkcs8.defaultAlgorithm} Security Property + * defines the default encryption algorithm and the + * {@code AlgorithmParameterSpec} are the provider's algorithm defaults. + * + * @since 25 + */ + @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) + public static EncryptedPrivateKeyInfo encryptKey(PrivateKey key, Key encKey, + String algorithm, AlgorithmParameterSpec params, Provider provider, + SecureRandom random) { + + Objects.requireNonNull(key); + Objects.requireNonNull(encKey); + if (algorithm == null) { + if (params != null) { + throw new NullPointerException("algorithm must be specified " + + "if params is non-null."); + } + algorithm = Pem.DEFAULT_ALGO; + } + return encryptKeyImpl(key, algorithm, encKey, params, provider, random); + } + + private static EncryptedPrivateKeyInfo encryptKeyImpl(PrivateKey key, + String algorithm, Key encryptKey, AlgorithmParameterSpec params, + Provider provider, SecureRandom random) { + AlgorithmId algId; + byte[] encryptedData; + Cipher c; + DerOutputStream out; + + if (random == null) { + random = JCAUtil.getDefSecureRandom(); + } + try { + if (provider == null) { + c = Cipher.getInstance(algorithm); + } else { + c = Cipher.getInstance(algorithm, provider); + } + c.init(Cipher.ENCRYPT_MODE, encryptKey, params, random); + encryptedData = c.doFinal(key.getEncoded()); + algId = new AlgorithmId(Pem.getPBEID(algorithm), c.getParameters()); + out = new DerOutputStream(); + algId.encode(out); + out.putOctetString(encryptedData); + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | + NoSuchPaddingException e) { + throw new IllegalArgumentException(e); + } catch (IllegalBlockSizeException | BadPaddingException | + InvalidKeyException e) { + throw new RuntimeException(e); + } + return new EncryptedPrivateKeyInfo( + DerValue.wrap(DerValue.tag_Sequence, out).toByteArray(), + encryptedData, algId, c.getParameters()); + } + + /** + * Extract the enclosed {@code PrivateKey} object from the encrypted data + * and return it. + * + * @param password the password used in the PBE encryption. This array + * will be cloned before being used. + * @return a {@code PrivateKey} + * @throws GeneralSecurityException if an error occurs parsing or + * decrypting the encrypted data, or producing the key object. + * @throws NullPointerException if {@code password} is null + * + * @since 25 + */ + @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) + public PrivateKey getKey(char[] password) throws GeneralSecurityException { + SecretKeyFactory skf; + PKCS8EncodedKeySpec p8KeySpec; + Objects.requireNonNull(password, "password cannot be null"); + PBEKeySpec keySpec = new PBEKeySpec(password); + skf = SecretKeyFactory.getInstance(getAlgName()); + p8KeySpec = getKeySpec(skf.generateSecret(keySpec)); + + return PKCS8Key.parseKey(p8KeySpec.getEncoded()); + } + + /** + * Extract the enclosed {@code PrivateKey} object from the encrypted data + * and return it. + * + * @param decryptKey the decryption key and cannot be {@code null} + * @param provider the {@code Provider} used for Cipher decryption and + * {@code PrivateKey} generation. A {@code null} value will + * use the default provider configuration. + * @return a {@code PrivateKey} + * @throws GeneralSecurityException if an error occurs parsing or + * decrypting the encrypted data, or producing the key object. + * @throws NullPointerException if {@code decryptKey} is null + * + * @since 25 + */ + @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) + public PrivateKey getKey(Key decryptKey, Provider provider) + throws GeneralSecurityException { + Objects.requireNonNull(decryptKey,"decryptKey cannot be null."); + PKCS8EncodedKeySpec p = getKeySpecImpl(decryptKey, provider); + try { + if (provider == null) { + return KeyFactory.getInstance( + KeyUtil.getAlgorithm(p.getEncoded())). + generatePrivate(p); + } + return KeyFactory.getInstance(KeyUtil.getAlgorithm(p.getEncoded()), + provider).generatePrivate(p); + } catch (IOException e) { + throw new GeneralSecurityException(e); + } + } + /** * Extract the enclosed PKCS8EncodedKeySpec object from the * encrypted data and return it. @@ -353,12 +601,8 @@ public PKCS8EncodedKeySpec getKeySpec(Key decryptKey) public PKCS8EncodedKeySpec getKeySpec(Key decryptKey, String providerName) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeyException { - if (decryptKey == null) { - throw new NullPointerException("decryptKey is null"); - } - if (providerName == null) { - throw new NullPointerException("provider is null"); - } + Objects.requireNonNull(decryptKey, "decryptKey is null"); + Objects.requireNonNull(providerName, "provider is null"); Provider provider = Security.getProvider(providerName); if (provider == null) { throw new NoSuchProviderException("provider " + @@ -387,12 +631,8 @@ public PKCS8EncodedKeySpec getKeySpec(Key decryptKey, public PKCS8EncodedKeySpec getKeySpec(Key decryptKey, Provider provider) throws NoSuchAlgorithmException, InvalidKeyException { - if (decryptKey == null) { - throw new NullPointerException("decryptKey is null"); - } - if (provider == null) { - throw new NullPointerException("provider is null"); - } + Objects.requireNonNull(decryptKey, "decryptKey is null"); + Objects.requireNonNull(provider, "provider is null"); return getKeySpecImpl(decryptKey, provider); } @@ -438,23 +678,9 @@ private static void checkTag(DerValue val, byte tag, String valName) } } - @SuppressWarnings("fallthrough") private static PKCS8EncodedKeySpec pkcs8EncodingToSpec(byte[] encodedKey) throws IOException { - DerInputStream in = new DerInputStream(encodedKey); - DerValue[] values = in.getSequence(3); - - switch (values.length) { - case 4: - checkTag(values[3], DerValue.TAG_CONTEXT, "attributes"); - /* fall through */ - case 3: - checkTag(values[0], DerValue.tag_Integer, "version"); - String keyAlg = AlgorithmId.parse(values[1]).getName(); - checkTag(values[2], DerValue.tag_OctetString, "privateKey"); - return new PKCS8EncodedKeySpec(encodedKey, keyAlg); - default: - throw new IOException("invalid key encoding"); - } + return new PKCS8EncodedKeySpec(encodedKey, + KeyUtil.getAlgorithm(encodedKey)); } } diff --git a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java index 5726671dfd45d..d4bcd34dbd609 100644 --- a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java +++ b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java @@ -80,6 +80,8 @@ public enum Feature { KEY_DERIVATION, @JEP(number = 502, title = "Stable Values", status = "Preview") STABLE_VALUES, + @JEP(number=470, title="PEM Encodings of Cryptographic Objects", status="Preview") + PEM_API, LANGUAGE_MODEL, /** * A key for testing. diff --git a/src/java.base/share/classes/sun/security/ec/ECKeyFactory.java b/src/java.base/share/classes/sun/security/ec/ECKeyFactory.java index 85d4d0bf2639b..cd84fd4dec3d1 100644 --- a/src/java.base/share/classes/sun/security/ec/ECKeyFactory.java +++ b/src/java.base/share/classes/sun/security/ec/ECKeyFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,6 +25,8 @@ package sun.security.ec; +import sun.security.pkcs.PKCS8Key; + import java.security.*; import java.security.interfaces.*; import java.security.spec.*; @@ -84,8 +86,7 @@ public ECKeyFactory() { * To be used by future Java ECDSA and ECDH implementations. */ public static ECKey toECKey(Key key) throws InvalidKeyException { - if (key instanceof ECKey) { - ECKey ecKey = (ECKey)key; + if (key instanceof ECKey ecKey) { checkKey(ecKey); return ecKey; } else { @@ -147,7 +148,7 @@ protected Key engineTranslateKey(Key key) throws InvalidKeyException { // see JCA doc protected PublicKey engineGeneratePublic(KeySpec keySpec) - throws InvalidKeySpecException { + throws InvalidKeySpecException { try { return implGeneratePublic(keySpec); } catch (InvalidKeySpecException e) { @@ -159,7 +160,7 @@ protected PublicKey engineGeneratePublic(KeySpec keySpec) // see JCA doc protected PrivateKey engineGeneratePrivate(KeySpec keySpec) - throws InvalidKeySpecException { + throws InvalidKeySpecException { try { return implGeneratePrivate(keySpec); } catch (InvalidKeySpecException e) { @@ -171,19 +172,13 @@ protected PrivateKey engineGeneratePrivate(KeySpec keySpec) // internal implementation of translateKey() for public keys. See JCA doc private PublicKey implTranslatePublicKey(PublicKey key) - throws InvalidKeyException { - if (key instanceof ECPublicKey) { - if (key instanceof ECPublicKeyImpl) { - return key; - } - ECPublicKey ecKey = (ECPublicKey)key; - return new ECPublicKeyImpl( - ecKey.getW(), - ecKey.getParams() - ); + throws InvalidKeyException { + if (key instanceof ECPublicKeyImpl) { + return key; + } else if (key instanceof ECPublicKey ecKey) { + return new ECPublicKeyImpl(ecKey.getW(), ecKey.getParams()); } else if ("X.509".equals(key.getFormat())) { - byte[] encoded = key.getEncoded(); - return new ECPublicKeyImpl(encoded); + return new ECPublicKeyImpl(key.getEncoded()); } else { throw new InvalidKeyException("Public keys must be instance " + "of ECPublicKey or have X.509 encoding"); @@ -192,16 +187,11 @@ private PublicKey implTranslatePublicKey(PublicKey key) // internal implementation of translateKey() for private keys. See JCA doc private PrivateKey implTranslatePrivateKey(PrivateKey key) - throws InvalidKeyException { - if (key instanceof ECPrivateKey) { - if (key instanceof ECPrivateKeyImpl) { - return key; - } - ECPrivateKey ecKey = (ECPrivateKey)key; - return new ECPrivateKeyImpl( - ecKey.getS(), - ecKey.getParams() - ); + throws InvalidKeyException { + if (key instanceof ECPrivateKeyImpl) { + return key; + } else if (key instanceof ECPrivateKey ecKey) { + return new ECPrivateKeyImpl(ecKey.getS(), ecKey.getParams()); } else if ("PKCS#8".equals(key.getFormat())) { byte[] encoded = key.getEncoded(); try { @@ -209,52 +199,54 @@ private PrivateKey implTranslatePrivateKey(PrivateKey key) } finally { Arrays.fill(encoded, (byte)0); } - } else { - throw new InvalidKeyException("Private keys must be instance " - + "of ECPrivateKey or have PKCS#8 encoding"); } + + throw new InvalidKeyException("Private keys must be instance " + + "of ECPrivateKey or have PKCS#8 encoding"); } // internal implementation of generatePublic. See JCA doc private PublicKey implGeneratePublic(KeySpec keySpec) - throws GeneralSecurityException { - if (keySpec instanceof X509EncodedKeySpec) { - X509EncodedKeySpec x509Spec = (X509EncodedKeySpec)keySpec; - return new ECPublicKeyImpl(x509Spec.getEncoded()); - } else if (keySpec instanceof ECPublicKeySpec) { - ECPublicKeySpec ecSpec = (ECPublicKeySpec)keySpec; - return new ECPublicKeyImpl( - ecSpec.getW(), - ecSpec.getParams() - ); - } else { - throw new InvalidKeySpecException("Only ECPublicKeySpec " - + "and X509EncodedKeySpec supported for EC public keys"); - } + throws GeneralSecurityException { + return switch (keySpec) { + case X509EncodedKeySpec x -> new ECPublicKeyImpl(x.getEncoded()); + case ECPublicKeySpec e -> + new ECPublicKeyImpl(e.getW(), e.getParams()); + case PKCS8EncodedKeySpec p8 -> { + PKCS8Key p8key = new ECPrivateKeyImpl(p8.getEncoded()); + if (!p8key.hasPublicKey()) { + throw new InvalidKeySpecException("No public key found."); + } + yield new ECPublicKeyImpl(p8key.getPubKeyEncoded()); + } + default -> + throw new InvalidKeySpecException(keySpec.getClass().getName() + + " not supported."); + }; } // internal implementation of generatePrivate. See JCA doc private PrivateKey implGeneratePrivate(KeySpec keySpec) - throws GeneralSecurityException { - if (keySpec instanceof PKCS8EncodedKeySpec) { - PKCS8EncodedKeySpec pkcsSpec = (PKCS8EncodedKeySpec)keySpec; - byte[] encoded = pkcsSpec.getEncoded(); - try { - return new ECPrivateKeyImpl(encoded); - } finally { - Arrays.fill(encoded, (byte) 0); + throws GeneralSecurityException { + return switch (keySpec) { + case PKCS8EncodedKeySpec p8 -> { + byte[] encoded = p8.getEncoded(); + try { + yield new ECPrivateKeyImpl(encoded); + } finally { + Arrays.fill(encoded, (byte) 0); + } } - } else if (keySpec instanceof ECPrivateKeySpec) { - ECPrivateKeySpec ecSpec = (ECPrivateKeySpec)keySpec; - return new ECPrivateKeyImpl(ecSpec.getS(), ecSpec.getParams()); - } else { - throw new InvalidKeySpecException("Only ECPrivateKeySpec " - + "and PKCS8EncodedKeySpec supported for EC private keys"); - } + case ECPrivateKeySpec e -> + new ECPrivateKeyImpl(e.getS(), e.getParams()); + default -> + throw new InvalidKeySpecException(keySpec.getClass().getName() + + " not supported."); + }; } protected T engineGetKeySpec(Key key, Class keySpec) - throws InvalidKeySpecException { + throws InvalidKeySpecException { try { // convert key to one of our keys // this also verifies that the key is a valid EC key and ensures @@ -263,8 +255,7 @@ protected T engineGetKeySpec(Key key, Class keySpec) } catch (InvalidKeyException e) { throw new InvalidKeySpecException(e); } - if (key instanceof ECPublicKey) { - ECPublicKey ecKey = (ECPublicKey)key; + if (key instanceof ECPublicKey ecKey) { if (keySpec.isAssignableFrom(ECPublicKeySpec.class)) { return keySpec.cast(new ECPublicKeySpec( ecKey.getW(), diff --git a/src/java.base/share/classes/sun/security/ec/ECPrivateKeyImpl.java b/src/java.base/share/classes/sun/security/ec/ECPrivateKeyImpl.java index b1b8b2d188f2c..65c4f093f27ea 100644 --- a/src/java.base/share/classes/sun/security/ec/ECPrivateKeyImpl.java +++ b/src/java.base/share/classes/sun/security/ec/ECPrivateKeyImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -40,6 +40,7 @@ import sun.security.util.*; import sun.security.x509.AlgorithmId; import sun.security.pkcs.PKCS8Key; +import sun.security.x509.X509Key; /** * Key implementation for EC private keys. @@ -73,6 +74,7 @@ public final class ECPrivateKeyImpl extends PKCS8Key implements ECPrivateKey { private byte[] arrayS; // private value as a little-endian array @SuppressWarnings("serial") // Type of field is not Serializable private ECParameterSpec params; + private byte[] domainParams; //Currently unsupported /** * Construct a key from its encoding. Called by the ECKeyFactory. @@ -111,7 +113,7 @@ private void makeEncoding(byte[] s) throws InvalidKeyException { out.putOctetString(privBytes); Arrays.fill(privBytes, (byte) 0); DerValue val = DerValue.wrap(DerValue.tag_Sequence, out); - key = val.toByteArray(); + privKeyMaterial = val.toByteArray(); val.clear(); } @@ -133,7 +135,7 @@ private void makeEncoding(BigInteger s) throws InvalidKeyException { out.putOctetString(sOctets); Arrays.fill(sOctets, (byte) 0); DerValue val = DerValue.wrap(DerValue.tag_Sequence, out); - key = val.toByteArray(); + privKeyMaterial = val.toByteArray(); val.clear(); } @@ -153,64 +155,78 @@ public BigInteger getS() { return s; } - private byte[] getArrayS0() { + // Return the internal arrayS byte[], if arrayS is null generate it. + public byte[] getArrayS() { if (arrayS == null) { arrayS = ECUtil.sArray(getS(), params); } return arrayS; } - public byte[] getArrayS() { - return getArrayS0().clone(); - } - // see JCA doc public ECParameterSpec getParams() { return params; } + /** + * Parse the ASN.1 of the privateKey Octet + */ private void parseKeyBits() throws InvalidKeyException { + // Parse private key material from PKCS8Key.decode() try { - DerInputStream in = new DerInputStream(key); + DerInputStream in = new DerInputStream(privKeyMaterial); DerValue derValue = in.getDerValue(); if (derValue.tag != DerValue.tag_Sequence) { throw new IOException("Not a SEQUENCE"); } DerInputStream data = derValue.data; int version = data.getInteger(); - if (version != 1) { + if (version != V2) { throw new IOException("Version must be 1"); } byte[] privData = data.getOctetString(); ArrayUtil.reverse(privData); arrayS = privData; - while (data.available() != 0) { - DerValue value = data.getDerValue(); - if (value.isContextSpecific((byte) 0)) { - // ignore for now - } else if (value.isContextSpecific((byte) 1)) { - // ignore for now - } else { - throw new InvalidKeyException("Unexpected value: " + value); - } - } + + // Validate parameters stored from PKCS8Key.decode() AlgorithmParameters algParams = this.algid.getParameters(); if (algParams == null) { throw new InvalidKeyException("EC domain parameters must be " + "encoded in the algorithm identifier"); } params = algParams.getParameterSpec(ECParameterSpec.class); + + if (data.available() == 0) { + return; + } + + DerValue value = data.getDerValue(); + if (value.isContextSpecific((byte) 0)) { + domainParams = value.getDataBytes(); // Save DER sequence + if (data.available() == 0) { + return; + } + value = data.getDerValue(); + } + + if (value.isContextSpecific((byte) 1)) { + DerValue bits = value.withTag(DerValue.tag_BitString); + pubKeyEncoded = new X509Key(algid, + bits.data.getUnalignedBitString()).getEncoded(); + } else { + throw new InvalidKeyException("Unexpected value: " + value); + } + } catch (IOException | InvalidParameterSpecException e) { throw new InvalidKeyException("Invalid EC private key", e); } } - @Override public PublicKey calculatePublicKey() { ECParameterSpec ecParams = getParams(); ECOperations ops = ECOperations.forParameters(ecParams) .orElseThrow(ProviderException::new); - MutablePoint pub = ops.multiply(ecParams.getGenerator(), getArrayS0()); + MutablePoint pub = ops.multiply(ecParams.getGenerator(), getArrayS()); AffinePoint affPub = pub.asAffine(); ECPoint w = new ECPoint(affPub.getX().asBigInteger(), affPub.getY().asBigInteger()); diff --git a/src/java.base/share/classes/sun/security/ec/XDHKeyFactory.java b/src/java.base/share/classes/sun/security/ec/XDHKeyFactory.java index 3e2ced4123644..00eb5b4353d55 100644 --- a/src/java.base/share/classes/sun/security/ec/XDHKeyFactory.java +++ b/src/java.base/share/classes/sun/security/ec/XDHKeyFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,9 @@ package sun.security.ec; -import java.security.KeyFactorySpi; -import java.security.Key; -import java.security.PublicKey; -import java.security.PrivateKey; -import java.security.InvalidKeyException; -import java.security.ProviderException; +import sun.security.pkcs.PKCS8Key; + +import java.security.*; import java.security.interfaces.XECKey; import java.security.interfaces.XECPrivateKey; import java.security.interfaces.XECPublicKey; @@ -160,9 +157,19 @@ private PublicKey generatePublicImpl(KeySpec keySpec) InvalidKeySpecException::new, publicKeySpec.getParams()); checkLockedParams(InvalidKeySpecException::new, params); return new XDHPublicKeyImpl(params, publicKeySpec.getU()); + } else if (keySpec instanceof PKCS8EncodedKeySpec p8) { + PKCS8Key p8key = new XDHPrivateKeyImpl(p8.getEncoded()); + if (!p8key.hasPublicKey()) { + throw new InvalidKeySpecException("No public key found."); + } + XDHPublicKeyImpl result = + new XDHPublicKeyImpl(p8key.getPubKeyEncoded()); + checkLockedParams(InvalidKeySpecException::new, + result.getParams()); + return result; } else { - throw new InvalidKeySpecException( - "Only X509EncodedKeySpec and XECPublicKeySpec are supported"); + throw new InvalidKeySpecException(keySpec.getClass().getName() + + " not supported."); } } diff --git a/src/java.base/share/classes/sun/security/ec/XDHPrivateKeyImpl.java b/src/java.base/share/classes/sun/security/ec/XDHPrivateKeyImpl.java index dfc0d0f6cd349..416a3e10af458 100644 --- a/src/java.base/share/classes/sun/security/ec/XDHPrivateKeyImpl.java +++ b/src/java.base/share/classes/sun/security/ec/XDHPrivateKeyImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -54,7 +54,7 @@ public final class XDHPrivateKeyImpl extends PKCS8Key implements XECPrivateKey { DerValue val = new DerValue(DerValue.tag_OctetString, k); try { - this.key = val.toByteArray(); + this.privKeyMaterial = val.toByteArray(); } finally { val.clear(); } @@ -67,7 +67,7 @@ public final class XDHPrivateKeyImpl extends PKCS8Key implements XECPrivateKey { InvalidKeyException::new, algid); paramSpec = new NamedParameterSpec(params.getName()); try { - DerInputStream derStream = new DerInputStream(key); + DerInputStream derStream = new DerInputStream(privKeyMaterial); k = derStream.getOctetString(); } catch (IOException ex) { throw new InvalidKeyException(ex); @@ -102,7 +102,6 @@ public Optional getScalar() { return Optional.of(getK()); } - @Override public PublicKey calculatePublicKey() { XECParameters params = paramSpec.getName().equalsIgnoreCase("X25519") ? XECParameters.X25519 diff --git a/src/java.base/share/classes/sun/security/ec/ed/EdDSAKeyFactory.java b/src/java.base/share/classes/sun/security/ec/ed/EdDSAKeyFactory.java index c282a34ef846f..b3316f5ffe0ff 100644 --- a/src/java.base/share/classes/sun/security/ec/ed/EdDSAKeyFactory.java +++ b/src/java.base/share/classes/sun/security/ec/ed/EdDSAKeyFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,9 @@ package sun.security.ec.ed; -import java.security.KeyFactorySpi; -import java.security.Key; -import java.security.PublicKey; -import java.security.PrivateKey; -import java.security.InvalidKeyException; -import java.security.ProviderException; +import sun.security.pkcs.PKCS8Key; + +import java.security.*; import java.security.interfaces.*; import java.security.spec.*; import java.util.Arrays; @@ -153,9 +150,15 @@ private PublicKey generatePublicImpl(KeySpec keySpec) InvalidKeySpecException::new, publicKeySpec.getParams()); checkLockedParams(InvalidKeySpecException::new, params); return new EdDSAPublicKeyImpl(params, publicKeySpec.getPoint()); + } else if (keySpec instanceof PKCS8EncodedKeySpec p8) { + PKCS8Key p8key = new EdDSAPrivateKeyImpl(p8.getEncoded()); + if (!p8key.hasPublicKey()) { + throw new InvalidKeySpecException("No public key found."); + } + return new EdDSAPublicKeyImpl(p8key.getPubKeyEncoded()); } else { - throw new InvalidKeySpecException( - "Only X509EncodedKeySpec and EdECPublicKeySpec are supported"); + throw new InvalidKeySpecException(keySpec.getClass().getName() + + " not supported."); } } diff --git a/src/java.base/share/classes/sun/security/ec/ed/EdDSAPrivateKeyImpl.java b/src/java.base/share/classes/sun/security/ec/ed/EdDSAPrivateKeyImpl.java index 661ec9ed1b7b5..10b19c1ce534a 100644 --- a/src/java.base/share/classes/sun/security/ec/ed/EdDSAPrivateKeyImpl.java +++ b/src/java.base/share/classes/sun/security/ec/ed/EdDSAPrivateKeyImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -56,7 +56,7 @@ public final class EdDSAPrivateKeyImpl DerValue val = new DerValue(DerValue.tag_OctetString, h); try { - this.key = val.toByteArray(); + privKeyMaterial = val.toByteArray(); } finally { val.clear(); } @@ -71,7 +71,7 @@ public final class EdDSAPrivateKeyImpl paramSpec = new NamedParameterSpec(params.getName()); try { - DerInputStream derStream = new DerInputStream(key); + DerInputStream derStream = new DerInputStream(privKeyMaterial); h = derStream.getOctetString(); } catch (IOException ex) { throw new InvalidKeyException(ex); @@ -81,8 +81,8 @@ public final class EdDSAPrivateKeyImpl void checkLength(EdDSAParameters params) throws InvalidKeyException { - if (params.getKeyLength() != this.h.length) { - throw new InvalidKeyException("key length is " + this.h.length + + if (params.getKeyLength() != h.length) { + throw new InvalidKeyException("key length is " + h.length + ", key length must be " + params.getKeyLength()); } } diff --git a/src/java.base/share/classes/sun/security/pkcs/NamedPKCS8Key.java b/src/java.base/share/classes/sun/security/pkcs/NamedPKCS8Key.java index 88a2909cfac1c..a748433da875c 100644 --- a/src/java.base/share/classes/sun/security/pkcs/NamedPKCS8Key.java +++ b/src/java.base/share/classes/sun/security/pkcs/NamedPKCS8Key.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -75,7 +75,7 @@ public NamedPKCS8Key(String fname, String pname, byte[] rawBytes) { DerValue val = new DerValue(DerValue.tag_OctetString, rawBytes); try { - this.key = val.toByteArray(); + this.privKeyMaterial = val.toByteArray(); } finally { val.clear(); } @@ -90,7 +90,7 @@ public NamedPKCS8Key(String fname, byte[] encoded) throws InvalidKeyException { if (algid.getEncodedParams() != null) { throw new InvalidKeyException("algorithm identifier has params"); } - rawBytes = new DerInputStream(key).getOctetString(); + rawBytes = new DerInputStream(privKeyMaterial).getOctetString(); } catch (IOException e) { throw new InvalidKeyException("Cannot parse input", e); } @@ -129,7 +129,7 @@ private void readObject(ObjectInputStream stream) @Override public void destroy() throws DestroyFailedException { Arrays.fill(rawBytes, (byte)0); - Arrays.fill(key, (byte)0); + Arrays.fill(privKeyMaterial, (byte)0); if (encodedKey != null) { Arrays.fill(encodedKey, (byte)0); } diff --git a/src/java.base/share/classes/sun/security/pkcs/PKCS8Key.java b/src/java.base/share/classes/sun/security/pkcs/PKCS8Key.java index 4cb277b43a830..5aa859c58e3b6 100644 --- a/src/java.base/share/classes/sun/security/pkcs/PKCS8Key.java +++ b/src/java.base/share/classes/sun/security/pkcs/PKCS8Key.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,22 +25,18 @@ package sun.security.pkcs; -import java.io.*; -import java.security.Key; -import java.security.KeyRep; -import java.security.PrivateKey; -import java.security.KeyFactory; -import java.security.MessageDigest; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; +import jdk.internal.access.SharedSecrets; +import sun.security.util.*; +import sun.security.x509.AlgorithmId; +import sun.security.x509.X509Key; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.security.*; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Arrays; -import jdk.internal.access.SharedSecrets; -import sun.security.x509.*; -import sun.security.util.*; - /** * Holds a PKCS#8 key, for example a private key * @@ -56,7 +52,7 @@ * ... * } * - * We support this format but do not parse attributes and publicKey now. + * We support this format but do not parse attributes. */ public class PKCS8Key implements PrivateKey, InternalPrivateKey { @@ -67,20 +63,29 @@ public class PKCS8Key implements PrivateKey, InternalPrivateKey { /* The algorithm information (name, parameters, etc). */ protected AlgorithmId algid; - /* The key bytes, without the algorithm information */ - protected byte[] key; + /* The private key OctetString for the algorithm subclasses to decode */ + protected byte[] privKeyMaterial; - /* The encoded for the key. Created on demand by encode(). */ + /* The pkcs8 encoding of this key(s). Created on demand. */ protected byte[] encodedKey; + /* The encoded x509 public key for v2 */ + protected byte[] pubKeyEncoded = null; + + /* ASN.1 Attributes */ + private byte[] attributes; + + /* PKCS8 version of the PEM */ + private int version; + /* The version for this key */ - private static final int V1 = 0; - private static final int V2 = 1; + public static final int V1 = 0; + public static final int V2 = 1; /** * Default constructor. Constructors in subclasses that create a new key * from its components require this. These constructors must initialize - * {@link #algid} and {@link #key}. + * {@link #algid} and {@link #privKeyMaterial}. */ protected PKCS8Key() { } @@ -91,7 +96,7 @@ protected PKCS8Key() { } * * This method is also used by {@link #parseKey} to create a raw key. */ - protected PKCS8Key(byte[] input) throws InvalidKeyException { + public PKCS8Key(byte[] input) throws InvalidKeyException { try { decode(new DerValue(input)); } catch (IOException e) { @@ -99,39 +104,70 @@ protected PKCS8Key(byte[] input) throws InvalidKeyException { } } + private PKCS8Key(byte[] privEncoding, byte[] pubEncoding) + throws InvalidKeyException { + this(privEncoding); + pubKeyEncoded = pubEncoding; + version = V2; + } + + public int getVersion() { + return version; + } + + /** + * Method for decoding PKCS8 v1 and v2 formats. Decoded values are stored + * in this class, key material remains in DER format for algorithm + * subclasses to decode. + */ private void decode(DerValue val) throws InvalidKeyException { try { if (val.tag != DerValue.tag_Sequence) { throw new InvalidKeyException("invalid key format"); } - int version = val.data.getInteger(); + // Support check for V1, aka 0, and V2, aka 1. + version = val.data.getInteger(); if (version != V1 && version != V2) { throw new InvalidKeyException("unknown version: " + version); } - algid = AlgorithmId.parse (val.data.getDerValue ()); - key = val.data.getOctetString(); + // Parse and store AlgorithmID + algid = AlgorithmId.parse(val.data.getDerValue()); + + // Store key material for subclasses to parse + privKeyMaterial = val.data.getOctetString(); - DerValue next; + // PKCS8 v1 typically ends here if (val.data.available() == 0) { return; } - next = val.data.getDerValue(); - if (next.isContextSpecific((byte)0)) { + + // OPTIONAL Context tag 0 for Attributes for PKCS8 v1 & v2 + // Uses 0xA0 context-specific/constructed or 0x80 + // context-specific/primitive. + DerValue v = val.data.getDerValue(); + if (v.isContextSpecific((byte)0)) { + attributes = v.getDataBytes(); // Save DER sequence if (val.data.available() == 0) { return; } - next = val.data.getDerValue(); + v = val.data.getDerValue(); } - if (next.isContextSpecific((byte)1)) { - if (version == V1) { - throw new InvalidKeyException("publicKey seen in v1"); + // OPTIONAL context tag 1 for Public Key for PKCS8 v2 only + if (version == V2) { + if (v.isContextSpecific((byte)1)) { + DerValue bits = v.withTag(DerValue.tag_BitString); + pubKeyEncoded = new X509Key(algid, + bits.getUnalignedBitString()).getEncoded(); + } else { + throw new InvalidKeyException("Invalid context tag"); } if (val.data.available() == 0) { return; } } + throw new InvalidKeyException("Extra bytes"); } catch (IOException e) { throw new InvalidKeyException("Unable to decode key", e); @@ -154,17 +190,29 @@ private void decode(DerValue val) throws InvalidKeyException { * handling, that specific need can be accommodated. * * @param encoded the DER-encoded SubjectPublicKeyInfo value - * @exception IOException on data format errors + * @exception InvalidKeyException on data format errors */ - public static PrivateKey parseKey(byte[] encoded) throws IOException { + public static PrivateKey parseKey(byte[] encoded) + throws InvalidKeyException { + return parseKey(encoded, null); + } + + public static PrivateKey parseKey(byte[] encoded, Provider provider) + throws InvalidKeyException { try { PKCS8Key rawKey = new PKCS8Key(encoded); - byte[] internal = rawKey.getEncodedInternal(); - PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(internal); + + PKCS8EncodedKeySpec pkcs8KeySpec = + new PKCS8EncodedKeySpec(rawKey.generateEncoding()); PrivateKey result = null; try { - result = KeyFactory.getInstance(rawKey.algid.getName()) + if (provider == null) { + result = KeyFactory.getInstance(rawKey.algid.getName()) .generatePrivate(pkcs8KeySpec); + } else { + result = KeyFactory.getInstance(rawKey.algid.getName(), + provider).generatePrivate(pkcs8KeySpec); + } } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { // Ignore and return raw key result = rawKey; @@ -176,8 +224,8 @@ public static PrivateKey parseKey(byte[] encoded) throws IOException { .clearEncodedKeySpec(pkcs8KeySpec); } return result; - } catch (InvalidKeyException e) { - throw new IOException("corrupt private key", e); + } catch (IOException e) { + throw new InvalidKeyException(e); } } @@ -188,10 +236,18 @@ public String getAlgorithm() { return algid.getName(); } + public byte[] getPubKeyEncoded() { + return pubKeyEncoded; + } + + public boolean hasPublicKey() { + return (pubKeyEncoded != null); + } + /** * Returns the algorithm ID to be used with this key. */ - public AlgorithmId getAlgorithmId () { + public AlgorithmId getAlgorithmId () { return algid; } @@ -210,6 +266,25 @@ public String getFormat() { return "PKCS#8"; } + /** + * With a given encoded Public and Private key, generate and return a + * PKCS8v2 DER-encoded byte[]. + * + * @param pubKeyEncoded DER-encoded PublicKey + * @param privKeyEncoded DER-encoded PrivateKey + * @return DER-encoded byte array + * @throws IOException thrown on encoding failure + */ + public static byte[] getEncoded(byte[] pubKeyEncoded, byte[] privKeyEncoded) + throws IOException { + try { + return new PKCS8Key(privKeyEncoded, pubKeyEncoded). + generateEncoding(); + } catch (InvalidKeyException e) { + throw new IOException(e); + } + } + /** * DER-encodes this key as a byte array stored inside this object * and return it. @@ -218,17 +293,53 @@ public String getFormat() { */ private synchronized byte[] getEncodedInternal() { if (encodedKey == null) { - DerOutputStream tmp = new DerOutputStream(); - tmp.putInteger(V1); - algid.encode(tmp); - tmp.putOctetString(key); - DerValue out = DerValue.wrap(DerValue.tag_Sequence, tmp); - encodedKey = out.toByteArray(); - out.clear(); + try { + encodedKey = generateEncoding(); + } catch (IOException e) { + return null; + } } return encodedKey; } + private byte[] generateEncoding() throws IOException { + DerOutputStream out = new DerOutputStream(); + out.putInteger(version); + algid.encode(out); + out.putOctetString(privKeyMaterial); + + if (version == V2) { + if (attributes != null) { + out.writeImplicit( + DerValue.createTag((byte) (DerValue.TAG_CONTEXT | + DerValue.TAG_CONSTRUCT), false, (byte) 0), + new DerOutputStream().putOctetString(attributes)); + + } + + if (pubKeyEncoded != null) { + X509Key x = new X509Key(); + try { + x.decode(pubKeyEncoded); + } catch (InvalidKeyException e) { + throw new IOException(e); + } + + // X509Key x = X509Key.parse(pubKeyEncoded); + DerOutputStream pubOut = new DerOutputStream(); + pubOut.putUnalignedBitString(x.getKey()); + out.writeImplicit( + DerValue.createTag(DerValue.TAG_CONTEXT, false, + (byte) 1), pubOut); + } + } + + DerValue val = DerValue.wrap(DerValue.tag_Sequence, out); + encodedKey = val.toByteArray(); + val.clear(); + return encodedKey; + } + @java.io.Serial protected Object writeReplace() throws java.io.ObjectStreamException { return new KeyRep(KeyRep.Type.PRIVATE, @@ -298,6 +409,6 @@ public void clear() { if (encodedKey != null) { Arrays.fill(encodedKey, (byte)0); } - Arrays.fill(key, (byte)0); + Arrays.fill(privKeyMaterial, (byte)0); } } diff --git a/src/java.base/share/classes/sun/security/provider/DSAPrivateKey.java b/src/java.base/share/classes/sun/security/provider/DSAPrivateKey.java index e34fd3a6af145..a3357b1879cf8 100644 --- a/src/java.base/share/classes/sun/security/provider/DSAPrivateKey.java +++ b/src/java.base/share/classes/sun/security/provider/DSAPrivateKey.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -70,7 +70,7 @@ public DSAPrivateKey(BigInteger x, BigInteger p, byte[] xbytes = x.toByteArray(); DerValue val = new DerValue(DerValue.tag_Integer, xbytes); - key = val.toByteArray(); + privKeyMaterial = val.toByteArray(); val.clear(); Arrays.fill(xbytes, (byte)0); } @@ -81,7 +81,7 @@ public DSAPrivateKey(BigInteger x, BigInteger p, public DSAPrivateKey(byte[] encoded) throws InvalidKeyException { super(encoded); try { - DerInputStream in = new DerInputStream(key); + DerInputStream in = new DerInputStream(privKeyMaterial); x = in.getBigInteger(); } catch (IOException e) { throw new InvalidKeyException(e.getMessage(), e); diff --git a/src/java.base/share/classes/sun/security/provider/KeyProtector.java b/src/java.base/share/classes/sun/security/provider/KeyProtector.java index 0faed9db73737..384c28e2bf8d9 100644 --- a/src/java.base/share/classes/sun/security/provider/KeyProtector.java +++ b/src/java.base/share/classes/sun/security/provider/KeyProtector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ package sun.security.provider; -import java.io.IOException; import java.security.SecureRandom; import java.security.*; import java.util.Arrays; @@ -300,8 +299,8 @@ public Key recover(EncryptedPrivateKeyInfo encrInfo) // which in turn parses the key material. try { return PKCS8Key.parseKey(plainKey); - } catch (IOException ioe) { - throw new UnrecoverableKeyException(ioe.getMessage()); + } catch (InvalidKeyException e) { + throw new UnrecoverableKeyException(e.getMessage()); } finally { Arrays.fill(plainKey, (byte)0); } diff --git a/src/java.base/share/classes/sun/security/provider/X509Factory.java b/src/java.base/share/classes/sun/security/provider/X509Factory.java index ad48c0c598d02..ec7a1ac998a93 100644 --- a/src/java.base/share/classes/sun/security/provider/X509Factory.java +++ b/src/java.base/share/classes/sun/security/provider/X509Factory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,6 +27,7 @@ import java.io.*; +import java.security.PEMRecord; import java.security.cert.*; import java.util.*; @@ -36,6 +37,7 @@ import sun.security.provider.certpath.X509CertificatePair; import sun.security.util.Cache; import sun.security.util.DerValue; +import sun.security.util.Pem; import sun.security.x509.X509CRLImpl; import sun.security.x509.X509CertImpl; @@ -556,118 +558,20 @@ private static byte[] readOneBlock(InputStream is) throws IOException { readBERInternal(is, bout, c); return bout.toByteArray(); } else { - // Read BASE64 encoded data, might skip info at the beginning - ByteArrayOutputStream data = new ByteArrayOutputStream(); - - // Step 1: Read until header is found - int hyphen = (c=='-') ? 1: 0; // count of consequent hyphens - int last = (c=='-') ? -1: c; // the char before hyphen - while (true) { - int next = is.read(); - if (next == -1) { - // We accept useless data after the last block, - // say, empty lines. + try { + PEMRecord rec; + try { + rec = Pem.readPEM(is, (c == '-' ? true : false)); + } catch (EOFException e) { return null; } - if (next == '-') { - hyphen++; - } else { - hyphen = 0; - last = next; - } - if (hyphen == 5 && (last == -1 || last == '\r' || last == '\n')) { - break; - } - } - - // Step 2: Read the rest of header, determine the line end - int end; - StringBuilder header = new StringBuilder("-----"); - while (true) { - int next = is.read(); - if (next == -1) { - throw new IOException("Incomplete data"); - } - if (next == '\n') { - end = '\n'; - break; - } - if (next == '\r') { - next = is.read(); - if (next == -1) { - throw new IOException("Incomplete data"); - } - if (next == '\n') { - end = '\n'; - } else { - end = '\r'; - // Skip all white space chars - if (next != 9 && next != 10 && next != 13 && next != 32) { - data.write(next); - } - } - break; - } - header.append((char)next); - } - - // Step 3: Read the data - while (true) { - int next = is.read(); - if (next == -1) { - throw new IOException("Incomplete data"); - } - if (next != '-') { - // Skip all white space chars - if (next != 9 && next != 10 && next != 13 && next != 32) { - data.write(next); - } - } else { - break; - } - } - - // Step 4: Consume the footer - StringBuilder footer = new StringBuilder("-"); - while (true) { - int next = is.read(); - // Add next == '\n' for maximum safety, in case endline - // is not consistent. - if (next == -1 || next == end || next == '\n') { - break; - } - if (next != '\r') footer.append((char)next); - } - - checkHeaderFooter(header.toString().stripTrailing(), - footer.toString().stripTrailing()); - - try { - return Base64.getDecoder().decode(data.toByteArray()); + return Base64.getDecoder().decode(rec.pem()); } catch (IllegalArgumentException e) { throw new IOException(e); } } } - private static void checkHeaderFooter(String header, - String footer) throws IOException { - if (header.length() < 16 || !header.startsWith("-----BEGIN ") || - !header.endsWith("-----")) { - throw new IOException("Illegal header: " + header); - } - if (footer.length() < 14 || !footer.startsWith("-----END ") || - !footer.endsWith("-----")) { - throw new IOException("Illegal footer: " + footer); - } - String headerType = header.substring(11, header.length()-5); - String footerType = footer.substring(9, footer.length()-5); - if (!headerType.equals(footerType)) { - throw new IOException("Header and footer do not match: " + - header + " " + footer); - } - } - /** * Read one BER data block. This method is aware of indefinite-length BER * encoding and will read all the subsections in a recursive way diff --git a/src/java.base/share/classes/sun/security/rsa/RSAKeyFactory.java b/src/java.base/share/classes/sun/security/rsa/RSAKeyFactory.java index c6fa1cf8980d1..f518bf5919b70 100644 --- a/src/java.base/share/classes/sun/security/rsa/RSAKeyFactory.java +++ b/src/java.base/share/classes/sun/security/rsa/RSAKeyFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,6 +32,7 @@ import java.security.spec.*; import java.util.Arrays; +import sun.security.pkcs.PKCS8Key; import sun.security.rsa.RSAUtil.KeyType; /** @@ -332,9 +333,15 @@ private PublicKey generatePublic(KeySpec keySpec) } catch (ProviderException e) { throw new InvalidKeySpecException(e); } + } else if (keySpec instanceof PKCS8EncodedKeySpec p8) { + PKCS8Key p8key = new PKCS8Key(p8.getEncoded()); + if (!p8key.hasPublicKey()) { + throw new InvalidKeySpecException("No public key found."); + } + return RSAPublicKeyImpl.newKey(type, "X.509", + p8key.getPubKeyEncoded()); } else { - throw new InvalidKeySpecException("Only RSAPublicKeySpec " - + "and X509EncodedKeySpec supported for RSA public keys"); + throw new InvalidKeySpecException(keySpec.getClass().getName() + " not supported."); } } diff --git a/src/java.base/share/classes/sun/security/rsa/RSAPrivateCrtKeyImpl.java b/src/java.base/share/classes/sun/security/rsa/RSAPrivateCrtKeyImpl.java index b6ef067d44971..857914e699719 100644 --- a/src/java.base/share/classes/sun/security/rsa/RSAPrivateCrtKeyImpl.java +++ b/src/java.base/share/classes/sun/security/rsa/RSAPrivateCrtKeyImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -69,6 +69,7 @@ public final class RSAPrivateCrtKeyImpl private BigInteger qe; // prime exponent q private BigInteger coeff; // CRT coefficient + // RSA or RSA-PSS KeyType private final transient KeyType type; // Optional parameters associated with this RSA key @@ -101,7 +102,7 @@ public static RSAPrivateKey newKey(KeyType type, String format, } case "PKCS#1": try { - BigInteger[] comps = parseASN1(encoded); + BigInteger[] comps = parsePKCS1(encoded); if ((comps[1].signum() == 0) || (comps[3].signum() == 0) || (comps[4].signum() == 0) || (comps[5].signum() == 0) || (comps[6].signum() == 0) || (comps[7].signum() == 0)) { @@ -237,7 +238,7 @@ private RSAPrivateCrtKeyImpl(byte[] encoded) throws InvalidKeyException { Arrays.fill(nbytes[6], (byte) 0); Arrays.fill(nbytes[7], (byte) 0); DerValue val = DerValue.wrap(DerValue.tag_Sequence, out); - key = val.toByteArray(); + privKeyMaterial = val.toByteArray(); val.clear(); } @@ -304,7 +305,7 @@ public AlgorithmParameterSpec getParams() { // utility method for parsing DER encoding of RSA private keys in PKCS#1 // format as defined in RFC 8017 Appendix A.1.2, i.e. SEQ of version, n, // e, d, p, q, pe, qe, and coeff, and return the parsed components. - private static BigInteger[] parseASN1(byte[] raw) throws IOException { + private static BigInteger[] parsePKCS1(byte[] raw) throws IOException { DerValue derValue = new DerValue(raw); try { if (derValue.tag != DerValue.tag_Sequence) { @@ -337,7 +338,7 @@ private static BigInteger[] parseASN1(byte[] raw) throws IOException { private void parseKeyBits() throws InvalidKeyException { try { - BigInteger[] comps = parseASN1(key); + BigInteger[] comps = parsePKCS1(privKeyMaterial); n = comps[0]; e = comps[1]; d = comps[2]; @@ -351,6 +352,30 @@ private void parseKeyBits() throws InvalidKeyException { } } + /** + * With a given PKCS#1/slleay/OpenSSL old default RSA binary encoding, + * decode and return the proper RSA encoded KeySpec + * @param encoded RSA binary encoding + * @return KeySpec + * @throws InvalidKeyException on decoding failure + */ + public static KeySpec getKeySpec(byte[] encoded) throws + InvalidKeyException { + try { + BigInteger[] comps = parsePKCS1(encoded); + if ((comps[1].signum() == 0) || (comps[3].signum() == 0) || + (comps[4].signum() == 0) || (comps[5].signum() == 0) || + (comps[6].signum() == 0) || (comps[7].signum() == 0)) { + return new RSAPrivateKeySpec(comps[0], comps[2]); + } else { + return new RSAPrivateCrtKeySpec(comps[0], + comps[1], comps[2], comps[3], comps[4], comps[5], + comps[6], comps[7]); + } + } catch (IOException ioe) { + throw new InvalidKeyException("Invalid PKCS#1 encoding", ioe); + } + } /** * Restores the state of this object from the stream. *

diff --git a/src/java.base/share/classes/sun/security/rsa/RSAPrivateKeyImpl.java b/src/java.base/share/classes/sun/security/rsa/RSAPrivateKeyImpl.java index 91bc5f771d6bf..c67f934b0c3bc 100644 --- a/src/java.base/share/classes/sun/security/rsa/RSAPrivateKeyImpl.java +++ b/src/java.base/share/classes/sun/security/rsa/RSAPrivateKeyImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -110,7 +110,7 @@ public final class RSAPrivateKeyImpl extends PKCS8Key implements RSAPrivateKey { out.putInteger(0); out.putInteger(0); DerValue val = DerValue.wrap(DerValue.tag_Sequence, out); - key = val.toByteArray(); + privKeyMaterial = val.toByteArray(); val.clear(); } diff --git a/src/java.base/share/classes/sun/security/rsa/RSAPublicKeyImpl.java b/src/java.base/share/classes/sun/security/rsa/RSAPublicKeyImpl.java index 5a0745604d2d6..47d95cdd6a0d6 100644 --- a/src/java.base/share/classes/sun/security/rsa/RSAPublicKeyImpl.java +++ b/src/java.base/share/classes/sun/security/rsa/RSAPublicKeyImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -82,7 +82,7 @@ public static RSAPublicKey newKey(KeyType type, String format, break; case "PKCS#1": try { - BigInteger[] comps = parseASN1(encoded); + BigInteger[] comps = parsePKCS1(encoded); key = new RSAPublicKeyImpl(type, null, comps[0], comps[1]); } catch (IOException ioe) { throw new InvalidKeyException("Invalid PKCS#1 encoding", ioe); @@ -199,7 +199,7 @@ public AlgorithmParameterSpec getParams() { // utility method for parsing DER encoding of RSA public keys in PKCS#1 // format as defined in RFC 8017 Appendix A.1.1, i.e. SEQ of n and e. - private static BigInteger[] parseASN1(byte[] raw) throws IOException { + private static BigInteger[] parsePKCS1(byte[] raw) throws IOException { DerValue derValue = new DerValue(raw); if (derValue.tag != DerValue.tag_Sequence) { throw new IOException("Not a SEQUENCE"); @@ -218,7 +218,7 @@ private static BigInteger[] parseASN1(byte[] raw) throws IOException { */ protected void parseKeyBits() throws InvalidKeyException { try { - BigInteger[] comps = parseASN1(getKey().toByteArray()); + BigInteger[] comps = parsePKCS1(getKey().toByteArray()); n = comps[0]; e = comps[1]; } catch (IOException e) { diff --git a/src/java.base/share/classes/sun/security/util/DerValue.java b/src/java.base/share/classes/sun/security/util/DerValue.java index f2fcf350b3947..19e7083180b2e 100644 --- a/src/java.base/share/classes/sun/security/util/DerValue.java +++ b/src/java.base/share/classes/sun/security/util/DerValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -67,6 +67,7 @@ public class DerValue { /** The tag class types */ public static final byte TAG_UNIVERSAL = (byte)0x000; + public static final byte TAG_CONSTRUCT = (byte)0x020; public static final byte TAG_APPLICATION = (byte)0x040; public static final byte TAG_CONTEXT = (byte)0x080; public static final byte TAG_PRIVATE = (byte)0x0c0; diff --git a/src/java.base/share/classes/sun/security/util/KeyUtil.java b/src/java.base/share/classes/sun/security/util/KeyUtil.java index 03768e114503e..7a58ac0d4e9ab 100644 --- a/src/java.base/share/classes/sun/security/util/KeyUtil.java +++ b/src/java.base/share/classes/sun/security/util/KeyUtil.java @@ -41,6 +41,7 @@ import jdk.internal.access.SharedSecrets; import sun.security.jca.JCAUtil; +import sun.security.x509.AlgorithmId; /** * A utility class to get key length, validate keys, etc. @@ -478,5 +479,72 @@ public static void destroySecretKeys(SecretKey... keys) { } } } + + /** + * With a given DER encoded bytes, read through and return the AlgorithmID + * stored if it can be found. If none is found or there is an IOException, + * null is returned. + * + * @param encoded DER encoded bytes + * @return AlgorithmID stored in the DER encoded bytes or null. + */ + public static String getAlgorithm(byte[] encoded) throws IOException { + try { + return getAlgorithmId(encoded).getName(); + } catch (IOException e) { + throw new IOException("No recognized algorithm detected in " + + "encoding", e); + } + } + + /** + * With a given DER encoded bytes, read through and return the AlgorithmID + * stored if it can be found. + * + * @param encoded DER encoded bytes + * @return AlgorithmID stored in the DER encoded bytes + * @throws IOException if there was a DER or other parsing error + */ + public static AlgorithmId getAlgorithmId(byte[] encoded) throws IOException { + DerInputStream is = new DerInputStream(encoded); + DerValue value = is.getDerValue(); + if (value.tag != DerValue.tag_Sequence) { + throw new IOException("Unknown DER Format: Value 1 not a Sequence"); + } + + is = value.data; + value = is.getDerValue(); + // This route is for: RSAPublic, Encrypted RSAPrivate, EC Public, + // Encrypted EC Private, + if (value.tag == DerValue.tag_Sequence) { + return AlgorithmId.parse(value); + } else if (value.tag == DerValue.tag_Integer) { + // RSAPrivate, ECPrivate + // current value is version, which can be ignored + value = is.getDerValue(); + if (value.tag == DerValue.tag_OctetString) { + value = is.getDerValue(); + if (value.tag == DerValue.tag_Sequence) { + return AlgorithmId.parse(value); + } else { + // OpenSSL/X9.62 (0xA0) + ObjectIdentifier oid = value.data.getOID(); + AlgorithmId algo = new AlgorithmId(oid, (AlgorithmParameters) null); + if (CurveDB.lookup(algo.getName()) != null) { + return new AlgorithmId(AlgorithmId.EC_oid); + } + + } + + } else if (value.tag == DerValue.tag_Sequence) { + // Public Key + return AlgorithmId.parse(value); + } + + } + throw new IOException("No algorithm detected"); + } + + } diff --git a/src/java.base/share/classes/sun/security/util/Pem.java b/src/java.base/share/classes/sun/security/util/Pem.java index ba01d91c82e81..0acbec8281a47 100644 --- a/src/java.base/share/classes/sun/security/util/Pem.java +++ b/src/java.base/share/classes/sun/security/util/Pem.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,14 +25,56 @@ package sun.security.util; -import java.io.IOException; +import sun.security.x509.AlgorithmId; + +import java.io.*; import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.security.PEMRecord; +import java.security.Security; +import java.util.Arrays; import java.util.Base64; +import java.util.HexFormat; +import java.util.Objects; +import java.util.regex.Pattern; /** * A utility class for PEM format encoding. */ public class Pem { + private static final char WS = 0x20; // Whitespace + private static final byte[] CRLF = new byte[] {'\r', '\n'}; + + // Default algorithm from jdk.epkcs8.defaultAlgorithm in java.security + public static final String DEFAULT_ALGO; + + // Pattern matching for EKPI operations + private static final Pattern pbePattern; + + // Lazy initialized PBES2 OID value + private static ObjectIdentifier PBES2OID; + + // Lazy initialized singleton encoder. + private static Base64.Encoder b64Encoder; + + static { + String algo = Security.getProperty("jdk.epkcs8.defaultAlgorithm"); + DEFAULT_ALGO = (algo == null || algo.isBlank()) ? + "PBEWithHmacSHA256AndAES_128" : algo; + pbePattern = Pattern.compile("^PBEWith.*And.*", + Pattern.CASE_INSENSITIVE); + } + + public static final String CERTIFICATE = "CERTIFICATE"; + public static final String X509_CRL = "X509 CRL"; + public static final String ENCRYPTED_PRIVATE_KEY = "ENCRYPTED PRIVATE KEY"; + public static final String PRIVATE_KEY = "PRIVATE KEY"; + public static final String RSA_PRIVATE_KEY = "RSA PRIVATE KEY"; + public static final String PUBLIC_KEY = "PUBLIC KEY"; + // old PEM types per RFC 7468 + public static final String X509_CERTIFICATE = "X509 CERTIFICATE"; + public static final String X_509_CERTIFICATE = "X.509 CERTIFICATE"; + public static final String CRL = "CRL"; /** * Decodes a PEM-encoded block. @@ -40,15 +82,264 @@ public class Pem { * @param input the input string, according to RFC 1421, can only contain * characters in the base-64 alphabet and whitespaces. * @return the decoded bytes - * @throws java.io.IOException if input is invalid */ - public static byte[] decode(String input) throws IOException { - byte[] src = input.replaceAll("\\s+", "") - .getBytes(StandardCharsets.ISO_8859_1); - try { + public static byte[] decode(String input) { + byte[] src = input.replaceAll("\\s+", ""). + getBytes(StandardCharsets.ISO_8859_1); return Base64.getDecoder().decode(src); - } catch (IllegalArgumentException e) { - throw new IOException(e); + } + + /** + * Return the OID for a given PBE algorithm. PBES1 has an OID for each + * algorithm, while PBES2 has one OID for everything that complies with + * the formatting. Therefore, if the algorithm is not PBES1, it will + * return PBES2. Cipher will determine if this is a valid PBE algorithm. + * PBES2 specifies AES as the cipher algorithm, but any block cipher could + * be supported. + */ + public static ObjectIdentifier getPBEID(String algorithm) { + + // Verify pattern matches PBE Standard Name spec + if (!pbePattern.matcher(algorithm).matches()) { + throw new IllegalArgumentException("Invalid algorithm format."); + } + + // Return the PBES1 OID if it matches + try { + return AlgorithmId.get(algorithm).getOID(); + } catch (NoSuchAlgorithmException e) { + // fall-through } + + // Lazy initialize + if (PBES2OID == null) { + try { + // Set to the hardcoded OID in KnownOID.java + PBES2OID = AlgorithmId.get("PBES2").getOID(); + } catch (NoSuchAlgorithmException e) { + // Should never fail. + throw new IllegalArgumentException(e); + } + } + return PBES2OID; + } + + /* + * RFC 7468 has some rules what generators should return given a historical + * type name. This converts read in PEM to the RFC. Change the type to + * be uniform is likely to help apps from not using all 3 certificate names. + */ + private static String typeConverter(String type) { + return switch (type) { + case Pem.X509_CERTIFICATE, Pem.X_509_CERTIFICATE -> Pem.CERTIFICATE; + case Pem.CRL -> Pem.X509_CRL; + default -> type; + }; + } + + /** + * Read the PEM text and return it in it's three components: header, + * base64, and footer. + * + * The method will leave the stream after reading the end of line of the + * footer or end of file + * @param is an InputStream + * @param shortHeader if true, the hyphen length is 4 because the first + * hyphen is assumed to have been read. This is needed + * for the CertificateFactory X509 implementation. + * @return a new PEMRecord + * @throws IOException on IO errors or PEM syntax errors that leave + * the read position not at the end of a PEM block + * @throws EOFException when at the unexpected end of the stream + * @throws IllegalArgumentException when a PEM syntax error occurs, + * but the read position in the stream is at the end of the block, so + * future reads can be successful. + */ + public static PEMRecord readPEM(InputStream is, boolean shortHeader) + throws IOException { + Objects.requireNonNull(is); + + int hyphen = (shortHeader ? 1 : 0); + int eol = 0; + + ByteArrayOutputStream os = new ByteArrayOutputStream(6); + // Find starting hyphens + do { + int d = is.read(); + switch (d) { + case '-' -> hyphen++; + case -1 -> { + if (os.size() == 0) { + throw new EOFException("No data available"); + } + throw new EOFException("No PEM data found"); + } + default -> hyphen = 0; + } + os.write(d); + } while (hyphen != 5); + + StringBuilder sb = new StringBuilder(64); + sb.append("-----"); + hyphen = 0; + int c; + + // Get header definition until first hyphen + do { + switch (c = is.read()) { + case '-' -> hyphen++; + case -1 -> throw new EOFException("Input ended prematurely"); + case '\n', '\r' -> throw new IOException("Incomplete header"); + default -> sb.append((char) c); + } + } while (hyphen == 0); + + // Verify header ending with 5 hyphens. + do { + switch (is.read()) { + case '-' -> hyphen++; + default -> + throw new IOException("Incomplete header"); + } + } while (hyphen < 5); + + sb.append("-----"); + String header = sb.toString(); + if (header.length() < 16 || !header.startsWith("-----BEGIN ") || + !header.endsWith("-----")) { + throw new IOException("Illegal header: " + header); + } + + hyphen = 0; + sb = new StringBuilder(1024); + + // Determine the line break using the char after the last hyphen + switch (is.read()) { + case WS -> {} // skip whitespace + case '\r' -> { + c = is.read(); + if (c == '\n') { + eol = '\n'; + } else { + eol = '\r'; + sb.append((char) c); + } + } + case '\n' -> eol = '\n'; + default -> + throw new IOException("No EOL character found"); + } + + // Read data until we find the first footer hyphen. + do { + switch (c = is.read()) { + case -1 -> + throw new EOFException("Incomplete header"); + case '-' -> hyphen++; + case WS, '\t', '\r', '\n' -> {} // skip whitespace and tab + default -> sb.append((char) c); + } + } while (hyphen == 0); + + String data = sb.toString(); + + // Verify footer starts with 5 hyphens. + do { + switch (is.read()) { + case '-' -> hyphen++; + case -1 -> throw new EOFException("Input ended prematurely"); + default -> throw new IOException("Incomplete footer"); + } + } while (hyphen < 5); + + hyphen = 0; + sb = new StringBuilder(64); + sb.append("-----"); + + // Look for Complete header by looking for the end of the hyphens + do { + switch (c = is.read()) { + case '-' -> hyphen++; + case -1 -> throw new EOFException("Input ended prematurely"); + default -> sb.append((char) c); + } + } while (hyphen == 0); + + // Verify ending with 5 hyphens. + do { + switch (is.read()) { + case '-' -> hyphen++; + case -1 -> throw new EOFException("Input ended prematurely"); + default -> throw new IOException("Incomplete footer"); + } + } while (hyphen < 5); + + while ((c = is.read()) != eol && c != -1 && c != WS) { + // skip when eol is '\n', the line separator is likely "\r\n". + if (c == '\r') { + continue; + } + throw new IOException("Invalid PEM format: " + + "No EOL char found in footer: 0x" + + HexFormat.of().toHexDigits((byte) c)); + } + + sb.append("-----"); + String footer = sb.toString(); + if (footer.length() < 14 || !footer.startsWith("-----END ") || + !footer.endsWith("-----")) { + // Not an IOE because the read pointer is correctly at the end. + throw new IOException("Illegal footer: " + footer); + } + + // Verify the object type in the header and the footer are the same. + String headerType = header.substring(11, header.length() - 5); + String footerType = footer.substring(9, footer.length() - 5); + if (!headerType.equals(footerType)) { + throw new IOException("Header and footer do not " + + "match: " + headerType + " " + footerType); + } + + // If there was data before finding the 5 dashes of the PEM header, + // backup 5 characters and save that data. + byte[] preData = null; + if (os.size() > 5) { + preData = Arrays.copyOf(os.toByteArray(), os.size() - 5); + } + + return new PEMRecord(typeConverter(headerType), data, preData); + } + + public static PEMRecord readPEM(InputStream is) throws IOException { + return readPEM(is, false); + } + + private static String pemEncoded(String type, String base64) { + return + "-----BEGIN " + type + "-----\r\n" + + base64 + (!base64.endsWith("\n") ? "\r\n" : "") + + "-----END " + type + "-----\r\n"; + } + + /** + * Construct a String-based encoding based off the type. leadingData + * is not used with this method. + * @return PEM in a string + */ + public static String pemEncoded(String type, byte[] der) { + if (b64Encoder == null) { + b64Encoder = Base64.getMimeEncoder(64, CRLF); + } + return pemEncoded(type, b64Encoder.encodeToString(der)); + } + + /** + * Construct a String-based encoding based off the type. leadingData + * is not used with this method. + * @return PEM in a string + */ + public static String pemEncoded(PEMRecord pem) { + String p = pem.pem().replaceAll("(.{64})", "$1\r\n"); + return pemEncoded(pem.type(), p); } } diff --git a/src/java.base/share/classes/sun/security/x509/X509Key.java b/src/java.base/share/classes/sun/security/x509/X509Key.java index 719748394e195..c83e06f651e80 100644 --- a/src/java.base/share/classes/sun/security/x509/X509Key.java +++ b/src/java.base/share/classes/sun/security/x509/X509Key.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,7 +36,6 @@ import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; -import java.util.Objects; import sun.security.util.HexDumpEncoder; import sun.security.util.*; @@ -83,7 +82,8 @@ public X509Key() { } * data is stored and transmitted losslessly, but no knowledge * about this particular algorithm is available. */ - private X509Key(AlgorithmId algid, BitArray key) { + @SuppressWarnings("this-escape") + public X509Key(AlgorithmId algid, BitArray key) { this.algid = algid; setKey(key); encode(); @@ -100,7 +100,7 @@ protected void setKey(BitArray key) { * Gets the key. The key may or may not be byte aligned. * @return a BitArray containing the key. */ - protected BitArray getKey() { + public BitArray getKey() { return (BitArray)bitStringKey.clone(); } @@ -129,7 +129,7 @@ public static PublicKey parse(DerValue in) throws IOException algorithm = AlgorithmId.parse(in.data.getDerValue()); try { subjectKey = buildX509Key(algorithm, - in.data.getUnalignedBitString()); + in.data.getUnalignedBitString()); } catch (InvalidKeyException e) { throw new IOException("subject key, " + e.getMessage(), e); @@ -154,7 +154,7 @@ public static PublicKey parse(DerValue in) throws IOException * @exception InvalidKeyException on invalid key encodings. */ protected void parseKeyBits() throws InvalidKeyException { - encode(); + getEncodedInternal(); } /* @@ -243,7 +243,7 @@ public String getAlgorithm() { /** * Returns the algorithm ID to be used with this key. */ - public AlgorithmId getAlgorithmId() { return algid; } + public AlgorithmId getAlgorithmId() { return algid; } /** * Encode SubjectPublicKeyInfo sequence on the DER output stream. @@ -260,7 +260,7 @@ public byte[] getEncoded() { return getEncodedInternal().clone(); } - public byte[] getEncodedInternal() { + private byte[] getEncodedInternal() { byte[] encoded = encodedKey; if (encoded == null) { DerOutputStream out = new DerOutputStream(); @@ -314,7 +314,7 @@ public String toString() * @param val a DER-encoded X.509 SubjectPublicKeyInfo value * @exception InvalidKeyException on parsing errors. */ - void decode(DerValue val) throws InvalidKeyException { + public void decode(DerValue val) throws InvalidKeyException { try { if (val.tag != DerValue.tag_Sequence) throw new InvalidKeyException("invalid key format"); diff --git a/src/java.base/share/conf/security/java.security b/src/java.base/share/conf/security/java.security index b115d47983848..b750c1b82b0b0 100644 --- a/src/java.base/share/conf/security/java.security +++ b/src/java.base/share/conf/security/java.security @@ -1549,3 +1549,12 @@ jdk.tls.alpnCharset=ISO_8859_1 # security property value defined here. # #jdk.security.krb5.name.case.sensitive=false + +# +# Default algorithm for PEMEncoder Encrypted PKCS#8 +# +# This property defines the default password-based encryption algorithm for +# java.security.PEMEncoder when configured for encryption with the +# withEncryption method. +# +jdk.epkcs8.defaultAlgorithm=PBEWithHmacSHA256AndAES_128 \ No newline at end of file diff --git a/test/jdk/java/security/KeyFactory/KeyFactoryGetKeySpecForInvalidSpec.java b/test/jdk/java/security/KeyFactory/KeyFactoryGetKeySpecForInvalidSpec.java index 71b05b0c2252f..a8346089db7dc 100644 --- a/test/jdk/java/security/KeyFactory/KeyFactoryGetKeySpecForInvalidSpec.java +++ b/test/jdk/java/security/KeyFactory/KeyFactoryGetKeySpecForInvalidSpec.java @@ -1,5 +1,6 @@ /* * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it diff --git a/test/jdk/java/security/PEM/PEMData.java b/test/jdk/java/security/PEM/PEMData.java new file mode 100644 index 0000000000000..e1f32cdbb7c82 --- /dev/null +++ b/test/jdk/java/security/PEM/PEMData.java @@ -0,0 +1,476 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import javax.crypto.EncryptedPrivateKeyInfo; +import java.security.DEREncodable; +import java.security.KeyPair; +import java.security.PEMRecord; +import java.security.cert.X509Certificate; +import java.security.interfaces.*; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Library class for PEMEncoderTest and PEMDecoderTest + */ +class PEMData { + public static final Entry ecsecp256 = new Entry("ecsecp256", + """ + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkW3Jx561NlEgBnut + KwDdi3cNwu7YYD/QtJ+9+AEBdoqhRANCAASL+REY4vvAI9M3gonaml5K3lRgHq5w + +OO4oO0VNduC44gUN1nrk7/wdNSpL+xXNEX52Dsff+2RD/fop224ANvB + -----END PRIVATE KEY----- + """, KeyPair.class); + + public static final Entry rsapriv = new Entry("rsapriv", + """ + -----BEGIN PRIVATE KEY----- + MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAOtjMnCzPy4jCeZb + OdOvmvU3jl7+cvPFgL5MfqDCM5a8yI0yImg/hzibJJHLk3emUVBSnekgHvCqyGLW + 3qGR2DuBEaMy0mkg8hfKcSpHLaYjDYaspO27d2qtb6d1qtsPoPjJFjWFYeW6K463 + OHG654K5/2FcJgQdlLVyp3zCiQU/AgMBAAECgYEAwNkDkTv5rlX8nWLuLJV5kh/T + H9a93SRZxw8qy5Bv7bZ7ZNrHP7uUkHbi7iPojKWRhwo43692SdzR0dCSk7LGgN9q + CYvndsYR6gifVGBi0WF+St4+NdtcQ3VlNdsojy2BdIx0oC+r7i3bn+zc968O/kI+ + EgdgrMcjjFqyx6tMHpECQQD8TYPKGHyN7Jdy28llCoUX/sL/yZ2vIi5mnDAFE5ae + KZQSkNAXG+8i9Qbs/Wdd5S3oZDqu+6DBn9gib80pYY05AkEA7tY59Oy8ka7nBlGP + g6Wo1usF2bKqk8vjko9ioZQay7f86aB10QFcAjCr+cCUm16Lc9DwzWl02nNggRZa + Jz8eNwJBAO+1zfLjFOPa14F/JHdlaVKE8EwKCFDuztsapd0M4Vtf8Zk6ERsDpU63 + Ml9T2zOwnM9g+whpdjDAZ59ATdJ1JrECQQDReJQ2SxeL0lGPCiOLu9RcQp7L81aF + 79G1bgp8WlAyEjlAkloiqEWRKiz7DDuKFR7Lwhognng9S+n87aS+PS57AkBh75t8 + 6onPAs4hkm+63dfzCojvEkALevO8J3OVX7YS5q9J1r75wDn60Ob0Zh+iiorpx8Ob + WqcWcoJqfdLEyBT+ + -----END PRIVATE KEY----- + """, RSAPrivateKey.class); + + public static final Entry rsaprivbc = new Entry("rsaprivbc", + """ + -----BEGIN PRIVATE KEY----- + MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAOtjMnCzPy4jCeZb + OdOvmvU3jl7+cvPFgL5MfqDCM5a8yI0yImg/hzibJJHLk3emUVBSnekgHvCqyGLW + 3qGR2DuBEaMy0mkg8hfKcSpHLaYjDYaspO27d2qtb6d1qtsPoPjJFjWFYeW6K463 + OHG654K5/2FcJgQdlLVyp3zCiQU/AgMBAAECgYEAwNkDkTv5rlX8nWLuLJV5kh/T + H9a93SRZxw8qy5Bv7bZ7ZNrHP7uUkHbi7iPojKWRhwo43692SdzR0dCSk7LGgN9q + CYvndsYR6gifVGBi0WF+St4+NdtcQ3VlNdsojy2BdIx0oC+r7i3bn+zc968O/kI+ + EgdgrMcjjFqyx6tMHpECQQD8TYPKGHyN7Jdy28llCoUX/sL/yZ2vIi5mnDAFE5ae + KZQSkNAXG+8i9Qbs/Wdd5S3oZDqu+6DBn9gib80pYY05AkEA7tY59Oy8ka7nBlGP + g6Wo1usF2bKqk8vjko9ioZQay7f86aB10QFcAjCr+cCUm16Lc9DwzWl02nNggRZa + Jz8eNwJBAO+1zfLjFOPa14F/JHdlaVKE8EwKCFDuztsapd0M4Vtf8Zk6ERsDpU63 + Ml9T2zOwnM9g+whpdjDAZ59ATdJ1JrECQQDReJQ2SxeL0lGPCiOLu9RcQp7L81aF + 79G1bgp8WlAyEjlAkloiqEWRKiz7DDuKFR7Lwhognng9S+n87aS+PS57AkBh75t8 + 6onPAs4hkm+63dfzCojvEkALevO8J3OVX7YS5q9J1r75wDn60Ob0Zh+iiorpx8Ob + WqcWcoJqfdLEyBT+ + -----END PRIVATE KEY----- + """, RSAPrivateKey.class); + + public static final Entry ec25519priv = new Entry("ed25519priv", + """ + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIFFZsmD+OKk67Cigc84/2fWtlKsvXWLSoMJ0MHh4jI4I + -----END PRIVATE KEY----- + """, EdECPrivateKey.class); + + public static final Entry rsapub = new Entry("rsapub", + """ + -----BEGIN PUBLIC KEY----- + MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDrYzJwsz8uIwnmWznTr5r1N45e + /nLzxYC+TH6gwjOWvMiNMiJoP4c4mySRy5N3plFQUp3pIB7wqshi1t6hkdg7gRGj + MtJpIPIXynEqRy2mIw2GrKTtu3dqrW+ndarbD6D4yRY1hWHluiuOtzhxuueCuf9h + XCYEHZS1cqd8wokFPwIDAQAB + -----END PUBLIC KEY----- + """, RSAPublicKey.class); + + public static final Entry rsapubbc = new Entry("rsapubbc", + """ + -----BEGIN PUBLIC KEY----- + MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDrYzJwsz8uIwnmWznTr5r1N45e + /nLzxYC+TH6gwjOWvMiNMiJoP4c4mySRy5N3plFQUp3pIB7wqshi1t6hkdg7gRGj + MtJpIPIXynEqRy2mIw2GrKTtu3dqrW+ndarbD6D4yRY1hWHluiuOtzhxuueCuf9h + XCYEHZS1cqd8wokFPwIDAQAB + -----END PUBLIC KEY----- + """, RSAPublicKey.class); + + public static final Entry ecsecp256pub = new Entry("ecsecp256pub", """ + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/kRGOL7wCPTN4KJ2ppeSt5UYB6u + cPjjuKDtFTXbguOIFDdZ65O/8HTUqS/sVzRF+dg7H3/tkQ/36KdtuADbwQ== + -----END PUBLIC KEY----- + """, ECPublicKey.class); + + // EC key with explicit parameters -- Not currently supported by SunEC + public static final String pubec_explicit = """ + -----BEGIN PUBLIC KEY----- + MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA + AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA//// + ///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd + NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5 + RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA + //////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABIv5ERji+8Aj0zeCidqaXkre + VGAernD447ig7RU124LjiBQ3WeuTv/B01Kkv7Fc0RfnYOx9/7ZEP9+inbbgA28E= + -----END PUBLIC KEY----- + """; + + public static final Entry oasbcpem = new Entry("oasbcpem", + """ + -----BEGIN PRIVATE KEY----- + MIIDCAIBATANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAOtjMnCzPy4jCeZbOdOvmvU3jl7+ + cvPFgL5MfqDCM5a8yI0yImg/hzibJJHLk3emUVBSnekgHvCqyGLW3qGR2DuBEaMy0mkg8hfKcSpH + LaYjDYaspO27d2qtb6d1qtsPoPjJFjWFYeW6K463OHG654K5/2FcJgQdlLVyp3zCiQU/AgMBAAEC + gYEAwNkDkTv5rlX8nWLuLJV5kh/TH9a93SRZxw8qy5Bv7bZ7ZNrHP7uUkHbi7iPojKWRhwo43692 + SdzR0dCSk7LGgN9qCYvndsYR6gifVGBi0WF+St4+NdtcQ3VlNdsojy2BdIx0oC+r7i3bn+zc968O + /kI+EgdgrMcjjFqyx6tMHpECQQD8TYPKGHyN7Jdy28llCoUX/sL/yZ2vIi5mnDAFE5aeKZQSkNAX + G+8i9Qbs/Wdd5S3oZDqu+6DBn9gib80pYY05AkEA7tY59Oy8ka7nBlGPg6Wo1usF2bKqk8vjko9i + oZQay7f86aB10QFcAjCr+cCUm16Lc9DwzWl02nNggRZaJz8eNwJBAO+1zfLjFOPa14F/JHdlaVKE + 8EwKCFDuztsapd0M4Vtf8Zk6ERsDpU63Ml9T2zOwnM9g+whpdjDAZ59ATdJ1JrECQQDReJQ2SxeL + 0lGPCiOLu9RcQp7L81aF79G1bgp8WlAyEjlAkloiqEWRKiz7DDuKFR7Lwhognng9S+n87aS+PS57 + AkBh75t86onPAs4hkm+63dfzCojvEkALevO8J3OVX7YS5q9J1r75wDn60Ob0Zh+iiorpx8ObWqcW + coJqfdLEyBT+gYGNADCBiQKBgQDrYzJwsz8uIwnmWznTr5r1N45e/nLzxYC+TH6gwjOWvMiNMiJo + P4c4mySRy5N3plFQUp3pIB7wqshi1t6hkdg7gRGjMtJpIPIXynEqRy2mIw2GrKTtu3dqrW+ndarb + D6D4yRY1hWHluiuOtzhxuueCuf9hXCYEHZS1cqd8wokFPwIDAQAB + -----END PRIVATE KEY----- + """, KeyPair.class); + + public static final Entry oasrfc8410 = new Entry("oasrfc8410", + """ + -----BEGIN PRIVATE KEY----- + MHICAQEwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC + oB8wHQYKKoZIhvcNAQkJFDEPDA1DdXJkbGUgQ2hhaXJzgSEAGb9ECWmEzf6FQbrB + Z9w7lshQhqowtrbLDFw4rXAxZuE= + -----END PRIVATE KEY----- + """, KeyPair.class); + + public static final Entry rsaOpenSSL = new Entry("rsaOpenSSL", + """ + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEAqozTLan1qFcOCWnS63jXQn5lLyGOKDv3GM11n2zkGGrChayj + cSzB2KTlDmN9NgOyFdqGNWbSgdmXR5ToHGHYwaKubJoQIoPQcsipWDI156d3+X/8 + BxCGY8l5nYwvS4olOXc+2kEjeFF1eamnm9IQ5DHZfaFPl0ri4Yfm1YHBAbt/7HvF + 3MBjgBj1xSsSFLW4O6ws6guRVGDfKBVyyRNUhRTbSua/nEz0wAjxF2PWT+ZTHS6M + 0siYwVTuPI4/n4ItoYoahvGb9JskkXP+bc/QZJCTFYdyxF5tKqVMSdYaJTxop02p + Jo3oeafVKSlBrr0K731xgNBKqBud44aKT5R96QIDAQABAoIBAQCD9Q/T7gOvayPm + LqXOISJURV1emRTXloX5/8Y5QtQ8/CVjrg6Lm3ikefjsKBgR+cwJUpmyqcrIQyXk + cZchlqdSMt/IEW/YdKqMlStJnRfOE+ok9lx2ztdcT9+0AWn6hXmFu/i6f9nE1yoQ + py6SxnbhSJyhsnTVd1CR9Uep/InsHvYW/15WlVMD1VuCSIt9sefqXwavbAfBaqbn + mjwBB/ulsqKhHSuRq/QWqlj+jyGqhhYmTguC1Qwt0woDbThiHtK+suCTAlGBj/A+ + IZ1U9d+VsHBcWDKBkxmlKWcJAGR3xXiKKy9vfzC+DU7L99kgay80VZarDyXgiy78 + 9xMMzRMBAoGBANoxnZhu1bUFtLqTJ1HfDm6UB+1zVd2Mu4DXYdy/AHjoaCLp05OQ + 0ZeyhO/eXPT+eGpzCxkWD7465KO/QDfnp54p/NS73jaJVdWQHBhzJx1MymqURy3N + JQeW4+ojzwSmVXcrs7Og6EBa4L+PWLpMLW2kODniCY+vp9f5LS6m8UPJAoGBAMgZ + 4rBw7B9YFZZW/EE4eos4Q7KtA5tEP6wvCq04oxfiSytWXifYX0ToPp0CHhZlWOxk + v9a/BDGqM7AxAQJs7mmIvT5AT2V1w7oTbFPnnAo6pQtLcfaxdFFqr0h6t0sXSOKC + rQeZAqqFqwuOyP7vT0goGlBruHkwS21NKkzCyzkhAoGAc2JjhbWu+8Cdt0CUPX5o + ol9T5eTlFnkSuuqrTNIQzN+SGkxu341o2QDFvhdoLwLW6OwXhVeeUanROSqtKiMu + B70Kf/EtbMephXtk8CUNHTh7nmr1TSo8F8xakHoJQts3PQL2T9qal1W3nnWOpU4d + g+qg9TMsfTiV2OdjVlVgJskCgYBSnjV1qjojuue22hVvDFW0c7en5z2M9wHfItEi + sjbMnrdwnklj5Dd5qPZpNz2a+59ag0Kd9OJTazXKMoF7MeTCGB4ivMTLXHNCudBJ + WGCZ7JrGbhEQzTX8g7L5lwlk7KlANLoiX++03lm//OVKNR6j6ULsH33cM6+A4pJr + fSYRYQKBgCr9iMTmL0x+n6AmMNecR+MhDxi99Oy0s2EBAYqN9g/8yNgwM4KR0cjz + EcgIOtkvoTrJ9Cquvuj+O7/d2yNoH0SZQ4IYJKq47/Z4kKhwXzJnBCCCBKgkjfub + RTQSNnSEgTaBD29l7FrhNRHX9lIKFZ23caCTBS6o3q3+KgPbq7ao + -----END RSA PRIVATE KEY----- + """, RSAPrivateKey.class); + + static final Entry ed25519ep8 = new Entry("ed25519ep8", + """ + -----BEGIN ENCRYPTED PRIVATE KEY----- + MIGqMGYGCSqGSIb3DQEFDTBZMDgGCSqGSIb3DQEFDDArBBRyYnoNyrcqvubzch00 + jyuAb5YizgICEAACARAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEAQIEEM8BgEgO + vdMyi46+Dw7cOjwEQLtx5ME0NOOo7vlCGm3H/4j+Tf5UXrMb1UrkPjqc8OiLbC0n + IycFtI70ciPjgwDSjtCcPxR8fSxJPrm2yOJsRVo= + -----END ENCRYPTED PRIVATE KEY----- + """, EdECPrivateKey.class, "fish".toCharArray()); + + // This is not meant to be decrypted and to stay as an EKPI + static final Entry ed25519ekpi = new Entry("ed25519ekpi", + ed25519ep8.pem(), EncryptedPrivateKeyInfo.class, null); + + static final Entry rsaCert = new Entry("rsaCert", + """ + -----BEGIN CERTIFICATE----- + MIIErDCCApQCCQD7ndjWbI/x0DANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxQ + RU0gVGVzdCBSU0EwIBcNMjQwMTA5MjMzNDIwWhgPMjA1MTA1MjYyMzM0MjBaMBcx + FTATBgNVBAMMDFBFTSBUZXN0IFJTQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC + AgoCggIBAKgO/Pciro8xn5iNjcVCR4IuXP+V1PNATtKAlMbWzwGVOupKgRcNeRbA + N9RlljxSgEChIWs0/DB9VsAw1wCIVeuIVxv0ZvhVAcuD8Yyl58eev1rptsSJhTkN + YJFxEPSP2kfWDxS21ltbg1bnY/c1SQbzWawDLJN16G+ICzQXo68UB5fCZV9Ugfgf + 9USPkCiC6aFt+RT7eQaN/JrjtCm+mFf4VbK7jYW7D8AfjviEY1HQCnPoTjHBxdy+ + o5s4aIOx1Wuu9wMoGuLXgY3do5/OSDCfByk7rc1drQB9GOKf2gkR8PL9TjK+R3Lq + wCA0a3jlCBiGPlH3oeZJrnp7jhAh/tVxbsd7yIdhQnasbiTfhew132AdPXoQE+ic + PFoh8MMtG1bdzt8EbvePC3GOjeyIP6f2Ixrh3B6wXzzYmJqBwON+X8TLQolcI1pa + Q7AUz5BScy3lO9nyJE/FJkX+Mr6n7WCdudCrQNP+0M845UvkgFyf4FcM7uUVugBm + AXy7sCqZgTeLdqHyTElMCoWzBa3MHKyiSCh8GUJH+I1yBY1gG95j3tITIOFvbZrk + vDiMwNtV9T6Ta2mb0+38GfKjbI6PF4DVrzB6xc7Q6/GwyhOb86YLOLlEHJfhuc+C + Pdy8hQrrulm2jiCO/skvHucABNJ2CENyWa7ljNJkcN6GNTziz4AhAgMBAAEwDQYJ + KoZIhvcNAQELBQADggIBAKFQE2AgYgc7/xzwveUAiZ55tfcds07UnazLCOdpz+JJ + W4MOt/1Qi9mUylqDEymfNZVLPd2dEjB4wJ57XBUjL+kXkH1SocuskxQPf05iz5zT + pEwg2fTmU73ilKMs5Q113nBnL9ZZtlRKCh1Oc5LvLW799uVXnU4UdSpWOBU9ePGY + +H1wUKf+e0/BkveQsZERYcamH9O9U/+h+bbhr3GpT1AVnuDRyF28OvRwARDCOVyy + ifh+xCR3WCnNcgfwCoH6cE1aXDKHchlAAZtvjc1lLud7/ECIg+15keVfTYk4HEbH + j/lprxyH7y99lMmRLQpnTve54RrZGGmg51UD7OmwPHLMGibfQkw6QgdNsggIYD6p + L91spgRRB+i4PTovocndOMR2RYgQEelGNqv8MsoUC7oRNxPCHxIEGuUPH1Vf3jnk + mTHbVzpjy57UtfcYp1uBFDf8WoWO1Mi6oXRw2YQA1YSMm1+3ftphxydcbRuBlS7O + 6Iiqk6XlFG9Dpd2jjAQQzJGtnC0QDgGz6/KGp1bGEhRnOWju07eLWvPbyaX5zeSh + 8gOYV33zkPhziWJt4uFMFIi7N2DLEk5UVZv1KTLZlfPl55DRs7j/Sb4vKHpB17AO + meVknxVvifDVY0TIz57t28Accsk6ClBCxNPluPU/8YLGAZJYsdDXjGcndQ13s5G7 + -----END CERTIFICATE----- + """, X509Certificate.class); + + static final Entry ecCert = new Entry("ecCert", + """ + -----BEGIN CERTIFICATE----- + MIIBFzCBvgIJAOGVk/ky59ojMAoGCCqGSM49BAMCMBMxETAPBgNVBAMMCFBFTSB0 + ZXN0MCAXDTI0MDEwOTIzMzEwNloYDzIwNTEwNTI2MjMzMTA2WjATMREwDwYDVQQD + DAhQRU0gdGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGYI0jD7JZzw4RYD + y9DCfaYNz0CHrpr9gJU5NXe6czvuNBdAOl/lJGQ1pqpEQSQaMDII68obvQyQQyFY + lU3G9QAwCgYIKoZIzj0EAwIDSAAwRQIgMwYld7aBzkcRt9mn27YOed5+n0xN1y8Q + VEcFjLI/tBYCIQDU3szDZ/PK2mUZwtgQxLqHdh+f1JY0UwQS6M8QUvoDHw== + -----END CERTIFICATE----- + """, X509Certificate.class); + + // EC cert with explicit parameters -- Not currently supported by SunEC + static final String ecCertEX = """ + -----BEGIN CERTIFICATE----- + MIICrDCCAjMCCQDKAlI7uc1CVDAKBggqhkjOPQQDAjATMREwDwYDVQQDDAhQRU0g + dGVzdDAgFw0yNDAxMDkyMzIxNTlaGA8yMDUxMDUyNjIzMjE1OVowEzERMA8GA1UE + AwwIUEVNIHRlc3QwggHMMIIBZAYHKoZIzj0CATCCAVcCAQEwPAYHKoZIzj0BAQIx + AP/////////////////////////////////////////+/////wAAAAAAAAAA//// + /zB7BDD//////////////////////////////////////////v////8AAAAAAAAA + AP////wEMLMxL6fiPufkmI4Fa+P4LRkYHZxu/oFBEgMUCI9QE4daxlY5jYou0Z0q + hcjt0+wq7wMVAKM1kmqjGaJ6HQCJamdzpIJ6zaxzBGEEqofKIr6LBTeOscce8yCt + dG4dO2KLp5uYWfdB4IJUKjhVAvJdv1UpbDpUXjhydgq3NhfeSpYmLG9dnpi/kpLc + Kfj0Hb0omhR86doxE7XwuMAKYLHOHX6BnXpDHXyQ6g5fAjEA//////////////// + ////////////////x2NNgfQ3Ld9YGg2ySLCneuzsGWrMxSlzAgEBA2IABO+IbTh6 + WqyzmxdCeJ0uUQ2v2jKxRuCKRyPlYAnpBmmQypsRS+GBdbBa0Mu6MTnVJh5uvqXn + q7IuHVEiE3EFKw0DNW30nINuQg6lTv6PgN/4nYBqsl5FQgzk2SYN3bw+7jAKBggq + hkjOPQQDAgNnADBkAjATCnbbn3CgPRPi9Nym0hKpBAXc30D4eVB3mz8snK0oKU0+ + VP3F0EWcyM2QDSZCXIgCMHWknAhIGFTHxqypYUV8eAd3SY7ujZ6EPR0uG//csBWG + IqHcgr8slqi35ycQn5yMsQ== + -----END CERTIFICATE----- + """; + + static final Entry ecsecp384 = new Entry("ecsecp384", + """ + -----BEGIN PRIVATE KEY----- + MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBVS52ZSKZ0oES7twD2 + GGwRIVu3uHlGIwlu0xzFe7sgIPntca2bHfYMhgGxrlCm0q+hZANiAAQNWgwWfLX8 + 8pYVjvwbfvDF9f+Oa9w6JjrfpWwFAUI6b1OPgrNUh+yXtUXnQNXnfUcIu0Os53bM + 8fTqPkQl6RyWEDHeXqJK8zTBHMeBq9nLfDPSbzQgLDyC64Orn0D8exM= + -----END PRIVATE KEY----- + """, KeyPair.class); + + public static final Entry ecCSR = new Entry("ecCSR", + """ + -----BEGIN CERTIFICATE REQUEST----- + MIICCTCCAbACAQAwRTELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxFDASBgNV + BAcMC1NhbnRhIENsYXJhMREwDwYDVQQDDAhUZXN0IENTUjCCAUswggEDBgcqhkjO + PQIBMIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP////// + /////////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY1 + 2Ko6k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3 + gZ9+kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO + 5+tKfA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racX + noTzucrC/GMlUQIBAQNCAAT3UJgGXD7xMwFSzBnkhsEXz3eJLjIE0HTP1Ax6x7QX + G3/+Z/qgOZ6UQCxeHOWMEgF1Ufc/tZkzgbvxWJ6gokeToBUwEwYJKoZIhvcNAQkH + MQYMBGZpc2gwCgYIKoZIzj0EAwIDRwAwRAIgUBTdrMDE4BqruYRh1rRyKQBf48WR + kIX8R4dBK9h1VRcCIEBR2Mzvku/huTbWTwKVlXBZeEmwIlxKwpRepPtViXcW + -----END CERTIFICATE REQUEST----- + """, PEMRecord.class); + + public static final String preData = "TEXT BLAH TEXT BLAH" + + System.lineSeparator(); + public static final String postData = "FINISHED" + System.lineSeparator(); + + public static final Entry ecCSRWithData = new Entry("ecCSRWithData", + preData + """ + -----BEGIN CERTIFICATE REQUEST----- + MIICCTCCAbACAQAwRTELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxFDASBgNV + BAcMC1NhbnRhIENsYXJhMREwDwYDVQQDDAhUZXN0IENTUjCCAUswggEDBgcqhkjO + PQIBMIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP////// + /////////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY1 + 2Ko6k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3 + gZ9+kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO + 5+tKfA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racX + noTzucrC/GMlUQIBAQNCAAT3UJgGXD7xMwFSzBnkhsEXz3eJLjIE0HTP1Ax6x7QX + G3/+Z/qgOZ6UQCxeHOWMEgF1Ufc/tZkzgbvxWJ6gokeToBUwEwYJKoZIhvcNAQkH + MQYMBGZpc2gwCgYIKoZIzj0EAwIDRwAwRAIgUBTdrMDE4BqruYRh1rRyKQBf48WR + kIX8R4dBK9h1VRcCIEBR2Mzvku/huTbWTwKVlXBZeEmwIlxKwpRepPtViXcW + -----END CERTIFICATE REQUEST----- + """ + postData, PEMRecord.class); + + final static Pattern CR = Pattern.compile("\r"); + final static Pattern LF = Pattern.compile("\n"); + final static Pattern LSDEFAULT = Pattern.compile(System.lineSeparator()); + + + public record Entry(String name, String pem, Class clazz, char[] password, + byte[] der) { + + public Entry(String name, String pem, Class clazz, char[] password, + byte[] der) { + this.name = name; + this.pem = pem; + this.clazz = clazz; + this.password = password; + if (pem != null && pem.length() > 0) { + String[] pemtext = pem.split("-----"); + this.der = Base64.getMimeDecoder().decode(pemtext[2]); + } else { + this.der = null; + } + } + Entry(String name, String pem, Class clazz, char[] password) { + this(name, pem, clazz, password, null); + } + + Entry(String name, String pem, Class clazz) { + this(name, pem, clazz, null, null); + } + + public Entry newClass(String name, Class c) { + return new Entry(name, pem, c, password); + } + + public Entry newClass(Class c) { + return newClass(name, c); + } + + Entry makeCRLF(String name) { + return new Entry(name, + Pattern.compile(System.lineSeparator()).matcher(pem).replaceAll("\r\n"), + clazz, password()); + } + + Entry makeCR(String name) { + return new Entry(name, + Pattern.compile(System.lineSeparator()).matcher(pem).replaceAll("\r"), + clazz, password()); + } + + Entry makeNoCRLF(String name) { + return new Entry(name, + LF.matcher(CR.matcher(pem).replaceAll("")). + replaceAll(""), + clazz, password()); + } + } + + static public Entry getEntry(String varname) { + return getEntry(passList, varname); + } + + static public Entry getEntry(List list, String varname) { + for (Entry entry : list) { + if (entry.name.compareToIgnoreCase(varname) == 0) { + return entry; + } + } + return null; + } + + static List passList = new ArrayList<>(); + static List entryList = new ArrayList<>(); + static List pubList = new ArrayList<>(); + static List privList = new ArrayList<>(); + static List oasList = new ArrayList<>(); + static List certList = new ArrayList<>(); + static List encryptedList = new ArrayList<>(); + static List failureEntryList = new ArrayList<>(); + + static { + pubList.add(rsapub); + pubList.add(rsapubbc); + pubList.add(ecsecp256pub.makeCR("ecsecp256pub-r")); + pubList.add(ecsecp256pub.makeCRLF("ecsecp256pub-rn")); + privList.add(rsapriv); + privList.add(rsaprivbc); + privList.add(ecsecp256); + privList.add(ecsecp384); + privList.add(ec25519priv); + privList.add(ed25519ekpi); // The non-EKPI version needs decryption + privList.add(rsaOpenSSL); + oasList.add(oasrfc8410); + oasList.add(oasbcpem); + + certList.add(rsaCert); + certList.add(ecCert); + + entryList.addAll(pubList); + entryList.addAll(privList); + entryList.addAll(oasList); + entryList.addAll(certList); + + encryptedList.add(ed25519ep8); + + passList.addAll(entryList); + passList.addAll(encryptedList); + + failureEntryList.add(new Entry("emptyPEM", "", DEREncodable.class, null)); + failureEntryList.add(new Entry("nullPEM", null, DEREncodable.class, null)); + } + + static void checkResults(PEMData.Entry entry, String result) { + try { + checkResults(entry.pem(), result); + } catch (AssertionError e) { + throw new AssertionError("Encoder PEM mismatch " + + entry.name(), e); + } + } + + static void checkResults(String expected, String result) { + // The below matches the \r\n generated PEM with the PEM passed + // into the test. + String pem = LF.matcher(CR.matcher(expected).replaceAll("")). + replaceAll(""); + result = LF.matcher(CR.matcher(result).replaceAll("")). + replaceAll(""); + try { + if (pem.compareTo(result) != 0) { + System.out.println("expected:\n" + pem); + System.out.println("generated:\n" + result); + indexDiff(pem, result); + } + } catch (AssertionError e) { + throw new AssertionError("Encoder PEM mismatch "); + } + } + + static void indexDiff(String a, String b) { + String lenerr = ""; + int len = a.length(); + int lenb = b.length(); + if (len != lenb) { + lenerr = ": Length mismatch: " + len + " vs " + lenb; + len = Math.min(len, lenb); + } + for (int i = 0; i < len; i++) { + if (a.charAt(i) != b.charAt(i)) { + throw new AssertionError("Char mistmatch, index #" + i + + " (" + a.charAt(i) + " vs " + b.charAt(i) + ")" + lenerr); + } + } + } +} \ No newline at end of file diff --git a/test/jdk/java/security/PEM/PEMDecoderTest.java b/test/jdk/java/security/PEM/PEMDecoderTest.java new file mode 100644 index 0000000000000..6486bfc83c713 --- /dev/null +++ b/test/jdk/java/security/PEM/PEMDecoderTest.java @@ -0,0 +1,510 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8298420 + * @modules java.base/sun.security.pkcs + * java.base/sun.security.util + * @summary Testing basic PEM API decoding + * @enablePreview + */ + +import javax.crypto.EncryptedPrivateKeyInfo; +import java.io.*; +import java.lang.Class; +import java.nio.charset.StandardCharsets; +import java.security.*; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.security.interfaces.*; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.*; +import java.util.Arrays; + +import sun.security.util.Pem; + +public class PEMDecoderTest { + + static HexFormat hex = HexFormat.of(); + + public static void main(String[] args) throws IOException { + System.out.println("Decoder test:"); + PEMData.entryList.forEach(PEMDecoderTest::test); + System.out.println("Decoder test returning DEREncodable class:"); + PEMData.entryList.forEach(entry -> test(entry, DEREncodable.class)); + System.out.println("Decoder test with encrypted PEM:"); + PEMData.encryptedList.forEach(PEMDecoderTest::testEncrypted); + System.out.println("Decoder test with OAS:"); + testTwoKeys(); + System.out.println("Decoder test RSA PEM setting RSAKey.class returned:"); + test(PEMData.rsapriv, RSAKey.class); + System.out.println("Decoder test failures:"); + PEMData.failureEntryList.forEach(PEMDecoderTest::testFailure); + System.out.println("Decoder test ecsecp256 PEM asking for ECPublicKey.class returned:"); + testFailure(PEMData.ecsecp256, ECPublicKey.class); + System.out.println("Decoder test rsapriv PEM setting P8EKS.class returned:"); + testClass(PEMData.rsapriv, RSAPrivateKey.class); + System.out.println("Decoder test rsaOpenSSL P1 PEM asking for RSAPublicKey.class returned:"); + testFailure(PEMData.rsaOpenSSL, RSAPublicKey.class); + System.out.println("Decoder test rsapub PEM asking X509EKS.class returned:"); + testClass(PEMData.rsapub, X509EncodedKeySpec.class, true); + System.out.println("Decoder test rsapriv PEM asking X509EKS.class returned:"); + testClass(PEMData.rsapriv, X509EncodedKeySpec.class, false); + System.out.println("Decoder test RSAcert PEM asking X509EKS.class returned:"); + testClass(PEMData.rsaCert, X509EncodedKeySpec.class, false); + System.out.println("Decoder test OAS RFC PEM asking PrivateKey.class returned:"); + testClass(PEMData.oasrfc8410, PrivateKey.class, true); + testClass(PEMData.oasrfc8410, PublicKey.class, true); + System.out.println("Decoder test ecsecp256:"); + testFailure(PEMData.ecsecp256pub.makeNoCRLF("pubecpem-no")); + System.out.println("Decoder test RSAcert with decryption Decoder:"); + PEMDecoder d = PEMDecoder.of().withDecryption("123".toCharArray()); + d.decode(PEMData.rsaCert.pem()); + System.out.println("Decoder test ecsecp256 private key with decryption Decoder:"); + ((KeyPair) d.decode(PEMData.ecsecp256.pem())).getPrivate(); + System.out.println("Decoder test ecsecp256 to P8EKS:"); + d.decode(PEMData.ecsecp256.pem(), PKCS8EncodedKeySpec.class); + + System.out.println("Checking if decode() returns the same encoding:"); + PEMData.privList.forEach(PEMDecoderTest::testDERCheck); + PEMData.oasList.forEach(PEMDecoderTest::testDERCheck); + + System.out.println("Check a Signature/Verify op is successful:"); + PEMData.privList.forEach(PEMDecoderTest::testSignature); + PEMData.oasList.forEach(PEMDecoderTest::testSignature); + + System.out.println("Checking if ecCSR:"); + test(PEMData.ecCSR); + System.out.println("Checking if ecCSR with preData:"); + DEREncodable result = PEMDecoder.of().decode(PEMData.ecCSRWithData.pem(), PEMRecord.class); + if (result instanceof PEMRecord rec) { + if (PEMData.preData.compareTo(new String(rec.leadingData())) != 0) { + System.err.println("expected: " + PEMData.preData); + 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()); + throw new AssertionError("ecCSRWithData: " + + "End of PEM data has an unexpected character"); + } + } else { + throw new AssertionError("ecCSRWithData didn't return a PEMRecord"); + } + + System.out.println("Decoding RSA pub using class PEMRecord:"); + result = PEMDecoder.of().decode(PEMData.rsapub.pem(), PEMRecord.class); + if (!(result instanceof PEMRecord)) { + throw new AssertionError("pubecpem didn't return a PEMRecord"); + } + if (((PEMRecord) result).type().compareTo(Pem.PUBLIC_KEY) != 0) { + throw new AssertionError("pubecpem PEMRecord didn't decode as a Public Key"); + } + + testInputStream(); + testPEMRecord(PEMData.rsapub); + testPEMRecord(PEMData.ecCert); + testPEMRecord(PEMData.ec25519priv); + testPEMRecord(PEMData.ecCSR); + testPEMRecord(PEMData.ecCSRWithData); + testPEMRecordDecode(PEMData.rsapub); + testPEMRecordDecode(PEMData.ecCert); + testPEMRecordDecode(PEMData.ec25519priv); + testPEMRecordDecode(PEMData.ecCSR); + testPEMRecordDecode(PEMData.ecCSRWithData); + + d = PEMDecoder.of(); + System.out.println("Check leadingData is null with back-to-back PEMs: "); + String s = new PEMRecord("ONE", "1212").toString() + + new PEMRecord("TWO", "3434").toString(); + var ins = new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)); + if (d.decode(ins, PEMRecord.class).leadingData() != null) { + throw new AssertionError("leading data not null on first pem"); + } + if (d.decode(ins, PEMRecord.class).leadingData() != null) { + throw new AssertionError("leading data not null on second pem"); + } + System.out.println("PASS"); + + System.out.println("Decode to EncryptedPrivateKeyInfo: "); + EncryptedPrivateKeyInfo ekpi = + d.decode(PEMData.ed25519ep8.pem(), EncryptedPrivateKeyInfo.class); + PrivateKey privateKey; + try { + privateKey = ekpi.getKey(PEMData.ed25519ep8.password()); + System.out.println("PASS"); + } catch (GeneralSecurityException e) { + throw new AssertionError("ed25519ep8 error", e); + } + + // PBE + System.out.println("EncryptedPrivateKeyInfo.encryptKey with PBE: "); + ekpi = EncryptedPrivateKeyInfo.encryptKey(privateKey, + "password".toCharArray(),"PBEWithMD5AndDES", null, null); + try { + ekpi.getKey("password".toCharArray()); + System.out.println("PASS"); + } catch (Exception e) { + throw new AssertionError("error getting key", e); + } + + // PBES2 + System.out.println("EncryptedPrivateKeyInfo.encryptKey with default: "); + ekpi = EncryptedPrivateKeyInfo.encryptKey(privateKey + , "password".toCharArray()); + try { + ekpi.getKey("password".toCharArray()); + System.out.println("PASS"); + } catch (Exception e) { + throw new AssertionError("error getting key", e); + } + } + + static void testInputStream() throws IOException { + ByteArrayOutputStream ba = new ByteArrayOutputStream(2048); + OutputStreamWriter os = new OutputStreamWriter(ba); + os.write(PEMData.preData); + os.write(PEMData.rsapub.pem()); + os.write(PEMData.preData); + os.write(PEMData.rsapub.pem()); + os.write(PEMData.postData); + os.flush(); + ByteArrayInputStream is = new ByteArrayInputStream(ba.toByteArray()); + + System.out.println("Decoding 2 RSA pub with pre & post data:"); + PEMRecord obj; + int keys = 0; + while (keys++ < 2) { + obj = PEMDecoder.of().decode(is, PEMRecord.class); + if (!PEMData.preData.equalsIgnoreCase( + new String(obj.leadingData()))) { + System.out.println("expected: \"" + PEMData.preData + "\""); + System.out.println("returned: \"" + + new String(obj.leadingData()) + "\""); + throw new AssertionError("Leading data incorrect"); + } + System.out.println(" Read public key."); + } + try { + PEMDecoder.of().decode(is, PEMRecord.class); + throw new AssertionError("3rd entry returned a PEMRecord"); + } catch (EOFException e) { + System.out.println("Success: No 3rd entry found. EOFE thrown."); + } + + // End of stream + try { + System.out.println("Failed: There should be no PEMRecord: " + + PEMDecoder.of().decode(is, PEMRecord.class)); + } catch (EOFException e) { + System.out.println("Success"); + return; + } catch (Exception e) { + throw new AssertionError("Caught unexpected exception " + + "should have been IOE EOF."); + } + + throw new AssertionError("Failed"); + } + + static void testPEMRecord(PEMData.Entry entry) { + PEMRecord r = PEMDecoder.of().decode(entry.pem(), PEMRecord.class); + String expected = entry.pem().split("-----")[2].replace(System.lineSeparator(), ""); + try { + PEMData.checkResults(expected, r.pem()); + } catch (AssertionError e) { + System.err.println("expected:\n" + expected); + System.err.println("received:\n" + r.pem()); + throw e; + } + + boolean result = switch(r.type()) { + case Pem.PRIVATE_KEY -> + PrivateKey.class.isAssignableFrom(entry.clazz()); + case Pem.PUBLIC_KEY -> + PublicKey.class.isAssignableFrom(entry.clazz()); + case Pem.CERTIFICATE, Pem.X509_CERTIFICATE -> + entry.clazz().isAssignableFrom(X509Certificate.class); + case Pem.X509_CRL -> + entry.clazz().isAssignableFrom(X509CRL.class); + case "CERTIFICATE REQUEST" -> + entry.clazz().isAssignableFrom(PEMRecord.class); + default -> false; + }; + + if (!result) { + System.err.println("PEMRecord type is a " + r.type()); + System.err.println("Entry is a " + entry.clazz().getName()); + throw new AssertionError("PEMRecord class didn't match:" + + entry.name()); + } + System.out.println("Success (" + entry.name() + ")"); + } + + + static void testPEMRecordDecode(PEMData.Entry entry) { + PEMRecord r = PEMDecoder.of().decode(entry.pem(), PEMRecord.class); + DEREncodable de = PEMDecoder.of().decode(r.toString()); + + boolean result = switch(r.type()) { + case Pem.PRIVATE_KEY -> + PrivateKey.class.isAssignableFrom(de.getClass()); + case Pem.PUBLIC_KEY -> + PublicKey.class.isAssignableFrom(de.getClass()); + case Pem.CERTIFICATE, Pem.X509_CERTIFICATE -> + (de instanceof X509Certificate); + case Pem.X509_CRL -> (de instanceof X509CRL); + case "CERTIFICATE REQUEST" -> (de instanceof PEMRecord); + default -> false; + }; + + if (!result) { + System.err.println("Entry is a " + entry.clazz().getName()); + System.err.println("PEMRecord type is a " + r.type()); + System.err.println("Returned was a " + entry.clazz().getName()); + throw new AssertionError("PEMRecord class didn't match:" + + entry.name()); + } + System.out.println("Success (" + entry.name() + ")"); + } + + + static void testFailure(PEMData.Entry entry) { + testFailure(entry, entry.clazz()); + } + + static void testFailure(PEMData.Entry entry, Class c) { + try { + test(entry.pem(), c, PEMDecoder.of()); + if (entry.pem().indexOf('\r') != -1) { + System.out.println("Found a CR."); + } + if (entry.pem().indexOf('\n') != -1) { + System.out.println("Found a LF"); + } + throw new AssertionError("Failure with " + + entry.name() + ": Not supposed to succeed."); + } catch (NullPointerException e) { + System.out.println("PASS (" + entry.name() + "): " + e.getClass() + + ": " + e.getMessage()); + } catch (IOException | RuntimeException e) { + System.out.println("PASS (" + entry.name() + "): " + e.getClass() + + ": " + e.getMessage()); + } + } + + static DEREncodable testEncrypted(PEMData.Entry entry) { + PEMDecoder decoder = PEMDecoder.of(); + if (!Objects.equals(entry.clazz(), EncryptedPrivateKeyInfo.class)) { + decoder = decoder.withDecryption(entry.password()); + } + + try { + return test(entry.pem(), entry.clazz(), decoder); + } catch (Exception | AssertionError e) { + throw new RuntimeException("Error with PEM (" + entry.name() + + "): " + e.getMessage(), e); + } + } + + // Change the Entry to use the given class as the expected class returned + static DEREncodable test(PEMData.Entry entry, Class c) { + return test(entry.newClass(c)); + } + + // Run test with a given Entry + static DEREncodable test(PEMData.Entry entry) { + try { + DEREncodable r = test(entry.pem(), entry.clazz(), PEMDecoder.of()); + System.out.println("PASS (" + entry.name() + ")"); + return r; + } catch (Exception | AssertionError e) { + throw new RuntimeException("Error with PEM (" + entry.name() + + "): " + e.getMessage(), e); + } + } + + static List getInterfaceList(Class ccc) { + Class[] interfaces = ccc.getInterfaces(); + List list = new ArrayList<>(Arrays.asList(interfaces)); + var x = ccc.getSuperclass(); + if (x != null) { + list.add(x); + } + List results = new ArrayList<>(list); + if (list.size() > 0) { + for (Class cname : list) { + try { + if (cname != null && + cname.getName().startsWith("java.security.")) { + results.addAll(getInterfaceList(cname)); + } + } catch (Exception e) { + System.err.println("Exception with " + cname); + } + } + } + return results; + } + + /** + * Perform the decoding test with the given decoder, on the given pem, and + * expect the clazz to be returned. + */ + static DEREncodable test(String pem, Class clazz, PEMDecoder decoder) + throws IOException { + DEREncodable pk = decoder.decode(pem); + + // Check that clazz matches what pk returned. + if (pk.getClass().equals(clazz)) { + return pk; + } + + // Search interfaces and inheritance to find a match with clazz + List list = getInterfaceList(pk.getClass()); + for (Class cc : list) { + if (cc != null && cc.equals(clazz)) { + return pk; + } + } + + throw new RuntimeException("Entry did not contain expected: " + + clazz.getName()); + } + + // Run the same key twice through the same decoder and make sure the + // result is the same + static void testTwoKeys() throws IOException { + PublicKey p1, p2; + PEMDecoder pd = PEMDecoder.of(); + p1 = pd.decode(PEMData.rsapub.pem(), RSAPublicKey.class); + p2 = pd.decode(PEMData.rsapub.pem(), RSAPublicKey.class); + if (!Arrays.equals(p1.getEncoded(), p2.getEncoded())) { + System.err.println("These two should have matched:"); + System.err.println(hex.parseHex(new String(p1.getEncoded()))); + System.err.println(hex.parseHex(new String(p2.getEncoded()))); + throw new AssertionError("Two decoding of the same" + + " key failed to match: "); + } + } + + static void testClass(PEMData.Entry entry, Class clazz) throws IOException { + var pk = PEMDecoder.of().decode(entry.pem(), clazz); + } + + static void testClass(PEMData.Entry entry, Class clazz, boolean pass) + throws RuntimeException { + try { + testClass(entry, clazz); + } catch (Exception e) { + if (pass) { + throw new RuntimeException(e); + } + } + } + + // Run test with a given Entry + static void testDERCheck(PEMData.Entry entry) { + if (entry.name().equals("rsaOpenSSL") || // PKCS1 data + entry.name().equals("ed25519ekpi")) { + return; + } + + PKCS8EncodedKeySpec p8 = PEMDecoder.of().decode(entry.pem(), + PKCS8EncodedKeySpec.class); + int result = Arrays.compare(entry.der(), p8.getEncoded()); + if (result != 0) { + System.err.println("Compare error with " + entry.name() + "(" + + result + ")"); + System.err.println("Expected DER: " + HexFormat.of(). + formatHex(entry.der())); + System.err.println("Returned DER: " + HexFormat.of(). + formatHex(p8.getEncoded())); + throw new AssertionError("Failed to match " + + "expected DER"); + } + System.out.println("PASS (" + entry.name() + ")"); + System.out.flush(); + } + + /** + * Run decoded keys through Signature to make sure they are valid keys + */ + static void testSignature(PEMData.Entry entry) { + Signature s; + byte[] data = "12345678".getBytes(); + PrivateKey privateKey; + + DEREncodable d = PEMDecoder.of().decode(entry.pem()); + switch (d) { + case PrivateKey p -> privateKey = p; + case KeyPair kp -> privateKey = kp.getPrivate(); + case EncryptedPrivateKeyInfo e -> { + System.out.println("SKIP: EncryptedPrivateKeyInfo " + + entry.name()); + return; + } + default -> throw new AssertionError("Private key " + + "should not be null"); + } + + String algorithm = switch(privateKey.getAlgorithm()) { + case "EC" -> "SHA256withECDSA"; + case "EdDSA" -> "EdDSA"; + case null -> { + System.out.println("Algorithm is null " + + entry.name()); + throw new AssertionError("PrivateKey algorithm" + + "should not be null"); + } + default -> "SHA256with" + privateKey.getAlgorithm(); + }; + + try { + if (d instanceof PrivateKey) { + s = Signature.getInstance(algorithm); + s.initSign(privateKey); + s.update(data); + s.sign(); + System.out.println("PASS (Sign): " + entry.name()); + } else if (d instanceof KeyPair) { + s = Signature.getInstance(algorithm); + s.initSign(privateKey); + s.update(data); + byte[] sig = s.sign(); + s.initVerify(((KeyPair)d).getPublic()); + s.verify(sig); + System.out.println("PASS (Sign/Verify): " + entry.name()); + } else { + System.out.println("SKIP: " + entry.name()); + } + } catch (Exception e) { + System.out.println("FAIL: " + entry.name()); + throw new AssertionError(e); + } + } +} \ No newline at end of file diff --git a/test/jdk/java/security/PEM/PEMEncoderTest.java b/test/jdk/java/security/PEM/PEMEncoderTest.java new file mode 100644 index 0000000000000..c8b19c313a5b7 --- /dev/null +++ b/test/jdk/java/security/PEM/PEMEncoderTest.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8298420 + * @summary Testing basic PEM API encoding + * @enablePreview + * @modules java.base/sun.security.util + */ + +import sun.security.util.Pem; + +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.spec.PBEParameterSpec; + +import java.nio.charset.StandardCharsets; +import java.security.*; +import java.security.spec.InvalidParameterSpecException; +import java.util.*; + +public class PEMEncoderTest { + + static Map keymap; + + public static void main(String[] args) throws Exception { + PEMEncoder encoder = PEMEncoder.of(); + + // These entries are removed + var newEntryList = new ArrayList<>(PEMData.entryList); + newEntryList.remove(PEMData.getEntry("rsaOpenSSL")); + newEntryList.remove(PEMData.getEntry("ecsecp256")); + newEntryList.remove(PEMData.getEntry("ecsecp384")); + keymap = generateObjKeyMap(newEntryList); + System.out.println("Same instance re-encode test:"); + keymap.keySet().stream().forEach(key -> test(key, encoder)); + System.out.println("New instance re-encode test:"); + keymap.keySet().stream().forEach(key -> test(key, PEMEncoder.of())); + System.out.println("Same instance re-encode testToString:"); + keymap.keySet().stream().forEach(key -> testToString(key, encoder)); + System.out.println("New instance re-encode testToString:"); + keymap.keySet().stream().forEach(key -> testToString(key, + PEMEncoder.of())); + + keymap = generateObjKeyMap(PEMData.encryptedList); + System.out.println("Same instance Encoder match test:"); + keymap.keySet().stream().forEach(key -> testEncryptedMatch(key, encoder)); + System.out.println("Same instance Encoder new withEnc test:"); + keymap.keySet().stream().forEach(key -> testEncrypted(key, encoder)); + System.out.println("New instance Encoder and withEnc test:"); + keymap.keySet().stream().forEach(key -> testEncrypted(key, PEMEncoder.of())); + System.out.println("Same instance encrypted Encoder test:"); + PEMEncoder encEncoder = encoder.withEncryption("fish".toCharArray()); + keymap.keySet().stream().forEach(key -> testSameEncryptor(key, encEncoder)); + try { + encoder.withEncryption(null); + } catch (Exception e) { + if (!(e instanceof NullPointerException)) { + throw new Exception("Should have been a NullPointerException thrown"); + } + } + + PEMDecoder d = PEMDecoder.of(); + PEMRecord pemRecord = + d.decode(PEMData.ed25519ep8.pem(), PEMRecord.class); + PEMData.checkResults(PEMData.ed25519ep8, pemRecord.toString()); + } + + static Map generateObjKeyMap(List list) { + Map keymap = new HashMap<>(); + PEMDecoder pemd = PEMDecoder.of(); + for (PEMData.Entry entry : list) { + try { + if (entry.password() != null) { + keymap.put(entry.name(), pemd.withDecryption( + entry.password()).decode(entry.pem())); + } else { + keymap.put(entry.name(), pemd.decode(entry.pem(), + entry.clazz())); + } + } catch (Exception e) { + System.err.println("Verify PEMDecoderTest passes before " + + "debugging this test."); + throw new AssertionError("Failed to initialize map on" + + " entry \"" + entry.name() + "\"", e); + } + } + return keymap; + } + + static void test(String key, PEMEncoder encoder) { + byte[] result; + PEMData.Entry entry = PEMData.getEntry(key); + try { + result = encoder.encode(keymap.get(key)); + } catch (RuntimeException e) { + throw new AssertionError("Encoder use failure with " + + entry.name(), e); + } + + PEMData.checkResults(entry, new String(result, StandardCharsets.UTF_8)); + System.out.println("PASS: " + entry.name()); + } + + static void testToString(String key, PEMEncoder encoder) { + String result; + PEMData.Entry entry = PEMData.getEntry(key); + try { + result = encoder.encodeToString(keymap.get(key)); + } catch (RuntimeException e) { + throw new AssertionError("Encoder use failure with " + + entry.name(), e); + } + + PEMData.checkResults(entry, result); + System.out.println("PASS: " + entry.name()); + } + + /* + Test cannot verify PEM was the same as known PEM because we have no + public access to the AlgoritmID.params and PBES2Parameters. + */ + static void testEncrypted(String key, PEMEncoder encoder) { + PEMData.Entry entry = PEMData.getEntry(key); + try { + encoder.withEncryption( + (entry.password() != null ? entry.password() : + "fish".toCharArray())) + .encodeToString(keymap.get(key)); + } catch (RuntimeException e) { + throw new AssertionError("Encrypted encoder failed with " + + entry.name(), e); + } + + System.out.println("PASS: " + entry.name()); + } + + /* + Test cannot verify PEM was the same as known PEM because we have no + public access to the AlgoritmID.params and PBES2Parameters. + */ + static void testSameEncryptor(String key, PEMEncoder encoder) { + PEMData.Entry entry = PEMData.getEntry(key); + try { + encoder.encodeToString(keymap.get(key)); + } catch (RuntimeException e) { + throw new AssertionError("Encrypted encoder failured with " + + entry.name(), e); + } + + System.out.println("PASS: " + entry.name()); + } + + static void testEncryptedMatch(String key, PEMEncoder encoder) { + String result; + PEMData.Entry entry = PEMData.getEntry(key); + try { + PrivateKey pkey = (PrivateKey) keymap.get(key); + EncryptedPrivateKeyInfo ekpi = PEMDecoder.of().decode(entry.pem(), + EncryptedPrivateKeyInfo.class); + if (entry.password() != null) { + EncryptedPrivateKeyInfo.encryptKey(pkey, entry.password(), + Pem.DEFAULT_ALGO, ekpi.getAlgParameters(). + getParameterSpec(PBEParameterSpec.class), + null); + } + result = encoder.encodeToString(ekpi); + } catch (RuntimeException | InvalidParameterSpecException e) { + throw new AssertionError("Encrypted encoder failure with " + + entry.name(), e); + } + + PEMData.checkResults(entry, result); + System.out.println("PASS: " + entry.name()); + } +} + diff --git a/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/EncryptKey.java b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/EncryptKey.java new file mode 100644 index 0000000000000..d1fccd9730ab0 --- /dev/null +++ b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/EncryptKey.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8298420 + * @summary Testing encryptKey + * @enablePreview + */ + +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.SecretKey; +import javax.crypto.spec.PBEParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.AlgorithmParameters; +import java.security.PEMDecoder; +import java.security.PrivateKey; +import java.util.Arrays; + +public class EncryptKey { + + private static final String encEdECKey = + """ + -----BEGIN ENCRYPTED PRIVATE KEY----- + MIGqMGYGCSqGSIb3DQEFDTBZMDgGCSqGSIb3DQEFDDArBBRyYnoNyrcqvubzch00 + jyuAb5YizgICEAACARAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEAQIEEM8BgEgO + vdMyi46+Dw7cOjwEQLtx5ME0NOOo7vlCGm3H/4j+Tf5UXrMb1UrkPjqc8OiLbC0n + IycFtI70ciPjgwDSjtCcPxR8fSxJPrm2yOJsRVo= + -----END ENCRYPTED PRIVATE KEY----- + """; + private static final String passwdText = "fish"; + private static final char[] password = passwdText.toCharArray(); + private static final SecretKey key = new SecretKeySpec( + passwdText.getBytes(), "PBE"); + + public static void main(String[] args) throws Exception { + EncryptedPrivateKeyInfo ekpi = PEMDecoder.of().decode(encEdECKey, + EncryptedPrivateKeyInfo.class); + PrivateKey priKey = PEMDecoder.of().withDecryption(password). + decode(encEdECKey, PrivateKey.class); + AlgorithmParameters ap = ekpi.getAlgParameters(); + + // Test encryptKey(PrivateKey, char[], String, ... ) + var e = EncryptedPrivateKeyInfo.encryptKey(priKey, password, + ekpi.getAlgName(), ap.getParameterSpec(PBEParameterSpec.class), + null); + if (!Arrays.equals(ekpi.getEncryptedData(), e.getEncryptedData())) { + throw new AssertionError("encryptKey() didn't match" + + " with expected."); + } + + // Test encryptKey(PrivateKey, Key, String, ...) + e = EncryptedPrivateKeyInfo.encryptKey(priKey, key, ekpi.getAlgName(), + ap.getParameterSpec(PBEParameterSpec.class),null, null); + if (!Arrays.equals(ekpi.getEncryptedData(), e.getEncryptedData())) { + throw new AssertionError("encryptKey() didn't match" + + " with expected."); + } + } +} diff --git a/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKey.java b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKey.java new file mode 100644 index 0000000000000..7c8951b3417d6 --- /dev/null +++ b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKey.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8298420 + * @summary Testing getKey + * @enablePreview + */ + +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.PEMDecoder; +import java.security.PrivateKey; +import java.util.Arrays; + +public class GetKey { + + private static final String encEdECKey = + """ + -----BEGIN ENCRYPTED PRIVATE KEY----- + MIGqMGYGCSqGSIb3DQEFDTBZMDgGCSqGSIb3DQEFDDArBBRyYnoNyrcqvubzch00 + jyuAb5YizgICEAACARAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEAQIEEM8BgEgO + vdMyi46+Dw7cOjwEQLtx5ME0NOOo7vlCGm3H/4j+Tf5UXrMb1UrkPjqc8OiLbC0n + IycFtI70ciPjgwDSjtCcPxR8fSxJPrm2yOJsRVo= + -----END ENCRYPTED PRIVATE KEY----- + """; + private static final String passwdText = "fish"; + private static final char[] password = passwdText.toCharArray(); + private static final SecretKey key = new SecretKeySpec( + passwdText.getBytes(), "PBE"); + + public static void main(String[] args) throws Exception { + EncryptedPrivateKeyInfo ekpi = PEMDecoder.of().decode(encEdECKey, + EncryptedPrivateKeyInfo.class); + PrivateKey priKey = PEMDecoder.of().withDecryption(password). + decode(encEdECKey, PrivateKey.class); + + // Test getKey(password) + if (!Arrays.equals(priKey.getEncoded(), + ekpi.getKey(password).getEncoded())) { + throw new AssertionError("getKey(char[]) didn't " + + "match with expected."); + } + + // Test getKey(key, provider) + if (!Arrays.equals(priKey.getEncoded(), + ekpi.getKey(key, null).getEncoded())) { + throw new AssertionError("getKey(key, provider) " + + "didn't match with expected."); + } + } +} diff --git a/test/jdk/sun/security/pkcs/pkcs8/PKCS8Test.java b/test/jdk/sun/security/pkcs/pkcs8/PKCS8Test.java index f4ac02c798263..49cca69971fbd 100644 --- a/test/jdk/sun/security/pkcs/pkcs8/PKCS8Test.java +++ b/test/jdk/sun/security/pkcs/pkcs8/PKCS8Test.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,22 +30,18 @@ * java.base/sun.security.util * java.base/sun.security.provider * java.base/sun.security.x509 - * @compile -XDignore.symbol.file PKCS8Test.java - * @run testng PKCS8Test + * @run main PKCS8Test */ -import java.io.IOException; import java.math.BigInteger; +import java.security.InvalidKeyException; import java.util.Arrays; import java.util.HexFormat; import jdk.test.lib.hexdump.ASN1Formatter; import jdk.test.lib.hexdump.HexPrinter; -import org.testng.Assert; -import org.testng.annotations.Test; import sun.security.pkcs.PKCS8Key; import sun.security.provider.DSAPrivateKey; -import sun.security.util.DerValue; public class PKCS8Test { @@ -62,8 +58,7 @@ public class PKCS8Test { "3009020102020103020104" + // p=2, q=3, g=4 "0403020101"); // PrivateKey OCTET int x = 1 - @Test - public void test() throws IOException { + public static void main(String[] args) throws Exception { byte[] encodedKey = new DSAPrivateKey( BigInteger.valueOf(1), @@ -71,34 +66,45 @@ public void test() throws IOException { BigInteger.valueOf(3), BigInteger.valueOf(4)).getEncoded(); - Assert.assertTrue(Arrays.equals(encodedKey, EXPECTED), + if (!Arrays.equals(encodedKey, EXPECTED)) { + throw new AssertionError( HexPrinter.simple() - .formatter(ASN1Formatter.formatter()) - .toString(encodedKey)); + .formatter(ASN1Formatter.formatter()) + .toString(encodedKey)); + } PKCS8Key decodedKey = (PKCS8Key)PKCS8Key.parseKey(encodedKey); - Assert.assertEquals(decodedKey.getAlgorithm(), ALGORITHM); - Assert.assertEquals(decodedKey.getFormat(), FORMAT); - Assert.assertEquals(decodedKey.getAlgorithmId().toString(), - EXPECTED_ALG_ID_CHRS); + assert(ALGORITHM.equalsIgnoreCase(decodedKey.getAlgorithm())); + assert(FORMAT.equalsIgnoreCase(decodedKey.getFormat())); + assert(EXPECTED_ALG_ID_CHRS.equalsIgnoreCase(decodedKey.getAlgorithmId().toString())); byte[] encodedOutput = decodedKey.getEncoded(); - Assert.assertTrue(Arrays.equals(encodedOutput, EXPECTED), + if (!Arrays.equals(encodedOutput, EXPECTED)) { + + throw new AssertionError( HexPrinter.simple() - .formatter(ASN1Formatter.formatter()) - .toString(encodedOutput)); + .formatter(ASN1Formatter.formatter()) + .toString(encodedOutput)); + } // Test additional fields enlarge(0, "8000"); // attributes - enlarge(1, "810100"); // public key for v2 - enlarge(1, "8000", "810100"); // both - - Assert.assertThrows(() -> enlarge(2)); // bad ver - Assert.assertThrows(() -> enlarge(0, "8000", "8000")); // no dup - Assert.assertThrows(() -> enlarge(0, "810100")); // no public in v1 - Assert.assertThrows(() -> enlarge(1, "810100", "8000")); // bad order - Assert.assertThrows(() -> enlarge(1, "820100")); // bad tag + + // PKCSv2 testing done by PEMEncoder/PEMDecoder tests + + assertThrows(() -> enlarge(2)); + assertThrows(() -> enlarge(0, "8000", "8000")); // no dup + assertThrows(() -> enlarge(0, "810100")); // no public in v1 + assertThrows(() -> enlarge(1, "810100", "8000")); // bad order + assertThrows(() -> enlarge(1, "820100")); // bad tag + } + + private static void assertThrows(Runnable o) { + try { + o.run(); + throw new AssertionError("Test failed"); + } catch (Exception e) {} } /** @@ -107,7 +113,7 @@ public void test() throws IOException { * @param newVersion new version * @param fields extra fields to add, in hex */ - static void enlarge(int newVersion, String... fields) throws IOException { + static void enlarge(int newVersion, String... fields) { byte[] original = EXPECTED.clone(); int length = original.length; for (String field : fields) { // append fields @@ -116,9 +122,13 @@ static void enlarge(int newVersion, String... fields) throws IOException { System.arraycopy(add, 0, original, length, add.length); length += add.length; } - Assert.assertTrue(length < 127); - original[1] = (byte)(length - 2); // the length field inside DER - original[4] = (byte)newVersion; // the version inside DER - PKCS8Key.parseKey(original); + assert (length < 127); + original[1] = (byte) (length - 2); // the length field inside DER + original[4] = (byte) newVersion; // the version inside DER + try { + PKCS8Key.parseKey(original); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } } }