Add pem-api module: PEMDecoder/PEMEncoder (JEP 538 preview on JDK 27, JEP 542 final in JDK 28) for JWT keys, TLS, encrypted keys and BouncyCastle migration

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CifR2BE7PBvRy5hT8TS9oz
This commit is contained in:
2026-10-26 01:50:05 +00:00
co-authored by Claude Sonnet 5.5
parent c3cc57f482
commit 2e25042a68
34 changed files with 895 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
import java.io.ByteArrayInputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.security.*;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPrivateKey;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.openssl.*;
import org.bouncycastle.openssl.jcajce.*;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.springframework.security.converter.RsaKeyConverters;
/** The same jobs done with BouncyCastle (bcpkix), with Spring Security's RsaKeyConverters, and with the JDK. */
public class Migration {
static final char[] PW = "changeit".toCharArray();
static Object bcParse(String pem) throws Exception {
try (PEMParser p = new PEMParser(new StringReader(pem))) { return p.readObject(); }
}
public static void main(String[] args) throws Exception {
PEMDecoder jdk = PEMDecoder.of();
JcaPEMKeyConverter conv = new JcaPEMKeyConverter();
Fx.h("1. load an RSA private key (PKCS#8)");
PrivateKey viaBc = conv.getPrivateKey((PrivateKeyInfo) bcParse(Fx.read("rsa-private-pkcs8.pem")));
PrivateKey viaJdk = jdk.decode(Fx.read("rsa-private-pkcs8.pem"), PrivateKey.class);
Fx.check(viaBc.equals(viaJdk), "BouncyCastle and the JDK produce equal keys");
System.out.println("BC parser returned: " + bcParse(Fx.read("rsa-private-pkcs8.pem")).getClass().getSimpleName());
Fx.h("2. load a certificate");
X509CertificateHolder holder = (X509CertificateHolder) bcParse(Fx.read("server.pem"));
X509Certificate c1 = new JcaX509CertificateConverter().getCertificate(holder);
X509Certificate c2 = jdk.decode(Fx.read("server.pem"), X509Certificate.class);
Fx.check(c1.equals(c2), "same certificate either way");
Fx.h("3. write keys");
StringWriter sw = new StringWriter();
try (JcaPEMWriter w = new JcaPEMWriter(sw)) { w.writeObject(c2.getPublicKey()); }
Fx.check(sw.toString().replace("\r\n", "\n").equals(PEMEncoder.of().encodeToString(c2.getPublicKey()).replace("\r\n", "\n")), "JcaPEMWriter and PEMEncoder write identical public-key text (line terminators aside)");
sw = new StringWriter();
try (JcaPEMWriter w = new JcaPEMWriter(sw)) { w.writeObject(viaJdk); }
System.out.println("JcaPEMWriter on an RSA private key writes: " + sw.toString().lines().findFirst().get());
System.out.println("PEMEncoder on an RSA private key writes: " + PEMEncoder.of().encodeToString(viaJdk).lines().findFirst().get());
Fx.h("4. encrypted PKCS#8 key (BC needs its own provider for AES/CBC/PKCS7Padding; the JDK does not)");
PKCS8EncryptedPrivateKeyInfo bcEnc = (PKCS8EncryptedPrivateKeyInfo) bcParse(Fx.read("rsa-private-encrypted.pem"));
InputDecryptorProvider dec = new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()).build(PW);
PrivateKey k1 = conv.getPrivateKey(bcEnc.decryptPrivateKeyInfo(dec));
PrivateKey k2 = jdk.withDecryption(PW).decode(Fx.read("rsa-private-encrypted.pem"), PrivateKey.class);
Fx.check(k1.equals(k2), "same key either way");
Fx.h("5. what BouncyCastle reads and the JDK does not");
Object sec1 = bcParse(Fx.read("ec-private-sec1.pem"));
KeyPair bcPair = conv.getKeyPair((PEMKeyPair) sec1);
System.out.println("EC PRIVATE KEY (SEC1): BC -> " + sec1.getClass().getSimpleName() + ", JDK -> " + Fx.describe(jdk.decode(Fx.read("ec-private-sec1.pem"))));
PrivateKey ecViaJdk = jdk.decode(Fx.read("ec-private-pkcs8.pem"), PrivateKey.class);
Fx.check(((ECPrivateKey) bcPair.getPrivate()).getS().equals(((ECPrivateKey) ecViaJdk).getS()), "after 'openssl pkcs8 -topk8 -nocrypt' the JDK reads the same secret scalar BC read from SEC1");
Object legacy = bcParse(Fx.read("rsa-private-legacy-encrypted.pem"));
PEMDecryptorProvider legacyDec = new JcePEMDecryptorProviderBuilder().setProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()).build(PW);
KeyPair legacyKp = conv.getKeyPair(((PEMEncryptedKeyPair) legacy).decryptKeyPair(legacyDec));
System.out.println("Proc-Type: 4,ENCRYPTED: BC -> " + legacy.getClass().getSimpleName() + " decrypted to " + legacyKp.getPrivate().getAlgorithm());
Fx.check(legacyKp.getPrivate().equals(viaJdk), "...and it is the same RSA key");
Fx.tryIt("Proc-Type: 4,ENCRYPTED: JDK", () -> jdk.withDecryption(PW).decode(Fx.read("rsa-private-legacy-encrypted.pem"), PrivateKey.class));
Fx.h("6. Spring Security RsaKeyConverters (RSA only; PKCS#8 and X.509 only)");
Fx.check(RsaKeyConverters.pkcs8().convert(new ByteArrayInputStream(Fx.read("rsa-private-pkcs8.pem").getBytes())).equals(viaJdk), "pkcs8() reads the RSA private key");
Fx.check(RsaKeyConverters.x509().convert(new ByteArrayInputStream(Fx.read("rsa-public.pem").getBytes())).equals(jdk.decode(Fx.read("rsa-public.pem"), PublicKey.class)), "x509() reads the RSA public key");
Fx.tryIt("pkcs8() on an EC key", () -> RsaKeyConverters.pkcs8().convert(new ByteArrayInputStream(Fx.read("ec-private-pkcs8.pem").getBytes())));
Fx.tryIt("pkcs8() on the PKCS#1 key", () -> RsaKeyConverters.pkcs8().convert(new ByteArrayInputStream(Fx.read("rsa-private-pkcs1.pem").getBytes())));
Fx.tryIt("JDK on the EC key", () -> jdk.decode(Fx.read("ec-private-pkcs8.pem"), PrivateKey.class));
}
}