From b5679594826e7b609524bea1c4ac7806af62bd5a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 26 Apr 2016 14:27:39 -0700 Subject: [PATCH] Add support for dynamic SSL key stores Add a SslStoreProvider interface that can be used to load key and trust stores from non file locations. Fixes gh-5208 --- ...tConfigurableEmbeddedServletContainer.java | 11 +++ .../ConfigurableEmbeddedServletContainer.java | 6 ++ .../context/embedded/SslStoreProvider.java | 44 +++++++++ .../JettyEmbeddedServletContainerFactory.java | 15 ++- .../TomcatEmbeddedJSSEImplementation.java | 98 +++++++++++++++++++ ...TomcatEmbeddedServletContainerFactory.java | 22 ++++- ...dertowEmbeddedServletContainerFactory.java | 67 +++++++------ ...tEmbeddedServletContainerFactoryTests.java | 59 +++++++++++ 8 files changed, 290 insertions(+), 32 deletions(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/embedded/SslStoreProvider.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedJSSEImplementation.java diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java index 24c8826e3f..4c40cdc003 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java @@ -71,6 +71,8 @@ public abstract class AbstractConfigurableEmbeddedServletContainer private Ssl ssl; + private SslStoreProvider sslStoreProvider; + private JspServlet jspServlet = new JspServlet(); private Compression compression; @@ -287,6 +289,15 @@ public abstract class AbstractConfigurableEmbeddedServletContainer return this.ssl; } + @Override + public void setSslStoreProvider(SslStoreProvider sslStoreProvider) { + this.sslStoreProvider = sslStoreProvider; + } + + public SslStoreProvider getSslStoreProvider() { + return this.sslStoreProvider; + } + @Override public void setJspServlet(JspServlet jspServlet) { this.jspServlet = jspServlet; diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java index f3cfba8442..9429cb34c5 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java @@ -151,6 +151,12 @@ public interface ConfigurableEmbeddedServletContainer { */ void setSsl(Ssl ssl); + /** + * Sets a provider that will be used to obtain SSL stores. + * @param sslStoreProvider the SSL store provider + */ + void setSslStoreProvider(SslStoreProvider sslStoreProvider); + /** * Sets the configuration that will be applied to the container's JSP servlet. * @param jspServlet the JSP servlet configuration diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/SslStoreProvider.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/SslStoreProvider.java new file mode 100644 index 0000000000..adbb5ba0d3 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/SslStoreProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.embedded; + +import java.security.KeyStore; + +/** + * Interface to provide SSL key stores for an {@link EmbeddedServletContainer} to use. Can + * be used when file based key stores cannot be used. + * + * @author Phillip Webb + * @since 1.4.0 + */ +public interface SslStoreProvider { + + /** + * Return the key store that should be used. + * @return the key store to use + * @throws Exception on load error + */ + KeyStore getKeyStore() throws Exception; + + /** + * Return the trust store that should be used. + * @return the trust store to use + * @throws Exception on load error + */ + KeyStore getTrustStore() throws Exception; + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java index 3d05880437..3111359f98 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java @@ -250,14 +250,25 @@ public class JettyEmbeddedServletContainerFactory configureSslClientAuth(factory, ssl); configureSslPasswords(factory, ssl); factory.setCertAlias(ssl.getKeyAlias()); - configureSslKeyStore(factory, ssl); if (ssl.getCiphers() != null) { factory.setIncludeCipherSuites(ssl.getCiphers()); } if (ssl.getEnabledProtocols() != null) { factory.setIncludeProtocols(ssl.getEnabledProtocols()); } - configureSslTrustStore(factory, ssl); + if (getSslStoreProvider() != null) { + try { + factory.setKeyStore(getSslStoreProvider().getKeyStore()); + factory.setTrustStore(getSslStoreProvider().getKeyStore()); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to set SSL store", ex); + } + } + else { + configureSslKeyStore(factory, ssl); + configureSslTrustStore(factory, ssl); + } } private void configureSslClientAuth(SslContextFactory factory, Ssl ssl) { diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedJSSEImplementation.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedJSSEImplementation.java new file mode 100644 index 0000000000..6681da0c4e --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedJSSEImplementation.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.embedded.tomcat; + +import java.io.IOException; +import java.security.KeyStore; + +import org.apache.tomcat.util.net.AbstractEndpoint; +import org.apache.tomcat.util.net.SSLUtil; +import org.apache.tomcat.util.net.ServerSocketFactory; +import org.apache.tomcat.util.net.jsse.JSSEImplementation; +import org.apache.tomcat.util.net.jsse.JSSESocketFactory; + +import org.springframework.boot.context.embedded.SslStoreProvider; + +/** + * {@link JSSEImplementation} for embedded Tomcat that supports {@link SslStoreProvider}. + * + * @author Phillip Webb + * @author Venil Noronha + * @since 1.4.0 + */ +public class TomcatEmbeddedJSSEImplementation extends JSSEImplementation { + + @Override + public ServerSocketFactory getServerSocketFactory(AbstractEndpoint endpoint) { + return new SocketFactory(endpoint); + } + + @Override + public SSLUtil getSSLUtil(AbstractEndpoint endpoint) { + return new SocketFactory(endpoint); + } + + /** + * {@link JSSESocketFactory} that supports {@link SslStoreProvider}. + */ + static class SocketFactory extends JSSESocketFactory { + + private final SslStoreProvider sslStoreProvider; + + SocketFactory(AbstractEndpoint endpoint) { + super(endpoint); + this.sslStoreProvider = (SslStoreProvider) endpoint + .getAttribute("sslStoreProvider"); + } + + @Override + protected KeyStore getKeystore(String type, String provider, String pass) + throws IOException { + if (this.sslStoreProvider != null) { + try { + KeyStore store = this.sslStoreProvider.getKeyStore(); + if (store != null) { + return store; + } + } + catch (Exception ex) { + throw new IOException(ex); + } + } + return super.getKeystore(type, provider, pass); + } + + @Override + protected KeyStore getTrustStore(String keystoreType, String keystoreProvider) + throws IOException { + if (this.sslStoreProvider != null) { + try { + KeyStore store = this.sslStoreProvider.getTrustStore(); + if (store != null) { + return store; + } + } + catch (Exception ex) { + throw new IOException(ex); + } + } + return super.getTrustStore(keystoreType, keystoreProvider); + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java index fb97d2a8ce..44a9410043 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java @@ -51,6 +51,7 @@ import org.apache.coyote.AbstractProtocol; import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http11.AbstractHttp11JsseProtocol; import org.apache.coyote.http11.AbstractHttp11Protocol; +import org.apache.coyote.http11.Http11NioProtocol; import org.springframework.beans.BeanUtils; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; @@ -63,6 +64,7 @@ import org.springframework.boot.context.embedded.MimeMappings; import org.springframework.boot.context.embedded.ServletContextInitializer; import org.springframework.boot.context.embedded.Ssl; import org.springframework.boot.context.embedded.Ssl.ClientAuth; +import org.springframework.boot.context.embedded.SslStoreProvider; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; @@ -320,13 +322,18 @@ public class TomcatEmbeddedServletContainerFactory protocol.setKeystorePass(ssl.getKeyStorePassword()); protocol.setKeyPass(ssl.getKeyPassword()); protocol.setKeyAlias(ssl.getKeyAlias()); - configureSslKeyStore(protocol, ssl); protocol.setCiphers(StringUtils.arrayToCommaDelimitedString(ssl.getCiphers())); if (ssl.getEnabledProtocols() != null) { protocol.setProperty("sslEnabledProtocols", StringUtils.arrayToCommaDelimitedString(ssl.getEnabledProtocols())); } - configureSslTrustStore(protocol, ssl); + if (getSslStoreProvider() != null) { + configureSslStoreProvider(protocol, getSslStoreProvider()); + } + else { + configureSslKeyStore(protocol, ssl); + configureSslTrustStore(protocol, ssl); + } } private void configureSslClientAuth(AbstractHttp11JsseProtocol protocol, Ssl ssl) { @@ -338,6 +345,16 @@ public class TomcatEmbeddedServletContainerFactory } } + protected void configureSslStoreProvider(AbstractHttp11JsseProtocol protocol, + SslStoreProvider sslStoreProvider) { + Assert.isInstanceOf(Http11NioProtocol.class, protocol, + "SslStoreProvider can only be used with Http11NioProtocol"); + ((Http11NioProtocol) protocol).getEndpoint().setAttribute("sslStoreProvider", + sslStoreProvider); + protocol.setSslImplementationName( + TomcatEmbeddedJSSEImplementation.class.getName()); + } + private void configureSslKeyStore(AbstractHttp11JsseProtocol protocol, Ssl ssl) { try { protocol.setKeystoreFile(ResourceUtils.getURL(ssl.getKeyStore()).toString()); @@ -355,6 +372,7 @@ public class TomcatEmbeddedServletContainerFactory } private void configureSslTrustStore(AbstractHttp11JsseProtocol protocol, Ssl ssl) { + if (ssl.getTrustStore() != null) { try { protocol.setTruststoreFile( diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java index 2ab91ecc78..bf3376c2de 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java @@ -296,22 +296,15 @@ public class UndertowEmbeddedServletContainerFactory private KeyManager[] getKeyManagers() { try { - Ssl ssl = getSsl(); - String keyStoreType = ssl.getKeyStoreType(); - if (keyStoreType == null) { - keyStoreType = "JKS"; - } - KeyStore keyStore = KeyStore.getInstance(keyStoreType); - URL url = ResourceUtils.getURL(ssl.getKeyStore()); - keyStore.load(url.openStream(), ssl.getKeyStorePassword().toCharArray()); - - // Get key manager to provide client credentials. + KeyStore keyStore = getKeyStore(); KeyManagerFactory keyManagerFactory = KeyManagerFactory .getInstance(KeyManagerFactory.getDefaultAlgorithm()); - char[] keyPassword = ssl.getKeyPassword() != null - ? ssl.getKeyPassword().toCharArray() - : ssl.getKeyStorePassword().toCharArray(); - keyManagerFactory.init(keyStore, keyPassword); + Ssl ssl = getSsl(); + String keyPassword = ssl.getKeyPassword(); + if (keyPassword == null) { + keyPassword = ssl.getKeyStorePassword(); + } + keyManagerFactory.init(keyStore, keyPassword.toCharArray()); return keyManagerFactory.getKeyManagers(); } catch (Exception ex) { @@ -319,24 +312,21 @@ public class UndertowEmbeddedServletContainerFactory } } + private KeyStore getKeyStore() throws Exception { + if (getSslStoreProvider() != null) { + return getSslStoreProvider().getKeyStore(); + } + Ssl ssl = getSsl(); + return loadKeyStore(ssl.getKeyStoreType(), ssl.getKeyStore(), + ssl.getKeyStorePassword()); + } + private TrustManager[] getTrustManagers() { try { - Ssl ssl = getSsl(); - String trustStoreType = ssl.getTrustStoreType(); - if (trustStoreType == null) { - trustStoreType = "JKS"; - } - String trustStore = ssl.getTrustStore(); - if (trustStore == null) { - return null; - } - KeyStore trustedKeyStore = KeyStore.getInstance(trustStoreType); - URL url = ResourceUtils.getURL(trustStore); - trustedKeyStore.load(url.openStream(), - ssl.getTrustStorePassword().toCharArray()); + KeyStore store = getTrustStore(); TrustManagerFactory trustManagerFactory = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(trustedKeyStore); + trustManagerFactory.init(store); return trustManagerFactory.getTrustManagers(); } catch (Exception ex) { @@ -344,6 +334,27 @@ public class UndertowEmbeddedServletContainerFactory } } + private KeyStore getTrustStore() throws Exception { + if (getSslStoreProvider() != null) { + return getSslStoreProvider().getTrustStore(); + } + Ssl ssl = getSsl(); + return loadKeyStore(ssl.getTrustStoreType(), ssl.getTrustStore(), + ssl.getTrustStorePassword()); + } + + private KeyStore loadKeyStore(String type, String resource, String password) + throws Exception { + type = (type == null ? "JKS" : type); + if (resource == null) { + return null; + } + KeyStore store = KeyStore.getInstance(type); + URL url = ResourceUtils.getURL(resource); + store.load(url.openStream(), password.toCharArray()); + return store; + } + private DeploymentManager createDeploymentManager( ServletContextInitializer... initializers) { DeploymentInfo deployment = Servlets.deployment(); diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java index f37bb48b34..3c3a48b372 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java @@ -31,6 +31,9 @@ import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.Charset; import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -71,6 +74,8 @@ import org.mockito.InOrder; import org.springframework.boot.ApplicationHome; import org.springframework.boot.ApplicationTemp; import org.springframework.boot.context.embedded.Ssl.ClientAuth; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpRequest; @@ -519,6 +524,32 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { .isEqualTo("test"); } + @Test + public void sslWithCustomSslStoreProvider() throws Exception { + AbstractEmbeddedServletContainerFactory factory = getFactory(); + addTestTxtFile(factory); + Ssl ssl = new Ssl(); + ssl.setClientAuth(ClientAuth.NEED); + ssl.setKeyPassword("password"); + factory.setSsl(ssl); + factory.setSslStoreProvider(new CustomSslStoreProvider()); + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(new FileInputStream(new File("src/test/resources/test.jks")), + "secret".toCharArray()); + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder() + .loadTrustMaterial(null, new TrustSelfSignedStrategy()) + .loadKeyMaterial(keyStore, "password".toCharArray()).build()); + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)) + .isEqualTo("test"); + } + @Test public void disableJspServletRegistration() throws Exception { AbstractEmbeddedServletContainerFactory factory = getFactory(); @@ -1056,4 +1087,32 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { } + public static class CustomSslStoreProvider implements SslStoreProvider { + + @Override + public KeyStore getKeyStore() throws Exception { + return loadStore(); + } + + @Override + public KeyStore getTrustStore() throws Exception { + return loadStore(); + } + + private KeyStore loadStore() throws KeyStoreException, IOException, + NoSuchAlgorithmException, CertificateException { + KeyStore keyStore = KeyStore.getInstance("JKS"); + Resource resource = new ClassPathResource("test.jks"); + InputStream inputStream = resource.getInputStream(); + try { + keyStore.load(inputStream, "secret".toCharArray()); + return keyStore; + } + finally { + inputStream.close(); + } + } + + } + }