From 103c2bdd7d581b669032c02c902aeed525d869e6 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Mon, 4 Apr 2022 14:37:16 -0500 Subject: [PATCH] Use Tomcat SSLHostConfig API for SSL configuration Closes gh-30531 --- .../tomcat/SslConnectorCustomizer.java | 80 +++++++------ ...lStoreProviderUrlStreamHandlerFactory.java | 111 ------------------ .../tomcat/SslConnectorCustomizerTests.java | 59 +++------- 3 files changed, 63 insertions(+), 187 deletions(-) delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslStoreProviderUrlStreamHandlerFactory.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java index 959844cb8f..3377f7213f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2022 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. @@ -19,11 +19,12 @@ package org.springframework.boot.web.embedded.tomcat; import java.io.FileNotFoundException; import org.apache.catalina.connector.Connector; -import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http11.AbstractHttp11JsseProtocol; import org.apache.coyote.http11.Http11NioProtocol; import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.SslStoreProvider; @@ -36,6 +37,8 @@ import org.springframework.util.StringUtils; * {@link TomcatConnectorCustomizer} that configures SSL support on the given connector. * * @author Brian Clozel + * @author Andy Wilkinson + * @author Scott Frederick */ class SslConnectorCustomizer implements TomcatConnectorCustomizer { @@ -67,56 +70,63 @@ class SslConnectorCustomizer implements TomcatConnectorCustomizer { */ protected void configureSsl(AbstractHttp11JsseProtocol protocol, Ssl ssl, SslStoreProvider sslStoreProvider) { protocol.setSSLEnabled(true); - protocol.setSslProtocol(ssl.getProtocol()); - configureSslClientAuth(protocol, ssl); + SSLHostConfig sslHostConfig = new SSLHostConfig(); + sslHostConfig.setHostName(protocol.getDefaultSSLHostConfigName()); + sslHostConfig.setSslProtocol(ssl.getProtocol()); + protocol.addSslHostConfig(sslHostConfig); + configureSslClientAuth(sslHostConfig, ssl); + SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED); if (ssl.getKeyStorePassword() != null) { - protocol.setKeystorePass(ssl.getKeyStorePassword()); + certificate.setCertificateKeystorePassword(ssl.getKeyStorePassword()); } if (ssl.getKeyPassword() != null) { - protocol.setKeyPass(ssl.getKeyPassword()); + certificate.setCertificateKeyPassword(ssl.getKeyPassword()); } - protocol.setKeyAlias(ssl.getKeyAlias()); + if (ssl.getKeyAlias() != null) { + certificate.setCertificateKeyAlias(ssl.getKeyAlias()); + } + sslHostConfig.addCertificate(certificate); String ciphers = StringUtils.arrayToCommaDelimitedString(ssl.getCiphers()); if (StringUtils.hasText(ciphers)) { - protocol.setCiphers(ciphers); + sslHostConfig.setCiphers(ciphers); } + configureEnabledProtocols(protocol, ssl); + if (sslStoreProvider != null) { + configureSslStoreProvider(protocol, sslHostConfig, certificate, sslStoreProvider); + } + else { + configureSslKeyStore(certificate, ssl); + configureSslTrustStore(sslHostConfig, ssl); + } + } + + private void configureEnabledProtocols(AbstractHttp11JsseProtocol protocol, Ssl ssl) { if (ssl.getEnabledProtocols() != null) { for (SSLHostConfig sslHostConfig : protocol.findSslHostConfigs()) { sslHostConfig.setProtocols(StringUtils.arrayToCommaDelimitedString(ssl.getEnabledProtocols())); } } - if (sslStoreProvider != null) { - configureSslStoreProvider(protocol, sslStoreProvider); - } - else { - configureSslKeyStore(protocol, ssl); - configureSslTrustStore(protocol, ssl); - } } - private void configureSslClientAuth(AbstractHttp11JsseProtocol protocol, Ssl ssl) { + private void configureSslClientAuth(SSLHostConfig config, Ssl ssl) { if (ssl.getClientAuth() == Ssl.ClientAuth.NEED) { - protocol.setClientAuth(Boolean.TRUE.toString()); + config.setCertificateVerification("required"); } else if (ssl.getClientAuth() == Ssl.ClientAuth.WANT) { - protocol.setClientAuth("want"); + config.setCertificateVerification("optional"); } } - protected void configureSslStoreProvider(AbstractHttp11JsseProtocol protocol, - SslStoreProvider sslStoreProvider) { + protected void configureSslStoreProvider(AbstractHttp11JsseProtocol protocol, SSLHostConfig sslHostConfig, + SSLHostConfigCertificate certificate, SslStoreProvider sslStoreProvider) { Assert.isInstanceOf(Http11NioProtocol.class, protocol, "SslStoreProvider can only be used with Http11NioProtocol"); - TomcatURLStreamHandlerFactory instance = TomcatURLStreamHandlerFactory.getInstance(); - instance.addUserFactory(new SslStoreProviderUrlStreamHandlerFactory(sslStoreProvider)); try { if (sslStoreProvider.getKeyStore() != null) { - protocol.setKeystorePass(""); - protocol.setKeystoreFile(SslStoreProviderUrlStreamHandlerFactory.KEY_STORE_URL); + certificate.setCertificateKeystore(sslStoreProvider.getKeyStore()); } if (sslStoreProvider.getTrustStore() != null) { - protocol.setTruststorePass(""); - protocol.setTruststoreFile(SslStoreProviderUrlStreamHandlerFactory.TRUST_STORE_URL); + sslHostConfig.setTrustStore(sslStoreProvider.getTrustStore()); } } catch (Exception ex) { @@ -124,38 +134,38 @@ class SslConnectorCustomizer implements TomcatConnectorCustomizer { } } - private void configureSslKeyStore(AbstractHttp11JsseProtocol protocol, Ssl ssl) { + private void configureSslKeyStore(SSLHostConfigCertificate certificate, Ssl ssl) { try { - protocol.setKeystoreFile(ResourceUtils.getURL(ssl.getKeyStore()).toString()); + certificate.setCertificateKeystoreFile(ResourceUtils.getURL(ssl.getKeyStore()).toString()); } catch (Exception ex) { throw new WebServerException("Could not load key store '" + ssl.getKeyStore() + "'", ex); } if (ssl.getKeyStoreType() != null) { - protocol.setKeystoreType(ssl.getKeyStoreType()); + certificate.setCertificateKeystoreType(ssl.getKeyStoreType()); } if (ssl.getKeyStoreProvider() != null) { - protocol.setKeystoreProvider(ssl.getKeyStoreProvider()); + certificate.setCertificateKeystoreProvider(ssl.getKeyStoreProvider()); } } - private void configureSslTrustStore(AbstractHttp11JsseProtocol protocol, Ssl ssl) { + private void configureSslTrustStore(SSLHostConfig sslHostConfig, Ssl ssl) { if (ssl.getTrustStore() != null) { try { - protocol.setTruststoreFile(ResourceUtils.getURL(ssl.getTrustStore()).toString()); + sslHostConfig.setTruststoreFile(ResourceUtils.getURL(ssl.getTrustStore()).toString()); } catch (FileNotFoundException ex) { throw new WebServerException("Could not load trust store: " + ex.getMessage(), ex); } } if (ssl.getTrustStorePassword() != null) { - protocol.setTruststorePass(ssl.getTrustStorePassword()); + sslHostConfig.setTruststorePassword(ssl.getTrustStorePassword()); } if (ssl.getTrustStoreType() != null) { - protocol.setTruststoreType(ssl.getTrustStoreType()); + sslHostConfig.setTruststoreType(ssl.getTrustStoreType()); } if (ssl.getTrustStoreProvider() != null) { - protocol.setTruststoreProvider(ssl.getTrustStoreProvider()); + sslHostConfig.setTruststoreProvider(ssl.getTrustStoreProvider()); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslStoreProviderUrlStreamHandlerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslStoreProviderUrlStreamHandlerFactory.java deleted file mode 100644 index be27b21095..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslStoreProviderUrlStreamHandlerFactory.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2012-2019 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 - * - * https://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.web.embedded.tomcat; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLStreamHandler; -import java.net.URLStreamHandlerFactory; -import java.security.KeyStore; - -import org.springframework.boot.web.server.SslStoreProvider; - -/** - * A {@link URLStreamHandlerFactory} that provides a {@link URLStreamHandler} for - * accessing an {@link SslStoreProvider}'s key store and trust store from a URL. - * - * @author Andy Wilkinson - */ -class SslStoreProviderUrlStreamHandlerFactory implements URLStreamHandlerFactory { - - private static final String PROTOCOL = "springbootssl"; - - private static final String KEY_STORE_PATH = "keyStore"; - - static final String KEY_STORE_URL = PROTOCOL + ":" + KEY_STORE_PATH; - - private static final String TRUST_STORE_PATH = "trustStore"; - - static final String TRUST_STORE_URL = PROTOCOL + ":" + TRUST_STORE_PATH; - - private final SslStoreProvider sslStoreProvider; - - SslStoreProviderUrlStreamHandlerFactory(SslStoreProvider sslStoreProvider) { - this.sslStoreProvider = sslStoreProvider; - } - - @Override - public URLStreamHandler createURLStreamHandler(String protocol) { - if (PROTOCOL.equals(protocol)) { - return new URLStreamHandler() { - - @Override - protected URLConnection openConnection(URL url) throws IOException { - try { - if (KEY_STORE_PATH.equals(url.getPath())) { - return new KeyStoreUrlConnection(url, - SslStoreProviderUrlStreamHandlerFactory.this.sslStoreProvider.getKeyStore()); - } - if (TRUST_STORE_PATH.equals(url.getPath())) { - return new KeyStoreUrlConnection(url, - SslStoreProviderUrlStreamHandlerFactory.this.sslStoreProvider.getTrustStore()); - } - } - catch (Exception ex) { - throw new IOException(ex); - } - throw new IOException("Invalid path: " + url.getPath()); - } - }; - } - return null; - } - - private static final class KeyStoreUrlConnection extends URLConnection { - - private final KeyStore keyStore; - - private KeyStoreUrlConnection(URL url, KeyStore keyStore) { - super(url); - this.keyStore = keyStore; - } - - @Override - public void connect() throws IOException { - - } - - @Override - public InputStream getInputStream() throws IOException { - - try { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - this.keyStore.store(stream, new char[0]); - return new ByteArrayInputStream(stream.toByteArray()); - } - catch (Exception ex) { - throw new IOException(ex); - } - } - - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java index 7c0deaa985..7935d8634a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -23,13 +23,15 @@ import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; +import java.util.Set; +import java.util.stream.Collectors; import org.apache.catalina.LifecycleState; import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; -import org.apache.coyote.http11.Http11NioProtocol; import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.SSLHostConfigCertificate; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -53,6 +55,8 @@ import static org.mockito.Mockito.mock; * Tests for {@link SslConnectorCustomizer} * * @author Brian Clozel + * @author Andy Wilkinson + * @author Scott Frederick */ @ExtendWith(OutputCaptureExtension.class) class SslConnectorCustomizerTests { @@ -129,7 +133,8 @@ class SslConnectorCustomizerTests { ssl.setKeyPassword("password"); ssl.setTrustStore("src/test/resources/test.jks"); SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class); - given(sslStoreProvider.getKeyStore()).willReturn(loadStore()); + KeyStore keyStore = loadStore(); + given(sslStoreProvider.getKeyStore()).willReturn(keyStore); SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, sslStoreProvider); Connector connector = this.tomcat.getConnector(); customizer.customize(connector); @@ -137,8 +142,9 @@ class SslConnectorCustomizerTests { SSLHostConfig sslHostConfig = connector.getProtocolHandler().findSslHostConfigs()[0]; SSLHostConfig sslHostConfigWithDefaults = new SSLHostConfig(); assertThat(sslHostConfig.getTruststoreFile()).isEqualTo(sslHostConfigWithDefaults.getTruststoreFile()); - assertThat(sslHostConfig.getCertificateKeystoreFile()) - .isEqualTo(SslStoreProviderUrlStreamHandlerFactory.KEY_STORE_URL); + Set certificates = sslHostConfig.getCertificates(); + assertThat(certificates).hasSize(1); + assertThat(certificates.iterator().next().getCertificateKeystore()).isEqualTo(keyStore); } @Test @@ -147,7 +153,8 @@ class SslConnectorCustomizerTests { ssl.setKeyPassword("password"); ssl.setKeyStore("src/test/resources/test.jks"); SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class); - given(sslStoreProvider.getTrustStore()).willReturn(loadStore()); + KeyStore trustStore = loadStore(); + given(sslStoreProvider.getTrustStore()).willReturn(trustStore); SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, sslStoreProvider); Connector connector = this.tomcat.getConnector(); customizer.customize(connector); @@ -156,10 +163,11 @@ class SslConnectorCustomizerTests { sslHostConfig.getCertificates(true); SSLHostConfig sslHostConfigWithDefaults = new SSLHostConfig(); sslHostConfigWithDefaults.getCertificates(true); - assertThat(sslHostConfig.getTruststoreFile()) - .isEqualTo(SslStoreProviderUrlStreamHandlerFactory.TRUST_STORE_URL); - assertThat(sslHostConfig.getCertificateKeystoreFile()) - .contains(sslHostConfigWithDefaults.getCertificateKeystoreFile()); + assertThat(sslHostConfig.getTruststore()).isEqualTo(trustStore); + System.out.println(sslHostConfig.getCertificates(false).stream() + .map(SSLHostConfigCertificate::getCertificateFile).collect(Collectors.toList())); + System.out.println(sslHostConfigWithDefaults.getCertificates(false).stream() + .map(SSLHostConfigCertificate::getCertificateFile).collect(Collectors.toList())); } @Test @@ -186,37 +194,6 @@ class SslConnectorCustomizerTests { .withMessageContaining("Could not load key store 'null'"); } - @Test - void keyStorePasswordIsNotSetWhenNull() { - Http11NioProtocol protocol = (Http11NioProtocol) this.tomcat.getConnector().getProtocolHandler(); - protocol.setKeystorePass("password"); - Ssl ssl = new Ssl(); - ssl.setKeyStore("src/test/resources/test.jks"); - new SslConnectorCustomizer(ssl, null).customize(this.tomcat.getConnector()); - assertThat(protocol.getKeystorePass()).isEqualTo("password"); - } - - @Test - void keyPasswordIsNotSetWhenNull() { - Http11NioProtocol protocol = (Http11NioProtocol) this.tomcat.getConnector().getProtocolHandler(); - protocol.setKeyPass("password"); - Ssl ssl = new Ssl(); - ssl.setKeyStore("src/test/resources/test.jks"); - new SslConnectorCustomizer(ssl, null).customize(this.tomcat.getConnector()); - assertThat(protocol.getKeyPass()).isEqualTo("password"); - } - - @Test - void trustStorePasswordIsNotSetWhenNull() { - Http11NioProtocol protocol = (Http11NioProtocol) this.tomcat.getConnector().getProtocolHandler(); - protocol.setTruststorePass("password"); - Ssl ssl = new Ssl(); - ssl.setKeyStore("src/test/resources/test.jks"); - ssl.setTrustStore("src/test/resources/test.jks"); - new SslConnectorCustomizer(ssl, null).customize(this.tomcat.getConnector()); - assertThat(protocol.getTruststorePass()).isEqualTo("password"); - } - private KeyStore loadStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { KeyStore keyStore = KeyStore.getInstance("JKS"); Resource resource = new ClassPathResource("test.jks");