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
+
+
+
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
+ 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
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
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
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
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
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
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}
+ *