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,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