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 c4c70fbd8ca7d26da7fb030fe12757d3496d691b
Author: Dimitris Soumis <[email protected]>
AuthorDate: Thu Oct 29 17:28:04 2026 +0200

    Share served-alias resolution across all SSLContext engines
---
 java/org/apache/tomcat/util/net/SSLContext.java    | 33 +++++++--
 .../apache/tomcat/util/net/SSLContextWrapper.java  |  6 ++
 .../tomcat/util/net/jsse/JSSESSLContext.java       | 21 +++---
 .../tomcat/util/net/openssl/OpenSSLContext.java    | 29 +++-----
 .../util/net/openssl/panama/OpenSSLContext.java    | 30 +++-----
 .../SSLUtilBaseCertificateSelectionTestBase.java   | 81 +++++++++++++++++++---
 .../TestSSLUtilBaseConfiguredAliasSelection.java   | 17 ++++-
 7 files changed, 151 insertions(+), 66 deletions(-)

diff --git a/java/org/apache/tomcat/util/net/SSLContext.java 
b/java/org/apache/tomcat/util/net/SSLContext.java
index 25adf54622..7c3c4134ef 100644
--- a/java/org/apache/tomcat/util/net/SSLContext.java
+++ b/java/org/apache/tomcat/util/net/SSLContext.java
@@ -26,6 +26,7 @@ import javax.net.ssl.SSLParameters;
 import javax.net.ssl.SSLServerSocketFactory;
 import javax.net.ssl.SSLSessionContext;
 import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509KeyManager;
 
 /**
  * This interface is needed to override the default SSLContext class to allow 
SSL implementation pluggability without
@@ -102,20 +103,42 @@ public interface SSLContext {
     X509Certificate[] getCertificateChain(String alias);
 
 
+    /**
+     * Returns the key managers that may serve the given certificate 
configuration, in preference order. Used by
+     * {@link #getServedAlias} to resolve the alias actually served. The 
default returns an empty array (no key manager
+     * known); implementations backed by one or more key managers override it 
to expose them.
+     *
+     * @param certificate The certificate configuration
+     *
+     * @return The key managers that may serve the certificate, possibly 
empty, never {@code null}
+     */
+    default X509KeyManager[] getServerKeyManagers(SSLHostConfigCertificate 
certificate) {
+        return new X509KeyManager[0];
+    }
+
+
     /**
      * Returns the alias the key manager would select to serve for the given 
certificate configuration during a
      * handshake. This can differ from {@link 
SSLHostConfigCertificate#getCertificateKeyAlias()}, for example after
-     * SunMSCAPI Windows-MY numbered-duplicate resolution. The default returns 
the configured alias (or
-     * {@link SSLUtilBase#DEFAULT_KEY_ALIAS} when none is configured); 
implementations backed by a key manager override
-     * it to resolve the alias actually served.
+     * SunMSCAPI Windows-MY numbered-duplicate resolution. Each key manager 
from {@link #getServerKeyManagers} is asked
+     * in turn; if none selects an alias the configured alias is used (or 
{@link SSLUtilBase#DEFAULT_KEY_ALIAS} when
+     * none is configured).
      *
      * @param certificate The certificate configuration
      *
      * @return The served alias, never {@code null}
      */
     default String getServedAlias(SSLHostConfigCertificate certificate) {
-        String alias = certificate.getCertificateKeyAlias();
-        return alias == null ? SSLUtilBase.DEFAULT_KEY_ALIAS : alias;
+        for (X509KeyManager keyManager : getServerKeyManagers(certificate)) {
+            if (keyManager != null) {
+                String alias = certificate.chooseServerAlias(keyManager);
+                if (alias != null) {
+                    return alias;
+                }
+            }
+        }
+        String configured = certificate.getCertificateKeyAlias();
+        return configured == null ? SSLUtilBase.DEFAULT_KEY_ALIAS : configured;
     }
 
 
diff --git a/java/org/apache/tomcat/util/net/SSLContextWrapper.java 
b/java/org/apache/tomcat/util/net/SSLContextWrapper.java
index 280d4dbbce..b1b434d5f0 100644
--- a/java/org/apache/tomcat/util/net/SSLContextWrapper.java
+++ b/java/org/apache/tomcat/util/net/SSLContextWrapper.java
@@ -80,6 +80,12 @@ class SSLContextWrapper implements SSLContext {
         return keyManager.getCertificateChain(alias);
     }
 
+
+    @Override
+    public X509KeyManager[] getServerKeyManagers(SSLHostConfigCertificate 
certificate) {
+        return new X509KeyManager[] { keyManager };
+    }
+
     @Override
     public X509Certificate[] getAcceptedIssuers() {
         return trustManager.getAcceptedIssuers();
diff --git a/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java 
b/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java
index eada0c2381..dff6603a1e 100644
--- a/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java
+++ b/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java
@@ -20,8 +20,10 @@ import java.security.KeyManagementException;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.security.cert.X509Certificate;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 import javax.net.ssl.KeyManager;
@@ -92,18 +94,17 @@ class JSSESSLContext implements SSLContext {
 
 
     @Override
-    public String getServedAlias(SSLHostConfigCertificate certificate) {
-        if (kms != null) {
-            for (KeyManager km : kms) {
-                if (km instanceof X509KeyManager) {
-                    String alias = 
certificate.chooseServerAlias((X509KeyManager) km);
-                    if (alias != null) {
-                        return alias;
-                    }
-                }
+    public X509KeyManager[] getServerKeyManagers(SSLHostConfigCertificate 
certificate) {
+        if (kms == null) {
+            return new X509KeyManager[0];
+        }
+        List<X509KeyManager> x509KeyManagers = new ArrayList<>();
+        for (KeyManager km : kms) {
+            if (km instanceof X509KeyManager) {
+                x509KeyManagers.add((X509KeyManager) km);
             }
         }
-        return SSLContext.super.getServedAlias(certificate);
+        return x509KeyManagers.toArray(new X509KeyManager[0]);
     }
 
     @Override
diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java 
b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java
index de276dd3c1..47e21b6537 100644
--- a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java
+++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java
@@ -57,7 +57,6 @@ import 
org.apache.tomcat.util.net.SSLHostConfig.CertificateVerification;
 import org.apache.tomcat.util.net.SSLHostConfigCertificate;
 import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
 import org.apache.tomcat.util.net.SSLHostConfigPreSharedKey;
-import org.apache.tomcat.util.net.SSLUtilBase;
 import org.apache.tomcat.util.res.StringManager;
 
 /**
@@ -596,16 +595,9 @@ public class OpenSSLContext implements 
org.apache.tomcat.util.net.SSLContext {
             SSLContext.setCertificateChainFile(ctx,
                     
SSLHostConfig.adjustRelativePath(certificate.getCertificateChainFile()), false);
         } else {
-            String alias = certificate.getCertificateKeyAlias();
             X509KeyManager x509KeyManager = 
certificate.getCertificateKeyManager();
-            if (alias == null) {
-                alias = SSLUtilBase.DEFAULT_KEY_ALIAS;
-            }
+            String alias = getServedAlias(certificate);
             X509Certificate[] chain = 
x509KeyManager.getCertificateChain(alias);
-            if (chain == null) {
-                alias = certificate.chooseServerAlias(x509KeyManager);
-                chain = x509KeyManager.getCertificateChain(alias);
-            }
             PrivateKey key = x509KeyManager.getPrivateKey(alias);
             StringBuilder sb = new StringBuilder(BEGIN_KEY);
             sb.append(Base64.getMimeEncoder(64, new byte[] { '\n' 
}).encodeToString(key.getEncoded()));
@@ -688,20 +680,17 @@ public class OpenSSLContext implements 
org.apache.tomcat.util.net.SSLContext {
 
     @Override
     public X509Certificate[] getCertificateChain(String alias) {
-        X509Certificate[] chain = null;
         X509KeyManager x509KeyManager = certificate.getCertificateKeyManager();
-        if (x509KeyManager != null) {
-            if (alias == null) {
-                alias = SSLUtilBase.DEFAULT_KEY_ALIAS;
-            }
-            chain = x509KeyManager.getCertificateChain(alias);
-            if (chain == null) {
-                alias = certificate.chooseServerAlias(x509KeyManager);
-                chain = x509KeyManager.getCertificateChain(alias);
-            }
+        if (x509KeyManager == null) {
+            return null;
         }
+        return x509KeyManager.getCertificateChain(alias);
+    }
 
-        return chain;
+    @Override
+    public X509KeyManager[] getServerKeyManagers(SSLHostConfigCertificate 
certificate) {
+        X509KeyManager x509KeyManager = certificate.getCertificateKeyManager();
+        return x509KeyManager == null ? new X509KeyManager[0] : new 
X509KeyManager[] { x509KeyManager };
     }
 
     @Override
diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java 
b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java
index a9e3d9d624..b6b3ed6c49 100644
--- a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java
+++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java
@@ -63,7 +63,6 @@ import 
org.apache.tomcat.util.net.SSLHostConfig.CertificateVerification;
 import org.apache.tomcat.util.net.SSLHostConfigCertificate;
 import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
 import org.apache.tomcat.util.net.SSLHostConfigPreSharedKey;
-import org.apache.tomcat.util.net.SSLUtilBase;
 import org.apache.tomcat.util.net.openssl.OpenSSLConf;
 import org.apache.tomcat.util.net.openssl.OpenSSLConfCmd;
 import org.apache.tomcat.util.net.openssl.OpenSSLPreSharedKeySelector;
@@ -1445,16 +1444,9 @@ public class OpenSSLContext implements 
org.apache.tomcat.util.net.SSLContext {
                 BIO_free(certificateBIO);
             }
         } else {
-            String alias = certificate.getCertificateKeyAlias();
             X509KeyManager x509KeyManager = 
certificate.getCertificateKeyManager();
-            if (alias == null) {
-                alias = SSLUtilBase.DEFAULT_KEY_ALIAS;
-            }
+            String alias = getServedAlias(certificate);
             X509Certificate[] chain = 
x509KeyManager.getCertificateChain(alias);
-            if (chain == null) {
-                alias = certificate.chooseServerAlias(x509KeyManager);
-                chain = x509KeyManager.getCertificateChain(alias);
-            }
             String encodedKey = BEGIN_KEY + Base64.getMimeEncoder(64, new 
byte[] { '\n' })
                     
.encodeToString(x509KeyManager.getPrivateKey(alias).getEncoded()) + END_KEY;
             var rawCertificate = 
localArena.allocateFrom(ValueLayout.JAVA_BYTE, chain[0].getEncoded());
@@ -1628,20 +1620,18 @@ public class OpenSSLContext implements 
org.apache.tomcat.util.net.SSLContext {
 
     @Override
     public X509Certificate[] getCertificateChain(String alias) {
-        X509Certificate[] chain = null;
         X509KeyManager x509KeyManager = certificate.getCertificateKeyManager();
-        if (x509KeyManager != null) {
-            if (alias == null) {
-                alias = SSLUtilBase.DEFAULT_KEY_ALIAS;
-            }
-            chain = x509KeyManager.getCertificateChain(alias);
-            if (chain == null) {
-                alias = certificate.chooseServerAlias(x509KeyManager);
-                chain = x509KeyManager.getCertificateChain(alias);
-            }
+        if (x509KeyManager == null) {
+            return null;
         }
+        return x509KeyManager.getCertificateChain(alias);
+    }
+
 
-        return chain;
+    @Override
+    public X509KeyManager[] getServerKeyManagers(SSLHostConfigCertificate 
certificate) {
+        X509KeyManager x509KeyManager = certificate.getCertificateKeyManager();
+        return x509KeyManager == null ? new X509KeyManager[0] : new 
X509KeyManager[] { x509KeyManager };
     }
 
     @Override
diff --git 
a/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java 
b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java
index 83ceeb0827..f120d834a1 100644
--- 
a/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java
+++ 
b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java
@@ -197,7 +197,7 @@ public abstract class 
SSLUtilBaseCertificateSelectionTestBase extends TomcatBase
 
 
     protected static KeyStore orderedKeystore(String malformedAlias, 
KeystoreEntry... entries) throws Exception {
-        return orderedKeystore(sunMSCAPIProvider(), "Windows-MY", true, 
malformedAlias, entries);
+        return orderedKeystore(sunMSCAPIProvider(), "Windows-MY", 
KeyMode.EXPORTABLE, malformedAlias, entries);
     }
 
 
@@ -208,14 +208,25 @@ public abstract class 
SSLUtilBaseCertificateSelectionTestBase extends TomcatBase
      * mock otherwise takes.
      */
     protected static KeyStore nonExportableKeystore(KeystoreEntry... entries) 
throws Exception {
-        return orderedKeystore(sunMSCAPIProvider(), "Windows-MY", false, null, 
entries);
+        return orderedKeystore(sunMSCAPIProvider(), "Windows-MY", 
KeyMode.NON_EXPORTABLE, null, entries);
+    }
+
+
+    /*
+     * As nonExportableKeystore(KeystoreEntry...) - the keys report a null 
format so the original key store is kept and
+     * each key manager is wrapped - but getEncoded() still returns the real 
key material, so OpenSSL can load it. This
+     * reaches the wrapped path (ksUsed == ks) for the OpenSSL 
implementations, where a lookup of the configured alias
+     * would otherwise resolve the wrong certificate.
+     */
+    protected static KeyStore wrappedExportableKeystore(KeystoreEntry... 
entries) throws Exception {
+        return orderedKeystore(sunMSCAPIProvider(), "Windows-MY", 
KeyMode.WRAPPED_EXPORTABLE, null, entries);
     }
 
 
     protected static KeyStore nonSunMSCAPIKeystore(KeystoreEntry... entries) 
throws Exception {
         return orderedKeystore(new Provider("SUN", 1.0, "test") {
             private static final long serialVersionUID = 4434321038151233913L;
-        }, "JKS", true, null, entries);
+        }, "JKS", KeyMode.EXPORTABLE, null, entries);
     }
 
 
@@ -226,7 +237,20 @@ public abstract class 
SSLUtilBaseCertificateSelectionTestBase extends TomcatBase
     }
 
 
-    protected static KeyStore orderedKeystore(Provider provider, String type, 
boolean exportableKeys,
+    /*
+     * How the mocked SunMSCAPI private keys present themselves, which selects 
the branch of
+     * SSLUtilBase.getKeyManagers() under test. EXPORTABLE keys report PKCS#8 
(in-memory copy); NON_EXPORTABLE and
+     * WRAPPED_EXPORTABLE report a null format (original key store kept, key 
managers wrapped). WRAPPED_EXPORTABLE
+     * additionally exposes the key encoding so the OpenSSL implementations 
can load it.
+     */
+    protected enum KeyMode {
+        EXPORTABLE,
+        NON_EXPORTABLE,
+        WRAPPED_EXPORTABLE
+    }
+
+
+    protected static KeyStore orderedKeystore(Provider provider, String type, 
KeyMode keyMode,
             String malformedAlias, KeystoreEntry... entries) throws Exception {
         File file = 
TesterKeystoreGenerator.generateKeystore(Arrays.asList(entries));
         KeyStore delegate = KeyStore.getInstance("JKS");
@@ -255,13 +279,22 @@ public abstract class 
SSLUtilBaseCertificateSelectionTestBase extends TomcatBase
                 .andStubAnswer(() -> 
delegate.getCertificateChain(EasyMock.getCurrentArgument(0)));
         EasyMock.expect(spi.engineGetKey(EasyMock.anyString(), 
EasyMock.anyObject(char[].class))).andStubAnswer(() -> {
             Key key = delegate.getKey(EasyMock.getCurrentArgument(0), 
EasyMock.getCurrentArgument(1));
-            return exportableKeys ? key : nonExportableView(key);
+            switch (keyMode) {
+                case NON_EXPORTABLE:
+                    return nonExportableView(key);
+                case WRAPPED_EXPORTABLE:
+                    return wrappedExportableView(key);
+                case EXPORTABLE:
+                default:
+                    return key;
+            }
         });
         EasyMock.replay(spi);
-        // A real JKS backs the mock. With exportableKeys the keys report 
PKCS#8, so getKeyManagers() takes its
-        // in-memory-copy branch; with exportableKeys == false they report a 
null format (see nonExportableView), so it
-        // takes SunMSCAPI's non-exportable-key path instead. The provider 
name and store type are overridden only so
-        // isSunMSCAPIWindowsMY() can be exercised off Windows.
+        // A real JKS backs the mock. EXPORTABLE keys report PKCS#8, so 
getKeyManagers() takes its in-memory-copy
+        // branch; NON_EXPORTABLE and WRAPPED_EXPORTABLE keys report a null 
format (see nonExportableView /
+        // wrappedExportableView), so it keeps the original key store and 
wraps each key manager - WRAPPED_EXPORTABLE
+        // still exposes the key encoding so OpenSSL can load it. 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;
@@ -299,6 +332,36 @@ public abstract class 
SSLUtilBaseCertificateSelectionTestBase extends TomcatBase
     }
 
 
+    /*
+     * As nonExportableView(Key) but getEncoded() returns the real key 
material, modeling a SunMSCAPI key that keeps
+     * the original key store in use (null format) yet can still be exported 
into OpenSSL.
+     */
+    private static Key wrappedExportableView(Key key) {
+        if (!(key instanceof PrivateKey)) {
+            return key;
+        }
+        final PrivateKey privateKey = (PrivateKey) key;
+        return new PrivateKey() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public String getAlgorithm() {
+                return privateKey.getAlgorithm();
+            }
+
+            @Override
+            public String getFormat() {
+                return null;
+            }
+
+            @Override
+            public byte[] getEncoded() {
+                return privateKey.getEncoded();
+            }
+        };
+    }
+
+
     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);
diff --git 
a/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java 
b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java
index f9f2a4c0e6..0a8857ad90 100644
--- 
a/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java
+++ 
b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java
@@ -232,8 +232,7 @@ public class TestSSLUtilBaseConfiguredAliasSelection 
extends SSLUtilBaseCertific
     /*
      * After the SunMSCAPI numbered-duplicate swap the served alias is the 
numbered duplicate, not the configured alias.
      * This is the alias that logCertificate reports and the reload check 
resolves. Non-exportable keys route
-     * getKeyManagers() through the production ksUsed == ks path. Scoped to 
JSSE: for OpenSSL the served alias is
-     * resolved from the native layer by a separate path.
+     * getKeyManagers() through the production ksUsed == ks path.
      */
     @Test
     public void testServedAliasIsTheNumberedDuplicate() throws Exception {
@@ -241,4 +240,18 @@ public class TestSSLUtilBaseConfiguredAliasSelection 
extends SSLUtilBaseCertific
         KeyStore keystore = nonExportableKeystore(key(HOST, CLIENT_AUTH), 
key(HOST + " (1)", SERVER_AUTH));
         Assert.assertEquals(HOST + " (1)", servedAlias(HOST, keystore));
     }
+
+
+    /*
+     * A SunMSCAPI key that reports a null format but is still exportable 
keeps the original key store in use
+     * (ksUsed == ks) and wraps each key manager, like a real non-exportable 
key, while remaining loadable by OpenSSL.
+     * In that wrapped path a lookup of the configured alias still resolves 
its own (clientAuth) chain, so the served
+     * alias - not that lookup - must drive what OpenSSL installs.
+     */
+    @Test
+    public void testServedAliasResolvesForOpenSSLWithOriginalKeystore() throws 
Exception {
+        Assume.assumeTrue(useOpenSSL);
+        KeyStore keystore = wrappedExportableKeystore(key(HOST, CLIENT_AUTH), 
key(HOST + " (1)", SERVER_AUTH));
+        Assert.assertEquals(HOST + " (1)", servedAlias(HOST, keystore));
+    }
 }


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

Reply via email to