<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> <version>1.86</version></dependency> import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Path;import java.security.AlgorithmParameters;import java.security.KeyFactory;import java.security.PublicKey;import java.security.interfaces.ECPrivateKey;import java.security.spec.ECGenParameterSpec;import java.security.spec.PKCS8EncodedKeySpec;import java.util.Base64;import org.bouncycastle.jce.ECNamedCurveTable;import org.bouncycastle.jce.provider.BouncyCastleProvider;import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;import org.bouncycastle.jce.spec.ECPublicKeySpec; public class ExportPublicKey { public static void main(String[] args) throws Exception { String pem = Files.readString(Path.of("key.pem")); byte[] pkcs8 = Base64.getMimeDecoder().decode(pem.replaceAll("-----[^-]+-----", "")); ECPrivateKey privateKey = (ECPrivateKey) KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(pkcs8)); // JCA has no "public key of this private key" call: compute Q = d·G with Bouncy Castle, // on the curve looked up by name so the SPKI names the curve instead of spelling it out AlgorithmParameters params = AlgorithmParameters.getInstance("EC"); params.init(privateKey.getParams()); String curveName = params.getParameterSpec(ECGenParameterSpec.class).getName(); // secp256r1 ECNamedCurveParameterSpec curve = ECNamedCurveTable.getParameterSpec(curveName); PublicKey publicKey = KeyFactory.getInstance("EC", new BouncyCastleProvider()) .generatePublic(new ECPublicKeySpec(curve.getG().multiply(privateKey.getS()).normalize(), curve)); Files.writeString(Path.of("pub.pem"), toPem(publicKey)); System.out.print(toPem(publicKey)); } // getEncoded() is SPKI DER; PEM is its base64 in 64-column lines between the armor lines static String toPem(PublicKey key) { Base64.Encoder base64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.US_ASCII)); return "-----BEGIN PUBLIC KEY-----\n" + base64.encodeToString(key.getEncoded()) + "\n-----END PUBLIC KEY-----\n"; }} // Keys kept in a keystore come with a certificate, and the certificate holds the public keyKeyStore keyStore = KeyStore.getInstance(new File("keys.p12"), System.getenv("STORE_PASS").toCharArray());PublicKey fromKeystore = keyStore.getCertificate("signing").getPublicKey(); // JDK only, no derivationSystem.out.print(ExportPublicKey.toPem(fromKeystore)); How it works
- The private key loads with the JDK alone:
PKCS8EncodedKeySpecreads the body ofBEGIN PRIVATE KEY, andgetS()exposes the secret scalar d. - An EC public key is the point Q = d·G. Bouncy Castle's
ECNamedCurveTablesupplies the curve and its generator G;multiply(...).normalize()computes Q in affine coordinates. - The curve is looked up by name — the
AlgorithmParametersround trip turns the key's parameters intosecp256r1— so the resulting SPKI carries the curve OID, exactly like OpenSSL's output. toPembase64-encodesgetEncoded()in 64-column lines with LF endings; the run above checks that the file is byte-identical toopenssl pkey -pubout.- A key that lives in a PKCS#12 or JKS keystore needs no math: every private key entry carries a certificate, and
getCertificate(alias).getPublicKey()returns the public key directly.
Gotchas
- Building the BC key from the private key's own
ECParameterSpecinstead of the named curve produces an SPKI with explicit curve parameters (311 bytes instead of 91). The JDK itself refuses to read it:IOException: Only named ECParameters supported. Base64.getMimeEncoder()without arguments writes 76-character lines with CRLF endings. Most parsers tolerate it, but RFC 7468 asks for 64 columns, and byte-for-byte comparisons with other tools fail — pass the line length and"\n"explicitly.- The label is part of the format: SPKI DER belongs under
BEGIN PUBLIC KEY. Writing it underBEGIN EC PUBLIC KEYorBEGIN RSA PUBLIC KEYproduces a file other stacks reject. - Passing
new BouncyCastleProvider()directly toKeyFactory.getInstancekeeps the JDK's provider order untouched;Security.addProviderwould make BC visible to every library in the process.