This is an automated email from the ASF dual-hosted git repository. dsoumis pushed a commit to branch 11.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit c8ea979e0d5ca0a176afbaddf1ec860882546ce5 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 | 223 +++++++++++++++++++++ 5 files changed, 268 insertions(+), 20 deletions(-) diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java b/java/org/apache/tomcat/util/net/AbstractEndpoint.java index efd5290f0c..57f93723b2 100644 --- a/java/org/apache/tomcat/util/net/AbstractEndpoint.java +++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java @@ -651,10 +651,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 3e59bc541f..bd08a47402 100644 --- a/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java +++ b/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java @@ -247,9 +247,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 fb90d22679..0071964603 100644 --- a/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java +++ b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java @@ -49,10 +49,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 @@ -339,6 +341,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..5497c4df37 --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestSSLContextServedAlias.java @@ -0,0 +1,223 @@ +/* + * 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.Serial; +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.List; +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( + keystoreFile(key("first", CertType.SERVER_AUTH), key("second", CertType.SERVER_AUTH)), + 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(List.of()); + 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( + keystoreFile(key(HOST, CertType.SERVER_AUTH), key("other", CertType.SERVER_AUTH)), + TesterSupport.JKS_PASS.toCharArray()); + + SSLHostConfigCertificate certificate = certificate(keystore, HOST, "PKIX"); + SSLContext context = sslUtil(certificate).createSSLContext(List.of()); + 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( + keystoreFile(key(HOST, CertType.SERVER_AUTH), key("other", CertType.SERVER_AUTH)), + 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(List.of()); + 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 { + @Serial + 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]
