From d40ec740f5d8c2e09d34bcf6e825cd05d522b46a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 Feb 2026 12:08:09 +0000 Subject: [PATCH] Upgrade to Tomcat 10.1.52 Tomcat 9.0.115, 10.1.52, and 11.0.18 include a breaking change [1] to how ciphers are configured when using HTTPS. Previously, a single setting was used but this has now been split in two; the existing ciphers setting for TLSv1.2 ciphers and a new ciperSuites setting for TLSv1.3. As part of this split, the behavior of the ciphers setting has been changed such that any TLSv1.3 ciphers are ignored and a warning is logged. This change in Tomcat is problematic without also making some changes in Boot. If we had done nothing, a user that had configured only TLSv1.3 cipers would have them all ignored, leaving their SSL connection unexpectedly using all of the default ciphers which may be less secure. This commit adapts to the breaking change in Tomcat by taking the user's list of ciphers and splitting into into TLSv1.2 and TLSv1.3 ciphers before passing them into Tomcat's two settings (ciphers and cipherSuites respectively). This is done defensively for backwards compatibility. If the methods to identify and configure the TLSv1.3 ciphers are not present, we assume that we're running with an earlier version of Tomcat and fall back to passing them all into the ciphers setting as we did previously. Closes gh-49084 [1] https://github.com/apache/tomcat/commit/9abf6bddb2e84ecf1668780bb3150b799f832ccf --- gradle.properties | 2 +- .../tomcat/SslConnectorCustomizer.java | 64 +++++++++++++++++-- .../tomcat/SslConnectorCustomizerTests.java | 39 ++++++++++- 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/gradle.properties b/gradle.properties index 1c545ab025f..758c76317c0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,6 +21,6 @@ nativeBuildToolsVersion=0.10.6 snakeYamlVersion=2.4 springFrameworkVersion=6.2.16-SNAPSHOT springFramework60xVersion=6.0.23 -tomcatVersion=10.1.50 +tomcatVersion=10.1.52 kotlin.stdlib.default.dependency=false 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 8529d3ebbe3..0e2d04bd277 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 @@ -16,6 +16,8 @@ package org.springframework.boot.web.embedded.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.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundleKey; @@ -111,14 +114,24 @@ 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(), "+"); @@ -145,4 +158,47 @@ 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 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/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 235bfde6d28..c9175bc0a59 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 @@ -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