This is an automated email from the ASF dual-hosted git repository.
anmolnar pushed a commit to branch branch-3.9
in repository https://gitbox.apache.org/repos/asf/zookeeper.git
The following commit(s) were added to refs/heads/branch-3.9 by this push:
new 2ab0215e7 ZOOKEEPER-4828: Honor ssl.context.supplier.class for
client-server TLS
2ab0215e7 is described below
commit 2ab0215e7c705695b23f8e71a7488bfea3dd5516
Author: Fredrik Svensson <[email protected]>
AuthorDate: Wed Aug 12 22:29:18 2026 +0200
ZOOKEEPER-4828: Honor ssl.context.supplier.class for client-server TLS
Author: SvenssonWeb
Closes #2434 from SvenssonWeb/ZOOKEEPER-4828-branch-3.9
---
.../apache/zookeeper/ClientCnxnSocketNetty.java | 3 +-
.../apache/zookeeper/common/ClientX509Util.java | 64 +++++++++++++++++++++-
.../java/org/apache/zookeeper/common/X509Util.java | 59 +++++++++++++-------
.../org/apache/zookeeper/common/X509UtilTest.java | 61 +++++++++++++++++++++
4 files changed, 164 insertions(+), 23 deletions(-)
diff --git
a/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxnSocketNetty.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxnSocketNetty.java
index 32379e74c..d654ec563 100755
---
a/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxnSocketNetty.java
+++
b/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxnSocketNetty.java
@@ -443,7 +443,8 @@ protected void initChannel(SocketChannel ch) throws
Exception {
// The synchronized is to prevent the race on shared variable
"sslContext".
// Basically we only need to create it once.
private synchronized void initSSL(ChannelPipeline pipeline)
- throws X509Exception.KeyManagerException,
X509Exception.TrustManagerException, SSLException {
+ throws X509Exception.SSLContextException,
X509Exception.KeyManagerException,
+ X509Exception.TrustManagerException, SSLException {
if (sslContext == null) {
try (ClientX509Util x509Util = new ClientX509Util()) {
sslContext =
x509Util.createNettySslContextForClient(clientConfig);
diff --git
a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientX509Util.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientX509Util.java
index 6994334b4..760a5a01f 100644
---
a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientX509Util.java
+++
b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientX509Util.java
@@ -19,12 +19,15 @@
package org.apache.zookeeper.common;
import io.netty.handler.ssl.DelegatingSslContext;
+import io.netty.handler.ssl.IdentityCipherSuiteFilter;
+import io.netty.handler.ssl.JdkSslContext;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import java.util.Arrays;
import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLParameters;
@@ -66,7 +69,13 @@ public String getSslProviderProperty() {
}
public SslContext createNettySslContextForClient(ZKConfig config)
- throws X509Exception.KeyManagerException,
X509Exception.TrustManagerException, SSLException {
+ throws X509Exception.SSLContextException,
X509Exception.KeyManagerException,
+ X509Exception.TrustManagerException, SSLException {
+ SSLContext suppliedSSLContext = loadSuppliedSSLContext(config);
+ if (suppliedSSLContext != null) {
+ return createNettyJdkSslContext(config, suppliedSSLContext, true);
+ }
+
String keyStoreLocation =
config.getProperty(getSslKeystoreLocationProperty(), "");
String keyStorePassword = getPasswordFromConfigPropertyOrFile(config,
getSslKeystorePasswdProperty(),
getSslKeystorePasswdPathProperty());
@@ -107,6 +116,11 @@ public SslContext createNettySslContextForClient(ZKConfig
config)
public SslContext createNettySslContextForServer(ZKConfig config)
throws X509Exception.SSLContextException,
X509Exception.KeyManagerException, X509Exception.TrustManagerException,
SSLException {
+ SSLContext suppliedSSLContext = loadSuppliedSSLContext(config);
+ if (suppliedSSLContext != null) {
+ return createNettyJdkSslContext(config, suppliedSSLContext, false);
+ }
+
String keyStoreLocation =
config.getProperty(getSslKeystoreLocationProperty(), "");
String keyStorePassword = getPasswordFromConfigPropertyOrFile(config,
getSslKeystorePasswdProperty(),
getSslKeystorePasswdPathProperty());
@@ -150,6 +164,54 @@ public SslContext createNettySslContextForServer(ZKConfig
config, KeyManager key
}
}
+ /**
+ * Wraps a user supplied {@link SSLContext} in a Netty {@link SslContext},
applying the configured
+ * protocols, cipher suites, client auth mode and hostname verification on
top of it.
+ *
+ * <p>A supplied SSLContext carries its own key and trust managers, so it
can only be used with the
+ * JDK SSL provider: the OpenSSL providers build their own native context
and cannot delegate to it.
+ *
+ * <p>Unlike the file based path, hostname verification is applied
whenever it is enabled. The file
+ * based path relies on {@link ZKTrustManager} to verify hostnames and
only falls back to endpoint
+ * identification when no trust manager is available, which is never the
case for a supplied context.
+ *
+ * @param config the configuration to read the SSL options from.
+ * @param sslContext the user supplied SSLContext.
+ * @param isClient {@code true} to create a client side context, {@code
false} for server side.
+ * @return the Netty SslContext.
+ * @throws X509Exception.SSLContextException if a non JDK SSL provider is
configured.
+ */
+ private SslContext createNettyJdkSslContext(ZKConfig config, SSLContext
sslContext, boolean isClient)
+ throws X509Exception.SSLContextException {
+ SslProvider sslProvider = getSslProvider(config);
+ if (sslProvider != SslProvider.JDK) {
+ throw new X509Exception.SSLContextException("An SSLContext
supplied through "
+ +
getSslContextSupplierClassProperty()
+ + " can only be used
with the JDK SSL provider, but "
+ +
getSslProviderProperty()
+ + " is set to "
+ + sslProvider);
+ }
+
+ SslContext nettySslContext = new JdkSslContext(
+ sslContext,
+ isClient,
+ getCipherSuites(config),
+ IdentityCipherSuiteFilter.INSTANCE,
+ null,
+ isClient ? X509Util.ClientAuth.NONE.toNettyClientAuth() :
getClientAuth(config).toNettyClientAuth(),
+ getEnabledProtocols(config),
+ false);
+
+ boolean hostnameVerificationEnabled = isClient
+ ? isServerHostnameVerificationEnabled(config)
+ : isClientHostnameVerificationEnabled(config);
+ if (hostnameVerificationEnabled) {
+ return addHostnameVerification(nettySslContext, isClient ?
"Server" : "Client");
+ }
+ return nettySslContext;
+ }
+
private SslContextBuilder handleTcnativeOcspStapling(SslContextBuilder
builder, ZKConfig config) {
SslProvider sslProvider = getSslProvider(config);
boolean tcnative = sslProvider == SslProvider.OPENSSL || sslProvider
== SslProvider.OPENSSL_REFCNT;
diff --git
a/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
index a89d57821..836195051 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
@@ -364,30 +364,47 @@ public int getSslHandshakeTimeoutMillis() {
}
}
- @SuppressWarnings("unchecked")
public SSLContextAndOptions createSSLContextAndOptions(ZKConfig config)
throws SSLContextException {
+ final SSLContext suppliedSSLContext = loadSuppliedSSLContext(config);
+ if (suppliedSSLContext != null) {
+ return new SSLContextAndOptions(this, config, suppliedSSLContext);
+ }
+ return createSSLContextAndOptionsFromConfig(config);
+ }
+
+ /**
+ * Loads an {@link SSLContext} from the {@link Supplier} implementation
named by the
+ * {@link #getSslContextSupplierClassProperty()} property. This allows a
user to take full control over
+ * the construction of the SSLContext, for example to use a hardware key
store or an SSLContext obtained
+ * from a container, rather than having ZooKeeper load key material from
files.
+ *
+ * @param config the configuration to read the supplier class name from.
+ * @return the supplied SSLContext, or {@code null} if the property is not
set.
+ * @throws SSLContextException if the supplier class cannot be loaded,
instantiated or invoked.
+ */
+ @SuppressWarnings("unchecked")
+ protected SSLContext loadSuppliedSSLContext(ZKConfig config) throws
SSLContextException {
final String supplierContextClassName =
config.getProperty(sslContextSupplierClassProperty);
- if (supplierContextClassName != null) {
- LOG.debug("Loading SSLContext supplier from property '{}'",
sslContextSupplierClassProperty);
+ if (supplierContextClassName == null) {
+ return null;
+ }
+ LOG.debug("Loading SSLContext supplier from property '{}'",
sslContextSupplierClassProperty);
- try {
- Class<?> sslContextClass =
Class.forName(supplierContextClassName);
- Supplier<SSLContext> sslContextSupplier =
(Supplier<SSLContext>) sslContextClass.getConstructor().newInstance();
- return new SSLContextAndOptions(this, config,
sslContextSupplier.get());
- } catch (ClassNotFoundException
- | ClassCastException
- | NoSuchMethodException
- | InvocationTargetException
- | InstantiationException
- | IllegalAccessException e) {
- throw new SSLContextException("Could not retrieve the
SSLContext from supplier source '"
- + supplierContextClassName
- + "' provided in the property '"
- + sslContextSupplierClassProperty
- + "'", e);
- }
- } else {
- return createSSLContextAndOptionsFromConfig(config);
+ try {
+ Class<?> sslContextClass = Class.forName(supplierContextClassName);
+ Supplier<SSLContext> sslContextSupplier = (Supplier<SSLContext>)
sslContextClass.getConstructor().newInstance();
+ return sslContextSupplier.get();
+ } catch (ClassNotFoundException
+ | ClassCastException
+ | NoSuchMethodException
+ | InvocationTargetException
+ | InstantiationException
+ | IllegalAccessException e) {
+ throw new SSLContextException("Could not retrieve the SSLContext
from supplier source '"
+ + supplierContextClassName
+ + "' provided in the property '"
+ + sslContextSupplierClassProperty
+ + "'", e);
}
}
diff --git
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
index 660ca64bf..f76b91ddf 100644
---
a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
+++
b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java
@@ -26,6 +26,7 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.netty.buffer.UnpooledByteBufAllocator;
+import io.netty.handler.ssl.JdkSslContext;
import io.netty.handler.ssl.SslContext;
import java.io.IOException;
import java.net.InetAddress;
@@ -91,6 +92,8 @@ public void cleanUp() {
System.clearProperty(x509Util.getCipherSuitesProperty());
System.clearProperty(x509Util.getSslProtocolProperty());
System.clearProperty(x509Util.getSslHandshakeDetectionTimeoutMillisProperty());
+
System.clearProperty(x509Util.getSslHostnameVerificationEnabledProperty());
+
System.clearProperty(x509Util.getSslClientHostnameVerificationEnabledProperty());
System.clearProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY);
System.clearProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET);
System.clearProperty(FIPS_MODE_PROPERTY);
@@ -725,6 +728,64 @@ public void
testCreateSSLContext_validCustomSSLContextClass(
assertEquals(SSLContext.getDefault(), sslContext);
}
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testCreateNettySslContextForClient_customSSLContextClass(
+ X509KeyType caKeyType, X509KeyType certKeyType, String
keyPassword, Integer paramIndex)
+ throws Exception {
+ init(caKeyType, certKeyType, keyPassword, paramIndex);
+ try (ClientX509Util clientX509Util = new ClientX509Util()) {
+ ZKConfig zkConfig = new ZKConfig();
+
zkConfig.setProperty(clientX509Util.getSslContextSupplierClassProperty(),
SslContextSupplier.class.getName());
+ // Disable hostname verification so the JdkSslContext is not
wrapped in a DelegatingSslContext.
+
zkConfig.setProperty(clientX509Util.getSslHostnameVerificationEnabledProperty(),
"false");
+
+ SslContext sslContext =
clientX509Util.createNettySslContextForClient(zkConfig);
+
+ assertTrue(sslContext instanceof JdkSslContext);
+ assertEquals(SSLContext.getDefault(), ((JdkSslContext)
sslContext).context());
+ assertTrue(sslContext.isClient());
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void testCreateNettySslContextForServer_customSSLContextClass(
+ X509KeyType caKeyType, X509KeyType certKeyType, String
keyPassword, Integer paramIndex)
+ throws Exception {
+ init(caKeyType, certKeyType, keyPassword, paramIndex);
+ try (ClientX509Util clientX509Util = new ClientX509Util()) {
+ ZKConfig zkConfig = new ZKConfig();
+
zkConfig.setProperty(clientX509Util.getSslContextSupplierClassProperty(),
SslContextSupplier.class.getName());
+ // A supplied SSLContext carries its own key material, so no key
store must be required.
+
zkConfig.setProperty(clientX509Util.getSslKeystoreLocationProperty(), "");
+ // Disable hostname verification so the JdkSslContext is not
wrapped in a DelegatingSslContext.
+
zkConfig.setProperty(clientX509Util.getSslHostnameVerificationEnabledProperty(),
"false");
+
+ SslContext sslContext =
clientX509Util.createNettySslContextForServer(zkConfig);
+
+ assertTrue(sslContext instanceof JdkSslContext);
+ assertEquals(SSLContext.getDefault(), ((JdkSslContext)
sslContext).context());
+ assertTrue(sslContext.isServer());
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void
testCreateNettySslContext_customSSLContextClassRejectsNonJdkProvider(
+ X509KeyType caKeyType, X509KeyType certKeyType, String
keyPassword, Integer paramIndex)
+ throws Exception {
+ init(caKeyType, certKeyType, keyPassword, paramIndex);
+ try (ClientX509Util clientX509Util = new ClientX509Util()) {
+ ZKConfig zkConfig = new ZKConfig();
+
zkConfig.setProperty(clientX509Util.getSslContextSupplierClassProperty(),
SslContextSupplier.class.getName());
+ zkConfig.setProperty(clientX509Util.getSslProviderProperty(),
"OPENSSL");
+
+ assertThrows(X509Exception.SSLContextException.class,
+ () -> clientX509Util.createNettySslContextForClient(zkConfig));
+ }
+ }
+
@ParameterizedTest
@MethodSource("data")
public void testCreateSSLContext_ocspWithJreProvider(