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

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

commit 4353c6232689dbbb518a51a8e41fcf43550532c4
Author: Dimitris Soumis <[email protected]>
AuthorDate: Wed Oct 28 15:22:38 2026 +0200

    Select a server-auth certificate for a configured Windows-MY alias
    
    When a Certificate is configured from a key store and no certificateKeyAlias
    is set, ignore key entries whose certificate is not valid for TLS server
    authentication when automatically selecting the key to use. A configured
    certificateKeyAlias is used even if its certificate is not valid, except in 
a
    Windows-MY key store accessed through the SunMSCAPI provider: there, if the
    configured certificate is not valid but a numbered duplicate of the alias
    ("alias (1)", "alias (2)", ...) carries a valid one, the duplicate is used
    instead - including when the configured alias is a keyless (trusted) entry,
    and continuing past gaps in the numbering.
---
 java/org/apache/tomcat/util/net/Constants.java     |  12 +
 .../apache/tomcat/util/net/LocalStrings.properties |   2 +
 java/org/apache/tomcat/util/net/SSLUtilBase.java   |  75 ++++-
 .../SSLUtilBaseCertificateSelectionTestBase.java   | 205 ++++++++++++
 ...stSSLUtilBaseAutomaticCertificateSelection.java | 156 ++++++++++
 .../TestSSLUtilBaseConfiguredAliasSelection.java   | 190 ++++++++++++
 .../util/net/TestSSLUtilBaseKeyUsageSelection.java | 343 ---------------------
 .../tomcat/util/net/TesterKeystoreGenerator.java   |   5 +-
 webapps/docs/changelog.xml                         |  10 +-
 webapps/docs/config/http.xml                       |  17 +-
 webapps/docs/ssl-howto.xml                         |  24 +-
 11 files changed, 684 insertions(+), 355 deletions(-)

diff --git a/java/org/apache/tomcat/util/net/Constants.java 
b/java/org/apache/tomcat/util/net/Constants.java
index cfc9af27de..56bcc62883 100644
--- a/java/org/apache/tomcat/util/net/Constants.java
+++ b/java/org/apache/tomcat/util/net/Constants.java
@@ -76,6 +76,18 @@ public class Constants {
     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";
 
+    /**
+     * SunMSCAPI provider name.
+     */
+    public static final String SUNMSCAPI_PROVIDER_NAME = "SunMSCAPI";
+
+    /**
+     * Windows-MY key store types.
+     */
+    public static final String WINDOWS_MY_KEYSTORE_TYPE = "WINDOWS-MY";
+    public static final String WINDOWS_MY_CURRENTUSER_KEYSTORE_TYPE = 
"WINDOWS-MY-CURRENTUSER";
+    public static final String WINDOWS_MY_LOCALMACHINE_KEYSTORE_TYPE = 
"WINDOWS-MY-LOCALMACHINE";
+
     /**
      * 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 724d1aa2a6..5821a88c6d 100644
--- a/java/org/apache/tomcat/util/net/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/net/LocalStrings.properties
@@ -208,8 +208,10 @@ sslUtilBase.keystore_load_failed=Failed to load keystore 
type [{0}] with path [{
 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.noServerAuthKey=The key store has private key entries but none has 
a certificate that is valid for TLS server authentication. Enable debug logging 
to see the skipped aliases, use a certificate whose Extended Key Usage permits 
server authentication, or configure certificateKeyAlias explicitly.
 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 as the configured alias [{1}] is not valid for 
that purpose and the key store contains more than one certificate under that 
name
 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.
diff --git a/java/org/apache/tomcat/util/net/SSLUtilBase.java 
b/java/org/apache/tomcat/util/net/SSLUtilBase.java
index 875319bf59..85880184b5 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;
@@ -395,13 +396,12 @@ public abstract class SSLUtilBase implements SSLUtil {
             ksUsed.setKeyEntry(keyAlias, privateKeyFile.getPrivateKey(), 
keyPassArray,
                     chain.toArray(new Certificate[0]));
         } else {
-            if (keyAlias != null && !ks.isKeyEntry(keyAlias)) {
-                throw new 
IOException(sm.getString("sslUtilBase.alias_no_key_entry", keyAlias));
-            } else if (keyAlias == null) {
+            if (keyAlias == null) {
                 Enumeration<String> aliases = ks.aliases();
                 if (!aliases.hasMoreElements()) {
                     throw new IOException(sm.getString("sslUtilBase.noKeys"));
                 }
+                boolean skippedForServerAuth = false;
                 while (aliases.hasMoreElements() && keyAlias == null) {
                     String candidate = aliases.nextElement();
                     if (!ks.isKeyEntry(candidate)) {
@@ -412,13 +412,34 @@ public abstract class SSLUtilBase implements SSLUtil {
                         if (log.isDebugEnabled()) {
                             
log.debug(sm.getString("sslUtilBase.skippedKeyUsage", candidate));
                         }
+                        skippedForServerAuth = true;
                         continue;
                     }
                     keyAlias = candidate;
                 }
                 if (keyAlias == null) {
+                    if (skippedForServerAuth) {
+                        throw new 
IOException(sm.getString("sslUtilBase.noServerAuthKey"));
+                    }
                     throw new 
IOException(sm.getString("sslUtilBase.alias_no_key_entry", (Object) null));
                 }
+            } else {
+                if (isSunMSCAPIWindowsMY(ks)) {
+                    Certificate configuredCert = ks.isKeyEntry(keyAlias) ? 
ks.getCertificate(keyAlias) : null;
+                    if (!(configuredCert instanceof X509Certificate) ||
+                            !isServerAuthCapable((X509Certificate) 
configuredCert)) {
+                        String duplicateAlias = 
findServerAuthAliasInWindowsMYFamily(ks, keyAlias);
+                        if (duplicateAlias != null) {
+                            if (log.isDebugEnabled()) {
+                                
log.debug(sm.getString("sslUtilBase.selectedServerAuthCert", duplicateAlias, 
keyAlias));
+                            }
+                            keyAlias = duplicateAlias;
+                        }
+                    }
+                }
+                if (!ks.isKeyEntry(keyAlias)) {
+                    throw new 
IOException(sm.getString("sslUtilBase.alias_no_key_entry", keyAlias));
+                }
             }
 
             Key k = ks.getKey(keyAlias, keyPassArray);
@@ -460,6 +481,38 @@ public abstract class SSLUtilBase implements SSLUtil {
     }
 
 
+    /**
+     * Searches a SunMSCAPI Windows-MY key store for a key entry whose 
certificate is valid for TLS server
+     * authentication, among the numbered duplicates of {@code baseAlias}. 
When several certificates share a Windows
+     * "friendly name", that provider appends {@code " (1)"}, {@code " (2)"}, 
etc. to give them unique aliases, as the
+     * key store API requires.
+     *
+     * @param ks        the SunMSCAPI Windows-MY key store to search
+     * @param baseAlias the configured alias. Not itself re-checked: the 
caller has already determined that its
+     *                      certificate is not valid for TLS server 
authentication.
+     *
+     * @return the alias of the first duplicate (in increasing numeric order 
starting at 1) whose certificate is
+     *             valid for TLS server authentication, or {@code null} if 
there is no such duplicate
+     *
+     * @throws KeyStoreException if the key store has not been initialized
+     */
+    private static String findServerAuthAliasInWindowsMYFamily(KeyStore ks, 
String baseAlias) throws KeyStoreException {
+        int limit = Collections.list(ks.aliases()).size();
+        for (int suffix = 1; suffix <= limit; suffix++) {
+            String candidateAlias = baseAlias + " (" + suffix + ")";
+            Certificate candidateCert = ks.getCertificate(candidateAlias);
+            if (candidateCert == null) {
+                continue;
+            }
+            if (candidateCert instanceof X509Certificate && 
ks.isKeyEntry(candidateAlias) &&
+                    isServerAuthCapable((X509Certificate) candidateCert)) {
+                return candidateAlias;
+            }
+        }
+        return null;
+    }
+
+
     /**
      * 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
@@ -504,6 +557,22 @@ public abstract class SSLUtilBase implements SSLUtil {
         return false;
     }
 
+    private static boolean isSunMSCAPIWindowsMY(KeyStore ks) {
+        if (ks == null || 
!Constants.SUNMSCAPI_PROVIDER_NAME.equals(ks.getProvider().getName())) {
+            return false;
+        }
+
+        //Windows-ROOT is excluded because it is a keyless trust store
+        switch (ks.getType().toUpperCase(Locale.ROOT)) {
+            case Constants.WINDOWS_MY_KEYSTORE_TYPE:
+            case Constants.WINDOWS_MY_CURRENTUSER_KEYSTORE_TYPE:
+            case Constants.WINDOWS_MY_LOCALMACHINE_KEYSTORE_TYPE:
+                return true;
+            default:
+                return false;
+        }
+    }
+
     @Override
     public String[] getEnabledProtocols() {
         return enabledProtocols;
diff --git 
a/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java 
b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java
new file mode 100644
index 0000000000..5e28cbbc8a
--- /dev/null
+++ 
b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java
@@ -0,0 +1,205 @@
+/*
+ * 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.FileInputStream;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.KeyStoreSpi;
+import java.security.Provider;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+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.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;
+
+/**
+ * Shared fixtures for the {@link SSLUtilBase#getKeyManagers()} 
certificate-selection tests. The two behaviors are
+ * split across the concrete subclasses: {@link 
TestSSLUtilBaseAutomaticCertificateSelection} covers Extended Key
+ * Usage based selection when no alias is configured, and {@link 
TestSSLUtilBaseConfiguredAliasSelection} covers a
+ * configured alias, including the SunMSCAPI Windows-MY numbered-duplicate 
resolution.
+ * <p>
+ * {@link #orderedKeystore(String, KeystoreEntry...)} returns a key store with 
a controlled alias enumeration order
+ * that presents as a SunMSCAPI Windows-MY store; {@link 
#nonSunMSCAPIKeystore(KeystoreEntry...)} presents as a plain
+ * JKS store, so that the SunMSCAPI-only duplicate search is skipped.
+ */
+public abstract class SSLUtilBaseCertificateSelectionTestBase extends 
TomcatBaseTest {
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> parameters() {
+        return Arrays.asList(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();
+    }
+
+    protected static final String HOST = "localhost";
+
+    protected static final String CLIENT_AUTH_EXTENDED_KEY_USAGE_OID = 
"1.3.6.1.5.5.7.3.2";
+
+    @SuppressWarnings("unused")
+    protected static final CertificateExtensionsCustomizer SERVER_AUTH = 
(keyPair, builder) -> builder
+            .addExtension(Extension.extendedKeyUsage, true, new 
ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth));
+    @SuppressWarnings("unused")
+    protected static final CertificateExtensionsCustomizer CLIENT_AUTH = 
(keyPair, builder) -> builder
+            .addExtension(Extension.extendedKeyUsage, true, new 
ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth));
+    @SuppressWarnings("unused")
+    protected static final CertificateExtensionsCustomizer ANY_AUTH = 
(keyPair, builder) -> builder
+            .addExtension(Extension.extendedKeyUsage, true, new 
ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage));
+
+
+    protected static KeystoreEntry key(String alias, 
CertificateExtensionsCustomizer eku) {
+        return KeystoreEntry.key(alias, HOST, eku);
+    }
+
+
+    protected X509Certificate present(String alias, KeystoreEntry... entries) 
throws Exception {
+        return present(alias, orderedKeystore(null, entries));
+    }
+
+
+    protected 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];
+    }
+
+
+    protected SSLUtil sslUtil(String alias, KeyStore keystore) throws 
Exception {
+
+        SSLHostConfig hostConfig = new SSLHostConfig();
+        SSLHostConfigCertificate certificate = new 
SSLHostConfigCertificate(hostConfig, Type.UNDEFINED);
+        certificate.setCertificateKeystore(keystore);
+        certificate.setCertificateKeystoreType("JKS");
+        certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS);
+        if (alias != null) {
+            certificate.setCertificateKeyAlias(alias);
+        }
+
+        return sslUtil(certificate);
+    }
+
+
+    protected 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;
+    }
+
+
+    protected static KeyStore orderedKeystore(String malformedAlias, 
KeystoreEntry... entries) throws Exception {
+        return orderedKeystore(new Provider("SunMSCAPI", 1.0, "test") {
+            private static final long serialVersionUID = -4141665633237722962L;
+        }, "Windows-MY", malformedAlias, entries);
+    }
+
+
+    protected static KeyStore nonSunMSCAPIKeystore(KeystoreEntry... entries) 
throws Exception {
+        return orderedKeystore(new Provider("SUN", 1.0, "test") {
+            private static final long serialVersionUID = 4434321038151233913L;
+        }, "JKS", null, entries);
+    }
+
+
+    protected static KeyStore orderedKeystore(Provider provider, String type, 
String malformedAlias,
+            KeystoreEntry... entries) throws Exception {
+        File file = 
TesterKeystoreGenerator.generateKeystore(Arrays.asList(entries));
+        KeyStore delegate = KeyStore.getInstance("JKS");
+        try (InputStream is = new FileInputStream(file)) {
+            delegate.load(is, 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);
+        // A real JKS backs the mock, so keys report PKCS#8 and 
getKeyManagers() takes its in-memory-copy branch
+        // rather than SunMSCAPI's non-exportable-key path; the provider name 
and store type are overridden only so
+        // isSunMSCAPIWindowsMY() can be exercised off Windows.
+        KeyStore result = new KeyStore(spi, provider, type) { };
+        result.load(null, null);
+        return result;
+    }
+
+
+    protected 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));
+    }
+}
diff --git 
a/test/org/apache/tomcat/util/net/TestSSLUtilBaseAutomaticCertificateSelection.java
 
b/test/org/apache/tomcat/util/net/TestSSLUtilBaseAutomaticCertificateSelection.java
new file mode 100644
index 0000000000..7ad1c58a5a
--- /dev/null
+++ 
b/test/org/apache/tomcat/util/net/TestSSLUtilBaseAutomaticCertificateSelection.java
@@ -0,0 +1,156 @@
+/*
+ * 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.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
+import org.apache.tomcat.util.net.TesterKeystoreGenerator.KeystoreEntry;
+
+/**
+ * Tests that, when no {@code certificateKeyAlias} is configured, {@link 
SSLUtilBase#getKeyManagers()} auto-selects the
+ * first key entry whose certificate is valid for TLS server authentication 
(by Extended Key Usage) and ignores the
+ * entries that are not. Applies to every key store type.
+ */
+@RunWith(Parameterized.class)
+public class TestSSLUtilBaseAutomaticCertificateSelection extends 
SSLUtilBaseCertificateSelectionTestBase {
+
+    @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());
+    }
+
+
+    @Test
+    public void testAutoSelectSkipsTrustedEntries() {
+        Assert.assertThrows(IOException.class, () -> present(null,
+                KeystoreEntry.trusted("ca", HOST, SERVER_AUTH),
+                key("key", CLIENT_AUTH)));
+    }
+
+
+    @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 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 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.setCertificateKeystoreType("JKS");
+        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(Collections.emptyList());
+        try {
+            Assert.assertEquals(keystore.getCertificate("server"), 
context.getCertificateChain("server")[0]);
+        } finally {
+            context.destroy();
+        }
+    }
+
+
+    @Test
+    public void testSSLContextLoadsSelectedCertificateFromKeystoreFile() 
throws Exception {
+        File file = TesterKeystoreGenerator.generateKeystore(
+                Arrays.asList(key(SSLUtilBase.DEFAULT_KEY_ALIAS, CLIENT_AUTH), 
key("server", SERVER_AUTH)));
+        KeyStore keystore = KeyStore.getInstance("JKS");
+        try (InputStream is = new FileInputStream(file)) {
+            keystore.load(is, 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(Collections.emptyList());
+        try {
+            Assert.assertEquals(keystore.getCertificate("server"), 
context.getCertificateChain("server")[0]);
+        } finally {
+            context.destroy();
+        }
+    }
+}
diff --git 
a/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java 
b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java
new file mode 100644
index 0000000000..911180f643
--- /dev/null
+++ 
b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java
@@ -0,0 +1,190 @@
+/*
+ * 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.IOException;
+import java.security.KeyStore;
+import java.security.cert.X509Certificate;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import org.apache.tomcat.util.net.TesterKeystoreGenerator.KeystoreEntry;
+
+/**
+ * Tests {@link SSLUtilBase#getKeyManagers()} when a {@code 
certificateKeyAlias} is configured. The configured alias is
+ * used even if its certificate is not valid for TLS server authentication, 
except in a SunMSCAPI Windows-MY key store:
+ * there, if a numbered duplicate of the alias (e.g. {@code "alias (1)"}) 
carries a certificate that is valid, it is
+ * used instead. Only numbered duplicates of the configured alias are 
considered, and only for that store type.
+ */
+@RunWith(Parameterized.class)
+public class TestSSLUtilBaseConfiguredAliasSelection extends 
SSLUtilBaseCertificateSelectionTestBase {
+
+    @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 
testConfiguredAliasUsesNumberedDuplicateWhenBaseNotServerAuth() throws 
Exception {
+        X509Certificate presented = present(HOST,
+                key(HOST, CLIENT_AUTH),
+                key(HOST + " (1)", SERVER_AUTH));
+        assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID);
+    }
+
+
+    @Test
+    public void testConfiguredAliasUnchangedForNonSunMSCAPIStore() throws 
Exception {
+        KeyStore keystore = nonSunMSCAPIKeystore(
+                key(HOST, CLIENT_AUTH),
+                key(HOST + " (1)", SERVER_AUTH));
+        X509Certificate presented = present(HOST, keystore);
+        Assert.assertEquals(keystore.getCertificate(HOST), presented);
+    }
+
+
+    @Test
+    public void testConfiguredAliasKeepsBaseWhenAlreadyServerAuth() throws 
Exception {
+        X509Certificate presented = present(HOST,
+                key(HOST, SERVER_AUTH),
+                key(HOST + " (1)", CLIENT_AUTH));
+        assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID);
+    }
+
+
+    @Test
+    public void 
testConfiguredAliasUsesSecondDuplicateWhenFirstIsNotServerAuth() throws 
Exception {
+        X509Certificate presented = present(HOST,
+                key(HOST, CLIENT_AUTH),
+                key(HOST + " (1)", CLIENT_AUTH),
+                key(HOST + " (2)", SERVER_AUTH));
+        assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID);
+    }
+
+
+    @Test
+    public void testConfiguredAliasSelectsFirstOfSeveralServerAuthDuplicates() 
throws Exception {
+        KeyStore keystore = orderedKeystore(null,
+                key("tomcat", CLIENT_AUTH),
+                key("tomcat (1)", SERVER_AUTH),
+                key("tomcat (2)", SERVER_AUTH));
+        X509Certificate presented = present("tomcat", keystore);
+        Assert.assertEquals(keystore.getCertificate("tomcat (1)"), presented);
+    }
+
+
+    @Test
+    public void testConfiguredAliasSkipsTrustedDuplicateAndUsesNextKeyEntry() 
throws Exception {
+        X509Certificate presented = present(HOST,
+                key(HOST, CLIENT_AUTH),
+                KeystoreEntry.trusted(HOST + " (1)", HOST, SERVER_AUTH),
+                key(HOST + " (2)", SERVER_AUTH));
+        assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID);
+    }
+
+
+    @Test
+    public void 
testConfiguredAliasFallsBackToBaseWhenNoDuplicateIsServerAuth() throws 
Exception {
+        X509Certificate presented = present(HOST,
+                key(HOST, CLIENT_AUTH),
+                key(HOST + " (1)", CLIENT_AUTH));
+        assertEku(presented, CLIENT_AUTH_EXTENDED_KEY_USAGE_OID);
+    }
+
+
+    @Test
+    public void testConfiguredAliasIgnoresAliasNotMatchingDuplicateFormat() 
throws Exception {
+        X509Certificate presented = present(HOST,
+                key(HOST, CLIENT_AUTH),
+                key(HOST + "(1)", SERVER_AUTH));
+        assertEku(presented, CLIENT_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 testConfiguredAliasUsesNumberedDuplicateWhenBaseIsMalformed() 
throws Exception {
+        KeyStore keystore =
+                orderedKeystore("malformed", key("malformed", CLIENT_AUTH), 
key("malformed (1)", SERVER_AUTH));
+        assertEku(present("malformed", keystore), 
Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID);
+    }
+
+
+    @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)));
+    }
+
+
+    /*
+     * In a SunMSCAPI Windows-MY store a certificate without a private key can 
take the bare friendly name while the
+     * key-bearing certificate of the same name is exposed as a numbered 
duplicate. The configured alias then names a
+     * non-key (trusted) entry, so the server-auth numbered duplicate must 
still be selected rather than failing
+     * start-up.
+     */
+    @Test
+    public void testConfiguredTrustedBaseAliasUsesServerAuthDuplicate() throws 
Exception {
+        X509Certificate presented = present(HOST,
+                KeystoreEntry.trusted(HOST, HOST, SERVER_AUTH),
+                key(HOST + " (1)", SERVER_AUTH));
+        assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID);
+    }
+
+
+    /*
+     * The numbered duplicates of a Windows-MY friendly name are normally 
contiguous, but should a gap appear (for
+     * example a lower-numbered duplicate was removed) the search must keep 
scanning past it rather than stop, so a valid
+     * server-auth duplicate beyond the gap is still found.
+     */
+    @Test
+    public void testConfiguredAliasSkipsGapInNumberedDuplicates() throws 
Exception {
+        X509Certificate presented = present("tomcat",
+                key("tomcat", CLIENT_AUTH),
+                key("tomcat (2)", SERVER_AUTH));
+        assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID);
+    }
+}
diff --git 
a/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java 
b/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java
deleted file mode 100644
index 84c500a7ca..0000000000
--- a/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java
+++ /dev/null
@@ -1,343 +0,0 @@
-/*
- * 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.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-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.Arrays;
-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 Arrays.asList(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(Arrays.asList(entries));
-        KeyStore delegate = KeyStore.getInstance("JKS");
-        try (InputStream is = new FileInputStream(file)) {
-            delegate.load(is, 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(Collections.emptyList());
-        try {
-            Assert.assertEquals(keystore.getCertificate("server"), 
context.getCertificateChain("server")[0]);
-        } finally {
-            context.destroy();
-        }
-    }
-
-
-    @Test
-    public void testSSLContextLoadsSelectedCertificateFromKeystoreFile() 
throws Exception {
-        File file = TesterKeystoreGenerator.generateKeystore(
-                Arrays.asList(key(SSLUtilBase.DEFAULT_KEY_ALIAS, CLIENT_AUTH), 
key("server", SERVER_AUTH)));
-        KeyStore keystore = KeyStore.getInstance("JKS");
-        try (InputStream is = new FileInputStream(file)) {
-            keystore.load(is, 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(Collections.emptyList());
-        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 0bc8e6a01e..444a850df4 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.KeyStore;
 import java.security.cert.X509Certificate;
 import java.util.Date;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
 
 import org.bouncycastle.asn1.x500.X500Name;
 import org.bouncycastle.asn1.x509.Extension;
@@ -39,6 +40,8 @@ import 
org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
 
 public final class TesterKeystoreGenerator {
 
+    private static final AtomicLong SERIAL_COUNTER = new AtomicLong(1);
+
     private TesterKeystoreGenerator() {}
 
     @FunctionalInterface
@@ -130,7 +133,7 @@ public final class TesterKeystoreGenerator {
         KeyPair keyPair = kpg.generateKeyPair();
 
         X500Name subject = new X500Name("CN=" + cn);
-        BigInteger serial = BigInteger.valueOf(System.nanoTime());
+        BigInteger serial = 
BigInteger.valueOf(SERIAL_COUNTER.getAndIncrement());
         long oneDay = 86400000L;
         Date notBefore = new Date(System.currentTimeMillis() - oneDay);
         Date notAfter = new Date(System.currentTimeMillis() + 365L * oneDay);
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 9e068da9ba..b7e1d2f7b1 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -518,8 +518,14 @@
         <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.
+        which key in the key store to use. A configured
+        <code>certificateKeyAlias</code> is used even if its certificate is not
+        valid for TLS server authentication, except in the Windows-MY key store
+        accessed through the SunMSCAPI provider: there, if the configured
+        certificate is not valid but the store holds a valid certificate under
+        that alias with a numeric suffix appended - the format that provider 
uses
+        to give unique aliases to several certificates that share a name - the
+        suffixed certificate is used instead.
         (dsoumis)
       </update>
     </changelog>
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index b45d105606..4960a5551e 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -1919,8 +1919,21 @@
       <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>
+      initialization fails.</p>
+      <p>A configured alias is used even if its certificate is not valid for 
TLS
+      server authentication. The exception is the Windows-MY store accessed
+      through the SunMSCAPI provider: if the certificate at the configured 
alias is
+      not valid there, but the store also contains a key entry whose alias is 
the
+      configured alias with <code>" (1)"</code>, <code>" (2)"</code>, etc. 
appended
+      and whose certificate is valid, that certificate is used instead. That 
store
+      exposes such duplicates because the key store API requires aliases to be
+      unique even when several certificates share a name. If none of those 
numbered
+      duplicates is valid, or the key store is of any other type, the 
configured
+      alias is used unchanged.</p>
+      <p>The SunMSCAPI provider does not expose a certificate's original 
Windows
+      friendly name through the key store API, so the match is a heuristic 
based
+      on the appended suffix alone (see
+      <a 
href="https://bugs.openjdk.org/browse/JDK-8395755";>JDK-8395755</a>).</p>
     </attribute>
 
     <attribute name="certificateKeyFile" required="false">
diff --git a/webapps/docs/ssl-howto.xml b/webapps/docs/ssl-howto.xml
index 36196bfa30..a47598653d 100644
--- a/webapps/docs/ssl-howto.xml
+++ b/webapps/docs/ssl-howto.xml
@@ -465,10 +465,26 @@ 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>
+<p>A configured <strong>certificateKeyAlias</strong> is normally used even if 
its
+certificate is unsuitable for server authentication. The one exception is the
+Windows-MY store accessed through the SunMSCAPI provider: if the certificate 
at the
+configured alias is unsuitable there, but the store also holds a suitable
+certificate under that same name with <code>" (1)"</code>, <code>" (2)"</code>,
+etc. appended - the way that store gives unique aliases to several certificates
+that share a name, since the key store API requires aliases to be unique - the
+suffixed certificate is used instead. This lets one configured alias keep 
working
+on a store that holds more than one certificate under that name. Every
+other key store type uses the configured alias as given. When several
+differently-named certificates are eligible, configure an alias to ensure that 
the
+intended certificate is used.</p>
+
+<p>Selection is made solely on the appended <code>" (N)"</code> suffix. The
+SunMSCAPI provider does not expose a certificate's original Windows friendly 
name
+through the key store API (see
+<a href="https://bugs.openjdk.org/browse/JDK-8395755";>JDK-8395755</a>), so a 
suffix
+that is genuinely part of a certificate's own name cannot be told apart from 
one
+added to disambiguate a shared name. The result is therefore a best-effort
+heuristic that selects the first numbered alias whose certificate is 
suitable.</p>
 
 </section>
 


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

Reply via email to