From 81dc1a179da77cd4ecd3e81bb58fa9a4a675ced4 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 22 Oct 2026 11:41:56 +0300 Subject: [PATCH 01/20] Document Windows keystores and TLS certificate reloading --- webapps/docs/changelog.xml | 4 +++ webapps/docs/config/http.xml | 10 ++++-- webapps/docs/ssl-howto.xml | 70 ++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index f5ef7143050c..724de2ce01f4 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -816,6 +816,10 @@ Manager: Drop session handling dedicated to extracting the locale from Tapestry attributes, used for locale session sorting. (remm) + + Documentation: Describe how to configure the Windows certificate store + and reload externally renewed TLS certificates. (dsoumis) + Host Manager: Host names are now always converted to lower case before diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml index 98607caa227b..2b07d251163d 100644 --- a/webapps/docs/config/http.xml +++ b/webapps/docs/config/http.xml @@ -1859,9 +1859,13 @@

In addition to the standard key store types (JKS and PKCS12), most Java - runtimes support additional key store types such as Windows-ROOT, - Windows-My, DKS as well as hardware security modules. Generally, to use - these additional keystore types with a TLS Connector in Tomcat:

+ runtimes support additional key store types such as Windows-MY, Windows-ROOT, + DKS as well as hardware security modules. On Windows, Windows-MY and + Windows-ROOT access the current user's certificate store, while + Windows-MY-LOCALMACHINE and Windows-ROOT-LOCALMACHINE access the local machine + store (reading the local machine store from a non-elevated process requires + Java 17.0.17, 21.0.9, 24 or later). Generally, to use these additional key + store types with a TLS Connector in Tomcat:

  • Set the certificateKeystoreType and/or truststoreType Connector diff --git a/webapps/docs/ssl-howto.xml b/webapps/docs/ssl-howto.xml index 8eee9f1210a4..d86ec7a0fc24 100644 --- a/webapps/docs/ssl-howto.xml +++ b/webapps/docs/ssl-howto.xml @@ -446,6 +446,76 @@ mailing list.

    +
    + +

    When certificates are renewed by an external process the +TLS certificate reload listener can load the renewed certificate without +restarting Tomcat. Add it to the Server element:

    + + + + ... +]]> + +

    The listener periodically checks each certificate and, when one is within +daysBefore days of expiry (14 by default), reloads the associated +SSLHostConfig so that the renewed certificate is picked up. The +certificate must be renewed by another process; the listener does not renew it +or re-read server.xml. See the +listener documentation for the available +attributes.

    + +
    + +
    + +

    On Windows, Tomcat can read certificates directly from the Windows certificate +store. Set certificateKeystoreType to +Windows-MY-LOCALMACHINE for the local machine store, or to +Windows-MY for the current user's store. Reading the local machine +store from a Tomcat service that is not running with elevated privileges requires +Java 17.0.17, 21.0.9, 24, or a later release; on earlier releases the service must +run with elevated privileges. See +JDK-8313367. +AD CS autoenrollment can issue and renew the machine's certificates +automatically.

    + +

    A Windows certificate store may contain several certificates for the same +machine. Configure certificateKeyAlias with the alias exposed +by Java for the intended certificate. If no alias is configured, Tomcat uses the +first key read from the store, in an implementation-dependent order. The +following example uses the alias tomcat; replace it with the alias +of the certificate to use:

    + + + + + +]]> + +

    When using the +reload listener +with a configured alias, ensure that the +renewed certificate is available under that alias. Reloading does not select a +different alias or prefer a certificate with a later expiry date.

    + +

    Two points to note:

    + +
      +
    • The account running Tomcat must have read access to the certificate's private + key. A renewed certificate may use a new key that does not inherit this + access, so grant it as part of the enrollment or renewal process.
    • +
    • Setting a unique friendly name on the intended certificate can help identify + it for use as the certificateKeyAlias. Verify the alias + exposed by Java, especially when several certificates share a name.
    • +
    + +
    +

    Tomcat supports the Online Certificate Status Protocol (OCSP) to verify the status of client provided certificates for a sub-set of connector From a87c32722cd8f1e5793a24e4e3fc259ac9e4dfed Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 22 Oct 2026 18:33:53 +0300 Subject: [PATCH 02/20] Select a key store certificate valid for TLS server authentication A key store can hold more than one certificate for a host. For example, a Windows-MY store populated by AD CS autoenrollment may contain both a TLS server certificate and a client authentication certificate. When no key alias was configured, automatic selection used the first key entry regardless of its key usage and could pick a certificate that is not valid for TLS server authentication. When no certificateKeyAlias is configured, skip key entries whose certificate is not valid for TLS server authentication. A certificate qualifies if its Extended Key Usage includes serverAuth or anyExtendedKeyUsage, or if it has no Extended Key Usage extension. A malformed Extended Key Usage is treated as not valid and ignored. If no eligible key entry is found, connector initialization fails. A configured certificateKeyAlias is always used unchanged, even if its certificate is not valid for server authentication. --- .../org/apache/tomcat/util/net/Constants.java | 6 + .../tomcat/util/net/LocalStrings.properties | 1 + .../apache/tomcat/util/net/SSLUtilBase.java | 59 +++- .../net/TestSSLUtilBaseKeyUsageSelection.java | 333 ++++++++++++++++++ .../util/net/TesterKeystoreGenerator.java | 84 ++++- webapps/docs/changelog.xml | 9 + webapps/docs/config/http.xml | 9 +- webapps/docs/ssl-howto.xml | 19 +- 8 files changed, 503 insertions(+), 17 deletions(-) create mode 100644 test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java diff --git a/java/org/apache/tomcat/util/net/Constants.java b/java/org/apache/tomcat/util/net/Constants.java index 1669adbfb3b6..bf48de922f60 100644 --- a/java/org/apache/tomcat/util/net/Constants.java +++ b/java/org/apache/tomcat/util/net/Constants.java @@ -68,6 +68,12 @@ public class Constants { */ public static final String SSL_PROTO_SSLv2Hello = "SSLv2Hello"; + /** + * EKU OIDs. + */ + public static final String SERVER_AUTH_EXTENDED_KEY_USAGE_OID = "1.3.6.1.5.5.7.3.1"; + public static final String ANY_EXTENDED_KEY_USAGE_OID = "2.5.29.37.0"; + /** * Private constructor to prevent instantiation. */ diff --git a/java/org/apache/tomcat/util/net/LocalStrings.properties b/java/org/apache/tomcat/util/net/LocalStrings.properties index 6bc78573bdfc..85f22ff48e59 100644 --- a/java/org/apache/tomcat/util/net/LocalStrings.properties +++ b/java/org/apache/tomcat/util/net/LocalStrings.properties @@ -181,6 +181,7 @@ sslUtilBase.noKeys=No aliases for private keys found in key store sslUtilBase.noVerificationDepth=The truststoreProvider [{0}] does not support the certificateVerificationDepth configuration option sslUtilBase.noneSupported=None of the [{0}] specified are supported by the SSL engine : [{1}] sslUtilBase.skipped=Tomcat interprets the [{0}] attribute in a manner consistent with the latest OpenSSL development branch. Some of the specified [{0}] are not supported by the configured SSL engine for this connector (which may use JSSE or an older OpenSSL version) and have been skipped: [{1}] +sslUtilBase.skippedKeyUsage=Skipping certificate with alias [{0}] as it is not valid for TLS server authentication sslUtilBase.ssl3=SSLv3 has been explicitly enabled. This protocol is known to be insecure. sslUtilBase.tls13.auth=The JSSE TLS 1.3 implementation does not support post handshake authentication (PHA) and is therefore incompatible with optional certificate authentication sslUtilBase.trustedCertNotChecked=The validity dates of the trusted certificate with alias [{0}] were not checked as the certificate was of an unknown type diff --git a/java/org/apache/tomcat/util/net/SSLUtilBase.java b/java/org/apache/tomcat/util/net/SSLUtilBase.java index 68ac2ecbb899..54dbbfb24fa6 100644 --- a/java/org/apache/tomcat/util/net/SSLUtilBase.java +++ b/java/org/apache/tomcat/util/net/SSLUtilBase.java @@ -36,6 +36,7 @@ import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateFactory; import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CertificateParsingException; import java.security.cert.CollectionCertStoreParameters; import java.security.cert.PKIXBuilderParameters; import java.security.cert.PKIXRevocationChecker; @@ -401,10 +402,18 @@ public KeyManager[] getKeyManagers() throws Exception { throw new IOException(sm.getString("sslUtilBase.noKeys")); } while (aliases.hasMoreElements() && keyAlias == null) { - keyAlias = aliases.nextElement(); - if (!ks.isKeyEntry(keyAlias)) { - keyAlias = null; + String candidate = aliases.nextElement(); + if (!ks.isKeyEntry(candidate)) { + continue; } + Certificate cert = ks.getCertificate(candidate); + if (cert instanceof X509Certificate x509 && !isServerAuthCapable(x509)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("sslUtilBase.skippedKeyUsage", candidate)); + } + continue; + } + keyAlias = candidate; } if (keyAlias == null) { throw new IOException(sm.getString("sslUtilBase.alias_no_key_entry", (Object) null)); @@ -450,6 +459,50 @@ public KeyManager[] getKeyManagers() throws Exception { } + /** + * Determines whether the given certificate may be used for TLS server authentication, based on its Extended Key + * Usage (EKU) extension. Used by {@link #getKeyManagers()} when auto-selecting a key entry so that certificates + * which are not valid for server authentication are ignored. + *

    + * The policy is deliberately lenient about absent usage, strict about declared usage and tolerant of a malformed + * extension: + *

      + *
    • A certificate with no EKU extension ({@link X509Certificate#getExtendedKeyUsage()} returns + * {@code null}) is treated as usable. An absent EKU places no restriction on the certificate's purpose + * so rejecting them would needlessly exclude the many server certificates issued without an EKU.
    • + *
    • A certificate that does carry an EKU extension is usable only if that extension explicitly + * lists the {@code id-kp-serverAuth} OID ({@value Constants#SERVER_AUTH_EXTENDED_KEY_USAGE_OID}) or the + * {@code anyExtendedKeyUsage} OID ({@value Constants#ANY_EXTENDED_KEY_USAGE_OID}). Any other declared usage + * - for example a certificate limited to client authentication - is rejected.
    • + *
    • A certificate whose EKU extension cannot be decoded is ignored (treated as not usable).
    • + *
    + * + * @param cert the certificate to test + * + * @return {@code true} if the certificate may be used for TLS server authentication + */ + private static boolean isServerAuthCapable(X509Certificate cert) { + List objectIdentifiers; + try { + objectIdentifiers = cert.getExtendedKeyUsage(); + } catch (CertificateParsingException e) { + return false; + } + + if (objectIdentifiers == null) { + return true; + } + + for (String objectIdentifier : objectIdentifiers) { + if (objectIdentifier.equals(Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID) || + objectIdentifier.equals(Constants.ANY_EXTENDED_KEY_USAGE_OID)) { + return true; + } + } + + return false; + } + @Override public String[] getEnabledProtocols() { return enabledProtocols; diff --git a/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java b/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java new file mode 100644 index 000000000000..b8594162fd2f --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java @@ -0,0 +1,333 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.util.net; + +import java.io.File; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreSpi; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.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 parameters() { + return List.of(new Object[] { "JSSE", Boolean.FALSE, + "org.apache.tomcat.util.net.jsse.JSSEImplementation" }, + new Object[] { "OpenSSL", Boolean.TRUE, + "org.apache.tomcat.util.net.openssl.OpenSSLImplementation" }, + new Object[] { "OpenSSL-FFM", Boolean.TRUE, + "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation" }); + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean useOpenSSL; + + @Parameter(2) + public String sslImplementationName; + + + @Before + public void configureSSLImplementation() throws Exception { + TesterSupport.configureSSLImplementation(getTomcatInstance(), sslImplementationName, useOpenSSL); + getTomcatInstance().getServer().init(); + } + + private static final String HOST = "localhost"; + + private static final String CLIENT_AUTH_EXTENDED_KEY_USAGE_OID = "1.3.6.1.5.5.7.3.2"; + + private static final CertificateExtensionsCustomizer SERVER_AUTH = (keyPair, builder) -> builder + .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)); + private static final CertificateExtensionsCustomizer CLIENT_AUTH = (keyPair, builder) -> builder + .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth)); + private static final CertificateExtensionsCustomizer ANY_AUTH = (keyPair, builder) -> builder + .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage)); + + + private static KeystoreEntry key(String alias, CertificateExtensionsCustomizer eku) { + return KeystoreEntry.key(alias, HOST, eku); + } + + + private X509Certificate present(String alias, KeystoreEntry... entries) throws Exception { + return present(alias, orderedKeystore(null, entries)); + } + + + private X509Certificate present(String alias, KeyStore keystore) throws Exception { + X509KeyManager keyManager = (X509KeyManager) sslUtil(alias, keystore).getKeyManagers()[0]; + X509Certificate[] chain = keyManager.getCertificateChain(keyManager.chooseServerAlias("RSA", null, null)); + Assert.assertNotNull("Key manager returned no certificate chain", chain); + return chain[0]; + } + + + private SSLUtil sslUtil(String alias, KeyStore keystore) throws Exception { + + SSLHostConfig hostConfig = new SSLHostConfig(); + SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); + certificate.setCertificateKeystore(keystore); + certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS); + if (alias != null) { + certificate.setCertificateKeyAlias(alias); + } + + return sslUtil(certificate); + } + + + private SSLUtil sslUtil(SSLHostConfigCertificate certificate) throws Exception { + SSLUtil util = SSLImplementation.getInstance(sslImplementationName).getSSLUtil(certificate); + SSLHostConfig hostConfig = certificate.getSSLHostConfig(); + hostConfig.setEnabledProtocols(util.getEnabledProtocols()); + hostConfig.setEnabledCiphers(util.getEnabledCiphers()); + return util; + } + + + private static KeyStore orderedKeystore(String malformedAlias, KeystoreEntry... entries) throws Exception { + File file = TesterKeystoreGenerator.generateKeystore(List.of(entries)); + KeyStore delegate = KeyStore.getInstance(file, TesterSupport.JKS_PASS.toCharArray()); + List aliases = Stream.of(entries).map(KeystoreEntry::alias).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 extendedKeyUsage = certificate.getExtendedKeyUsage(); + Assert.assertNotNull("Presented certificate has no Extended Key Usage", extendedKeyUsage); + Assert.assertTrue("Presented certificate must be valid for Extended Key Usage " + oid, + extendedKeyUsage.contains(oid)); + } + + + /* + * A configured alias must be honored exactly, even when its certificate is not valid for server authentication and + * a server-auth certificate sharing the Subject is also present. Auto-selection must never override the user's + * choice (see the uses of SSLHostConfigCertificate.getCertificateKeyAlias() across the connectors). + */ + @Test + public void testConfiguredAliasIsUsedUnchangedWhenNotServerAuth() throws Exception { + X509Certificate presented = present(HOST, + key(HOST, CLIENT_AUTH), + key("localhost-server", SERVER_AUTH)); + assertEku(presented, CLIENT_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testConfiguredAliasIsUsedWhenServerAuth() throws Exception { + X509Certificate presented = present(HOST, + key(HOST, SERVER_AUTH), + key("localhost-client", CLIENT_AUTH)); + assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testAutoSelectPrefersServerAuthOverClientAuth() throws Exception { + X509Certificate presented = present(null, + key("first", CLIENT_AUTH), + key("second", SERVER_AUTH)); + assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testAutoSelectAcceptsAnyExtendedKeyUsage() throws Exception { + X509Certificate presented = present(null, + key("first", CLIENT_AUTH), + key("any", ANY_AUTH)); + assertEku(presented, Constants.ANY_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testAutoSelectAcceptsCertificateWithoutExtendedKeyUsage() throws Exception { + X509Certificate presented = present(null, + key("first", CLIENT_AUTH), + key("none", null)); + Assert.assertNull("A certificate with no Extended Key Usage is unrestricted and must be selectable", + presented.getExtendedKeyUsage()); + } + + + /* + * Trusted (keyless) certificate entries are not key entries, so they can never be selected as the server key even + * when they are the only server-auth-capable certificate. + */ + @Test + public void testAutoSelectSkipsTrustedEntries() { + Assert.assertThrows(IOException.class, () -> present(null, + KeystoreEntry.trusted("ca", HOST, SERVER_AUTH), + key("key", CLIENT_AUTH))); + } + + + /* + * No unsuitable key entry may be selected when all certificates exclude server authentication. + */ + @Test + public void testAutoSelectFailsWhenNoneServerAuth() { + Assert.assertThrows(IOException.class, () -> present(null, + key("first", CLIENT_AUTH), + key("second", CLIENT_AUTH))); + } + + + @Test + public void testAutoSelectSkipsMalformedExtendedKeyUsage() throws Exception { + KeyStore keystore = orderedKeystore("malformed", key("malformed", CLIENT_AUTH), key("server", SERVER_AUTH)); + assertEku(present(null, keystore), Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testConfiguredAliasWithMalformedExtendedKeyUsageIsUsed() throws Exception { + KeyStore keystore = orderedKeystore("malformed", key("malformed", CLIENT_AUTH), key("server", SERVER_AUTH)); + Assert.assertEquals(keystore.getCertificateChain("malformed")[0], present("malformed", keystore)); + } + + + @Test + public void testAutoSelectFailsWhenOnlyMalformedExtendedKeyUsage() throws Exception { + KeyStore keystore = orderedKeystore("malformed", key("malformed", CLIENT_AUTH)); + Assert.assertThrows(IOException.class, () -> present(null, keystore)); + } + + + @Test + public void testAutoSelectUsesFirstEligibleKey() throws Exception { + KeyStore keystore = orderedKeystore(null, key("client", CLIENT_AUTH), key("first", ANY_AUTH), + key("second", SERVER_AUTH)); + Assert.assertEquals(keystore.getCertificate("first"), present(null, keystore)); + } + + + @Test + public void testConfiguredMissingAliasFails() { + Assert.assertThrows(IOException.class, () -> present("missing", key("server", SERVER_AUTH))); + } + + + @Test + public void testConfiguredTrustedAliasFails() { + Assert.assertThrows(IOException.class, () -> present("trusted", KeystoreEntry.trusted("trusted", HOST, SERVER_AUTH), + key("server", SERVER_AUTH))); + } + + + @Test + public void testAutoSelectDoesNotSkipKeyReadFailure() throws Exception { + KeyStore keystore = orderedKeystore(null, key("first", SERVER_AUTH), key("second", SERVER_AUTH)); + SSLHostConfig hostConfig = new SSLHostConfig(); + SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); + certificate.setCertificateKeystore(keystore); + certificate.setCertificateKeyPassword("incorrect"); + SSLUtil util = SSLImplementation.getInstance(sslImplementationName).getSSLUtil(certificate); + Assert.assertThrows(UnrecoverableKeyException.class, util::getKeyManagers); + } + + + @Test + public void testSSLContextUsesSelectedCertificate() throws Exception { + KeyStore keystore = orderedKeystore(null, key("client", CLIENT_AUTH), key("server", SERVER_AUTH)); + SSLContext context = sslUtil(null, keystore).createSSLContext(List.of()); + try { + Assert.assertEquals(keystore.getCertificate("server"), context.getCertificateChain("server")[0]); + } finally { + context.destroy(); + } + } + + + @Test + public void testSSLContextLoadsSelectedCertificateFromKeystoreFile() throws Exception { + File file = TesterKeystoreGenerator.generateKeystore( + List.of(key(SSLUtilBase.DEFAULT_KEY_ALIAS, CLIENT_AUTH), key("server", SERVER_AUTH))); + KeyStore keystore = KeyStore.getInstance(file, TesterSupport.JKS_PASS.toCharArray()); + SSLHostConfig hostConfig = new SSLHostConfig(); + SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); + certificate.setCertificateKeystoreFile(file.getAbsolutePath()); + certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS); + SSLContext context = sslUtil(certificate).createSSLContext(List.of()); + try { + Assert.assertEquals(keystore.getCertificate("server"), context.getCertificateChain("server")[0]); + } finally { + context.destroy(); + } + } +} diff --git a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java index 00f1772fccea..480ddf045b68 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 org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.Extension; @@ -50,25 +51,84 @@ void customize(KeyPair keyPair, X509v3CertificateBuilder certBuilder) } /** - * Generate a temporary JKS keystore containing a self-signed RSA certificate. + * Generate a temporary JKS keystore containing a single self-signed RSA certificate. * - * @param cn the Common Name for the certificate subject - * @param alias the keystore alias for the key entry - * @param sanNames DNS Subject Alternative Names to include, or {@code null} for none - * @param customizer callback to add extensions to the certificate, or {@code null} for none. + * @param cn the Common Name for the certificate subject + * @param alias the keystore alias for the key entry + * @param sanNames DNS Subject Alternative Names to include, or {@code null} for none + * @param customizer callback to add extensions to the certificate, or {@code null} for none * - * @return a temporary keystore file with password {@link TesterSupport#JKS_PASS} + * @return a temporary keystore file with password {@link TesterSupport#JKS_PASS} * - * @throws Exception if certificate generation or keystore creation fails + * @throws Exception if certificate generation or keystore creation fails */ public static File generateKeystore(String cn, String alias, String[] sanNames, CertificateExtensionsCustomizer customizer) throws Exception { + CertAndKey generated = generateSelfSignedCertificate(cn, sanNames, customizer); + + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(null, null); + ks.setKeyEntry(alias, generated.keyPair().getPrivate(), TesterSupport.JKS_PASS.toCharArray(), + new X509Certificate[] { generated.certificate() }); + + return storeToTempFile(ks); + } + + /** + * Generate a temporary JKS keystore from an explicit list of entries. Each entry has its own Common Name and may be + * either a private key entry or a trusted certificate entry, which lets tests reproduce stores that mix certificates + * with different Subjects and certificate-only (keyless) entries. + * + * @param entries the entries to add, in order + * + * @return a temporary keystore file with password {@link TesterSupport#JKS_PASS} + * + * @throws Exception if certificate generation or keystore creation fails + */ + public static File generateKeystore(List entries) throws Exception { + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(null, null); + + for (KeystoreEntry entry : entries) { + CertAndKey generated = generateSelfSignedCertificate(entry.cn(), null, entry.customizer()); + if (entry.keyEntry()) { + ks.setKeyEntry(entry.alias(), generated.keyPair().getPrivate(), TesterSupport.JKS_PASS.toCharArray(), + new X509Certificate[] { generated.certificate() }); + } else { + ks.setCertificateEntry(entry.alias(), generated.certificate()); + } + } + + return storeToTempFile(ks); + } + + /** + * A single entry for {@link #generateKeystore(List)}. + * + * @param alias the keystore alias + * @param cn the certificate Subject Common Name + * @param keyEntry {@code true} for a private key entry, {@code false} for a trusted certificate entry + * @param customizer extensions to add to the certificate, or {@code null} for none + */ + public record KeystoreEntry(String alias, String cn, boolean keyEntry, CertificateExtensionsCustomizer customizer) { + + public static KeystoreEntry key(String alias, String cn, CertificateExtensionsCustomizer customizer) { + return new KeystoreEntry(alias, cn, true, customizer); + } + + public static KeystoreEntry trusted(String alias, String cn, CertificateExtensionsCustomizer customizer) { + return new KeystoreEntry(alias, cn, false, customizer); + } + } + + private static CertAndKey generateSelfSignedCertificate(String cn, String[] sanNames, + CertificateExtensionsCustomizer customizer) throws Exception { KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(4096); KeyPair keyPair = kpg.generateKeyPair(); X500Name subject = new X500Name("CN=" + cn); - BigInteger serial = BigInteger.valueOf(System.currentTimeMillis()); + BigInteger serial = BigInteger.valueOf(System.nanoTime()); long oneDay = 86400000L; Date notBefore = new Date(System.currentTimeMillis() - oneDay); Date notAfter = new Date(System.currentTimeMillis() + 365L * oneDay); @@ -90,17 +150,17 @@ public static File generateKeystore(String cn, String alias, String[] sanNames, ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()); X509Certificate certificate = new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer)); + return new CertAndKey(keyPair, certificate); + } - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(null, null); - ks.setKeyEntry(alias, keyPair.getPrivate(), TesterSupport.JKS_PASS.toCharArray(), new X509Certificate[] { certificate }); + private record CertAndKey(KeyPair keyPair, X509Certificate certificate) {} + private static File storeToTempFile(KeyStore ks) throws Exception { File keystoreFile = File.createTempFile("test-cert-", ".jks"); keystoreFile.deleteOnExit(); try (FileOutputStream fos = new FileOutputStream(keystoreFile)) { ks.store(fos, TesterSupport.JKS_PASS.toCharArray()); } - return keystoreFile; } diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 724de2ce01f4..e982b23a3d86 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -484,6 +484,15 @@ drop TLS 1.3 when rebuilding the list of enabled protocols during initialization. (dsoumis) + + When a Certificate is configured using a key store and no + certificateKeyAlias 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 certificateKeyAlias is always used unchanged. + (dsoumis) + Keep the asynchronous timeout in place until the timeout event has diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml index 2b07d251163d..bcea8da85b01 100644 --- a/webapps/docs/config/http.xml +++ b/webapps/docs/config/http.xml @@ -1664,12 +1664,19 @@

    JSSE only.

    The alias used for the server key and certificate in the keystore. If - not specified, the first key read from the keystore will be used. The + not specified, the first eligible key read from the keystore will be used. The order in which keys are read from the keystore is implementation dependent. It may not be the case that keys are read from the keystore in the same order as they were added. If more than one key is present in the keystore it is strongly recommended that a keyAlias is configured to ensure that the correct key is used.

    +

    If no alias is specified, key entries whose certificate has a malformed + Extended Key Usage extension, or one that includes neither + serverAuth nor anyExtendedKeyUsage, 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.

    diff --git a/webapps/docs/ssl-howto.xml b/webapps/docs/ssl-howto.xml index d86ec7a0fc24..441c85164235 100644 --- a/webapps/docs/ssl-howto.xml +++ b/webapps/docs/ssl-howto.xml @@ -446,6 +446,23 @@ mailing list.

    +
    + +

    When no certificateKeyAlias is configured, Tomcat selects the +first eligible key entry in the key store's enumeration order. A certificate +qualifies if its Extended Key Usage includes +serverAuth or anyExtendedKeyUsage, or if it has no +Extended Key Usage extension at all. Certificates whose Extended Key Usage +excludes both purposes or cannot be parsed are ignored. If no eligible key entry +is found, connector initialization fails.

    + +

    A configured certificateKeyAlias 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.

    + +
    +

    When certificates are renewed by an external process the @@ -483,7 +500,7 @@ automatically.

    A Windows certificate store may contain several certificates for the same machine. Configure certificateKeyAlias with the alias exposed by Java for the intended certificate. If no alias is configured, Tomcat uses the -first key read from the store, in an implementation-dependent order. The +first eligible key read from the store, in an implementation-dependent order. The following example uses the alias tomcat; replace it with the alias of the certificate to use:

    From 6ca553a495ee54b6740edf18a379a7ddd32b1c15 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Wed, 28 Oct 2026 15:22:38 +0200 Subject: [PATCH 03/20] 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. --- .../org/apache/tomcat/util/net/Constants.java | 12 + .../tomcat/util/net/LocalStrings.properties | 2 + .../apache/tomcat/util/net/SSLUtilBase.java | 72 +++- ...LUtilBaseCertificateSelectionTestBase.java | 201 +++++++++++ ...UtilBaseAutomaticCertificateSelection.java | 150 ++++++++ ...stSSLUtilBaseConfiguredAliasSelection.java | 190 ++++++++++ .../net/TestSSLUtilBaseKeyUsageSelection.java | 333 ------------------ .../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, 671 insertions(+), 345 deletions(-) create mode 100644 test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java create mode 100644 test/org/apache/tomcat/util/net/TestSSLUtilBaseAutomaticCertificateSelection.java create mode 100644 test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java delete mode 100644 test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java diff --git a/java/org/apache/tomcat/util/net/Constants.java b/java/org/apache/tomcat/util/net/Constants.java index bf48de922f60..28fb7140f45c 100644 --- a/java/org/apache/tomcat/util/net/Constants.java +++ b/java/org/apache/tomcat/util/net/Constants.java @@ -74,6 +74,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 85f22ff48e59..b911b3ac7212 100644 --- a/java/org/apache/tomcat/util/net/LocalStrings.properties +++ b/java/org/apache/tomcat/util/net/LocalStrings.properties @@ -178,8 +178,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 54dbbfb24fa6..761915661c61 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.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; @@ -394,13 +395,12 @@ public KeyManager[] getKeyManagers() throws Exception { 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 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)) { @@ -411,13 +411,33 @@ public KeyManager[] getKeyManagers() throws Exception { 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 x509) || !isServerAuthCapable(x509)) { + 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); @@ -459,6 +479,38 @@ public KeyManager[] getKeyManagers() throws Exception { } + /** + * 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 x509 && ks.isKeyEntry(candidateAlias) && + isServerAuthCapable(x509)) { + 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 @@ -503,6 +555,20 @@ private static boolean isServerAuthCapable(X509Certificate cert) { 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 + return switch (ks.getType().toUpperCase(Locale.ROOT)) { + case Constants.WINDOWS_MY_KEYSTORE_TYPE, + Constants.WINDOWS_MY_CURRENTUSER_KEYSTORE_TYPE, + Constants.WINDOWS_MY_LOCALMACHINE_KEYSTORE_TYPE -> true; + default -> 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 000000000000..8a9f9be7b65c --- /dev/null +++ b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java @@ -0,0 +1,201 @@ +/* + * 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.Serial; +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.Collection; +import java.util.Collections; +import java.util.List; +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. + *

    + * {@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 parameters() { + return List.of(new Object[] { "JSSE", Boolean.FALSE, + "org.apache.tomcat.util.net.jsse.JSSEImplementation" }, + new Object[] { "OpenSSL", Boolean.TRUE, + "org.apache.tomcat.util.net.openssl.OpenSSLImplementation" }, + new Object[] { "OpenSSL-FFM", Boolean.TRUE, + "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation" }); + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean useOpenSSL; + + @Parameter(2) + public String sslImplementationName; + + + @Before + public void configureSSLImplementation() throws Exception { + TesterSupport.configureSSLImplementation(getTomcatInstance(), sslImplementationName, useOpenSSL); + getTomcatInstance().getServer().init(); + } + + 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") { + @Serial + 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") { + @Serial + 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(List.of(entries)); + KeyStore delegate = KeyStore.getInstance(file, TesterSupport.JKS_PASS.toCharArray()); + List aliases = Stream.of(entries).map(KeystoreEntry::alias).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 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 000000000000..776a60e15e83 --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestSSLUtilBaseAutomaticCertificateSelection.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.util.net; + +import java.io.File; +import java.io.IOException; +import java.security.KeyStore; +import java.security.UnrecoverableKeyException; +import java.security.cert.X509Certificate; +import java.util.List; + +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(List.of()); + try { + Assert.assertEquals(keystore.getCertificate("server"), context.getCertificateChain("server")[0]); + } finally { + context.destroy(); + } + } + + + @Test + public void testSSLContextLoadsSelectedCertificateFromKeystoreFile() throws Exception { + File file = TesterKeystoreGenerator.generateKeystore( + List.of(key(SSLUtilBase.DEFAULT_KEY_ALIAS, CLIENT_AUTH), key("server", SERVER_AUTH))); + KeyStore keystore = KeyStore.getInstance(file, TesterSupport.JKS_PASS.toCharArray()); + SSLHostConfig hostConfig = new SSLHostConfig(); + SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); + certificate.setCertificateKeystoreFile(file.getAbsolutePath()); + certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS); + SSLContext context = sslUtil(certificate).createSSLContext(List.of()); + try { + Assert.assertEquals(keystore.getCertificate("server"), context.getCertificateChain("server")[0]); + } finally { + context.destroy(); + } + } +} diff --git a/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java new file mode 100644 index 000000000000..911180f643b8 --- /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 b8594162fd2f..000000000000 --- a/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java +++ /dev/null @@ -1,333 +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.IOException; -import java.security.KeyStore; -import java.security.KeyStoreSpi; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.stream.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 parameters() { - return List.of(new Object[] { "JSSE", Boolean.FALSE, - "org.apache.tomcat.util.net.jsse.JSSEImplementation" }, - new Object[] { "OpenSSL", Boolean.TRUE, - "org.apache.tomcat.util.net.openssl.OpenSSLImplementation" }, - new Object[] { "OpenSSL-FFM", Boolean.TRUE, - "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation" }); - } - - @Parameter(0) - public String connectorName; - - @Parameter(1) - public boolean useOpenSSL; - - @Parameter(2) - public String sslImplementationName; - - - @Before - public void configureSSLImplementation() throws Exception { - TesterSupport.configureSSLImplementation(getTomcatInstance(), sslImplementationName, useOpenSSL); - getTomcatInstance().getServer().init(); - } - - private static final String HOST = "localhost"; - - private static final String CLIENT_AUTH_EXTENDED_KEY_USAGE_OID = "1.3.6.1.5.5.7.3.2"; - - private static final CertificateExtensionsCustomizer SERVER_AUTH = (keyPair, builder) -> builder - .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)); - private static final CertificateExtensionsCustomizer CLIENT_AUTH = (keyPair, builder) -> builder - .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth)); - private static final CertificateExtensionsCustomizer ANY_AUTH = (keyPair, builder) -> builder - .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage)); - - - private static KeystoreEntry key(String alias, CertificateExtensionsCustomizer eku) { - return KeystoreEntry.key(alias, HOST, eku); - } - - - private X509Certificate present(String alias, KeystoreEntry... entries) throws Exception { - return present(alias, orderedKeystore(null, entries)); - } - - - private X509Certificate present(String alias, KeyStore keystore) throws Exception { - X509KeyManager keyManager = (X509KeyManager) sslUtil(alias, keystore).getKeyManagers()[0]; - X509Certificate[] chain = keyManager.getCertificateChain(keyManager.chooseServerAlias("RSA", null, null)); - Assert.assertNotNull("Key manager returned no certificate chain", chain); - return chain[0]; - } - - - private SSLUtil sslUtil(String alias, KeyStore keystore) throws Exception { - - SSLHostConfig hostConfig = new SSLHostConfig(); - SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); - certificate.setCertificateKeystore(keystore); - certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS); - if (alias != null) { - certificate.setCertificateKeyAlias(alias); - } - - return sslUtil(certificate); - } - - - private SSLUtil sslUtil(SSLHostConfigCertificate certificate) throws Exception { - SSLUtil util = SSLImplementation.getInstance(sslImplementationName).getSSLUtil(certificate); - SSLHostConfig hostConfig = certificate.getSSLHostConfig(); - hostConfig.setEnabledProtocols(util.getEnabledProtocols()); - hostConfig.setEnabledCiphers(util.getEnabledCiphers()); - return util; - } - - - private static KeyStore orderedKeystore(String malformedAlias, KeystoreEntry... entries) throws Exception { - File file = TesterKeystoreGenerator.generateKeystore(List.of(entries)); - KeyStore delegate = KeyStore.getInstance(file, TesterSupport.JKS_PASS.toCharArray()); - List aliases = Stream.of(entries).map(KeystoreEntry::alias).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 extendedKeyUsage = certificate.getExtendedKeyUsage(); - Assert.assertNotNull("Presented certificate has no Extended Key Usage", extendedKeyUsage); - Assert.assertTrue("Presented certificate must be valid for Extended Key Usage " + oid, - extendedKeyUsage.contains(oid)); - } - - - /* - * A configured alias must be honored exactly, even when its certificate is not valid for server authentication and - * a server-auth certificate sharing the Subject is also present. Auto-selection must never override the user's - * choice (see the uses of SSLHostConfigCertificate.getCertificateKeyAlias() across the connectors). - */ - @Test - public void testConfiguredAliasIsUsedUnchangedWhenNotServerAuth() throws Exception { - X509Certificate presented = present(HOST, - key(HOST, CLIENT_AUTH), - key("localhost-server", SERVER_AUTH)); - assertEku(presented, CLIENT_AUTH_EXTENDED_KEY_USAGE_OID); - } - - - @Test - public void testConfiguredAliasIsUsedWhenServerAuth() throws Exception { - X509Certificate presented = present(HOST, - key(HOST, SERVER_AUTH), - key("localhost-client", CLIENT_AUTH)); - assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); - } - - - @Test - public void testAutoSelectPrefersServerAuthOverClientAuth() throws Exception { - X509Certificate presented = present(null, - key("first", CLIENT_AUTH), - key("second", SERVER_AUTH)); - assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); - } - - - @Test - public void testAutoSelectAcceptsAnyExtendedKeyUsage() throws Exception { - X509Certificate presented = present(null, - key("first", CLIENT_AUTH), - key("any", ANY_AUTH)); - assertEku(presented, Constants.ANY_EXTENDED_KEY_USAGE_OID); - } - - - @Test - public void testAutoSelectAcceptsCertificateWithoutExtendedKeyUsage() throws Exception { - X509Certificate presented = present(null, - key("first", CLIENT_AUTH), - key("none", null)); - Assert.assertNull("A certificate with no Extended Key Usage is unrestricted and must be selectable", - presented.getExtendedKeyUsage()); - } - - - /* - * Trusted (keyless) certificate entries are not key entries, so they can never be selected as the server key even - * when they are the only server-auth-capable certificate. - */ - @Test - public void testAutoSelectSkipsTrustedEntries() { - Assert.assertThrows(IOException.class, () -> present(null, - KeystoreEntry.trusted("ca", HOST, SERVER_AUTH), - key("key", CLIENT_AUTH))); - } - - - /* - * No unsuitable key entry may be selected when all certificates exclude server authentication. - */ - @Test - public void testAutoSelectFailsWhenNoneServerAuth() { - Assert.assertThrows(IOException.class, () -> present(null, - key("first", CLIENT_AUTH), - key("second", CLIENT_AUTH))); - } - - - @Test - public void testAutoSelectSkipsMalformedExtendedKeyUsage() throws Exception { - KeyStore keystore = orderedKeystore("malformed", key("malformed", CLIENT_AUTH), key("server", SERVER_AUTH)); - assertEku(present(null, keystore), Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); - } - - - @Test - public void testConfiguredAliasWithMalformedExtendedKeyUsageIsUsed() throws Exception { - KeyStore keystore = orderedKeystore("malformed", key("malformed", CLIENT_AUTH), key("server", SERVER_AUTH)); - Assert.assertEquals(keystore.getCertificateChain("malformed")[0], present("malformed", keystore)); - } - - - @Test - public void testAutoSelectFailsWhenOnlyMalformedExtendedKeyUsage() throws Exception { - KeyStore keystore = orderedKeystore("malformed", key("malformed", CLIENT_AUTH)); - Assert.assertThrows(IOException.class, () -> present(null, keystore)); - } - - - @Test - public void testAutoSelectUsesFirstEligibleKey() throws Exception { - KeyStore keystore = orderedKeystore(null, key("client", CLIENT_AUTH), key("first", ANY_AUTH), - key("second", SERVER_AUTH)); - Assert.assertEquals(keystore.getCertificate("first"), present(null, keystore)); - } - - - @Test - public void testConfiguredMissingAliasFails() { - Assert.assertThrows(IOException.class, () -> present("missing", key("server", SERVER_AUTH))); - } - - - @Test - public void testConfiguredTrustedAliasFails() { - Assert.assertThrows(IOException.class, () -> present("trusted", KeystoreEntry.trusted("trusted", HOST, SERVER_AUTH), - key("server", SERVER_AUTH))); - } - - - @Test - public void testAutoSelectDoesNotSkipKeyReadFailure() throws Exception { - KeyStore keystore = orderedKeystore(null, key("first", SERVER_AUTH), key("second", SERVER_AUTH)); - SSLHostConfig hostConfig = new SSLHostConfig(); - SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); - certificate.setCertificateKeystore(keystore); - certificate.setCertificateKeyPassword("incorrect"); - SSLUtil util = SSLImplementation.getInstance(sslImplementationName).getSSLUtil(certificate); - Assert.assertThrows(UnrecoverableKeyException.class, util::getKeyManagers); - } - - - @Test - public void testSSLContextUsesSelectedCertificate() throws Exception { - KeyStore keystore = orderedKeystore(null, key("client", CLIENT_AUTH), key("server", SERVER_AUTH)); - SSLContext context = sslUtil(null, keystore).createSSLContext(List.of()); - try { - Assert.assertEquals(keystore.getCertificate("server"), context.getCertificateChain("server")[0]); - } finally { - context.destroy(); - } - } - - - @Test - public void testSSLContextLoadsSelectedCertificateFromKeystoreFile() throws Exception { - File file = TesterKeystoreGenerator.generateKeystore( - List.of(key(SSLUtilBase.DEFAULT_KEY_ALIAS, CLIENT_AUTH), key("server", SERVER_AUTH))); - KeyStore keystore = KeyStore.getInstance(file, TesterSupport.JKS_PASS.toCharArray()); - SSLHostConfig hostConfig = new SSLHostConfig(); - SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); - certificate.setCertificateKeystoreFile(file.getAbsolutePath()); - certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS); - SSLContext context = sslUtil(certificate).createSSLContext(List.of()); - try { - Assert.assertEquals(keystore.getCertificate("server"), context.getCertificateChain("server")[0]); - } finally { - context.destroy(); - } - } -} diff --git a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java index 480ddf045b68..f7339ab81162 100644 --- a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java +++ b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java @@ -27,6 +27,7 @@ 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; @@ -42,6 +43,8 @@ public final class TesterKeystoreGenerator { + private static final AtomicLong SERIAL_COUNTER = new AtomicLong(1); + private TesterKeystoreGenerator() {} @FunctionalInterface @@ -128,7 +131,7 @@ private static CertAndKey generateSelfSignedCertificate(String cn, String[] sanN 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 e982b23a3d86..4d2b724a348f 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -489,8 +489,14 @@ certificateKeyAlias 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 certificateKeyAlias is always used unchanged. + which key in the key store to use. A configured + certificateKeyAlias 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) diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml index bcea8da85b01..940bb7953464 100644 --- a/webapps/docs/config/http.xml +++ b/webapps/docs/config/http.xml @@ -1675,8 +1675,21 @@ serverAuth nor anyExtendedKeyUsage, 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.

    + initialization fails.

    +

    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 " (1)", " (2)", 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.

    +

    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 + JDK-8395755).

    diff --git a/webapps/docs/ssl-howto.xml b/webapps/docs/ssl-howto.xml index 441c85164235..946bfe1cab68 100644 --- a/webapps/docs/ssl-howto.xml +++ b/webapps/docs/ssl-howto.xml @@ -456,10 +456,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.

    -

    A configured certificateKeyAlias 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.

    +

    A configured certificateKeyAlias 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 " (1)", " (2)", +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.

    + +

    Selection is made solely on the appended " (N)" suffix. The +SunMSCAPI provider does not expose a certificate's original Windows friendly name +through the key store API (see +JDK-8395755), 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.

    From 11adba433dd9ccc97556aeffa56719eec0afcf6c Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Wed, 28 Oct 2026 15:23:07 +0200 Subject: [PATCH 04/20] Use the certificate actually served after Windows-MY alias resolution The configured certificateKeyAlias can differ from the certificate actually served once the SunMSCAPI Windows-MY numbered-duplicate resolution has selected a different alias. Consumers that looked the certificate up by the configured alias therefore reported or monitored the wrong certificate. --- .../catalina/manager/ManagerServlet.java | 7 +- .../tomcat/util/net/AbstractEndpoint.java | 19 ++- .../tomcat/util/net/LocalStrings.properties | 1 + .../apache/tomcat/util/net/SSLContext.java | 38 +++++- .../apache/tomcat/util/net/SSLHostConfig.java | 6 +- .../util/net/SSLHostConfigCertificate.java | 25 ++++ .../tomcat/util/net/jsse/JSSESSLContext.java | 17 +++ ...LUtilBaseCertificateSelectionTestBase.java | 125 ++++++++++++++++-- ...stSSLUtilBaseConfiguredAliasSelection.java | 59 +++++++++ 9 files changed, 268 insertions(+), 29 deletions(-) diff --git a/java/org/apache/catalina/manager/ManagerServlet.java b/java/org/apache/catalina/manager/ManagerServlet.java index 71cdb510cb0f..8d899467dc85 100644 --- a/java/org/apache/catalina/manager/ManagerServlet.java +++ b/java/org/apache/catalina/manager/ManagerServlet.java @@ -75,7 +75,6 @@ import org.apache.tomcat.util.net.SSLContext; import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.net.SSLHostConfigCertificate; -import org.apache.tomcat.util.net.SSLUtilBase; import org.apache.tomcat.util.res.StringManager; import org.apache.tomcat.util.security.Escape; @@ -1690,14 +1689,10 @@ protected Map> getConnectorCerts(StringManager smClient) { sslHostConfigCert.getType(); List certList = new ArrayList<>(); SSLContext sslContext = sslHostConfigCert.getSslContext(); - String alias = sslHostConfigCert.getCertificateKeyAlias(); - if (alias == null) { - alias = SSLUtilBase.DEFAULT_KEY_ALIAS; - } if (sslContext == null) { certList.add(smClient.getString("managerServlet.certsNotLoaded")); } else { - X509Certificate[] certs = sslContext.getCertificateChain(alias); + X509Certificate[] certs = sslContext.getCertificateChain(sslHostConfigCert); if (certs == null) { certList.add(smClient.getString("managerServlet.certsNotAvailable")); } else { diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java b/java/org/apache/tomcat/util/net/AbstractEndpoint.java index 522efbee7672..7d8b004e32f1 100644 --- a/java/org/apache/tomcat/util/net/AbstractEndpoint.java +++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java @@ -613,6 +613,7 @@ protected void createSSLContext(SSLHostConfig sslHostConfig) throws IllegalArgum */ protected void logCertificate(SSLHostConfigCertificate certificate) { SSLHostConfig sslHostConfig = certificate.getSSLHostConfig(); + SSLContext sslContext = certificate.getSslContext(); String certificateInfo; @@ -647,13 +648,19 @@ protected void logCertificate(SSLHostConfigCertificate certificate) { getLogCertificate().info(sm.getString("endpoint.tls.info", getName(), sslHostConfig.getHostName(), certificate.getType(), certificateInfo, trustStoreSource)); - SSLContext sslContext = certificate.getSslContext(); - if (getLogCertificate().isDebugEnabled() && sslContext != null) { - String alias = certificate.getCertificateKeyAlias(); - if (alias == null) { - alias = SSLUtilBase.DEFAULT_KEY_ALIAS; + // When a configured alias was resolved to a different alias that 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. + String configuredAlias = certificate.getCertificateKeyAlias(); + if (sslContext != null && configuredAlias != null) { + String servedAlias = sslContext.getServedAlias(certificate); + if (servedAlias != null && !servedAlias.equals(configuredAlias)) { + getLogCertificate() + .info(sm.getString("endpoint.tls.info.cert.aliasResolved", configuredAlias, servedAlias)); } - X509Certificate[] x509Certificates = sslContext.getCertificateChain(alias); + } + + if (getLogCertificate().isDebugEnabled() && sslContext != null) { + X509Certificate[] x509Certificates = sslContext.getCertificateChain(certificate); if (x509Certificates != null && x509Certificates.length > 0) { getLogCertificate().debug(generateCertificateDebug(x509Certificates[0])); } else { diff --git a/java/org/apache/tomcat/util/net/LocalStrings.properties b/java/org/apache/tomcat/util/net/LocalStrings.properties index b911b3ac7212..a3290d4eee80 100644 --- a/java/org/apache/tomcat/util/net/LocalStrings.properties +++ b/java/org/apache/tomcat/util/net/LocalStrings.properties @@ -125,6 +125,7 @@ endpoint.tls.cert.encodingError=Certificate fingerprints not available endpoint.tls.cert.noCerts=Certificate details not available as the certificate chain returned from the SSLContext was empty endpoint.tls.enabledGroups=Configured named groups {0} for the TLS connector endpoint.tls.info=Connector [{0}], TLS virtual host [{1}], certificate type [{2}] configured from {3} with trust store [{4}] +endpoint.tls.info.cert.aliasResolved=The configured certificate alias [{0}] was resolved to alias [{1}] which is valid for TLS server authentication endpoint.tls.info.cert.keystore=keystore [{0}] using alias [{1}] endpoint.tls.info.cert.keystore.direct=Set directly - location unknown endpoint.tls.info.cert.pem=key [{0}], certificate [{1}] and certificate chain [{2}] diff --git a/java/org/apache/tomcat/util/net/SSLContext.java b/java/org/apache/tomcat/util/net/SSLContext.java index f51287aa800b..25adf5462240 100644 --- a/java/org/apache/tomcat/util/net/SSLContext.java +++ b/java/org/apache/tomcat/util/net/SSLContext.java @@ -90,13 +90,49 @@ default SSLEngine createSSLEngine(boolean clientMode) { SSLParameters getSupportedSSLParameters(); /** - * Returns the certificate chain for the given alias. + * Returns the certificate chain for the given key store alias. This is a low-level lookup by exact alias; callers + * that want the certificate actually served for a configured {@link SSLHostConfigCertificate} should use + * {@link #getCertificateChain(SSLHostConfigCertificate)}, which resolves the served alias (it can differ from the + * configured alias, for example after SunMSCAPI Windows-MY numbered-duplicate resolution). * * @param alias The alias + * * @return The certificate chain */ X509Certificate[] getCertificateChain(String alias); + + /** + * 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. + * + * @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; + } + + + /** + * Returns the certificate chain actually served for the given certificate configuration. Unlike + * {@link #getCertificateChain(String)}, which looks up a fixed alias, this uses {@link #getServedAlias}, so callers + * such as the certificate reload check observe the certificate that is really in use. This can differ from the + * configured alias, for example after SunMSCAPI Windows-MY duplicate resolution. + * + * @param certificate The certificate configuration + * + * @return The served certificate chain, or {@code null} if none can be determined + */ + default X509Certificate[] getCertificateChain(SSLHostConfigCertificate certificate) { + return getCertificateChain(getServedAlias(certificate)); + } + /** * Returns the accepted issuers. * diff --git a/java/org/apache/tomcat/util/net/SSLHostConfig.java b/java/org/apache/tomcat/util/net/SSLHostConfig.java index 94582622d082..abd15b99b736 100644 --- a/java/org/apache/tomcat/util/net/SSLHostConfig.java +++ b/java/org/apache/tomcat/util/net/SSLHostConfig.java @@ -1587,11 +1587,7 @@ public Set certificatesExpiringBefore(Date date) { for (SSLHostConfigCertificate sslHostConfigCertificate : sslHostConfigCertificates) { SSLContext sslContext = sslHostConfigCertificate.getSslContext(); if (sslContext != null) { - String alias = sslHostConfigCertificate.getCertificateKeyAlias(); - if (alias == null) { - alias = SSLUtilBase.DEFAULT_KEY_ALIAS; - } - X509Certificate[] certificates = sslContext.getCertificateChain(alias); + X509Certificate[] certificates = sslContext.getCertificateChain(sslHostConfigCertificate); if (certificates != null && certificates.length > 0) { X509Certificate certificate = certificates[0]; Date expirationDate = certificate.getNotAfter(); diff --git a/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java b/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java index edd52cda0fad..0f0c7b17632e 100644 --- a/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java +++ b/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java @@ -244,6 +244,31 @@ public Type getType() { } + /** + * 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(). + * + * @param keyManager the key manager that will serve this certificate + * + * @return the served alias, or {@code null} if the key manager has no suitable entry + */ + public String chooseServerAlias(X509KeyManager keyManager) { + Type[] candidateTypes = Type.UNDEFINED.equals(type) ? Type.values() : new Type[] { type }; + for (Type candidateType : candidateTypes) { + if (candidateType == Type.UNDEFINED) { + continue; + } + String alias = keyManager.chooseServerAlias(candidateType.getKeyType(), null, null); + if (alias != null) { + return alias; + } + } + return null; + } + + /** * Returns the password for the certificate's private key. * diff --git a/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java b/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java index 088257110b1e..9679da9a1a31 100644 --- a/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java +++ b/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java @@ -34,6 +34,7 @@ import javax.net.ssl.X509TrustManager; import org.apache.tomcat.util.net.SSLContext; +import org.apache.tomcat.util.net.SSLHostConfigCertificate; class JSSESSLContext implements SSLContext { @@ -89,6 +90,22 @@ public X509Certificate[] getCertificateChain(String alias) { return result; } + + @Override + public String getServedAlias(SSLHostConfigCertificate certificate) { + if (kms != null) { + for (KeyManager km : kms) { + if (km instanceof X509KeyManager x509KeyManager) { + String alias = certificate.chooseServerAlias(x509KeyManager); + if (alias != null) { + return alias; + } + } + } + } + return SSLContext.super.getServedAlias(certificate); + } + @Override public X509Certificate[] getAcceptedIssuers() { Set certs = new HashSet<>(); diff --git a/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java index 8a9f9be7b65c..c549e4603cdb 100644 --- a/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java +++ b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java @@ -19,14 +19,18 @@ import java.io.File; import java.io.Serial; +import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreSpi; +import java.security.PrivateKey; import java.security.Provider; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.List; +import java.util.Set; import java.util.stream.Stream; import javax.net.ssl.X509KeyManager; @@ -140,11 +144,68 @@ protected SSLUtil sslUtil(SSLHostConfigCertificate certificate) throws Exception } + /* + * Builds and registers an SSLContext for the given configured alias and key store, then returns the certificates + * that SSLHostConfig.certificatesExpiringBefore() - the input to the TLS certificate reload listener - would flag. + * The cut-off date is far enough in the future that every certificate qualifies, so the result is exactly the set + * of certificates the listener monitors for reload. + */ + protected Set certificatesMonitoredForReload(String alias, KeyStore keystore) throws Exception { + SSLHostConfigCertificate certificate = registeredCertificate(alias, keystore); + SSLContext context = sslUtil(certificate).createSSLContext(List.of()); + try { + certificate.setSslContext(context); + Date farFuture = new Date(System.currentTimeMillis() + 3650L * 24 * 60 * 60 * 1000); + return certificate.getSSLHostConfig().certificatesExpiringBefore(farFuture); + } finally { + context.destroy(); + } + } + + + /* + * Builds and registers an SSLContext for the given configured alias and key store, then returns the alias the + * context reports as actually served - which is what the certificate logging and reload paths resolve. + */ + protected String servedAlias(String alias, KeyStore keystore) throws Exception { + SSLHostConfigCertificate certificate = registeredCertificate(alias, keystore); + SSLContext context = sslUtil(certificate).createSSLContext(List.of()); + try { + certificate.setSslContext(context); + return context.getServedAlias(certificate); + } finally { + context.destroy(); + } + } + + + private static SSLHostConfigCertificate registeredCertificate(String alias, KeyStore keystore) { + 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); + } + hostConfig.addCertificate(certificate); + return certificate; + } + + protected static KeyStore orderedKeystore(String malformedAlias, KeystoreEntry... entries) throws Exception { - return orderedKeystore(new Provider("SunMSCAPI", "1.0", "test") { - @Serial - private static final long serialVersionUID = -4141665633237722962L; - }, "Windows-MY", malformedAlias, entries); + return orderedKeystore(sunMSCAPIProvider(), "Windows-MY", true, malformedAlias, entries); + } + + + /* + * As orderedKeystore(String, KeystoreEntry...) but the private keys report a null format, like a non-exportable + * SunMSCAPI key. This routes SSLUtilBase.getKeyManagers() through its production path that keeps the original key + * store (ksUsed == ks) and wraps each key manager, rather than the in-memory-copy branch the exportable JKS-backed + * mock otherwise takes. + */ + protected static KeyStore nonExportableKeystore(KeystoreEntry... entries) throws Exception { + return orderedKeystore(sunMSCAPIProvider(), "Windows-MY", false, null, entries); } @@ -152,12 +213,20 @@ protected static KeyStore nonSunMSCAPIKeystore(KeystoreEntry... entries) throws return orderedKeystore(new Provider("SUN", "1.0", "test") { @Serial private static final long serialVersionUID = 4434321038151233913L; - }, "JKS", null, entries); + }, "JKS", true, null, entries); } - protected static KeyStore orderedKeystore(Provider provider, String type, String malformedAlias, - KeystoreEntry... entries) throws Exception { + private static Provider sunMSCAPIProvider() { + return new Provider("SunMSCAPI", "1.0", "test") { + @Serial + private static final long serialVersionUID = -4141665633237722962L; + }; + } + + + protected static KeyStore orderedKeystore(Provider provider, String type, boolean exportableKeys, + String malformedAlias, KeystoreEntry... entries) throws Exception { File file = TesterKeystoreGenerator.generateKeystore(List.of(entries)); KeyStore delegate = KeyStore.getInstance(file, TesterSupport.JKS_PASS.toCharArray()); List aliases = Stream.of(entries).map(KeystoreEntry::alias).toList(); @@ -180,11 +249,14 @@ protected static KeyStore orderedKeystore(Provider provider, String type, String }); 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.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); + }); 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 + // 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. KeyStore result = new KeyStore(spi, provider, type) { }; result.load(null, null); @@ -192,6 +264,37 @@ protected static KeyStore orderedKeystore(Provider provider, String type, String } + /* + * A view of a private key that reports a null format, as a non-exportable SunMSCAPI key does, while staying usable + * by the key manager. getEncoded() returns null for the same reason; these selection tests never sign, so the key + * material itself is never needed. + */ + private static Key nonExportableView(Key key) { + if (!(key instanceof PrivateKey privateKey)) { + return key; + } + return new PrivateKey() { + @Serial + private static final long serialVersionUID = 1L; + + @Override + public String getAlgorithm() { + return privateKey.getAlgorithm(); + } + + @Override + public String getFormat() { + return null; + } + + @Override + public byte[] getEncoded() { + return null; + } + }; + } + + protected static void assertEku(X509Certificate certificate, String oid) throws Exception { List 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 911180f643b8..05189fc0efb8 100644 --- a/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java +++ b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java @@ -20,8 +20,10 @@ import java.io.IOException; import java.security.KeyStore; import java.security.cert.X509Certificate; +import java.util.Set; import org.junit.Assert; +import org.junit.Assume; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -160,6 +162,27 @@ public void testConfiguredTrustedAliasFails() { } + /* + * After the SunMSCAPI numbered-duplicate swap, the certificate served to clients is a numbered duplicate of the + * configured alias. The TLS certificate reload listener (via SSLHostConfig.certificatesExpiringBefore()) must + * monitor that served certificate, not the certificate at the configured alias. Non-exportable keys route + * getKeyManagers() through the production ksUsed == ks path a real SunMSCAPI store uses. Scoped to JSSE: for + * OpenSSL the served certificate is held in the native layer and resolved by a separate code path. + */ + @Test + public void testReloadListenerMonitorsTheServedCertificate() throws Exception { + Assume.assumeTrue("JSSE".equals(connectorName)); + KeyStore keystore = nonExportableKeystore(key(HOST, CLIENT_AUTH), key(HOST + " (1)", SERVER_AUTH)); + + Set monitored = certificatesMonitoredForReload(HOST, keystore); + + X509Certificate served = (X509Certificate) keystore.getCertificateChain(HOST + " (1)")[0]; + X509Certificate notServed = (X509Certificate) keystore.getCertificateChain(HOST)[0]; + Assert.assertTrue("The served certificate must be monitored for reload", monitored.contains(served)); + Assert.assertFalse("The unserved configured certificate must not be monitored", monitored.contains(notServed)); + } + + /* * 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 @@ -187,4 +210,40 @@ public void testConfiguredAliasSkipsGapInNumberedDuplicates() throws Exception { key("tomcat (2)", SERVER_AUTH)); assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); } + + + /* + * For an OpenSSL connector the served certificate is loaded into the native layer, but the reload check must still + * report it. With exportable keys getKeyManagers() copies only the resolved key into an in-memory store, so + * OpenSSLContext.getCertificateChain() finds nothing under the configured alias and already falls back to the served + * alias - no OpenSSL-specific change is needed. (Non-exportable SunMSCAPI keys cannot be loaded by OpenSSL at all, + * so this exportable case is the only one that runs.) + */ + @Test + public void testReloadResolvesServedCertificateWithOpenSSL() throws Exception { + Assume.assumeTrue(useOpenSSL); + KeyStore keystore = orderedKeystore(null, key(HOST, CLIENT_AUTH), key(HOST + " (1)", SERVER_AUTH)); + + Set monitored = certificatesMonitoredForReload(HOST, keystore); + + X509Certificate served = (X509Certificate) keystore.getCertificateChain(HOST + " (1)")[0]; + X509Certificate notServed = (X509Certificate) keystore.getCertificateChain(HOST)[0]; + Assert.assertTrue("OpenSSL reload must resolve the served certificate", monitored.contains(served)); + Assert.assertFalse("OpenSSL reload must not report the unserved configured certificate", + monitored.contains(notServed)); + } + + + /* + * 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. + */ + @Test + public void testServedAliasIsTheNumberedDuplicate() throws Exception { + Assume.assumeTrue("JSSE".equals(connectorName)); + KeyStore keystore = nonExportableKeystore(key(HOST, CLIENT_AUTH), key(HOST + " (1)", SERVER_AUTH)); + Assert.assertEquals(HOST + " (1)", servedAlias(HOST, keystore)); + } } From 5789950d00bddabded0d52ce85b9fa362d17724e Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 29 Oct 2026 14:22:14 +0200 Subject: [PATCH 05/20] Fix typos --- java/org/apache/tomcat/util/net/AbstractEndpoint.java | 2 +- java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java b/java/org/apache/tomcat/util/net/AbstractEndpoint.java index 7d8b004e32f1..5bf6ff0c2c52 100644 --- a/java/org/apache/tomcat/util/net/AbstractEndpoint.java +++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java @@ -648,7 +648,7 @@ protected void logCertificate(SSLHostConfigCertificate certificate) { getLogCertificate().info(sm.getString("endpoint.tls.info", getName(), sslHostConfig.getHostName(), certificate.getType(), certificateInfo, trustStoreSource)); - // When a configured alias was resolved to a different alias that is actually served (e.g. a SunMSCAPI + // 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. String configuredAlias = certificate.getCertificateKeyAlias(); if (sslContext != null && configuredAlias != null) { diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java index 619fc78b4cf3..94f17ec0ae22 100644 --- a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java @@ -631,7 +631,7 @@ private static String findAlias(X509KeyManager keyManager, SSLHostConfigCertific List candidateTypes = new ArrayList<>(); if (Type.UNDEFINED.equals(type)) { - // Try all types to find an suitable alias + // Try all types to find a suitable alias candidateTypes.addAll(Arrays.asList(Type.values())); candidateTypes.remove(Type.UNDEFINED); } else { From 0bef422991554bb4ccf4b804b7935f5711156c3f Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 29 Oct 2026 14:30:38 +0200 Subject: [PATCH 06/20] Scan only the contiguous run of Windows-MY numbered duplicates --- java/org/apache/tomcat/util/net/SSLUtilBase.java | 13 +++++++------ .../TestSSLUtilBaseConfiguredAliasSelection.java | 9 ++------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/java/org/apache/tomcat/util/net/SSLUtilBase.java b/java/org/apache/tomcat/util/net/SSLUtilBase.java index 761915661c61..94e5e4019be6 100644 --- a/java/org/apache/tomcat/util/net/SSLUtilBase.java +++ b/java/org/apache/tomcat/util/net/SSLUtilBase.java @@ -489,25 +489,26 @@ public KeyManager[] getKeyManagers() throws Exception { * @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 + * @return the alias of the first duplicate in the contiguous run (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++) { + int suffix = 1; + while (true) { String candidateAlias = baseAlias + " (" + suffix + ")"; Certificate candidateCert = ks.getCertificate(candidateAlias); if (candidateCert == null) { - continue; + // The first missing number ends the contiguous run of numbered duplicates. + return null; } if (candidateCert instanceof X509Certificate x509 && ks.isKeyEntry(candidateAlias) && isServerAuthCapable(x509)) { return candidateAlias; } + suffix++; } - return null; } diff --git a/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java index 05189fc0efb8..f9f2a4c0e633 100644 --- a/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java +++ b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java @@ -198,17 +198,12 @@ public void testConfiguredTrustedBaseAliasUsesServerAuthDuplicate() throws Excep } - /* - * 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 { + public void testConfiguredAliasStopsAtGapAndKeepsBase() throws Exception { X509Certificate presented = present("tomcat", key("tomcat", CLIENT_AUTH), key("tomcat (2)", SERVER_AUTH)); - assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); + assertEku(presented, CLIENT_AUTH_EXTENDED_KEY_USAGE_OID); } From 6ac4f04f7679fa410f3eef167ee1114800bc6a38 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 29 Oct 2026 14:51:29 +0200 Subject: [PATCH 07/20] Reuse SSLHostConfigCertificate.chooseServerAlias in the OpenSSL contexts --- .../util/net/openssl/OpenSSLContext.java | 32 ++----------------- .../net/openssl/panama/OpenSSLContext.java | 31 ++---------------- 2 files changed, 4 insertions(+), 59 deletions(-) diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java index 94f17ec0ae22..0eb47b834dad 100644 --- a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java @@ -29,9 +29,7 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; -import java.util.Arrays; import java.util.Base64; -import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.locks.Lock; @@ -585,7 +583,7 @@ public void addCertificate(SSLHostConfigCertificate certificate) throws Exceptio } X509Certificate[] chain = x509KeyManager.getCertificateChain(alias); if (chain == null) { - alias = findAlias(x509KeyManager, certificate); + alias = certificate.chooseServerAlias(x509KeyManager); chain = x509KeyManager.getCertificateChain(alias); } PrivateKey key = x509KeyManager.getPrivateKey(alias); @@ -621,32 +619,6 @@ private static int getCertificateIndex(SSLHostConfigCertificate certificate) { } - /* - * Find a valid alias when none was specified in the config. - */ - private static String findAlias(X509KeyManager keyManager, SSLHostConfigCertificate certificate) { - - Type type = certificate.getType(); - String result = null; - - List candidateTypes = new ArrayList<>(); - if (Type.UNDEFINED.equals(type)) { - // Try all types to find a suitable alias - candidateTypes.addAll(Arrays.asList(Type.values())); - candidateTypes.remove(Type.UNDEFINED); - } else { - // Look for the specific type to find a suitable alias - candidateTypes.add(type); - } - - Iterator iter = candidateTypes.iterator(); - while (result == null && iter.hasNext()) { - result = keyManager.chooseServerAlias(iter.next().getKeyType(), null, null); - } - - return result; - } - private static X509TrustManager chooseTrustManager(TrustManager[] managers) { for (TrustManager m : managers) { if (m instanceof X509TrustManager) { @@ -703,7 +675,7 @@ public X509Certificate[] getCertificateChain(String alias) { } chain = x509KeyManager.getCertificateChain(alias); if (chain == null) { - alias = findAlias(x509KeyManager, certificate); + alias = certificate.chooseServerAlias(x509KeyManager); chain = x509KeyManager.getCertificateChain(alias); } } 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 d093bba4d744..cedc6770670b 100644 --- a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java +++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java @@ -30,7 +30,6 @@ import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -1202,7 +1201,7 @@ public boolean addCertificate(SSLHostConfigCertificate certificate, Arena localA } X509Certificate[] chain = x509KeyManager.getCertificateChain(alias); if (chain == null) { - alias = findAlias(x509KeyManager, certificate); + alias = certificate.chooseServerAlias(x509KeyManager); chain = x509KeyManager.getCertificateChain(alias); } // Convert the in-memory key manager entry to native objects with @@ -1317,32 +1316,6 @@ private static MemorySegment createMemoryBio(Arena arena, byte[] bytes) { } - /* - * Find a valid alias when none was specified in the config. - */ - private static String findAlias(X509KeyManager keyManager, SSLHostConfigCertificate certificate) { - - Type type = certificate.getType(); - String result = null; - - List candidateTypes = new ArrayList<>(); - if (Type.UNDEFINED.equals(type)) { - // Try all types to find a suitable alias - candidateTypes.addAll(Arrays.asList(Type.values())); - candidateTypes.remove(Type.UNDEFINED); - } else { - // Look for the specific type to find a suitable alias - candidateTypes.add(type); - } - - Iterator iter = candidateTypes.iterator(); - while (result == null && iter.hasNext()) { - result = keyManager.chooseServerAlias(iter.next().getKeyType(), null, null); - } - - return result; - } - private static X509TrustManager chooseTrustManager(TrustManager[] managers) { for (TrustManager m : managers) { if (m instanceof X509TrustManager) { @@ -1410,7 +1383,7 @@ public X509Certificate[] getCertificateChain(String alias) { } chain = x509KeyManager.getCertificateChain(alias); if (chain == null) { - alias = findAlias(x509KeyManager, certificate); + alias = certificate.chooseServerAlias(x509KeyManager); chain = x509KeyManager.getCertificateChain(alias); } } else { From 2486144081a01f01f9f46477f49fd85c300f6f7d Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 29 Oct 2026 17:28:04 +0200 Subject: [PATCH 08/20] Share served-alias resolution across all SSLContext engines --- .../apache/tomcat/util/net/SSLContext.java | 33 ++++++-- .../tomcat/util/net/SSLContextWrapper.java | 6 ++ .../tomcat/util/net/jsse/JSSESSLContext.java | 21 ++--- .../util/net/openssl/OpenSSLContext.java | 29 +++---- .../net/openssl/panama/OpenSSLContext.java | 35 +++------ ...LUtilBaseCertificateSelectionTestBase.java | 77 ++++++++++++++++--- ...stSSLUtilBaseConfiguredAliasSelection.java | 17 +++- 7 files changed, 148 insertions(+), 70 deletions(-) diff --git a/java/org/apache/tomcat/util/net/SSLContext.java b/java/org/apache/tomcat/util/net/SSLContext.java index 25adf5462240..7c3c4134ef6c 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.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 @@ default SSLEngine createSSLEngine(boolean clientMode) { 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 280d4dbbcecb..b1b434d5f087 100644 --- a/java/org/apache/tomcat/util/net/SSLContextWrapper.java +++ b/java/org/apache/tomcat/util/net/SSLContextWrapper.java @@ -80,6 +80,12 @@ public X509Certificate[] getCertificateChain(String alias) { 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 9679da9a1a31..29b07ed12b2c 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.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 @@ public X509Certificate[] getCertificateChain(String alias) { @Override - public String getServedAlias(SSLHostConfigCertificate certificate) { - if (kms != null) { - for (KeyManager km : kms) { - if (km instanceof X509KeyManager x509KeyManager) { - String alias = certificate.chooseServerAlias(x509KeyManager); - if (alias != null) { - return alias; - } - } + public X509KeyManager[] getServerKeyManagers(SSLHostConfigCertificate certificate) { + if (kms == null) { + return new X509KeyManager[0]; + } + List x509KeyManagers = new ArrayList<>(); + for (KeyManager km : kms) { + if (km instanceof X509KeyManager x509KeyManager) { + x509KeyManagers.add(x509KeyManager); } } - 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 0eb47b834dad..cc764adc0996 100644 --- a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java @@ -58,7 +58,6 @@ 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; /** @@ -576,16 +575,9 @@ public void addCertificate(SSLHostConfigCertificate certificate) throws Exceptio SSLContext.setCertificateChainFile(state.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); String encodedKey = BEGIN_KEY + Base64.getMimeEncoder(64, new byte[] { '\n' }).encodeToString(key.getEncoded()) + END_KEY; @@ -667,20 +659,17 @@ public SSLParameters getSupportedSSLParameters() { @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 cedc6770670b..7f1d492bc763 100644 --- a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java +++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java @@ -57,7 +57,6 @@ 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; @@ -1194,16 +1193,9 @@ public boolean addCertificate(SSLHostConfigCertificate certificate, Arena localA } } } 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); - } // Convert the in-memory key manager entry to native objects with // the shared loader (PKCS#8 key via d2i_AutoPrivateKey, // certificates via d2i_X509) @@ -1375,25 +1367,20 @@ public SSLParameters getSupportedSSLParameters() { @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); - } - } else { - // No key manager (e.g. a PEM or PKCS#12 file configuration): read - // the chain from the configured source with the shared loader so - // the certificate data is available via JMX and for expiry checks - chain = CertificateLoader.getCertificateChain(certificate); + return x509KeyManager.getCertificateChain(alias); } + // No key manager (e.g. a PEM or PKCS#12 file configuration): read the chain from the configured source with + // the shared loader so the certificate data is available via JMX and for expiry checks + return CertificateLoader.getCertificateChain(certificate); + } + - 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 c549e4603cdb..e658cc5328f3 100644 --- a/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java +++ b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java @@ -194,7 +194,7 @@ private static SSLHostConfigCertificate registeredCertificate(String alias, KeyS 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); } @@ -205,7 +205,18 @@ protected static KeyStore orderedKeystore(String malformedAlias, KeystoreEntry.. * 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); } @@ -213,7 +224,7 @@ protected static KeyStore nonSunMSCAPIKeystore(KeystoreEntry... entries) throws return orderedKeystore(new Provider("SUN", "1.0", "test") { @Serial private static final long serialVersionUID = 4434321038151233913L; - }, "JKS", true, null, entries); + }, "JKS", KeyMode.EXPORTABLE, null, entries); } @@ -225,7 +236,20 @@ private static Provider sunMSCAPIProvider() { } - 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(List.of(entries)); KeyStore delegate = KeyStore.getInstance(file, TesterSupport.JKS_PASS.toCharArray()); @@ -251,13 +275,18 @@ protected static KeyStore orderedKeystore(Provider provider, String type, boolea .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); + return switch (keyMode) { + case EXPORTABLE -> key; + case NON_EXPORTABLE -> nonExportableView(key); + case WRAPPED_EXPORTABLE -> wrappedExportableView(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; @@ -295,6 +324,36 @@ public byte[] getEncoded() { } + /* + * 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 privateKey)) { + return key; + } + return new PrivateKey() { + @Serial + 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 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 f9f2a4c0e633..0a8857ad90d3 100644 --- a/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java +++ b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java @@ -232,8 +232,7 @@ public void testReloadResolvesServedCertificateWithOpenSSL() throws Exception { /* * 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 void testServedAliasIsTheNumberedDuplicate() throws Exception { 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)); + } } From 29057c0290a03578714330d49977a268ae37bb4e Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 29 Oct 2026 17:47:59 +0200 Subject: [PATCH 09/20] Ignore alias case when reporting a resolved certificate alias. --- java/org/apache/tomcat/util/net/AbstractEndpoint.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java b/java/org/apache/tomcat/util/net/AbstractEndpoint.java index 5bf6ff0c2c52..0dcad47a1c38 100644 --- a/java/org/apache/tomcat/util/net/AbstractEndpoint.java +++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java @@ -653,14 +653,14 @@ protected void logCertificate(SSLHostConfigCertificate certificate) { String configuredAlias = certificate.getCertificateKeyAlias(); if (sslContext != null && configuredAlias != null) { String servedAlias = sslContext.getServedAlias(certificate); - if (servedAlias != null && !servedAlias.equals(configuredAlias)) { + if (servedAlias != null && !servedAlias.equalsIgnoreCase(configuredAlias)) { getLogCertificate() .info(sm.getString("endpoint.tls.info.cert.aliasResolved", configuredAlias, servedAlias)); } } if (getLogCertificate().isDebugEnabled() && sslContext != null) { - X509Certificate[] x509Certificates = sslContext.getCertificateChain(certificate); + X509Certificate[] x509Certificates = sslContext.getCertificateChain(sslContext.getServedAlias(certificate)); if (x509Certificates != null && x509Certificates.length > 0) { getLogCertificate().debug(generateCertificateDebug(x509Certificates[0])); } else { From 00f5777e1e00713b36b949c05baf24667383baf2 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 29 Oct 2026 17:48:22 +0200 Subject: [PATCH 10/20] Simplify by removing redundant method. --- .../catalina/manager/ManagerServlet.java | 3 ++- .../apache/tomcat/util/net/SSLContext.java | 23 +++---------------- .../apache/tomcat/util/net/SSLHostConfig.java | 3 ++- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/java/org/apache/catalina/manager/ManagerServlet.java b/java/org/apache/catalina/manager/ManagerServlet.java index 8d899467dc85..7a4c77450adf 100644 --- a/java/org/apache/catalina/manager/ManagerServlet.java +++ b/java/org/apache/catalina/manager/ManagerServlet.java @@ -1692,7 +1692,8 @@ protected Map> getConnectorCerts(StringManager smClient) { if (sslContext == null) { certList.add(smClient.getString("managerServlet.certsNotLoaded")); } else { - X509Certificate[] certs = sslContext.getCertificateChain(sslHostConfigCert); + X509Certificate[] certs = sslContext.getCertificateChain( + sslContext.getServedAlias(sslHostConfigCert)); if (certs == null) { certList.add(smClient.getString("managerServlet.certsNotAvailable")); } else { diff --git a/java/org/apache/tomcat/util/net/SSLContext.java b/java/org/apache/tomcat/util/net/SSLContext.java index 7c3c4134ef6c..0f282f3b2d67 100644 --- a/java/org/apache/tomcat/util/net/SSLContext.java +++ b/java/org/apache/tomcat/util/net/SSLContext.java @@ -91,10 +91,7 @@ default SSLEngine createSSLEngine(boolean clientMode) { SSLParameters getSupportedSSLParameters(); /** - * Returns the certificate chain for the given key store alias. This is a low-level lookup by exact alias; callers - * that want the certificate actually served for a configured {@link SSLHostConfigCertificate} should use - * {@link #getCertificateChain(SSLHostConfigCertificate)}, which resolves the served alias (it can differ from the - * configured alias, for example after SunMSCAPI Windows-MY numbered-duplicate resolution). + * Returns the certificate chain for the given alias. * * @param alias The alias * @@ -137,25 +134,11 @@ default String getServedAlias(SSLHostConfigCertificate certificate) { } } } - String configured = certificate.getCertificateKeyAlias(); - return configured == null ? SSLUtilBase.DEFAULT_KEY_ALIAS : configured; + String configuredAlias = certificate.getCertificateKeyAlias(); + return configuredAlias == null ? SSLUtilBase.DEFAULT_KEY_ALIAS : configuredAlias; } - /** - * Returns the certificate chain actually served for the given certificate configuration. Unlike - * {@link #getCertificateChain(String)}, which looks up a fixed alias, this uses {@link #getServedAlias}, so callers - * such as the certificate reload check observe the certificate that is really in use. This can differ from the - * configured alias, for example after SunMSCAPI Windows-MY duplicate resolution. - * - * @param certificate The certificate configuration - * - * @return The served certificate chain, or {@code null} if none can be determined - */ - default X509Certificate[] getCertificateChain(SSLHostConfigCertificate certificate) { - return getCertificateChain(getServedAlias(certificate)); - } - /** * Returns the accepted issuers. * diff --git a/java/org/apache/tomcat/util/net/SSLHostConfig.java b/java/org/apache/tomcat/util/net/SSLHostConfig.java index abd15b99b736..5da5973e0be9 100644 --- a/java/org/apache/tomcat/util/net/SSLHostConfig.java +++ b/java/org/apache/tomcat/util/net/SSLHostConfig.java @@ -1587,7 +1587,8 @@ public Set certificatesExpiringBefore(Date date) { for (SSLHostConfigCertificate sslHostConfigCertificate : sslHostConfigCertificates) { SSLContext sslContext = sslHostConfigCertificate.getSslContext(); if (sslContext != null) { - X509Certificate[] certificates = sslContext.getCertificateChain(sslHostConfigCertificate); + X509Certificate[] certificates = sslContext.getCertificateChain( + sslContext.getServedAlias(sslHostConfigCertificate)); if (certificates != null && certificates.length > 0) { X509Certificate certificate = certificates[0]; Date expirationDate = certificate.getNotAfter(); From 48d0fedd44513ab89a28971a420f6318bfe591ab Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 29 Oct 2026 18:01:26 +0200 Subject: [PATCH 11/20] Resolve the served alias once when logging a certificate --- .../tomcat/util/net/AbstractEndpoint.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java b/java/org/apache/tomcat/util/net/AbstractEndpoint.java index 0dcad47a1c38..29724d3368a8 100644 --- a/java/org/apache/tomcat/util/net/AbstractEndpoint.java +++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java @@ -648,23 +648,24 @@ protected void logCertificate(SSLHostConfigCertificate certificate) { getLogCertificate().info(sm.getString("endpoint.tls.info", getName(), sslHostConfig.getHostName(), certificate.getType(), certificateInfo, trustStoreSource)); - // 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. - String configuredAlias = certificate.getCertificateKeyAlias(); - if (sslContext != null && configuredAlias != null) { + if (sslContext != null) { String servedAlias = sslContext.getServedAlias(certificate); - if (servedAlias != null && !servedAlias.equalsIgnoreCase(configuredAlias)) { + + // 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. + String configuredAlias = certificate.getCertificateKeyAlias(); + if (configuredAlias != null && servedAlias != null && !servedAlias.equalsIgnoreCase(configuredAlias)) { getLogCertificate() .info(sm.getString("endpoint.tls.info.cert.aliasResolved", configuredAlias, servedAlias)); } - } - if (getLogCertificate().isDebugEnabled() && sslContext != null) { - X509Certificate[] x509Certificates = sslContext.getCertificateChain(sslContext.getServedAlias(certificate)); - if (x509Certificates != null && x509Certificates.length > 0) { - getLogCertificate().debug(generateCertificateDebug(x509Certificates[0])); - } else { - getLogCertificate().debug(sm.getString("endpoint.tls.cert.noCerts")); + if (getLogCertificate().isDebugEnabled()) { + X509Certificate[] x509Certificates = sslContext.getCertificateChain(servedAlias); + if (x509Certificates != null && x509Certificates.length > 0) { + getLogCertificate().debug(generateCertificateDebug(x509Certificates[0])); + } else { + getLogCertificate().debug(sm.getString("endpoint.tls.cert.noCerts")); + } } } } From a70a42297fee8b3595a8a5068fbe7a5ed0adad08 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 29 Oct 2026 18:10:29 +0200 Subject: [PATCH 12/20] Enhance docs --- java/org/apache/tomcat/util/net/SSLUtilBase.java | 5 +++-- webapps/docs/config/http.xml | 6 ++++-- webapps/docs/ssl-howto.xml | 8 +++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/java/org/apache/tomcat/util/net/SSLUtilBase.java b/java/org/apache/tomcat/util/net/SSLUtilBase.java index 94e5e4019be6..2e01fec02249 100644 --- a/java/org/apache/tomcat/util/net/SSLUtilBase.java +++ b/java/org/apache/tomcat/util/net/SSLUtilBase.java @@ -525,8 +525,9 @@ private static String findServerAuthAliasInWindowsMYFamily(KeyStore ks, String b * so rejecting them would needlessly exclude the many server certificates issued without an EKU.
  • *
  • A certificate that does carry an EKU extension is usable only if that extension explicitly * lists the {@code id-kp-serverAuth} OID ({@value Constants#SERVER_AUTH_EXTENDED_KEY_USAGE_OID}) or the - * {@code anyExtendedKeyUsage} OID ({@value Constants#ANY_EXTENDED_KEY_USAGE_OID}). Any other declared usage - * - for example a certificate limited to client authentication - is rejected.
  • + * {@code anyExtendedKeyUsage} OID ({@value Constants#ANY_EXTENDED_KEY_USAGE_OID}). Any other usage - for + * example a certificate limited to client authentication, or an extension that lists no usages at all - is + * rejected. *
  • A certificate whose EKU extension cannot be decoded is ignored (treated as not usable).
  • *
* diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml index 940bb7953464..c235bf0372ae 100644 --- a/webapps/docs/config/http.xml +++ b/webapps/docs/config/http.xml @@ -1677,8 +1677,10 @@ extension remain eligible. If no eligible key entry is found, connector initialization fails.

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 + server authentication. The exception is a Windows-MY store accessed through + the SunMSCAPI provider (key store type Windows-MY, + Windows-MY-CURRENTUSER or Windows-MY-LOCALMACHINE): + 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 " (1)", " (2)", etc. appended and whose certificate is valid, that certificate is used instead. That store diff --git a/webapps/docs/ssl-howto.xml b/webapps/docs/ssl-howto.xml index 946bfe1cab68..2fbbbc138ae3 100644 --- a/webapps/docs/ssl-howto.xml +++ b/webapps/docs/ssl-howto.xml @@ -457,9 +457,11 @@ excludes both purposes or cannot be parsed are ignored. If no eligible key entry is found, connector initialization fails.

A configured certificateKeyAlias 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 is unsuitable for server authentication. The one exception is a +Windows-MY store accessed through the SunMSCAPI provider (key store type +Windows-MY, Windows-MY-CURRENTUSER or +Windows-MY-LOCALMACHINE): if the certificate at the configured alias +is unsuitable there, but the store also holds a suitable certificate under that same name with " (1)", " (2)", 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 From bf6d7d5f46dd9e4ee076e21cdcdc21436ecc5376 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 29 Oct 2026 18:17:56 +0200 Subject: [PATCH 13/20] Skip key entries without a server-auth X.509 certificate when auto-selecting --- java/org/apache/tomcat/util/net/SSLUtilBase.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/java/org/apache/tomcat/util/net/SSLUtilBase.java b/java/org/apache/tomcat/util/net/SSLUtilBase.java index 2e01fec02249..ddfd8c19ec57 100644 --- a/java/org/apache/tomcat/util/net/SSLUtilBase.java +++ b/java/org/apache/tomcat/util/net/SSLUtilBase.java @@ -406,15 +406,17 @@ public KeyManager[] getKeyManagers() throws Exception { if (!ks.isKeyEntry(candidate)) { continue; } + // Select only a key entry with an X.509 certificate valid for server authentication. A non-X.509 + // or absent certificate cannot be validated or serve TLS, so skip it rather than prefer it. Certificate cert = ks.getCertificate(candidate); - if (cert instanceof X509Certificate x509 && !isServerAuthCapable(x509)) { + if (cert instanceof X509Certificate x509 && isServerAuthCapable(x509)) { + keyAlias = candidate; + } else { if (log.isDebugEnabled()) { log.debug(sm.getString("sslUtilBase.skippedKeyUsage", candidate)); } skippedForServerAuth = true; - continue; } - keyAlias = candidate; } if (keyAlias == null) { if (skippedForServerAuth) { From d2bd9ac203a3768b6ad57901607606c45b5980bb Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 29 Oct 2026 21:03:25 +0200 Subject: [PATCH 14/20] Replace runtime certificate generation with pre-generated fixtures --- ...LUtilBaseCertificateSelectionTestBase.java | 126 ++++++++++++++---- ...UtilBaseAutomaticCertificateSelection.java | 36 +++-- ...stSSLUtilBaseConfiguredAliasSelection.java | 82 ++++++------ .../util/net/TesterKeystoreGenerator.java | 87 ++---------- .../tomcat/util/net/localhost-rsa-anyauth.jks | Bin 0 -> 2639 bytes .../util/net/localhost-rsa-clientauth.jks | Bin 0 -> 2643 bytes .../tomcat/util/net/localhost-rsa-noeku.jks | Bin 0 -> 2757 bytes .../util/net/localhost-rsa-serverauth.jks | Bin 0 -> 2644 bytes .../util/net/localhost-rsa-serverauth2.jks | Bin 0 -> 2641 bytes 9 files changed, 169 insertions(+), 162 deletions(-) create mode 100644 test/org/apache/tomcat/util/net/localhost-rsa-anyauth.jks create mode 100644 test/org/apache/tomcat/util/net/localhost-rsa-clientauth.jks create mode 100644 test/org/apache/tomcat/util/net/localhost-rsa-noeku.jks create mode 100644 test/org/apache/tomcat/util/net/localhost-rsa-serverauth.jks create mode 100644 test/org/apache/tomcat/util/net/localhost-rsa-serverauth2.jks diff --git a/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java index e658cc5328f3..fb90d22679e6 100644 --- a/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java +++ b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java @@ -18,6 +18,8 @@ package org.apache.tomcat.util.net; import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; import java.io.Serial; import java.security.Key; import java.security.KeyStore; @@ -29,7 +31,9 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Stream; @@ -42,11 +46,6 @@ 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; /** @@ -91,19 +90,37 @@ public void configureSSLImplementation() throws Exception { 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)); + private static final String SERVER_AUTH_KEYSTORE = TesterSupport.SSL_DIR + "localhost-rsa-serverauth.jks"; + private static final String SERVER_AUTH_KEYSTORE_2 = TesterSupport.SSL_DIR + "localhost-rsa-serverauth2.jks"; + private static final String CLIENT_AUTH_KEYSTORE = TesterSupport.SSL_DIR + "localhost-rsa-clientauth.jks"; + private static final String ANY_AUTH_KEYSTORE = TesterSupport.SSL_DIR + "localhost-rsa-anyauth.jks"; + private static final String NO_EKU_KEYSTORE = TesterSupport.SSL_DIR + "localhost-rsa-noeku.jks"; + + protected enum CertType { + SERVER_AUTH, + CLIENT_AUTH, + ANY_AUTH, + NO_EKU + } + + private static Map> certificatePool; + + private record CertAndKey(PrivateKey key, X509Certificate certificate) { + } + /** + * An entry to place into a test key store: the alias, which pre-generated certificate to use, and whether it is a + * private key entry or a trusted (certificate-only) entry. + */ + protected record KeystoreEntry(String alias, CertType certType, boolean keyEntry) { + + protected static KeystoreEntry trusted(String alias) { + return new KeystoreEntry(alias, CertType.SERVER_AUTH, false); + } + } - protected static KeystoreEntry key(String alias, CertificateExtensionsCustomizer eku) { - return KeystoreEntry.key(alias, HOST, eku); + protected static KeystoreEntry key(String alias, CertType certType) { + return new KeystoreEntry(alias, certType, true); } @@ -150,8 +167,8 @@ protected SSLUtil sslUtil(SSLHostConfigCertificate certificate) throws Exception * The cut-off date is far enough in the future that every certificate qualifies, so the result is exactly the set * of certificates the listener monitors for reload. */ - protected Set certificatesMonitoredForReload(String alias, KeyStore keystore) throws Exception { - SSLHostConfigCertificate certificate = registeredCertificate(alias, keystore); + protected Set certificatesMonitoredForReload(KeyStore keystore) throws Exception { + SSLHostConfigCertificate certificate = registeredCertificate(keystore); SSLContext context = sslUtil(certificate).createSSLContext(List.of()); try { certificate.setSslContext(context); @@ -167,8 +184,8 @@ protected Set certificatesMonitoredForReload(String alias, KeyS * Builds and registers an SSLContext for the given configured alias and key store, then returns the alias the * context reports as actually served - which is what the certificate logging and reload paths resolve. */ - protected String servedAlias(String alias, KeyStore keystore) throws Exception { - SSLHostConfigCertificate certificate = registeredCertificate(alias, keystore); + protected String servedAlias(KeyStore keystore) throws Exception { + SSLHostConfigCertificate certificate = registeredCertificate(keystore); SSLContext context = sslUtil(certificate).createSSLContext(List.of()); try { certificate.setSslContext(context); @@ -179,20 +196,76 @@ protected String servedAlias(String alias, KeyStore keystore) throws Exception { } - private static SSLHostConfigCertificate registeredCertificate(String alias, KeyStore keystore) { + private static SSLHostConfigCertificate registeredCertificate(KeyStore keystore) { 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); - } + certificate.setCertificateKeyAlias(SSLUtilBaseCertificateSelectionTestBase.HOST); hostConfig.addCertificate(certificate); return certificate; } + private static synchronized Map> certificatePool() throws Exception { + if (certificatePool == null) { + Map> pool = new EnumMap<>(CertType.class); + pool.put(CertType.SERVER_AUTH, List.of(load(SERVER_AUTH_KEYSTORE), load(SERVER_AUTH_KEYSTORE_2))); + pool.put(CertType.CLIENT_AUTH, List.of(load(CLIENT_AUTH_KEYSTORE))); + pool.put(CertType.ANY_AUTH, List.of(load(ANY_AUTH_KEYSTORE))); + pool.put(CertType.NO_EKU, List.of(load(NO_EKU_KEYSTORE))); + certificatePool = pool; + } + return certificatePool; + } + + + private static CertAndKey load(String keystoreFile) throws Exception { + KeyStore keystore = KeyStore.getInstance(new File(keystoreFile), TesterSupport.JKS_PASS.toCharArray()); + String alias = keystore.aliases().nextElement(); + return new CertAndKey((PrivateKey) keystore.getKey(alias, TesterSupport.JKS_PASS.toCharArray()), + (X509Certificate) keystore.getCertificate(alias)); + } + + + /* + * Populates the key store from the pre-generated pool, one certificate per entry. Repeated entries of the same + * category are given distinct certificates where the pool holds more than one (so two server-auth entries in one + * store differ, as the duplicate-selection test requires). + */ + private static void populate(KeyStore keystore, KeystoreEntry... entries) throws Exception { + Map used = new EnumMap<>(CertType.class); + for (KeystoreEntry entry : entries) { + List options = certificatePool().get(entry.certType()); + CertAndKey chosen = options.get((used.merge(entry.certType(), 1, Integer::sum) - 1) % options.size()); + if (entry.keyEntry()) { + keystore.setKeyEntry(entry.alias(), chosen.key(), TesterSupport.JKS_PASS.toCharArray(), + new X509Certificate[] { chosen.certificate() }); + } else { + keystore.setCertificateEntry(entry.alias(), chosen.certificate()); + } + } + } + + + /* + * Writes a temporary JKS file populated from the pool, for the tests that configure a certificateKeystoreFile + * rather than an in-memory key store. + */ + protected static File keystoreFile(KeystoreEntry... entries) throws Exception { + KeyStore keystore = KeyStore.getInstance("JKS"); + keystore.load(null, null); + populate(keystore, entries); + File file = File.createTempFile("cert-selection-", ".jks"); + file.deleteOnExit(); + try (OutputStream os = new FileOutputStream(file)) { + keystore.store(os, TesterSupport.JKS_PASS.toCharArray()); + } + return file; + } + + protected static KeyStore orderedKeystore(String malformedAlias, KeystoreEntry... entries) throws Exception { return orderedKeystore(sunMSCAPIProvider(), "Windows-MY", KeyMode.EXPORTABLE, malformedAlias, entries); } @@ -251,8 +324,9 @@ protected enum KeyMode { protected static KeyStore orderedKeystore(Provider provider, String type, KeyMode keyMode, String malformedAlias, KeystoreEntry... entries) throws Exception { - File file = TesterKeystoreGenerator.generateKeystore(List.of(entries)); - KeyStore delegate = KeyStore.getInstance(file, TesterSupport.JKS_PASS.toCharArray()); + KeyStore delegate = KeyStore.getInstance("JKS"); + delegate.load(null, null); + populate(delegate, entries); List aliases = Stream.of(entries).map(KeystoreEntry::alias).toList(); // JKS type of keystore enumeration order is unspecified. diff --git a/test/org/apache/tomcat/util/net/TestSSLUtilBaseAutomaticCertificateSelection.java b/test/org/apache/tomcat/util/net/TestSSLUtilBaseAutomaticCertificateSelection.java index 776a60e15e83..d868c269bbf0 100644 --- a/test/org/apache/tomcat/util/net/TestSSLUtilBaseAutomaticCertificateSelection.java +++ b/test/org/apache/tomcat/util/net/TestSSLUtilBaseAutomaticCertificateSelection.java @@ -30,7 +30,6 @@ 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 @@ -43,8 +42,8 @@ public class TestSSLUtilBaseAutomaticCertificateSelection extends SSLUtilBaseCer @Test public void testAutoSelectPrefersServerAuthOverClientAuth() throws Exception { X509Certificate presented = present(null, - key("first", CLIENT_AUTH), - key("second", SERVER_AUTH)); + key("first", CertType.CLIENT_AUTH), + key("second", CertType.SERVER_AUTH)); assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); } @@ -52,8 +51,8 @@ public void testAutoSelectPrefersServerAuthOverClientAuth() throws Exception { @Test public void testAutoSelectAcceptsAnyExtendedKeyUsage() throws Exception { X509Certificate presented = present(null, - key("first", CLIENT_AUTH), - key("any", ANY_AUTH)); + key("first", CertType.CLIENT_AUTH), + key("any", CertType.ANY_AUTH)); assertEku(presented, Constants.ANY_EXTENDED_KEY_USAGE_OID); } @@ -61,8 +60,8 @@ public void testAutoSelectAcceptsAnyExtendedKeyUsage() throws Exception { @Test public void testAutoSelectAcceptsCertificateWithoutExtendedKeyUsage() throws Exception { X509Certificate presented = present(null, - key("first", CLIENT_AUTH), - key("none", null)); + key("first", CertType.CLIENT_AUTH), + key("none", CertType.NO_EKU)); Assert.assertNull("A certificate with no Extended Key Usage is unrestricted and must be selectable", presented.getExtendedKeyUsage()); } @@ -71,44 +70,44 @@ public void testAutoSelectAcceptsCertificateWithoutExtendedKeyUsage() throws Exc @Test public void testAutoSelectSkipsTrustedEntries() { Assert.assertThrows(IOException.class, () -> present(null, - KeystoreEntry.trusted("ca", HOST, SERVER_AUTH), - key("key", CLIENT_AUTH))); + KeystoreEntry.trusted("ca"), + key("key", CertType.CLIENT_AUTH))); } @Test public void testAutoSelectFailsWhenNoneServerAuth() { Assert.assertThrows(IOException.class, () -> present(null, - key("first", CLIENT_AUTH), - key("second", CLIENT_AUTH))); + key("first", CertType.CLIENT_AUTH), + key("second", CertType.CLIENT_AUTH))); } @Test public void testAutoSelectSkipsMalformedExtendedKeyUsage() throws Exception { - KeyStore keystore = orderedKeystore("malformed", key("malformed", CLIENT_AUTH), key("server", SERVER_AUTH)); + KeyStore keystore = orderedKeystore("malformed", key("malformed", CertType.CLIENT_AUTH), key("server", CertType.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)); + KeyStore keystore = orderedKeystore("malformed", key("malformed", CertType.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)); + KeyStore keystore = orderedKeystore(null, key("client", CertType.CLIENT_AUTH), key("first", CertType.ANY_AUTH), + key("second", CertType.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)); + KeyStore keystore = orderedKeystore(null, key("first", CertType.SERVER_AUTH), key("second", CertType.SERVER_AUTH)); SSLHostConfig hostConfig = new SSLHostConfig(); SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); certificate.setCertificateKeystore(keystore); @@ -121,7 +120,7 @@ public void testAutoSelectDoesNotSkipKeyReadFailure() throws Exception { @Test public void testSSLContextUsesSelectedCertificate() throws Exception { - KeyStore keystore = orderedKeystore(null, key("client", CLIENT_AUTH), key("server", SERVER_AUTH)); + KeyStore keystore = orderedKeystore(null, key("client", CertType.CLIENT_AUTH), key("server", CertType.SERVER_AUTH)); SSLContext context = sslUtil(null, keystore).createSSLContext(List.of()); try { Assert.assertEquals(keystore.getCertificate("server"), context.getCertificateChain("server")[0]); @@ -133,8 +132,7 @@ public void testSSLContextUsesSelectedCertificate() throws Exception { @Test public void testSSLContextLoadsSelectedCertificateFromKeystoreFile() throws Exception { - File file = TesterKeystoreGenerator.generateKeystore( - List.of(key(SSLUtilBase.DEFAULT_KEY_ALIAS, CLIENT_AUTH), key("server", SERVER_AUTH))); + File file = keystoreFile(key(SSLUtilBase.DEFAULT_KEY_ALIAS, CertType.CLIENT_AUTH), key("server", CertType.SERVER_AUTH)); KeyStore keystore = KeyStore.getInstance(file, TesterSupport.JKS_PASS.toCharArray()); SSLHostConfig hostConfig = new SSLHostConfig(); SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); diff --git a/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java index 0a8857ad90d3..fc4a37f02c91 100644 --- a/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java +++ b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java @@ -28,8 +28,6 @@ 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: @@ -42,8 +40,8 @@ public class TestSSLUtilBaseConfiguredAliasSelection extends SSLUtilBaseCertific @Test public void testConfiguredAliasIsUsedUnchangedWhenNotServerAuth() throws Exception { X509Certificate presented = present(HOST, - key(HOST, CLIENT_AUTH), - key("localhost-server", SERVER_AUTH)); + key(HOST, CertType.CLIENT_AUTH), + key("localhost-server", CertType.SERVER_AUTH)); assertEku(presented, CLIENT_AUTH_EXTENDED_KEY_USAGE_OID); } @@ -51,8 +49,8 @@ public void testConfiguredAliasIsUsedUnchangedWhenNotServerAuth() throws Excepti @Test public void testConfiguredAliasIsUsedWhenServerAuth() throws Exception { X509Certificate presented = present(HOST, - key(HOST, SERVER_AUTH), - key("localhost-client", CLIENT_AUTH)); + key(HOST, CertType.SERVER_AUTH), + key("localhost-client", CertType.CLIENT_AUTH)); assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); } @@ -60,8 +58,8 @@ public void testConfiguredAliasIsUsedWhenServerAuth() throws Exception { @Test public void testConfiguredAliasUsesNumberedDuplicateWhenBaseNotServerAuth() throws Exception { X509Certificate presented = present(HOST, - key(HOST, CLIENT_AUTH), - key(HOST + " (1)", SERVER_AUTH)); + key(HOST, CertType.CLIENT_AUTH), + key(HOST + " (1)", CertType.SERVER_AUTH)); assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); } @@ -69,8 +67,8 @@ public void testConfiguredAliasUsesNumberedDuplicateWhenBaseNotServerAuth() thro @Test public void testConfiguredAliasUnchangedForNonSunMSCAPIStore() throws Exception { KeyStore keystore = nonSunMSCAPIKeystore( - key(HOST, CLIENT_AUTH), - key(HOST + " (1)", SERVER_AUTH)); + key(HOST, CertType.CLIENT_AUTH), + key(HOST + " (1)", CertType.SERVER_AUTH)); X509Certificate presented = present(HOST, keystore); Assert.assertEquals(keystore.getCertificate(HOST), presented); } @@ -79,8 +77,8 @@ public void testConfiguredAliasUnchangedForNonSunMSCAPIStore() throws Exception @Test public void testConfiguredAliasKeepsBaseWhenAlreadyServerAuth() throws Exception { X509Certificate presented = present(HOST, - key(HOST, SERVER_AUTH), - key(HOST + " (1)", CLIENT_AUTH)); + key(HOST, CertType.SERVER_AUTH), + key(HOST + " (1)", CertType.CLIENT_AUTH)); assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); } @@ -88,9 +86,9 @@ public void testConfiguredAliasKeepsBaseWhenAlreadyServerAuth() throws Exception @Test public void testConfiguredAliasUsesSecondDuplicateWhenFirstIsNotServerAuth() throws Exception { X509Certificate presented = present(HOST, - key(HOST, CLIENT_AUTH), - key(HOST + " (1)", CLIENT_AUTH), - key(HOST + " (2)", SERVER_AUTH)); + key(HOST, CertType.CLIENT_AUTH), + key(HOST + " (1)", CertType.CLIENT_AUTH), + key(HOST + " (2)", CertType.SERVER_AUTH)); assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); } @@ -98,9 +96,9 @@ public void testConfiguredAliasUsesSecondDuplicateWhenFirstIsNotServerAuth() thr @Test public void testConfiguredAliasSelectsFirstOfSeveralServerAuthDuplicates() throws Exception { KeyStore keystore = orderedKeystore(null, - key("tomcat", CLIENT_AUTH), - key("tomcat (1)", SERVER_AUTH), - key("tomcat (2)", SERVER_AUTH)); + key("tomcat", CertType.CLIENT_AUTH), + key("tomcat (1)", CertType.SERVER_AUTH), + key("tomcat (2)", CertType.SERVER_AUTH)); X509Certificate presented = present("tomcat", keystore); Assert.assertEquals(keystore.getCertificate("tomcat (1)"), presented); } @@ -109,9 +107,9 @@ public void testConfiguredAliasSelectsFirstOfSeveralServerAuthDuplicates() throw @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)); + key(HOST, CertType.CLIENT_AUTH), + KeystoreEntry.trusted(HOST + " (1)"), + key(HOST + " (2)", CertType.SERVER_AUTH)); assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); } @@ -119,8 +117,8 @@ public void testConfiguredAliasSkipsTrustedDuplicateAndUsesNextKeyEntry() throws @Test public void testConfiguredAliasFallsBackToBaseWhenNoDuplicateIsServerAuth() throws Exception { X509Certificate presented = present(HOST, - key(HOST, CLIENT_AUTH), - key(HOST + " (1)", CLIENT_AUTH)); + key(HOST, CertType.CLIENT_AUTH), + key(HOST + " (1)", CertType.CLIENT_AUTH)); assertEku(presented, CLIENT_AUTH_EXTENDED_KEY_USAGE_OID); } @@ -128,15 +126,15 @@ public void testConfiguredAliasFallsBackToBaseWhenNoDuplicateIsServerAuth() thro @Test public void testConfiguredAliasIgnoresAliasNotMatchingDuplicateFormat() throws Exception { X509Certificate presented = present(HOST, - key(HOST, CLIENT_AUTH), - key(HOST + "(1)", SERVER_AUTH)); + key(HOST, CertType.CLIENT_AUTH), + key(HOST + "(1)", CertType.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)); + KeyStore keystore = orderedKeystore("malformed", key("malformed", CertType.CLIENT_AUTH), key("server", CertType.SERVER_AUTH)); Assert.assertEquals(keystore.getCertificateChain("malformed")[0], present("malformed", keystore)); } @@ -144,21 +142,21 @@ public void testConfiguredAliasWithMalformedExtendedKeyUsageIsUsed() throws Exce @Test public void testConfiguredAliasUsesNumberedDuplicateWhenBaseIsMalformed() throws Exception { KeyStore keystore = - orderedKeystore("malformed", key("malformed", CLIENT_AUTH), key("malformed (1)", SERVER_AUTH)); + orderedKeystore("malformed", key("malformed", CertType.CLIENT_AUTH), key("malformed (1)", CertType.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))); + Assert.assertThrows(IOException.class, () -> present("missing", key("server", CertType.SERVER_AUTH))); } @Test public void testConfiguredTrustedAliasFails() { - Assert.assertThrows(IOException.class, () -> present("trusted", KeystoreEntry.trusted("trusted", HOST, SERVER_AUTH), - key("server", SERVER_AUTH))); + Assert.assertThrows(IOException.class, () -> present("trusted", KeystoreEntry.trusted("trusted"), + key("server", CertType.SERVER_AUTH))); } @@ -172,9 +170,9 @@ public void testConfiguredTrustedAliasFails() { @Test public void testReloadListenerMonitorsTheServedCertificate() throws Exception { Assume.assumeTrue("JSSE".equals(connectorName)); - KeyStore keystore = nonExportableKeystore(key(HOST, CLIENT_AUTH), key(HOST + " (1)", SERVER_AUTH)); + KeyStore keystore = nonExportableKeystore(key(HOST, CertType.CLIENT_AUTH), key(HOST + " (1)", CertType.SERVER_AUTH)); - Set monitored = certificatesMonitoredForReload(HOST, keystore); + Set monitored = certificatesMonitoredForReload(keystore); X509Certificate served = (X509Certificate) keystore.getCertificateChain(HOST + " (1)")[0]; X509Certificate notServed = (X509Certificate) keystore.getCertificateChain(HOST)[0]; @@ -192,8 +190,8 @@ public void testReloadListenerMonitorsTheServedCertificate() throws Exception { @Test public void testConfiguredTrustedBaseAliasUsesServerAuthDuplicate() throws Exception { X509Certificate presented = present(HOST, - KeystoreEntry.trusted(HOST, HOST, SERVER_AUTH), - key(HOST + " (1)", SERVER_AUTH)); + KeystoreEntry.trusted(HOST), + key(HOST + " (1)", CertType.SERVER_AUTH)); assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); } @@ -201,8 +199,8 @@ public void testConfiguredTrustedBaseAliasUsesServerAuthDuplicate() throws Excep @Test public void testConfiguredAliasStopsAtGapAndKeepsBase() throws Exception { X509Certificate presented = present("tomcat", - key("tomcat", CLIENT_AUTH), - key("tomcat (2)", SERVER_AUTH)); + key("tomcat", CertType.CLIENT_AUTH), + key("tomcat (2)", CertType.SERVER_AUTH)); assertEku(presented, CLIENT_AUTH_EXTENDED_KEY_USAGE_OID); } @@ -217,9 +215,9 @@ public void testConfiguredAliasStopsAtGapAndKeepsBase() throws Exception { @Test public void testReloadResolvesServedCertificateWithOpenSSL() throws Exception { Assume.assumeTrue(useOpenSSL); - KeyStore keystore = orderedKeystore(null, key(HOST, CLIENT_AUTH), key(HOST + " (1)", SERVER_AUTH)); + KeyStore keystore = orderedKeystore(null, key(HOST, CertType.CLIENT_AUTH), key(HOST + " (1)", CertType.SERVER_AUTH)); - Set monitored = certificatesMonitoredForReload(HOST, keystore); + Set monitored = certificatesMonitoredForReload(keystore); X509Certificate served = (X509Certificate) keystore.getCertificateChain(HOST + " (1)")[0]; X509Certificate notServed = (X509Certificate) keystore.getCertificateChain(HOST)[0]; @@ -237,8 +235,8 @@ public void testReloadResolvesServedCertificateWithOpenSSL() throws Exception { @Test public void testServedAliasIsTheNumberedDuplicate() throws Exception { Assume.assumeTrue("JSSE".equals(connectorName)); - KeyStore keystore = nonExportableKeystore(key(HOST, CLIENT_AUTH), key(HOST + " (1)", SERVER_AUTH)); - Assert.assertEquals(HOST + " (1)", servedAlias(HOST, keystore)); + KeyStore keystore = nonExportableKeystore(key(HOST, CertType.CLIENT_AUTH), key(HOST + " (1)", CertType.SERVER_AUTH)); + Assert.assertEquals(HOST + " (1)", servedAlias(keystore)); } @@ -251,7 +249,7 @@ public void testServedAliasIsTheNumberedDuplicate() throws Exception { @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)); + KeyStore keystore = wrappedExportableKeystore(key(HOST, CertType.CLIENT_AUTH), key(HOST + " (1)", CertType.SERVER_AUTH)); + Assert.assertEquals(HOST + " (1)", servedAlias(keystore)); } } diff --git a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java index f7339ab81162..00f1772fccea 100644 --- a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java +++ b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java @@ -26,8 +26,6 @@ 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; @@ -43,8 +41,6 @@ public final class TesterKeystoreGenerator { - private static final AtomicLong SERIAL_COUNTER = new AtomicLong(1); - private TesterKeystoreGenerator() {} @FunctionalInterface @@ -54,84 +50,25 @@ void customize(KeyPair keyPair, X509v3CertificateBuilder certBuilder) } /** - * Generate a temporary JKS keystore containing a single self-signed RSA certificate. + * Generate a temporary JKS keystore containing a self-signed RSA certificate. * - * @param cn the Common Name for the certificate subject - * @param alias the keystore alias for the key entry - * @param sanNames DNS Subject Alternative Names to include, or {@code null} for none - * @param customizer callback to add extensions to the certificate, or {@code null} for none + * @param cn the Common Name for the certificate subject + * @param alias the keystore alias for the key entry + * @param sanNames DNS Subject Alternative Names to include, or {@code null} for none + * @param customizer callback to add extensions to the certificate, or {@code null} for none. * - * @return a temporary keystore file with password {@link TesterSupport#JKS_PASS} + * @return a temporary keystore file with password {@link TesterSupport#JKS_PASS} * - * @throws Exception if certificate generation or keystore creation fails + * @throws Exception if certificate generation or keystore creation fails */ public static File generateKeystore(String cn, String alias, String[] sanNames, CertificateExtensionsCustomizer customizer) throws Exception { - CertAndKey generated = generateSelfSignedCertificate(cn, sanNames, customizer); - - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(null, null); - ks.setKeyEntry(alias, generated.keyPair().getPrivate(), TesterSupport.JKS_PASS.toCharArray(), - new X509Certificate[] { generated.certificate() }); - - return storeToTempFile(ks); - } - - /** - * Generate a temporary JKS keystore from an explicit list of entries. Each entry has its own Common Name and may be - * either a private key entry or a trusted certificate entry, which lets tests reproduce stores that mix certificates - * with different Subjects and certificate-only (keyless) entries. - * - * @param entries the entries to add, in order - * - * @return a temporary keystore file with password {@link TesterSupport#JKS_PASS} - * - * @throws Exception if certificate generation or keystore creation fails - */ - public static File generateKeystore(List entries) throws Exception { - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(null, null); - - for (KeystoreEntry entry : entries) { - CertAndKey generated = generateSelfSignedCertificate(entry.cn(), null, entry.customizer()); - if (entry.keyEntry()) { - ks.setKeyEntry(entry.alias(), generated.keyPair().getPrivate(), TesterSupport.JKS_PASS.toCharArray(), - new X509Certificate[] { generated.certificate() }); - } else { - ks.setCertificateEntry(entry.alias(), generated.certificate()); - } - } - - return storeToTempFile(ks); - } - - /** - * A single entry for {@link #generateKeystore(List)}. - * - * @param alias the keystore alias - * @param cn the certificate Subject Common Name - * @param keyEntry {@code true} for a private key entry, {@code false} for a trusted certificate entry - * @param customizer extensions to add to the certificate, or {@code null} for none - */ - public record KeystoreEntry(String alias, String cn, boolean keyEntry, CertificateExtensionsCustomizer customizer) { - - public static KeystoreEntry key(String alias, String cn, CertificateExtensionsCustomizer customizer) { - return new KeystoreEntry(alias, cn, true, customizer); - } - - public static KeystoreEntry trusted(String alias, String cn, CertificateExtensionsCustomizer customizer) { - return new KeystoreEntry(alias, cn, false, customizer); - } - } - - private static CertAndKey generateSelfSignedCertificate(String cn, String[] sanNames, - CertificateExtensionsCustomizer customizer) throws Exception { KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(4096); KeyPair keyPair = kpg.generateKeyPair(); X500Name subject = new X500Name("CN=" + cn); - BigInteger serial = BigInteger.valueOf(SERIAL_COUNTER.getAndIncrement()); + BigInteger serial = BigInteger.valueOf(System.currentTimeMillis()); long oneDay = 86400000L; Date notBefore = new Date(System.currentTimeMillis() - oneDay); Date notAfter = new Date(System.currentTimeMillis() + 365L * oneDay); @@ -153,17 +90,17 @@ private static CertAndKey generateSelfSignedCertificate(String cn, String[] sanN ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()); X509Certificate certificate = new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer)); - return new CertAndKey(keyPair, certificate); - } - private record CertAndKey(KeyPair keyPair, X509Certificate certificate) {} + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(null, null); + ks.setKeyEntry(alias, keyPair.getPrivate(), TesterSupport.JKS_PASS.toCharArray(), new X509Certificate[] { certificate }); - private static File storeToTempFile(KeyStore ks) throws Exception { File keystoreFile = File.createTempFile("test-cert-", ".jks"); keystoreFile.deleteOnExit(); try (FileOutputStream fos = new FileOutputStream(keystoreFile)) { ks.store(fos, TesterSupport.JKS_PASS.toCharArray()); } + return keystoreFile; } diff --git a/test/org/apache/tomcat/util/net/localhost-rsa-anyauth.jks b/test/org/apache/tomcat/util/net/localhost-rsa-anyauth.jks new file mode 100644 index 0000000000000000000000000000000000000000..9133e6551bbd75364165f1dc97e9b336f59cc8fa GIT binary patch literal 2639 zcmcgu`8(8W8=rkJMi^OQjO@EFvSn)w!ZF#G$YdLgFk(V-3}cIwvPUUVsc5oq=jfD> zB_T_e6OP@XWI2rWrT3icI`2R5{%~K<^I3lQT=#Qd_x0>A?=OQuAjn~Xe-wg74fOJ$ z9m2w7Bq0?9f*k-1*bcyn;7~w-VPJU(4;Tz%z!r8-XZu>|JH(G(d>NtCrN@@Gq?8jB z5QD+5UL5u({Nee`CRNMwgQi33<#}G;2Aesg3|72ZQRU237h+Fo;8#`6fe~AM32>`| z(N&~gBh7L#qrL0h9UfT+d}mj#=d-0hC0Bg2+e>FvMrX5Gi(lfCk2STObt6THR2wFr zZ=ho8-@@*%3rTK0b`zO&v^AtA<)@e>1gJPo?Bv&T5M$H5K?mM*g>EGB8(_-B>l1Gs zIm0Un=yTD{c3IEhBC4EKZb)pZqzYJjmvj2<0Wo#EKW>2Df zlH9CIWf$_4A}W9!?Y379_cH}}W|Gh%GS!uST^5hzy01;kwbaZ{CtJZLWIb|lHm#L9 zzsZkWYKLdwJpaRppqoTL^R?qfUTAOSHtZN68ej zB>w)bTQQmuRm6agG&|r##aN0|N%^XsN`whysqyY6P{HdofFM~+Z8Rgc=q0X8Pcw2X zA~!ZDr(C!BI?tm&m%OiS<&}H|DteD^tbLSJ75u>8rHU=pjjocn60N*(|BCB$ndM*T zhQ4Pi2K8T=s8y4qh*4Rk57u@OgFU`|9mk!-SQOMb4A+5*VjiPQCU+%wxP-4Mb0U9- z*i@?J@p(yv$%zEd#EPy|-jI;f9wQ>TB6oT}oC>hHKaTHCIH_zb>}6w<8r)%?qN{G* zy)=H(xUwwdKt9&4-ra$XV7?VB95IS$%JDOO;R=cz819~bnVy3p!9yaW6jdcsU%`8F zwcAAYy+zNxRd1-LznaW2RCZR24YVxw^U^OCGI6>6&^YQuwGY&K!We^azCF_spLWM} z7Vh6)=H8Dcg;P%dK}OY9&k6<#HN(SaeOZ0oo6ELNNyMP?U&kXiKX1|AP4_)7;1Uk* zM=dJ#iEqJ!9)#Hl$)#ZE5W)a9oFCU;y61P_*55UXQJDYbW-Q;dN4TJ^^#fVGH`E81 znT?+)>vc*>_j8}r7fP20)>qs1FiBx%<7+qfeG`*(rN3fk`jA(7LVNpP8VIL-d*(Vm zd(c+IZxoCm9mn<@+>^hxQ`t=`h8`pYe|#!3RH&b$E0>L9J#bpsG=Jd`AzpXg>Kx8` zcxL!Y^X;&MJJ-N;q4ldVoY$s8k}C4-J2N`Dc)7c|&y@Zf)Ab}1rFuJ(Le&RGc>GEWiurof zJ+sEXbh>{N{N!^-FkO-Ps?(jX?aa&&Bh!<9taN`b3oc%uuZ$G?Y^-0IMN)P!BfSp) zS$k(WWjxqvTYL~ZouI?ly(T#zpl!N+j{@z6zt0vrxGC`sGe}(|ihD?7%`o#7#JNd- zaNP9U7wt@mmq_WoxPq~eEE9#>>?N6*?oSe1m*4ej-PJr*k*T(FL*Pz}iPi(w2V`Tk zu8w@l;0%5U(v&~bV<&Z$m#eI?Zn533mZ+s-E{J7WcQ+U*<9-n_(}If3514N~Ni%mz zzlat^R<&rvKlT_dOYfP|w%YzJ=&{hq1VdSC#@`)xMblFIHj8iN$6ACtD{BBcAP}4# zV8G!31G4-O3IRhP5N-|tjbN8gG)vq>gTW{`2#C#8MFH#xr~?dvfjHRx1K2PSV-r;j zz;mP^F&s{mfIv!+FO3?cDg#IzksKINdq1)mKG?(S@UWx$(84`J$YQ3{D?#2KG|Hjz z1R!{1L}7UTHrfAJ)?(H~f~qJWa-=~q$NyKeCx_C+2zWqL0IjB}3aII*YHMh#Yq}gl z?L(;jHw0p@|C99p8k-m3{%>sd0IHWqfFCuK1~9xg^vf2ja3p0!tYl%{7N(q)|PN6N0V9Lbgh1Fg7 zvt}*NZ|LSH_tGhpyddGpGUkw%&kGj0r(kZw30f~Ne@gFnTUq`uL40Bdm8W;3XkW~f zmlw~!GgmrcLv#H0DI?0YDpxW}tN@bTZF2f2(;AhLKS2>LDD}jaQ=3wkCa^Z`H@q+O zuXo~^)}K8Z&z)T%Tu3&~biO=5<1lNtL$w$#pg|iG~s*A zM*Vd@ny_Q5ks4ibe+c_nME=nP{+cqW@Dl>*qH(~CmgSh*CWet_@{OLlO8g9gf&;qj0b2u>4>{K9e2W_qRu}qcy~hHJ0@bMjG8yp`mo?2Kl7e(T?$$J zv$gOb%hP7y`VEQK1Cqj~Sb>dlixt2c=TUiny1%7fCCsH@d9Zw?L2ij;HmcSf+rNt9 zS>1^-h;i8oSJfle@yhz2<6tT^<)O21gI4~}i?ksM?yvFalW%<;9{NmG1+TUBhqBR= z=S*i&>rsuL_Vv;U^LWj8yE`Malbq+W35hnOz-zmClVNUW`J*0{+-!c9IDNU?k@&k7 z$Z=^$lz-5`u%qeiN4#O3nZt-pRc)e+KNnwhV07A^tV%$?^AZjv>uKM^d=72FEr&ai z31Cf%v+=ZCl32N7*0oHl&ASe-y{7Cg#h9P@CZ`#cW~QqtEK`@+ju&u~9BwMSxVPt7 z7yM?)-7sNLxV@e5B$d#!AcT&auWDbYSu2xvj}DC$j295t*43{pX-wEO@?|S-Fg`CK zs7kLQN?N|pjExIIx#Z;ca$9AMq}Y!ru4tDE`fh1dh$_%uR}gdN2#Md545h|40;U)V z$_@%0TwnA53Sup=DR3-ADh^?EK6Dx>PbWoBv?Xx&!O{?>9hdrFncVR2b{r>B($8dM PL90GJCok?6U0nMQ5vg=> literal 0 HcmV?d00001 diff --git a/test/org/apache/tomcat/util/net/localhost-rsa-clientauth.jks b/test/org/apache/tomcat/util/net/localhost-rsa-clientauth.jks new file mode 100644 index 0000000000000000000000000000000000000000..5c330f1f8ed72fd891fcae6cedc7623bff26d333 GIT binary patch literal 2643 zcmcgt`8U-28=uv_#AJ!=p)Ys%jIANNnq*(cR))qpwqa(H-B3u#z9mJno4RzfmFpr| zD#Vp_5GtawjD5NEz4x5^{Rh53Jm-C0%MY*fJkNRF2g?V`AP@+0RN$Y*L=N!7yONJ! z(QRh^Xb=c?2*kj40S+cMc_uInEC=BMgJChS1y4tycg@4SWiW2xYgdU+>9$NzfU=1} zo^17>iKvl4Puyl?U3aplXDep?S1$lydGqO(V_!92f~e(C(1!;4f$Qv-y>VzbZ}=bS zeWqe4UdPoiS&@ZR2I`)NW_woUZ+l;Ad8RF*TidtT_Cy=?Z+k_?4N*SooDDDzzul>z zSgPM(2v?TV#yp(z`_hh{<*K&~xiqH~tKYU4>cNbwh@%=EY)}?uM`}Yb=cBN$T$qet zF7^S@T~_1gnBLFiG@CM7vz$j^(-b4u{7kvxLnJvVe_*@$zG>Et_*;i0x|VUupL1`E zI%`ztcKa5}dKG{~>G0@QqG)M+wU}vtcywlk)RU?D=5x7K7lKF0g!tG#^*Nq4zr@Cw zAQ5K6EPYhySY^-z`+TCEE)uOB`A0ii`1RLotjI||qSYI@Ymf3=MJ1YKcyG8~9ivGz z3Ruq3cbw_?XY7nJ>oir?y-)+(w=b`t7b9Srl!t+*-sr9&r1gznyrAt^nH+dv&#am7 z18??vhq?Q%FrkcS26ny!6$kc}HH?$g&6ho(mdv`)Mu$ET)BHPS_$GH3^A@M|E_3tZ z5X`CxAkdQGi5#sc|DHBBDS6ua`*->S&C3nwC>-)fSuUNNtSY#^_~K>p8E2oN=@3z* za-_{c`9WxIfz9Q}lW)SEcx2=SKJlAlU>MX#s{2-JzNTX73-)24c}EeZ16 zoHDfpc4JX|%eUl`tF=3x@)*)ELz~zP-rgnhMuc3$np`#bW#w@vLnu4Lyr*ZWGJg^0 zCe2G8T2sq7bObxg?VEkpR2#P2CDQxk7nwvOB(_O@4J^rWRj)DwkuF3;&a%J+!7+Gj zxWYu}U2qNe&UqbzR}MKOgJkNds6nVsNi7C6P$R?F%_U#CLI()fiiY%M*wzpX{->o5 zdMdhI@};^K0$3)SfXUr?Y>Wf0j8gbID3_~!4*`;h6DzUoNImVYqY%hqszdnHJq~^4 zqXQz!23)=9+6X_~WkZ))s;c%;Z>fpW~Dn8aja7&&JQ}hP;+_i-y>5f zgpx=Ovzv#9ap;2pT7d`!TvOQL+QMLY2C~@$|PEauw*IlHf4L%$A0WVj^XR8WX$8(`!oA1>SY33gxP%X7oeY;g9KysBZfg6s`;_7$Pvn0^-H`xVbsQOM6WGj102E-I@Ohci|i_1>m3 zXM`V?=*SnfGC`wn+6IO)Fj86->b<(HS#4X(spCC?cU_Z*x}Rv{6T;O>UA%v>Js(&B z37fmi5-0Whl-G=eXK$Kk&$Bna6WPuZqGZNMwF3p-mOqhJU79a+|Ec9(1tJC&hogo<`w+qG|Jt#{b zg)kw(7c|-r?RlGHoTvUgi*S1dRX`mCf^z~fa8@7& zvholL0Ye~=6RZFy6RTXDVcaGs7|ad_0n|(sJHW~WwS_SuAhy>302Tz~!bKDU;5k;9 z5o~tezJA{Rp5y?3lq?{9OtK+FZM@vY^@y(cqsKbHgB|Ojm-<3_-|}h-vGR;uU7zx48(vX|J_tD90ZL4bAyg}Hb@K@44ML?u|onlNr0XIierAB%59JHEoI&+jIw_UB%v7bIJ zUvE|~hEG$^7BxXqM`2tXE_qG2n~0PoL89u1kM5rtnwkPei4kvS;u?9V)=N>yl-S-M znh#Q-y#NV?1%H;TDs;l)CgJxsMDQv}k2k|D+D<}X>-(DNnebeb{Y}6IC z=@A2s0-eEA{S0ch$bFwBhLX#Yh%*QM;)ZIv_8?t1CK1!C>z>z|XE6HBH`^*2gy!mc zP4i({#W|Y(a9#PwiL@MWPOHi0-e{V4`n|?+_qb$|UE=;}R zF!J5+kJUIjE^SKZ3Drt0?eH+=KM*#YW#g=E=me+&=FMS03d*I6&zVl~wstbs^mt>~ z$4|(!E`O#(jJ9+faA}`DHC@8~;$_BQU>iwWx#uZ9NC%K6{b%G^k{97@VX)UewD-h| zl00(N6T?z}bySFo);VtIgnL(OE8BQZ`}*2`;qzN{-g)M42P82xP16geJ`tjm0($G` z{fvL>>Y>xJwEQeH6QK>f9){C`;yb*T686UDdmJD_NWZdb|4n!*CUovqcE>E;--b{> z%Uo>LHfcZh2bfuL^k%Fyts{q9j!iX0{i_^^x3P P^YD(G;Cnl))tJ^_0d|8U literal 0 HcmV?d00001 diff --git a/test/org/apache/tomcat/util/net/localhost-rsa-noeku.jks b/test/org/apache/tomcat/util/net/localhost-rsa-noeku.jks new file mode 100644 index 0000000000000000000000000000000000000000..4cccd21a4bd6c0b1d1bb58cd9fdd4974935f2cdc GIT binary patch literal 2757 zcmcguc{J4P8=uW!Y%$1gkR|JPEJL!BrED>XvP_J~HpaL@ma!F*wPY=q$a*u$a*4^3 zY;}=+&lX)|$yT^izkAQQzrTL}{oeOH=RKeI^Eu~z&U2pkd7k67<24Wn1fd!Dr@={n zKCU7j&QVe(Ms0`JFRA!Izqu_P z4da|c)cRHdncOx{v_-SxIJ&Hs8%5IzE|+&DY6o=7EW-E$M=4fvo z5hn7N69=6+HwRy{9}PDt9+qU6%lDt)FPXqmLB*P`CxM@KM8`Vzhx&EoJ&qgOe!JE3 zn!n~W?7LKr++fcaK`%^i)pJIo9KWoV4hosxci-saif~ljWN`MMp1F zBQIc{2l^YxIdW~kj#?P=$m|%LHuh)(H5}kv&^$+eF|kKS|SWk7RTbH8Gy_twLnf8vVR<@9Vy=$Hi0bU+k@X0D# z33`QHT~VL9*PLrBq_b!jf0chV+@N$+CpWW}kgsn02F(Y8tVzNZlPNM2l*O|>uhdF$Q)rbFv)u61nDPT>11#=IDGO$5a&);Fit zk@(T>Me$nZooPg(|Kp=)8^bAUkC$GDezwlv#Bi;*`C#O_9qx;+Is15@&^PzhTRGlf zdx4!>R@XNfDs>Mq?w}K$EFe`B>kI!*(X42*%2P`xM_5rar=^y@j`|(QB&X)~RJ*4$ zOR?^BS`(s+H-Os@Ccr6K;iF2!S~7ezrD5hVXj zhoEf+zV3ZUB`8$pcE8EgYS3zm2>B_;y>u8^21A|Cxw)j-ynL`{z@OcbJ$Th(u1-As zuJU+sK9^R2>(hL5wVTL9p^GN#ouMAR+sZ6Ur%p;YMtw@n)5BOX8PzKK#!o6j_LM(w z%+x12O`?()2@8`~1|_U!0;9fGA!Hqt=kTCm$>_^}zTEetK!F{mh`{gkc7>ApyG(HZL#8T=zM`pf0D|f?MuMMrXJ}5B(poSREJ1=@M`!A|moz0IQDQ zehj;p?%;(D%U*+0zS=huzk2khNxs5JeXPL_x1VfkMC#2;>wKzyfEKjx&ziV*!JiXyuZefo1|2;ZPeG90{?p{s%B1 zAqE%FNPztZLytt*d3yVJ`raV<`JyEO@gJ86q!88vkJ2UJTxo~3pF1fCN5rEH{Q`a6 za3oKfkPqPbA!I_b|1HA)tkx(?GkvrWAn=2OBDwyX!{P%-D1BW(=roHW1`Q~lL#wH% zDXBWppc)OT{S5)~-G4m&zq-uHK*!E@;%jA}{Ka**Jl z4X)Mgp|hSzJ_TKC@ll75S?a55LrJ*8|^1h}R)Rkz^KN2wzQsO>m860FX&4RHQ zNvIV7y2Lom4sZHD3;9B&GS=8GF)YUO2WD_?*8cGg@(zs4%#+}4NNI7)d-W{wGxS0- zd35kQak@cS`13#;PSCB;rn2WOZHh zAW$#}Tu7b)$df-4O%Mr#03bjX&gc?n4368^1GB({xdkl0LO88kVph|1qHOCsysm3 zXmvH(`rnds0Mn0W*tA(c)=pzQ_cUH=QtxR!+!kb+E!hqZqsJV(z(wPb^MNlcT(*?3TZz?I zYL~xpk$KD46_ouL1`61EbFMq5U1E4+WQi5qy^K%e_W~uIr5L^vjLhY_n$?RChPUWf zuo+>+fQEOx{tL|h*^E_!eKC~mGuKt<#Rm@anV52Buw>M->fPI7gCjBR=g@A%5})|FWYmf^*SQL0qQ^6wgm}s_DovCHiqT8>w^hTdHzZ7=Rjv69wxgrz63f8$Xm3sW z^WsgjT{R!(ghaO_Izz1S=lkBg?_tYAqa6cfzLz)I+#Pe0w70v#^=Cx>#H2Gj=DBK@ wv{aA!RB%(|qm8tJw@Jw$I-C;u@DWlrCCUEI0_R*A@QXp=P06NQpLwRg05XxFcK`qY literal 0 HcmV?d00001 diff --git a/test/org/apache/tomcat/util/net/localhost-rsa-serverauth.jks b/test/org/apache/tomcat/util/net/localhost-rsa-serverauth.jks new file mode 100644 index 0000000000000000000000000000000000000000..dae52f78a36bc33e78cdb73fd8fb2589ace92025 GIT binary patch literal 2644 zcmcgt`8(8m8=mjX7{*v)%9b_RrSDkB)`G0bV5|va-?GO2|K#vOeT?l`#oAw0% zBlt9$_L3LrGf{+HWl3b{pT1!_@DJmwIfYnScaaFUxF$cTD7fud4+>cRfTDpT-spZsC$(NC&(-XY(Ed~lNXl4(nyRC^?9h#-e&0!I` zI&L9bv(bKHU4DzJHFq{;y4I6djR(zg2U zao-aLWN~H@PP6WNU-NZeAwG1cJ1 z3_(ic2?1V~(Bz`W)oG!acXPgm&9^Vq5vCAZ7D-l$N=^QyqL{_kwLP`ZbJhd5RY)}H zyyWS?_gLU@Q@XqsFa6@BMb`F~=Fn&>cgX?EpFuubjKM$V#T_2;Zm z#?bFumO1yOTs!h08gg4aT~fXI;Gyi*`4>)jLs`_9^$H~ld!ime<&3*7D^eworPTnd z6~v*)na)q~u+kwO(kZ|9gXE^P4qrTP926Q~w(=akrF%-Yepf6bStsp=NkzeQc!-ul zF8A$cgUCA}#edaU1Dy3^ln1*JEK)jdy`FpDag5dawz##;#kvJ8z5T~u&8uSijd`Z6 zqs??C%BMnWH0gcQGKC~vb`;rheK*>=&c6S}77bshod%x1GV9G%Qt zwG_9-PtGhSU5Mv2ejsd|W68&^WTT0-f74Pz($kX-x+m3ZX8`-EY#rV#(9_XvUHh*39;<1|9CkX@xY2QWqQR z42Z2n?WO+O;(ndk);EpCE|%yW zZ!3)3Y*S(P?)W7Av|86ZG`K7A%r#8(4B+~Ppl#6a~*oYK3>uXwhD zLSDVSflmz1+ksj5l(HTYYh3I2Zr)R(pu*U%jcs4OA2bQ6_ZfR7fP6WUA`rbIJK2dc zSHeg7R%l0^bbIYqc@beqP`F79w)&Ev)h*C2b7MpjHz+EOa0y3STl?CHq^z|+A?fI< zi(R_w@Mf$;xe;7D&xZWm}LZ+5Ple(QZS3pn7)>c0`CJcH6q_{?0`jb16Ac@hA=% zK{5>zBtw5>!=L~Z3Ozv!G9r-Tk=l{li~xW-ip#J>3<^XdU?y+`8fs$v51>OsHLhUL zAj>gBi>5bo^|Fn?2i4g&Xj+^voUc8GV|7CB2{n2CW<>XH!_&V}ydfoDLAo#l; z3AsRyV<8I7^0x^8JFWR~`sx^7ko%Z}p-=rUhj;Yz=U2ZB^0G5Z%VR)kMGRIBDk z6}!+RRY(}A&8VSAH1^x^BOyvW=UXajBHRtMs*t7=2`554dIQJP&xnM7WFfkqq#?@q z#ky?u*4HX(u$;B8=O?`)W+0L%VmWK|6cx16T!jLI5W!Sp`mVoF(e>=+wO!S1U~2e= zFP~UU$%)TgTa=KiGA>I2n7i9%N<;2=1k;KxrYgXgM7!dAe;ka3q7eS z-K{WqViy-_%8={x-TszPaET*`J{AbU89;gj-O;J1p`nEVAnLfu42Q#@G*kcnlA}F` zk)Z%2Icu!sP(p6)Pm5YEgPAxdGM!TvS254Z-S@KgP70KrM*eb2^BZHjTggKpp zxy4}j_K~VEozF>`=A8zlxbjqNf-mq9G zh5LO&sc>9cu!jyEDnaz~J2%>KDf?A%sfC6;M+tNLoCy{+?iLL#Sef>l5uzQZP)v*A zozKrZd`#ku_eIMRhXc4Ksxr%0J^6byuDwsyZ zBs1qQSBVNfZdMxfo(@re?-tGbosXWQ)uybdl+m@ehTCVy@NnTktjN|!-dD=Uq+Emk zWnbk|zqSGY*x>$KWSW+;{8vN5l+u`HNKd8yNbI$a3DvoxSkF&YnGbDxj9Lu4uaqUH YcP^>pR;I=(DuFB?&e1UK3{2a904+L%SO5S3 literal 0 HcmV?d00001 diff --git a/test/org/apache/tomcat/util/net/localhost-rsa-serverauth2.jks b/test/org/apache/tomcat/util/net/localhost-rsa-serverauth2.jks new file mode 100644 index 0000000000000000000000000000000000000000..32aa3d834c5d526330a2906e67ba302ea106a7c3 GIT binary patch literal 2641 zcmcgt`8U-28=o1oVC*xLvhNXn#=gtgr3gc0xf5b)5C(}1V_#B|J^StwGUQsya)(4B zO{7q$)QyOSv3}`$?>YDT4}5=k&ilNUA71Bqp7Xr-miCrFAQ0rBz`qJk^7r+?lMY}> z62dS51cL4Y6ettmfU_&W!BDU~gcl5kQlOvd9XDNvy)Pvny)r#5)LF8%T(OsXs%kb= zAhk}%sF;4Fz(F$g1JAc+chvVv^|5Z+S>3hDh4*$(OTTJ#CObnfwv(aT_uOtd(zJif z(H-g73UtmS2i??wY?&*P9c3E6AD=7xz`k#qtw5p?lQpE1foT62H)yk(PH4A>2%L{h zwCgxGLye4dcURTOe${?gjBlkj3uCkXY6xJp9iDq=s{*Tq^vV^^r?xBZl6Z<5dM7!$ zN2A7fP1AADXGc0eVG@Zr3=G2Ky05)%rp2J&b=p}7O8H_pVVuJ7`6}a`_J^T(1Q{EQ zynXz7=1!G+)79st3JA}89Ff{#nR*Xq67>k}7`b<2ZZ(bla|5}^Y5D#ySJDM14HxAN zq<^bhW1Z%%ul$HcO`aRMAU=6sN=&(hpAjfsXr%pP3K!cz8FEtoYltx=cV#Nc-dNuU zc+OV*{TNm7{jzxmAY~Jfi4K~?b@n`7AmcT?vsVVh@l7w?2IJXpVq1yLCG`!claKzES_p!G^^_5GG<5+p!QTq=;J#KXRBS{ns1$k zU5O;IN4A3d7tYVI!LZb=hvjV%cRQ~nn3Yo}u3y%?*tn({6S^?QLl#;tH>q=$I39Mq z6`{>5`0~f#hsb*KK~m_)N73g@!zbE}>}nd;&G%BFscBL*RQ5-ibY z7U399BXt9>kkFr#B1c`3+#yxz4EcM%)r|2et@F+q-!xI5fT+#rT&&o^^amBT+5nrM zgEg2H=gY-!@Amijj%)rTiqyF4*ay!)m;x;&9Q1Ui!|LBK5;(Ey{Oq0SU zN){dyw&qld{e&yfl#97sq^1atOP(3^k$AoIot&CZzMlPYG_VlaqARM`*YB_m*d*jO~QhC)>^ofb1!{It7PB)=_rTf$R z{BZJb{I)7coxV+JTdv~jX}Roncdbjp1|nl~Z10!l6F$|VL2a8)oUtK`MKPvC7hX>6 zk>?vlo^{L`m$3;-MwXef9_`_NcQu*uT|y(vRohnQ=>0%h{rJZ-HX$EaQNQ(=)af%Y zG5Wq#8V)J9WW?x>fnBs)zCJ5^ddjhDF1K$byS>zJ*UL(DX@ohW|H`a7%wBu*8KNUV z;a%vUeK^de#L>Jk*b26)hWfdaKP}pOqnTgRY^dedv^TR?c6O#ZMTosEp2P7{Vz$pv z1-9Asv%C?eb@+{H`2cfsvzGjF!Js9Tab@(h*euuL*kJUpqX@g8qy}h#Krl{#0%HRx zkkw)q2p9r^@UQ}$a5njP^Y{%;Fc=8~0nzs`NPrE_f`h_Q5S-mVfE5KXHo>3(-a`d} zVs|7EeF=WvB!54QEFgVIvZF-oeZ0gBui-ro9y|Zbq)>dImzb%4u%9QML^v=W1CAaV zktp83P4>S|wV2IWBa8?jbf~eQ`2Sb4_X;A385sg1M>v($F@UlbMpI2wMcw%TY92t% zzabEv_)pURYivG%=fAPpi2fdUqK|(N37~)_{@qkC48%eKbAt|eb_fLw22o>hw1)1* z0Ior`$4zfpazZ}-h?f#Y+=-|kfo!+uH7OQaIB@%Y=}o|?sz%4eTMa4#QD&S-`CjetN+=s3uY<|8AX!fXy2+Dlmpfn5X9z_oT%7 zo*sPq%=2q^i6D8P;WM(H46_oYVhxVxbUeu`VKm_1RM&eXIh^+ueN9U+ki3oO+xNu< z9U*3t0IhKwx~`Ceb;ZaaEk7~`>u?Psc>D%DW)7k8!2H~WcS;>M0)oz)e(Q198`HP0 zlju@PhE+Hl@VwL%Ha8#k60`mmd-lnutX8`W5QoK4)UxK}uFt-+fUtl;;4*)}_t%;U zqo5D~1PC9rQJ4!V07Y+!^z-UjdngUBsoqL_ciA#_C5s0TJ(MM(M*)E-w4U-ajh8%3 zn|KF~vVawjVCjf@F93CDkcM&q>~PkDe;x)yuz&&NA;}GevOr+(|2>j}J!hdnz#zYC zyJiH98T_0Xp?s`1rEmu>a7*)4X=qz1QgKnpjK|#Y?BwSMK|W4hNlHcm1t4^JiyPMX z*>)ibPP-r6+BU!X0#dge3wSyf>^4weOX`lEaTcEVQIceH$BAASi}yB_Tiq-@vj}uM zvA8Uzs%*j$>(mnUn?CdPuaBKc&CBdm+bzn=?WO0#HvGLuExJ{&$34)?D#6~L)`>3< z4IhabS*(@$&K06Gl`qfb8pZs&HbXzLXB#Qd_+~yuYMJ;{+HkM3FJe42Y91xu@Hs;y zS-#+r=O&8#ktmVR-NJp1Vi$z3M@8&azPU1KM(E?;R1IiobTwfGjNQcJds z$&y@QHVkh_CJ$w$Iw~Sl+S<5qRuPdUpRDXxAK&VE#TQ!jQT$E-Ow@v?klug7DrqBy zyL*RLGTb!4+6c>%rVEuOZXZX#S9k!|CRsH}FFCg`JtGtQUc`dvEX^7xiwVBpT2P$# z6S|ny3v4|qTVldQ{w5kp>ul#ehmi#EoJ7@M=Qy~epVGs-Z8jm*Y|THn@-uRlqPsH7 O|)I+RcV literal 0 HcmV?d00001 From 430ead8ff3cedcf9759b97031cead78407f925dc Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 29 Oct 2026 22:36:30 +0200 Subject: [PATCH 15/20] Skip an entry only when its cert is provably unusable. Absent/non-X.509 certs are accepted. --- java/org/apache/tomcat/util/net/SSLUtilBase.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/java/org/apache/tomcat/util/net/SSLUtilBase.java b/java/org/apache/tomcat/util/net/SSLUtilBase.java index ddfd8c19ec57..2e01fec02249 100644 --- a/java/org/apache/tomcat/util/net/SSLUtilBase.java +++ b/java/org/apache/tomcat/util/net/SSLUtilBase.java @@ -406,17 +406,15 @@ public KeyManager[] getKeyManagers() throws Exception { if (!ks.isKeyEntry(candidate)) { continue; } - // Select only a key entry with an X.509 certificate valid for server authentication. A non-X.509 - // or absent certificate cannot be validated or serve TLS, so skip it rather than prefer it. Certificate cert = ks.getCertificate(candidate); - if (cert instanceof X509Certificate x509 && isServerAuthCapable(x509)) { - keyAlias = candidate; - } else { + if (cert instanceof X509Certificate x509 && !isServerAuthCapable(x509)) { if (log.isDebugEnabled()) { log.debug(sm.getString("sslUtilBase.skippedKeyUsage", candidate)); } skippedForServerAuth = true; + continue; } + keyAlias = candidate; } if (keyAlias == null) { if (skippedForServerAuth) { From 5a0b9ebb3d1a964b194cf4436030cd9da76c1e6f Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 29 Oct 2026 22:39:18 +0200 Subject: [PATCH 16/20] The wrapped SSLContext and its key manager are supplied pre-configured, so Tomcat cannot reliably predict which alias a handshake will serve: the socket-based chooseServerAlias callback used elsewhere to probe the served alias may disagree with the chooseEngineServerAlias callback that an SSLEngine handshake actually uses. The alias the operator configured is therefore the only dependable answer for certificate monitoring. --- .../apache/tomcat/util/net/SSLContextWrapper.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/java/org/apache/tomcat/util/net/SSLContextWrapper.java b/java/org/apache/tomcat/util/net/SSLContextWrapper.java index b1b434d5f087..5c75ad3764df 100644 --- a/java/org/apache/tomcat/util/net/SSLContextWrapper.java +++ b/java/org/apache/tomcat/util/net/SSLContextWrapper.java @@ -81,9 +81,19 @@ public X509Certificate[] getCertificateChain(String alias) { } + /** + * {@inheritDoc} + *

+ * The wrapped {@code javax.net.ssl.SSLContext} and its key manager are supplied pre-configured, so Tomcat cannot + * reliably predict which alias a handshake will serve: the socket-based {@link X509KeyManager#chooseServerAlias} + * callback used elsewhere to probe the served alias may disagree with the {@code chooseEngineServerAlias} callback + * that an {@code SSLEngine} handshake actually uses. The alias the operator configured is therefore the only + * dependable answer for certificate monitoring. + */ @Override - public X509KeyManager[] getServerKeyManagers(SSLHostConfigCertificate certificate) { - return new X509KeyManager[] { keyManager }; + public String getServedAlias(SSLHostConfigCertificate certificate) { + String configuredAlias = certificate.getCertificateKeyAlias(); + return configuredAlias == null ? SSLUtilBase.DEFAULT_KEY_ALIAS : configuredAlias; } @Override From 2d3d8820b9ab885f376b03d8d6f762690d9a9984 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 29 Oct 2026 23:07:52 +0200 Subject: [PATCH 17/20] Non-exportable Windows keys require JSSE --- webapps/docs/ssl-howto.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/webapps/docs/ssl-howto.xml b/webapps/docs/ssl-howto.xml index 2fbbbc138ae3..6dc648d1595b 100644 --- a/webapps/docs/ssl-howto.xml +++ b/webapps/docs/ssl-howto.xml @@ -538,6 +538,13 @@ with a configured alias, ensure that the renewed certificate is available under that alias. Reloading does not select a different alias or prefer a certificate with a later expiry date.

+

If the certificate's private key is marked non-exportable, pin the JSSE implementation by adding +sslImplementationName="org.apache.tomcat.util.net.jsse.JSSEImplementation" +to the Connector. A connector otherwise selects an OpenSSL +implementation automatically when one is available, and OpenSSL cannot use a +private key it is unable to read, so initialization fails. An exportable key works +with either implementation.

+

Two points to note:

    From ed549e186e377200b8f7e9e255d720f06847bb35 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 29 Oct 2026 23:32:41 +0200 Subject: [PATCH 18/20] Add TestSSLContextWrapper --- .../util/net/TestSSLContextWrapper.java | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 test/org/apache/tomcat/util/net/TestSSLContextWrapper.java diff --git a/test/org/apache/tomcat/util/net/TestSSLContextWrapper.java b/test/org/apache/tomcat/util/net/TestSSLContextWrapper.java new file mode 100644 index 000000000000..a9f897be69ba --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestSSLContextWrapper.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.net.Socket; +import java.security.KeyStore; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Set; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests expiry monitoring for a supplied {@code javax.net.ssl.SSLContext} (see + * {@link SSLUtil#createSSLContext}) whose custom key manager selects different aliases for the socket and engine + * server-alias callbacks. Tomcat serves over an {@code SSLEngine}, so monitoring must track the engine selection, not + * the socket one. + */ +public class TestSSLContextWrapper { + + private static final char[] PASSWORD = TesterSupport.JKS_PASS.toCharArray(); + + private static final String ENGINE_KEYSTORE = "test/org/apache/tomcat/util/net/localhost-rsa-serverauth.jks"; + private static final String SOCKET_KEYSTORE = "test/org/apache/tomcat/util/net/localhost-rsa-serverauth2.jks"; + + private static final X509TrustManager TRUST = new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + // No-op: the test never validates a peer. + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + // No-op: the test never validates a peer. + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + + /* + * A key manager that deliberately disagrees between the socket and engine server-alias callbacks, as the JDK + * contract permits. The engine callback reflects the alias an SSLEngine handshake actually serves. + */ + private static final class SplitKeyManager extends X509ExtendedKeyManager { + private final X509KeyManager delegate; + + SplitKeyManager(X509KeyManager delegate) { + this.delegate = delegate; + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return "RSA".equals(keyType) ? "socket" : null; + } + + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) { + return "RSA".equals(keyType) ? "engine" : null; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return delegate.getCertificateChain(alias); + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return delegate.getPrivateKey(alias); + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return delegate.getServerAliases(keyType, issuers); + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return delegate.getClientAliases(keyType, issuers); + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return delegate.chooseClientAlias(keyType, issuers, socket); + } + } + + private static void copyKey(KeyStore target, String alias, String keystoreFile) throws Exception { + KeyStore source = KeyStore.getInstance(new File(keystoreFile), PASSWORD); + String original = source.aliases().nextElement(); + target.setKeyEntry(alias, source.getKey(original, PASSWORD), PASSWORD, source.getCertificateChain(original)); + } + + @Test + public void testExpiryMonitoringTracksEngineServedCertificate() throws Exception { + KeyStore store = KeyStore.getInstance("JKS"); + store.load(null, null); + copyKey(store, "engine", ENGINE_KEYSTORE); + copyKey(store, "socket", SOCKET_KEYSTORE); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(store, PASSWORD); + X509ExtendedKeyManager keyManager = new SplitKeyManager((X509KeyManager) kmf.getKeyManagers()[0]); + + javax.net.ssl.SSLContext jsse = javax.net.ssl.SSLContext.getInstance("TLS"); + jsse.init(new KeyManager[] { keyManager }, new TrustManager[] { TRUST }, null); + + SSLContext context = SSLUtil.createSSLContext(jsse, keyManager, TRUST); + SSLHostConfig host = new SSLHostConfig(); + SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(host, SSLHostConfigCertificate.Type.RSA); + certificate.setCertificateKeyAlias("engine"); + certificate.setSslContext(context); + host.addCertificate(certificate); + + X509Certificate engineCertificate = context.getCertificateChain("engine")[0]; + X509Certificate socketCertificate = context.getCertificateChain("socket")[0]; + Assert.assertNotEquals("Fixtures must provide two distinct certificates", engineCertificate, + socketCertificate); + + Set monitored = host.certificatesExpiringBefore(new Date(Long.MAX_VALUE)); + + Assert.assertTrue("The engine-served certificate must be monitored for expiry", + monitored.contains(engineCertificate)); + Assert.assertFalse("The socket-selected certificate must not be monitored", + monitored.contains(socketCertificate)); + } +} From 258431520d4efdf328a35a901002b73c4ee0a35a Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Fri, 30 Oct 2026 01:02:52 +0200 Subject: [PATCH 19/20] Catch RuntimeException in case of weird provider --- java/org/apache/tomcat/util/net/SSLUtilBase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/org/apache/tomcat/util/net/SSLUtilBase.java b/java/org/apache/tomcat/util/net/SSLUtilBase.java index 2e01fec02249..c88c09d9c99c 100644 --- a/java/org/apache/tomcat/util/net/SSLUtilBase.java +++ b/java/org/apache/tomcat/util/net/SSLUtilBase.java @@ -539,7 +539,7 @@ private static boolean isServerAuthCapable(X509Certificate cert) { List objectIdentifiers; try { objectIdentifiers = cert.getExtendedKeyUsage(); - } catch (CertificateParsingException e) { + } catch (CertificateParsingException | RuntimeException e) { return false; } From bb33136afa9319ffdde422676fe28d48637e014d Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Fri, 30 Oct 2026 14:16:42 +0200 Subject: [PATCH 20/20] Align behavior with Panama --- .../org/apache/tomcat/util/net/openssl/LocalStrings.properties | 1 + java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties b/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties index dc73269ae3cd..3819c025c013 100644 --- a/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties +++ b/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties @@ -45,6 +45,7 @@ openssl.doubleInit=SSL context already initialized, ignoring openssl.errApplyConf=Could not apply OpenSSLConf to SSL context openssl.errCheckConf=Error during OpenSSLConf check openssl.errMakeConf=Could not create OpenSSLConf context +openssl.errorLoadingCertificate=Error loading certificate: [{0}] openssl.errorSSLCtxInit=Error initializing SSL context openssl.failSslContextMake=Unable to create SSLContext. Check that SSLEngine is enabled in the AprLifecycleListener, the AprLifecycleListener has initialised correctly and that a valid SSLProtocol has been specified openssl.invalidSslProtocol=An invalid value [{0}] was provided for the SSLProtocol attribute diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java index cc764adc0996..ba223897837c 100644 --- a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java @@ -579,6 +579,9 @@ public void addCertificate(SSLHostConfigCertificate certificate) throws Exceptio String alias = getServedAlias(certificate); X509Certificate[] chain = x509KeyManager.getCertificateChain(alias); PrivateKey key = x509KeyManager.getPrivateKey(alias); + if (key == null || key.getEncoded() == null || chain == null || chain.length == 0) { + throw new SSLException(sm.getString("openssl.errorLoadingCertificate", alias)); + } String encodedKey = BEGIN_KEY + Base64.getMimeEncoder(64, new byte[] { '\n' }).encodeToString(key.getEncoded()) + END_KEY; SSLContext.setCertificateRaw(state.ctx, chain[0].getEncoded(),