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 b8ebc644226f6494736f66e06a61ab4903a8b134
Author: Dimitris Soumis <[email protected]>
AuthorDate: Wed Nov 4 15:07:27 2026 +0200

    Prefer the configured alias when Tomcat did not wrap the key manager
    
    A FIPS key manager factory is used unwrapped, so its key manager picks an 
alias itself. Serve, load and monitor the configured alias when such a key 
manager holds it, and only log a resolved alias for a Windows-MY numbered 
duplicate rather than, e.g., a NewSunX509 synthetic alias.
---
 .../apache/tomcat/util/net/AbstractEndpoint.java   |   6 +-
 java/org/apache/tomcat/util/net/SSLContext.java    |  39 +++-
 .../tomcat/util/net/SSLHostConfigCertificate.java  |   8 +-
 .../SSLUtilBaseCertificateSelectionTestBase.java   |  12 +-
 .../tomcat/util/net/TestSSLContextServedAlias.java | 229 +++++++++++++++++++++
 5 files changed, 274 insertions(+), 20 deletions(-)

diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java 
b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
index 20bed17f8e..f504efb091 100644
--- a/java/org/apache/tomcat/util/net/AbstractEndpoint.java
+++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
@@ -555,10 +555,10 @@ public abstract class AbstractEndpoint<S, U> {
         if (sslContext != null) {
             String servedAlias = sslContext.getServedAlias(certificate);
 
-            // When a configured alias was resolved to a different alias than 
is actually served (e.g. a SunMSCAPI
-            // Windows-MY numbered duplicate), report it so the alias logged 
above is not mistaken for the one in use.
+            // Only a SunMSCAPI Windows-MY numbered duplicate ("alias (1)" 
etc., see SSLUtilBase) replaces a configured
+            // alias. Report it so the alias logged above is not mistaken for 
the one in use.
             String configuredAlias = certificate.getCertificateKeyAlias();
-            if (configuredAlias != null && servedAlias != null && 
!servedAlias.equalsIgnoreCase(configuredAlias)) {
+            if (configuredAlias != null && 
servedAlias.startsWith(configuredAlias + " (")) {
                 getLogCertificate()
                         
.info(sm.getString("endpoint.tls.info.cert.aliasResolved", configuredAlias, 
servedAlias));
             }
diff --git a/java/org/apache/tomcat/util/net/SSLContext.java 
b/java/org/apache/tomcat/util/net/SSLContext.java
index 0f282f3b2d..5cb645cbc6 100644
--- a/java/org/apache/tomcat/util/net/SSLContext.java
+++ b/java/org/apache/tomcat/util/net/SSLContext.java
@@ -28,6 +28,8 @@ import javax.net.ssl.SSLSessionContext;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.X509KeyManager;
 
+import org.apache.tomcat.util.net.jsse.JSSEKeyManager;
+
 /**
  * This interface is needed to override the default SSLContext class to allow 
SSL implementation pluggability without
  * having to use JCE. With regular JSSE it will do nothing but delegate to the 
SSLContext.
@@ -115,27 +117,44 @@ public interface SSLContext {
 
 
     /**
-     * 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. 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).
+     * Returns the alias of the certificate served for the given certificate 
configuration, for use with
+     * {@link #getCertificateChain(String)}. The key managers from {@link 
#getServerKeyManagers} are consulted in
+     * preference order and the first one that names an alias decides, just as 
JSSE only uses the first key manager:
+     * <ul>
+     * <li>A {@link JSSEKeyManager}, which Tomcat pins to an alias, serves 
that alias in every handshake. This can
+     *     differ from {@link 
SSLHostConfigCertificate#getCertificateKeyAlias()}, for example after SunMSCAPI
+     *     Windows-MY numbered-duplicate resolution.</li>
+     * <li>Any other key manager chooses an alias itself, for example one from 
a FIPS key manager factory, which Tomcat
+     *     does not wrap, or one over an in-memory key store that holds only 
the selected key. The configured alias
+     *     (or {@link SSLUtilBase#DEFAULT_KEY_ALIAS} when none is configured) 
is used if that key manager holds it,
+     *     otherwise the alias it would choose - for example for a key that an 
in-memory key store holds under another
+     *     alias, or the synthetic alias generated by the {@code NewSunX509} 
key manager.</li>
+     * </ul>
+     * If no key manager names an alias, the configured (or default) alias is 
returned.
      *
      * @param certificate The certificate configuration
      *
      * @return The served alias, never {@code null}
      */
     default String getServedAlias(SSLHostConfigCertificate certificate) {
+        String configuredAlias = certificate.getCertificateKeyAlias();
+        String alias = configuredAlias == null ? SSLUtilBase.DEFAULT_KEY_ALIAS 
: configuredAlias;
         for (X509KeyManager keyManager : getServerKeyManagers(certificate)) {
-            if (keyManager != null) {
-                String alias = certificate.chooseServerAlias(keyManager);
-                if (alias != null) {
+            if (keyManager == null) {
+                continue;
+            }
+            if (!(keyManager instanceof JSSEKeyManager)) {
+                X509Certificate[] chain = 
keyManager.getCertificateChain(alias);
+                if (chain != null && chain.length > 0) {
                     return alias;
                 }
             }
+            String chosenAlias = certificate.chooseServerAlias(keyManager);
+            if (chosenAlias != null) {
+                return chosenAlias;
+            }
         }
-        String configuredAlias = certificate.getCertificateKeyAlias();
-        return configuredAlias == null ? SSLUtilBase.DEFAULT_KEY_ALIAS : 
configuredAlias;
+        return alias;
     }
 
 
diff --git a/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java 
b/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java
index b413f90e13..464ee735e6 100644
--- a/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java
+++ b/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java
@@ -245,9 +245,11 @@ public class SSLHostConfigCertificate implements 
Serializable {
 
     /**
      * Returns the alias the given key manager would choose to serve for this 
certificate, or {@code null} if it has no
-     * suitable entry. When {@link #getType()} is {@link Type#UNDEFINED} every 
key type is tried. This reflects the
-     * alias actually used in a handshake, which can differ from {@link 
#getCertificateKeyAlias()} - for example after
-     * the SunMSCAPI Windows-MY numbered-duplicate resolution performed by 
SSLUtilBase#getKeyManagers().
+     * suitable entry. When {@link #getType()} is {@link Type#UNDEFINED} every 
key type is tried. For a key manager that
+     * Tomcat has pinned to an alias this is the alias used in every 
handshake, which can differ from
+     * {@link #getCertificateKeyAlias()} - for example after the SunMSCAPI 
Windows-MY numbered-duplicate resolution
+     * performed by SSLUtilBase#getKeyManagers(). Other key managers may 
choose differently in a handshake, for example
+     * depending on the negotiated key type or the requested server name.
      *
      * @param keyManager the key manager that will serve this certificate
      *
diff --git 
a/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java 
b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java
index 98e7a6e7ed..2dd91c959d 100644
--- 
a/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java
+++ 
b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java
@@ -52,10 +52,12 @@ import 
org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
 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.
+ * Shared fixtures for the {@link SSLUtilBase#getKeyManagers()} 
certificate-selection tests. The behaviors are split
+ * across the concrete subclasses: {@link 
TestSSLUtilBaseAutomaticCertificateSelection} covers Extended Key Usage based
+ * selection when no alias is configured, {@link 
TestSSLUtilBaseConfiguredAliasSelection} covers a configured alias,
+ * including the SunMSCAPI Windows-MY numbered-duplicate resolution, and 
{@link TestSSLContextServedAlias} covers how
+ * {@link SSLContext#getServedAlias(SSLHostConfigCertificate)} resolves the 
alias served by key managers that Tomcat did
+ * not wrap.
  * <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
@@ -381,6 +383,8 @@ public abstract class 
SSLUtilBaseCertificateSelectionTestBase extends TomcatBase
         spi.engineLoad(null, null);
         EasyMock.expectLastCall().once();
         EasyMock.expect(spi.engineAliases()).andStubAnswer(() -> 
Collections.enumeration(aliases));
+        EasyMock.expect(spi.engineContainsAlias(EasyMock.anyString()))
+                .andStubAnswer(() -> 
delegate.containsAlias(EasyMock.getCurrentArgument(0)));
         EasyMock.expect(spi.engineIsKeyEntry(EasyMock.anyString()))
                 .andStubAnswer(() -> 
delegate.isKeyEntry(EasyMock.getCurrentArgument(0)));
         
EasyMock.expect(spi.engineGetCertificate(EasyMock.anyString())).andStubAnswer(()
 -> {
diff --git a/test/org/apache/tomcat/util/net/TestSSLContextServedAlias.java 
b/test/org/apache/tomcat/util/net/TestSSLContextServedAlias.java
new file mode 100644
index 0000000000..a8d42bb70d
--- /dev/null
+++ b/test/org/apache/tomcat/util/net/TestSSLContextServedAlias.java
@@ -0,0 +1,229 @@
+/*
+ * 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.FileInputStream;
+import java.io.InputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.Security;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.KeyManagerFactorySpi;
+import javax.net.ssl.ManagerFactoryParameters;
+import javax.net.ssl.X509KeyManager;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import org.apache.tomcat.unittest.TesterLogValidationFilter;
+import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
+
+/**
+ * Tests how {@link SSLContext#getServedAlias(SSLHostConfigCertificate)} 
resolves the served alias when Tomcat did not
+ * wrap the key managers, so that the alias is chosen by the key manager 
itself rather than pinned by
+ * {@link org.apache.tomcat.util.net.jsse.JSSEKeyManager}, and how the served 
alias is reported when an endpoint logs
+ * the certificate. The served alias selects the certificate that OpenSSL 
loads and that is logged, reported by the
+ * Manager and monitored for expiry.
+ */
+@RunWith(Parameterized.class)
+public class TestSSLContextServedAlias extends 
SSLUtilBaseCertificateSelectionTestBase {
+
+    private static final String FIPS_PROVIDER = "TEST_FIPS_KEY_MANAGER";
+    private static final String FIPS_ALGORITHM = "TestFipsSunX509";
+
+
+    @After
+    public void removeFipsProvider() {
+        Security.removeProvider(FIPS_PROVIDER);
+    }
+
+
+    /*
+     * A FIPS key manager factory is used without wrapping, so its key manager 
picks a certificate by itself when asked.
+     * When that key manager holds the configured alias, the configured alias 
must still be the one that OpenSSL loads
+     * and that is logged and monitored, as it was before served aliases were 
resolved through the key manager. The
+     * configured alias is the one SunX509 does not pick, so a key manager 
choice that takes precedence fails the test.
+     */
+    @Test
+    public void testFipsKeyManagerServesConfiguredAlias() throws Exception {
+        KeyStore keystore = KeyStore.getInstance("JKS");
+        try (InputStream is = new FileInputStream(
+                keystoreFile(key("first", CertType.SERVER_AUTH), key("second", 
CertType.SERVER_AUTH)))) {
+            keystore.load(is, TesterSupport.JKS_PASS.toCharArray());
+        }
+        KeyManagerFactory sunX509 = KeyManagerFactory.getInstance("SunX509");
+        sunX509.init(keystore, TesterSupport.JKS_PASS.toCharArray());
+        String keyManagerChoice = ((X509KeyManager) 
sunX509.getKeyManagers()[0]).chooseServerAlias("RSA", null, null);
+        String configuredAlias = "first".equals(keyManagerChoice) ? "second" : 
"first";
+        Security.addProvider(new FipsKeyManagerFactoryProvider());
+
+        SSLHostConfigCertificate certificate = certificate(keystore, 
configuredAlias, FIPS_ALGORITHM);
+        SSLContext context = 
sslUtil(certificate).createSSLContext(Collections.emptyList());
+        try {
+            String servedAlias = context.getServedAlias(certificate);
+            Assert.assertEquals(configuredAlias, servedAlias);
+            Assert.assertEquals(keystore.getCertificate(configuredAlias), 
context.getCertificateChain(servedAlias)[0]);
+        } finally {
+            context.destroy();
+        }
+    }
+
+
+    /*
+     * The NewSunX509 key manager (algorithm PKIX) returns synthetic aliases 
of the form uid.index.alias and does not
+     * recognize the plain configured alias. The alias that key manager 
chooses must then be used, so that the
+     * configured certificate is still found rather than none at all.
+     */
+    @Test
+    public void testSyntheticAliasResolvesConfiguredCertificate() throws 
Exception {
+        KeyStore keystore = KeyStore.getInstance("JKS");
+        try (InputStream is = new FileInputStream(
+                keystoreFile(key(HOST, CertType.SERVER_AUTH), key("other", 
CertType.SERVER_AUTH)))) {
+            keystore.load(is, TesterSupport.JKS_PASS.toCharArray());
+        }
+
+        SSLHostConfigCertificate certificate = certificate(keystore, HOST, 
"PKIX");
+        SSLContext context = 
sslUtil(certificate).createSSLContext(Collections.emptyList());
+        try {
+            X509Certificate[] chain = 
context.getCertificateChain(context.getServedAlias(certificate));
+            Assert.assertNotNull("No certificate chain for the served alias", 
chain);
+            Assert.assertEquals(keystore.getCertificate(HOST), chain[0]);
+        } finally {
+            context.destroy();
+        }
+    }
+
+
+    /*
+     * A configured alias replaced by a SunMSCAPI Windows-MY numbered 
duplicate is reported at INFO after the
+     * certificate summary, which names the configured alias, so that it is 
not mistaken for the alias in use.
+     */
+    @Test
+    public void testNumberedDuplicateIsReportedWhenLogged() throws Exception {
+        KeyStore keystore = wrappedExportableKeystore(key(HOST, 
CertType.CLIENT_AUTH),
+                key(HOST + " (1)", CertType.SERVER_AUTH));
+        SSLHostConfigCertificate certificate = certificate(keystore, HOST, 
KeyManagerFactory.getDefaultAlgorithm());
+        Assert.assertEquals(1, infoLinesWhenLogged(certificate, "[" + HOST + " 
(1)]"));
+    }
+
+
+    /*
+     * A key manager that names entries itself, as NewSunX509 does with 
synthetic aliases of the form uid.index.alias,
+     * did not replace the configured alias, so no INFO line may report its 
synthetic alias as the one in use.
+     */
+    @Test
+    public void testSyntheticAliasIsNotReportedWhenLogged() throws Exception {
+        KeyStore keystore = KeyStore.getInstance("JKS");
+        try (InputStream is = new FileInputStream(
+                keystoreFile(key(HOST, CertType.SERVER_AUTH), key("other", 
CertType.SERVER_AUTH)))) {
+            keystore.load(is, TesterSupport.JKS_PASS.toCharArray());
+        }
+        SSLHostConfigCertificate certificate = certificate(keystore, HOST, 
"PKIX");
+        Assert.assertEquals(0, infoLinesWhenLogged(certificate, "." + HOST + 
"]"));
+    }
+
+
+    /*
+     * Logs the certificate as an endpoint does when it initializes TLS and 
returns the number of INFO lines that
+     * contain the given text.
+     */
+    private int infoLinesWhenLogged(SSLHostConfigCertificate certificate, 
String text) throws Exception {
+        SSLContext context = 
sslUtil(certificate).createSSLContext(Collections.emptyList());
+        try {
+            certificate.setSslContext(context);
+            NioEndpoint endpoint = new NioEndpoint();
+            // Creating the endpoint initialized NioEndpoint, so its 
certificate logger now exists
+            String loggerName = NioEndpoint.class.getName() + ".certificate";
+            TesterLogValidationFilter filter = 
TesterLogValidationFilter.add(Level.INFO, text, null, loggerName);
+            try {
+                endpoint.logCertificate(certificate);
+            } finally {
+                
LogManager.getLogManager().getLogger(loggerName).setFilter(null);
+            }
+            return filter.getMessageCount();
+        } finally {
+            context.destroy();
+        }
+    }
+
+
+    private static SSLHostConfigCertificate certificate(KeyStore keystore, 
String alias, String keyManagerAlgorithm) {
+        SSLHostConfig hostConfig = new SSLHostConfig();
+        hostConfig.setKeyManagerAlgorithm(keyManagerAlgorithm);
+        SSLHostConfigCertificate certificate = new 
SSLHostConfigCertificate(hostConfig, Type.UNDEFINED);
+        certificate.setCertificateKeystore(keystore);
+        certificate.setCertificateKeystoreType("JKS");
+        certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS);
+        certificate.setCertificateKeyAlias(alias);
+        hostConfig.addCertificate(certificate);
+        return certificate;
+    }
+
+
+    /*
+     * Presents as a FIPS module (its provider information contains "FIPS"), 
so SSLUtilBase passes it the key store
+     * unmodified and uses its key managers unwrapped, while SunX509 does the 
actual key management.
+     */
+    private static final class FipsKeyManagerFactoryProvider extends Provider {
+        private static final long serialVersionUID = 1L;
+
+        FipsKeyManagerFactoryProvider() {
+            super(FIPS_PROVIDER, 1.0, "Test FIPS key manager factory");
+            put("KeyManagerFactory." + FIPS_ALGORITHM, 
FipsKeyManagerFactorySpi.class.getName());
+        }
+    }
+
+
+    public static final class FipsKeyManagerFactorySpi extends 
KeyManagerFactorySpi {
+
+        private final KeyManagerFactory delegate;
+
+        public FipsKeyManagerFactorySpi() throws NoSuchAlgorithmException {
+            delegate = KeyManagerFactory.getInstance("SunX509");
+        }
+
+        @Override
+        protected void engineInit(KeyStore ks, char[] password)
+                throws KeyStoreException, NoSuchAlgorithmException, 
UnrecoverableKeyException {
+            delegate.init(ks, password);
+        }
+
+        @Override
+        protected void engineInit(ManagerFactoryParameters spec) throws 
InvalidAlgorithmParameterException {
+            delegate.init(spec);
+        }
+
+        @Override
+        protected KeyManager[] engineGetKeyManagers() {
+            return delegate.getKeyManagers();
+        }
+    }
+}


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

Reply via email to