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
+50
View File
@@ -0,0 +1,50 @@
import java.security.*;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
/** The smallest thing that works: PEM text to objects and back. */
public class RoundTrip {
static String lf(String s) { return s.replace("\r\n", "\n"); }
public static void main(String[] args) throws Exception {
PEMDecoder decoder = PEMDecoder.of();
PEMEncoder encoder = PEMEncoder.of();
Fx.h("decode a public key, a private key and a certificate");
String pubText = Fx.read("rsa-public.pem");
PublicKey pub = decoder.decode(pubText, PublicKey.class);
System.out.println(pub.getAlgorithm() + " " + pub.getFormat() + " " + ((RSAPublicKey) pub).getModulus().bitLength() + " bits");
PrivateKey priv = decoder.decode(Fx.read("rsa-private-pkcs8.pem"), PrivateKey.class);
System.out.println(priv.getAlgorithm() + " " + priv.getFormat());
X509Certificate cert = decoder.decode(Fx.read("server.pem"), X509Certificate.class);
System.out.println(cert.getSubjectX500Principal() + " issued by " + cert.getIssuerX500Principal());
Fx.h("encode them again: the same text openssl wrote, apart from the line terminator");
String encoded = encoder.encodeToString(pub);
System.out.println("PEMEncoder line terminator is CRLF: " + encoded.contains("\r\n") + "; the openssl file uses LF: " + !pubText.contains("\r"));
Fx.check(!encoded.equals(pubText), "raw text differs from the file (CRLF vs LF)");
Fx.check(lf(encoded).equals(pubText), "public key re-encodes to identical text once CRLF is normalised to LF");
Fx.check(lf(encoder.encodeToString(priv)).equals(Fx.read("rsa-private-pkcs8.pem")), "private key re-encodes to identical text (LF-normalised)");
Fx.check(lf(encoder.encodeToString(cert)).equals(Fx.read("server.pem")), "certificate re-encodes to identical text (LF-normalised)");
Fx.h("encode a key pair that was generated just now");
KeyPairGenerator g = KeyPairGenerator.getInstance("EC");
g.initialize(256);
KeyPair kp = g.generateKeyPair();
String text = encoder.encodeToString(kp);
System.out.println(text.lines().filter(l -> l.startsWith("-----")).toList());
System.out.println("line width of the Base64 body: " + text.lines().skip(1).findFirst().get().length());
KeyPair back = decoder.decode(text, KeyPair.class);
Fx.check(((java.security.interfaces.ECPrivateKey) back.getPrivate()).getS().equals(((java.security.interfaces.ECPrivateKey) kp.getPrivate()).getS()), "private scalar survives the round trip");
Fx.check(back.getPublic().equals(kp.getPublic()), "public key survives the round trip");
// encoding a KeyPair writes the public key INTO the PKCS#8 structure, so the bytes differ from the bare private key
Fx.check(!java.util.Arrays.equals(back.getPrivate().getEncoded(), kp.getPrivate().getEncoded()), "...but the private key's own encoding is not byte-for-byte the same (the PKCS#8 now carries the public key)");
Fx.h("what does decode(String) with no target class hand back?");
for (String f : new String[]{"rsa-public.pem", "rsa-private-pkcs8.pem", "ed25519-private.pem", "server.pem", "rsa-private-encrypted.pem"}) {
Fx.tryIt(f, () -> decoder.decode(Fx.read(f)));
}
}
}