You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ds...@apache.org on 2026/10/21 19:00:15 UTC
(tomcat) branch main updated: Select a JSSE certificate valid for TLS server authentication
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 <ji...@gmail.com>
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.
---
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: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
Re: (tomcat) branch main updated: Select a JSSE certificate valid for TLS server authentication
Posted by Dimitris Soumis <ds...@apache.org>.
On Thu, Oct 22, 2026 at 7:50 PM Mark Thomas <ma...@apache.org> wrote:
> On 22/10/2026 17:31, Dimitris Soumis wrote:
> > On Wed, Oct 21, 2026 at 11:24 PM Mark Thomas <ma...@apache.org> wrote:
> >
> >> On 21/10/2026 20:00, dsoumis@apache.org 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 <ji...@gmail.com>
> >>> 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/security-dev@openjdk.org/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: dev-unsubscribe@tomcat.apache.org
> >>> For additional commands, e-mail: dev-help@tomcat.apache.org
> >>>
> >>
> >>
> >> ---------------------------------------------------------------------
> >> To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
> >> For additional commands, e-mail: dev-help@tomcat.apache.org
> >
> >
> > Dimitris
> >
>
>
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
> For additional commands, e-mail: dev-help@tomcat.apache.org
>
>
Re: (tomcat) branch main updated: Select a JSSE certificate valid for TLS server authentication
Posted by Mark Thomas <ma...@apache.org>.
On 22/10/2026 17:31, Dimitris Soumis wrote:
> On Wed, Oct 21, 2026 at 11:24 PM Mark Thomas <ma...@apache.org> wrote:
>
>> On 21/10/2026 20:00, dsoumis@apache.org 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 <ji...@gmail.com>
>>> 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
>
>>
>> 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: dev-unsubscribe@tomcat.apache.org
>>> For additional commands, e-mail: dev-help@tomcat.apache.org
>>>
>>
>>
>> ---------------------------------------------------------------------
>> To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
>> For additional commands, e-mail: dev-help@tomcat.apache.org
>
>
> Dimitris
>
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
Re: (tomcat) branch main updated: Select a JSSE certificate valid for TLS server authentication
Posted by Dimitris Soumis <ds...@apache.org>.
On Wed, Oct 21, 2026 at 11:24 PM Mark Thomas <ma...@apache.org> wrote:
> On 21/10/2026 20:00, dsoumis@apache.org 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 <ji...@gmail.com>
> > 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.
>
> 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: dev-unsubscribe@tomcat.apache.org
> > For additional commands, e-mail: dev-help@tomcat.apache.org
> >
>
>
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
> For additional commands, e-mail: dev-help@tomcat.apache.org
Dimitris
Re: (tomcat) branch main updated: Select a JSSE certificate valid for TLS server authentication
Posted by Mark Thomas <ma...@apache.org>.
On 21/10/2026 20:00, dsoumis@apache.org 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 <ji...@gmail.com>
> 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() )
Do OpenSSL/Native and OpenSSL/FFM connectors configured with a keystore
use this new selection process?
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: dev-unsubscribe@tomcat.apache.org
> For additional commands, e-mail: dev-help@tomcat.apache.org
>
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org