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:
@@ -70,6 +70,7 @@ These modules keep only sources, a `run.sh` and their transcripts (`output/`). T
|
|||||||
| [`lazy-constants/`](lazy-constants) | Lazy Constants in JDK 27 (JEP 531): Replacing Double-Checked Locking | `LazyConstant` against the holder idiom and double-checked locking, the 26 to 27 failure-semantics change, `List`/`Map`/`Set.ofLazy`, the preview-flag traps and a JMH read-path benchmark |
|
| [`lazy-constants/`](lazy-constants) | Lazy Constants in JDK 27 (JEP 531): Replacing Double-Checked Locking | `LazyConstant` against the holder idiom and double-checked locking, the 26 to 27 failure-semantics change, `List`/`Map`/`Set.ofLazy`, the preview-flag traps and a JMH read-path benchmark |
|
||||||
| [`http3/`](http3) | HTTP/3 in the Java HTTP Client (JEP 517): Migrating from HttpClient 11 | Java 11 code unchanged on 25 and 26, the `HTTP_3` opt-in on client vs request, the three `H3_DISCOVERY` modes, fallback timings (including the 60 s `HTTP_3_URI_ONLY` failure), proxy downgrade, HTTP/1.1 vs 2 vs 3 under iptables packet loss, with a local Hypercorn + aioquic server |
|
| [`http3/`](http3) | HTTP/3 in the Java HTTP Client (JEP 517): Migrating from HttpClient 11 | Java 11 code unchanged on 25 and 26, the `HTTP_3` opt-in on client vs request, the three `H3_DISCOVERY` modes, fallback timings (including the 60 s `HTTP_3_URI_ONLY` failure), proxy downgrade, HTTP/1.1 vs 2 vs 3 under iptables packet loss, with a local Hypercorn + aioquic server |
|
||||||
| [`compact-strings/`](compact-strings) | Compact Strings in Java: Measured with JOL on JDK 25 (plus Compact Object Headers) | the `String` object and its `byte[]` field by field, Latin-1 vs UTF-16 footprint by length, one million strings with and without `-XX:-CompactStrings`, what flips a string to UTF-16 (and what flips it back), compact object headers on JDK 25 (opt-in) vs 27 (default), a JMH comparison of six operations |
|
| [`compact-strings/`](compact-strings) | Compact Strings in Java: Measured with JOL on JDK 25 (plus Compact Object Headers) | the `String` object and its `byte[]` field by field, Latin-1 vs UTF-16 footprint by length, one million strings with and without `-XX:-CompactStrings`, what flips a string to UTF-16 (and what flips it back), compact object headers on JDK 25 (opt-in) vs 27 (default), a JMH comparison of six operations |
|
||||||
|
| [`pem-api/`](pem-api) | PEM Encodings in Java (JEP 538/542): Read and Write Keys and Certificates Without BouncyCastle | `PEMDecoder`/`PEMEncoder` round trips, JWT signing (RS256/ES256/EdDSA) and a TLS server built from PEM files, encrypted PKCS#8 keys, the failure catalogue (wrong target class, SEC1 `EC PRIVATE KEY`, CRLF output), BouncyCastle and Spring Security `RsaKeyConverters` side by side, preview flag on JDK 27 vs the JDK 28 EA build |
|
||||||
|
|
||||||
## Captured output (`docs/output/`)
|
## Captured output (`docs/output/`)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# pem-api
|
||||||
|
|
||||||
|
Companion code for the ankurm.com article **PEM Encodings in Java (JEP 538 / JEP 542): Read and Write Keys and Certificates Without BouncyCastle**.
|
||||||
|
|
||||||
|
`java.security.PEMDecoder` / `PEMEncoder` were previewed in JDK 25 (JEP 470) and 26 (JEP 524), and are a **third preview in JDK 27 (JEP 538)**,
|
||||||
|
so JDK 27 needs `--enable-preview`. JEP 542 finalizes the API in JDK 28 (targeted; not GA until March 2027). The scripts run on JDK 27.0.1 (Temurin) and on the JDK 28 early-access build 21 (no flag).
|
||||||
|
|
||||||
|
| File | What it shows | Output |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/RoundTrip.java` | decode / encode keys and certificates, CRLF line endings, what untyped `decode` returns | `output/01-round-trip.txt` |
|
||||||
|
| `src/JwtSigning.java` | RS256 / ES256 / EdDSA signing with keys from PEM, the ES256 DER vs R||S trap | `output/02-jwt-signing.txt` |
|
||||||
|
| `src/TlsFromPem.java` | key + certificate chain + CA from PEM into an `SSLContext`, a local HTTPS server and client | `output/03-tls.txt` |
|
||||||
|
| `src/EncryptedKeys.java` | write / read encrypted PKCS#8, choose algorithm and cost, wrong password, OpenSSL files | `output/04-encrypted.txt` |
|
||||||
|
| `src/Failures.java` | wrong target class, SEC1 `EC PRIVATE KEY`, malformed text, bundles | `output/05-failures.txt` |
|
||||||
|
| `src/Migration.java` | the same jobs with BouncyCastle `bcpkix` 1.86, Spring Security 7.1.2 `RsaKeyConverters`, and the JDK | `output/06-migration.txt` |
|
||||||
|
| (script) | compile without `--enable-preview`; run without it | `output/07-preview-flag.txt` |
|
||||||
|
| (script) | the same demos on JDK 28 EA, no flag, diffed against the JDK 27 run | `output/08-jdk28-final.txt` |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
JDK27=/path/to/jdk-27 JDK28=/path/to/jdk-28-ea ./run.sh # JDK28 is optional; needs Maven Central once, for bcpkix and spring-security-core
|
||||||
|
```
|
||||||
|
|
||||||
|
`fixtures/` holds throw-away OpenSSL-generated keys and certificates (see `fixtures/README.md`). Output is byte-stable; the ECDSA DER length (random, 70-72) is printed as a range check for that reason.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Throw-away keys generated with OpenSSL 3.0.13 for this demo only. They protect nothing. Do not reuse them.
|
||||||
|
Password for the encrypted files: `changeit`.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDFTCCAf2gAwIBAgIUCSAcRT4u1yYgnV75DA+j83tCQVEwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwGjEYMBYGA1UEAwwPcGVtLWFwaSBkZW1vIENBMB4XDTI2MTAyNjAxNDA1M1oX
|
||||||
|
DTM2MTAyMzAxNDA1M1owGjEYMBYGA1UEAwwPcGVtLWFwaSBkZW1vIENBMIIBIjAN
|
||||||
|
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu4m1GA1zj99imolAmktmnHBMz9LK
|
||||||
|
wpT73gXpxAymH0nMe991uduv2l79KS2ItpwlwovXy6L4vk8oPauN3iN0aIm6MUUS
|
||||||
|
j32fJmk+NY7CIkZwBt6ys69yhRCbf5a7ydHQdlNSO+QAqfD+DveTZI8UkOqxLBXP
|
||||||
|
MWZlOSTN5tGsLBt2pVvMWXf2Dlt4PqHOHjU4NNiZeHwpOj9Z7JQAT0dOI57aYy1+
|
||||||
|
b3vFUaG2TsKTsxwaxcwU4X6WV/yF3lVXVM1g9nRctdRq4BIOsTdViONfvSopMqhz
|
||||||
|
vvowsX/w94dXvzCRfpJ5Z0lyi2+9xUID9pdF9G1v0K1aHeNOrSB20Z2QswIDAQAB
|
||||||
|
o1MwUTAdBgNVHQ4EFgQUWNf+XHdl2Z/EhZm+oee/6tFj4tswHwYDVR0jBBgwFoAU
|
||||||
|
WNf+XHdl2Z/EhZm+oee/6tFj4tswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
|
||||||
|
AQsFAAOCAQEAGGt+BC8ZiJLpX1Hxhrf5ycXYWOqGI9StMVxDR26f4qNQdAkm1k+s
|
||||||
|
j8pFWdYTxd5TAomOLSKd0L/SSFK1WN5HWYALU8w5wxz5S1lh4COLYJ8arlM27dAr
|
||||||
|
poZ7ODFj/kU9DiRSbRKj0S1gWX0QMxk6koz5IfSNqJUxCElcu1tcLTvvTd1UPL01
|
||||||
|
A5F2z2Yzb6CZe4lLtmI23qhM4DdFPjBn9HA6RgyoRB2fiYd3KMv5eSxeN6wFwlCj
|
||||||
|
1tXafCeqR3cWdS28dyA0uKEE6eCH0dS9eJE373g4XUe9tR5Q6t8Hj4sKW/y2x2e3
|
||||||
|
0No5yG4y7XFYvupjujgy64RrmCXPgmheVA==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5DkYLeQPJV8WSlOv
|
||||||
|
RorCeu8sce3UJ4faLsEP5N+e8YahRANCAAROEzEWo1RQXPHGPYXHHcCauMuJY5is
|
||||||
|
kxndCQ0FD3unn4F4/kFy8PpFbipVPLV1XybI0Vmrq5QjJb+VG0Dyt/Pk
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIOQ5GC3kDyVfFkpTr0aKwnrvLHHt1CeH2i7BD+TfnvGGoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEThMxFqNUUFzxxj2Fxx3AmrjLiWOYrJMZ3QkNBQ97p5+BeP5BcvD6
|
||||||
|
RW4qVTy1dV8myNFZq6uUIyW/lRtA8rfz5A==
|
||||||
|
-----END EC PRIVATE KEY-----
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEThMxFqNUUFzxxj2Fxx3AmrjLiWOY
|
||||||
|
rJMZ3QkNBQ97p5+BeP5BcvD6RW4qVTy1dV8myNFZq6uUIyW/lRtA8rfz5A==
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MC4CAQAwBQYDK2VwBCIEIG1a+f3CV8oDhLgpsiLuyV6eXvaULpBqfTiyil3lxq9l
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||||
|
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIPSkURdWa2RECAggA
|
||||||
|
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBA3pcMudzEuNJ7AsZaSF7qPBIIE
|
||||||
|
0EpPeCIYHNYqBmE0XBTn8gm60u7AHZpMrS/UayoB2EN64yxvpyK18ZN0WTFZoPQK
|
||||||
|
NcfXpsHNtBb9qM6GcKbxcAvX3OWSk1VLmilw1pQw816R9Y6sZ1nqAfRmGC9Nppio
|
||||||
|
k23pLtS+tQcJbDDd2p6Kl4V9lu2ABwqaiK7ERpabppymNJUYoD3TysHDirma7uFz
|
||||||
|
UWjNYHURMGW/IYfyNis4Bp8Iqg9+YiVtYEh55onMXGYdrLM2Spro434q9hO4GEn/
|
||||||
|
Qq/ujtwGCxwkbuD7D0wzS9ZQb4ubOL5sVJWBTSaLRa9TBpoVZNqIlNSaDrpJPehN
|
||||||
|
GoHKAmSSLSnYqQGRlaWWOxZbIpy+msC9vJ/sFd66PRuR6zJ/F8DdV+Si79mDKjVn
|
||||||
|
1IVOnn7dx3HkYtI8Jo1l4NFCXDellOevmK7G4HIa15a6yMd4exkGMV9Yjl+6T8MS
|
||||||
|
owBMN43Zo0rO8Lys+o2Q8f1hqUoc9brroptgJkGxLB5HqVV74LiQVNNw27EtCSLx
|
||||||
|
eo4Xsx08uVsItcPHd3lNpWVHU/hK2X5j88+36gNrGbbiZAw4cSuq2JhsHiz9bhMK
|
||||||
|
WOj+fXTqEEnLMHa7hZE75JmigcHo7S3dymZD7V5k+IVGzzNsQ3O+lRpzjj3xJm7O
|
||||||
|
A4wLG95iwlieZzt0NpNGbrU4DkOj0jpGObleWYWWjXkBRfDuUf79f9nr+uFiZH0M
|
||||||
|
aTjhEU7sS3ljOcc2ch1WhrkP4PyHXpDJgPSjvdyLeVEOfZlCW1q+nxFTGwkkakFz
|
||||||
|
GzzWPoxNeZdjWZ19eAXn/n4nw+xbwAOUM8TO5pHbv2dQNJkfY291E9qU3cE/dvYA
|
||||||
|
DQ8PrEOZuXbbyYGsFvq6rfeZMOV3yDH/PvBzbaRuCx0g/6e/ju03IVWMPi1O+GOy
|
||||||
|
Mr+D9OA4Xf7xQRW9TiXfr64OhBV02pEfD3SK2lypE4+ZFrIYKh6u7lT+0DSt8Va4
|
||||||
|
T4WzUb/2qpjHPuBiNoxQJ/pE20m75s4Iij0vV7J6tsqVQZHLuYjpi1dwF3L70Z41
|
||||||
|
Zm8wQbYK8uI6jh8qB7iolr9hUfw8i6uxEOeIwQcurEM0xR3eRz9d3mBo5hkwhW1c
|
||||||
|
SOFwO89vAUDZp+njlYUgssy/gZnKOKbAvKBfOq1TGfvrMK1PqCl8dAnnJ2c1CEgM
|
||||||
|
v2U/iSMMH5wHAl0DlMLNcCXpnR4q3jg1HWG6V1iaKv985N8pEbKK/Szk944FD06s
|
||||||
|
NvNhHwmQADUZgJB224XP3LqlrmN/52x4+CCeAMJWwMU27e31WuoFs39uS+/kc3of
|
||||||
|
BBkuPd1P4mdKkmbZuhMXinKvvrg+cla3PBPrklINYxfIspO0Cd4KsIfvLWIh32yC
|
||||||
|
NGRK2x6SSDVt/eF+sfBOI8bkiNAVwWzJFPfsrhnJbOVPoFTyWpBfCCGzADHrdWZw
|
||||||
|
sRYgh19Xm92OKtK+Lhgj+OqEvCNc4sdWypaSWhWeUStyB8bmNDY5YkEajWxnjbiM
|
||||||
|
fKBLgHJHnpQYy0/eykVeQYk+LbKcpIIjBXxzhhZ8GL4RfcF33Ej4P9XnX65zLO0O
|
||||||
|
vLaDToYSoiYpofludHBcq9a48WT2f1tasjd4sXiyK6oiiAUOPyQLCdhWPFLpODfW
|
||||||
|
NqfHxMAdnXDJN09pHJFD9qVlloKuQSe2d3i/OwaCSS/J
|
||||||
|
-----END ENCRYPTED PRIVATE KEY-----
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
Proc-Type: 4,ENCRYPTED
|
||||||
|
DEK-Info: AES-256-CBC,74535134409159C4FE67EF01205160E7
|
||||||
|
|
||||||
|
A5ZS2v3peoqsMxlI0Egjp0bW77USJN9abGrqzqxIZFkq2rvXT4VAdMuJurZ7ENAK
|
||||||
|
WJKGKxXlBj9X0tJmP3pnACQ9I5nbJVygflodzs/HRrJKProPdPHl5UZ7tYlRm2Vw
|
||||||
|
nMfHJsr/j59f0Y9/8+TpHPsc8uWEbNrf32Sx9X89bvXQ1gBl8fuOqBwoOYJww70l
|
||||||
|
0xVTjJso68wwLV9NMkzqM+LxRIh/WEX3Qd//M3WOGYyA24xB4aIimgtyIPxweZcj
|
||||||
|
0qKVC3rLQ0P2FBBBlvIM4uh6C6R1KfZVgluSSwB83VDvO1N76chDCxyKvknKlPrA
|
||||||
|
QLcl7oPfoh2gv31j0cTK0sQ+SLuuDmTKvAeWGq36clgCJWSjJ9MSvh4MVMoAcW1M
|
||||||
|
q6hlOHsRuRb3G/ILcZnJ/n7G6+W/gSMMKGnIwBcstPvoQrNATBLtr8IXw5+vT/82
|
||||||
|
oB6y4MjSzF7G0hg+0siuxZvgf5S7r2IrZWetABgSpWU1HTGGCgom7mHf8f6T1bc0
|
||||||
|
Q7Ey8+uXzhltvjuOuNId83bHjAoIGxMCHAEadjDwfBll+nC5FjErxigurteKGTHc
|
||||||
|
84Q8pGD8A8izEP49E8lNsBBBJx5OvcBnq4fW4aX78oLj/xOpU+y769yc7CCnooTc
|
||||||
|
FWORC/7eUpVvMbEYcEfEj0bG+slyJ99kHEFQEe7m5OJTGbkFK91MCRE7bY2R9UCx
|
||||||
|
w1mFrdhEWKUvwINB5czPfNHmz7ACYkVekWekh/7a77ygD6KD9tTaCat9HTzBpHR0
|
||||||
|
0H0Wzp03o8kcs1DGpO2z9CPsFTd0lwYP3+xN8jSNUA3mpV3sov1MZrRGJkqt+TWp
|
||||||
|
e6IPTlNxbJfuxRkbayyHBPjkBTjFwZOfWmkn0ySLT7Ujc7/zaz6+XcFoZsVF/tRr
|
||||||
|
T5c2flSygpwzHCxJ4+WFJ4yoEUXd5CDRXaLgll3Y8pTVy8RfleyKfQcFWWIdFZLg
|
||||||
|
3uzmKbEZIJ7L0IfAzCzRupQBJJRdZCX/Zd4WAcNq//JiyYSiKM06WKjsYtzld9AQ
|
||||||
|
SFGR/zFA0ReoTX3+75HovlRmLPBgwUBJCLK2SCDVYaDNDJOXXS8YII3RZ+K9BNIJ
|
||||||
|
vnEXOPNAfNOWIaLX/F3FILQCrBgBKgV7/wJSH0s8JN2sV6rsSo9/IaBEqMIly/Ui
|
||||||
|
s99jyumLCobV9Pw4PhLaFpQpsAhB2nxjsRxRcomIgvOlIbn0v5FiBuLacTrs3JsI
|
||||||
|
X+TS3gz939kn1w68e9c3F0ido95A2tAxiWuGpYiUbz4y7PToy9mE8rEqjlM8qYu3
|
||||||
|
KlMluvRKH2mnd4KKZci4zoBEw5dyFUK+yj6NPwERWQ9b3KzcUNDsK9MhLMGhzXzw
|
||||||
|
Hc0YqprUN5OWmo4X2GMdtXBzT6wqD+lkh0jZjoET6hPy6XTbZHOb/kDbGQFhtU5q
|
||||||
|
3F1GUYSBG2BHSKbLLZDiGiQFERisLKbKNg8XaHCEi3iggFcWh96WWTLr5mWsHMmB
|
||||||
|
TXwqziMPj0qzoBmvlMDaurtfCh73eRoqJ6iwmWuK1kSSPp2m2XrrTSSs/1wG9aOv
|
||||||
|
PT1SXSrXXCtiN8Phm6emtpS7dDj7kJeJCiHuBBS49rWcGCKRyojUGHsf9cu7B6uC
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEogIBAAKCAQEAybOBX3YVfWzrzRLOJmJX7gsquk3pMtUzwqgEku/qElWUyy/x
|
||||||
|
CXfhmGTzHrYq0iSaOk6nx5xj6gzSas/VcPgDeRyRE74SQRWWGXT5q3v+FR2anBP6
|
||||||
|
bvPRiEzM6eejn0C1aLXc7m0FgbKLItX9khdftGSb1xEdBCyhmhaCuNiuMXOxcHko
|
||||||
|
2lS47W7YRbTlASup8LYCWgwil40YLQxgfQVqzZ6iSJjtP77rCrLOeLu/+kep67N5
|
||||||
|
92BdI0N0WenhzEgyARoWZSBwg8wbdwhtSYetg2afXnmFDGv1f7bmztm5bdZa1e8D
|
||||||
|
F+5St6mrgcoBGE5aDg5ROo+YfmMVQo5y7/H14QIDAQABAoH/D/RjB2oPyRwUiuQx
|
||||||
|
6lddHTdE6fe/uoWi0oL32aegnFKosmnYZydUcPmLQaKnQHgkuvf9zYxRWvIosso9
|
||||||
|
1/HGVI4t66HdT8ilVt4p4aatMn5k/HOYG66tpFpu2xG3n6E+uEmqcDGvfdLhrp++
|
||||||
|
rw+qQlqy+RAhe3MMVWPycyywmrioGWeFa8enrE4y2jU+SKtVijBKd7tVskgzspJG
|
||||||
|
9lfzMnSBR3NllyX8L6MBz2pOzcENi+a5wWCZhgymOrphdSl1BRhB9nQB+/6rjSBB
|
||||||
|
mzGXbQY3+XQP3wZnHfmH15FIPQXSY+5ua0N2jSX7XRGNcWwQiLXJ1KPRiV2RqO3K
|
||||||
|
JA9BAoGBAOocTmsHYBk4WNcu25wV3oi2/goBTWlPJ3d0FIhf02/q0u0DH2HHXIEs
|
||||||
|
XB8zYqFIAgJjfVIX2hDqtxAB5ndrIfxU3jHhkzIvAOr6xxvLH00VAR/bZIEHWBFt
|
||||||
|
LvtatSYQ2eEvz2jL3UorZABWwyBs18K1/vEfI+dRtoPeWyNjoS5RAoGBANyPceke
|
||||||
|
KWRApdkq/gBUs+8syem6bwxdC0Iqc2w7Rg1Vxf4pluzswEYJC40tC8aYNID3aHG+
|
||||||
|
nkDUsCGb7w+JsllMw3ICd35UwupfOkUTlUjLv1NIBMC09ySldj/xUmmSBTIOYREg
|
||||||
|
hE1CK/beiYTeWgzWKud+TVzFNkuKJ06qqpqRAoGBAKuvwpqfOAyG1DBKvudC6oYc
|
||||||
|
nUU9G0ZFtE3YJU6159vFvJXwmk0c5Wwd2hlPl99rbX3O9JMFxvhtCX+FxTyCptPk
|
||||||
|
cWbnZQmCiwvIiciKOBRReRehz50Ig7+T8MaJCRw3c3G/nD3gdBL2q0nMVyEqcTjs
|
||||||
|
O2EdUzlLJayjnvbfE89hAoGBANCHZsFq3OOGI8ehWCjj7IMzK2up8pgT2DMkg6BO
|
||||||
|
ojq1X86fb6xtYpRUtPEzJ7EJ8ooEYGE6QdE1xZjhHzLnkxHO+/Js1qFAblC83BC3
|
||||||
|
JSuAR1HsA2JXNlsVfSmTdcGOZw8efTN+NNf+uiZb3FUdDLKaQlJAHdCaPG1LH0LH
|
||||||
|
JSmhAoGAKtv416KJHkpIQfPEyOqNxcKzOFtmVhx5azGD2Ddh4Q6vDDLvgfdDtQLQ
|
||||||
|
NEwtVd3A9kIE6WEkkmcRyDOYQHWtTttRNaFE01fmUGEnG8OMtjC/A5fK70Kck1CY
|
||||||
|
3hD2biU3hnKf5EvW5C7CflGeX1vg+8pm0d4x3Ddy4/Zq7U3met4=
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDJs4FfdhV9bOvN
|
||||||
|
Es4mYlfuCyq6Teky1TPCqASS7+oSVZTLL/EJd+GYZPMetirSJJo6TqfHnGPqDNJq
|
||||||
|
z9Vw+AN5HJETvhJBFZYZdPmre/4VHZqcE/pu89GITMzp56OfQLVotdzubQWBsosi
|
||||||
|
1f2SF1+0ZJvXER0ELKGaFoK42K4xc7FweSjaVLjtbthFtOUBK6nwtgJaDCKXjRgt
|
||||||
|
DGB9BWrNnqJImO0/vusKss54u7/6R6nrs3n3YF0jQ3RZ6eHMSDIBGhZlIHCDzBt3
|
||||||
|
CG1Jh62DZp9eeYUMa/V/tubO2blt1lrV7wMX7lK3qauBygEYTloODlE6j5h+YxVC
|
||||||
|
jnLv8fXhAgMBAAECgf8P9GMHag/JHBSK5DHqV10dN0Tp97+6haLSgvfZp6CcUqiy
|
||||||
|
adhnJ1Rw+YtBoqdAeCS69/3NjFFa8iiyyj3X8cZUji3rod1PyKVW3inhpq0yfmT8
|
||||||
|
c5gbrq2kWm7bEbefoT64SapwMa990uGun76vD6pCWrL5ECF7cwxVY/JzLLCauKgZ
|
||||||
|
Z4Vrx6esTjLaNT5Iq1WKMEp3u1WySDOykkb2V/MydIFHc2WXJfwvowHPak7NwQ2L
|
||||||
|
5rnBYJmGDKY6umF1KXUFGEH2dAH7/quNIEGbMZdtBjf5dA/fBmcd+YfXkUg9BdJj
|
||||||
|
7m5rQ3aNJftdEY1xbBCItcnUo9GJXZGo7cokD0ECgYEA6hxOawdgGThY1y7bnBXe
|
||||||
|
iLb+CgFNaU8nd3QUiF/Tb+rS7QMfYcdcgSxcHzNioUgCAmN9UhfaEOq3EAHmd2sh
|
||||||
|
/FTeMeGTMi8A6vrHG8sfTRUBH9tkgQdYEW0u+1q1JhDZ4S/PaMvdSitkAFbDIGzX
|
||||||
|
wrX+8R8j51G2g95bI2OhLlECgYEA3I9x6R4pZECl2Sr+AFSz7yzJ6bpvDF0LQipz
|
||||||
|
bDtGDVXF/imW7OzARgkLjS0Lxpg0gPdocb6eQNSwIZvvD4myWUzDcgJ3flTC6l86
|
||||||
|
RROVSMu/U0gEwLT3JKV2P/FSaZIFMg5hESCETUIr9t6JhN5aDNYq535NXMU2S4on
|
||||||
|
TqqqmpECgYEAq6/Cmp84DIbUMEq+50LqhhydRT0bRkW0TdglTrXn28W8lfCaTRzl
|
||||||
|
bB3aGU+X32ttfc70kwXG+G0Jf4XFPIKm0+RxZudlCYKLC8iJyIo4FFF5F6HPnQiD
|
||||||
|
v5PwxokJHDdzcb+cPeB0EvarScxXISpxOOw7YR1TOUslrKOe9t8Tz2ECgYEA0Idm
|
||||||
|
wWrc44Yjx6FYKOPsgzMra6nymBPYMySDoE6iOrVfzp9vrG1ilFS08TMnsQnyigRg
|
||||||
|
YTpB0TXFmOEfMueTEc778mzWoUBuULzcELclK4BHUewDYlc2WxV9KZN1wY5nDx59
|
||||||
|
M3401/66JlvcVR0MsppCUkAd0Jo8bUsfQsclKaECgYAq2/jXookeSkhB88TI6o3F
|
||||||
|
wrM4W2ZWHHlrMYPYN2HhDq8MMu+B90O1AtA0TC1V3cD2QgTpYSSSZxHIM5hAda1O
|
||||||
|
21E1oUTTV+ZQYScbw4y2ML8Dl8rvQpyTUJjeEPZuJTeGcp/kS9bkLsJ+UZ5fW+D7
|
||||||
|
ymbR3jHcN3Lj9mrtTeZ63g==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAybOBX3YVfWzrzRLOJmJX
|
||||||
|
7gsquk3pMtUzwqgEku/qElWUyy/xCXfhmGTzHrYq0iSaOk6nx5xj6gzSas/VcPgD
|
||||||
|
eRyRE74SQRWWGXT5q3v+FR2anBP6bvPRiEzM6eejn0C1aLXc7m0FgbKLItX9khdf
|
||||||
|
tGSb1xEdBCyhmhaCuNiuMXOxcHko2lS47W7YRbTlASup8LYCWgwil40YLQxgfQVq
|
||||||
|
zZ6iSJjtP77rCrLOeLu/+kep67N592BdI0N0WenhzEgyARoWZSBwg8wbdwhtSYet
|
||||||
|
g2afXnmFDGv1f7bmztm5bdZa1e8DF+5St6mrgcoBGE5aDg5ROo+YfmMVQo5y7/H1
|
||||||
|
4QIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICTzCCATegAwIBAgIUZmRuo+eancCdHWVyPKWCj5VH0cMwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwGjEYMBYGA1UEAwwPcGVtLWFwaSBkZW1vIENBMB4XDTI2MTAyNjAxNDA1M1oX
|
||||||
|
DTM2MTAyMzAxNDA1M1owFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0C
|
||||||
|
AQYIKoZIzj0DAQcDQgAEspmXXLwV6zHbch9SrHNAr5pZtpQWmPVG5H5PNCQoSAXo
|
||||||
|
SGxW5mQUHCUKCs2fmVZ47KHASMBDGrhb3Q591R1kFKNeMFwwGgYDVR0RBBMwEYIJ
|
||||||
|
bG9jYWxob3N0hwR/AAABMB0GA1UdDgQWBBT0AmAjlRBDLvRfihaFy6CfvjLj/zAf
|
||||||
|
BgNVHSMEGDAWgBRY1/5cd2XZn8SFmb6h57/q0WPi2zANBgkqhkiG9w0BAQsFAAOC
|
||||||
|
AQEAswC6WWfiDP7WgyWD8IkWah2mlxauTtqyUX3pH+dydZURkjoDEtLCgpaX399g
|
||||||
|
dbNHa3HYpFJH/c5NMwEG/Zko7OhtLaE/v3yH77vzHnJbQp4MI3HRP3Moa1dpSe+g
|
||||||
|
eKkSSMC7suwby+ykB4+V/5gDkeyMWYj25za1jnCoYCFDervPGZ/TQ7kycAWyxiU5
|
||||||
|
+CmKCwpPkiMX56yC1CN5kdjIUmEXlEZ21baeJBOLL4zwmtYoc/QqMPjtxUlPFCh9
|
||||||
|
K83Jn0pk5BnitIi2OLTG8NiUT5PV/Jk5PKDr/HpskDNiTskx9CZXNawmcey+CeeT
|
||||||
|
YennaowCuIPG4GOo/16vB/UM/w==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDFTCCAf2gAwIBAgIUCSAcRT4u1yYgnV75DA+j83tCQVEwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwGjEYMBYGA1UEAwwPcGVtLWFwaSBkZW1vIENBMB4XDTI2MTAyNjAxNDA1M1oX
|
||||||
|
DTM2MTAyMzAxNDA1M1owGjEYMBYGA1UEAwwPcGVtLWFwaSBkZW1vIENBMIIBIjAN
|
||||||
|
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu4m1GA1zj99imolAmktmnHBMz9LK
|
||||||
|
wpT73gXpxAymH0nMe991uduv2l79KS2ItpwlwovXy6L4vk8oPauN3iN0aIm6MUUS
|
||||||
|
j32fJmk+NY7CIkZwBt6ys69yhRCbf5a7ydHQdlNSO+QAqfD+DveTZI8UkOqxLBXP
|
||||||
|
MWZlOSTN5tGsLBt2pVvMWXf2Dlt4PqHOHjU4NNiZeHwpOj9Z7JQAT0dOI57aYy1+
|
||||||
|
b3vFUaG2TsKTsxwaxcwU4X6WV/yF3lVXVM1g9nRctdRq4BIOsTdViONfvSopMqhz
|
||||||
|
vvowsX/w94dXvzCRfpJ5Z0lyi2+9xUID9pdF9G1v0K1aHeNOrSB20Z2QswIDAQAB
|
||||||
|
o1MwUTAdBgNVHQ4EFgQUWNf+XHdl2Z/EhZm+oee/6tFj4tswHwYDVR0jBBgwFoAU
|
||||||
|
WNf+XHdl2Z/EhZm+oee/6tFj4tswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
|
||||||
|
AQsFAAOCAQEAGGt+BC8ZiJLpX1Hxhrf5ycXYWOqGI9StMVxDR26f4qNQdAkm1k+s
|
||||||
|
j8pFWdYTxd5TAomOLSKd0L/SSFK1WN5HWYALU8w5wxz5S1lh4COLYJ8arlM27dAr
|
||||||
|
poZ7ODFj/kU9DiRSbRKj0S1gWX0QMxk6koz5IfSNqJUxCElcu1tcLTvvTd1UPL01
|
||||||
|
A5F2z2Yzb6CZe4lLtmI23qhM4DdFPjBn9HA6RgyoRB2fiYd3KMv5eSxeN6wFwlCj
|
||||||
|
1tXafCeqR3cWdS28dyA0uKEE6eCH0dS9eJE373g4XUe9tR5Q6t8Hj4sKW/y2x2e3
|
||||||
|
0No5yG4y7XFYvupjujgy64RrmCXPgmheVA==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgmOPW4XsEHwKxsZYV
|
||||||
|
McWveS7DqdDiLNg8OKuX1HxadoihRANCAASymZdcvBXrMdtyH1Ksc0Cvmlm2lBaY
|
||||||
|
9Ubkfk80JChIBehIbFbmZBQcJQoKzZ+ZVnjsocBIwEMauFvdDn3VHWQU
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICTzCCATegAwIBAgIUZmRuo+eancCdHWVyPKWCj5VH0cMwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwGjEYMBYGA1UEAwwPcGVtLWFwaSBkZW1vIENBMB4XDTI2MTAyNjAxNDA1M1oX
|
||||||
|
DTM2MTAyMzAxNDA1M1owFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0C
|
||||||
|
AQYIKoZIzj0DAQcDQgAEspmXXLwV6zHbch9SrHNAr5pZtpQWmPVG5H5PNCQoSAXo
|
||||||
|
SGxW5mQUHCUKCs2fmVZ47KHASMBDGrhb3Q591R1kFKNeMFwwGgYDVR0RBBMwEYIJ
|
||||||
|
bG9jYWxob3N0hwR/AAABMB0GA1UdDgQWBBT0AmAjlRBDLvRfihaFy6CfvjLj/zAf
|
||||||
|
BgNVHSMEGDAWgBRY1/5cd2XZn8SFmb6h57/q0WPi2zANBgkqhkiG9w0BAQsFAAOC
|
||||||
|
AQEAswC6WWfiDP7WgyWD8IkWah2mlxauTtqyUX3pH+dydZURkjoDEtLCgpaX399g
|
||||||
|
dbNHa3HYpFJH/c5NMwEG/Zko7OhtLaE/v3yH77vzHnJbQp4MI3HRP3Moa1dpSe+g
|
||||||
|
eKkSSMC7suwby+ykB4+V/5gDkeyMWYj25za1jnCoYCFDervPGZ/TQ7kycAWyxiU5
|
||||||
|
+CmKCwpPkiMX56yC1CN5kdjIUmEXlEZ21baeJBOLL4zwmtYoc/QqMPjtxUlPFCh9
|
||||||
|
K83Jn0pk5BnitIi2OLTG8NiUT5PV/Jk5PKDr/HpskDNiTskx9CZXNawmcey+CeeT
|
||||||
|
YennaowCuIPG4GOo/16vB/UM/w==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Used ONLY to fetch the two comparison libraries into target/lib (run.sh calls dependency:copy-dependencies).
|
||||||
|
The JDK-only demos in src/ need none of this. -->
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>pem-api</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcpkix-jdk18on</artifactId>
|
||||||
|
<version>1.86</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-core</artifactId>
|
||||||
|
<version>7.1.2</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
Executable
+53
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# PEM encodings in Java: JEP 538 (third preview, JDK 27) -> JEP 542 (final, JDK 28).
|
||||||
|
# Regenerates every file in output/. JDK27=/path/to/jdk27 JDK28=/path/to/jdk28-ea ./run.sh
|
||||||
|
set -uo pipefail
|
||||||
|
unset JAVA_TOOL_OPTIONS
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
JDK27="${JDK27:?set JDK27}"; JDK28="${JDK28:-}"
|
||||||
|
OUT="$HERE/output"; mkdir -p "$OUT"; B="$(mktemp -d)"; trap 'rm -rf "$B"' EXIT
|
||||||
|
cd "$HERE"
|
||||||
|
jv() { "$1/bin/java" -version 2>&1 | sed -n 2p | sed 's/.*(build \(.*\))/\1/'; }
|
||||||
|
|
||||||
|
# comparison libraries (BouncyCastle bcpkix, spring-security-core) -> target/lib
|
||||||
|
[ -d target/lib ] || mvn -q -B dependency:copy-dependencies -DoutputDirectory=target/lib -DincludeScope=compile >/dev/null
|
||||||
|
CP_LIBS="$(ls target/lib/*.jar | tr '\n' ':')"
|
||||||
|
|
||||||
|
P27="--enable-preview --release 27"
|
||||||
|
mkdir -p "$B/c27"; "$JDK27/bin/javac" $P27 -cp "$CP_LIBS" -d "$B/c27" src/*.java 2>&1 | grep -v '^Note:'
|
||||||
|
run27() { echo "\$ java --enable-preview -cp ... $1 ($(jv "$JDK27"))"; "$JDK27/bin/java" --enable-preview -Dfixtures=fixtures -cp "$B/c27:$CP_LIBS" "$1" 2>&1; echo "exit=$?"; }
|
||||||
|
|
||||||
|
{ run27 RoundTrip; } > "$OUT/01-round-trip.txt"
|
||||||
|
{ run27 JwtSigning; } > "$OUT/02-jwt-signing.txt"
|
||||||
|
{ run27 TlsFromPem; } > "$OUT/03-tls.txt"
|
||||||
|
{ run27 EncryptedKeys; } > "$OUT/04-encrypted.txt"
|
||||||
|
{ run27 Failures; } > "$OUT/05-failures.txt"
|
||||||
|
{ run27 Migration; } > "$OUT/06-migration.txt"
|
||||||
|
|
||||||
|
# 07 - the preview flag: what you see without it
|
||||||
|
{
|
||||||
|
echo "\$ javac RoundTrip.java Fx.java (no flags) ($(jv "$JDK27"))"
|
||||||
|
mkdir -p "$B/n"; "$JDK27/bin/javac" -d "$B/n" src/Fx.java src/RoundTrip.java 2>&1; echo "exit=${PIPESTATUS[0]}"
|
||||||
|
echo
|
||||||
|
echo "\$ java RoundTrip (compiled with --enable-preview, run without it) ($(jv "$JDK27"))"
|
||||||
|
"$JDK27/bin/java" -cp "$B/c27" RoundTrip 2>&1 | head -3
|
||||||
|
echo
|
||||||
|
echo "\$ javac --enable-preview --release 27 RoundTrip.java Fx.java"
|
||||||
|
mkdir -p "$B/p"; "$JDK27/bin/javac" --enable-preview --release 27 -Xlint:preview -d "$B/p" src/Fx.java src/RoundTrip.java 2>&1 | head -12; echo "exit=${PIPESTATUS[0]}"
|
||||||
|
} > "$OUT/07-preview-flag.txt"
|
||||||
|
|
||||||
|
# 08 - the same demos on the JDK 28 early-access build, with NO preview flag (JEP 542 finalizes the API)
|
||||||
|
if [ -n "$JDK28" ]; then
|
||||||
|
mkdir -p "$B/c28"
|
||||||
|
{
|
||||||
|
echo "\$ javac src/{Fx,RoundTrip,JwtSigning,TlsFromPem,EncryptedKeys,Failures}.java (no flags) ($(jv "$JDK28"))"
|
||||||
|
"$JDK28/bin/javac" -d "$B/c28" src/Fx.java src/RoundTrip.java src/JwtSigning.java src/TlsFromPem.java src/EncryptedKeys.java src/Failures.java 2>&1; echo "exit=${PIPESTATUS[0]}"
|
||||||
|
for c in RoundTrip JwtSigning TlsFromPem EncryptedKeys Failures; do
|
||||||
|
echo; echo "\$ java $c (no flags)"
|
||||||
|
"$JDK28/bin/java" -Dfixtures=fixtures -cp "$B/c28" $c > "$B/28-$c.txt" 2>&1; echo "exit=$?"
|
||||||
|
"$JDK27/bin/java" --enable-preview -Dfixtures=fixtures -cp "$B/c27" $c > "$B/27-$c.txt" 2>&1
|
||||||
|
if diff -q "$B/27-$c.txt" "$B/28-$c.txt" >/dev/null; then echo "output identical to the JDK 27 run"; else echo "output DIFFERS from the JDK 27 run:"; diff "$B/27-$c.txt" "$B/28-$c.txt" | head -20; fi
|
||||||
|
done
|
||||||
|
} > "$OUT/08-jdk28-final.txt"
|
||||||
|
fi
|
||||||
|
echo done
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import java.security.*;
|
||||||
|
import javax.crypto.EncryptedPrivateKeyInfo;
|
||||||
|
import javax.crypto.spec.PBEParameterSpec;
|
||||||
|
|
||||||
|
/** Encrypted private keys: write one, read one, read one openssl wrote, and every way it can go wrong. */
|
||||||
|
public class EncryptedKeys {
|
||||||
|
static final char[] PW = "changeit".toCharArray();
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
PEMDecoder d = PEMDecoder.of();
|
||||||
|
PEMEncoder e = PEMEncoder.of();
|
||||||
|
PrivateKey key = d.decode(Fx.read("rsa-private-pkcs8.pem"), PrivateKey.class);
|
||||||
|
|
||||||
|
Fx.h("write an encrypted key with the defaults");
|
||||||
|
String enc = e.withEncryption(PW).encodeToString(key);
|
||||||
|
System.out.println(enc.lines().filter(l -> l.startsWith("-----")).toList());
|
||||||
|
EncryptedPrivateKeyInfo info = (EncryptedPrivateKeyInfo) d.decode(enc);
|
||||||
|
System.out.println("default algorithm: " + info.getAlgName());
|
||||||
|
PBEParameterSpec spec = info.getAlgParameters().getParameterSpec(PBEParameterSpec.class);
|
||||||
|
System.out.println("default iteration count: " + spec.getIterationCount() + ", salt bytes: " + spec.getSalt().length);
|
||||||
|
|
||||||
|
Fx.h("read it back");
|
||||||
|
Fx.check(d.withDecryption(PW).decode(enc, PrivateKey.class).equals(key), "withDecryption(password) returns the original key");
|
||||||
|
|
||||||
|
Fx.h("choose the algorithm and cost yourself");
|
||||||
|
PBEParameterSpec custom = new PBEParameterSpec(new SecureRandom().generateSeed(16), 600_000);
|
||||||
|
EncryptedPrivateKeyInfo strong = EncryptedPrivateKeyInfo.encrypt(key, PW, "PBEWithHmacSHA512AndAES_256", custom, null);
|
||||||
|
String strongPem = e.encodeToString(strong);
|
||||||
|
EncryptedPrivateKeyInfo again = (EncryptedPrivateKeyInfo) d.decode(strongPem);
|
||||||
|
System.out.println(again.getAlgName() + ", iterations " + again.getAlgParameters().getParameterSpec(PBEParameterSpec.class).getIterationCount());
|
||||||
|
Fx.check(again.getKey(PW).equals(key), "getKey(password) on the decoded EncryptedPrivateKeyInfo works too");
|
||||||
|
|
||||||
|
Fx.h("a file written by openssl pkcs8 -topk8 -v2 aes-256-cbc");
|
||||||
|
EncryptedPrivateKeyInfo fromOpenssl = (EncryptedPrivateKeyInfo) d.decode(Fx.read("rsa-private-encrypted.pem"));
|
||||||
|
System.out.println(fromOpenssl.getAlgName());
|
||||||
|
Fx.check(d.withDecryption(PW).decode(Fx.read("rsa-private-encrypted.pem"), PrivateKey.class).equals(key), "decrypts to the same key as the unencrypted file");
|
||||||
|
|
||||||
|
Fx.h("failures");
|
||||||
|
String f = Fx.read("rsa-private-encrypted.pem");
|
||||||
|
Fx.tryIt("no password, ask for PrivateKey", () -> d.decode(f, PrivateKey.class));
|
||||||
|
Fx.tryIt("no password, no class", () -> d.decode(f));
|
||||||
|
Fx.tryIt("wrong password", () -> d.withDecryption("nope".toCharArray()).decode(f, PrivateKey.class));
|
||||||
|
Fx.tryIt("password on a key that is not encrypted", () -> d.withDecryption(PW).decode(Fx.read("rsa-private-pkcs8.pem"), PrivateKey.class));
|
||||||
|
Fx.tryIt("openssl 'traditional' encrypted (Proc-Type: 4,ENCRYPTED)", () -> d.withDecryption(PW).decode(Fx.read("rsa-private-legacy-encrypted.pem"), PrivateKey.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import java.security.*;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
|
/** Everything that goes wrong when you feed the decoder something it does not expect. */
|
||||||
|
public class Failures {
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
PEMDecoder d = PEMDecoder.of();
|
||||||
|
|
||||||
|
Fx.h("the file holds a different kind of object than the class you asked for");
|
||||||
|
Fx.tryIt("certificate as PublicKey", () -> d.decode(Fx.read("server.pem"), PublicKey.class));
|
||||||
|
Fx.tryIt("private key as PublicKey", () -> d.decode(Fx.read("rsa-private-pkcs8.pem"), PublicKey.class));
|
||||||
|
Fx.tryIt("RSA private key as KeyPair", () -> d.decode(Fx.read("rsa-private-pkcs8.pem"), KeyPair.class));
|
||||||
|
|
||||||
|
Fx.h("untyped decode: the result type depends on what the file contains");
|
||||||
|
Fx.tryIt("RSA PKCS#8", () -> d.decode(Fx.read("rsa-private-pkcs8.pem")));
|
||||||
|
Fx.tryIt("EC PKCS#8 (openssl embeds the public key)", () -> d.decode(Fx.read("ec-private-pkcs8.pem")));
|
||||||
|
Fx.tryIt("EC PKCS#8, asked for PrivateKey", () -> d.decode(Fx.read("ec-private-pkcs8.pem"), PrivateKey.class));
|
||||||
|
Fx.tryIt("EC PKCS#8, asked for KeyPair", () -> d.decode(Fx.read("ec-private-pkcs8.pem"), KeyPair.class));
|
||||||
|
|
||||||
|
Fx.h("the old OpenSSL formats");
|
||||||
|
Fx.tryIt("RSA PRIVATE KEY (PKCS#1)", () -> d.decode(Fx.read("rsa-private-pkcs1.pem"), PrivateKey.class));
|
||||||
|
Fx.tryIt("EC PRIVATE KEY (SEC1), untyped", () -> d.decode(Fx.read("ec-private-sec1.pem")));
|
||||||
|
Fx.tryIt("EC PRIVATE KEY (SEC1), asked for PrivateKey", () -> d.decode(Fx.read("ec-private-sec1.pem"), PrivateKey.class));
|
||||||
|
PEM raw = d.decode(Fx.read("ec-private-sec1.pem"), PEM.class);
|
||||||
|
System.out.println("as a PEM object: type=" + raw.type() + ", decoded bytes=" + raw.decode().length);
|
||||||
|
|
||||||
|
Fx.h("malformed text");
|
||||||
|
String pub = Fx.read("rsa-public.pem");
|
||||||
|
Fx.tryIt("empty string", () -> d.decode(""));
|
||||||
|
Fx.tryIt("no PEM in the text", () -> d.decode("hello"));
|
||||||
|
Fx.tryIt("footer missing", () -> d.decode(pub.replace("-----END PUBLIC KEY-----", "")));
|
||||||
|
Fx.tryIt("body is not Base64 of a key", () -> d.decode("-----BEGIN PUBLIC KEY-----\n!!!!\n-----END PUBLIC KEY-----\n"));
|
||||||
|
Fx.tryIt("Windows line endings (CRLF)", () -> d.decode(pub.replace("\n", "\r\n")));
|
||||||
|
Fx.tryIt("comment lines before the header", () -> d.decode("# my key\n" + pub));
|
||||||
|
|
||||||
|
Fx.h("several objects in one string: only the first is returned");
|
||||||
|
String chain = Fx.read("server-chain.pem");
|
||||||
|
X509Certificate first = d.decode(chain, X509Certificate.class);
|
||||||
|
System.out.println("decode(String) on a 2-certificate bundle -> " + first.getSubjectX500Principal());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/** Tiny helper shared by the demos: read a fixture, print a section header, assert. */
|
||||||
|
final class Fx {
|
||||||
|
static final Path DIR = Path.of(System.getProperty("fixtures", "fixtures"));
|
||||||
|
static String read(String name) {
|
||||||
|
try { return Files.readString(DIR.resolve(name)); }
|
||||||
|
catch (IOException e) { throw new RuntimeException(e); }
|
||||||
|
}
|
||||||
|
static void h(String s) { System.out.println("== " + s); }
|
||||||
|
static void check(boolean ok, String what) {
|
||||||
|
if (!ok) throw new AssertionError(what);
|
||||||
|
System.out.println("ok: " + what);
|
||||||
|
}
|
||||||
|
/** Run, and print the exception class and message instead of a stack trace. */
|
||||||
|
static void tryIt(String label, java.util.concurrent.Callable<Object> c) {
|
||||||
|
try {
|
||||||
|
Object o = c.call();
|
||||||
|
System.out.println(label + " -> " + describe(o));
|
||||||
|
} catch (Throwable t) {
|
||||||
|
System.out.println(label + " !! " + t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static String describe(Object o) {
|
||||||
|
if (o instanceof java.security.PEM p) return "PEM(type=" + p.type() + ") - NOT decoded";
|
||||||
|
if (o instanceof java.security.KeyPair) return "KeyPair";
|
||||||
|
if (o instanceof java.security.PrivateKey k) return "PrivateKey " + k.getAlgorithm();
|
||||||
|
if (o instanceof java.security.PublicKey k) return "PublicKey " + k.getAlgorithm();
|
||||||
|
if (o instanceof java.security.cert.X509Certificate c) return "X509Certificate " + c.getSubjectX500Principal();
|
||||||
|
if (o instanceof javax.crypto.EncryptedPrivateKeyInfo e) return "EncryptedPrivateKeyInfo " + e.getAlgName();
|
||||||
|
return String.valueOf(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.*;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/** Sign and verify a JWT with keys loaded from PEM files. No JWT library: the point is the key handling. */
|
||||||
|
public class JwtSigning {
|
||||||
|
static final Base64.Encoder B64 = Base64.getUrlEncoder().withoutPadding();
|
||||||
|
|
||||||
|
static String signingInput(String alg) {
|
||||||
|
String header = "{\"alg\":\"" + alg + "\",\"typ\":\"JWT\"}";
|
||||||
|
String payload = "{\"sub\":\"ankur\",\"iss\":\"https://ankurm.com\"}";
|
||||||
|
return B64.encodeToString(header.getBytes(StandardCharsets.UTF_8)) + "." + B64.encodeToString(payload.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] sign(String jcaAlg, PrivateKey key, String input) throws Exception {
|
||||||
|
Signature s = Signature.getInstance(jcaAlg);
|
||||||
|
s.initSign(key);
|
||||||
|
s.update(input.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
return s.sign();
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean verify(String jcaAlg, PublicKey key, String input, byte[] sig) throws Exception {
|
||||||
|
Signature s = Signature.getInstance(jcaAlg);
|
||||||
|
s.initVerify(key);
|
||||||
|
s.update(input.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
return s.verify(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
PEMDecoder d = PEMDecoder.of();
|
||||||
|
|
||||||
|
Fx.h("RS256 with an RSA key from PEM");
|
||||||
|
PrivateKey rsaPriv = d.decode(Fx.read("rsa-private-pkcs8.pem"), PrivateKey.class);
|
||||||
|
PublicKey rsaPub = d.decode(Fx.read("rsa-public.pem"), PublicKey.class);
|
||||||
|
String in = signingInput("RS256");
|
||||||
|
byte[] sig = sign("SHA256withRSA", rsaPriv, in);
|
||||||
|
String jwt = in + "." + B64.encodeToString(sig);
|
||||||
|
System.out.println("header.payload = " + in);
|
||||||
|
System.out.println("signature bytes = " + sig.length + ", jwt parts = " + jwt.split("\\.").length);
|
||||||
|
Fx.check(verify("SHA256withRSA", rsaPub, in, sig), "RS256 signature verifies with the public PEM");
|
||||||
|
|
||||||
|
Fx.h("ES256 with an EC key from PEM (the trap is the signature format)");
|
||||||
|
PrivateKey ecPriv = d.decode(Fx.read("ec-private-pkcs8.pem"), PrivateKey.class);
|
||||||
|
PublicKey ecPub = d.decode(Fx.read("ec-public.pem"), PublicKey.class);
|
||||||
|
String in2 = signingInput("ES256");
|
||||||
|
byte[] der = sign("SHA256withECDSA", ecPriv, in2);
|
||||||
|
byte[] raw = sign("SHA256withECDSAinP1363Format", ecPriv, in2);
|
||||||
|
System.out.println("SHA256withECDSA -> DER, starts 0x" + Integer.toHexString(der[0] & 0xff) + ", length varies run to run, 70-72: " + (der.length >= 70 && der.length <= 72) + " (JWT readers reject this)");
|
||||||
|
System.out.println("SHA256withECDSAinP1363Format -> R||S, length " + raw.length + " (what RFC 7518 requires for ES256)");
|
||||||
|
Fx.check(raw.length == 64, "ES256 signature is exactly 64 bytes");
|
||||||
|
Fx.check(verify("SHA256withECDSAinP1363Format", ecPub, in2, raw), "ES256 signature verifies with the public PEM");
|
||||||
|
|
||||||
|
Fx.h("EdDSA with an Ed25519 key from PEM");
|
||||||
|
PrivateKey edPriv = d.decode(Fx.read("ed25519-private.pem"), PrivateKey.class);
|
||||||
|
byte[] ed = sign("Ed25519", edPriv, signingInput("EdDSA"));
|
||||||
|
System.out.println(edPriv.getAlgorithm() + " signature bytes = " + ed.length
|
||||||
|
+ " (the PKCS#8 file holds no public key, so there is nothing to decode for verifying here)");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import com.sun.net.httpserver.HttpsConfigurator;
|
||||||
|
import com.sun.net.httpserver.HttpsServer;
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.security.*;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.net.ssl.*;
|
||||||
|
|
||||||
|
/** Build an SSLContext from three PEM files and talk to ourselves over TLS. */
|
||||||
|
public class TlsFromPem {
|
||||||
|
static List<Certificate> readAll(String pemText) throws Exception {
|
||||||
|
List<Certificate> out = new ArrayList<>();
|
||||||
|
try (InputStream in = new java.io.ByteArrayInputStream(pemText.getBytes())) {
|
||||||
|
PEMDecoder d = PEMDecoder.of();
|
||||||
|
while (true) {
|
||||||
|
try { out.add(d.decode(in, X509Certificate.class)); }
|
||||||
|
catch (EOFException end) { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
PEMDecoder d = PEMDecoder.of();
|
||||||
|
|
||||||
|
Fx.h("read a two-certificate chain file");
|
||||||
|
List<Certificate> chain = readAll(Fx.read("server-chain.pem"));
|
||||||
|
for (Certificate c : chain) System.out.println(" " + ((X509Certificate) c).getSubjectX500Principal());
|
||||||
|
Fx.check(chain.size() == 2, "the stream decoder returns the two certificates one by one");
|
||||||
|
|
||||||
|
Fx.h("server side: key + chain -> KeyStore -> KeyManagerFactory -> SSLContext");
|
||||||
|
PrivateKey key = d.decode(Fx.read("server-key.pem"), PrivateKey.class);
|
||||||
|
char[] pw = new char[0];
|
||||||
|
KeyStore ks = KeyStore.getInstance("PKCS12");
|
||||||
|
ks.load(null, null);
|
||||||
|
ks.setKeyEntry("server", key, pw, chain.toArray(new Certificate[0]));
|
||||||
|
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||||
|
kmf.init(ks, pw);
|
||||||
|
SSLContext serverCtx = SSLContext.getInstance("TLS");
|
||||||
|
serverCtx.init(kmf.getKeyManagers(), null, null);
|
||||||
|
|
||||||
|
HttpsServer server = HttpsServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
|
||||||
|
server.setHttpsConfigurator(new HttpsConfigurator(serverCtx));
|
||||||
|
server.createContext("/", ex -> {
|
||||||
|
byte[] body = "hello over TLS\n".getBytes();
|
||||||
|
ex.sendResponseHeaders(200, body.length);
|
||||||
|
ex.getResponseBody().write(body);
|
||||||
|
ex.close();
|
||||||
|
});
|
||||||
|
server.start();
|
||||||
|
URI uri = URI.create("https://localhost:" + server.getAddress().getPort() + "/");
|
||||||
|
|
||||||
|
try {
|
||||||
|
Fx.h("client trusting only the CA certificate from ca.pem");
|
||||||
|
KeyStore ts = KeyStore.getInstance("PKCS12");
|
||||||
|
ts.load(null, null);
|
||||||
|
ts.setCertificateEntry("ca", d.decode(Fx.read("ca.pem"), X509Certificate.class));
|
||||||
|
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||||
|
tmf.init(ts);
|
||||||
|
SSLContext clientCtx = SSLContext.getInstance("TLS");
|
||||||
|
clientCtx.init(null, tmf.getTrustManagers(), null);
|
||||||
|
HttpClient trusting = HttpClient.newBuilder().sslContext(clientCtx).build();
|
||||||
|
HttpResponse<String> r = trusting.send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString());
|
||||||
|
System.out.println("HTTP " + r.statusCode() + " " + r.body().strip() + " (" + r.sslSession().get().getProtocol() + ")");
|
||||||
|
System.out.println("server presented: " + ((X509Certificate) r.sslSession().get().getPeerCertificates()[0]).getSubjectX500Principal());
|
||||||
|
Fx.check(r.statusCode() == 200, "request over TLS succeeds");
|
||||||
|
|
||||||
|
Fx.h("same request with the JVM's default trust store (the CA is not in it)");
|
||||||
|
try {
|
||||||
|
HttpClient.newHttpClient().send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println(e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
server.stop(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user