Javalin: Creating KeyStore programmatically, from LetsEncrypt certificate

Created on 13 Feb 2019  路  11Comments  路  Source: tipsy/javalin

I am trying to use Javalin (I love the project and the API!!) to make secure WebSockets + HTTPS REST API run at the same port.

Previously I have been using TooTallNate/Java-WebSocket for using WebSockets, they also have an example of how to create the KeyStore programmatically which if you follow the links ends up at a Stack Overflow answer. The issue with TooTallNate/Java-WebSocket is that it is only a WebSocket server and therefore I would need to use two different ports for WebSockets + HTTPS REST API.

So I tried to combine the TooTallNate/Java-WebSocket example above with the Javalin example and ended up with the following code: (ServerConfig is just my configuration object containing configuration for which ports to use and where to find the certificate files)

public static Javalin javalin(ServerConfig serverConfig) {
    return Javalin.create().server(() -> createServer(serverConfig));
}

private static Server createServer(ServerConfig serverConfig) {
    Server server = new Server();
    ServerConnector connector = new ServerConnector(server);
    connector.setPort(serverConfig.webSocketPort);
    if (serverConfig.useSecureWebsockets()) {
        ServerConnector sslConnector = new ServerConnector(server, createSslContextFactory(serverConfig));
        sslConnector.setPort(serverConfig.webSocketPortSSL);
        server.setConnectors(new Connector[]{sslConnector, connector});
    } else {
        server.setConnectors(new Connector[]{connector});
    }
    return server;
}

private static SslContextFactory createSslContextFactory(ServerConfig serverConfig) {
    String pathTo = serverConfig.certificatePath;
    String keyPassword = serverConfig.certificatePassword;
    try {
        byte[] certBytes = parseDERFromPEM(Files.readAllBytes(new File(pathTo + File.separator + "cert.pem").toPath()), "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
        byte[] keyBytes = parseDERFromPEM(Files.readAllBytes(new File(pathTo + File.separator + "privkey.pem").toPath()), "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----");

        X509Certificate cert = generateCertificateFromDER(certBytes);
        RSAPrivateKey key = generatePrivateKeyFromDER(keyBytes);

        KeyStore keystore = KeyStore.getInstance("JKS");
        keystore.load(null);
        keystore.setCertificateEntry("cert-alias", cert);
        keystore.setKeyEntry("key-alias", key, keyPassword.toCharArray(), new X509Certificate[]{cert});

        SslContextFactory sslContextFactory = new SslContextFactory();
        sslContextFactory.setKeyStore(keystore);
        sslContextFactory.setKeyStorePassword(keyPassword);

        return sslContextFactory;
    } catch (IOException | KeyStoreException | InvalidKeySpecException | NoSuchAlgorithmException | CertificateException e) {
        throw new IllegalArgumentException(e);
    }
}

private static byte[] parseDERFromPEM(byte[] pem, String beginDelimiter, String endDelimiter) {
    String data = new String(pem);
    String[] tokens = data.split(beginDelimiter);
    tokens = tokens[1].split(endDelimiter);
    return DatatypeConverter.parseBase64Binary(tokens[0]);
}

private static RSAPrivateKey generatePrivateKeyFromDER(byte[] keyBytes) throws InvalidKeySpecException, NoSuchAlgorithmException {
    PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
    KeyFactory factory = KeyFactory.getInstance("RSA");
    return (RSAPrivateKey) factory.generatePrivate(spec);
}

private static X509Certificate generateCertificateFromDER(byte[] certBytes) throws CertificateException {
    CertificateFactory factory = CertificateFactory.getInstance("X.509");

    return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(certBytes));
}

This however ends up with me getting "ERR_SSL_VERSION_OR_CIPHER_MISMATCH" whenever I try to connect with HTTPS to a REST endpoint or when connecting with WebSockets.

I understand that it might be me that is the problem here and not Javalin, but I am hoping for any assistance.

INFO

Most helpful comment

I found the solution myself!

Change:

KeyStore keystore = KeyStore.getInstance("JKS");

to:

KeyStore keystore = KeyStore.getInstance("PKCS12");

Done!

Now for another request: Document this. I bet more people than I would be interested in not having to do the KeyStore generation manually. (Or is what I'm doing here not recommended?)

All 11 comments

I found the solution myself!

Change:

KeyStore keystore = KeyStore.getInstance("JKS");

to:

KeyStore keystore = KeyStore.getInstance("PKCS12");

Done!

Now for another request: Document this. I bet more people than I would be interested in not having to do the KeyStore generation manually. (Or is what I'm doing here not recommended?)

@Zomis Nice! This seems like a bit too elaborate for the standard docs, would you like to write a tutorial?

@tipsy Where would this tutorial be located in that case? In the Javalin wiki? Or on my own non-existing coding blog? Or as a self-answered question on Stack Overflow? (Or other options?)

@tipsy Might take a while but yes, I could do that!

That would be great!

Hi @Zomis, did you decide if you're doing this?

@tipsy That is the plan, yes. I just haven't quite figured out how it works when certificate expires. At the moment I need to restart the server for that, but I'm planning on investigating a way to automatically get the underlying Jetty server to reload the certificate information.

Thanks, was just going through the issues. Looking forward to it!

I'm going to close the issue, but I'm still very interested in having a tutorial for this. Let me know!

@tipsy Not sure what format you want the tutorial in, but here's what I can contribute with:

Use the code below.

You will need to create a ServerConfig class and add the appropriate properties and methods to it, or replace them with values read from somewhere else (DO NOT HARDCODE, that's a bad practice!). The values will depend on your own requirements. A summary about the required values:

  • javalinHttps is a boolean for whether or not to use HTTPS
  • certificatePath is the directory of your Let's encrypt certificates, such as /etc/letsencrypt/live/www.example.com (no trailing slash)
  • certificatePassword is a password you come up with yourself, it's only used programmatically. I suggest generating one.
  • javalinHttpPort and javalinHttpsPort are the port numbers for Http and Https, respectively.

Some gotchas:

  • Make sure that your application have permissions to read the file, and the directories that it reside in. Also remember that it might be a symlink and you need to have access also to the full path of where the symlink is pointing.
  • When running in a Docker container, you will need to pass the certificates also to the Docker container - as a volume for example.
  • This example does not cover reloading of the certificate, which is required to do every ~90 days in the period between Let's Encrypt renewing it and the old one expires.
public static Javalin javalin(ServerConfig serverConfig) {
    return Javalin.create().server(() -> createServer(serverConfig));
}

private static Server createServer(ServerConfig serverConfig) {
    Server server = new Server();
    ServerConnector connector = new ServerConnector(server);
    connector.setPort(serverConfig.javalinHttpPort);
    if (serverConfig.javalinHttps) {
        ServerConnector sslConnector = new ServerConnector(server, createSslContextFactory(serverConfig));
        sslConnector.setPort(serverConfig.javalinHttpsPort);
        server.setConnectors(new Connector[]{sslConnector, connector});
    } else {
        server.setConnectors(new Connector[]{connector});
    }
    return server;
}

private static SslContextFactory createSslContextFactory(ServerConfig serverConfig) {
    String pathTo = serverConfig.certificatePath;
    String keyPassword = serverConfig.certificatePassword;
    try {
        byte[] certBytes = parseDERFromPEM(Files.readAllBytes(new File(pathTo + File.separator + "cert.pem").toPath()), "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
        byte[] keyBytes = parseDERFromPEM(Files.readAllBytes(new File(pathTo + File.separator + "privkey.pem").toPath()), "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----");

        X509Certificate cert = generateCertificateFromDER(certBytes);
        RSAPrivateKey key = generatePrivateKeyFromDER(keyBytes);

        KeyStore keystore = KeyStore.getInstance("PKCS12");
        keystore.load(null);
        keystore.setCertificateEntry("cert-alias", cert);
        keystore.setKeyEntry("key-alias", key, keyPassword.toCharArray(), new X509Certificate[]{cert});

        SslContextFactory sslContextFactory = new SslContextFactory();
        sslContextFactory.setKeyStore(keystore);
        sslContextFactory.setKeyStorePassword(keyPassword);

        return sslContextFactory;
    } catch (IOException | KeyStoreException | InvalidKeySpecException | NoSuchAlgorithmException | CertificateException e) {
        throw new IllegalArgumentException(e);
    }
}

private static byte[] parseDERFromPEM(byte[] pem, String beginDelimiter, String endDelimiter) {
    String data = new String(pem);
    String[] tokens = data.split(beginDelimiter);
    tokens = tokens[1].split(endDelimiter);
    return DatatypeConverter.parseBase64Binary(tokens[0]);
}

private static RSAPrivateKey generatePrivateKeyFromDER(byte[] keyBytes) throws InvalidKeySpecException, NoSuchAlgorithmException {
    PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
    KeyFactory factory = KeyFactory.getInstance("RSA");
    return (RSAPrivateKey) factory.generatePrivate(spec);
}

private static X509Certificate generateCertificateFromDER(byte[] certBytes) throws CertificateException {
    CertificateFactory factory = CertificateFactory.getInstance("X.509");

    return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(certBytes));
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

accron-1 picture accron-1  路  4Comments

gane5h picture gane5h  路  3Comments

jonerer picture jonerer  路  4Comments

ShikaSD picture ShikaSD  路  5Comments

MFernstrom picture MFernstrom  路  3Comments