On Thu, Oct 22, 2026 at 7:50 PM Mark Thomas <[email protected]> wrote:

> On 22/10/2026 17:31, Dimitris Soumis wrote:
> > 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.
>
> Thanks. I just took a quick look and provided some feedback. Let me know
> if it doesn't make sense. The good news is we can fake a keystore that
> looks like an MS store with duplicate keys.
>
> Mark
>
I investigated this, as described in a comment in
https://github.com/apache/tomcat/pull/1098

TL;DR: I see this as an OpenJDK issue and sent the following email:
https://mail.openjdk.org/archives/list/[email protected]/thread/XUCB6BURVVSJEFJMCEHP34XNUOSPVAGR/

Dimitris

>
> >
> >>
> >> 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
> >
>
>
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: [email protected]
> For additional commands, e-mail: [email protected]
>
>

Reply via email to