510 lines
21 KiB
Java
510 lines
21 KiB
Java
/*
|
|
* 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.content().lastIndexOf("F") > rec.content().length() - 5) {
|
|
System.err.println("received: " + rec.content());
|
|
throw new AssertionError("ecCSRWithData: " +
|
|
"End of PEM data has an unexpected character");
|
|
}
|
|
} 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.content());
|
|
} catch (AssertionError e) {
|
|
System.err.println("expected:\n" + expected);
|
|
System.err.println("received:\n" + r.content());
|
|
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<Class> list = new ArrayList<>(Arrays.asList(interfaces));
|
|
var x = ccc.getSuperclass();
|
|
if (x != null) {
|
|
list.add(x);
|
|
}
|
|
List<Class> 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<Class> 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);
|
|
}
|
|
}
|
|
} |