This is an automated email from the ASF dual-hosted git repository.
dsoumis pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push:
new f35fac5048 Select a JSSE certificate valid for TLS server
authentication
f35fac5048 is described below
commit f35fac504827b3c26a43b658e7c6ccee40be4721
Author: Dimitrios Soumis <[email protected]>
AuthorDate: Wed Oct 21 22:00:09 2026 +0300
Select a JSSE certificate valid for TLS server authentication
When configuring a JSSE Certificate from a key store that exposes several
certificates sharing a Subject, ignore those that are not valid for TLS
server authentication (by extended key usage) so a usable certificate is
selected rather than relying on the key store alias alone.
---
java/org/apache/tomcat/util/net/Constants.java | 7 +
.../apache/tomcat/util/net/LocalStrings.properties | 4 +
java/org/apache/tomcat/util/net/SSLUtilBase.java | 107 +++++++++++++
.../util/net/TestSSLUtilBaseKeyUsageSelection.java | 178 +++++++++++++++++++++
.../tomcat/util/net/TesterKeystoreGenerator.java | 84 ++++++++--
webapps/docs/changelog.xml | 7 +
webapps/docs/config/http.xml | 16 +-
webapps/docs/ssl-howto.xml | 94 +++++++++++
8 files changed, 482 insertions(+), 15 deletions(-)
diff --git a/java/org/apache/tomcat/util/net/Constants.java
b/java/org/apache/tomcat/util/net/Constants.java
index 1669adbfb3..80fcbca6a9 100644
--- a/java/org/apache/tomcat/util/net/Constants.java
+++ b/java/org/apache/tomcat/util/net/Constants.java
@@ -68,6 +68,13 @@ 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 CLIENT_AUTH_EXTENDED_KEY_USAGE_OID =
"1.3.6.1.5.5.7.3.2";
+ 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 6bc78573bd..db025be928 100644
--- a/java/org/apache/tomcat/util/net/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/net/LocalStrings.properties
@@ -175,12 +175,16 @@ sslUtilBase.alias_no_key_entry=Alias name [{0}] does not
identify a key entry
sslUtilBase.fipsPemNotSupported=FIPS enabled so PEM format certificate files
are not supported. Configure the certificate using a key store rather than the
certificateFile attribute
sslUtilBase.invalidTrustManagerClassName=The trustManagerClassName provided
[{0}] does not implement javax.net.ssl.TrustManager
sslUtilBase.keystore_load_failed=Failed to load keystore type [{0}] with path
[{1}] due to [{2}]
+sslUtilBase.multipleServerAuthCerts=Starting from alias [{0}], multiple
certificates valid for TLS server authentication were found: [{1}]. The first
one will be used.
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.noServerAuthCert=No certificate valid for TLS server
authentication was found for alias [{0}]. The configured alias will be used
unchanged.
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 in preference to the configured alias [{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 68ac2ecbb8..6c1903d157 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.nio.charset.StandardCharsets;
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;
@@ -36,6 +37,7 @@ import java.security.cert.CertificateException;
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;
@@ -59,6 +61,7 @@ import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509KeyManager;
+import javax.security.auth.x500.X500Principal;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
@@ -411,6 +414,8 @@ public abstract class SSLUtilBase implements SSLUtil {
}
}
+ keyAlias = selectServerAuthAlias(ks, keyAlias,
certificate.getCertificateKeyAlias() != null);
+
Key k = ks.getKey(keyAlias, keyPassArray);
if (k != null &&
!"DKS".equalsIgnoreCase(certificate.getCertificateKeystoreType()) &&
"PKCS#8".equalsIgnoreCase(k.getFormat())) {
@@ -450,6 +455,108 @@ public abstract class SSLUtilBase implements SSLUtil {
}
+ /*
+ * Prefer a key entry whose certificate is usable for TLS server
authentication. This handles key stores that can
+ * expose several certificates sharing a Subject (and therefore colliding
on similar aliases).
+ * The certificate currently identified by the alias provides the Subject
to match.
+ * When no alias was configured every key entry is a candidate.
+ * When no usable certificate is found, the configured alias is returned
unchanged.
+ */
+ private String selectServerAuthAlias(KeyStore ks, String currentAlias,
boolean aliasConfigured)
+ throws KeyStoreException, CertificateParsingException {
+
+ Certificate current = ks.getCertificate(currentAlias);
+ X509Certificate currentX509 = current instanceof X509Certificate x509
? x509 : null;
+
+ // If the current selection is already usable there is nothing to do.
+ if (currentX509 != null && isServerAuthCapable(currentX509)) {
+ return currentAlias;
+ }
+
+ X500Principal subject = currentX509 == null ? null :
currentX509.getSubjectX500Principal();
+
+ List<String> matches = new ArrayList<>();
+ // Defense-in-depth so no misbehaving provider can ever hang a
TLS-config path.
+ int remaining = ks.size();
+ Enumeration<String> aliases = ks.aliases();
+ while (aliases.hasMoreElements() && remaining-- > 0) {
+ String alias = aliases.nextElement();
+ if (!ks.isKeyEntry(alias)) {
+ continue;
+ }
+ Certificate candidate = ks.getCertificate(alias);
+ if (!(candidate instanceof X509Certificate x509Candidate)) {
+ continue;
+ }
+ // When the user named an alias, only consider certificates that
share its Subject.
+ if (aliasConfigured && subject != null &&
!subject.equals(x509Candidate.getSubjectX500Principal())) {
+ continue;
+ }
+ if (isServerAuthCapable(x509Candidate)) {
+ matches.add(alias);
+ } else if (log.isDebugEnabled()) {
+ log.debug(sm.getString("sslUtilBase.skippedKeyUsage", alias));
+ }
+ }
+
+ if (matches.isEmpty()) {
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("sslUtilBase.noServerAuthCert",
currentAlias));
+ }
+ return currentAlias;
+ }
+ if (matches.size() > 1 && log.isDebugEnabled()) {
+ log.debug(sm.getString("sslUtilBase.multipleServerAuthCerts",
currentAlias, matches));
+ }
+ String selected = matches.get(0);
+ if (!selected.equals(currentAlias) && log.isDebugEnabled()) {
+ log.debug(sm.getString("sslUtilBase.selectedServerAuthCert",
selected, currentAlias));
+ }
+ return selected;
+ }
+
+
+ /**
+ * Determines whether the given certificate may be used for TLS server
authentication, based on its Extended Key
+ * Usage (EKU) extension. Called by {@link #selectServerAuthAlias} to
choose between certificates that share a
+ * Subject.
+ * <p>
+ * The policy is deliberately lenient about absent usage and strict about
declared usage:
+ * <ul>
+ * <li>A certificate with <strong>no EKU extension</strong> ({@link
X509Certificate#getExtendedKeyUsage()} returns
+ * {@code null}) is treated as usable. An absent EKU places no
restriction on the certificate's purpose - the
+ * way TLS implementations generally interpret such certificates - so
rejecting them would needlessly exclude
+ * the many server certificates issued without an EKU.</li>
+ * <li>A certificate that <strong>does</strong> 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, such
as a Remote Desktop authentication
+ * certificate - is rejected, which is what lets this feature skip the
wrong certificate.</li>
+ * </ul>
+ *
+ * @param cert the certificate to test
+ *
+ * @return {@code true} if the certificate may be used for TLS server
authentication
+ *
+ * @throws CertificateParsingException if the Extended Key Usage extension
cannot be decoded
+ */
+ private static boolean isServerAuthCapable(X509Certificate cert) throws
CertificateParsingException {
+ List<String> objectIdentifiers = cert.getExtendedKeyUsage();
+
+ 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 0000000000..a64224c80b
--- /dev/null
+++ b/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java
@@ -0,0 +1,178 @@
+/*
+ * 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.security.cert.X509Certificate;
+import java.util.List;
+
+import javax.net.ssl.X509KeyManager;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+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.apache.tomcat.util.net.jsse.JSSEUtil;
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
+
+/**
+ * Tests selection of a TLS server authentication certificate when a key store
holds several certificates that share a
+ * Subject.
+ */
+public class TestSSLUtilBaseKeyUsageSelection {
+
+ private static final String HOST = "localhost";
+
+ @SuppressWarnings("unused")
+ private static final CertificateExtensionsCustomizer SERVER_AUTH =
(keyPair, builder) -> builder
+ .addExtension(Extension.extendedKeyUsage, true, new
ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth));
+ @SuppressWarnings("unused")
+ private static final CertificateExtensionsCustomizer CLIENT_AUTH =
(keyPair, builder) -> builder
+ .addExtension(Extension.extendedKeyUsage, true, new
ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth));
+ @SuppressWarnings("unused")
+ private static final CertificateExtensionsCustomizer ANY_AUTH = (keyPair,
builder) -> builder
+ .addExtension(Extension.extendedKeyUsage, true, new
ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage));
+
+
+ private static KeystoreEntry key(String alias,
CertificateExtensionsCustomizer eku) {
+ return KeystoreEntry.key(alias, HOST, eku);
+ }
+
+
+ /*
+ * Builds a key store from the given entries, configures a JSSE
certificate pointed at alias (null for none), and
+ * returns the leaf certificate the key manager actually presents.
+ */
+ private static X509Certificate present(String alias, KeystoreEntry...
entries) throws Exception {
+ File keystore =
TesterKeystoreGenerator.generateKeystore(List.of(entries));
+
+ SSLHostConfig hostConfig = new SSLHostConfig();
+ SSLHostConfigCertificate certificate = new
SSLHostConfigCertificate(hostConfig, Type.UNDEFINED);
+ certificate.setCertificateKeystoreFile(keystore.getAbsolutePath());
+ certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS);
+ if (alias != null) {
+ certificate.setCertificateKeyAlias(alias);
+ }
+
+ X509KeyManager keyManager = (X509KeyManager) new
JSSEUtil(certificate).getKeyManagers()[0];
+ X509Certificate[] chain =
keyManager.getCertificateChain(keyManager.chooseServerAlias("RSA", null, null));
+ Assert.assertNotNull("Key manager returned no certificate chain",
chain);
+ return chain[0];
+ }
+
+
+ private static void assertEku(X509Certificate certificate, String oid)
throws Exception {
+ List<String> extendedKeyUsage = certificate.getExtendedKeyUsage();
+ Assert.assertNotNull("Presented certificate has no Extended Key
Usage", extendedKeyUsage);
+ Assert.assertTrue("Presented certificate must be valid for Extended
Key Usage " + oid,
+ extendedKeyUsage.contains(oid));
+ }
+
+
+ @Test
+ public void
testSelectsServerAuthCertificateOverConfiguredClientAuthAlias() throws
Exception {
+ X509Certificate presented = present(HOST,
+ key(HOST, CLIENT_AUTH),
+ key("localhost-server", SERVER_AUTH));
+ assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID);
+ }
+
+
+ @Test
+ public void testKeepsConfiguredAliasWhenItIsAlreadyServerAuth() throws
Exception {
+ X509Certificate presented = present(HOST,
+ key(HOST, SERVER_AUTH),
+ key("localhost-client", CLIENT_AUTH));
+ assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID);
+ }
+
+
+ @Test
+ public void testSelectsCertificateWithAnyExtendedKeyUsage() throws
Exception {
+ X509Certificate presented = present(HOST,
+ key(HOST, CLIENT_AUTH),
+ key("localhost-any", ANY_AUTH));
+ assertEku(presented, Constants.ANY_EXTENDED_KEY_USAGE_OID);
+ }
+
+
+ @Test
+ public void testSelectsCertificateWithoutExtendedKeyUsage() throws
Exception {
+ X509Certificate presented = present(HOST,
+ key(HOST, CLIENT_AUTH),
+ key("localhost-none", null));
+ Assert.assertNull("A certificate with no Extended Key Usage is
unrestricted and must be selectable",
+ presented.getExtendedKeyUsage());
+ }
+
+
+ @Test
+ public void testSelectsServerAuthCertificateWhenNoAliasConfigured() throws
Exception {
+ X509Certificate presented = present(null,
+ key("first", CLIENT_AUTH),
+ key("second", SERVER_AUTH));
+ assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID);
+ }
+
+
+ @Test
+ public void testSelectsServerAuthCertificateWhenMultipleMatch() throws
Exception {
+ // Two server-auth certificates share the Subject. Which one wins
depends on key-store enumeration order,
+ // which JKS does not guarantee, so this only asserts that the
ambiguity resolves to a server-auth
+ // certificate rather than the client-auth one - not which of the two
is chosen.
+ X509Certificate presented = present(HOST,
+ key(HOST, CLIENT_AUTH),
+ key("localhost-s1", SERVER_AUTH),
+ key("localhost-s2", SERVER_AUTH));
+ assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID);
+ }
+
+
+ @Test
+ public void testFallsBackToConfiguredAliasWhenNoServerAuthCertificate()
throws Exception {
+ X509Certificate presented = present(HOST,
+ key(HOST, CLIENT_AUTH),
+ key("localhost-2", CLIENT_AUTH));
+ assertEku(presented, Constants.CLIENT_AUTH_EXTENDED_KEY_USAGE_OID);
+ }
+
+
+ @Test
+ public void testIgnoresServerAuthCertificateWithDifferentSubject() throws
Exception {
+ X509Certificate presented = present(HOST,
+ key(HOST, CLIENT_AUTH),
+ KeystoreEntry.key("other-server", "other.example",
SERVER_AUTH));
+ assertEku(presented, Constants.CLIENT_AUTH_EXTENDED_KEY_USAGE_OID);
+ }
+
+
+ @Test
+ public void testIgnoresTrustedCertificateEntries() throws Exception {
+ // The only server-auth-capable certificate is a trusted (keyless)
entry, so it cannot be used as a server key.
+ // Selection must skip it and fall back to the configured key entry.
If trusted entries were NOT skipped, the
+ // keyless "ca" would be chosen and the resulting key manager would
have no chain for it.
+ X509Certificate presented = present(HOST,
+ KeystoreEntry.trusted("ca", HOST, SERVER_AUTH),
+ key(HOST, CLIENT_AUTH));
+ assertEku(presented, Constants.CLIENT_AUTH_EXTENDED_KEY_USAGE_OID);
+ }
+}
diff --git a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java
b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java
index 00f1772fcc..480ddf045b 100644
--- a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java
+++ b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java
@@ -26,6 +26,7 @@ import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Date;
+import java.util.List;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Extension;
@@ -50,25 +51,84 @@ public final class TesterKeystoreGenerator {
}
/**
- * Generate a temporary JKS keystore containing a self-signed RSA
certificate.
+ * Generate a temporary JKS keystore containing a single self-signed RSA
certificate.
*
- * @param cn the Common Name for the certificate subject
- * @param alias the keystore alias for the key entry
- * @param sanNames DNS Subject Alternative Names to include, or {@code
null} for none
- * @param customizer callback to add extensions to the certificate, or
{@code null} for none.
+ * @param cn the Common Name for the certificate subject
+ * @param alias the keystore alias for the key entry
+ * @param sanNames DNS Subject Alternative Names to include, or {@code
null} for none
+ * @param customizer callback to add extensions to the certificate, or
{@code null} for none
*
- * @return a temporary keystore file with password {@link
TesterSupport#JKS_PASS}
+ * @return a temporary keystore file with password {@link
TesterSupport#JKS_PASS}
*
- * @throws Exception if certificate generation or keystore creation fails
+ * @throws Exception if certificate generation or keystore creation fails
*/
public static File generateKeystore(String cn, String alias, String[]
sanNames,
CertificateExtensionsCustomizer
customizer) throws Exception {
+ CertAndKey generated = generateSelfSignedCertificate(cn, sanNames,
customizer);
+
+ KeyStore ks = KeyStore.getInstance("JKS");
+ ks.load(null, null);
+ ks.setKeyEntry(alias, generated.keyPair().getPrivate(),
TesterSupport.JKS_PASS.toCharArray(),
+ new X509Certificate[] { generated.certificate() });
+
+ return storeToTempFile(ks);
+ }
+
+ /**
+ * Generate a temporary JKS keystore from an explicit list of entries.
Each entry has its own Common Name and may be
+ * either a private key entry or a trusted certificate entry, which lets
tests reproduce stores that mix certificates
+ * with different Subjects and certificate-only (keyless) entries.
+ *
+ * @param entries the entries to add, in order
+ *
+ * @return a temporary keystore file with password {@link
TesterSupport#JKS_PASS}
+ *
+ * @throws Exception if certificate generation or keystore creation fails
+ */
+ public static File generateKeystore(List<KeystoreEntry> entries) throws
Exception {
+ KeyStore ks = KeyStore.getInstance("JKS");
+ ks.load(null, null);
+
+ for (KeystoreEntry entry : entries) {
+ CertAndKey generated = generateSelfSignedCertificate(entry.cn(),
null, entry.customizer());
+ if (entry.keyEntry()) {
+ ks.setKeyEntry(entry.alias(),
generated.keyPair().getPrivate(), TesterSupport.JKS_PASS.toCharArray(),
+ new X509Certificate[] { generated.certificate() });
+ } else {
+ ks.setCertificateEntry(entry.alias(), generated.certificate());
+ }
+ }
+
+ return storeToTempFile(ks);
+ }
+
+ /**
+ * A single entry for {@link #generateKeystore(List)}.
+ *
+ * @param alias the keystore alias
+ * @param cn the certificate Subject Common Name
+ * @param keyEntry {@code true} for a private key entry, {@code false}
for a trusted certificate entry
+ * @param customizer extensions to add to the certificate, or {@code null}
for none
+ */
+ public record KeystoreEntry(String alias, String cn, boolean keyEntry,
CertificateExtensionsCustomizer customizer) {
+
+ public static KeystoreEntry key(String alias, String cn,
CertificateExtensionsCustomizer customizer) {
+ return new KeystoreEntry(alias, cn, true, customizer);
+ }
+
+ public static KeystoreEntry trusted(String alias, String cn,
CertificateExtensionsCustomizer customizer) {
+ return new KeystoreEntry(alias, cn, false, customizer);
+ }
+ }
+
+ private static CertAndKey generateSelfSignedCertificate(String cn,
String[] sanNames,
+
CertificateExtensionsCustomizer customizer) throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(4096);
KeyPair keyPair = kpg.generateKeyPair();
X500Name subject = new X500Name("CN=" + cn);
- BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
+ BigInteger serial = BigInteger.valueOf(System.nanoTime());
long oneDay = 86400000L;
Date notBefore = new Date(System.currentTimeMillis() - oneDay);
Date notAfter = new Date(System.currentTimeMillis() + 365L * oneDay);
@@ -90,17 +150,17 @@ public final class TesterKeystoreGenerator {
ContentSigner signer = new
JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
X509Certificate certificate = new
JcaX509CertificateConverter().getCertificate(certBuilder.build(signer));
+ return new CertAndKey(keyPair, certificate);
+ }
- KeyStore ks = KeyStore.getInstance("JKS");
- ks.load(null, null);
- ks.setKeyEntry(alias, keyPair.getPrivate(),
TesterSupport.JKS_PASS.toCharArray(), new X509Certificate[] { certificate });
+ private record CertAndKey(KeyPair keyPair, X509Certificate certificate) {}
+ private static File storeToTempFile(KeyStore ks) throws Exception {
File keystoreFile = File.createTempFile("test-cert-", ".jks");
keystoreFile.deleteOnExit();
try (FileOutputStream fos = new FileOutputStream(keystoreFile)) {
ks.store(fos, TesterSupport.JKS_PASS.toCharArray());
}
-
return keystoreFile;
}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index b7011ac705..38e9f53c50 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -484,6 +484,13 @@
drop TLS 1.3 when rebuilding the list of enabled protocols during
initialization. (dsoumis)
</fix>
+ <update>
+ When configuring a JSSE <code>Certificate</code> from a key store that
+ exposes several certificates sharing a Subject, ignore certificates
+ that are not valid for TLS server authentication so that a usable
+ certificate is selected rather than relying on the key store to
+ return the correct one for the configured alias. (dsoumis)
+ </update>
<!-- Entries for backport and removal before 12.0.0-M1 below this line
-->
<fix>
Keep the asynchronous timeout in place until the timeout event has
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index 98607caa22..e244572972 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -1670,6 +1670,12 @@
the same order as they were added. If more than one key is present in the
keystore it is strongly recommended that a keyAlias is configured to
ensure that the correct key is used.</p>
+ <p>When several certificates in the key store share a Subject, Tomcat
automatically ignores any that are
+ not valid for TLS server authentication - i.e. whose Extended Key Usage
+ excludes both <code>serverAuth</code> and
<code>anyExtendedKeyUsage</code> -
+ so a usable certificate is selected. Certificates with no Extended Key
Usage
+ extension are unrestricted and remain eligible. If none qualify, the
+ configured alias is used unchanged.</p>
</attribute>
<attribute name="certificateKeyFile" required="false">
@@ -1859,9 +1865,13 @@
<subsection name="Key store types">
<p>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:</p>
+ 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:</p>
<ul>
<li>Set the certificateKeystoreType and/or truststoreType Connector
diff --git a/webapps/docs/ssl-howto.xml b/webapps/docs/ssl-howto.xml
index 8eee9f1210..af291b8714 100644
--- a/webapps/docs/ssl-howto.xml
+++ b/webapps/docs/ssl-howto.xml
@@ -446,6 +446,100 @@ mailing list</a>.</p>
</subsection>
</section>
+<section name="Automatic certificate selection and reloading">
+
+<p>When a key store contains more than one certificate, Tomcat must choose
which
+one to present to clients, and certificates must be replaced before they
expire.
+Two features help to automate this so that a single configuration can be
deployed
+to many hosts without per-host customization: selection of a certificate by key
+usage, and automatic reloading of renewed certificates.</p>
+
+<subsection name="Selecting a certificate by key usage">
+
+<p>When a key store holds several certificates - for example one intended for
TLS
+server authentication alongside one intended for another purpose, which may
share
+the same Subject - Tomcat selects a certificate that is valid for TLS server
+authentication. A certificate qualifies if its Extended Key Usage includes
+<code>serverAuth</code> or <code>anyExtendedKeyUsage</code>, or if it has no
+Extended Key Usage extension at all. Certificates that declare a different
purpose,
+such as client authentication, are ignored.</p>
+
+<p>Because of this, it is usually not necessary to set
+<strong>certificateKeyAlias</strong>: Tomcat picks a usable server certificate
+automatically. When <strong>certificateKeyAlias</strong> is set, only
certificates
+that share the Subject of that alias are considered.</p>
+
+<p>If no certificate valid for server authentication is found, the configured
+alias (or, when none is configured, the first key in the store) is used
unchanged,
+so existing configurations are not affected.</p>
+
+</subsection>
+
+<subsection name="Reloading certificates before they expire">
+
+<p>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 <strong>Server</strong> element:</p>
+
+<source><![CDATA[<Server ...>
+ <Listener
className="org.apache.catalina.security.TLSCertificateReloadListener" />
+ ...
+</Server>]]></source>
+
+<p>The listener periodically checks each certificate and, when one is within
+<code>daysBefore</code> days of expiry (14 by default), reloads the associated
+<code>SSLHostConfig</code> so that the renewed certificate is picked up. See
the
+<a href="config/listeners.html">listener documentation</a> for the available
+attributes.</p>
+
+</subsection>
+
+<subsection name="Example: the Windows certificate store">
+
+<p>On Windows, Tomcat can read certificates directly from the Windows
certificate
+store. Set <strong>certificateKeystoreType</strong> to
+<code>Windows-MY-LOCALMACHINE</code> for the local machine store, or to
+<code>Windows-MY</code> 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. This works well with AD CS autoenrollment, which
+issues and renews the machine's certificates automatically.</p>
+
+<p>The local machine store usually contains several certificates issued for the
+machine's name, such as a TLS server certificate and a Remote Desktop
+authentication certificate. Because Tomcat selects the certificate that is
valid
+for server authentication, the same configuration can be deployed to every host
+with no <strong>certificateKeyAlias</strong>:</p>
+
+<source><![CDATA[<Connector port="8443" protocol="HTTP/1.1"
+ SSLEnabled="true" scheme="https" secure="true">
+ <SSLHostConfig>
+ <Certificate certificateKeystoreType="Windows-MY-LOCALMACHINE"
+ certificateKeystoreFile=""
+ certificateKeystorePassword=""
+ protocols="TLSv1.2,TLSv1.3" />
+ </SSLHostConfig>
+</Connector>]]></source>
+
+<p>Combined with the reload listener described above this gives fully automated
+certificate handling: AD CS issues and renews the certificate, while Tomcat
+selects the correct one and reloads it before it expires.</p>
+
+<p>Two points to note:</p>
+
+<ul>
+ <li>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.</li>
+ <li>If a host holds more than one certificate valid for server
authentication,
+ set a unique friendly name on the intended certificate and use it as the
+ <strong>certificateKeyAlias</strong>.</li>
+</ul>
+
+</subsection>
+
+</section>
+
<section name="Using OCSP Certificates">
<p>Tomcat supports the Online Certificate Status Protocol (OCSP) to verify the
status of client provided certificates for a sub-set of connector
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]