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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user