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))); } } }