October 28, 2019

Create HTTPS Certificates in Java: The unsafe way

TLDR: I recommend using Bouncy Castle instead of this method. I used this method in the past but I’m using Bouncy Castle now to support Java 11+.

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.

This guide is heavily based on Ke Pi’s blog post. However, this approach is fragile because it is using OpenJDK implementation details by using sun packages. These APIs are unstable and you should avoid them. It won’t work with enabled security manager or OpenJDK 11+. Prefer using a Bouncy Castle, see here.

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.

gamlor.info Certificate
Figure 1. gamlor.info Certificate

Generate the Certificate

First, we 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.

Unsafe use of JDK internals
Figure 2. Unsafe use of JDK internals
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 {
    CertAndKeyGen keyGen = new CertAndKeyGen("RSA", "SHA256withRSA", null);
    // 1024 bit RSA key
    keyGen.generate(1024);

    PrivateKey privateKey = keyGen.getPrivateKey();
    long years10 = 10 * 365 * 24 * 3600;
    // Create self-signed certificate to start
    X509Certificate cert = keyGen.getSelfCertificate(new X500Name("CN=" + cnName), new Date(), years10);

    // Allow editing the cert info to add more information
    CertificateExtensions extensions = new CertificateExtensions();
    // Make the cert to a Cert Authority to sign more certs when needed
    if (isCA) {
        int maxChainLength = -1; // unlimited
        extensions.set(BasicConstraintsExtension.NAME, new BasicConstraintsExtension(true, isCA, maxChainLength));
    }
    // Modern browsers demand the DNS name entry
    if (domain != null) {
        GeneralNames dnsNames = new GeneralNames();
        dnsNames.add(new GeneralName(new DNSName(domain)));
        SubjectAlternativeNameExtension domainsExt = new SubjectAlternativeNameExtension(dnsNames);
        extensions.set(SubjectAlternativeNameExtension.NAME, domainsExt);
    }
    // Create a editable cert info from our basic certificate
    X509CertInfo info = new X509CertInfo(cert.getTBSCertificate());
    // And add our information
    info.set(X509CertInfo.EXTENSIONS, extensions);

    // If there is no issuer, we self-sign our certificate
    if (issuer == null) {
        issuer = new GeneratedCert(privateKey, cert);
    }

    X509Certificate signedCert = signCertificate(info, issuer);
    return new GeneratedCert(privateKey, signedCert);
}

private static X509CertImpl signCertificate(X509CertInfo infoToEdit,
                                            GeneratedCert issuer) throws Exception {
    // Set the issuer name in the cert info
    Principal issuerName = issuer.certificate.getSubjectDN();
    infoToEdit.set(X509CertInfo.ISSUER, issuerName);

    // Use the private key of the issuer to sign the certificate
    X509CertImpl ourCert = new X509CertImpl(infoToEdit);
    ourCert.sign(issuer.privateKey, issuer.certificate.getSigAlgName());
    return ourCert;
}

Build the Chain

With our certificate build function we now create a certificate chain. We start with a root CA, then create a intermediate 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:

Generated Certificate
Figure 3. Generated Certificate

Source Code

Here is the full source.

Tags: Java