Create HTTPS Certificates in Java with Bouncy Castle
Multiple times I needed to create HTTPS certificates programmatically. For example to create test certificates for HTTPS in development without complicated setup. There is surprisingly little information out there about how to create certificates programmatically in Java. Most guides OpenSSL or another command-line tool. I wanted to avoid to run an external program but wanted to do it programmatically.
In the past, I used a hacky way to do in Java by using JDK internals. However, as I moved to Java 11+ I needed a better way, since the API’s changed and are now inaccessible due to the module system.
Cert Chains
Let’s refresh how HTTPS certificate chains work. Your browser has a few trusted root CA certificates. The owner of these certificates usually signs an intermediate certificate. And that certificate then signs a certificate for a domain. You can see this if you inspect a website’s certificate in your browser.
Get Bouncy Castle
Bouncy Castle has many libraries for different purposes and to support ancient Java versions. It is confusing
what you need to download. For creating certificates you need the bcpkix
library, the JDK 1.5 onwards version.
The last release of it is 1.64. Here is the Maven repo info: https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on/1.64
Generate the Certificate
First, we need to generate a self-signed certificate. We use the CertAndKeyGen, set the key size and generate the certificate with the desired name. Then we add extra bits of information, like if the certificate can sign other certificates and the DNS name. Modern browsers require a DNS name otherwise, they will complain about the certificate. The self-signed certificate is then later signed with the given issuer certificate.
import sun.security.tools.keytool.CertAndKeyGen;
import sun.security.x509.*;
// To create a certificate chain we need the issuers' certificate and private key. Keep these together to pass around
final static class GeneratedCert {
public final PrivateKey privateKey;
public final X509Certificate certificate;
public GeneratedCert(PrivateKey privateKey, X509Certificate certificate) {
this.privateKey = privateKey;
this.certificate = certificate;
}
}
/**
* @param cnName The CN={name} of the certificate. When the certificate is for a domain it should be the domain name
* @param domain Nullable. The DNS domain for the certificate.
* @param issuer Issuer who signs this certificate. Null for a self-signed certificate
* @param isCA Can this certificate be used to sign other certificates
* @return Newly created certificate with its private key
*/
private static GeneratedCert createCertificate(String cnName, String domain, GeneratedCert issuer, boolean isCA) throws Exception {
// Generate the key-pair with the official Java API's
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
KeyPair certKeyPair = keyGen.generateKeyPair();
X500Name name = new X500Name("CN=" + cnName);
// If you issue more than just test certificates, you might want a decent serial number schema ^.^
BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis());
Instant validFrom = Instant.now();
Instant validUntil = validFrom.plus(10 * 360, ChronoUnit.DAYS);
// If there is no issuer, we self-sign our certificate.
X500Name issuerName;
PrivateKey issuerKey;
if (issuer == null) {
issuerName = name;
issuerKey = certKeyPair.getPrivate();
} else {
issuerName = new X500Name(issuer.certificate.getSubjectDN().getName());
issuerKey = issuer.privateKey;
}
// The cert builder to build up our certificate information
JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
issuerName,
serialNumber,
Date.from(validFrom), Date.from(validUntil),
name, certKeyPair.getPublic());
// Make the cert to a Cert Authority to sign more certs when needed
if (isCA) {
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(isCA));
}
// Modern browsers demand the DNS name entry
if (domain != null) {
builder.addExtension(Extension.subjectAlternativeName, false,
new GeneralNames(new GeneralName(GeneralName.dNSName, domain)));
}
// Finally, sign the certificate:
ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSA").build(issuerKey);
X509CertificateHolder certHolder = builder.build(signer);
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certHolder);
return new GeneratedCert(certKeyPair.getPrivate(), cert);
}
Build the Chain
With our certificate build function we now create a certificate chain. We start with a root CA, then create a indermediate CA and finally create domain certificates:
GeneratedCert rootCA = createCertificate("do_not_trust_test_certs_root", /*domain=*/null, /*issuer=*/null, /*isCa=*/true);
GeneratedCert issuer = createCertificate("do_not_trust_test_certs_issuer", /*domain=*/null, rootCA, /*isCa=*/true);
GeneratedCert domain = createCertificate("local.gamlor.info", "local.gamlor.info", issuer, /*isCa=*/false);
GeneratedCert otherD = createCertificate("other.gamlor.info", "other.gamlor.info", issuer, /*isCa=*/false);
Store as PKCS12 (.pfx) file
Lastly we store the created certificate and private keys in PKCS12 (*.pfx) file. For example Jetty happily uses such a file for serving HTTPS:
char[] emptyPassword = new char[0];
KeyStore keyStore = KeyStore.getInstance("PKCS12");
// Key store expects a load first to initialize.
keyStore.load(null, emptyPassword);
// Store our domain certificate, with the private key and the cert chain
keyStore.setKeyEntry("local.gamlor.info", domain.privateKey, emptyPassword,
new X509Certificate[]{domain.certificate, issuer.certificate, rootCA.certificate});
keyStore.setKeyEntry("other.local.gamlor.info", otherD.privateKey, emptyPassword,
new X509Certificate[]{otherD.certificate, issuer.certificate, rootCA.certificate});
// Store to a file
try (FileOutputStream store = new FileOutputStream("my-cert.pfx")) {
keyStore.store(store, emptyPassword);
}
Here is the generated cert after I imported the pfx file into the Windows Cert store:
Source Code
Here is the full source.