On Wed, Oct 21, 2026 at 11:24 PM Mark Thomas <[email protected]> wrote:
> On 21/10/2026 20:00, [email protected] wrote: > > 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. > > -1 to back-port without further changes. > > Cert with malformed EKU nows triggers start-up failure. The cert should > just be ignored. > > The configured alias should not be ignored. If it is configued it should > be used even if the cert is not valid and start-up failed. Ignoring the > alias also triggers all sorts of edge cases (look for uses of > getCertificateKeyAlias() ) Mark, I have addressed this again on https://github.com/apache/tomcat/pull/1098 taking your feedback into consideration. > > Do OpenSSL/Native and OpenSSL/FFM connectors configured with a keystore > use this new selection process? > I added those to the test class, my bad. > > To sum up, I think the only change that should happen here is that > certificates with invalid EKU should be ignored. Nothing else should > change. > > Mark > > > > > > > --- > > 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] > > > > > --------------------------------------------------------------------- > To unsubscribe, e-mail: [email protected] > For additional commands, e-mail: [email protected] Dimitris
