Use Tomcat SSLHostConfig API for SSL configuration
Closes gh-30531
This commit is contained in:
parent
1c71567c94
commit
103c2bdd7d
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<SSLHostConfigCertificate> 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");
|
||||
|
|
|
|||
Loading…
Reference in New Issue