On 21/10/2026 20:00, [email protected] wrote:
This is an automated email from the ASF dual-hosted git repository.

dsoumis pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
      new f35fac5048 Select a JSSE certificate valid for TLS server 
authentication
f35fac5048 is described below

commit f35fac504827b3c26a43b658e7c6ccee40be4721
Author: Dimitrios Soumis <[email protected]>
AuthorDate: Wed Oct 21 22:00:09 2026 +0300

     Select a JSSE certificate valid for TLS server authentication
When configuring a JSSE Certificate from a key store that exposes several
     certificates sharing a Subject, ignore those that are not valid for TLS
     server authentication (by extended key usage) so a usable certificate is
     selected rather than relying on the key store alias alone.

-1 to back-port without further changes.

Cert with malformed EKU nows triggers start-up failure. The cert should just be ignored.

The configured alias should not be ignored. If it is configued it should be used even if the cert is not valid and start-up failed. Ignoring the alias also triggers all sorts of edge cases (look for uses of getCertificateKeyAlias() )

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: [email protected]
For additional commands, e-mail: [email protected]



---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to