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 82364dcb55b..cc8f5a90836 100644 --- 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 1e50b84257c..82b1bb3fd1e 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,6 +19,8 @@ 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; @@ -26,6 +28,7 @@ import java.security.Security; 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; @@ -62,7 +65,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); + } + SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); KeyManager km = buildKeyManager(config); @@ -97,6 +106,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); + } + KeyManager km = buildKeyManager(config); if (km == null) { throw new X509Exception.SSLContextException( @@ -133,6 +147,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. + * + *

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. + * + *

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 460a6f3ac55..2a91e87d217 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 @@ -360,30 +360,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 sslContextSupplier = (Supplier) 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 sslContextSupplier = (Supplier) 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 660ca64bfdb..f76b91ddf9f 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(