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(

Reply via email to