Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
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.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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
*
* <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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
Loading