diff --git a/gradle.properties b/gradle.properties index 46d588c056f..795a568cc62 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,6 +23,6 @@ nullabilityPluginVersion=0.0.11 snakeYamlVersion=2.5 springFrameworkVersion=7.0.4-SNAPSHOT springFramework60xVersion=6.0.23 -tomcatVersion=11.0.15 +tomcatVersion=11.0.18 kotlin.stdlib.default.dependency=false diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/SslConnectorCustomizer.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/SslConnectorCustomizer.java index 9b5afd1e6ee..590d3924c60 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/SslConnectorCustomizer.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/SslConnectorCustomizer.java @@ -16,6 +16,8 @@ package org.springframework.boot.tomcat; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import org.apache.catalina.connector.Connector; @@ -25,6 +27,7 @@ import org.apache.coyote.http11.AbstractHttp11Protocol; import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.net.SSLHostConfigCertificate; import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; +import org.apache.tomcat.util.net.openssl.ciphers.OpenSSLCipherConfigurationParser; import org.jspecify.annotations.Nullable; import org.springframework.boot.ssl.SslBundle; @@ -113,14 +116,24 @@ public class SslConnectorCustomizer { certificate.setCertificateKeyAlias(key.getAlias()); } sslHostConfig.addCertificate(certificate); - if (options.getCiphers() != null) { - String ciphers = StringUtils.arrayToCommaDelimitedString(options.getCiphers()); - sslHostConfig.setCiphers(ciphers); - } + configureCiphers(options, sslHostConfig); configureSslStores(sslHostConfig, certificate, stores); configureEnabledProtocols(sslHostConfig, options); } + private void configureCiphers(SslOptions options, SSLHostConfig sslHostConfig) { + CipherConfiguration cipherConfiguration = CipherConfiguration.from(options); + if (cipherConfiguration != null) { + sslHostConfig.setCiphers(cipherConfiguration.tls12Ciphers); + try { + sslHostConfig.setCipherSuites(cipherConfiguration.tls13Ciphers); + } + catch (Exception ex) { + // Tomcat version without setCipherSuites method. Continue. + } + } + } + private void configureEnabledProtocols(SSLHostConfig sslHostConfig, SslOptions options) { if (options.getEnabledProtocols() != null) { String enabledProtocols = StringUtils.arrayToDelimitedString(options.getEnabledProtocols(), "+"); @@ -147,4 +160,47 @@ public class SslConnectorCustomizer { } } + private static class CipherConfiguration { + + private final String tls12Ciphers; + + private final String tls13Ciphers; + + CipherConfiguration(String tls12Ciphers, String tls13Ciphers) { + this.tls12Ciphers = tls12Ciphers; + this.tls13Ciphers = tls13Ciphers; + } + + static @Nullable CipherConfiguration from(SslOptions options) { + List tls12Ciphers = new ArrayList<>(); + List tls13Ciphers = new ArrayList<>(); + String[] ciphers = options.getCiphers(); + if (ciphers == null || ciphers.length == 0) { + return null; + } + for (String cipher : ciphers) { + if (isTls13(cipher)) { + tls13Ciphers.add(cipher); + } + else { + tls12Ciphers.add(cipher); + } + } + return new CipherConfiguration(StringUtils.collectionToCommaDelimitedString(tls12Ciphers), + StringUtils.collectionToCommaDelimitedString(tls13Ciphers)); + } + + private static boolean isTls13(String cipher) { + try { + return OpenSSLCipherConfigurationParser.isTls13Cipher(cipher); + } + catch (Exception ex) { + // Tomcat version without isTls13Cipher method. Continue, treating all + // ciphers as TLSv1.2 + return false; + } + } + + } + } diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/SslConnectorCustomizerTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/SslConnectorCustomizerTests.java index 69b49538ce0..af81da18728 100644 --- a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/SslConnectorCustomizerTests.java +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/SslConnectorCustomizerTests.java @@ -23,6 +23,7 @@ import org.apache.catalina.startup.Tomcat; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.openssl.ciphers.Cipher; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -73,7 +74,7 @@ class SslConnectorCustomizerTests { @Test @WithPackageResources("test.jks") - void sslCiphersConfiguration() throws Exception { + void tls12CiphersConfiguration() throws Exception { Ssl ssl = new Ssl(); ssl.setKeyStore("classpath:test.jks"); ssl.setKeyStorePassword("secret"); @@ -84,6 +85,42 @@ class SslConnectorCustomizerTests { this.tomcat.start(); SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs(); assertThat(sslHostConfigs[0].getCiphers()).isEqualTo("ALPHA:BRAVO:CHARLIE"); + assertThat(sslHostConfigs[0].getCipherSuites()).isEmpty(); + } + + @Test + @WithPackageResources("test.jks") + void tls13CiphersConfiguration() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStore("classpath:test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setCiphers(new String[] { Cipher.TLS_AES_128_CCM_SHA256.getOpenSSLAlias(), + Cipher.TLS_AES_256_GCM_SHA384.getOpenSSLAlias() }); + Connector connector = this.tomcat.getConnector(); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); + customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap()); + this.tomcat.start(); + SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs(); + assertThat(sslHostConfigs[0].getCiphers()).isEmpty(); + assertThat(sslHostConfigs[0].getCipherSuites()).isEqualTo("TLS_AES_128_CCM_SHA256:TLS_AES_256_GCM_SHA384"); + } + + @Test + @WithPackageResources("test.jks") + void mixedTls12AndTls13CiphersConfiguration() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStore("classpath:test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setCiphers(new String[] { Cipher.TLS_AES_128_CCM_SHA256.getOpenSSLAlias(), + Cipher.TLS_DH_DSS_WITH_AES_128_CBC_SHA256.getOpenSSLAlias(), + Cipher.TLS_AES_256_GCM_SHA384.getOpenSSLAlias() }); + Connector connector = this.tomcat.getConnector(); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); + customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap()); + this.tomcat.start(); + SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs(); + assertThat(sslHostConfigs[0].getCiphers()).isEqualTo("DH-DSS-AES128-SHA256"); + assertThat(sslHostConfigs[0].getCipherSuites()).isEqualTo("TLS_AES_128_CCM_SHA256:TLS_AES_256_GCM_SHA384"); } @Test