This is an automated email from the ASF dual-hosted git repository. dsoumis pushed a commit to branch 11.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 1b5775aef1cfb747273b4fc6e9d8016e2041901c Author: Dimitris Soumis <[email protected]> AuthorDate: Thu Oct 29 17:28:04 2026 +0200 Share served-alias resolution across all SSLContext engines --- java/org/apache/tomcat/util/net/SSLContext.java | 33 ++++++++-- .../apache/tomcat/util/net/SSLContextWrapper.java | 6 ++ .../tomcat/util/net/jsse/JSSESSLContext.java | 21 +++--- .../tomcat/util/net/openssl/OpenSSLContext.java | 29 +++----- .../util/net/openssl/panama/OpenSSLContext.java | 30 +++------ .../SSLUtilBaseCertificateSelectionTestBase.java | 77 +++++++++++++++++++--- .../TestSSLUtilBaseConfiguredAliasSelection.java | 17 ++++- 7 files changed, 147 insertions(+), 66 deletions(-) diff --git a/java/org/apache/tomcat/util/net/SSLContext.java b/java/org/apache/tomcat/util/net/SSLContext.java index 25adf54622..7c3c4134ef 100644 --- a/java/org/apache/tomcat/util/net/SSLContext.java +++ b/java/org/apache/tomcat/util/net/SSLContext.java @@ -26,6 +26,7 @@ import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSessionContext; import javax.net.ssl.TrustManager; +import javax.net.ssl.X509KeyManager; /** * This interface is needed to override the default SSLContext class to allow SSL implementation pluggability without @@ -102,20 +103,42 @@ public interface SSLContext { X509Certificate[] getCertificateChain(String alias); + /** + * Returns the key managers that may serve the given certificate configuration, in preference order. Used by + * {@link #getServedAlias} to resolve the alias actually served. The default returns an empty array (no key manager + * known); implementations backed by one or more key managers override it to expose them. + * + * @param certificate The certificate configuration + * + * @return The key managers that may serve the certificate, possibly empty, never {@code null} + */ + default X509KeyManager[] getServerKeyManagers(SSLHostConfigCertificate certificate) { + return new X509KeyManager[0]; + } + + /** * Returns the alias the key manager would select to serve for the given certificate configuration during a * handshake. This can differ from {@link SSLHostConfigCertificate#getCertificateKeyAlias()}, for example after - * SunMSCAPI Windows-MY numbered-duplicate resolution. The default returns the configured alias (or - * {@link SSLUtilBase#DEFAULT_KEY_ALIAS} when none is configured); implementations backed by a key manager override - * it to resolve the alias actually served. + * SunMSCAPI Windows-MY numbered-duplicate resolution. Each key manager from {@link #getServerKeyManagers} is asked + * in turn; if none selects an alias the configured alias is used (or {@link SSLUtilBase#DEFAULT_KEY_ALIAS} when + * none is configured). * * @param certificate The certificate configuration * * @return The served alias, never {@code null} */ default String getServedAlias(SSLHostConfigCertificate certificate) { - String alias = certificate.getCertificateKeyAlias(); - return alias == null ? SSLUtilBase.DEFAULT_KEY_ALIAS : alias; + for (X509KeyManager keyManager : getServerKeyManagers(certificate)) { + if (keyManager != null) { + String alias = certificate.chooseServerAlias(keyManager); + if (alias != null) { + return alias; + } + } + } + String configured = certificate.getCertificateKeyAlias(); + return configured == null ? SSLUtilBase.DEFAULT_KEY_ALIAS : configured; } diff --git a/java/org/apache/tomcat/util/net/SSLContextWrapper.java b/java/org/apache/tomcat/util/net/SSLContextWrapper.java index 280d4dbbce..b1b434d5f0 100644 --- a/java/org/apache/tomcat/util/net/SSLContextWrapper.java +++ b/java/org/apache/tomcat/util/net/SSLContextWrapper.java @@ -80,6 +80,12 @@ class SSLContextWrapper implements SSLContext { return keyManager.getCertificateChain(alias); } + + @Override + public X509KeyManager[] getServerKeyManagers(SSLHostConfigCertificate certificate) { + return new X509KeyManager[] { keyManager }; + } + @Override public X509Certificate[] getAcceptedIssuers() { return trustManager.getAcceptedIssuers(); diff --git a/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java b/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java index 9679da9a1a..29b07ed12b 100644 --- a/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java +++ b/java/org/apache/tomcat/util/net/jsse/JSSESSLContext.java @@ -20,8 +20,10 @@ import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; import javax.net.ssl.KeyManager; @@ -92,18 +94,17 @@ class JSSESSLContext implements SSLContext { @Override - public String getServedAlias(SSLHostConfigCertificate certificate) { - if (kms != null) { - for (KeyManager km : kms) { - if (km instanceof X509KeyManager x509KeyManager) { - String alias = certificate.chooseServerAlias(x509KeyManager); - if (alias != null) { - return alias; - } - } + public X509KeyManager[] getServerKeyManagers(SSLHostConfigCertificate certificate) { + if (kms == null) { + return new X509KeyManager[0]; + } + List<X509KeyManager> x509KeyManagers = new ArrayList<>(); + for (KeyManager km : kms) { + if (km instanceof X509KeyManager x509KeyManager) { + x509KeyManagers.add(x509KeyManager); } } - return SSLContext.super.getServedAlias(certificate); + return x509KeyManagers.toArray(new X509KeyManager[0]); } @Override diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java index 0eb47b834d..cc764adc09 100644 --- a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java @@ -58,7 +58,6 @@ import org.apache.tomcat.util.net.SSLHostConfig.CertificateVerification; import org.apache.tomcat.util.net.SSLHostConfigCertificate; import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; import org.apache.tomcat.util.net.SSLHostConfigPreSharedKey; -import org.apache.tomcat.util.net.SSLUtilBase; import org.apache.tomcat.util.res.StringManager; /** @@ -576,16 +575,9 @@ public class OpenSSLContext implements org.apache.tomcat.util.net.SSLContext { SSLContext.setCertificateChainFile(state.ctx, SSLHostConfig.adjustRelativePath(certificate.getCertificateChainFile()), false); } else { - String alias = certificate.getCertificateKeyAlias(); X509KeyManager x509KeyManager = certificate.getCertificateKeyManager(); - if (alias == null) { - alias = SSLUtilBase.DEFAULT_KEY_ALIAS; - } + String alias = getServedAlias(certificate); X509Certificate[] chain = x509KeyManager.getCertificateChain(alias); - if (chain == null) { - alias = certificate.chooseServerAlias(x509KeyManager); - chain = x509KeyManager.getCertificateChain(alias); - } PrivateKey key = x509KeyManager.getPrivateKey(alias); String encodedKey = BEGIN_KEY + Base64.getMimeEncoder(64, new byte[] { '\n' }).encodeToString(key.getEncoded()) + END_KEY; @@ -667,20 +659,17 @@ public class OpenSSLContext implements org.apache.tomcat.util.net.SSLContext { @Override public X509Certificate[] getCertificateChain(String alias) { - X509Certificate[] chain = null; X509KeyManager x509KeyManager = certificate.getCertificateKeyManager(); - if (x509KeyManager != null) { - if (alias == null) { - alias = SSLUtilBase.DEFAULT_KEY_ALIAS; - } - chain = x509KeyManager.getCertificateChain(alias); - if (chain == null) { - alias = certificate.chooseServerAlias(x509KeyManager); - chain = x509KeyManager.getCertificateChain(alias); - } + if (x509KeyManager == null) { + return null; } + return x509KeyManager.getCertificateChain(alias); + } - return chain; + @Override + public X509KeyManager[] getServerKeyManagers(SSLHostConfigCertificate certificate) { + X509KeyManager x509KeyManager = certificate.getCertificateKeyManager(); + return x509KeyManager == null ? new X509KeyManager[0] : new X509KeyManager[] { x509KeyManager }; } @Override diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java index a9e3d9d624..b6b3ed6c49 100644 --- a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java +++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java @@ -63,7 +63,6 @@ import org.apache.tomcat.util.net.SSLHostConfig.CertificateVerification; import org.apache.tomcat.util.net.SSLHostConfigCertificate; import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; import org.apache.tomcat.util.net.SSLHostConfigPreSharedKey; -import org.apache.tomcat.util.net.SSLUtilBase; import org.apache.tomcat.util.net.openssl.OpenSSLConf; import org.apache.tomcat.util.net.openssl.OpenSSLConfCmd; import org.apache.tomcat.util.net.openssl.OpenSSLPreSharedKeySelector; @@ -1445,16 +1444,9 @@ public class OpenSSLContext implements org.apache.tomcat.util.net.SSLContext { BIO_free(certificateBIO); } } else { - String alias = certificate.getCertificateKeyAlias(); X509KeyManager x509KeyManager = certificate.getCertificateKeyManager(); - if (alias == null) { - alias = SSLUtilBase.DEFAULT_KEY_ALIAS; - } + String alias = getServedAlias(certificate); X509Certificate[] chain = x509KeyManager.getCertificateChain(alias); - if (chain == null) { - alias = certificate.chooseServerAlias(x509KeyManager); - chain = x509KeyManager.getCertificateChain(alias); - } String encodedKey = BEGIN_KEY + Base64.getMimeEncoder(64, new byte[] { '\n' }) .encodeToString(x509KeyManager.getPrivateKey(alias).getEncoded()) + END_KEY; var rawCertificate = localArena.allocateFrom(ValueLayout.JAVA_BYTE, chain[0].getEncoded()); @@ -1628,20 +1620,18 @@ public class OpenSSLContext implements org.apache.tomcat.util.net.SSLContext { @Override public X509Certificate[] getCertificateChain(String alias) { - X509Certificate[] chain = null; X509KeyManager x509KeyManager = certificate.getCertificateKeyManager(); - if (x509KeyManager != null) { - if (alias == null) { - alias = SSLUtilBase.DEFAULT_KEY_ALIAS; - } - chain = x509KeyManager.getCertificateChain(alias); - if (chain == null) { - alias = certificate.chooseServerAlias(x509KeyManager); - chain = x509KeyManager.getCertificateChain(alias); - } + if (x509KeyManager == null) { + return null; } + return x509KeyManager.getCertificateChain(alias); + } + - return chain; + @Override + public X509KeyManager[] getServerKeyManagers(SSLHostConfigCertificate certificate) { + X509KeyManager x509KeyManager = certificate.getCertificateKeyManager(); + return x509KeyManager == null ? new X509KeyManager[0] : new X509KeyManager[] { x509KeyManager }; } @Override diff --git a/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java index c549e4603c..e658cc5328 100644 --- a/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java +++ b/test/org/apache/tomcat/util/net/SSLUtilBaseCertificateSelectionTestBase.java @@ -194,7 +194,7 @@ public abstract class SSLUtilBaseCertificateSelectionTestBase extends TomcatBase protected static KeyStore orderedKeystore(String malformedAlias, KeystoreEntry... entries) throws Exception { - return orderedKeystore(sunMSCAPIProvider(), "Windows-MY", true, malformedAlias, entries); + return orderedKeystore(sunMSCAPIProvider(), "Windows-MY", KeyMode.EXPORTABLE, malformedAlias, entries); } @@ -205,7 +205,18 @@ public abstract class SSLUtilBaseCertificateSelectionTestBase extends TomcatBase * mock otherwise takes. */ protected static KeyStore nonExportableKeystore(KeystoreEntry... entries) throws Exception { - return orderedKeystore(sunMSCAPIProvider(), "Windows-MY", false, null, entries); + return orderedKeystore(sunMSCAPIProvider(), "Windows-MY", KeyMode.NON_EXPORTABLE, null, entries); + } + + + /* + * As nonExportableKeystore(KeystoreEntry...) - the keys report a null format so the original key store is kept and + * each key manager is wrapped - but getEncoded() still returns the real key material, so OpenSSL can load it. This + * reaches the wrapped path (ksUsed == ks) for the OpenSSL implementations, where a lookup of the configured alias + * would otherwise resolve the wrong certificate. + */ + protected static KeyStore wrappedExportableKeystore(KeystoreEntry... entries) throws Exception { + return orderedKeystore(sunMSCAPIProvider(), "Windows-MY", KeyMode.WRAPPED_EXPORTABLE, null, entries); } @@ -213,7 +224,7 @@ public abstract class SSLUtilBaseCertificateSelectionTestBase extends TomcatBase return orderedKeystore(new Provider("SUN", "1.0", "test") { @Serial private static final long serialVersionUID = 4434321038151233913L; - }, "JKS", true, null, entries); + }, "JKS", KeyMode.EXPORTABLE, null, entries); } @@ -225,7 +236,20 @@ public abstract class SSLUtilBaseCertificateSelectionTestBase extends TomcatBase } - protected static KeyStore orderedKeystore(Provider provider, String type, boolean exportableKeys, + /* + * How the mocked SunMSCAPI private keys present themselves, which selects the branch of + * SSLUtilBase.getKeyManagers() under test. EXPORTABLE keys report PKCS#8 (in-memory copy); NON_EXPORTABLE and + * WRAPPED_EXPORTABLE report a null format (original key store kept, key managers wrapped). WRAPPED_EXPORTABLE + * additionally exposes the key encoding so the OpenSSL implementations can load it. + */ + protected enum KeyMode { + EXPORTABLE, + NON_EXPORTABLE, + WRAPPED_EXPORTABLE + } + + + protected static KeyStore orderedKeystore(Provider provider, String type, KeyMode keyMode, String malformedAlias, KeystoreEntry... entries) throws Exception { File file = TesterKeystoreGenerator.generateKeystore(List.of(entries)); KeyStore delegate = KeyStore.getInstance(file, TesterSupport.JKS_PASS.toCharArray()); @@ -251,13 +275,18 @@ public abstract class SSLUtilBaseCertificateSelectionTestBase extends TomcatBase .andStubAnswer(() -> delegate.getCertificateChain(EasyMock.getCurrentArgument(0))); EasyMock.expect(spi.engineGetKey(EasyMock.anyString(), EasyMock.anyObject(char[].class))).andStubAnswer(() -> { Key key = delegate.getKey(EasyMock.getCurrentArgument(0), EasyMock.getCurrentArgument(1)); - return exportableKeys ? key : nonExportableView(key); + return switch (keyMode) { + case EXPORTABLE -> key; + case NON_EXPORTABLE -> nonExportableView(key); + case WRAPPED_EXPORTABLE -> wrappedExportableView(key); + }; }); EasyMock.replay(spi); - // A real JKS backs the mock. With exportableKeys the keys report PKCS#8, so getKeyManagers() takes its - // in-memory-copy branch; with exportableKeys == false they report a null format (see nonExportableView), so it - // takes SunMSCAPI's non-exportable-key path instead. The provider name and store type are overridden only so - // isSunMSCAPIWindowsMY() can be exercised off Windows. + // A real JKS backs the mock. EXPORTABLE keys report PKCS#8, so getKeyManagers() takes its in-memory-copy + // branch; NON_EXPORTABLE and WRAPPED_EXPORTABLE keys report a null format (see nonExportableView / + // wrappedExportableView), so it keeps the original key store and wraps each key manager - WRAPPED_EXPORTABLE + // still exposes the key encoding so OpenSSL can load it. The provider name and store type are overridden only + // so isSunMSCAPIWindowsMY() can be exercised off Windows. KeyStore result = new KeyStore(spi, provider, type) { }; result.load(null, null); return result; @@ -295,6 +324,36 @@ public abstract class SSLUtilBaseCertificateSelectionTestBase extends TomcatBase } + /* + * As nonExportableView(Key) but getEncoded() returns the real key material, modeling a SunMSCAPI key that keeps + * the original key store in use (null format) yet can still be exported into OpenSSL. + */ + private static Key wrappedExportableView(Key key) { + if (!(key instanceof PrivateKey privateKey)) { + return key; + } + return new PrivateKey() { + @Serial + private static final long serialVersionUID = 1L; + + @Override + public String getAlgorithm() { + return privateKey.getAlgorithm(); + } + + @Override + public String getFormat() { + return null; + } + + @Override + public byte[] getEncoded() { + return privateKey.getEncoded(); + } + }; + } + + protected static void assertEku(X509Certificate certificate, String oid) throws Exception { List<String> extendedKeyUsage = certificate.getExtendedKeyUsage(); Assert.assertNotNull("Presented certificate has no Extended Key Usage", extendedKeyUsage); diff --git a/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java index f9f2a4c0e6..0a8857ad90 100644 --- a/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java +++ b/test/org/apache/tomcat/util/net/TestSSLUtilBaseConfiguredAliasSelection.java @@ -232,8 +232,7 @@ public class TestSSLUtilBaseConfiguredAliasSelection extends SSLUtilBaseCertific /* * After the SunMSCAPI numbered-duplicate swap the served alias is the numbered duplicate, not the configured alias. * This is the alias that logCertificate reports and the reload check resolves. Non-exportable keys route - * getKeyManagers() through the production ksUsed == ks path. Scoped to JSSE: for OpenSSL the served alias is - * resolved from the native layer by a separate path. + * getKeyManagers() through the production ksUsed == ks path. */ @Test public void testServedAliasIsTheNumberedDuplicate() throws Exception { @@ -241,4 +240,18 @@ public class TestSSLUtilBaseConfiguredAliasSelection extends SSLUtilBaseCertific KeyStore keystore = nonExportableKeystore(key(HOST, CLIENT_AUTH), key(HOST + " (1)", SERVER_AUTH)); Assert.assertEquals(HOST + " (1)", servedAlias(HOST, keystore)); } + + + /* + * A SunMSCAPI key that reports a null format but is still exportable keeps the original key store in use + * (ksUsed == ks) and wraps each key manager, like a real non-exportable key, while remaining loadable by OpenSSL. + * In that wrapped path a lookup of the configured alias still resolves its own (clientAuth) chain, so the served + * alias - not that lookup - must drive what OpenSSL installs. + */ + @Test + public void testServedAliasResolvesForOpenSSLWithOriginalKeystore() throws Exception { + Assume.assumeTrue(useOpenSSL); + KeyStore keystore = wrappedExportableKeystore(key(HOST, CLIENT_AUTH), key(HOST + " (1)", SERVER_AUTH)); + Assert.assertEquals(HOST + " (1)", servedAlias(HOST, keystore)); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
