Skip to content
ecdsa.com

Self-signed certificate · Java

How to create a self-signed EC P-256 certificate with SANs in Java

The JDK has no public API for building certificates, so Java code uses Bouncy Castle's JcaX509v3CertificateBuilder: it takes the issuer and the subject (the same name), a serial, the validity and the public key, addExtension adds the SANs and key usages, and a JcaContentSignerBuilder signs with SHA256withECDSA. The run decodes cert.pem with OpenSSL and verifies it for localhost and 127.0.0.1.

Tested with org.bouncycastle:bcpkix-jdk18on 1.86 (OpenJDK 25.0.4.1 (Temurin), SunEC keys).

SelfSigned.java — P-256 key and self-signed server certificate with Bouncy Castle
import java.io.FileWriter;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.spec.ECGenParameterSpec;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
public class SelfSigned {
public static void main(String[] args) throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
generator.initialize(new ECGenParameterSpec("secp256r1")); // P-256
KeyPair keys = generator.generateKeyPair();
X500Name name = new X500Name("CN=localhost");
Instant notBefore = Instant.now();
JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
name, // issuer = subject: self-signed
new BigInteger(127, new SecureRandom()), // random, positive serial
Date.from(notBefore), Date.from(notBefore.plus(Duration.ofDays(365))),
name, keys.getPublic());
builder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(new GeneralName[] {
new GeneralName(GeneralName.dNSName, "localhost"),
new GeneralName(GeneralName.iPAddress, "127.0.0.1"),
}));
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false));
builder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature));
builder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth));
X509CertificateHolder cert = builder.build(new JcaContentSignerBuilder("SHA256withECDSA").build(keys.getPrivate()));
try (JcaPEMWriter out = new JcaPEMWriter(new FileWriter("cert.pem"))) {
out.writeObject(cert);
}
try (JcaPEMWriter out = new JcaPEMWriter(new FileWriter("key.pem"))) {
out.writeObject(new JcaPKCS8Generator(keys.getPrivate(), null));
}
}
}

How it works

  1. KeyPairGenerator.getInstance("EC") with ECGenParameterSpec("secp256r1") creates the P-256 key in the JDK's own provider.
  2. new BigInteger(127, new SecureRandom()) is a random positive serial; the validity is Instant.now() plus 365 days, converted to Date.
  3. GeneralName.dNSName and GeneralName.iPAddress are the SAN types — Bouncy Castle turns the address text into its bytes — and the boolean of addExtension is critical.
  4. JcaPEMWriter writes the certificate holder as PEM and, through JcaPKCS8Generator, the key as PKCS#8; new JcaX509CertificateConverter().getCertificate(holder) gives a java.security.cert.X509Certificate when you need one.

Gotchas

  • Without a library, the JDK's keytool does the same in one command: keytool -genkeypair -alias localhost -keyalg EC -groupname secp256r1 -sigalg SHA256withECDSA -dname CN=localhost -ext san=dns:localhost,ip:127.0.0.1 -ext bc:c=ca:false -ext ku:c=digitalSignature -ext eku=serverAuth -validity 365 -keystore localhost.p12. The key then stays in the PKCS#12 keystore, and without -sigalg the keytool of JDK 25 signs a P-256 certificate with SHA384withECDSA.
  • GeneralName.iPAddress needs a literal address: a host name there fails with java.lang.IllegalArgumentException: IP Address is invalid.
  • JcaPEMWriter buffers: until it is closed, the file stays empty (0 bytes in the run), so write inside try-with-resources.
  • X500Name, GeneralName and the other ASN.1 classes are in bcprov, not bcpkix; put bcpkix, bcprov and bcutil of the same release on the class path together.

Related recipes