This is an automated email from the ASF dual-hosted git repository.

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

commit b1ec63c405f9ae6dd2c23805dffd359e0fc47fa5
Author: Dimitris Soumis <[email protected]>
AuthorDate: Thu Oct 22 18:33:53 2026 +0300

    Select a key store certificate valid for TLS server authentication
    
    A key store can hold more than one certificate for a host. For example, a 
Windows-MY store populated by AD CS autoenrollment may contain both a TLS 
server certificate and a client authentication certificate. When no key alias 
was configured, automatic selection used the first key entry regardless of its 
key usage and could pick a certificate that is not valid for TLS server 
authentication. When no certificateKeyAlias is configured, skip key entries 
whose certificate is not valid for T [...]
    
    A configured certificateKeyAlias is always used unchanged, even if its 
certificate is not valid for server authentication.
---
 java/org/apache/tomcat/util/net/Constants.java     |   6 +
 .../apache/tomcat/util/net/LocalStrings.properties |   1 +
 java/org/apache/tomcat/util/net/SSLUtilBase.java   |  59 +++-
 .../util/net/TestSSLUtilBaseKeyUsageSelection.java | 334 +++++++++++++++++++++
 .../tomcat/util/net/TesterKeystoreGenerator.java   | 130 +++++++-
 webapps/docs/changelog.xml                         |   9 +
 webapps/docs/config/http.xml                       |   9 +-
 webapps/docs/ssl-howto.xml                         |  19 +-
 8 files changed, 550 insertions(+), 17 deletions(-)

diff --git a/java/org/apache/tomcat/util/net/Constants.java 
b/java/org/apache/tomcat/util/net/Constants.java
index 701835b696..cfc9af27de 100644
--- a/java/org/apache/tomcat/util/net/Constants.java
+++ b/java/org/apache/tomcat/util/net/Constants.java
@@ -70,6 +70,12 @@ public class Constants {
 
     public static final boolean IS_SECURITY_ENABLED = 
(System.getSecurityManager() != null);
 
+    /**
+     * 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 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 b95a948cc3..9a85d9d2f0 100644
--- a/java/org/apache/tomcat/util/net/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/net/LocalStrings.properties
@@ -189,6 +189,7 @@ sslUtilBase.noKeys=No aliases for private keys found in key 
store
 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.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 f4ea8ba56f..875319bf59 100644
--- a/java/org/apache/tomcat/util/net/SSLUtilBase.java
+++ b/java/org/apache/tomcat/util/net/SSLUtilBase.java
@@ -36,6 +36,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;
@@ -402,10 +403,18 @@ public abstract class SSLUtilBase implements SSLUtil {
                     throw new IOException(sm.getString("sslUtilBase.noKeys"));
                 }
                 while (aliases.hasMoreElements() && keyAlias == null) {
-                    keyAlias = aliases.nextElement();
-                    if (!ks.isKeyEntry(keyAlias)) {
-                        keyAlias = null;
+                    String candidate = aliases.nextElement();
+                    if (!ks.isKeyEntry(candidate)) {
+                        continue;
                     }
+                    Certificate cert = ks.getCertificate(candidate);
+                    if (cert instanceof X509Certificate && 
!isServerAuthCapable((X509Certificate) cert)) {
+                        if (log.isDebugEnabled()) {
+                            
log.debug(sm.getString("sslUtilBase.skippedKeyUsage", candidate));
+                        }
+                        continue;
+                    }
+                    keyAlias = candidate;
                 }
                 if (keyAlias == null) {
                     throw new 
IOException(sm.getString("sslUtilBase.alias_no_key_entry", (Object) null));
@@ -451,6 +460,50 @@ public abstract class SSLUtilBase implements SSLUtil {
     }
 
 
+    /**
+     * Determines whether the given certificate may be used for TLS server 
authentication, based on its Extended Key
+     * Usage (EKU) extension. Used by {@link #getKeyManagers()} when 
auto-selecting a key entry so that certificates
+     * which are not valid for server authentication are ignored.
+     * <p>
+     * The policy is deliberately lenient about absent usage, strict about 
declared usage and tolerant of a malformed
+     * extension:
+     * <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
+     *     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 - is 
rejected.</li>
+     * <li>A certificate whose EKU extension <strong>cannot be 
decoded</strong> is ignored (treated as not usable).</li>
+     * </ul>
+     *
+     * @param cert the certificate to test
+     *
+     * @return {@code true} if the certificate may be used for TLS server 
authentication
+     */
+    private static boolean isServerAuthCapable(X509Certificate cert) {
+        List<String> objectIdentifiers;
+        try {
+            objectIdentifiers = cert.getExtendedKeyUsage();
+        } catch (CertificateParsingException e) {
+            return false;
+        }
+
+        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..bbb05485a1
--- /dev/null
+++ b/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java
@@ -0,0 +1,334 @@
+/*
+ * 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.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreSpi;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.net.ssl.X509KeyManager;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+
+import org.apache.catalina.startup.TomcatBaseTest;
+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.bouncycastle.asn1.x509.ExtendedKeyUsage;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
+import org.easymock.EasyMock;
+
+/**
+ * Tests that, when no alias is configured, a key entry whose certificate is 
valid for TLS server authentication is
+ * auto-selected and entries that are not valid (by Extended Key Usage) are 
ignored. A configured alias is always used
+ * unchanged, regardless of its Extended Key Usage.
+ */
+@RunWith(Parameterized.class)
+public class TestSSLUtilBaseKeyUsageSelection extends TomcatBaseTest {
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> parameters() {
+        return List.of(new Object[] { "JSSE", Boolean.FALSE,
+                "org.apache.tomcat.util.net.jsse.JSSEImplementation" },
+                new Object[] { "OpenSSL", Boolean.TRUE,
+                        
"org.apache.tomcat.util.net.openssl.OpenSSLImplementation" },
+                new Object[] { "OpenSSL-FFM", Boolean.TRUE,
+                        
"org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation" });
+    }
+
+    @Parameter(0)
+    public String connectorName;
+
+    @Parameter(1)
+    public boolean useOpenSSL;
+
+    @Parameter(2)
+    public String sslImplementationName;
+
+
+    @Before
+    public void configureSSLImplementation() throws Exception {
+        TesterSupport.configureSSLImplementation(getTomcatInstance(), 
sslImplementationName, useOpenSSL);
+        getTomcatInstance().getServer().init();
+    }
+
+    private static final String HOST = "localhost";
+
+    private static final String CLIENT_AUTH_EXTENDED_KEY_USAGE_OID = 
"1.3.6.1.5.5.7.3.2";
+
+    private static final CertificateExtensionsCustomizer SERVER_AUTH = 
(keyPair, builder) -> builder
+            .addExtension(Extension.extendedKeyUsage, true, new 
ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth));
+    private static final CertificateExtensionsCustomizer CLIENT_AUTH = 
(keyPair, builder) -> builder
+            .addExtension(Extension.extendedKeyUsage, true, new 
ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth));
+    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);
+    }
+
+
+    private X509Certificate present(String alias, KeystoreEntry... entries) 
throws Exception {
+        return present(alias, orderedKeystore(null, entries));
+    }
+
+
+    private X509Certificate present(String alias, KeyStore keystore) throws 
Exception {
+        X509KeyManager keyManager = (X509KeyManager) sslUtil(alias, 
keystore).getKeyManagers()[0];
+        X509Certificate[] chain = 
keyManager.getCertificateChain(keyManager.chooseServerAlias("RSA", null, null));
+        Assert.assertNotNull("Key manager returned no certificate chain", 
chain);
+        return chain[0];
+    }
+
+
+    private SSLUtil sslUtil(String alias, KeyStore keystore) throws Exception {
+
+        SSLHostConfig hostConfig = new SSLHostConfig();
+        SSLHostConfigCertificate certificate = new 
SSLHostConfigCertificate(hostConfig, Type.UNDEFINED);
+        certificate.setCertificateKeystore(keystore);
+        certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS);
+        if (alias != null) {
+            certificate.setCertificateKeyAlias(alias);
+        }
+
+        return sslUtil(certificate);
+    }
+
+
+    private SSLUtil sslUtil(SSLHostConfigCertificate certificate) throws 
Exception {
+        SSLUtil util = 
SSLImplementation.getInstance(sslImplementationName).getSSLUtil(certificate);
+        SSLHostConfig hostConfig = certificate.getSSLHostConfig();
+        hostConfig.setEnabledProtocols(util.getEnabledProtocols());
+        hostConfig.setEnabledCiphers(util.getEnabledCiphers());
+        return util;
+    }
+
+
+    private static KeyStore orderedKeystore(String malformedAlias, 
KeystoreEntry... entries) throws Exception {
+        File file = TesterKeystoreGenerator.generateKeystore(List.of(entries));
+        KeyStore delegate = KeyStore.getInstance(file, 
TesterSupport.JKS_PASS.toCharArray());
+        List<String> aliases = 
Stream.of(entries).map(KeystoreEntry::alias).collect(Collectors.toList());
+
+        // JKS type of keystore enumeration order is unspecified.
+        // Control that boundary while keeping real keys, certificate chains, 
and key managers.
+        // Malformed EKU must be injected here because a JKS parser can reject 
it while loading.
+        X509Certificate malformed = EasyMock.createMock(X509Certificate.class);
+        EasyMock.expect(malformed.getExtendedKeyUsage()).andStubThrow(new 
CertificateParsingException("Invalid EKU"));
+        EasyMock.replay(malformed);
+        KeyStoreSpi spi = EasyMock.createMock(KeyStoreSpi.class);
+        spi.engineLoad(null, null);
+        EasyMock.expectLastCall().once();
+        EasyMock.expect(spi.engineAliases()).andStubAnswer(() -> 
Collections.enumeration(aliases));
+        EasyMock.expect(spi.engineIsKeyEntry(EasyMock.anyString()))
+                .andStubAnswer(() -> 
delegate.isKeyEntry(EasyMock.getCurrentArgument(0)));
+        
EasyMock.expect(spi.engineGetCertificate(EasyMock.anyString())).andStubAnswer(()
 -> {
+            String alias = EasyMock.getCurrentArgument(0);
+            return alias.equals(malformedAlias) ? malformed : 
delegate.getCertificate(alias);
+        });
+        EasyMock.expect(spi.engineGetCertificateChain(EasyMock.anyString()))
+                .andStubAnswer(() -> 
delegate.getCertificateChain(EasyMock.getCurrentArgument(0)));
+        EasyMock.expect(spi.engineGetKey(EasyMock.anyString(), 
EasyMock.anyObject(char[].class)))
+                .andStubAnswer(() -> 
delegate.getKey(EasyMock.getCurrentArgument(0), 
EasyMock.getCurrentArgument(1)));
+        EasyMock.replay(spi);
+        KeyStore result = new KeyStore(spi, delegate.getProvider(), "JKS") { };
+        result.load(null, null);
+        return result;
+    }
+
+
+    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));
+    }
+
+
+    /*
+     * A configured alias must be honored exactly, even when its certificate 
is not valid for server authentication and
+     * a server-auth certificate sharing the Subject is also present. 
Auto-selection must never override the user's
+     * choice (see the uses of 
SSLHostConfigCertificate.getCertificateKeyAlias() across the connectors).
+     */
+    @Test
+    public void testConfiguredAliasIsUsedUnchangedWhenNotServerAuth() throws 
Exception {
+        X509Certificate presented = present(HOST,
+                key(HOST, CLIENT_AUTH),
+                key("localhost-server", SERVER_AUTH));
+        assertEku(presented, CLIENT_AUTH_EXTENDED_KEY_USAGE_OID);
+    }
+
+
+    @Test
+    public void testConfiguredAliasIsUsedWhenServerAuth() 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 testAutoSelectPrefersServerAuthOverClientAuth() 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 testAutoSelectAcceptsAnyExtendedKeyUsage() throws Exception {
+        X509Certificate presented = present(null,
+                key("first", CLIENT_AUTH),
+                key("any", ANY_AUTH));
+        assertEku(presented, Constants.ANY_EXTENDED_KEY_USAGE_OID);
+    }
+
+
+    @Test
+    public void testAutoSelectAcceptsCertificateWithoutExtendedKeyUsage() 
throws Exception {
+        X509Certificate presented = present(null,
+                key("first", CLIENT_AUTH),
+                key("none", null));
+        Assert.assertNull("A certificate with no Extended Key Usage is 
unrestricted and must be selectable",
+                presented.getExtendedKeyUsage());
+    }
+
+
+    /*
+     * Trusted (keyless) certificate entries are not key entries, so they can 
never be selected as the server key even
+     * when they are the only server-auth-capable certificate.
+     */
+    @Test
+    public void testAutoSelectSkipsTrustedEntries() {
+        Assert.assertThrows(IOException.class, () -> present(null,
+                KeystoreEntry.trusted("ca", HOST, SERVER_AUTH),
+                key("key", CLIENT_AUTH)));
+    }
+
+
+    /*
+     * No unsuitable key entry may be selected when all certificates exclude 
server authentication.
+     */
+    @Test
+    public void testAutoSelectFailsWhenNoneServerAuth() {
+        Assert.assertThrows(IOException.class, () -> present(null,
+                key("first", CLIENT_AUTH),
+                key("second", CLIENT_AUTH)));
+    }
+
+
+    @Test
+    public void testAutoSelectSkipsMalformedExtendedKeyUsage() throws 
Exception {
+        KeyStore keystore = orderedKeystore("malformed", key("malformed", 
CLIENT_AUTH), key("server", SERVER_AUTH));
+        assertEku(present(null, keystore), 
Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID);
+    }
+
+
+    @Test
+    public void testConfiguredAliasWithMalformedExtendedKeyUsageIsUsed() 
throws Exception {
+        KeyStore keystore = orderedKeystore("malformed", key("malformed", 
CLIENT_AUTH), key("server", SERVER_AUTH));
+        Assert.assertEquals(keystore.getCertificateChain("malformed")[0], 
present("malformed", keystore));
+    }
+
+
+    @Test
+    public void testAutoSelectFailsWhenOnlyMalformedExtendedKeyUsage() throws 
Exception {
+        KeyStore keystore = orderedKeystore("malformed", key("malformed", 
CLIENT_AUTH));
+        Assert.assertThrows(IOException.class, () -> present(null, keystore));
+    }
+
+
+    @Test
+    public void testAutoSelectUsesFirstEligibleKey() throws Exception {
+        KeyStore keystore = orderedKeystore(null, key("client", CLIENT_AUTH), 
key("first", ANY_AUTH),
+                key("second", SERVER_AUTH));
+        Assert.assertEquals(keystore.getCertificate("first"), present(null, 
keystore));
+    }
+
+
+    @Test
+    public void testConfiguredMissingAliasFails() {
+        Assert.assertThrows(IOException.class, () -> present("missing", 
key("server", SERVER_AUTH)));
+    }
+
+
+    @Test
+    public void testConfiguredTrustedAliasFails() {
+        Assert.assertThrows(IOException.class, () -> present("trusted", 
KeystoreEntry.trusted("trusted", HOST, SERVER_AUTH),
+                key("server", SERVER_AUTH)));
+    }
+
+
+    @Test
+    public void testAutoSelectDoesNotSkipKeyReadFailure() throws Exception {
+        KeyStore keystore = orderedKeystore(null, key("first", SERVER_AUTH), 
key("second", SERVER_AUTH));
+        SSLHostConfig hostConfig = new SSLHostConfig();
+        SSLHostConfigCertificate certificate = new 
SSLHostConfigCertificate(hostConfig, Type.UNDEFINED);
+        certificate.setCertificateKeystore(keystore);
+        certificate.setCertificateKeyPassword("incorrect");
+        SSLUtil util = 
SSLImplementation.getInstance(sslImplementationName).getSSLUtil(certificate);
+        Assert.assertThrows(UnrecoverableKeyException.class, 
util::getKeyManagers);
+    }
+
+
+    @Test
+    public void testSSLContextUsesSelectedCertificate() throws Exception {
+        KeyStore keystore = orderedKeystore(null, key("client", CLIENT_AUTH), 
key("server", SERVER_AUTH));
+        SSLContext context = sslUtil(null, 
keystore).createSSLContext(List.of());
+        try {
+            Assert.assertEquals(keystore.getCertificate("server"), 
context.getCertificateChain("server")[0]);
+        } finally {
+            context.destroy();
+        }
+    }
+
+
+    @Test
+    public void testSSLContextLoadsSelectedCertificateFromKeystoreFile() 
throws Exception {
+        File file = TesterKeystoreGenerator.generateKeystore(
+                List.of(key(SSLUtilBase.DEFAULT_KEY_ALIAS, CLIENT_AUTH), 
key("server", SERVER_AUTH)));
+        KeyStore keystore = KeyStore.getInstance(file, 
TesterSupport.JKS_PASS.toCharArray());
+        SSLHostConfig hostConfig = new SSLHostConfig();
+        SSLHostConfigCertificate certificate = new 
SSLHostConfigCertificate(hostConfig, Type.UNDEFINED);
+        certificate.setCertificateKeystoreFile(file.getAbsolutePath());
+        certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS);
+        SSLContext context = sslUtil(certificate).createSSLContext(List.of());
+        try {
+            Assert.assertEquals(keystore.getCertificate("server"), 
context.getCertificateChain("server")[0]);
+        } finally {
+            context.destroy();
+        }
+    }
+}
diff --git a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java 
b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java
index 00f1772fcc..091a6fae4e 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,113 @@ 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)}.
+     */
+    public static final class KeystoreEntry {
+
+        private final String alias;
+        private final String cn;
+        private final boolean keyEntry;
+        private final CertificateExtensionsCustomizer customizer;
+
+        /**
+         * @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 KeystoreEntry(String alias, String cn, boolean keyEntry, 
CertificateExtensionsCustomizer customizer) {
+            this.alias = alias;
+            this.cn = cn;
+            this.keyEntry = keyEntry;
+            this.customizer = customizer;
+        }
+
+        public String alias() {
+            return alias;
+        }
+
+        public String cn() {
+            return cn;
+        }
+
+        public boolean keyEntry() {
+            return keyEntry;
+        }
+
+        public CertificateExtensionsCustomizer customizer() {
+            return 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 +179,34 @@ 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 static final class CertAndKey {
+
+        private final KeyPair keyPair;
+        private final X509Certificate certificate;
+
+        private CertAndKey(KeyPair keyPair, X509Certificate certificate) {
+            this.keyPair = keyPair;
+            this.certificate = certificate;
+        }
 
+        private KeyPair keyPair() {
+            return keyPair;
+        }
+
+        private X509Certificate certificate() {
+            return 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 a9c9086990..49a768d2cf 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -524,6 +524,15 @@
         <bug>70305</bug>: Move callback registrations in FFM code to the
         SSL context. (remm)
       </fix>
+      <update>
+        When a <code>Certificate</code> is configured using a key store and no
+        <code>certificateKeyAlias</code> is set, ignore key entries whose
+        certificate has a malformed Extended Key Usage extension or one that
+        does not permit TLS server authentication when automatically selecting
+        which key in the key store to use.
+        A configured <code>certificateKeyAlias</code> is always used unchanged.
+        (dsoumis)
+      </update>
     </changelog>
   </subsection>
   <subsection name="Jasper">
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index 0d3ae6cd2c..7a19e6ab52 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -1752,12 +1752,19 @@
     <attribute name="certificateKeyAlias" required="false">
       <p>JSSE only.</p>
       <p>The alias used for the server key and certificate in the keystore. If
-      not specified, the first key read from the keystore will be used. The
+      not specified, the first eligible key read from the keystore will be 
used. The
       order in which keys are read from the keystore is implementation
       dependent. It may not be the case that keys are read from the keystore in
       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>If no alias is specified, key entries whose certificate has a 
malformed
+      Extended Key Usage extension, or one that includes neither
+      <code>serverAuth</code> nor <code>anyExtendedKeyUsage</code>, are ignored
+      when selecting the key to use. Certificates with no Extended Key Usage
+      extension remain eligible. If no eligible key entry is found, connector
+      initialization fails. A configured alias is used unchanged and is not
+      subject to this filtering.</p>
     </attribute>
 
     <attribute name="certificateKeyFile" required="false">
diff --git a/webapps/docs/ssl-howto.xml b/webapps/docs/ssl-howto.xml
index ce53b4df24..987a7ddd33 100644
--- a/webapps/docs/ssl-howto.xml
+++ b/webapps/docs/ssl-howto.xml
@@ -457,6 +457,23 @@ mailing list</a>.</p>
 </subsection>
 </section>
 
+<section name="Automatic certificate selection">
+
+<p>When no <strong>certificateKeyAlias</strong> is configured, Tomcat selects 
the
+first eligible key entry in the key store's enumeration order. 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 whose Extended Key Usage
+excludes both purposes or cannot be parsed are ignored. If no eligible key 
entry
+is found, connector initialization fails.</p>
+
+<p>A configured <strong>certificateKeyAlias</strong> is used unchanged, even if
+its certificate is unsuitable for server authentication. When several 
certificates
+are eligible, configure an alias to ensure that the intended certificate is
+used.</p>
+
+</section>
+
 <section name="Reloading certificates before they expire">
 
 <p>When certificates are renewed by an external process the
@@ -494,7 +511,7 @@ automatically.</p>
 <p>A Windows certificate store may contain several certificates for the same
 machine. Configure <strong>certificateKeyAlias</strong> with the alias exposed
 by Java for the intended certificate. If no alias is configured, Tomcat uses 
the
-first key read from the store, in an implementation-dependent order. The
+first eligible key read from the store, in an implementation-dependent order. 
The
 following example uses the alias <code>tomcat</code>; replace it with the alias
 of the certificate to use:</p>
 


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

Reply via email to