On Thu, Oct 22, 2026 at 7:50 PM Mark Thomas <[email protected]> wrote:
> On 22/10/2026 17:31, Dimitris Soumis wrote: > > On Wed, Oct 21, 2026 at 11:24 PM Mark Thomas <[email protected]> wrote: > > > >> On 21/10/2026 20:00, [email protected] wrote: > >>> This is an automated email from the ASF dual-hosted git repository. > >>> > >>> dsoumis pushed a commit to branch main > >>> in repository https://gitbox.apache.org/repos/asf/tomcat.git > >>> > >>> > >>> The following commit(s) were added to refs/heads/main by this push: > >>> new f35fac5048 Select a JSSE certificate valid for TLS server > >> authentication > >>> f35fac5048 is described below > >>> > >>> commit f35fac504827b3c26a43b658e7c6ccee40be4721 > >>> Author: Dimitrios Soumis <[email protected]> > >>> AuthorDate: Wed Oct 21 22:00:09 2026 +0300 > >>> > >>> Select a JSSE certificate valid for TLS server authentication > >>> > >>> When configuring a JSSE Certificate from a key store that exposes > >> several > >>> certificates sharing a Subject, ignore those that are not valid > for > >> TLS > >>> server authentication (by extended key usage) so a usable > >> certificate is > >>> selected rather than relying on the key store alias alone. > >> > >> -1 to back-port without further changes. > >> > >> Cert with malformed EKU nows triggers start-up failure. The cert should > >> just be ignored. > >> > >> The configured alias should not be ignored. If it is configued it should > >> be used even if the cert is not valid and start-up failed. Ignoring the > >> alias also triggers all sorts of edge cases (look for uses of > >> getCertificateKeyAlias() ) > > > > Mark, > > > > I have addressed this again on > https://github.com/apache/tomcat/pull/1098 > > taking your feedback into consideration. > > Thanks. I just took a quick look and provided some feedback. Let me know > if it doesn't make sense. The good news is we can fake a keystore that > looks like an MS store with duplicate keys. > > Mark > I investigated this, as described in a comment in https://github.com/apache/tomcat/pull/1098 TL;DR: I see this as an OpenJDK issue and sent the following email: https://mail.openjdk.org/archives/list/[email protected]/thread/XUCB6BURVVSJEFJMCEHP34XNUOSPVAGR/ Dimitris > > > > >> > >> Do OpenSSL/Native and OpenSSL/FFM connectors configured with a keystore > >> use this new selection process? > >> > > > > I added those to the test class, my bad. > > > > > >> > >> To sum up, I think the only change that should happen here is that > >> certificates with invalid EKU should be ignored. Nothing else should > >> change. > >> > >> Mark > >> > >> > >> > >> > >> > >>> --- > >>> java/org/apache/tomcat/util/net/Constants.java | 7 + > >>> .../apache/tomcat/util/net/LocalStrings.properties | 4 + > >>> java/org/apache/tomcat/util/net/SSLUtilBase.java | 107 > +++++++++++++ > >>> .../util/net/TestSSLUtilBaseKeyUsageSelection.java | 178 > >> +++++++++++++++++++++ > >>> .../tomcat/util/net/TesterKeystoreGenerator.java | 84 ++++++++-- > >>> webapps/docs/changelog.xml | 7 + > >>> webapps/docs/config/http.xml | 16 +- > >>> webapps/docs/ssl-howto.xml | 94 +++++++++++ > >>> 8 files changed, 482 insertions(+), 15 deletions(-) > >>> > >>> diff --git a/java/org/apache/tomcat/util/net/Constants.java > >> b/java/org/apache/tomcat/util/net/Constants.java > >>> index 1669adbfb3..80fcbca6a9 100644 > >>> --- a/java/org/apache/tomcat/util/net/Constants.java > >>> +++ b/java/org/apache/tomcat/util/net/Constants.java > >>> @@ -68,6 +68,13 @@ public class Constants { > >>> */ > >>> public static final String SSL_PROTO_SSLv2Hello = "SSLv2Hello"; > >>> > >>> + /** > >>> + * EKU OIDs. > >>> + */ > >>> + public static final String SERVER_AUTH_EXTENDED_KEY_USAGE_OID = > >> "1.3.6.1.5.5.7.3.1"; > >>> + public static final String CLIENT_AUTH_EXTENDED_KEY_USAGE_OID = > >> "1.3.6.1.5.5.7.3.2"; > >>> + public static final String ANY_EXTENDED_KEY_USAGE_OID = > >> "2.5.29.37.0"; > >>> + > >>> /** > >>> * Private constructor to prevent instantiation. > >>> */ > >>> diff --git a/java/org/apache/tomcat/util/net/LocalStrings.properties > >> b/java/org/apache/tomcat/util/net/LocalStrings.properties > >>> index 6bc78573bd..db025be928 100644 > >>> --- a/java/org/apache/tomcat/util/net/LocalStrings.properties > >>> +++ b/java/org/apache/tomcat/util/net/LocalStrings.properties > >>> @@ -175,12 +175,16 @@ sslUtilBase.alias_no_key_entry=Alias name [{0}] > >> does not identify a key entry > >>> sslUtilBase.fipsPemNotSupported=FIPS enabled so PEM format > certificate > >> files are not supported. Configure the certificate using a key store > rather > >> than the certificateFile attribute > >>> sslUtilBase.invalidTrustManagerClassName=The trustManagerClassName > >> provided [{0}] does not implement javax.net.ssl.TrustManager > >>> sslUtilBase.keystore_load_failed=Failed to load keystore type [{0}] > >> with path [{1}] due to [{2}] > >>> +sslUtilBase.multipleServerAuthCerts=Starting from alias [{0}], > multiple > >> certificates valid for TLS server authentication were found: [{1}]. The > >> first one will be used. > >>> sslUtilBase.noCertFile=SSLHostConfig attribute certificateFile must > be > >> defined when using an SSL connector > >>> sslUtilBase.noCrlSupport=The truststoreProvider [{0}] does not > support > >> the certificateRevocationFile configuration option > >>> sslUtilBase.noKeys=No aliases for private keys found in key store > >>> +sslUtilBase.noServerAuthCert=No certificate valid for TLS server > >> authentication was found for alias [{0}]. The configured alias will be > used > >> unchanged. > >>> sslUtilBase.noVerificationDepth=The truststoreProvider [{0}] does > not > >> support the certificateVerificationDepth configuration option > >>> sslUtilBase.noneSupported=None of the [{0}] specified are supported > by > >> the SSL engine : [{1}] > >>> +sslUtilBase.selectedServerAuthCert=Selected the certificate with alias > >> [{0}] for TLS server authentication in preference to the configured > alias > >> [{1}] > >>> sslUtilBase.skipped=Tomcat interprets the [{0}] attribute in a > manner > >> consistent with the latest OpenSSL development branch. Some of the > >> specified [{0}] are not supported by the configured SSL engine for this > >> connector (which may use JSSE or an older OpenSSL version) and have been > >> skipped: [{1}] > >>> +sslUtilBase.skippedKeyUsage=Skipping certificate with alias [{0}] as > it > >> is not valid for TLS server authentication > >>> sslUtilBase.ssl3=SSLv3 has been explicitly enabled. This protocol is > >> known to be insecure. > >>> sslUtilBase.tls13.auth=The JSSE TLS 1.3 implementation does not > >> support post handshake authentication (PHA) and is therefore > incompatible > >> with optional certificate authentication > >>> sslUtilBase.trustedCertNotChecked=The validity dates of the trusted > >> certificate with alias [{0}] were not checked as the certificate was of > an > >> unknown type > >>> diff --git a/java/org/apache/tomcat/util/net/SSLUtilBase.java > >> b/java/org/apache/tomcat/util/net/SSLUtilBase.java > >>> index 68ac2ecbb8..6c1903d157 100644 > >>> --- a/java/org/apache/tomcat/util/net/SSLUtilBase.java > >>> +++ b/java/org/apache/tomcat/util/net/SSLUtilBase.java > >>> @@ -25,6 +25,7 @@ import java.nio.charset.StandardCharsets; > >>> import java.security.DomainLoadStoreParameter; > >>> import java.security.Key; > >>> import java.security.KeyStore; > >>> +import java.security.KeyStoreException; > >>> import java.security.cert.CRL; > >>> import java.security.cert.CRLException; > >>> import java.security.cert.CertPathParameters; > >>> @@ -36,6 +37,7 @@ import java.security.cert.CertificateException; > >>> import java.security.cert.CertificateExpiredException; > >>> import java.security.cert.CertificateFactory; > >>> import java.security.cert.CertificateNotYetValidException; > >>> +import java.security.cert.CertificateParsingException; > >>> import java.security.cert.CollectionCertStoreParameters; > >>> import java.security.cert.PKIXBuilderParameters; > >>> import java.security.cert.PKIXRevocationChecker; > >>> @@ -59,6 +61,7 @@ import javax.net.ssl.SSLSessionContext; > >>> import javax.net.ssl.TrustManager; > >>> import javax.net.ssl.TrustManagerFactory; > >>> import javax.net.ssl.X509KeyManager; > >>> +import javax.security.auth.x500.X500Principal; > >>> > >>> import org.apache.juli.logging.Log; > >>> import org.apache.juli.logging.LogFactory; > >>> @@ -411,6 +414,8 @@ public abstract class SSLUtilBase implements > SSLUtil > >> { > >>> } > >>> } > >>> > >>> + keyAlias = selectServerAuthAlias(ks, keyAlias, > >> certificate.getCertificateKeyAlias() != null); > >>> + > >>> Key k = ks.getKey(keyAlias, keyPassArray); > >>> if (k != null && > >> !"DKS".equalsIgnoreCase(certificate.getCertificateKeystoreType()) && > >>> "PKCS#8".equalsIgnoreCase(k.getFormat())) { > >>> @@ -450,6 +455,108 @@ public abstract class SSLUtilBase implements > >> SSLUtil { > >>> } > >>> > >>> > >>> + /* > >>> + * Prefer a key entry whose certificate is usable for TLS server > >> authentication. This handles key stores that can > >>> + * expose several certificates sharing a Subject (and therefore > >> colliding on similar aliases). > >>> + * The certificate currently identified by the alias provides the > >> Subject to match. > >>> + * When no alias was configured every key entry is a candidate. > >>> + * When no usable certificate is found, the configured alias is > >> returned unchanged. > >>> + */ > >>> + private String selectServerAuthAlias(KeyStore ks, String > >> currentAlias, boolean aliasConfigured) > >>> + throws KeyStoreException, CertificateParsingException { > >>> + > >>> + Certificate current = ks.getCertificate(currentAlias); > >>> + X509Certificate currentX509 = current instanceof > >> X509Certificate x509 ? x509 : null; > >>> + > >>> + // If the current selection is already usable there is nothing > >> to do. > >>> + if (currentX509 != null && isServerAuthCapable(currentX509)) { > >>> + return currentAlias; > >>> + } > >>> + > >>> + X500Principal subject = currentX509 == null ? null : > >> currentX509.getSubjectX500Principal(); > >>> + > >>> + List<String> matches = new ArrayList<>(); > >>> + // Defense-in-depth so no misbehaving provider can ever hang a > >> TLS-config path. > >>> + int remaining = ks.size(); > >>> + Enumeration<String> aliases = ks.aliases(); > >>> + while (aliases.hasMoreElements() && remaining-- > 0) { > >>> + String alias = aliases.nextElement(); > >>> + if (!ks.isKeyEntry(alias)) { > >>> + continue; > >>> + } > >>> + Certificate candidate = ks.getCertificate(alias); > >>> + if (!(candidate instanceof X509Certificate > x509Candidate)) { > >>> + continue; > >>> + } > >>> + // When the user named an alias, only consider > certificates > >> that share its Subject. > >>> + if (aliasConfigured && subject != null && > >> !subject.equals(x509Candidate.getSubjectX500Principal())) { > >>> + continue; > >>> + } > >>> + if (isServerAuthCapable(x509Candidate)) { > >>> + matches.add(alias); > >>> + } else if (log.isDebugEnabled()) { > >>> + log.debug(sm.getString("sslUtilBase.skippedKeyUsage", > >> alias)); > >>> + } > >>> + } > >>> + > >>> + if (matches.isEmpty()) { > >>> + if (log.isDebugEnabled()) { > >>> + log.debug(sm.getString("sslUtilBase.noServerAuthCert", > >> currentAlias)); > >>> + } > >>> + return currentAlias; > >>> + } > >>> + if (matches.size() > 1 && log.isDebugEnabled()) { > >>> + > >> log.debug(sm.getString("sslUtilBase.multipleServerAuthCerts", > currentAlias, > >> matches)); > >>> + } > >>> + String selected = matches.get(0); > >>> + if (!selected.equals(currentAlias) && log.isDebugEnabled()) { > >>> + > >> log.debug(sm.getString("sslUtilBase.selectedServerAuthCert", selected, > >> currentAlias)); > >>> + } > >>> + return selected; > >>> + } > >>> + > >>> + > >>> + /** > >>> + * Determines whether the given certificate may be used for TLS > >> server authentication, based on its Extended Key > >>> + * Usage (EKU) extension. Called by {@link #selectServerAuthAlias} > >> to choose between certificates that share a > >>> + * Subject. > >>> + * <p> > >>> + * The policy is deliberately lenient about absent usage and > strict > >> about declared usage: > >>> + * <ul> > >>> + * <li>A certificate with <strong>no EKU extension</strong> > ({@link > >> X509Certificate#getExtendedKeyUsage()} returns > >>> + * {@code null}) is treated as usable. An absent EKU places no > >> restriction on the certificate's purpose - the > >>> + * way TLS implementations generally interpret such > >> certificates - so rejecting them would needlessly exclude > >>> + * the many server certificates issued without an EKU.</li> > >>> + * <li>A certificate that <strong>does</strong> carry an EKU > >> extension is usable only if that extension explicitly > >>> + * lists the {@code id-kp-serverAuth} OID ({@value > >> Constants#SERVER_AUTH_EXTENDED_KEY_USAGE_OID}) or the > >>> + * {@code anyExtendedKeyUsage} OID ({@value > >> Constants#ANY_EXTENDED_KEY_USAGE_OID}). Any other declared usage > >>> + * - for example a certificate limited to client > >> authentication, such as a Remote Desktop authentication > >>> + * certificate - is rejected, which is what lets this feature > >> skip the wrong certificate.</li> > >>> + * </ul> > >>> + * > >>> + * @param cert the certificate to test > >>> + * > >>> + * @return {@code true} if the certificate may be used for TLS > >> server authentication > >>> + * > >>> + * @throws CertificateParsingException if the Extended Key Usage > >> extension cannot be decoded > >>> + */ > >>> + private static boolean isServerAuthCapable(X509Certificate cert) > >> throws CertificateParsingException { > >>> + List<String> objectIdentifiers = cert.getExtendedKeyUsage(); > >>> + > >>> + if (objectIdentifiers == null) { > >>> + return true; > >>> + } > >>> + > >>> + for (String objectIdentifier : objectIdentifiers) { > >>> + if > >> (objectIdentifier.equals(Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID) > || > >>> + > >> objectIdentifier.equals(Constants.ANY_EXTENDED_KEY_USAGE_OID)) { > >>> + return true; > >>> + } > >>> + } > >>> + > >>> + return false; > >>> + } > >>> + > >>> @Override > >>> public String[] getEnabledProtocols() { > >>> return enabledProtocols; > >>> diff --git > >> a/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java > >> b/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java > >>> new file mode 100644 > >>> index 0000000000..a64224c80b > >>> --- /dev/null > >>> +++ > >> b/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java > >>> @@ -0,0 +1,178 @@ > >>> +/* > >>> + * 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.net; > >>> + > >>> +import java.io.File; > >>> +import java.security.cert.X509Certificate; > >>> +import java.util.List; > >>> + > >>> +import javax.net.ssl.X509KeyManager; > >>> + > >>> +import org.junit.Assert; > >>> +import org.junit.Test; > >>> + > >>> +import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; > >>> +import org.apache.tomcat.util.net > >> .TesterKeystoreGenerator.CertificateExtensionsCustomizer; > >>> +import org.apache.tomcat.util.net > >> .TesterKeystoreGenerator.KeystoreEntry; > >>> +import org.apache.tomcat.util.net.jsse.JSSEUtil; > >>> +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; > >>> +import org.bouncycastle.asn1.x509.Extension; > >>> +import org.bouncycastle.asn1.x509.KeyPurposeId; > >>> + > >>> +/** > >>> + * Tests selection of a TLS server authentication certificate when a > >> key store holds several certificates that share a > >>> + * Subject. > >>> + */ > >>> +public class TestSSLUtilBaseKeyUsageSelection { > >>> + > >>> + private static final String HOST = "localhost"; > >>> + > >>> + @SuppressWarnings("unused") > >>> + private static final CertificateExtensionsCustomizer SERVER_AUTH = > >> (keyPair, builder) -> builder > >>> + .addExtension(Extension.extendedKeyUsage, true, new > >> ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)); > >>> + @SuppressWarnings("unused") > >>> + private static final CertificateExtensionsCustomizer CLIENT_AUTH = > >> (keyPair, builder) -> builder > >>> + .addExtension(Extension.extendedKeyUsage, true, new > >> ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth)); > >>> + @SuppressWarnings("unused") > >>> + private static final CertificateExtensionsCustomizer ANY_AUTH = > >> (keyPair, builder) -> builder > >>> + .addExtension(Extension.extendedKeyUsage, true, new > >> ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage)); > >>> + > >>> + > >>> + private static KeystoreEntry key(String alias, > >> CertificateExtensionsCustomizer eku) { > >>> + return KeystoreEntry.key(alias, HOST, eku); > >>> + } > >>> + > >>> + > >>> + /* > >>> + * Builds a key store from the given entries, configures a JSSE > >> certificate pointed at alias (null for none), and > >>> + * returns the leaf certificate the key manager actually presents. > >>> + */ > >>> + private static X509Certificate present(String alias, > >> KeystoreEntry... entries) throws Exception { > >>> + File keystore = > >> TesterKeystoreGenerator.generateKeystore(List.of(entries)); > >>> + > >>> + SSLHostConfig hostConfig = new SSLHostConfig(); > >>> + SSLHostConfigCertificate certificate = new > >> SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); > >>> + > >> certificate.setCertificateKeystoreFile(keystore.getAbsolutePath()); > >>> + > >> certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS); > >>> + if (alias != null) { > >>> + certificate.setCertificateKeyAlias(alias); > >>> + } > >>> + > >>> + X509KeyManager keyManager = (X509KeyManager) new > >> JSSEUtil(certificate).getKeyManagers()[0]; > >>> + X509Certificate[] chain = > >> keyManager.getCertificateChain(keyManager.chooseServerAlias("RSA", null, > >> null)); > >>> + Assert.assertNotNull("Key manager returned no certificate > >> chain", chain); > >>> + return chain[0]; > >>> + } > >>> + > >>> + > >>> + private static void assertEku(X509Certificate certificate, String > >> oid) throws Exception { > >>> + List<String> extendedKeyUsage = > >> certificate.getExtendedKeyUsage(); > >>> + Assert.assertNotNull("Presented certificate has no Extended > Key > >> Usage", extendedKeyUsage); > >>> + Assert.assertTrue("Presented certificate must be valid for > >> Extended Key Usage " + oid, > >>> + extendedKeyUsage.contains(oid)); > >>> + } > >>> + > >>> + > >>> + @Test > >>> + public void > >> testSelectsServerAuthCertificateOverConfiguredClientAuthAlias() throws > >> Exception { > >>> + X509Certificate presented = present(HOST, > >>> + key(HOST, CLIENT_AUTH), > >>> + key("localhost-server", SERVER_AUTH)); > >>> + assertEku(presented, > >> Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); > >>> + } > >>> + > >>> + > >>> + @Test > >>> + public void testKeepsConfiguredAliasWhenItIsAlreadyServerAuth() > >> throws Exception { > >>> + X509Certificate presented = present(HOST, > >>> + key(HOST, SERVER_AUTH), > >>> + key("localhost-client", CLIENT_AUTH)); > >>> + assertEku(presented, > >> Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); > >>> + } > >>> + > >>> + > >>> + @Test > >>> + public void testSelectsCertificateWithAnyExtendedKeyUsage() throws > >> Exception { > >>> + X509Certificate presented = present(HOST, > >>> + key(HOST, CLIENT_AUTH), > >>> + key("localhost-any", ANY_AUTH)); > >>> + assertEku(presented, Constants.ANY_EXTENDED_KEY_USAGE_OID); > >>> + } > >>> + > >>> + > >>> + @Test > >>> + public void testSelectsCertificateWithoutExtendedKeyUsage() throws > >> Exception { > >>> + X509Certificate presented = present(HOST, > >>> + key(HOST, CLIENT_AUTH), > >>> + key("localhost-none", null)); > >>> + Assert.assertNull("A certificate with no Extended Key Usage is > >> unrestricted and must be selectable", > >>> + presented.getExtendedKeyUsage()); > >>> + } > >>> + > >>> + > >>> + @Test > >>> + public void > testSelectsServerAuthCertificateWhenNoAliasConfigured() > >> throws Exception { > >>> + X509Certificate presented = present(null, > >>> + key("first", CLIENT_AUTH), > >>> + key("second", SERVER_AUTH)); > >>> + assertEku(presented, > >> Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); > >>> + } > >>> + > >>> + > >>> + @Test > >>> + public void testSelectsServerAuthCertificateWhenMultipleMatch() > >> throws Exception { > >>> + // Two server-auth certificates share the Subject. Which one > >> wins depends on key-store enumeration order, > >>> + // which JKS does not guarantee, so this only asserts that the > >> ambiguity resolves to a server-auth > >>> + // certificate rather than the client-auth one - not which of > >> the two is chosen. > >>> + X509Certificate presented = present(HOST, > >>> + key(HOST, CLIENT_AUTH), > >>> + key("localhost-s1", SERVER_AUTH), > >>> + key("localhost-s2", SERVER_AUTH)); > >>> + assertEku(presented, > >> Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); > >>> + } > >>> + > >>> + > >>> + @Test > >>> + public void > >> testFallsBackToConfiguredAliasWhenNoServerAuthCertificate() throws > >> Exception { > >>> + X509Certificate presented = present(HOST, > >>> + key(HOST, CLIENT_AUTH), > >>> + key("localhost-2", CLIENT_AUTH)); > >>> + assertEku(presented, > >> Constants.CLIENT_AUTH_EXTENDED_KEY_USAGE_OID); > >>> + } > >>> + > >>> + > >>> + @Test > >>> + public void testIgnoresServerAuthCertificateWithDifferentSubject() > >> throws Exception { > >>> + X509Certificate presented = present(HOST, > >>> + key(HOST, CLIENT_AUTH), > >>> + KeystoreEntry.key("other-server", "other.example", > >> SERVER_AUTH)); > >>> + assertEku(presented, > >> Constants.CLIENT_AUTH_EXTENDED_KEY_USAGE_OID); > >>> + } > >>> + > >>> + > >>> + @Test > >>> + public void testIgnoresTrustedCertificateEntries() throws > Exception > >> { > >>> + // The only server-auth-capable certificate is a trusted > >> (keyless) entry, so it cannot be used as a server key. > >>> + // Selection must skip it and fall back to the configured key > >> entry. If trusted entries were NOT skipped, the > >>> + // keyless "ca" would be chosen and the resulting key manager > >> would have no chain for it. > >>> + X509Certificate presented = present(HOST, > >>> + KeystoreEntry.trusted("ca", HOST, SERVER_AUTH), > >>> + key(HOST, CLIENT_AUTH)); > >>> + assertEku(presented, > >> Constants.CLIENT_AUTH_EXTENDED_KEY_USAGE_OID); > >>> + } > >>> +} > >>> diff --git > >> a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java > >> b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java > >>> index 00f1772fcc..480ddf045b 100644 > >>> --- a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java > >>> +++ b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java > >>> @@ -26,6 +26,7 @@ import java.security.KeyPairGenerator; > >>> import java.security.KeyStore; > >>> import java.security.cert.X509Certificate; > >>> import java.util.Date; > >>> +import java.util.List; > >>> > >>> import org.bouncycastle.asn1.x500.X500Name; > >>> import org.bouncycastle.asn1.x509.Extension; > >>> @@ -50,25 +51,84 @@ public final class TesterKeystoreGenerator { > >>> } > >>> > >>> /** > >>> - * Generate a temporary JKS keystore containing a self-signed RSA > >> certificate. > >>> + * Generate a temporary JKS keystore containing a single > >> self-signed RSA certificate. > >>> * > >>> - * @param cn the Common Name for the certificate subject > >>> - * @param alias the keystore alias for the key entry > >>> - * @param sanNames DNS Subject Alternative Names to include, or > >> {@code null} for none > >>> - * @param customizer callback to add extensions to the > certificate, > >> or {@code null} for none. > >>> + * @param cn the Common Name for the certificate subject > >>> + * @param alias the keystore alias for the key entry > >>> + * @param sanNames DNS Subject Alternative Names to include, or > >> {@code null} for none > >>> + * @param customizer callback to add extensions to the > certificate, > >> or {@code null} for none > >>> * > >>> - * @return a temporary keystore file with password {@link > >> TesterSupport#JKS_PASS} > >>> + * @return a temporary keystore file with password {@link > >> TesterSupport#JKS_PASS} > >>> * > >>> - * @throws Exception if certificate generation or keystore > >> creation fails > >>> + * @throws Exception if certificate generation or keystore > creation > >> fails > >>> */ > >>> public static File generateKeystore(String cn, String alias, > >> String[] sanNames, > >>> > >> CertificateExtensionsCustomizer customizer) throws Exception { > >>> + CertAndKey generated = generateSelfSignedCertificate(cn, > >> sanNames, customizer); > >>> + > >>> + KeyStore ks = KeyStore.getInstance("JKS"); > >>> + ks.load(null, null); > >>> + ks.setKeyEntry(alias, generated.keyPair().getPrivate(), > >> TesterSupport.JKS_PASS.toCharArray(), > >>> + new X509Certificate[] { generated.certificate() }); > >>> + > >>> + return storeToTempFile(ks); > >>> + } > >>> + > >>> + /** > >>> + * Generate a temporary JKS keystore from an explicit list of > >> entries. Each entry has its own Common Name and may be > >>> + * either a private key entry or a trusted certificate entry, > which > >> lets tests reproduce stores that mix certificates > >>> + * with different Subjects and certificate-only (keyless) entries. > >>> + * > >>> + * @param entries the entries to add, in order > >>> + * > >>> + * @return a temporary keystore file with password {@link > >> TesterSupport#JKS_PASS} > >>> + * > >>> + * @throws Exception if certificate generation or keystore > creation > >> fails > >>> + */ > >>> + public static File generateKeystore(List<KeystoreEntry> entries) > >> throws Exception { > >>> + KeyStore ks = KeyStore.getInstance("JKS"); > >>> + ks.load(null, null); > >>> + > >>> + for (KeystoreEntry entry : entries) { > >>> + CertAndKey generated = generateSelfSignedCertificate( > >> entry.cn(), null, entry.customizer()); > >>> + if (entry.keyEntry()) { > >>> + ks.setKeyEntry(entry.alias(), > >> generated.keyPair().getPrivate(), TesterSupport.JKS_PASS.toCharArray(), > >>> + new X509Certificate[] { > generated.certificate() > >> }); > >>> + } else { > >>> + ks.setCertificateEntry(entry.alias(), > >> generated.certificate()); > >>> + } > >>> + } > >>> + > >>> + return storeToTempFile(ks); > >>> + } > >>> + > >>> + /** > >>> + * A single entry for {@link #generateKeystore(List)}. > >>> + * > >>> + * @param alias the keystore alias > >>> + * @param cn the certificate Subject Common Name > >>> + * @param keyEntry {@code true} for a private key entry, {@code > >> false} for a trusted certificate entry > >>> + * @param customizer extensions to add to the certificate, or > >> {@code null} for none > >>> + */ > >>> + public record KeystoreEntry(String alias, String cn, boolean > >> keyEntry, CertificateExtensionsCustomizer customizer) { > >>> + > >>> + public static KeystoreEntry key(String alias, String cn, > >> CertificateExtensionsCustomizer customizer) { > >>> + return new KeystoreEntry(alias, cn, true, customizer); > >>> + } > >>> + > >>> + public static KeystoreEntry trusted(String alias, String cn, > >> CertificateExtensionsCustomizer customizer) { > >>> + return new KeystoreEntry(alias, cn, false, customizer); > >>> + } > >>> + } > >>> + > >>> + private static CertAndKey generateSelfSignedCertificate(String cn, > >> String[] sanNames, > >>> + > >> CertificateExtensionsCustomizer customizer) throws Exception { > >>> KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); > >>> kpg.initialize(4096); > >>> KeyPair keyPair = kpg.generateKeyPair(); > >>> > >>> X500Name subject = new X500Name("CN=" + cn); > >>> - BigInteger serial = > >> BigInteger.valueOf(System.currentTimeMillis()); > >>> + BigInteger serial = BigInteger.valueOf(System.nanoTime()); > >>> long oneDay = 86400000L; > >>> Date notBefore = new Date(System.currentTimeMillis() - > oneDay); > >>> Date notAfter = new Date(System.currentTimeMillis() + 365L * > >> oneDay); > >>> @@ -90,17 +150,17 @@ public final class TesterKeystoreGenerator { > >>> > >>> ContentSigner signer = new > >> JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()); > >>> X509Certificate certificate = new > >> JcaX509CertificateConverter().getCertificate(certBuilder.build(signer)); > >>> + return new CertAndKey(keyPair, certificate); > >>> + } > >>> > >>> - KeyStore ks = KeyStore.getInstance("JKS"); > >>> - ks.load(null, null); > >>> - ks.setKeyEntry(alias, keyPair.getPrivate(), > >> TesterSupport.JKS_PASS.toCharArray(), new X509Certificate[] { > certificate > >> }); > >>> + private record CertAndKey(KeyPair keyPair, X509Certificate > >> certificate) {} > >>> > >>> + private static File storeToTempFile(KeyStore ks) throws Exception > { > >>> File keystoreFile = File.createTempFile("test-cert-", > ".jks"); > >>> keystoreFile.deleteOnExit(); > >>> try (FileOutputStream fos = new > >> FileOutputStream(keystoreFile)) { > >>> ks.store(fos, TesterSupport.JKS_PASS.toCharArray()); > >>> } > >>> - > >>> return keystoreFile; > >>> } > >>> > >>> diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml > >>> index b7011ac705..38e9f53c50 100644 > >>> --- a/webapps/docs/changelog.xml > >>> +++ b/webapps/docs/changelog.xml > >>> @@ -484,6 +484,13 @@ > >>> drop TLS 1.3 when rebuilding the list of enabled protocols > >> during > >>> initialization. (dsoumis) > >>> </fix> > >>> + <update> > >>> + When configuring a JSSE <code>Certificate</code> from a key > >> store that > >>> + exposes several certificates sharing a Subject, ignore > >> certificates > >>> + that are not valid for TLS server authentication so that a > >> usable > >>> + certificate is selected rather than relying on the key store > to > >>> + return the correct one for the configured alias. (dsoumis) > >>> + </update> > >>> <!-- Entries for backport and removal before 12.0.0-M1 below > >> this line --> > >>> <fix> > >>> Keep the asynchronous timeout in place until the timeout > event > >> has > >>> diff --git a/webapps/docs/config/http.xml > b/webapps/docs/config/http.xml > >>> index 98607caa22..e244572972 100644 > >>> --- a/webapps/docs/config/http.xml > >>> +++ b/webapps/docs/config/http.xml > >>> @@ -1670,6 +1670,12 @@ > >>> the same order as they were added. If more than one key is > >> present in the > >>> keystore it is strongly recommended that a keyAlias is > >> configured to > >>> ensure that the correct key is used.</p> > >>> + <p>When several certificates in the key store share a Subject, > >> Tomcat automatically ignores any that are > >>> + not valid for TLS server authentication - i.e. whose Extended > Key > >> Usage > >>> + excludes both <code>serverAuth</code> and > >> <code>anyExtendedKeyUsage</code> - > >>> + so a usable certificate is selected. Certificates with no > >> Extended Key Usage > >>> + extension are unrestricted and remain eligible. If none qualify, > >> the > >>> + configured alias is used unchanged.</p> > >>> </attribute> > >>> > >>> <attribute name="certificateKeyFile" required="false"> > >>> @@ -1859,9 +1865,13 @@ > >>> <subsection name="Key store types"> > >>> > >>> <p>In addition to the standard key store types (JKS and PKCS12), > >> most Java > >>> - runtimes support additional key store types such as Windows-ROOT, > >>> - Windows-My, DKS as well as hardware security modules. Generally, > to > >> use > >>> - these additional keystore types with a TLS Connector in > Tomcat:</p> > >>> + runtimes support additional key store types such as Windows-MY, > >> Windows-ROOT, > >>> + DKS as well as hardware security modules. On Windows, Windows-MY > and > >>> + Windows-ROOT access the current user's certificate store, while > >>> + Windows-MY-LOCALMACHINE and Windows-ROOT-LOCALMACHINE access the > >> local machine > >>> + store (reading the local machine store from a non-elevated process > >> requires > >>> + Java 17.0.17, 21.0.9, 24 or later). Generally, to use these > >> additional key > >>> + store types with a TLS Connector in Tomcat:</p> > >>> > >>> <ul> > >>> <li>Set the certificateKeystoreType and/or truststoreType > >> Connector > >>> diff --git a/webapps/docs/ssl-howto.xml b/webapps/docs/ssl-howto.xml > >>> index 8eee9f1210..af291b8714 100644 > >>> --- a/webapps/docs/ssl-howto.xml > >>> +++ b/webapps/docs/ssl-howto.xml > >>> @@ -446,6 +446,100 @@ mailing list</a>.</p> > >>> </subsection> > >>> </section> > >>> > >>> +<section name="Automatic certificate selection and reloading"> > >>> + > >>> +<p>When a key store contains more than one certificate, Tomcat must > >> choose which > >>> +one to present to clients, and certificates must be replaced before > >> they expire. > >>> +Two features help to automate this so that a single configuration can > >> be deployed > >>> +to many hosts without per-host customization: selection of a > >> certificate by key > >>> +usage, and automatic reloading of renewed certificates.</p> > >>> + > >>> +<subsection name="Selecting a certificate by key usage"> > >>> + > >>> +<p>When a key store holds several certificates - for example one > >> intended for TLS > >>> +server authentication alongside one intended for another purpose, > which > >> may share > >>> +the same Subject - Tomcat selects a certificate that is valid for TLS > >> server > >>> +authentication. A certificate qualifies if its Extended Key Usage > >> includes > >>> +<code>serverAuth</code> or <code>anyExtendedKeyUsage</code>, or if it > >> has no > >>> +Extended Key Usage extension at all. Certificates that declare a > >> different purpose, > >>> +such as client authentication, are ignored.</p> > >>> + > >>> +<p>Because of this, it is usually not necessary to set > >>> +<strong>certificateKeyAlias</strong>: Tomcat picks a usable server > >> certificate > >>> +automatically. When <strong>certificateKeyAlias</strong> is set, only > >> certificates > >>> +that share the Subject of that alias are considered.</p> > >>> + > >>> +<p>If no certificate valid for server authentication is found, the > >> configured > >>> +alias (or, when none is configured, the first key in the store) is > used > >> unchanged, > >>> +so existing configurations are not affected.</p> > >>> + > >>> +</subsection> > >>> + > >>> +<subsection name="Reloading certificates before they expire"> > >>> + > >>> +<p>When certificates are renewed by an external process the > >>> +TLS certificate reload listener can load the renewed certificate > without > >>> +restarting Tomcat. Add it to the <strong>Server</strong> element:</p> > >>> + > >>> +<source><![CDATA[<Server ...> > >>> + <Listener > >> className="org.apache.catalina.security.TLSCertificateReloadListener" /> > >>> + ... > >>> +</Server>]]></source> > >>> + > >>> +<p>The listener periodically checks each certificate and, when one is > >> within > >>> +<code>daysBefore</code> days of expiry (14 by default), reloads the > >> associated > >>> +<code>SSLHostConfig</code> so that the renewed certificate is picked > >> up. See the > >>> +<a href="config/listeners.html">listener documentation</a> for the > >> available > >>> +attributes.</p> > >>> + > >>> +</subsection> > >>> + > >>> +<subsection name="Example: the Windows certificate store"> > >>> + > >>> +<p>On Windows, Tomcat can read certificates directly from the Windows > >> certificate > >>> +store. Set <strong>certificateKeystoreType</strong> to > >>> +<code>Windows-MY-LOCALMACHINE</code> for the local machine store, or > to > >>> +<code>Windows-MY</code> for the current user's store. Reading the > local > >> machine > >>> +store from a Tomcat service that is not running with elevated > >> privileges requires > >>> +Java 17.0.17, 21.0.9, 24, or a later release; on earlier releases the > >> service must > >>> +run with elevated privileges. This works well with AD CS > >> autoenrollment, which > >>> +issues and renews the machine's certificates automatically.</p> > >>> + > >>> +<p>The local machine store usually contains several certificates > issued > >> for the > >>> +machine's name, such as a TLS server certificate and a Remote Desktop > >>> +authentication certificate. Because Tomcat selects the certificate > that > >> is valid > >>> +for server authentication, the same configuration can be deployed to > >> every host > >>> +with no <strong>certificateKeyAlias</strong>:</p> > >>> + > >>> +<source><![CDATA[<Connector port="8443" protocol="HTTP/1.1" > >>> + SSLEnabled="true" scheme="https" secure="true"> > >>> + <SSLHostConfig> > >>> + <Certificate certificateKeystoreType="Windows-MY-LOCALMACHINE" > >>> + certificateKeystoreFile="" > >>> + certificateKeystorePassword="" > >>> + protocols="TLSv1.2,TLSv1.3" /> > >>> + </SSLHostConfig> > >>> +</Connector>]]></source> > >>> + > >>> +<p>Combined with the reload listener described above this gives fully > >> automated > >>> +certificate handling: AD CS issues and renews the certificate, while > >> Tomcat > >>> +selects the correct one and reloads it before it expires.</p> > >>> + > >>> +<p>Two points to note:</p> > >>> + > >>> +<ul> > >>> + <li>The account running Tomcat must have read access to the > >> certificate's private > >>> + key. A renewed certificate may use a new key that does not > >> inherit this > >>> + access, so grant it as part of the enrollment or renewal > >> process.</li> > >>> + <li>If a host holds more than one certificate valid for server > >> authentication, > >>> + set a unique friendly name on the intended certificate and use > it > >> as the > >>> + <strong>certificateKeyAlias</strong>.</li> > >>> +</ul> > >>> + > >>> +</subsection> > >>> + > >>> +</section> > >>> + > >>> <section name="Using OCSP Certificates"> > >>> <p>Tomcat supports the Online Certificate Status Protocol (OCSP) to > >> verify the > >>> status of client provided certificates for a sub-set of connector > >>> > >>> > >>> --------------------------------------------------------------------- > >>> To unsubscribe, e-mail: [email protected] > >>> For additional commands, e-mail: [email protected] > >>> > >> > >> > >> --------------------------------------------------------------------- > >> To unsubscribe, e-mail: [email protected] > >> For additional commands, e-mail: [email protected] > > > > > > Dimitris > > > > > --------------------------------------------------------------------- > To unsubscribe, e-mail: [email protected] > For additional commands, e-mail: [email protected] > >
