From 400839a60ff3bd5a4af60710fbd07ce4ae5601a0 Mon Sep 17 00:00:00 2001 From: Alex Rankin Date: Mon, 3 Dec 2018 11:12:19 +0000 Subject: [PATCH] Adding ability to specify custom SSLContext for client --- .../org/apache/zookeeper/common/X509Util.java | 150 ++++++++++-------- .../zookeeper/common/ZKClientSSLContext.java | 35 ++++ .../org/apache/zookeeper/common/ZKConfig.java | 2 + .../apache/zookeeper/common/X509UtilTest.java | 25 +++ .../common/ZKTestClientSSLContext.java | 29 ++++ 5 files changed, 175 insertions(+), 66 deletions(-) create mode 100644 zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKClientSSLContext.java create mode 100644 zookeeper-server/src/test/java/org/apache/zookeeper/common/ZKTestClientSSLContext.java 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 e3625a51c60..76c3c4dd35d 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 @@ -17,19 +17,11 @@ */ package org.apache.zookeeper.common; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.net.Socket; -import java.security.GeneralSecurityException; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.NoSuchAlgorithmException; -import java.security.Security; -import java.security.cert.PKIXBuilderParameters; -import java.security.cert.X509CertSelector; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicReference; +import org.apache.zookeeper.common.X509Exception.KeyManagerException; +import org.apache.zookeeper.common.X509Exception.SSLContextException; +import org.apache.zookeeper.common.X509Exception.TrustManagerException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.net.ssl.CertPathTrustManagerParameters; import javax.net.ssl.KeyManager; @@ -43,12 +35,19 @@ import javax.net.ssl.X509ExtendedTrustManager; import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; - -import org.apache.zookeeper.common.X509Exception.KeyManagerException; -import org.apache.zookeeper.common.X509Exception.SSLContextException; -import org.apache.zookeeper.common.X509Exception.TrustManagerException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.Socket; +import java.security.GeneralSecurityException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.Security; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.X509CertSelector; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; /** * Utility code for X509 handling @@ -85,6 +84,7 @@ public abstract class X509Util { private String sslTruststoreLocationProperty = getConfigPrefix() + "trustStore.location"; private String sslTruststorePasswdProperty = getConfigPrefix() + "trustStore.password"; private String sslTruststoreTypeProperty = getConfigPrefix() + "trustStore.type"; + private String sslClientContextProperty = getConfigPrefix() + "client.context"; private String sslHostnameVerificationEnabledProperty = getConfigPrefix() + "hostnameVerification"; private String sslCrlEnabledProperty = getConfigPrefix() + "crl"; private String sslOcspEnabledProperty = getConfigPrefix() + "ocsp"; @@ -138,6 +138,10 @@ public String getSslTruststoreTypeProperty() { return sslTruststoreTypeProperty; } + public String getSslClientContextProperty() { + return sslClientContextProperty; + } + public String getSslHostnameVerificationEnabledProperty() { return sslHostnameVerificationEnabledProperty; } @@ -208,62 +212,76 @@ public int getSslHandshakeTimeoutMillis() { } public SSLContext createSSLContext(ZKConfig config) throws SSLContextException { - KeyManager[] keyManagers = null; - TrustManager[] trustManagers = null; + if (config.getProperty(sslClientContextProperty) != null) { + LOG.debug("Loading SSLContext from property '" + sslClientContextProperty + "'"); + String sslClientContextClass = config.getProperty(sslClientContextProperty); + try { + Class sslContextClass = Class.forName(sslClientContextClass); + ZKClientSSLContext sslContext = (ZKClientSSLContext) sslContextClass.getConstructor().newInstance(); + return sslContext.getSSLContext(); + } catch (ClassNotFoundException | ClassCastException | NoSuchMethodException | InvocationTargetException | + InstantiationException | IllegalAccessException e) { + throw new SSLContextException("Could not retrieve the SSLContext from source '" + sslClientContextClass + + "' provided in the property '" + sslClientContextProperty + "'", e); + } + } else { + KeyManager[] keyManagers = null; + TrustManager[] trustManagers = null; - String keyStoreLocationProp = config.getProperty(sslKeystoreLocationProperty, ""); - String keyStorePasswordProp = config.getProperty(sslKeystorePasswdProperty, ""); - String keyStoreTypeProp = config.getProperty(sslKeystoreTypeProperty); + String keyStoreLocationProp = config.getProperty(sslKeystoreLocationProperty, ""); + String keyStorePasswordProp = config.getProperty(sslKeystorePasswdProperty, ""); + String keyStoreTypeProp = config.getProperty(sslKeystoreTypeProperty); - // There are legal states in some use cases for null KeyManager or TrustManager. - // But if a user wanna specify one, location is required. Password defaults to empty string if it is not - // specified by the user. + // There are legal states in some use cases for null KeyManager or TrustManager. + // But if a user wanna specify one, location is required. Password defaults to empty string if it is not + // specified by the user. - if (keyStoreLocationProp.isEmpty()) { - LOG.warn(getSslKeystoreLocationProperty() + " not specified"); - } else { - try { - keyManagers = new KeyManager[]{ - createKeyManager(keyStoreLocationProp, keyStorePasswordProp, keyStoreTypeProp)}; - } catch (KeyManagerException keyManagerException) { - throw new SSLContextException("Failed to create KeyManager", keyManagerException); - } catch (IllegalArgumentException e) { - throw new SSLContextException("Bad value for " + sslKeystoreTypeProperty + ": " + keyStoreTypeProp, e); + if (keyStoreLocationProp.isEmpty()) { + LOG.warn(getSslKeystoreLocationProperty() + " not specified"); + } else { + try { + keyManagers = new KeyManager[]{ + createKeyManager(keyStoreLocationProp, keyStorePasswordProp, keyStoreTypeProp)}; + } catch (KeyManagerException keyManagerException) { + throw new SSLContextException("Failed to create KeyManager", keyManagerException); + } catch (IllegalArgumentException e) { + throw new SSLContextException("Bad value for " + sslKeystoreTypeProperty + ": " + keyStoreTypeProp, e); + } } - } - String trustStoreLocationProp = config.getProperty(sslTruststoreLocationProperty, ""); - String trustStorePasswordProp = config.getProperty(sslTruststorePasswdProperty, ""); - String trustStoreTypeProp = config.getProperty(sslTruststoreTypeProperty); + String trustStoreLocationProp = config.getProperty(sslTruststoreLocationProperty, ""); + String trustStorePasswordProp = config.getProperty(sslTruststorePasswdProperty, ""); + String trustStoreTypeProp = config.getProperty(sslTruststoreTypeProperty); - boolean sslCrlEnabled = config.getBoolean(this.sslCrlEnabledProperty); - boolean sslOcspEnabled = config.getBoolean(this.sslOcspEnabledProperty); - boolean sslServerHostnameVerificationEnabled = - config.getBoolean(this.getSslHostnameVerificationEnabledProperty(), true); - boolean sslClientHostnameVerificationEnabled = - sslServerHostnameVerificationEnabled && shouldVerifyClientHostname(); + boolean sslCrlEnabled = config.getBoolean(this.sslCrlEnabledProperty); + boolean sslOcspEnabled = config.getBoolean(this.sslOcspEnabledProperty); + boolean sslServerHostnameVerificationEnabled = + config.getBoolean(this.getSslHostnameVerificationEnabledProperty(), true); + boolean sslClientHostnameVerificationEnabled = + sslServerHostnameVerificationEnabled && shouldVerifyClientHostname(); - if (trustStoreLocationProp.isEmpty()) { - LOG.warn(getSslTruststoreLocationProperty() + " not specified"); - } else { - try { - trustManagers = new TrustManager[]{ - createTrustManager(trustStoreLocationProp, trustStorePasswordProp, trustStoreTypeProp, sslCrlEnabled, sslOcspEnabled, - sslServerHostnameVerificationEnabled, sslClientHostnameVerificationEnabled)}; - } catch (TrustManagerException trustManagerException) { - throw new SSLContextException("Failed to create TrustManager", trustManagerException); - } catch (IllegalArgumentException e) { - throw new SSLContextException("Bad value for " + sslTruststoreTypeProperty + ": " + trustStoreTypeProp, e); + if (trustStoreLocationProp.isEmpty()) { + LOG.warn(getSslTruststoreLocationProperty() + " not specified"); + } else { + try { + trustManagers = new TrustManager[]{ + createTrustManager(trustStoreLocationProp, trustStorePasswordProp, trustStoreTypeProp, sslCrlEnabled, sslOcspEnabled, + sslServerHostnameVerificationEnabled, sslClientHostnameVerificationEnabled)}; + } catch (TrustManagerException trustManagerException) { + throw new SSLContextException("Failed to create TrustManager", trustManagerException); + } catch (IllegalArgumentException e) { + throw new SSLContextException("Bad value for " + sslTruststoreTypeProperty + ": " + trustStoreTypeProp, e); + } } - } - String protocol = System.getProperty(sslProtocolProperty, DEFAULT_PROTOCOL); - try { - SSLContext sslContext = SSLContext.getInstance(protocol); - sslContext.init(keyManagers, trustManagers, null); - return sslContext; - } catch (NoSuchAlgorithmException|KeyManagementException sslContextInitException) { - throw new SSLContextException(sslContextInitException); + String protocol = System.getProperty(sslProtocolProperty, DEFAULT_PROTOCOL); + try { + SSLContext sslContext = SSLContext.getInstance(protocol); + sslContext.init(keyManagers, trustManagers, null); + return sslContext; + } catch (NoSuchAlgorithmException | KeyManagementException sslContextInitException) { + throw new SSLContextException(sslContextInitException); + } } } diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKClientSSLContext.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKClientSSLContext.java new file mode 100644 index 00000000000..aec72ca1ec0 --- /dev/null +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKClientSSLContext.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.zookeeper.common; + +import javax.net.ssl.SSLContext; + +/** + * An interface for providing a custom {@link SSLContext} object to {@link X509Util} using {@link X509Util#getSslClientContextProperty()} + */ +public interface ZKClientSSLContext { + + /** + * Returns an {@link SSLContext} for use within the {@link X509Util} + * + * @return {@link SSLContext} for use within {@link X509Util} + * @throws X509Exception.SSLContextException if {@link SSLContext} cannot be created + */ + SSLContext getSSLContext() throws X509Exception.SSLContextException; + +} diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKConfig.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKConfig.java index effc0d52e52..50c992877a4 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKConfig.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKConfig.java @@ -124,6 +124,8 @@ private void putSSLProperties(X509Util x509Util) { System.getProperty(x509Util.getSslTruststorePasswdProperty())); properties.put(x509Util.getSslTruststoreTypeProperty(), System.getProperty(x509Util.getSslTruststoreTypeProperty())); + properties.put(x509Util.getSslClientContextProperty(), + System.getProperty(x509Util.getSslClientContextProperty())); properties.put(x509Util.getSslHostnameVerificationEnabledProperty(), System.getProperty(x509Util.getSslHostnameVerificationEnabledProperty())); properties.put(x509Util.getSslCrlEnabledProperty(), 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 546cf553460..de336e81be8 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 @@ -17,6 +17,10 @@ */ package org.apache.zookeeper.common; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import java.security.Security; import java.util.Collection; @@ -384,6 +388,27 @@ public void testGetSslHandshakeDetectionTimeoutMillisProperty() { } } + @Test + public void testCreateSSLContext_invalidCustomSSLContextClass() { + ZKConfig zkConfig = new ZKConfig(); + ClientX509Util clientX509Util = new ClientX509Util(); + zkConfig.setProperty(clientX509Util.getSslClientContextProperty(), String.class.getCanonicalName()); + try { + clientX509Util.createSSLContext(zkConfig); + fail("SSLContextException expected."); + } catch (X509Exception.SSLContextException e) { + assertTrue(e.getMessage().contains(clientX509Util.getSslClientContextProperty())); + } + } + @Test + public void testCreateSSLContext_validCustomSSLContextClass() throws X509Exception.SSLContextException { + ZKConfig zkConfig = new ZKConfig(); + ClientX509Util clientX509Util = new ClientX509Util(); + zkConfig.setProperty(clientX509Util.getSslClientContextProperty(), ZKTestClientSSLContext.class.getCanonicalName()); + final SSLContext sslContext = clientX509Util.createSSLContext(zkConfig); + assertNull(sslContext); + } + // Warning: this will reset the x509Util private void setCustomCipherSuites() { System.setProperty(x509Util.getCipherSuitesProperty(), customCipherSuites[0] + "," + customCipherSuites[1]); diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/ZKTestClientSSLContext.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ZKTestClientSSLContext.java new file mode 100644 index 00000000000..b91dd298bb4 --- /dev/null +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ZKTestClientSSLContext.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.zookeeper.common; + +import javax.net.ssl.SSLContext; + +public class ZKTestClientSSLContext implements ZKClientSSLContext { + + @Override + public SSLContext getSSLContext() { + return null; + } + +}