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.
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?)
On https://javalin.io/tutorials/, the source code is here: https://github.com/javalin/javalin.github.io
@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 HTTPScertificatePath 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:
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));
}
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?)