diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/Ssl.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/Ssl.java index acf2535433..9b363a6e61 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/Ssl.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/Ssl.java @@ -41,6 +41,11 @@ public class Ssl { */ private String[] ciphers; + /** + * Enabled SSL protocols. + */ + private String[] enabledProtocols; + /** * Alias that identifies the key in the key store. */ @@ -168,6 +173,14 @@ public class Ssl { this.keyStoreProvider = keyStoreProvider; } + public String[] getEnabledProtocols() { + return this.enabledProtocols; + } + + public void setEnabledProtocols(String[] enabledProtocols) { + this.enabledProtocols = enabledProtocols; + } + public String getTrustStore() { return this.trustStore; } 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 21eaef66f2..c5de3778d6 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 @@ -215,6 +215,9 @@ public class JettyEmbeddedServletContainerFactory if (ssl.getCiphers() != null) { factory.setIncludeCipherSuites(ssl.getCiphers()); } + if (ssl.getEnabledProtocols() != null) { + factory.setIncludeProtocols(ssl.getEnabledProtocols()); + } configureSslTrustStore(factory, ssl); } 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 0ee888b4dd..6292dffbb3 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 @@ -321,8 +321,11 @@ public class TomcatEmbeddedServletContainerFactory protocol.setKeyPass(ssl.getKeyPassword()); protocol.setKeyAlias(ssl.getKeyAlias()); configureSslKeyStore(protocol, ssl); - String ciphers = StringUtils.arrayToCommaDelimitedString(ssl.getCiphers()); - protocol.setCiphers(ciphers); + protocol.setCiphers(StringUtils.arrayToCommaDelimitedString(ssl.getCiphers())); + if (ssl.getEnabledProtocols() != null) { + protocol.setProperty("sslEnabledProtocols", + StringUtils.arrayToCommaDelimitedString(ssl.getEnabledProtocols())); + } configureSslTrustStore(protocol, ssl); } 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 e3a3e2ff4a..05b97dd36f 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 @@ -62,6 +62,7 @@ import io.undertow.servlet.handlers.DefaultServlet; import io.undertow.servlet.util.ImmediateInstanceFactory; import org.xnio.OptionMap; import org.xnio.Options; +import org.xnio.Sequence; import org.xnio.SslClientAuthMode; import org.xnio.Xnio; import org.xnio.XnioWorker; @@ -259,6 +260,14 @@ public class UndertowEmbeddedServletContainerFactory builder.addHttpsListener(port, getListenAddress(), sslContext); builder.setSocketOption(Options.SSL_CLIENT_AUTH_MODE, getSslClientAuthMode(ssl)); + if (ssl.getEnabledProtocols() != null) { + builder.setSocketOption(Options.SSL_ENABLED_PROTOCOLS, + Sequence.of(ssl.getEnabledProtocols())); + } + if (ssl.getCiphers() != null) { + builder.setSocketOption(Options.SSL_ENABLED_CIPHER_SUITES, + Sequence.of(ssl.getCiphers())); + } } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException(ex); 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 892ce92e2d..88ed56ebcd 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 @@ -404,7 +404,7 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { AbstractEmbeddedServletContainerFactory factory = getFactory(); addTestTxtFile(factory); factory.setSsl(getSsl(ClientAuth.NEED, null, "classpath:test.p12", - "classpath:test.p12")); + "classpath:test.p12", null, null)); this.container = factory.getEmbeddedServletContainer(); this.container.start(); KeyStore keyStore = KeyStore.getInstance("pkcs12"); @@ -428,7 +428,7 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { AbstractEmbeddedServletContainerFactory factory = getFactory(); addTestTxtFile(factory); factory.setSsl(getSsl(ClientAuth.NEED, "password", "classpath:test.jks", - "classpath:test.jks")); + "classpath:test.jks", null, null)); this.container = factory.getEmbeddedServletContainer(); this.container.start(); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); @@ -526,11 +526,11 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { } private Ssl getSsl(ClientAuth clientAuth, String keyPassword, String keyStore) { - return getSsl(clientAuth, keyPassword, keyStore, null); + return getSsl(clientAuth, keyPassword, keyStore, null, null, null); } private Ssl getSsl(ClientAuth clientAuth, String keyPassword, String keyStore, - String trustStore) { + String trustStore, String[] supportedProtocols, String[] ciphers) { Ssl ssl = new Ssl(); ssl.setClientAuth(clientAuth); if (keyPassword != null) { @@ -546,9 +546,37 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { ssl.setTrustStorePassword("secret"); ssl.setTrustStoreType(getStoreType(trustStore)); } + if (ciphers != null) { + ssl.setCiphers(ciphers); + } + if (supportedProtocols != null) { + ssl.setEnabledProtocols(supportedProtocols); + } return ssl; } + protected void testRestrictedSSLProtocolsAndCipherSuites(String[] protocols, + String[] ciphers) throws Exception { + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setSsl(getSsl(null, "password", "src/test/resources/test.jks", null, + protocols, ciphers)); + this.container = factory.getEmbeddedServletContainer( + new ServletRegistrationBean(new ExampleServlet(true, false), "/hello")); + this.container.start(); + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder() + .loadTrustMaterial(null, new TrustSelfSignedStrategy()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + + assertThat(getResponse(getLocalUrl("https", "/hello"), requestFactory)) + .contains("scheme=https"); + } + private String getStoreType(String keyStore) { return (keyStore.endsWith(".p12") ? "pkcs12" : null); } diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java index 72b8e7ded2..94799c0fb5 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java @@ -152,6 +152,56 @@ public class JettyEmbeddedServletContainerFactoryTests }); } + @Test + public void sslEnabledMultiProtocolsConfiguration() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyPassword("password"); + ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" }); + ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" }); + + JettyEmbeddedServletContainerFactory factory = getFactory(); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + JettyEmbeddedServletContainer jettyContainer = (JettyEmbeddedServletContainer) this.container; + ServerConnector connector = (ServerConnector) jettyContainer.getServer() + .getConnectors()[0]; + SslConnectionFactory connectionFactory = connector + .getConnectionFactory(SslConnectionFactory.class); + + assertThat(connectionFactory.getSslContextFactory().getIncludeProtocols()) + .isEqualTo(new String[] { "TLSv1.1", "TLSv1.2" }); + } + + @Test + public void sslEnabledProtocolsConfiguration() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyPassword("password"); + ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" }); + ssl.setEnabledProtocols(new String[] { "TLSv1.1" }); + + JettyEmbeddedServletContainerFactory factory = getFactory(); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + JettyEmbeddedServletContainer jettyContainer = (JettyEmbeddedServletContainer) this.container; + ServerConnector connector = (ServerConnector) jettyContainer.getServer() + .getConnectors()[0]; + SslConnectionFactory connectionFactory = connector + .getConnectionFactory(SslConnectionFactory.class); + + assertThat(connectionFactory.getSslContextFactory().getIncludeProtocols()) + .isEqualTo(new String[] { "TLSv1.1" }); + } + private void assertTimeout(JettyEmbeddedServletContainerFactory factory, int expected) { this.container = factory.getEmbeddedServletContainer(); diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java index 18d2f0c04f..857e5385b3 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.context.embedded.tomcat; import java.io.File; +import java.io.IOException; import java.nio.charset.Charset; import java.util.Arrays; import java.util.HashMap; @@ -41,11 +42,13 @@ import org.mockito.InOrder; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactoryTests; +import org.springframework.boot.context.embedded.EmbeddedServletContainerException; import org.springframework.boot.context.embedded.Ssl; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.SocketUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyObject; @@ -260,6 +263,78 @@ public class TomcatEmbeddedServletContainerFactoryTests assertThat(jsseProtocol.getCiphers()).isEqualTo("ALPHA,BRAVO,CHARLIE"); } + @Test + public void sslEnabledMultipleProtocolsConfiguration() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStore("test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" }); + ssl.setCiphers(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "BRAVO" }); + + TomcatEmbeddedServletContainerFactory factory = getFactory(); + factory.setSsl(ssl); + + this.container = factory + .getEmbeddedServletContainer(sessionServletRegistration()); + Tomcat tomcat = ((TomcatEmbeddedServletContainer) this.container).getTomcat(); + Connector connector = tomcat.getConnector(); + + AbstractHttp11JsseProtocol jsseProtocol = (AbstractHttp11JsseProtocol) connector + .getProtocolHandler(); + assertThat(jsseProtocol.getSslProtocol()).isEqualTo("TLS"); + assertThat(jsseProtocol.getProperty("sslEnabledProtocols")) + .isEqualTo("TLSv1.1,TLSv1.2"); + } + + @Test + public void sslEnabledProtocolsConfiguration() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStore("test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setEnabledProtocols(new String[] { "TLSv1.2" }); + ssl.setCiphers(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "BRAVO" }); + + TomcatEmbeddedServletContainerFactory factory = getFactory(); + factory.setSsl(ssl); + + this.container = factory + .getEmbeddedServletContainer(sessionServletRegistration()); + Tomcat tomcat = ((TomcatEmbeddedServletContainer) this.container).getTomcat(); + Connector connector = tomcat.getConnector(); + + AbstractHttp11JsseProtocol jsseProtocol = (AbstractHttp11JsseProtocol) connector + .getProtocolHandler(); + assertThat(jsseProtocol.getSslProtocol()).isEqualTo("TLS"); + assertThat(jsseProtocol.getProperty("sslEnabledProtocols")).isEqualTo("TLSv1.2"); + } + + @Test + public void primaryConnectorPortClashThrowsIllegalStateException() + throws InterruptedException, IOException { + final int port = SocketUtils.findAvailableTcpPort(40000); + + doWithBlockedPort(port, new Runnable() { + + @Override + public void run() { + TomcatEmbeddedServletContainerFactory factory = getFactory(); + factory.setPort(port); + + try { + TomcatEmbeddedServletContainerFactoryTests.this.container = factory + .getEmbeddedServletContainer(); + TomcatEmbeddedServletContainerFactoryTests.this.container.start(); + fail(); + } + catch (EmbeddedServletContainerException ex) { + // Ignore + } + } + + }); + + } + @Override protected void addConnector(int port, AbstractEmbeddedServletContainerFactory factory) { diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactoryTests.java index a42d7e3123..a1f93ec099 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactoryTests.java @@ -26,6 +26,8 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import javax.net.ssl.SSLHandshakeException; + import io.undertow.Undertow.Builder; import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.DeploymentManager; @@ -201,6 +203,36 @@ public class UndertowEmbeddedServletContainerFactoryTests }); } + @Test(expected = SSLHandshakeException.class) + public void sslRestrictedProtocolsEmptyCipherFailure() throws Exception { + testRestrictedSSLProtocolsAndCipherSuites(new String[] { "TLSv1.2" }, + new String[] { "TLS_EMPTY_RENEGOTIATION_INFO_SCSV" }); + } + + @Test(expected = SSLHandshakeException.class) + public void sslRestrictedProtocolsECDHETLS1Failure() throws Exception { + testRestrictedSSLProtocolsAndCipherSuites(new String[] { "TLSv1" }, + new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256" }); + } + + @Test + public void sslRestrictedProtocolsECDHESuccess() throws Exception { + testRestrictedSSLProtocolsAndCipherSuites(new String[] { "TLSv1.2" }, + new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256" }); + } + + @Test + public void sslRestrictedProtocolsRSATLS12Success() throws Exception { + testRestrictedSSLProtocolsAndCipherSuites(new String[] { "TLSv1.2" }, + new String[] { "TLS_RSA_WITH_AES_128_CBC_SHA256" }); + } + + @Test(expected = SSLHandshakeException.class) + public void sslRestrictedProtocolsRSATLS11Failure() throws Exception { + testRestrictedSSLProtocolsAndCipherSuites(new String[] { "TLSv1.1" }, + new String[] { "TLS_RSA_WITH_AES_128_CBC_SHA256" }); + } + @Override protected Object getJspServlet() { return null; // Undertow does not support JSPs