This is an automated email from the ASF dual-hosted git repository. remm pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit bdc822ffe795126aed32d3d3472f8b8175f14623 Author: remm <[email protected]> AuthorDate: Thu Sep 11 20:55:46 2025 +0200 Add certificate selection code specific to TLS 1.3 The signature algorithms preferences sent in the client hello should be enough to select the appropriate certificate (groups can be used as well, but it seems redundant and more complex). BZ69800: Add groups configuration on SSLHostConfig and pass that to JSSE (unsure about the actual usefulness though, I don't see any reason to disable a group at this point). Remove the OpenSSL specific code extracted from mod_ssl since it is supposed to be doing the same thing. --- .../org/apache/tomcat/util/compat/Jre20Compat.java | 66 +++++++++++++++++++ .../org/apache/tomcat/util/compat/Jre21Compat.java | 2 +- java/org/apache/tomcat/util/compat/JreCompat.java | 32 +++++++++ .../tomcat/util/net/AbstractJsseEndpoint.java | 76 ++++++++++++---------- java/org/apache/tomcat/util/net/SSLHostConfig.java | 37 +++++++++++ .../tomcat/util/net/SSLHostConfigCertificate.java | 8 ++- .../apache/tomcat/util/net/SecureNioChannel.java | 17 ++++- .../util/net/openssl/ciphers/Authentication.java | 1 + webapps/docs/changelog.xml | 13 ++-- webapps/docs/config/http.xml | 9 +++ 10 files changed, 221 insertions(+), 40 deletions(-) diff --git a/java/org/apache/tomcat/util/compat/Jre20Compat.java b/java/org/apache/tomcat/util/compat/Jre20Compat.java new file mode 100644 index 0000000000..79c7aa9329 --- /dev/null +++ b/java/org/apache/tomcat/util/compat/Jre20Compat.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.tomcat.util.compat; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public class Jre20Compat extends Jre19Compat { + + private static final Log log = LogFactory.getLog(Jre20Compat.class); + private static final StringManager sm = StringManager.getManager(Jre20Compat.class); + + private static final boolean supported; + private static final Method setNamedGroupsMethod; + + + static { + Class<?> c1 = null; + Method m1 = null; + + try { + c1 = Class.forName("javax.net.ssl.SSLParameters"); + m1 = c1.getMethod("setNamedGroups", String[].class); + } catch (ClassNotFoundException e) { + // Must be pre-Java 20 + log.debug(sm.getString("jre20Compat.javaPre20"), e); + } catch (ReflectiveOperationException e) { + // Should never happen + log.error(sm.getString("jre20Compat.unexpected"), e); + } + supported = (c1 != null); + setNamedGroupsMethod = m1; + } + + static boolean isSupported() { + return supported; + } + + @Override + public void setNamedGroupsMethod(Object sslParameters, String[] names) { + try { + setNamedGroupsMethod.invoke(sslParameters, (Object[]) names); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new UnsupportedOperationException(e); + } + } + +} diff --git a/java/org/apache/tomcat/util/compat/Jre21Compat.java b/java/org/apache/tomcat/util/compat/Jre21Compat.java index 23ebd27feb..6fc13c3b69 100644 --- a/java/org/apache/tomcat/util/compat/Jre21Compat.java +++ b/java/org/apache/tomcat/util/compat/Jre21Compat.java @@ -27,7 +27,7 @@ import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.res.StringManager; -public class Jre21Compat extends Jre19Compat { +public class Jre21Compat extends Jre20Compat { private static final Log log = LogFactory.getLog(Jre21Compat.class); private static final StringManager sm = StringManager.getManager(Jre21Compat.class); diff --git a/java/org/apache/tomcat/util/compat/JreCompat.java b/java/org/apache/tomcat/util/compat/JreCompat.java index 72e31a4161..5c006f6043 100644 --- a/java/org/apache/tomcat/util/compat/JreCompat.java +++ b/java/org/apache/tomcat/util/compat/JreCompat.java @@ -62,6 +62,7 @@ public class JreCompat { private static final boolean jre12Available; private static final boolean jre16Available; private static final boolean jre19Available; + private static final boolean jre20Available; private static final boolean jre21Available; private static final boolean jre22Available; @@ -91,6 +92,7 @@ public class JreCompat { instance = new Jre22Compat(); jre22Available = true; jre21Available = true; + jre20Available = true; jre19Available = true; jre16Available = true; jre12Available = true; @@ -99,6 +101,16 @@ public class JreCompat { instance = new Jre21Compat(); jre22Available = false; jre21Available = true; + jre20Available = true; + jre19Available = true; + jre16Available = true; + jre12Available = true; + jre9Available = true; + } else if (Jre20Compat.isSupported()) { + instance = new Jre20Compat(); + jre22Available = false; + jre21Available = false; + jre20Available = true; jre19Available = true; jre16Available = true; jre12Available = true; @@ -107,6 +119,7 @@ public class JreCompat { instance = new Jre19Compat(); jre22Available = false; jre21Available = false; + jre20Available = false; jre19Available = true; jre16Available = true; jre12Available = true; @@ -115,6 +128,7 @@ public class JreCompat { instance = new Jre16Compat(); jre22Available = false; jre21Available = false; + jre20Available = false; jre19Available = false; jre16Available = true; jre12Available = true; @@ -123,6 +137,7 @@ public class JreCompat { instance = new Jre12Compat(); jre22Available = false; jre21Available = false; + jre20Available = false; jre19Available = false; jre16Available = false; jre12Available = true; @@ -131,6 +146,7 @@ public class JreCompat { instance = new Jre9Compat(); jre22Available = false; jre21Available = false; + jre20Available = false; jre19Available = false; jre16Available = false; jre12Available = false; @@ -139,6 +155,7 @@ public class JreCompat { instance = new JreCompat(); jre22Available = false; jre21Available = false; + jre20Available = false; jre19Available = false; jre16Available = false; jre12Available = false; @@ -199,6 +216,11 @@ public class JreCompat { } + public static boolean isJre20Available() { + return jre20Available; + } + + public static boolean isJre21Available() { return jre21Available; } @@ -623,4 +645,14 @@ public class JreCompat { } } } + + /** + * TLS groups configuration from JSSE API in Java 20. + * @param sslParameters the parameters object + * @param names the names of the groups to enable + */ + public void setNamedGroupsMethod(Object sslParameters, String[] names) { + throw new UnsupportedOperationException(sm.getString("jreCompat.noNamedGroups")); + } + } diff --git a/java/org/apache/tomcat/util/net/AbstractJsseEndpoint.java b/java/org/apache/tomcat/util/net/AbstractJsseEndpoint.java index 81c6d2b827..9b1690edc8 100644 --- a/java/org/apache/tomcat/util/net/AbstractJsseEndpoint.java +++ b/java/org/apache/tomcat/util/net/AbstractJsseEndpoint.java @@ -31,9 +31,15 @@ import javax.net.ssl.SSLParameters; import org.apache.tomcat.util.compat.JreCompat; import org.apache.tomcat.util.net.openssl.OpenSSLStatus; import org.apache.tomcat.util.net.openssl.ciphers.Cipher; +import org.apache.tomcat.util.net.openssl.ciphers.Group; +import org.apache.tomcat.util.net.openssl.ciphers.SignatureAlgorithm; public abstract class AbstractJsseEndpoint<S, U> extends AbstractEndpoint<S,U> { + static final ThreadLocal<List<String>> clientRequestedProtocolsThreadLocal = new ThreadLocal<>(); + static final ThreadLocal<List<Group>> clientSupportedGroupsThreadLocal = new ThreadLocal<>(); + static final ThreadLocal<List<SignatureAlgorithm>> clientSignatureAlgorithmsThreadLocal = new ThreadLocal<>(); + private String sslImplementationName = null; private int sniParseLimit = 64 * 1024; @@ -85,24 +91,6 @@ public abstract class AbstractJsseEndpoint<S, U> extends AbstractEndpoint<S,U> { @Override protected void createSSLContext(SSLHostConfig sslHostConfig) throws IllegalArgumentException { - boolean useHybridSslContext = false; - if (sslHostConfig.getProtocols().contains(Constants.SSL_PROTO_TLSv1_3) && OpenSSLStatus.isAvailable()) { - // If TLS 1.3 is enabled, check if a hybrid scheme using a single SSL context - // should be attempted - boolean nonMldsaFound = false; - boolean mldsaFound = false; - for (SSLHostConfigCertificate certificate : sslHostConfig.getCertificates(true)) { - if (certificate.getType().equals(SSLHostConfigCertificate.Type.MLDSA)) { - mldsaFound = true; - } else { - nonMldsaFound = true; - } - } - if (mldsaFound && nonMldsaFound) { - useHybridSslContext = true; - } - } - boolean firstCertificate = true; for (SSLHostConfigCertificate certificate : sslHostConfig.getCertificates(true)) { SSLUtil sslUtil = sslImplementation.getSSLUtil(certificate); @@ -130,18 +118,6 @@ public abstract class AbstractJsseEndpoint<S, U> extends AbstractEndpoint<S,U> { certificate.setSslContextGenerated(sslContext); } - // If using a hybrid scheme, add any MLDSA certificates to all other SSL contexts - if (useHybridSslContext && !certificate.getType().equals(SSLHostConfigCertificate.Type.MLDSA)) { - for (SSLHostConfigCertificate certificateToAdd : sslHostConfig.getCertificates(true)) { - // Add additional certificate to all non MLDSA contexts - if (certificateToAdd.getType().equals(SSLHostConfigCertificate.Type.MLDSA)) { - if (!sslUtil.addSecondCertificate(sslContext, certificateToAdd)) { - throw new IllegalArgumentException(sm.getString("endpoint.errorCreatingSSLContext")); - } - } - } - } - logCertificate(certificate); } @@ -150,9 +126,14 @@ public abstract class AbstractJsseEndpoint<S, U> extends AbstractEndpoint<S,U> { protected SSLEngine createSSLEngine(String sniHostName, List<Cipher> clientRequestedCiphers, List<String> clientRequestedApplicationProtocols) { + List<String> clientRequestedProtocols = clientRequestedProtocolsThreadLocal.get(); + List<Group> clientSupportedGroups = clientSupportedGroupsThreadLocal.get(); + List<SignatureAlgorithm> clientSignatureAlgorithms = clientSignatureAlgorithmsThreadLocal.get(); + SSLHostConfig sslHostConfig = getSSLHostConfig(sniHostName); - SSLHostConfigCertificate certificate = selectCertificate(sslHostConfig, clientRequestedCiphers); + SSLHostConfigCertificate certificate = selectCertificate(sslHostConfig, clientRequestedCiphers, + clientRequestedProtocols, clientSignatureAlgorithms); SSLContext sslContext = certificate.getSslContext(); if (sslContext == null) { @@ -171,7 +152,6 @@ public abstract class AbstractJsseEndpoint<S, U> extends AbstractEndpoint<S,U> { // Only try to negotiate if both client and server have at least // one protocol in common // Note: Tomcat does not explicitly negotiate http/1.1 - // TODO: Is this correct? Should it change? List<String> commonProtocols = new ArrayList<>(negotiableProtocols); commonProtocols.retainAll(clientRequestedApplicationProtocols); if (commonProtocols.size() > 0) { @@ -179,6 +159,23 @@ public abstract class AbstractJsseEndpoint<S, U> extends AbstractEndpoint<S,U> { JreCompat.getInstance().setApplicationProtocols(sslParameters, commonProtocolsArray); } } + // Merge server groups with the client groups + if (JreCompat.isJre20Available()) { + List<String> supportedGroups = new ArrayList<>(); + LinkedHashSet<Group> serverSupportedGroups = sslHostConfig.getGroupList(); + if (serverSupportedGroups != null) { + for (Group group : clientSupportedGroups) { + if (serverSupportedGroups.contains(group)) { + supportedGroups.add(group.toString()); + } + } + } else { + for (Group group : clientSupportedGroups) { + supportedGroups.add(group.toString()); + } + } + JreCompat.getInstance().setNamedGroupsMethod(sslParameters, supportedGroups.toArray(new String[0])); + } switch (sslHostConfig.getCertificateVerification()) { case NONE: sslParameters.setNeedClientAuth(false); @@ -199,13 +196,26 @@ public abstract class AbstractJsseEndpoint<S, U> extends AbstractEndpoint<S,U> { } - private SSLHostConfigCertificate selectCertificate(SSLHostConfig sslHostConfig, List<Cipher> clientCiphers) { + private SSLHostConfigCertificate selectCertificate(SSLHostConfig sslHostConfig, List<Cipher> clientCiphers, + List<String> clientRequestedProtocols, List<SignatureAlgorithm> clientSignatureAlgorithms) { Set<SSLHostConfigCertificate> certificates = sslHostConfig.getCertificates(true); if (certificates.size() == 1) { return certificates.iterator().next(); } + // Use signature algorithm for cipher matching with TLS 1.3 + if ((clientRequestedProtocols.contains(Constants.SSL_PROTO_TLSv1_3)) && + sslHostConfig.getProtocols().contains(Constants.SSL_PROTO_TLSv1_3)) { + for (SignatureAlgorithm signatureAlgorithm : clientSignatureAlgorithms) { + for (SSLHostConfigCertificate certificate : certificates) { + if (certificate.getType().isCompatibleWith(signatureAlgorithm)) { + return certificate; + } + } + } + } + LinkedHashSet<Cipher> serverCiphers = sslHostConfig.getCipherList(); List<Cipher> candidateCiphers = new ArrayList<>(); diff --git a/java/org/apache/tomcat/util/net/SSLHostConfig.java b/java/org/apache/tomcat/util/net/SSLHostConfig.java index d438abb75e..73a87d67ff 100644 --- a/java/org/apache/tomcat/util/net/SSLHostConfig.java +++ b/java/org/apache/tomcat/util/net/SSLHostConfig.java @@ -38,6 +38,7 @@ import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.net.openssl.OpenSSLConf; import org.apache.tomcat.util.net.openssl.ciphers.Cipher; +import org.apache.tomcat.util.net.openssl.ciphers.Group; import org.apache.tomcat.util.net.openssl.ciphers.OpenSSLCipherConfigurationParser; import org.apache.tomcat.util.res.StringManager; @@ -108,6 +109,8 @@ public class SSLHostConfig implements Serializable { // Values <0 mean use the implementation default private int sessionCacheSize = -1; private int sessionTimeout = 86400; + private String groups = null; + private LinkedHashSet<Group> groupList = null; // JSSE private String keyManagerAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); private boolean revocationEnabled = false; @@ -579,6 +582,40 @@ public class SSLHostConfig implements Serializable { } + /** + * @return the configured named groups + */ + public String getGroups() { + return groups; + } + + + /** + * Set the enabled named groups. + * @param groupsString the case sensitive comma separated list of groups + */ + public void setGroups(String groupsString) { + if (groupsString != null) { + LinkedHashSet<Group> groupList = new LinkedHashSet<>(); + String[] groupNames = groupsString.split(","); + for (String groupName : groupNames) { + Group group = Group.valueOf(groupName.trim()); + groupList.add(group); + } + this.groups = groupsString; + this.groupList = groupList; + } + } + + + /** + * @return the groupList + */ + public LinkedHashSet<Group> getGroupList() { + return this.groupList; + } + + // ---------------------------------- JSSE specific configuration properties // TODO: These certificate setters can be removed once it is no longer diff --git a/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java b/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java index e7f8d628e4..b7606dd89b 100644 --- a/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java +++ b/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java @@ -30,6 +30,7 @@ import javax.net.ssl.X509KeyManager; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.net.openssl.ciphers.Authentication; +import org.apache.tomcat.util.net.openssl.ciphers.SignatureAlgorithm; import org.apache.tomcat.util.res.StringManager; public class SSLHostConfigCertificate implements Serializable { @@ -318,7 +319,7 @@ public class SSLHostConfigCertificate implements Serializable { RSA(Authentication.RSA), DSA(Authentication.DSS), EC(Authentication.ECDH, Authentication.ECDSA), - MLDSA; + MLDSA(Authentication.MLDSA); private final Set<Authentication> compatibleAuthentications; @@ -332,6 +333,11 @@ public class SSLHostConfigCertificate implements Serializable { public boolean isCompatibleWith(Authentication au) { return compatibleAuthentications.contains(au); } + + public boolean isCompatibleWith(SignatureAlgorithm al) { + return al.toString().toUpperCase().startsWith(toString()); + } + } enum StoreType { diff --git a/java/org/apache/tomcat/util/net/SecureNioChannel.java b/java/org/apache/tomcat/util/net/SecureNioChannel.java index cf20a23f15..184216e2aa 100644 --- a/java/org/apache/tomcat/util/net/SecureNioChannel.java +++ b/java/org/apache/tomcat/util/net/SecureNioChannel.java @@ -42,6 +42,8 @@ import org.apache.tomcat.util.compat.JreCompat; import org.apache.tomcat.util.net.NioEndpoint.NioSocketWrapper; import org.apache.tomcat.util.net.TLSClientHelloExtractor.ExtractorResult; import org.apache.tomcat.util.net.openssl.ciphers.Cipher; +import org.apache.tomcat.util.net.openssl.ciphers.Group; +import org.apache.tomcat.util.net.openssl.ciphers.SignatureAlgorithm; import org.apache.tomcat.util.res.StringManager; /** @@ -274,6 +276,8 @@ public class SecureNioChannel extends NioChannel { String hostName = null; List<Cipher> clientRequestedCiphers = null; List<String> clientRequestedApplicationProtocols = null; + List<Group> clientSupportedGroups = null; + List<SignatureAlgorithm> clientSignatureAlgorithms = null; switch (extractor.getResult()) { case COMPLETE: hostName = extractor.getSNIValue(); @@ -281,6 +285,8 @@ public class SecureNioChannel extends NioChannel { //$FALL-THROUGH$ to set the client requested ciphers case NOT_PRESENT: clientRequestedCiphers = extractor.getClientRequestedCiphers(); + clientSupportedGroups = extractor.getClientSupportedGroups(); + clientSignatureAlgorithms = extractor.getClientSignatureAlgorithms(); break; case NEED_READ: return SelectionKey.OP_READ; @@ -304,7 +310,16 @@ public class SecureNioChannel extends NioChannel { log.trace(sm.getString("channel.nio.ssl.sniHostName", sc, hostName)); } - sslEngine = endpoint.createSSLEngine(hostName, clientRequestedCiphers, clientRequestedApplicationProtocols); + try { + AbstractJsseEndpoint.clientRequestedProtocolsThreadLocal.set(extractor.getClientRequestedProtocols()); + AbstractJsseEndpoint.clientSupportedGroupsThreadLocal.set(clientSupportedGroups); + AbstractJsseEndpoint.clientSignatureAlgorithmsThreadLocal.set(clientSignatureAlgorithms); + sslEngine = endpoint.createSSLEngine(hostName, clientRequestedCiphers, clientRequestedApplicationProtocols); + } finally { + AbstractJsseEndpoint.clientRequestedProtocolsThreadLocal.set(null); + AbstractJsseEndpoint.clientSupportedGroupsThreadLocal.set(null); + AbstractJsseEndpoint.clientSignatureAlgorithmsThreadLocal.set(null); + } // Populate additional TLS attributes obtained from the handshake that // aren't available from the session diff --git a/java/org/apache/tomcat/util/net/openssl/ciphers/Authentication.java b/java/org/apache/tomcat/util/net/openssl/ciphers/Authentication.java index ec192db6fb..052808a36b 100644 --- a/java/org/apache/tomcat/util/net/openssl/ciphers/Authentication.java +++ b/java/org/apache/tomcat/util/net/openssl/ciphers/Authentication.java @@ -29,5 +29,6 @@ public enum Authentication { GOST01 /* GOST R 34.10-2001 */, FZA /* Fortezza */, SRP /* Secure Remote Password */, + MLDSA /* ML-DSA */, ANY /* TLS 1.3 */ } diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index baa613b221..8b8612c519 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -133,10 +133,15 @@ <subsection name="Coyote"> <changelog> <update> - Add hybrid PQC support to OpenSSL, based on code from - <code>mod_ssl</code>. Using this OpenSSL specific code path, - additional PQC certificates defined with type <code>MLDSA</code> are - added to contexts which use classic certificates. (jfclere/remm) + Add specific certificate selection code for TLS 1.3 supporting post + quantum cryptography. Certificates defined with type + <code>MLDSA</code> will be selected depending on the TLS client hello. + (remm) + </update> + <update> + Add <code>groups</code> attribute on <code>SSLHostConfig</code> + allowing to restrict which groups can be enabled on the SSL engine. + (remm) </update> <add> Optimize the conversion of HTTP method from byte form to String form. diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml index 35019594b7..7b8f005d40 100644 --- a/webapps/docs/config/http.xml +++ b/webapps/docs/config/http.xml @@ -1491,6 +1491,15 @@ not the full chain.</p> </attribute> + <attribute name="groups" required="false"> + <p>JSSE only.</p> + <p>Allows only allowing certain named groups. The value should be a case + sensitive comma separated list of the names of the groups.</p> + <p>. If not specified, the default named groups of the provider will be + used, and any named groups specified by the client will be passed to it. + </p> + </attribute> + <attribute name="honorCipherOrder" required="false"> <p>Set to <code>true</code> to enforce the server's cipher order (from the <code>ciphers</code> setting) instead of allowing --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
