This is an automated email from the ASF dual-hosted git repository. dsoumis pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 4353c6232689dbbb518a51a8e41fcf43550532c4 Author: Dimitris Soumis <[email protected]> AuthorDate: Wed Oct 28 15:22:38 2026 +0200 Select a server-auth certificate for a configured Windows-MY alias When a Certificate is configured from a key store and no certificateKeyAlias is set, ignore key entries whose certificate is not valid for TLS server authentication when automatically selecting the key to use. A configured certificateKeyAlias is used even if its certificate is not valid, except in a Windows-MY key store accessed through the SunMSCAPI provider: there, if the configured certificate is not valid but a numbered duplicate of the alias ("alias (1)", "alias (2)", ...) carries a valid one, the duplicate is used instead - including when the configured alias is a keyless (trusted) entry, and continuing past gaps in the numbering. --- java/org/apache/tomcat/util/net/Constants.java | 12 + .../apache/tomcat/util/net/LocalStrings.properties | 2 + java/org/apache/tomcat/util/net/SSLUtilBase.java | 75 ++++- .../SSLUtilBaseCertificateSelectionTestBase.java | 205 ++++++++++++ ...stSSLUtilBaseAutomaticCertificateSelection.java | 156 ++++++++++ .../TestSSLUtilBaseConfiguredAliasSelection.java | 190 ++++++++++++ .../util/net/TestSSLUtilBaseKeyUsageSelection.java | 343 --------------------- .../tomcat/util/net/TesterKeystoreGenerator.java | 5 +- webapps/docs/changelog.xml | 10 +- webapps/docs/config/http.xml | 17 +- webapps/docs/ssl-howto.xml | 24 +- 11 files changed, 684 insertions(+), 355 deletions(-) diff --git a/java/org/apache/tomcat/util/net/Constants.java b/java/org/apache/tomcat/util/net/Constants.java index cfc9af27de..56bcc62883 100644 --- a/java/org/apache/tomcat/util/net/Constants.java +++ b/java/org/apache/tomcat/util/net/Constants.java @@ -76,6 +76,18 @@ public class Constants { public static final String SERVER_AUTH_EXTENDED_KEY_USAGE_OID = "1.3.6.1.5.5.7.3.1"; public static final String ANY_EXTENDED_KEY_USAGE_OID = "2.5.29.37.0"; + /** + * SunMSCAPI provider name. + */ + public static final String SUNMSCAPI_PROVIDER_NAME = "SunMSCAPI"; + + /** + * Windows-MY key store types. + */ + public static final String WINDOWS_MY_KEYSTORE_TYPE = "WINDOWS-MY"; + public static final String WINDOWS_MY_CURRENTUSER_KEYSTORE_TYPE = "WINDOWS-MY-CURRENTUSER"; + public static final String WINDOWS_MY_LOCALMACHINE_KEYSTORE_TYPE = "WINDOWS-MY-LOCALMACHINE"; + /** * Private constructor to prevent instantiation. */ diff --git a/java/org/apache/tomcat/util/net/LocalStrings.properties b/java/org/apache/tomcat/util/net/LocalStrings.properties index 724d1aa2a6..5821a88c6d 100644 --- a/java/org/apache/tomcat/util/net/LocalStrings.properties +++ b/java/org/apache/tomcat/util/net/LocalStrings.properties @@ -208,8 +208,10 @@ sslUtilBase.keystore_load_failed=Failed to load keystore type [{0}] with path [{ sslUtilBase.noCertFile=SSLHostConfig attribute certificateFile must be defined when using an SSL connector sslUtilBase.noCrlSupport=The truststoreProvider [{0}] does not support the certificateRevocationFile configuration option sslUtilBase.noKeys=No aliases for private keys found in key store +sslUtilBase.noServerAuthKey=The key store has private key entries but none has a certificate that is valid for TLS server authentication. Enable debug logging to see the skipped aliases, use a certificate whose Extended Key Usage permits server authentication, or configure certificateKeyAlias explicitly. sslUtilBase.noVerificationDepth=The truststoreProvider [{0}] does not support the certificateVerificationDepth configuration option sslUtilBase.noneSupported=None of the [{0}] specified are supported by the SSL engine : [{1}] +sslUtilBase.selectedServerAuthCert=Selected the certificate with alias [{0}] for TLS server authentication as the configured alias [{1}] is not valid for that purpose and the key store contains more than one certificate under that name sslUtilBase.skipped=Tomcat interprets the [{0}] attribute in a manner consistent with the latest OpenSSL development branch. Some of the specified [{0}] are not supported by the configured SSL engine for this connector (which may use JSSE or an older OpenSSL version) and have been skipped: [{1}] sslUtilBase.skippedKeyUsage=Skipping certificate with alias [{0}] as it is not valid for TLS server authentication sslUtilBase.ssl3=SSLv3 has been explicitly enabled. This protocol is known to be insecure. diff --git a/java/org/apache/tomcat/util/net/SSLUtilBase.java b/java/org/apache/tomcat/util/net/SSLUtilBase.java index 875319bf59..85880184b5 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; @@ -395,13 +396,12 @@ public abstract class SSLUtilBase implements SSLUtil { ksUsed.setKeyEntry(keyAlias, privateKeyFile.getPrivateKey(), keyPassArray, chain.toArray(new Certificate[0])); } else { - if (keyAlias != null && !ks.isKeyEntry(keyAlias)) { - throw new IOException(sm.getString("sslUtilBase.alias_no_key_entry", keyAlias)); - } else if (keyAlias == null) { + if (keyAlias == null) { Enumeration<String> aliases = ks.aliases(); if (!aliases.hasMoreElements()) { throw new IOException(sm.getString("sslUtilBase.noKeys")); } + boolean skippedForServerAuth = false; while (aliases.hasMoreElements() && keyAlias == null) { String candidate = aliases.nextElement(); if (!ks.isKeyEntry(candidate)) { @@ -412,13 +412,34 @@ public abstract class SSLUtilBase implements SSLUtil { if (log.isDebugEnabled()) { log.debug(sm.getString("sslUtilBase.skippedKeyUsage", candidate)); } + skippedForServerAuth = true; continue; } keyAlias = candidate; } if (keyAlias == null) { + if (skippedForServerAuth) { + throw new IOException(sm.getString("sslUtilBase.noServerAuthKey")); + } throw new IOException(sm.getString("sslUtilBase.alias_no_key_entry", (Object) null)); } + } else { + if (isSunMSCAPIWindowsMY(ks)) { + Certificate configuredCert = ks.isKeyEntry(keyAlias) ? ks.getCertificate(keyAlias) : null; + if (!(configuredCert instanceof X509Certificate) || + !isServerAuthCapable((X509Certificate) configuredCert)) { + String duplicateAlias = findServerAuthAliasInWindowsMYFamily(ks, keyAlias); + if (duplicateAlias != null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("sslUtilBase.selectedServerAuthCert", duplicateAlias, keyAlias)); + } + keyAlias = duplicateAlias; + } + } + } + if (!ks.isKeyEntry(keyAlias)) { + throw new IOException(sm.getString("sslUtilBase.alias_no_key_entry", keyAlias)); + } } Key k = ks.getKey(keyAlias, keyPassArray); @@ -460,6 +481,38 @@ public abstract class SSLUtilBase implements SSLUtil { } + /** + * Searches a SunMSCAPI Windows-MY key store for a key entry whose certificate is valid for TLS server + * authentication, among the numbered duplicates of {@code baseAlias}. When several certificates share a Windows + * "friendly name", that provider appends {@code " (1)"}, {@code " (2)"}, etc. to give them unique aliases, as the + * key store API requires. + * + * @param ks the SunMSCAPI Windows-MY key store to search + * @param baseAlias the configured alias. Not itself re-checked: the caller has already determined that its + * certificate is not valid for TLS server authentication. + * + * @return the alias of the first duplicate (in increasing numeric order starting at 1) whose certificate is + * valid for TLS server authentication, or {@code null} if there is no such duplicate + * + * @throws KeyStoreException if the key store has not been initialized + */ + private static String findServerAuthAliasInWindowsMYFamily(KeyStore ks, String baseAlias) throws KeyStoreException { + int limit = Collections.list(ks.aliases()).size(); + for (int suffix = 1; suffix <= limit; suffix++) { + String candidateAlias = baseAlias + " (" + suffix + ")"; + Certificate candidateCert = ks.getCertificate(candidateAlias); + if (candidateCert == null) { + continue; + } + if (candidateCert instanceof X509Certificate && ks.isKeyEntry(candidateAlias) && + isServerAuthCapable((X509Certificate) candidateCert)) { + return candidateAlias; + } + } + return null; + } + + /** * Determines whether the given certificate may be used for TLS server authentication, based on its Extended Key * Usage (EKU) extension. Used by {@link #getKeyManagers()} when auto-selecting a key entry so that certificates @@ -504,6 +557,22 @@ public abstract class SSLUtilBase implements SSLUtil { return false; } + private static boolean isSunMSCAPIWindowsMY(KeyStore ks) { + if (ks == null || !Constants.SUNMSCAPI_PROVIDER_NAME.equals(ks.getProvider().getName())) { + return false; + } + + //Windows-ROOT is excluded because it is a keyless trust store + switch (ks.getType().toUpperCase(Locale.ROOT)) { + case Constants.WINDOWS_MY_KEYSTORE_TYPE: + case Constants.WINDOWS_MY_CURRENTUSER_KEYSTORE_TYPE: + case Constants.WINDOWS_MY_LOCALMACHINE_KEYSTORE_TYPE: + return true; + default: + return false; + } + } + @Override public String[] getEnabledProtocols() { return enabledProtocols; diff --git a/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java new file mode 100644 index 0000000000..5e28cbbc8a --- /dev/null +++ b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java @@ -0,0 +1,205 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.util.net; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreSpi; +import java.security.Provider; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.net.ssl.X509KeyManager; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; +import org.apache.tomcat.util.net.TesterKeystoreGenerator.CertificateExtensionsCustomizer; +import org.apache.tomcat.util.net.TesterKeystoreGenerator.KeystoreEntry; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.easymock.EasyMock; + +/** + * Shared fixtures for the {@link SSLUtilBase#getKeyManagers()} certificate-selection tests. The two behaviors are + * split across the concrete subclasses: {@link TestSSLUtilBaseAutomaticCertificateSelection} covers Extended Key + * Usage based selection when no alias is configured, and {@link TestSSLUtilBaseConfiguredAliasSelection} covers a + * configured alias, including the SunMSCAPI Windows-MY numbered-duplicate resolution. + * <p> + * {@link #orderedKeystore(String, KeystoreEntry...)} returns a key store with a controlled alias enumeration order + * that presents as a SunMSCAPI Windows-MY store; {@link #nonSunMSCAPIKeystore(KeystoreEntry...)} presents as a plain + * JKS store, so that the SunMSCAPI-only duplicate search is skipped. + */ +public abstract class SSLUtilBaseCertificateSelectionTestBase extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{0}") + public static Collection<Object[]> parameters() { + return Arrays.asList(new Object[] { "JSSE", Boolean.FALSE, + "org.apache.tomcat.util.net.jsse.JSSEImplementation" }, + new Object[] { "OpenSSL", Boolean.TRUE, + "org.apache.tomcat.util.net.openssl.OpenSSLImplementation" }, + new Object[] { "OpenSSL-FFM", Boolean.TRUE, + "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation" }); + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean useOpenSSL; + + @Parameter(2) + public String sslImplementationName; + + + @Before + public void configureSSLImplementation() throws Exception { + TesterSupport.configureSSLImplementation(getTomcatInstance(), sslImplementationName, useOpenSSL); + getTomcatInstance().getServer().init(); + } + + protected static final String HOST = "localhost"; + + protected static final String CLIENT_AUTH_EXTENDED_KEY_USAGE_OID = "1.3.6.1.5.5.7.3.2"; + + @SuppressWarnings("unused") + protected static final CertificateExtensionsCustomizer SERVER_AUTH = (keyPair, builder) -> builder + .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)); + @SuppressWarnings("unused") + protected static final CertificateExtensionsCustomizer CLIENT_AUTH = (keyPair, builder) -> builder + .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth)); + @SuppressWarnings("unused") + protected static final CertificateExtensionsCustomizer ANY_AUTH = (keyPair, builder) -> builder + .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage)); + + + protected static KeystoreEntry key(String alias, CertificateExtensionsCustomizer eku) { + return KeystoreEntry.key(alias, HOST, eku); + } + + + protected X509Certificate present(String alias, KeystoreEntry... entries) throws Exception { + return present(alias, orderedKeystore(null, entries)); + } + + + protected X509Certificate present(String alias, KeyStore keystore) throws Exception { + X509KeyManager keyManager = (X509KeyManager) sslUtil(alias, keystore).getKeyManagers()[0]; + X509Certificate[] chain = keyManager.getCertificateChain(keyManager.chooseServerAlias("RSA", null, null)); + Assert.assertNotNull("Key manager returned no certificate chain", chain); + return chain[0]; + } + + + protected SSLUtil sslUtil(String alias, KeyStore keystore) throws Exception { + + SSLHostConfig hostConfig = new SSLHostConfig(); + SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); + certificate.setCertificateKeystore(keystore); + certificate.setCertificateKeystoreType("JKS"); + certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS); + if (alias != null) { + certificate.setCertificateKeyAlias(alias); + } + + return sslUtil(certificate); + } + + + protected SSLUtil sslUtil(SSLHostConfigCertificate certificate) throws Exception { + SSLUtil util = SSLImplementation.getInstance(sslImplementationName).getSSLUtil(certificate); + SSLHostConfig hostConfig = certificate.getSSLHostConfig(); + hostConfig.setEnabledProtocols(util.getEnabledProtocols()); + hostConfig.setEnabledCiphers(util.getEnabledCiphers()); + return util; + } + + + protected static KeyStore orderedKeystore(String malformedAlias, KeystoreEntry... entries) throws Exception { + return orderedKeystore(new Provider("SunMSCAPI", 1.0, "test") { + private static final long serialVersionUID = -4141665633237722962L; + }, "Windows-MY", malformedAlias, entries); + } + + + protected static KeyStore nonSunMSCAPIKeystore(KeystoreEntry... entries) throws Exception { + return orderedKeystore(new Provider("SUN", 1.0, "test") { + private static final long serialVersionUID = 4434321038151233913L; + }, "JKS", null, entries); + } + + + protected static KeyStore orderedKeystore(Provider provider, String type, String malformedAlias, + KeystoreEntry... entries) throws Exception { + File file = TesterKeystoreGenerator.generateKeystore(Arrays.asList(entries)); + KeyStore delegate = KeyStore.getInstance("JKS"); + try (InputStream is = new FileInputStream(file)) { + delegate.load(is, TesterSupport.JKS_PASS.toCharArray()); + } + List<String> aliases = Stream.of(entries).map(KeystoreEntry::alias).collect(Collectors.toList()); + + // JKS type of keystore enumeration order is unspecified. + // Control that boundary while keeping real keys, certificate chains, and key managers. + // Malformed EKU must be injected here because a JKS parser can reject it while loading. + X509Certificate malformed = EasyMock.createMock(X509Certificate.class); + EasyMock.expect(malformed.getExtendedKeyUsage()).andStubThrow(new CertificateParsingException("Invalid EKU")); + EasyMock.replay(malformed); + KeyStoreSpi spi = EasyMock.createMock(KeyStoreSpi.class); + spi.engineLoad(null, null); + EasyMock.expectLastCall().once(); + EasyMock.expect(spi.engineAliases()).andStubAnswer(() -> Collections.enumeration(aliases)); + EasyMock.expect(spi.engineIsKeyEntry(EasyMock.anyString())) + .andStubAnswer(() -> delegate.isKeyEntry(EasyMock.getCurrentArgument(0))); + EasyMock.expect(spi.engineGetCertificate(EasyMock.anyString())).andStubAnswer(() -> { + String alias = EasyMock.getCurrentArgument(0); + return alias.equals(malformedAlias) ? malformed : delegate.getCertificate(alias); + }); + EasyMock.expect(spi.engineGetCertificateChain(EasyMock.anyString())) + .andStubAnswer(() -> delegate.getCertificateChain(EasyMock.getCurrentArgument(0))); + EasyMock.expect(spi.engineGetKey(EasyMock.anyString(), EasyMock.anyObject(char[].class))) + .andStubAnswer(() -> delegate.getKey(EasyMock.getCurrentArgument(0), EasyMock.getCurrentArgument(1))); + EasyMock.replay(spi); + // A real JKS backs the mock, so keys report PKCS#8 and getKeyManagers() takes its in-memory-copy branch + // rather than SunMSCAPI's non-exportable-key path; the provider name and store type are overridden only so + // isSunMSCAPIWindowsMY() can be exercised off Windows. + KeyStore result = new KeyStore(spi, provider, type) { }; + result.load(null, null); + return result; + } + + + protected static void assertEku(X509Certificate certificate, String oid) throws Exception { + List<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)); + } +} diff --git a/test/org/apache/tomcat/util/net/TestSSLUtilBaseAutomaticCertificateSelection.java b/test/org/apache/tomcat/util/net/TestSSLUtilBaseAutomaticCertificateSelection.java new file mode 100644 index 0000000000..7ad1c58a5a --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestSSLUtilBaseAutomaticCertificateSelection.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.util.net; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.UnrecoverableKeyException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; +import org.apache.tomcat.util.net.TesterKeystoreGenerator.KeystoreEntry; + +/** + * Tests that, when no {@code certificateKeyAlias} is configured, {@link SSLUtilBase#getKeyManagers()} auto-selects the + * first key entry whose certificate is valid for TLS server authentication (by Extended Key Usage) and ignores the + * entries that are not. Applies to every key store type. + */ +@RunWith(Parameterized.class) +public class TestSSLUtilBaseAutomaticCertificateSelection extends SSLUtilBaseCertificateSelectionTestBase { + + @Test + public void testAutoSelectPrefersServerAuthOverClientAuth() throws Exception { + X509Certificate presented = present(null, + key("first", CLIENT_AUTH), + key("second", SERVER_AUTH)); + assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testAutoSelectAcceptsAnyExtendedKeyUsage() throws Exception { + X509Certificate presented = present(null, + key("first", CLIENT_AUTH), + key("any", ANY_AUTH)); + assertEku(presented, Constants.ANY_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testAutoSelectAcceptsCertificateWithoutExtendedKeyUsage() throws Exception { + X509Certificate presented = present(null, + key("first", CLIENT_AUTH), + key("none", null)); + Assert.assertNull("A certificate with no Extended Key Usage is unrestricted and must be selectable", + presented.getExtendedKeyUsage()); + } + + + @Test + public void testAutoSelectSkipsTrustedEntries() { + Assert.assertThrows(IOException.class, () -> present(null, + KeystoreEntry.trusted("ca", HOST, SERVER_AUTH), + key("key", CLIENT_AUTH))); + } + + + @Test + public void testAutoSelectFailsWhenNoneServerAuth() { + Assert.assertThrows(IOException.class, () -> present(null, + key("first", CLIENT_AUTH), + key("second", CLIENT_AUTH))); + } + + + @Test + public void testAutoSelectSkipsMalformedExtendedKeyUsage() throws Exception { + KeyStore keystore = orderedKeystore("malformed", key("malformed", CLIENT_AUTH), key("server", SERVER_AUTH)); + assertEku(present(null, keystore), Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testAutoSelectFailsWhenOnlyMalformedExtendedKeyUsage() throws Exception { + KeyStore keystore = orderedKeystore("malformed", key("malformed", CLIENT_AUTH)); + Assert.assertThrows(IOException.class, () -> present(null, keystore)); + } + + + @Test + public void testAutoSelectUsesFirstEligibleKey() throws Exception { + KeyStore keystore = orderedKeystore(null, key("client", CLIENT_AUTH), key("first", ANY_AUTH), + key("second", SERVER_AUTH)); + Assert.assertEquals(keystore.getCertificate("first"), present(null, keystore)); + } + + + @Test + public void testAutoSelectDoesNotSkipKeyReadFailure() throws Exception { + KeyStore keystore = orderedKeystore(null, key("first", SERVER_AUTH), key("second", SERVER_AUTH)); + SSLHostConfig hostConfig = new SSLHostConfig(); + SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); + certificate.setCertificateKeystore(keystore); + certificate.setCertificateKeystoreType("JKS"); + certificate.setCertificateKeyPassword("incorrect"); + SSLUtil util = SSLImplementation.getInstance(sslImplementationName).getSSLUtil(certificate); + Assert.assertThrows(UnrecoverableKeyException.class, util::getKeyManagers); + } + + + @Test + public void testSSLContextUsesSelectedCertificate() throws Exception { + KeyStore keystore = orderedKeystore(null, key("client", CLIENT_AUTH), key("server", SERVER_AUTH)); + SSLContext context = sslUtil(null, keystore).createSSLContext(Collections.emptyList()); + try { + Assert.assertEquals(keystore.getCertificate("server"), context.getCertificateChain("server")[0]); + } finally { + context.destroy(); + } + } + + + @Test + public void testSSLContextLoadsSelectedCertificateFromKeystoreFile() throws Exception { + File file = TesterKeystoreGenerator.generateKeystore( + Arrays.asList(key(SSLUtilBase.DEFAULT_KEY_ALIAS, CLIENT_AUTH), key("server", SERVER_AUTH))); + KeyStore keystore = KeyStore.getInstance("JKS"); + try (InputStream is = new FileInputStream(file)) { + keystore.load(is, TesterSupport.JKS_PASS.toCharArray()); + } + SSLHostConfig hostConfig = new SSLHostConfig(); + SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); + certificate.setCertificateKeystoreFile(file.getAbsolutePath()); + certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS); + SSLContext context = sslUtil(certificate).createSSLContext(Collections.emptyList()); + try { + Assert.assertEquals(keystore.getCertificate("server"), context.getCertificateChain("server")[0]); + } finally { + context.destroy(); + } + } +} diff --git a/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java new file mode 100644 index 0000000000..911180f643 --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.util.net; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.cert.X509Certificate; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.apache.tomcat.util.net.TesterKeystoreGenerator.KeystoreEntry; + +/** + * Tests {@link SSLUtilBase#getKeyManagers()} when a {@code certificateKeyAlias} is configured. The configured alias is + * used even if its certificate is not valid for TLS server authentication, except in a SunMSCAPI Windows-MY key store: + * there, if a numbered duplicate of the alias (e.g. {@code "alias (1)"}) carries a certificate that is valid, it is + * used instead. Only numbered duplicates of the configured alias are considered, and only for that store type. + */ +@RunWith(Parameterized.class) +public class TestSSLUtilBaseConfiguredAliasSelection extends SSLUtilBaseCertificateSelectionTestBase { + + @Test + public void testConfiguredAliasIsUsedUnchangedWhenNotServerAuth() throws Exception { + X509Certificate presented = present(HOST, + key(HOST, CLIENT_AUTH), + key("localhost-server", SERVER_AUTH)); + assertEku(presented, CLIENT_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testConfiguredAliasIsUsedWhenServerAuth() throws Exception { + X509Certificate presented = present(HOST, + key(HOST, SERVER_AUTH), + key("localhost-client", CLIENT_AUTH)); + assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testConfiguredAliasUsesNumberedDuplicateWhenBaseNotServerAuth() throws Exception { + X509Certificate presented = present(HOST, + key(HOST, CLIENT_AUTH), + key(HOST + " (1)", SERVER_AUTH)); + assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testConfiguredAliasUnchangedForNonSunMSCAPIStore() throws Exception { + KeyStore keystore = nonSunMSCAPIKeystore( + key(HOST, CLIENT_AUTH), + key(HOST + " (1)", SERVER_AUTH)); + X509Certificate presented = present(HOST, keystore); + Assert.assertEquals(keystore.getCertificate(HOST), presented); + } + + + @Test + public void testConfiguredAliasKeepsBaseWhenAlreadyServerAuth() throws Exception { + X509Certificate presented = present(HOST, + key(HOST, SERVER_AUTH), + key(HOST + " (1)", CLIENT_AUTH)); + assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testConfiguredAliasUsesSecondDuplicateWhenFirstIsNotServerAuth() throws Exception { + X509Certificate presented = present(HOST, + key(HOST, CLIENT_AUTH), + key(HOST + " (1)", CLIENT_AUTH), + key(HOST + " (2)", SERVER_AUTH)); + assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testConfiguredAliasSelectsFirstOfSeveralServerAuthDuplicates() throws Exception { + KeyStore keystore = orderedKeystore(null, + key("tomcat", CLIENT_AUTH), + key("tomcat (1)", SERVER_AUTH), + key("tomcat (2)", SERVER_AUTH)); + X509Certificate presented = present("tomcat", keystore); + Assert.assertEquals(keystore.getCertificate("tomcat (1)"), presented); + } + + + @Test + public void testConfiguredAliasSkipsTrustedDuplicateAndUsesNextKeyEntry() throws Exception { + X509Certificate presented = present(HOST, + key(HOST, CLIENT_AUTH), + KeystoreEntry.trusted(HOST + " (1)", HOST, SERVER_AUTH), + key(HOST + " (2)", SERVER_AUTH)); + assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testConfiguredAliasFallsBackToBaseWhenNoDuplicateIsServerAuth() throws Exception { + X509Certificate presented = present(HOST, + key(HOST, CLIENT_AUTH), + key(HOST + " (1)", CLIENT_AUTH)); + assertEku(presented, CLIENT_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testConfiguredAliasIgnoresAliasNotMatchingDuplicateFormat() throws Exception { + X509Certificate presented = present(HOST, + key(HOST, CLIENT_AUTH), + key(HOST + "(1)", SERVER_AUTH)); + assertEku(presented, CLIENT_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testConfiguredAliasWithMalformedExtendedKeyUsageIsUsed() throws Exception { + KeyStore keystore = orderedKeystore("malformed", key("malformed", CLIENT_AUTH), key("server", SERVER_AUTH)); + Assert.assertEquals(keystore.getCertificateChain("malformed")[0], present("malformed", keystore)); + } + + + @Test + public void testConfiguredAliasUsesNumberedDuplicateWhenBaseIsMalformed() throws Exception { + KeyStore keystore = + orderedKeystore("malformed", key("malformed", CLIENT_AUTH), key("malformed (1)", SERVER_AUTH)); + assertEku(present("malformed", keystore), Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + @Test + public void testConfiguredMissingAliasFails() { + Assert.assertThrows(IOException.class, () -> present("missing", key("server", SERVER_AUTH))); + } + + + @Test + public void testConfiguredTrustedAliasFails() { + Assert.assertThrows(IOException.class, () -> present("trusted", KeystoreEntry.trusted("trusted", HOST, SERVER_AUTH), + key("server", SERVER_AUTH))); + } + + + /* + * In a SunMSCAPI Windows-MY store a certificate without a private key can take the bare friendly name while the + * key-bearing certificate of the same name is exposed as a numbered duplicate. The configured alias then names a + * non-key (trusted) entry, so the server-auth numbered duplicate must still be selected rather than failing + * start-up. + */ + @Test + public void testConfiguredTrustedBaseAliasUsesServerAuthDuplicate() throws Exception { + X509Certificate presented = present(HOST, + KeystoreEntry.trusted(HOST, HOST, SERVER_AUTH), + key(HOST + " (1)", SERVER_AUTH)); + assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); + } + + + /* + * The numbered duplicates of a Windows-MY friendly name are normally contiguous, but should a gap appear (for + * example a lower-numbered duplicate was removed) the search must keep scanning past it rather than stop, so a valid + * server-auth duplicate beyond the gap is still found. + */ + @Test + public void testConfiguredAliasSkipsGapInNumberedDuplicates() throws Exception { + X509Certificate presented = present("tomcat", + key("tomcat", CLIENT_AUTH), + key("tomcat (2)", SERVER_AUTH)); + assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); + } +} diff --git a/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java b/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java deleted file mode 100644 index 84c500a7ca..0000000000 --- a/test/org/apache/tomcat/util/net/TestSSLUtilBaseKeyUsageSelection.java +++ /dev/null @@ -1,343 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.tomcat.util.net; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.security.KeyStore; -import java.security.KeyStoreSpi; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import javax.net.ssl.X509KeyManager; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; - -import org.apache.catalina.startup.TomcatBaseTest; -import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; -import org.apache.tomcat.util.net.TesterKeystoreGenerator.CertificateExtensionsCustomizer; -import org.apache.tomcat.util.net.TesterKeystoreGenerator.KeystoreEntry; -import org.bouncycastle.asn1.x509.ExtendedKeyUsage; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.KeyPurposeId; -import org.easymock.EasyMock; - -/** - * Tests that, when no alias is configured, a key entry whose certificate is valid for TLS server authentication is - * auto-selected and entries that are not valid (by Extended Key Usage) are ignored. A configured alias is always used - * unchanged, regardless of its Extended Key Usage. - */ -@RunWith(Parameterized.class) -public class TestSSLUtilBaseKeyUsageSelection extends TomcatBaseTest { - - @Parameterized.Parameters(name = "{0}") - public static Collection<Object[]> parameters() { - return Arrays.asList(new Object[] { "JSSE", Boolean.FALSE, - "org.apache.tomcat.util.net.jsse.JSSEImplementation" }, - new Object[] { "OpenSSL", Boolean.TRUE, - "org.apache.tomcat.util.net.openssl.OpenSSLImplementation" }, - new Object[] { "OpenSSL-FFM", Boolean.TRUE, - "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation" }); - } - - @Parameter(0) - public String connectorName; - - @Parameter(1) - public boolean useOpenSSL; - - @Parameter(2) - public String sslImplementationName; - - - @Before - public void configureSSLImplementation() throws Exception { - TesterSupport.configureSSLImplementation(getTomcatInstance(), sslImplementationName, useOpenSSL); - getTomcatInstance().getServer().init(); - } - - private static final String HOST = "localhost"; - - private static final String CLIENT_AUTH_EXTENDED_KEY_USAGE_OID = "1.3.6.1.5.5.7.3.2"; - - private static final CertificateExtensionsCustomizer SERVER_AUTH = (keyPair, builder) -> builder - .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)); - private static final CertificateExtensionsCustomizer CLIENT_AUTH = (keyPair, builder) -> builder - .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth)); - private static final CertificateExtensionsCustomizer ANY_AUTH = (keyPair, builder) -> builder - .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage)); - - - private static KeystoreEntry key(String alias, CertificateExtensionsCustomizer eku) { - return KeystoreEntry.key(alias, HOST, eku); - } - - - private X509Certificate present(String alias, KeystoreEntry... entries) throws Exception { - return present(alias, orderedKeystore(null, entries)); - } - - - private X509Certificate present(String alias, KeyStore keystore) throws Exception { - X509KeyManager keyManager = (X509KeyManager) sslUtil(alias, keystore).getKeyManagers()[0]; - X509Certificate[] chain = keyManager.getCertificateChain(keyManager.chooseServerAlias("RSA", null, null)); - Assert.assertNotNull("Key manager returned no certificate chain", chain); - return chain[0]; - } - - - private SSLUtil sslUtil(String alias, KeyStore keystore) throws Exception { - - SSLHostConfig hostConfig = new SSLHostConfig(); - SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); - certificate.setCertificateKeystore(keystore); - certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS); - if (alias != null) { - certificate.setCertificateKeyAlias(alias); - } - - return sslUtil(certificate); - } - - - private SSLUtil sslUtil(SSLHostConfigCertificate certificate) throws Exception { - SSLUtil util = SSLImplementation.getInstance(sslImplementationName).getSSLUtil(certificate); - SSLHostConfig hostConfig = certificate.getSSLHostConfig(); - hostConfig.setEnabledProtocols(util.getEnabledProtocols()); - hostConfig.setEnabledCiphers(util.getEnabledCiphers()); - return util; - } - - - private static KeyStore orderedKeystore(String malformedAlias, KeystoreEntry... entries) throws Exception { - File file = TesterKeystoreGenerator.generateKeystore(Arrays.asList(entries)); - KeyStore delegate = KeyStore.getInstance("JKS"); - try (InputStream is = new FileInputStream(file)) { - delegate.load(is, TesterSupport.JKS_PASS.toCharArray()); - } - List<String> aliases = Stream.of(entries).map(KeystoreEntry::alias).collect(Collectors.toList()); - - // JKS type of keystore enumeration order is unspecified. - // Control that boundary while keeping real keys, certificate chains, and key managers. - // Malformed EKU must be injected here because a JKS parser can reject it while loading. - X509Certificate malformed = EasyMock.createMock(X509Certificate.class); - EasyMock.expect(malformed.getExtendedKeyUsage()).andStubThrow(new CertificateParsingException("Invalid EKU")); - EasyMock.replay(malformed); - KeyStoreSpi spi = EasyMock.createMock(KeyStoreSpi.class); - spi.engineLoad(null, null); - EasyMock.expectLastCall().once(); - EasyMock.expect(spi.engineAliases()).andStubAnswer(() -> Collections.enumeration(aliases)); - EasyMock.expect(spi.engineIsKeyEntry(EasyMock.anyString())) - .andStubAnswer(() -> delegate.isKeyEntry(EasyMock.getCurrentArgument(0))); - EasyMock.expect(spi.engineGetCertificate(EasyMock.anyString())).andStubAnswer(() -> { - String alias = EasyMock.getCurrentArgument(0); - return alias.equals(malformedAlias) ? malformed : delegate.getCertificate(alias); - }); - EasyMock.expect(spi.engineGetCertificateChain(EasyMock.anyString())) - .andStubAnswer(() -> delegate.getCertificateChain(EasyMock.getCurrentArgument(0))); - EasyMock.expect(spi.engineGetKey(EasyMock.anyString(), EasyMock.anyObject(char[].class))) - .andStubAnswer(() -> delegate.getKey(EasyMock.getCurrentArgument(0), EasyMock.getCurrentArgument(1))); - EasyMock.replay(spi); - KeyStore result = new KeyStore(spi, delegate.getProvider(), "JKS") { }; - result.load(null, null); - return result; - } - - - private static void assertEku(X509Certificate certificate, String oid) throws Exception { - List<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)); - } - - - /* - * A configured alias must be honored exactly, even when its certificate is not valid for server authentication and - * a server-auth certificate sharing the Subject is also present. Auto-selection must never override the user's - * choice (see the uses of SSLHostConfigCertificate.getCertificateKeyAlias() across the connectors). - */ - @Test - public void testConfiguredAliasIsUsedUnchangedWhenNotServerAuth() throws Exception { - X509Certificate presented = present(HOST, - key(HOST, CLIENT_AUTH), - key("localhost-server", SERVER_AUTH)); - assertEku(presented, CLIENT_AUTH_EXTENDED_KEY_USAGE_OID); - } - - - @Test - public void testConfiguredAliasIsUsedWhenServerAuth() throws Exception { - X509Certificate presented = present(HOST, - key(HOST, SERVER_AUTH), - key("localhost-client", CLIENT_AUTH)); - assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); - } - - - @Test - public void testAutoSelectPrefersServerAuthOverClientAuth() throws Exception { - X509Certificate presented = present(null, - key("first", CLIENT_AUTH), - key("second", SERVER_AUTH)); - assertEku(presented, Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); - } - - - @Test - public void testAutoSelectAcceptsAnyExtendedKeyUsage() throws Exception { - X509Certificate presented = present(null, - key("first", CLIENT_AUTH), - key("any", ANY_AUTH)); - assertEku(presented, Constants.ANY_EXTENDED_KEY_USAGE_OID); - } - - - @Test - public void testAutoSelectAcceptsCertificateWithoutExtendedKeyUsage() throws Exception { - X509Certificate presented = present(null, - key("first", CLIENT_AUTH), - key("none", null)); - Assert.assertNull("A certificate with no Extended Key Usage is unrestricted and must be selectable", - presented.getExtendedKeyUsage()); - } - - - /* - * Trusted (keyless) certificate entries are not key entries, so they can never be selected as the server key even - * when they are the only server-auth-capable certificate. - */ - @Test - public void testAutoSelectSkipsTrustedEntries() { - Assert.assertThrows(IOException.class, () -> present(null, - KeystoreEntry.trusted("ca", HOST, SERVER_AUTH), - key("key", CLIENT_AUTH))); - } - - - /* - * No unsuitable key entry may be selected when all certificates exclude server authentication. - */ - @Test - public void testAutoSelectFailsWhenNoneServerAuth() { - Assert.assertThrows(IOException.class, () -> present(null, - key("first", CLIENT_AUTH), - key("second", CLIENT_AUTH))); - } - - - @Test - public void testAutoSelectSkipsMalformedExtendedKeyUsage() throws Exception { - KeyStore keystore = orderedKeystore("malformed", key("malformed", CLIENT_AUTH), key("server", SERVER_AUTH)); - assertEku(present(null, keystore), Constants.SERVER_AUTH_EXTENDED_KEY_USAGE_OID); - } - - - @Test - public void testConfiguredAliasWithMalformedExtendedKeyUsageIsUsed() throws Exception { - KeyStore keystore = orderedKeystore("malformed", key("malformed", CLIENT_AUTH), key("server", SERVER_AUTH)); - Assert.assertEquals(keystore.getCertificateChain("malformed")[0], present("malformed", keystore)); - } - - - @Test - public void testAutoSelectFailsWhenOnlyMalformedExtendedKeyUsage() throws Exception { - KeyStore keystore = orderedKeystore("malformed", key("malformed", CLIENT_AUTH)); - Assert.assertThrows(IOException.class, () -> present(null, keystore)); - } - - - @Test - public void testAutoSelectUsesFirstEligibleKey() throws Exception { - KeyStore keystore = orderedKeystore(null, key("client", CLIENT_AUTH), key("first", ANY_AUTH), - key("second", SERVER_AUTH)); - Assert.assertEquals(keystore.getCertificate("first"), present(null, keystore)); - } - - - @Test - public void testConfiguredMissingAliasFails() { - Assert.assertThrows(IOException.class, () -> present("missing", key("server", SERVER_AUTH))); - } - - - @Test - public void testConfiguredTrustedAliasFails() { - Assert.assertThrows(IOException.class, () -> present("trusted", KeystoreEntry.trusted("trusted", HOST, SERVER_AUTH), - key("server", SERVER_AUTH))); - } - - - @Test - public void testAutoSelectDoesNotSkipKeyReadFailure() throws Exception { - KeyStore keystore = orderedKeystore(null, key("first", SERVER_AUTH), key("second", SERVER_AUTH)); - SSLHostConfig hostConfig = new SSLHostConfig(); - SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); - certificate.setCertificateKeystore(keystore); - certificate.setCertificateKeyPassword("incorrect"); - SSLUtil util = SSLImplementation.getInstance(sslImplementationName).getSSLUtil(certificate); - Assert.assertThrows(UnrecoverableKeyException.class, util::getKeyManagers); - } - - - @Test - public void testSSLContextUsesSelectedCertificate() throws Exception { - KeyStore keystore = orderedKeystore(null, key("client", CLIENT_AUTH), key("server", SERVER_AUTH)); - SSLContext context = sslUtil(null, keystore).createSSLContext(Collections.emptyList()); - try { - Assert.assertEquals(keystore.getCertificate("server"), context.getCertificateChain("server")[0]); - } finally { - context.destroy(); - } - } - - - @Test - public void testSSLContextLoadsSelectedCertificateFromKeystoreFile() throws Exception { - File file = TesterKeystoreGenerator.generateKeystore( - Arrays.asList(key(SSLUtilBase.DEFAULT_KEY_ALIAS, CLIENT_AUTH), key("server", SERVER_AUTH))); - KeyStore keystore = KeyStore.getInstance("JKS"); - try (InputStream is = new FileInputStream(file)) { - keystore.load(is, TesterSupport.JKS_PASS.toCharArray()); - } - SSLHostConfig hostConfig = new SSLHostConfig(); - SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(hostConfig, Type.UNDEFINED); - certificate.setCertificateKeystoreFile(file.getAbsolutePath()); - certificate.setCertificateKeystorePassword(TesterSupport.JKS_PASS); - SSLContext context = sslUtil(certificate).createSSLContext(Collections.emptyList()); - try { - Assert.assertEquals(keystore.getCertificate("server"), context.getCertificateChain("server")[0]); - } finally { - context.destroy(); - } - } -} diff --git a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java index 0bc8e6a01e..444a850df4 100644 --- a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java +++ b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java @@ -26,6 +26,7 @@ import java.security.KeyStore; import java.security.cert.X509Certificate; import java.util.Date; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.Extension; @@ -39,6 +40,8 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; public final class TesterKeystoreGenerator { + private static final AtomicLong SERIAL_COUNTER = new AtomicLong(1); + private TesterKeystoreGenerator() {} @FunctionalInterface @@ -130,7 +133,7 @@ public final class TesterKeystoreGenerator { KeyPair keyPair = kpg.generateKeyPair(); X500Name subject = new X500Name("CN=" + cn); - BigInteger serial = BigInteger.valueOf(System.nanoTime()); + BigInteger serial = BigInteger.valueOf(SERIAL_COUNTER.getAndIncrement()); long oneDay = 86400000L; Date notBefore = new Date(System.currentTimeMillis() - oneDay); Date notAfter = new Date(System.currentTimeMillis() + 365L * oneDay); diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 9e068da9ba..b7e1d2f7b1 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -518,8 +518,14 @@ <code>certificateKeyAlias</code> is set, ignore key entries whose certificate has a malformed Extended Key Usage extension or one that does not permit TLS server authentication when automatically selecting - which key in the key store to use. - A configured <code>certificateKeyAlias</code> is always used unchanged. + which key in the key store to use. A configured + <code>certificateKeyAlias</code> is used even if its certificate is not + valid for TLS server authentication, except in the Windows-MY key store + accessed through the SunMSCAPI provider: there, if the configured + certificate is not valid but the store holds a valid certificate under + that alias with a numeric suffix appended - the format that provider uses + to give unique aliases to several certificates that share a name - the + suffixed certificate is used instead. (dsoumis) </update> </changelog> diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml index b45d105606..4960a5551e 100644 --- a/webapps/docs/config/http.xml +++ b/webapps/docs/config/http.xml @@ -1919,8 +1919,21 @@ <code>serverAuth</code> nor <code>anyExtendedKeyUsage</code>, are ignored when selecting the key to use. Certificates with no Extended Key Usage extension remain eligible. If no eligible key entry is found, connector - initialization fails. A configured alias is used unchanged and is not - subject to this filtering.</p> + initialization fails.</p> + <p>A configured alias is used even if its certificate is not valid for TLS + server authentication. The exception is the Windows-MY store accessed + through the SunMSCAPI provider: if the certificate at the configured alias is + not valid there, but the store also contains a key entry whose alias is the + configured alias with <code>" (1)"</code>, <code>" (2)"</code>, etc. appended + and whose certificate is valid, that certificate is used instead. That store + exposes such duplicates because the key store API requires aliases to be + unique even when several certificates share a name. If none of those numbered + duplicates is valid, or the key store is of any other type, the configured + alias is used unchanged.</p> + <p>The SunMSCAPI provider does not expose a certificate's original Windows + friendly name through the key store API, so the match is a heuristic based + on the appended suffix alone (see + <a href="https://bugs.openjdk.org/browse/JDK-8395755">JDK-8395755</a>).</p> </attribute> <attribute name="certificateKeyFile" required="false"> diff --git a/webapps/docs/ssl-howto.xml b/webapps/docs/ssl-howto.xml index 36196bfa30..a47598653d 100644 --- a/webapps/docs/ssl-howto.xml +++ b/webapps/docs/ssl-howto.xml @@ -465,10 +465,26 @@ Extended Key Usage extension at all. Certificates whose Extended Key Usage excludes both purposes or cannot be parsed are ignored. If no eligible key entry is found, connector initialization fails.</p> -<p>A configured <strong>certificateKeyAlias</strong> is used unchanged, even if -its certificate is unsuitable for server authentication. When several certificates -are eligible, configure an alias to ensure that the intended certificate is -used.</p> +<p>A configured <strong>certificateKeyAlias</strong> is normally used even if its +certificate is unsuitable for server authentication. The one exception is the +Windows-MY store accessed through the SunMSCAPI provider: if the certificate at the +configured alias is unsuitable there, but the store also holds a suitable +certificate under that same name with <code>" (1)"</code>, <code>" (2)"</code>, +etc. appended - the way that store gives unique aliases to several certificates +that share a name, since the key store API requires aliases to be unique - the +suffixed certificate is used instead. This lets one configured alias keep working +on a store that holds more than one certificate under that name. Every +other key store type uses the configured alias as given. When several +differently-named certificates are eligible, configure an alias to ensure that the +intended certificate is used.</p> + +<p>Selection is made solely on the appended <code>" (N)"</code> suffix. The +SunMSCAPI provider does not expose a certificate's original Windows friendly name +through the key store API (see +<a href="https://bugs.openjdk.org/browse/JDK-8395755">JDK-8395755</a>), so a suffix +that is genuinely part of a certificate's own name cannot be told apart from one +added to disambiguate a shared name. The result is therefore a best-effort +heuristic that selects the first numbered alias whose certificate is suitable.</p> </section> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
