Skip to content

Commit 9abf6bd

Browse files
committed
Add support for setting TLS 1.3 cipher suites
For consistency between OpenSSL and JSSE TLS implementations, TLSv.13 cipher suites included in the ciphers attribute of an SSLHostConfig are now always ignored (previously they would be ignored with OpenSSL implementations and used with JSSE implementations) and a warning is logged that the cipher suite has been ignored.
1 parent 666456c commit 9abf6bd

File tree

9 files changed

+388
-34
lines changed

9 files changed

+388
-34
lines changed

java/org/apache/tomcat/util/net/LocalStrings.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ socketWrapper.writeTimeout=Write timeout
147147
sslHostConfig.certificate.notype=Multiple certificates were specified and at least one is missing the required attribute type
148148
sslHostConfig.certificateVerificationInvalid=The certificate verification value [{0}] is not recognised
149149
sslHostConfig.fileNotFound=Configured file [{0}] does not exist
150+
sslHostConfig.ignoreNonTls13Ciphersuite=The non-TLS 1.3 cipher suite [{0}] included in the TLS 1.3 cipher suite list will be ignored
151+
sslHostConfig.ignoreTls13Ciphersuite=The TLS 1.3 cipher suite [{0}] included in the TLS 1.2 and below ciphers list will be ignored
150152
sslHostConfig.invalid_truststore_password=The provided trust store password could not be used to unlock and/or validate the trust store. Retrying to access the trust store with a null password which will skip validation.
151153
sslHostConfig.mismatch=The property [{0}] was set on the SSLHostConfig named [{1}] and is for the [{2}] configuration syntax but the SSLHostConfig is being used with the [{3}] configuration syntax
152154
sslHostConfig.mismatch.trust=The trust configuration property [{0}] was set on the SSLHostConfig named [{1}] and is for the [{2}] configuration syntax but the SSLHostConfig is being used with the [{3}] trust configuration syntax

java/org/apache/tomcat/util/net/SSLHostConfig.java

Lines changed: 110 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,10 @@ public class SSLHostConfig implements Serializable {
111111
private int certificateVerificationDepth = 10;
112112
// Used to track if certificateVerificationDepth has been explicitly set
113113
private boolean certificateVerificationDepthConfigured = false;
114-
private String ciphers = DEFAULT_TLS_CIPHERS;
114+
private String ciphers = DEFAULT_TLS_CIPHERS_12;
115+
private String cipherSuites = DEFAULT_TLS_CIPHERS_13;
115116
private LinkedHashSet<Cipher> cipherList = null;
117+
private LinkedHashSet<Cipher> cipherSuiteList = null;
116118
private List<String> jsseCipherNames = null;
117119
private boolean honorCipherOrder = false;
118120
private final Set<String> protocols = new HashSet<>();
@@ -383,36 +385,59 @@ public boolean isCertificateVerificationDepthConfigured() {
383385

384386

385387
/**
386-
* Set the new cipher configuration. Note: Regardless of the format used to set the configuration, it is always
387-
* stored in OpenSSL format.
388+
* Set the new cipher (TLSv1.2 and below) configuration. Note: Regardless of the format used to set the
389+
* configuration, it is always stored in OpenSSL format.
388390
*
389391
* @param ciphersList The new cipher configuration in OpenSSL or JSSE format
390392
*/
391393
public void setCiphers(String ciphersList) {
392394
// Ciphers is stored in OpenSSL format. Convert the provided value if
393395
// necessary.
394-
if (ciphersList != null && !ciphersList.contains(":")) {
395-
StringBuilder sb = new StringBuilder();
396-
// Not obviously in OpenSSL format. Might be a single OpenSSL or JSSE
397-
// cipher name. Might be a comma separated list of cipher names
398-
String[] ciphers = ciphersList.split(",");
399-
for (String cipher : ciphers) {
400-
String trimmed = cipher.trim();
401-
if (!trimmed.isEmpty()) {
402-
String openSSLName = OpenSSLCipherConfigurationParser.jsseToOpenSSL(trimmed);
403-
if (openSSLName == null) {
404-
// Not a JSSE name. Maybe an OpenSSL name or alias
405-
openSSLName = trimmed;
396+
if (ciphersList != null) {
397+
if (ciphersList.contains(":")) {
398+
// OpenSSL format
399+
StringBuilder sb = new StringBuilder();
400+
String[] components = ciphersList.split(":");
401+
// Remove any TLS 1.3 cipher suites
402+
for (String component : components) {
403+
String trimmed = component.trim();
404+
if (OpenSSLCipherConfigurationParser.isTls13Cipher(trimmed)) {
405+
log.warn(sm.getString("sslHostConfig.ignoreTls13Ciphersuite", trimmed));
406+
} else {
407+
if (!sb.isEmpty()) {
408+
sb.append(':');
409+
}
410+
sb.append(trimmed);
406411
}
407-
if (!sb.isEmpty()) {
408-
sb.append(':');
412+
}
413+
this.ciphers = sb.toString();
414+
} else {
415+
// Not obviously in OpenSSL format. Might be a single OpenSSL or JSSE
416+
// cipher name. Might be a comma separated list of cipher names
417+
StringBuilder sb = new StringBuilder();
418+
String[] ciphers = ciphersList.split(",");
419+
for (String cipher : ciphers) {
420+
String trimmed = cipher.trim();
421+
if (!trimmed.isEmpty()) {
422+
if (OpenSSLCipherConfigurationParser.isTls13Cipher(trimmed)) {
423+
log.warn(sm.getString("sslHostConfig.ignoreTls13Ciphersuite", trimmed));
424+
continue;
425+
}
426+
String openSSLName = OpenSSLCipherConfigurationParser.jsseToOpenSSL(trimmed);
427+
if (openSSLName == null) {
428+
// Not a JSSE name. Maybe an OpenSSL name or alias
429+
openSSLName = trimmed;
430+
}
431+
if (!sb.isEmpty()) {
432+
sb.append(':');
433+
}
434+
sb.append(openSSLName);
409435
}
410-
sb.append(openSSLName);
411436
}
437+
this.ciphers = sb.toString();
412438
}
413-
this.ciphers = sb.toString();
414439
} else {
415-
this.ciphers = ciphersList;
440+
this.ciphers = null;
416441
}
417442
this.cipherList = null;
418443
this.jsseCipherNames = null;
@@ -443,12 +468,76 @@ public LinkedHashSet<Cipher> getCipherList() {
443468
*/
444469
public List<String> getJsseCipherNames() {
445470
if (jsseCipherNames == null) {
446-
jsseCipherNames = OpenSSLCipherConfigurationParser.convertForJSSE(getCipherList());
471+
Set<Cipher> jsseCiphers = new HashSet<>();
472+
jsseCiphers.addAll(getCipherSuiteList());
473+
jsseCiphers.addAll(getCipherList());
474+
jsseCipherNames = OpenSSLCipherConfigurationParser.convertForJSSE(jsseCiphers);
447475
}
448476
return jsseCipherNames;
449477
}
450478

451479

480+
/**
481+
* Set the cipher suite (TLSv1.3) configuration.
482+
*
483+
* @param cipherSuites The cipher suites to use in a colon-separated, preference order list
484+
*/
485+
public void setCipherSuites(String cipherSuites) {
486+
StringBuilder sb = new StringBuilder();
487+
String[] values;
488+
if (cipherSuites.contains(":")) {
489+
// OpenSSL format
490+
values = cipherSuites.split(":");
491+
} else {
492+
// JSSE format or possible a single cipher suite name
493+
values = cipherSuites.split(",");
494+
}
495+
for (String value : values) {
496+
String trimmed = value.trim();
497+
if (!trimmed.isEmpty()) {
498+
if (!OpenSSLCipherConfigurationParser.isTls13Cipher(trimmed)) {
499+
log.warn(sm.getString("sslHostConfig.ignoreNonTls13Ciphersuite", trimmed));
500+
continue;
501+
}
502+
/*
503+
* OpenSSL and JSSE names for TLSv1.3 cipher suites are currently (January 2026) the same but handle the
504+
* possible future case where they are not.
505+
*/
506+
String openSSLName = OpenSSLCipherConfigurationParser.jsseToOpenSSL(trimmed);
507+
if (openSSLName == null) {
508+
// Not a JSSE name. Maybe an OpenSSL name or alias
509+
openSSLName = trimmed;
510+
}
511+
if (!sb.isEmpty()) {
512+
sb.append(':');
513+
}
514+
sb.append(trimmed);
515+
}
516+
}
517+
this.cipherSuites = sb.toString();
518+
this.cipherSuiteList = null;
519+
this.jsseCipherNames = null;
520+
}
521+
522+
523+
/**
524+
* Obtain the current cipher suite (TLSv1.3) configuration.
525+
*
526+
* @return An OpenSSL cipher suite string for the current configuration.
527+
*/
528+
public String getCipherSuites() {
529+
return cipherSuites;
530+
}
531+
532+
533+
private LinkedHashSet<Cipher> getCipherSuiteList() {
534+
if (cipherSuiteList == null) {
535+
cipherSuiteList = OpenSSLCipherConfigurationParser.parse(getCipherSuites());
536+
}
537+
return cipherSuiteList;
538+
}
539+
540+
452541
public void setHonorCipherOrder(boolean honorCipherOrder) {
453542
this.honorCipherOrder = honorCipherOrder;
454543
}

java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ public void init(KeyManager[] kms, TrustManager[] tms, SecureRandom sr) throws K
318318

319319
// Configure the ciphers that the client is permitted to negotiate
320320
SSLContext.setCipherSuite(state.ctx, sslHostConfig.getCiphers());
321+
SSLContext.setCipherSuitesEx(state.ctx, sslHostConfig.getCipherSuites());
321322

322323
// If there is no certificate file must be using a KeyStore so a KeyManager is required.
323324
// If there is a certificate file a KeyManager is helpful but not strictly necessary.

java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -515,14 +515,14 @@ public void init(KeyManager[] kms, TrustManager[] tms, SecureRandom sr) throws K
515515
}
516516
if (maxTlsVersion >= TLS1_3_VERSION()) {
517517
try {
518-
if (SSL_CTX_set_ciphersuites(state.sslCtx, localArena.allocateFrom(sslHostConfig.getCiphers())) <= 0) {
519-
tls13Warning = sm.getString("engine.failedCipherSuite", sslHostConfig.getCiphers());
518+
if (SSL_CTX_set_ciphersuites(state.sslCtx, localArena.allocateFrom(sslHostConfig.getCipherSuites())) <= 0) {
519+
tls13Warning = sm.getString("engine.failedCipherSuite", sslHostConfig.getCipherSuites());
520520
} else {
521521
ciphersSet = true;
522522
}
523523
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
524524
// Ignore unavailable TLS 1.3 call, which might be compiled out sometimes on LibreSSL
525-
tls13Warning = sm.getString("engine.failedCipherSuite", sslHostConfig.getCiphers());
525+
tls13Warning = sm.getString("engine.failedCipherSuite", sslHostConfig.getCipherSuites());
526526
}
527527
}
528528
if (!ciphersSet) {

test/org/apache/tomcat/util/net/TestSSLHostConfig.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,76 @@ public void testCipher04() {
7676
}
7777

7878

79+
@Test
80+
public void testCipher05() {
81+
SSLHostConfig hc = new SSLHostConfig();
82+
Cipher c = Cipher.TLS_AES_128_CCM_SHA256;
83+
84+
// Single TLSv1.3 name - should be filtered out
85+
hc.setCiphers(c.getOpenSSLAlias());
86+
Assert.assertEquals("", hc.getCiphers());
87+
}
88+
89+
90+
@Test
91+
public void testCipher06() {
92+
SSLHostConfig hc = new SSLHostConfig();
93+
Cipher c1 = Cipher.TLS_AES_128_CCM_SHA256;
94+
Cipher c2 = Cipher.TLS_RSA_WITH_NULL_MD5;
95+
96+
// TLSv1.3 then TLSv1.2 - TLSv1.3 name should be filtered out
97+
hc.setCiphers(c1.getOpenSSLAlias() + ":" + c2.getOpenSSLAlias());
98+
Assert.assertEquals(c2.getOpenSSLAlias(), hc.getCiphers());
99+
}
100+
101+
102+
@Test
103+
public void testCipher07() {
104+
SSLHostConfig hc = new SSLHostConfig();
105+
Cipher c1 = Cipher.TLS_AES_128_CCM_SHA256;
106+
Cipher c2 = Cipher.TLS_RSA_WITH_NULL_MD5;
107+
108+
// TLSv1.2 then TLSv1.3 - TLSv1.3 name should be filtered out
109+
hc.setCiphers(c2.getOpenSSLAlias() + ":" + c1.getOpenSSLAlias());
110+
Assert.assertEquals(c2.getOpenSSLAlias(), hc.getCiphers());
111+
}
112+
113+
114+
@Test
115+
public void testCiphersuite01() {
116+
SSLHostConfig hc = new SSLHostConfig();
117+
Cipher c = Cipher.TLS_AES_128_CCM_SHA256;
118+
119+
// Single TLSv1.3 cipher suite name
120+
hc.setCipherSuites(c.getOpenSSLAlias());
121+
Assert.assertEquals(c.getOpenSSLAlias(), hc.getCipherSuites());
122+
}
123+
124+
125+
@Test
126+
public void testCiphersuite02() {
127+
SSLHostConfig hc = new SSLHostConfig();
128+
Cipher c1 = Cipher.TLS_AES_128_CCM_SHA256;
129+
Cipher c2 = Cipher.TLS_RSA_WITH_NULL_MD5;
130+
131+
// TLSv1.3 then TLSv1.2 - TLSv1.2 name should be filtered out
132+
hc.setCipherSuites(c1.getOpenSSLAlias() + ":" + c2.getOpenSSLAlias());
133+
Assert.assertEquals(c1.getOpenSSLAlias(), hc.getCipherSuites());
134+
}
135+
136+
137+
@Test
138+
public void testCiphersuite03() {
139+
SSLHostConfig hc = new SSLHostConfig();
140+
Cipher c1 = Cipher.TLS_AES_128_CCM_SHA256;
141+
Cipher c2 = Cipher.TLS_RSA_WITH_NULL_MD5;
142+
143+
// TLSv1.2 then TLSv1.3 - TLSv1.2 name should be filtered out
144+
hc.setCipherSuites(c2.getOpenSSLAlias() + ":" + c1.getOpenSSLAlias());
145+
Assert.assertEquals(c1.getOpenSSLAlias(), hc.getCipherSuites());
146+
}
147+
148+
79149
@Test
80150
public void testSerialization() throws IOException, ClassNotFoundException {
81151
// Dummy OpenSSL command name/value pair

0 commit comments

Comments
 (0)