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
+24
View File
@@ -0,0 +1,24 @@
$ java --enable-preview -cp ... RoundTrip (27.0.1.0.2+1)
== decode a public key, a private key and a certificate
RSA X.509 2048 bits
RSA PKCS#8
CN=localhost issued by CN=pem-api demo CA
== encode them again: the same text openssl wrote, apart from the line terminator
PEMEncoder line terminator is CRLF: true; the openssl file uses LF: true
ok: raw text differs from the file (CRLF vs LF)
ok: public key re-encodes to identical text once CRLF is normalised to LF
ok: private key re-encodes to identical text (LF-normalised)
ok: certificate re-encodes to identical text (LF-normalised)
== encode a key pair that was generated just now
[-----BEGIN PRIVATE KEY-----, -----END PRIVATE KEY-----]
line width of the Base64 body: 64
ok: private scalar survives the round trip
ok: public key survives the round trip
ok: ...but the private key's own encoding is not byte-for-byte the same (the PKCS#8 now carries the public key)
== what does decode(String) with no target class hand back?
rsa-public.pem -> PublicKey RSA
rsa-private-pkcs8.pem -> PrivateKey RSA
ed25519-private.pem -> PrivateKey EdDSA
server.pem -> X509Certificate CN=localhost
rsa-private-encrypted.pem -> EncryptedPrivateKeyInfo PBEWithHmacSHA256AndAES_256
exit=0
+13
View File
@@ -0,0 +1,13 @@
$ java --enable-preview -cp ... JwtSigning (27.0.1.0.2+1)
== RS256 with an RSA key from PEM
header.payload = eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbmt1ciIsImlzcyI6Imh0dHBzOi8vYW5rdXJtLmNvbSJ9
signature bytes = 256, jwt parts = 3
ok: RS256 signature verifies with the public PEM
== ES256 with an EC key from PEM (the trap is the signature format)
SHA256withECDSA -> DER, starts 0x30, length varies run to run, 70-72: true (JWT readers reject this)
SHA256withECDSAinP1363Format -> R||S, length 64 (what RFC 7518 requires for ES256)
ok: ES256 signature is exactly 64 bytes
ok: ES256 signature verifies with the public PEM
== EdDSA with an Ed25519 key from PEM
EdDSA signature bytes = 64 (the PKCS#8 file holds no public key, so there is nothing to decode for verifying here)
exit=0
+13
View File
@@ -0,0 +1,13 @@
$ java --enable-preview -cp ... TlsFromPem (27.0.1.0.2+1)
== read a two-certificate chain file
CN=localhost
CN=pem-api demo CA
ok: the stream decoder returns the two certificates one by one
== server side: key + chain -> KeyStore -> KeyManagerFactory -> SSLContext
== client trusting only the CA certificate from ca.pem
HTTP 200 hello over TLS (TLSv1.3)
server presented: CN=localhost
ok: request over TLS succeeds
== same request with the JVM's default trust store (the CA is not in it)
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
exit=0
+20
View File
@@ -0,0 +1,20 @@
$ java --enable-preview -cp ... EncryptedKeys (27.0.1.0.2+1)
== write an encrypted key with the defaults
[-----BEGIN ENCRYPTED PRIVATE KEY-----, -----END ENCRYPTED PRIVATE KEY-----]
default algorithm: PBEWithHmacSHA256AndAES_128
default iteration count: 4096, salt bytes: 20
== read it back
ok: withDecryption(password) returns the original key
== choose the algorithm and cost yourself
PBEWithHmacSHA512AndAES_256, iterations 600000
ok: getKey(password) on the decoded EncryptedPrivateKeyInfo works too
== a file written by openssl pkcs8 -topk8 -v2 aes-256-cbc
PBEWithHmacSHA256AndAES_256
ok: decrypts to the same key as the unencrypted file
== failures
no password, ask for PrivateKey !! java.lang.ClassCastException: Cannot cast javax.crypto.EncryptedPrivateKeyInfo to java.security.PrivateKey
no password, no class -> EncryptedPrivateKeyInfo PBEWithHmacSHA256AndAES_256
wrong password !! javax.crypto.CryptoException: java.security.InvalidKeyException: Cannot retrieve the PKCS8EncodedKeySpec
password on a key that is not encrypted -> PrivateKey RSA
openssl 'traditional' encrypted (Proc-Type: 4,ENCRYPTED) !! java.lang.IllegalArgumentException: java.security.InvalidKeyException: Invalid PKCS#1 encoding
exit=0
+25
View File
@@ -0,0 +1,25 @@
$ java --enable-preview -cp ... Failures (27.0.1.0.2+1)
== the file holds a different kind of object than the class you asked for
certificate as PublicKey !! java.lang.ClassCastException: Cannot cast sun.security.x509.X509CertImpl to java.security.PublicKey
private key as PublicKey !! java.lang.ClassCastException: Cannot cast sun.security.rsa.RSAPrivateCrtKeyImpl to java.security.PublicKey
RSA private key as KeyPair !! java.lang.ClassCastException: Cannot cast sun.security.rsa.RSAPrivateCrtKeyImpl to java.security.KeyPair
== untyped decode: the result type depends on what the file contains
RSA PKCS#8 -> PrivateKey RSA
EC PKCS#8 (openssl embeds the public key) -> KeyPair
EC PKCS#8, asked for PrivateKey -> PrivateKey EC
EC PKCS#8, asked for KeyPair -> KeyPair
== the old OpenSSL formats
RSA PRIVATE KEY (PKCS#1) -> PrivateKey RSA
EC PRIVATE KEY (SEC1), untyped -> PEM(type=EC PRIVATE KEY) - NOT decoded
EC PRIVATE KEY (SEC1), asked for PrivateKey !! java.lang.ClassCastException: Cannot cast java.security.PEM to java.security.PrivateKey
as a PEM object: type=EC PRIVATE KEY, decoded bytes=121
== malformed text
empty string !! java.lang.IllegalArgumentException: java.io.EOFException: No data available
no PEM in the text !! java.lang.IllegalArgumentException: java.io.EOFException: No PEM data found
footer missing !! java.lang.IllegalArgumentException: java.io.EOFException: Incomplete header
body is not Base64 of a key !! java.lang.IllegalArgumentException: java.io.IOException: No recognized algorithm detected in encoding
Windows line endings (CRLF) -> PublicKey RSA
comment lines before the header -> PublicKey RSA
== several objects in one string: only the first is returned
decode(String) on a 2-certificate bundle -> CN=localhost
exit=0
+25
View File
@@ -0,0 +1,25 @@
$ java --enable-preview -cp ... Migration (27.0.1.0.2+1)
== 1. load an RSA private key (PKCS#8)
ok: BouncyCastle and the JDK produce equal keys
BC parser returned: PrivateKeyInfo
== 2. load a certificate
ok: same certificate either way
== 3. write keys
ok: JcaPEMWriter and PEMEncoder write identical public-key text (line terminators aside)
JcaPEMWriter on an RSA private key writes: -----BEGIN RSA PRIVATE KEY-----
PEMEncoder on an RSA private key writes: -----BEGIN PRIVATE KEY-----
== 4. encrypted PKCS#8 key (BC needs its own provider for AES/CBC/PKCS7Padding; the JDK does not)
ok: same key either way
== 5. what BouncyCastle reads and the JDK does not
EC PRIVATE KEY (SEC1): BC -> PEMKeyPair, JDK -> PEM(type=EC PRIVATE KEY) - NOT decoded
ok: after 'openssl pkcs8 -topk8 -nocrypt' the JDK reads the same secret scalar BC read from SEC1
Proc-Type: 4,ENCRYPTED: BC -> PEMEncryptedKeyPair decrypted to RSA
ok: ...and it is the same RSA key
Proc-Type: 4,ENCRYPTED: JDK !! java.lang.IllegalArgumentException: java.security.InvalidKeyException: Invalid PKCS#1 encoding
== 6. Spring Security RsaKeyConverters (RSA only; PKCS#8 and X.509 only)
ok: pkcs8() reads the RSA private key
ok: x509() reads the RSA public key
pkcs8() on an EC key !! java.lang.IllegalArgumentException: java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: Invalid RSA private key
pkcs8() on the PKCS#1 key !! java.lang.IllegalArgumentException: Key is not in PEM-encoded PKCS#8 format, please check that the header begins with -----BEGIN PRIVATE KEY-----
JDK on the EC key -> PrivateKey EC
exit=0
+42
View File
@@ -0,0 +1,42 @@
$ javac RoundTrip.java Fx.java (no flags) (27.0.1.0.2+1)
src/Fx.java:27: error: PEM is a preview API and is disabled by default.
if (o instanceof java.security.PEM p) return "PEM(type=" + p.type() + ") - NOT decoded";
^
(use --enable-preview to enable preview APIs)
src/RoundTrip.java:10: error: PEMDecoder is a preview API and is disabled by default.
PEMDecoder decoder = PEMDecoder.of();
^
(use --enable-preview to enable preview APIs)
src/RoundTrip.java:10: error: PEMDecoder is a preview API and is disabled by default.
PEMDecoder decoder = PEMDecoder.of();
^
(use --enable-preview to enable preview APIs)
src/RoundTrip.java:11: error: PEMEncoder is a preview API and is disabled by default.
PEMEncoder encoder = PEMEncoder.of();
^
(use --enable-preview to enable preview APIs)
src/RoundTrip.java:11: error: PEMEncoder is a preview API and is disabled by default.
PEMEncoder encoder = PEMEncoder.of();
^
(use --enable-preview to enable preview APIs)
5 errors
exit=1
$ java RoundTrip (compiled with --enable-preview, run without it) (27.0.1.0.2+1)
Error: LinkageError occurred while loading main class RoundTrip
java.lang.UnsupportedClassVersionError: Preview features are not enabled for RoundTrip (class file version 71.65535). Try running with '--enable-preview'
$ javac --enable-preview --release 27 RoundTrip.java Fx.java
src/Fx.java:27: warning: [preview] PEM is a preview API and may be removed in a future release.
if (o instanceof java.security.PEM p) return "PEM(type=" + p.type() + ") - NOT decoded";
^
src/RoundTrip.java:10: warning: [preview] PEMDecoder is a preview API and may be removed in a future release.
PEMDecoder decoder = PEMDecoder.of();
^
src/RoundTrip.java:10: warning: [preview] PEMDecoder is a preview API and may be removed in a future release.
PEMDecoder decoder = PEMDecoder.of();
^
src/RoundTrip.java:11: warning: [preview] PEMEncoder is a preview API and may be removed in a future release.
PEMEncoder encoder = PEMEncoder.of();
^
exit=0
+22
View File
@@ -0,0 +1,22 @@
$ javac src/{Fx,RoundTrip,JwtSigning,TlsFromPem,EncryptedKeys,Failures}.java (no flags) (28-ea+21-1707)
exit=0
$ java RoundTrip (no flags)
exit=0
output identical to the JDK 27 run
$ java JwtSigning (no flags)
exit=0
output identical to the JDK 27 run
$ java TlsFromPem (no flags)
exit=0
output identical to the JDK 27 run
$ java EncryptedKeys (no flags)
exit=0
output identical to the JDK 27 run
$ java Failures (no flags)
exit=0
output identical to the JDK 27 run