From b37e9dff587c5f0bedca8aa81d6f650eedfd552b Mon Sep 17 00:00:00 2001 From: Alex Szebenyi Date: Thu, 18 Dec 2014 08:38:09 -0500 Subject: [PATCH] Added SSL support to RexsterHttpServer. --- .../rexster/util/RexsterSslHelper.java | 251 ++++++++++++++++++ rexster-server/config/rexster.xml | 18 ++ .../rexster/server/HttpRexsterServer.java | 29 ++ 3 files changed, 298 insertions(+) create mode 100644 rexster-core/src/main/java/com/tinkerpop/rexster/util/RexsterSslHelper.java diff --git a/rexster-core/src/main/java/com/tinkerpop/rexster/util/RexsterSslHelper.java b/rexster-core/src/main/java/com/tinkerpop/rexster/util/RexsterSslHelper.java new file mode 100644 index 00000000..c23a2fdd --- /dev/null +++ b/rexster-core/src/main/java/com/tinkerpop/rexster/util/RexsterSslHelper.java @@ -0,0 +1,251 @@ +package com.tinkerpop.rexster.util; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.apache.commons.configuration.Configuration; +import org.apache.log4j.Logger; + +/** + * Assists in various parts of SSL setup for Rexster, particularly creating the SSLContext from a configuration. The + * default configuration is for clients relying on the default JVM TrustStore, it is expected that servers will specify + * at least some SSL properties. + */ +public class RexsterSslHelper { + public static final String KEY_HTTP_SSL_ENABLED = "http.enable-ssl"; + public static final String KEY_SSL_PROTOCOL = "ssl.protocol"; + public static final String KEY_SSL_TRUST_STORE = "ssl.trust-store"; + public static final String KEY_SSL_TRUST_STORE_PASSWORD = "ssl.trust-store-password"; + public static final String KEY_SSL_TRUST_STORE_PROVIDER = "ssl.trust-store-provider"; + public static final String KEY_SSL_TRUST_MANAGER_FACTORY_ALGORITHM = "ssl.trust-manager-factory.algorithm"; + public static final String KEY_SSL_KEY_STORE = "ssl.key-store"; + public static final String KEY_SSL_KEY_STORE_PASSWORD = "ssl.key-store-password"; + public static final String KEY_SSL_KEY_STORE_PROVIDER = "ssl.key-store-provider"; + public static final String KEY_SSL_KEY_MANAGER_FACTORY_ALGORITHM = "ssl.key-manager-factory.algorithm"; + public static final String KEY_SSL_NEED_CLIENT_AUTH = "ssl.need-client-auth"; + public static final String KEY_SSL_WANT_CLIENT_AUTH = "ssl.want-client-auth"; + private static final Logger logger = Logger.getLogger(RexsterSslHelper.class); + private static final String DEFAULT_STORE_PROVIDER = "JKS"; + private static final String DEFAULT_SSL_PROTOCOL = "TLS"; + + /** + * An empty String. Not specifying a keystore results in no keystore being used. Not having a keystore is + * appropriate for clients when client-auth is disabled, in which case only a truststore is needed. + */ + private static final String DEFAULT_KEY_STORE_PATH = ""; + + /** + * No default keystore password. + */ + private static final char[] DEFAULT_KEY_STORE_PASSWORD = null; + + /** + * Use the default JVM truststore if omit. + */ + private static final String DEFAULT_TRUST_STORE_PATH = System.getenv("JAVA_HOME") + "/jre/lib/security/cacerts"; + + /** + * Standard JVM default truststore password. + */ + private static final String DEFAULT_TRUST_STORE_PASSWORD = "changeit"; + + /** + * Configuration this class utilizes for it's SSL functions. + */ + private final Configuration configuration; + + /** + * Constructor. + * + * @param configuration Configuration which includes SSL properties. + */ + public RexsterSslHelper(Configuration configuration) { + this.configuration = configuration; + } + + /** + * Logs a message and throws an SSLException with the same message. + * + * @param msg The message to log and pass into the exception. + * @param e The exception to throw with the message. + * @throws SSLException Rethrown exception indicating the cause exception was SSL-related. + */ + private static void logAndRethrow(String msg, Exception e) throws SSLException { + logger.error(msg, e); + throw new SSLException(msg, e); + } + + /** + * Creates an {@link javax.net.ssl.SSLContext} object using {@code configuration}. + * + * @return An SSLContext object that can be used for securing Rexster servers with SSL. + * @throws SSLException If a variety of SSL related exceptions occur. + */ + public SSLContext createRexsterSslContext() throws SSLException { + String rexsterHome = System.getenv("REXSTER_HOME"); + rexsterHome = rexsterHome == null ? "" : rexsterHome; + + logger.info("Creating SSLContext."); + final char[] secretServerPassword = getKeyStorePassword(); + + TrustManagerFactory tmf = null; + KeyManagerFactory kmf = null; + + try { + tmf = initTrustManagerFactory(rexsterHome); + // Keystore only used if it has been specified. + if (!getKeyStore().isEmpty()) { + kmf = initKeyManagerFactory(rexsterHome, secretServerPassword); + } + } catch (final IOException e) { + logAndRethrow("Problem loading KeyStore files!", e); + } catch (final CertificateException e) { + logAndRethrow("Problem with certificate while loading KeyStore!", e); + } catch (final NoSuchAlgorithmException e) { + logAndRethrow("Invalid KeyStore algorithm!", e); + } catch (final UnrecoverableKeyException e) { + logAndRethrow("Problem initializing KeyManagerFactory!", e); + } catch (final KeyStoreException e) { + logAndRethrow("Unable to load Keystore!", e); + } + + return initSslContext(tmf, kmf); + } + + public final String getSslProtocol() { + return configuration.getString(KEY_SSL_PROTOCOL, DEFAULT_SSL_PROTOCOL); + } + + public final String getTrustStore() { + return configuration.getString(KEY_SSL_TRUST_STORE, DEFAULT_TRUST_STORE_PATH); + } + + public final char[] getTrustStorePassword() { + return configuration.getString(KEY_SSL_TRUST_STORE_PASSWORD, DEFAULT_TRUST_STORE_PASSWORD).toCharArray(); + } + + public final String getTrustStoreProvider() { + return configuration.getString(KEY_SSL_TRUST_STORE_PROVIDER, DEFAULT_STORE_PROVIDER); + } + + public final String getKeyStore() { + return configuration.getString(KEY_SSL_KEY_STORE, DEFAULT_KEY_STORE_PATH); + } + + public final String getKeyStoreProvider() { + return configuration.getString(KEY_SSL_KEY_STORE_PROVIDER, DEFAULT_STORE_PROVIDER); + } + + public final boolean getNeedClientAuth() { + return configuration.getBoolean(KEY_SSL_NEED_CLIENT_AUTH, false); + } + + public final boolean getWantClientAuth() { + return configuration.getBoolean(KEY_SSL_WANT_CLIENT_AUTH, false); + } + + private char[] getKeyStorePassword() { + final String keyStorePassword = configuration.getString(KEY_SSL_KEY_STORE_PASSWORD, null); + if (keyStorePassword == null) { + return DEFAULT_KEY_STORE_PASSWORD; + } + return keyStorePassword.toCharArray(); + } + + /** + * Initializes the SSL Context based on {@code configuration}; + * + * @param trustManagerFactory Provides trust managers to use with the produced SSLContext, if any. + * @param keyManagerFactory Provides key managers to use with the produced SSLContext, if any. + * @return an SSLContext based on this class' configuration. + * @throws SSLException If a variety of SSL related errors occur. + */ + private SSLContext initSslContext(TrustManagerFactory trustManagerFactory, KeyManagerFactory keyManagerFactory) + throws SSLException { + SSLContext sslContext = null; + final String sslProtocol = getSslProtocol(); + + KeyManager[] keyManagers = null; + + if (keyManagerFactory != null) { + keyManagers = keyManagerFactory.getKeyManagers(); + } + final TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + try { + sslContext = SSLContext.getInstance(sslProtocol); + sslContext.init(keyManagers, trustManagers, null); + } catch (final NoSuchAlgorithmException e) { + logAndRethrow(String.format("Invalid SSL Protocol '%s'", sslProtocol), e); + } catch (final KeyManagementException e) { + logAndRethrow("Unable to initialize SSLContext.", e); + } + return sslContext; + } + + private TrustManagerFactory initTrustManagerFactory(String rexsterHome) + throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException { + final String tmfAlgorithm = this.configuration.getString( + KEY_SSL_TRUST_MANAGER_FACTORY_ALGORITHM, TrustManagerFactory.getDefaultAlgorithm()); + final TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); + + final KeyStore trustStore = KeyStore.getInstance(getTrustStoreProvider()); + InputStream trustStoreInputStream = null; + + final String trustStorePath = getTrustStore(); + + // If truststore is intentionally blank, do not use a truststore. + if (!trustStorePath.isEmpty()) { + try { + trustStoreInputStream = new FileInputStream(rexsterHome + trustStorePath); + trustStore.load(trustStoreInputStream, getTrustStorePassword()); + } finally { + if (trustStoreInputStream != null) { + trustStoreInputStream.close(); + } + } + } + tmf.init(trustStore); + + return tmf; + } + + private KeyManagerFactory initKeyManagerFactory( + String rexsterHome, char[] secretServerPassword) + throws NoSuchAlgorithmException, IOException, CertificateException, UnrecoverableKeyException, + KeyStoreException { + final String kmfAlgorithm = this.configuration.getString( + KEY_SSL_KEY_MANAGER_FACTORY_ALGORITHM, KeyManagerFactory.getDefaultAlgorithm()); + + final KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgorithm); + + final KeyStore keyStore = KeyStore.getInstance(getKeyStoreProvider()); + final String keyStorePath = getKeyStore(); + InputStream keyStoreInputStream = null; + + try { + keyStoreInputStream = new FileInputStream(rexsterHome + keyStorePath); + keyStore.load(keyStoreInputStream, secretServerPassword); + kmf.init(keyStore, secretServerPassword); + } finally { + if (keyStoreInputStream != null) { + keyStoreInputStream.close(); + } + } + + return kmf; + } +} diff --git a/rexster-server/config/rexster.xml b/rexster-server/config/rexster.xml index 7d8530ca..4f19d099 100644 --- a/rexster-server/config/rexster.xml +++ b/rexster-server/config/rexster.xml @@ -7,6 +7,7 @@ public UTF-8 false + false true 2097152 8192 @@ -67,6 +68,23 @@ + + TLS + JKS + JKS + + config/ssl/serverKeyStore.jks + + + + SunX509 + + + SunX509 + + false + false + jmx diff --git a/rexster-server/src/main/java/com/tinkerpop/rexster/server/HttpRexsterServer.java b/rexster-server/src/main/java/com/tinkerpop/rexster/server/HttpRexsterServer.java index d3c5b700..dd74dd7a 100644 --- a/rexster-server/src/main/java/com/tinkerpop/rexster/server/HttpRexsterServer.java +++ b/rexster-server/src/main/java/com/tinkerpop/rexster/server/HttpRexsterServer.java @@ -25,6 +25,8 @@ import com.tinkerpop.rexster.servlet.DogHouseServlet; import com.tinkerpop.rexster.servlet.EvaluatorServlet; import com.tinkerpop.rexster.servlet.RexsterStaticHttpHandler; +import com.tinkerpop.rexster.util.RexsterSslHelper; + import org.apache.commons.configuration.HierarchicalConfiguration; import org.apache.commons.configuration.XMLConfiguration; import org.apache.log4j.Level; @@ -37,13 +39,21 @@ import org.glassfish.grizzly.http.server.ServerConfiguration; import org.glassfish.grizzly.servlet.ServletRegistration; import org.glassfish.grizzly.servlet.WebappContext; +import org.glassfish.grizzly.ssl.SSLEngineConfigurator; import org.glassfish.grizzly.threadpool.GrizzlyExecutorService; import org.glassfish.grizzly.threadpool.ThreadPoolConfig; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; +import javax.net.ssl.SSLException; import javax.ws.rs.core.Context; import java.io.File; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; /** * Initializes the HTTP server for Rexster serving REST and Dog House. @@ -482,6 +492,11 @@ private void configureNetworkListener() throws Exception { NetworkListener listener = this.httpServer.getListener("grizzly"); if (listener == null) { listener = new NetworkListener("grizzly", rexsterServerHost, rexsterServerPort); + + if (properties.getConfiguration().getBoolean(RexsterSslHelper.KEY_HTTP_SSL_ENABLED, false)) { + secureWithSsl(listener); + } + this.httpServer.addListener(listener); allowPortChange = false; } @@ -528,4 +543,18 @@ private void configureNetworkListener() throws Exception { logger.info(String.format("Using %s IOStrategy for HTTP/REST.", strategy.getClass().getName())); } } + + private void secureWithSsl(NetworkListener listener) throws SSLException { + logger.info("Attempting to secure HttpRexsterServer with SSL..."); + RexsterSslHelper rexsterSslHelper = new RexsterSslHelper(properties.getConfiguration()); + final SSLEngineConfigurator configurator = + new SSLEngineConfigurator(rexsterSslHelper.createRexsterSslContext()); + + configurator.setNeedClientAuth(rexsterSslHelper.getNeedClientAuth()).setWantClientAuth( + rexsterSslHelper.getWantClientAuth()).setClientMode(false); + + listener.setSecure(true); + listener.setSSLEngineConfig(configurator); + logger.info("HttpRexsterServer successfully secured with SSL!"); + } }