jLuger.de - TLS with bouncycastle and programatically generated certificate

When using HTTPS your are using HTTP over a TLS secured connection. TLS requires the site to have certificate to identify itself. The site certificate is signed by another entity whose certificate is signed by a root certificate which your browser knows and trusts. As you may know you can create such a root certificate yourself and add it to the default ones. That way you can create site certificates that your browser trusts. I have done creating such a root certificate and written a program that uses that root certificate to sign a programmatically generated site certificate. As an addon I've only used a plain socket and started wrapping the established connection to use TLS. As there isn't much documentation for bouncycastle I'm going to show how this could be done with this framework.

First the simple "HTTP" server that uses the same response for all kind of queries. It does only the required parsing to send the response in time.
    private static final String MESSAGE = "HTTP/1.1 200 OK\r\n"
+ "Content-Type: text/html; charset=UTF-8\r\n"
+ "\r\n" +
"<html>\n" +
"<head>\n" +
" <title>An Example Page</title>\n" +
"</head>\n" +
"<body>\n" +
" Hello World, this is a very simple HTML document.\n" +
"</body>\n" +
"</html>";

public static void main( String[] args ) throws IOException
{
ServerSocket serverSocket = new ServerSocket(8080);
while(true) {
final Socket socket = serverSocket.accept();
Thread thread = new Thread(()->{
try {
InputStream inputStream = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
while ((line=reader.readLine())!=null) {
System.out.println(line);
if ("".equals(line.trim())) {
break;
}
}
OutputStream outputStream = socket.getOutputStream();
outputStream.write(MESSAGE.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
});
thread.start();
}
}
The search for the empty line was the required HTTP parsing I had to do so that Firefox would accept my response.

The following code wraps the socket connection with TLS.
package de.jluger.tlstestserver;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Date;

import javax.security.auth.x500.X500Principal;

import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.tls.Certificate;
import org.bouncycastle.crypto.tls.DefaultTlsServer;
import org.bouncycastle.crypto.tls.DefaultTlsSignerCredentials;
import org.bouncycastle.crypto.tls.HashAlgorithm;
import org.bouncycastle.crypto.tls.ProtocolVersion;
import org.bouncycastle.crypto.tls.SignatureAlgorithm;
import org.bouncycastle.crypto.tls.SignatureAndHashAlgorithm;
import org.bouncycastle.crypto.tls.TlsServerProtocol;
import org.bouncycastle.crypto.tls.TlsSignerCredentials;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;

/**
* Simple TLS secured server that answers all requests with the same page. Dynamically creates
* a new site certificate that is signed by a fixed root certificate.
*/
public class App {
private static final long ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000;
private static final long TEN_YEARS_IN_MILLIS = 10l * 365 * ONE_DAY_IN_MILLIS;
/**
* The http response for all given queries.
*/
private static final String MESSAGE = "HTTP/1.1 200 OK\r\n"
+ "Content-Type: text/html; charset=UTF-8\r\n"
+ "\r\n" +
"<html>\n" +
"<head>\n" +
" <title>An Example Page</title>\n" +
"</head>\n" +
"<body>\n" +
" Hello World, this is a very simple HTML document.\n" +
"</body>\n" +
"</html>";

/**
* Container for the certificate data needed by {@link DefaultTlsSignerCredentials}
*/
private static class CertificateData {
private Certificate certificate;
private AsymmetricKeyParameter privateKeyParameter;

public CertificateData(Certificate certificate,AsymmetricKeyParameter privateKeyParameter) {
this.certificate = certificate;
this.privateKeyParameter = privateKeyParameter;
}

public Certificate getCertificate() {
return certificate;
}

public AsymmetricKeyParameter getPrivateKeyParameter() {
return privateKeyParameter;
}
}

/**
* Builds dynamically a site certificate signed by fixed root certificate.
* All methods, except the {@link CertificateDataBuilder#build()}, provide
* a required data piece for the certificate and have to be called once.
*/
private static class CertificateDataBuilder {
private KeyPair subjectKeyPair = null;
private PrivateKey issuerPrivateKey = null;
private org.bouncycastle.asn1.x509.Certificate issuerCertificate = null;
private String hostname = null;
private Date notBefore = null;
private Date notAfter = null;
private BigInteger serial = null;

public CertificateDataBuilder createSubjectKeyPair() {
KeyPairGenerator kpGen = null;
try {
kpGen = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
throw new RuntimeException(e);
}
kpGen.initialize(2048, new SecureRandom());
subjectKeyPair = kpGen.generateKeyPair();
return this;
}

private PemObject loadPemResource(String resource) throws IOException {
InputStream inputStream = App.class.getResourceAsStream(resource);
try (PemReader p = new PemReader(new InputStreamReader(inputStream));) {
PemObject o = p.readPemObject();
return o;
}
}

public CertificateDataBuilder loadIssuerPrivateKey() throws IOException {
PemObject o = loadPemResource("/rootCA.key");
KeyFactory keyFactory;
try {
keyFactory = KeyFactory.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
KeySpec privateKeySpec = new PKCS8EncodedKeySpec(o.getContent());
try {
issuerPrivateKey = keyFactory.generatePrivate(privateKeySpec);
} catch (InvalidKeySpecException e) {
throw new RuntimeException(e);
}
return this;
}

public CertificateDataBuilder loadIssuerCertificate() throws IOException {
PemObject pem = loadPemResource("/rootCA.crt");
if (pem.getType().endsWith("CERTIFICATE"))
{
issuerCertificate = org.bouncycastle.asn1.x509.Certificate.getInstance(pem.getContent());
} else {
throw new RuntimeException("Failed to laod root certificate.");
}
return this;
}

public CertificateDataBuilder setHostname(String hostname) {
this.hostname = hostname;
return this;
}
public CertificateDataBuilder setNotBefore(Date notBefore) {
this.notBefore = notBefore;
return this;
}
public CertificateDataBuilder setNotAfter(Date notAfter) {
this.notAfter = notAfter;
return this;
}
public CertificateDataBuilder setSerial(BigInteger serial) {
this.serial = serial;
return this;
}

public CertificateData build() throws IOException {
if (subjectKeyPair == null || issuerPrivateKey == null || issuerCertificate == null
|| hostname == null || notBefore == null || notAfter == null || serial == null) {
throw new IllegalStateException("Builder not initialized");
}
X500Principal subject = new X500Principal("CN=" + hostname);
X500Principal issuer = new X500Principal(issuerCertificate.getSubject().getEncoded());
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(issuer, serial,
notBefore, notAfter, subject, subjectKeyPair.getPublic());
certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false));
ContentSigner sigGen;
try {
sigGen = new JcaContentSignerBuilder("SHA256WithRSAEncryption").setProvider(
BouncyCastleProvider.PROVIDER_NAME).build(issuerPrivateKey);
} catch (OperatorCreationException e) {
throw new RuntimeException(e);
}
org.bouncycastle.asn1.x509.Certificate subjectCertificate = certBuilder.build(sigGen)
.toASN1Structure();
Certificate cCert = new Certificate(new org.bouncycastle.asn1.x509.Certificate[] {
subjectCertificate, issuerCertificate });
AsymmetricKeyParameter privateKeyParameter = PrivateKeyFactory.createKey(subjectKeyPair
.getPrivate().getEncoded());
return new CertificateData(cCert, privateKeyParameter);
}
}

/**
* A {@link DefaultTlsServer} that uses TLS 1.2 and RSA for the certificate keys.
*/
private static class RsaSignerTls12Server extends DefaultTlsServer {
private CertificateData certificateData;

public RsaSignerTls12Server(CertificateData certificateData) {
this.certificateData = certificateData;
}

protected TlsSignerCredentials getRSASignerCredentials() throws IOException {
SignatureAndHashAlgorithm signatureAndHashAlgorithm = new SignatureAndHashAlgorithm(
HashAlgorithm.sha256, SignatureAlgorithm.rsa);
return new DefaultTlsSignerCredentials(context, certificateData.getCertificate(),
certificateData.getPrivateKeyParameter(), signatureAndHashAlgorithm);
}

protected org.bouncycastle.crypto.tls.ProtocolVersion getMaximumVersion() {
return ProtocolVersion.TLSv12;
};
}

/**
* Demonstrate how to build a certificate for localhost and how to wrap a plain text
* socket to support TLS. Understands enough HTTP to send a fixed respones for all
* http queries.
*/
public static void main(String[] args) throws IOException {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
CertificateDataBuilder certBuilder = new CertificateDataBuilder();
certBuilder.createSubjectKeyPair().loadIssuerCertificate().loadIssuerPrivateKey();
certBuilder.setHostname("localhost");
certBuilder.setNotAfter(new Date(System.currentTimeMillis() + TEN_YEARS_IN_MILLIS));
certBuilder.setNotBefore(new Date(System.currentTimeMillis() - ONE_DAY_IN_MILLIS));
certBuilder.setSerial(BigInteger.valueOf(System.currentTimeMillis()));
CertificateData certificateData = certBuilder.build();
try (ServerSocket serverSocket = new ServerSocket(8080);) {
while (true) {
final Socket socket = serverSocket.accept();
Thread thread = new Thread(() -> {
try {
TlsServerProtocol sslServer = new TlsServerProtocol(socket.getInputStream(),
socket.getOutputStream(), new SecureRandom());
sslServer.accept(new RsaSignerTls12Server(certificateData));
InputStream inputStream = sslServer.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line);
if ("".equals(line.trim())) {
break;
}
}
OutputStream outputStream = sslServer.getOutputStream();
outputStream.write(MESSAGE.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
});
thread.start();
}
}
}

}

In order to run this program you need to generate a file called rootCA.key and rootCA.crt.

For rootCA.key I've used this command:
openssl genrsa -out rootCA.key 2048
For rooCA.crt I've use this command:
openssl req -x509 -new -nodes -key rootCA.key -days 3650 -out rootCA.crt
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:DE
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Selfsigner
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:Selfsigner CA
Email Address []:

You may notice that the main method stayed almost the same. The wrapping of the socket object with TLS was taken from http://stackoverflow.com/questions/18065170/how-do-i-do-tls-with-bouncycastle.

The class DefaultTlsServer is abstract but has no abstract methods. I have tried to not override any method and failed. You need to provide an implementation of a *SignerCredentials method. To answer which one is right you may want to go to google and not enter any text in the search field but look at their site information. It told me (using Firefox) that it uses the cipher TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 and TLS 1.2. So I've fine tuned my application to use the same cipher. You may want to go to https://www.thesprawl.org/research/tls-and-ssl-cipher-suites/ for an explanation of the name.

Per default DefaultTlsServer uses TLS 1.1. To get it to 1.2 I had to override the method getMaximumVersion and provide an instance of SignatureAndHashAlgorithm to DefaultTlsSignerCredentials.

A note about the certificate building. You may wonder why the serial takes the date as argument. During development I've given it a fixed value. The first time it worked but the second time I've called the server and changed the key of the certificate I've got an error saying that reusing the serial forbidden. So as soon as relevant data of the certificate change you need to change the serial. Using the date is the simplest way to achieve this.
The signing of the site certificate is done by using the private key of the root certificate for the JcaContentSignerBuilder and putting the root/issuer certificate behind the site certificate int the array provided to Certificate constructor. See https://en.wikipedia.org/wiki/X.509#Certificate_chains_and_cross-certification for an explanation of this.

In order to test the TLS connection I have used this command:
openssl s_client -connect localhost:8080 -CAfile TlsTestServer/ca/rootCA.crt
...
Verify return code: 0 (ok)
---
The important part of the output is the "Verify return code". It has to be 0.

As a site note, when you are OK to use static certificates you may want to look at SSLEngine.