Skip to content
ecdsa.com

Export public key PEM · Java

How to export a public key as PEM in Java

To export a public key as PEM in Java, start from a PublicKey: getEncoded() already returns SPKI DER, and PEM is that DER in base64 between BEGIN PUBLIC KEY lines. Getting the public key out of a bare private key is the hard part — the JCA has no call for it — so this recipe computes it with Bouncy Castle, and shows the JDK-only route for keys stored in a keystore.

Tested with Bouncy Castle (bcprov-jdk18on) 1.86 (OpenJDK 25.0.4.1 (Temurin), Maven 3.9.16) and OpenSSL 3.0.13 (openssl pkey / pkcs12 cross-check).

pom.xml — Bouncy Castle (Gradle: org.bouncycastle:bcprov-jdk18on:1.86)
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.86</version>
</dependency>
ExportPublicKey.java — private key PEM in, public key PEM out
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";
}
}
JDK only: the public key of a keystore entry
// Keys kept in a keystore come with a certificate, and the certificate holds the public key
KeyStore keyStore = KeyStore.getInstance(new File("keys.p12"), System.getenv("STORE_PASS").toCharArray());
PublicKey fromKeystore = keyStore.getCertificate("signing").getPublicKey(); // JDK only, no derivation
System.out.print(ExportPublicKey.toPem(fromKeystore));

How it works

  1. The private key loads with the JDK alone: PKCS8EncodedKeySpec reads the body of BEGIN PRIVATE KEY, and getS() exposes the secret scalar d.
  2. An EC public key is the point Q = d·G. Bouncy Castle's ECNamedCurveTable supplies the curve and its generator G; multiply(...).normalize() computes Q in affine coordinates.
  3. The curve is looked up by name — the AlgorithmParameters round trip turns the key's parameters into secp256r1 — so the resulting SPKI carries the curve OID, exactly like OpenSSL's output.
  4. toPem base64-encodes getEncoded() in 64-column lines with LF endings; the run above checks that the file is byte-identical to openssl pkey -pubout.
  5. 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 ECParameterSpec instead 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 under BEGIN EC PUBLIC KEY or BEGIN RSA PUBLIC KEY produces a file other stacks reject.
  • Passing new BouncyCastleProvider() directly to KeyFactory.getInstance keeps the JDK's provider order untouched; Security.addProvider would make BC visible to every library in the process.

Related recipes