From 03f20342470064c8c64f5894bba3d34e94fa8eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Paksy?= Date: Thu, 23 Jul 2026 14:19:57 +0200 Subject: [PATCH 1/2] ZOOKEEPER-5070: Support Single EKU certificates Added client keystore, server truststore config --- .../common/ClientServerX509KeyManager.java | 131 +++++++ .../common/ClientServerX509TrustManager.java | 97 +++++ .../zookeeper/common/ClientX509Util.java | 4 +- .../org/apache/zookeeper/common/X509Util.java | 152 +++++++- .../org/apache/zookeeper/common/ZKConfig.java | 8 + .../ClientServerX509KeyManagerTest.java | 308 +++++++++++++++ .../ClientServerX509TrustManagerTest.java | 240 ++++++++++++ .../zookeeper/common/X509TestContext.java | 8 + .../zookeeper/common/X509TestHelpers.java | 28 +- .../apache/zookeeper/common/X509UtilTest.java | 356 ++++++++++++++++++ 10 files changed, 1320 insertions(+), 12 deletions(-) create mode 100644 zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509KeyManager.java create mode 100644 zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509TrustManager.java create mode 100644 zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509KeyManagerTest.java create mode 100644 zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509TrustManagerTest.java diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509KeyManager.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509KeyManager.java new file mode 100644 index 00000000000..9c0f1c66652 --- /dev/null +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509KeyManager.java @@ -0,0 +1,131 @@ +/* + * 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 java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; + +/** + * An {@link X509ExtendedKeyManager} that delegates client-mode and server-mode + * key selection to separate underlying key managers. This allows a ZooKeeper + * node to use different keystores (and therefore different certificates) for + * its client role (outgoing connections) and server role (incoming connections), + * enabling the use of certificates with a single Extended Key Usage (EKU). + */ +public class ClientServerX509KeyManager extends X509ExtendedKeyManager { + + private static final String CLIENT_PREFIX = "client:"; + private static final String SERVER_PREFIX = "server:"; + + private final X509KeyManager clientKeyManager; + private final X509KeyManager serverKeyManager; + + public ClientServerX509KeyManager(X509KeyManager clientKeyManager, X509KeyManager serverKeyManager) { + this.clientKeyManager = clientKeyManager; + this.serverKeyManager = serverKeyManager; + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + String alias = clientKeyManager.chooseClientAlias(keyType, issuers, socket); + return alias != null ? CLIENT_PREFIX + alias : null; + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + String alias = serverKeyManager.chooseServerAlias(keyType, issuers, socket); + return alias != null ? SERVER_PREFIX + alias : null; + } + + @Override + public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) { + if (clientKeyManager instanceof X509ExtendedKeyManager) { + String alias = ((X509ExtendedKeyManager) clientKeyManager) + .chooseEngineClientAlias(keyType, issuers, engine); + return alias != null ? CLIENT_PREFIX + alias : null; + } + return chooseClientAlias(keyType, issuers, null); + } + + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) { + if (serverKeyManager instanceof X509ExtendedKeyManager) { + String alias = ((X509ExtendedKeyManager) serverKeyManager) + .chooseEngineServerAlias(keyType, issuers, engine); + return alias != null ? SERVER_PREFIX + alias : null; + } + return chooseServerAlias(keyType, issuers, null); + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + if (alias == null) { + return null; + } + if (alias.startsWith(CLIENT_PREFIX)) { + return clientKeyManager.getCertificateChain(alias.substring(CLIENT_PREFIX.length())); + } + if (alias.startsWith(SERVER_PREFIX)) { + return serverKeyManager.getCertificateChain(alias.substring(SERVER_PREFIX.length())); + } + return serverKeyManager.getCertificateChain(alias); + } + + @Override + public PrivateKey getPrivateKey(String alias) { + if (alias == null) { + return null; + } + if (alias.startsWith(CLIENT_PREFIX)) { + return clientKeyManager.getPrivateKey(alias.substring(CLIENT_PREFIX.length())); + } + if (alias.startsWith(SERVER_PREFIX)) { + return serverKeyManager.getPrivateKey(alias.substring(SERVER_PREFIX.length())); + } + return serverKeyManager.getPrivateKey(alias); + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + String[] aliases = clientKeyManager.getClientAliases(keyType, issuers); + return prefixAliases(aliases, CLIENT_PREFIX); + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + String[] aliases = serverKeyManager.getServerAliases(keyType, issuers); + return prefixAliases(aliases, SERVER_PREFIX); + } + + private static String[] prefixAliases(String[] aliases, String prefix) { + if (aliases == null) { + return null; + } + String[] prefixed = new String[aliases.length]; + for (int i = 0; i < aliases.length; i++) { + prefixed[i] = prefix + aliases[i]; + } + return prefixed; + } +} diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509TrustManager.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509TrustManager.java new file mode 100644 index 00000000000..81a60b13f9c --- /dev/null +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509TrustManager.java @@ -0,0 +1,97 @@ +/* + * 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 java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; + +/** + * An {@link X509ExtendedTrustManager} that delegates to separate trust managers for + * client and server certificate validation. This allows configuring different truststores + * for validating client certificates (when acting as a server) vs. server certificates + * (when acting as a client). + * + * + */ +public class ClientServerX509TrustManager extends X509ExtendedTrustManager { + + private final X509ExtendedTrustManager clientTrustManager; + private final X509ExtendedTrustManager serverTrustManager; + + /** + * @param clientTrustManager used to validate server certificates (when acting as TLS client) + * @param serverTrustManager used to validate client certificates (when acting as TLS server) + */ + public ClientServerX509TrustManager(X509ExtendedTrustManager clientTrustManager, + X509ExtendedTrustManager serverTrustManager) { + this.clientTrustManager = clientTrustManager; + this.serverTrustManager = serverTrustManager; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + serverTrustManager.checkClientTrusted(chain, authType); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + serverTrustManager.checkClientTrusted(chain, authType, socket); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { + serverTrustManager.checkClientTrusted(chain, authType, engine); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + clientTrustManager.checkServerTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + clientTrustManager.checkServerTrusted(chain, authType, socket); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { + clientTrustManager.checkServerTrusted(chain, authType, engine); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + X509Certificate[] clientIssuers = clientTrustManager.getAcceptedIssuers(); + X509Certificate[] serverIssuers = serverTrustManager.getAcceptedIssuers(); + List combined = new ArrayList<>(clientIssuers.length + serverIssuers.length); + combined.addAll(Arrays.asList(clientIssuers)); + combined.addAll(Arrays.asList(serverIssuers)); + return combined.toArray(new X509Certificate[0]); + } +} 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 82b1bb3fd1e..1c1d6ca88de 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 @@ -74,7 +74,7 @@ public SslContext createNettySslContextForClient(ZKConfig config) SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); - KeyManager km = buildKeyManager(config); + KeyManager km = buildClientKeyManager(config); if (km != null) { sslContextBuilder.keyManager(km); } @@ -116,7 +116,7 @@ public SslContext createNettySslContextForServer(ZKConfig config) throw new X509Exception.SSLContextException( "Keystore is required for SSL server: " + getSslKeystoreLocationProperty()); } - return createNettySslContextForServer(config, km, buildTrustManager(config)); + return createNettySslContextForServer(config, km, buildServerTrustManager(config)); } public SslContext createNettySslContextForServer(ZKConfig config, KeyManager keyManager, TrustManager trustManager) throws SSLException { 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 2a91e87d217..5156521466b 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 @@ -169,10 +169,18 @@ public io.netty.handler.ssl.ClientAuth toNettyClientAuth() { private final String sslKeystorePasswdProperty = getConfigPrefix() + "keyStore.password"; private final String sslKeystorePasswdPathProperty = getConfigPrefix() + "keyStore.passwordPath"; private final String sslKeystoreTypeProperty = getConfigPrefix() + "keyStore.type"; + private final String sslClientKeystoreLocationProperty = getConfigPrefix() + "client.keyStore.location"; + private final String sslClientKeystorePasswdProperty = getConfigPrefix() + "client.keyStore.password"; + private final String sslClientKeystorePasswdPathProperty = getConfigPrefix() + "client.keyStore.passwordPath"; + private final String sslClientKeystoreTypeProperty = getConfigPrefix() + "client.keyStore.type"; private final String sslTruststoreLocationProperty = getConfigPrefix() + "trustStore.location"; private final String sslTruststorePasswdProperty = getConfigPrefix() + "trustStore.password"; private final String sslTruststorePasswdPathProperty = getConfigPrefix() + "trustStore.passwordPath"; private final String sslTruststoreTypeProperty = getConfigPrefix() + "trustStore.type"; + private final String sslServerTruststoreLocationProperty = getConfigPrefix() + "server.trustStore.location"; + private final String sslServerTruststorePasswdProperty = getConfigPrefix() + "server.trustStore.password"; + private final String sslServerTruststorePasswdPathProperty = getConfigPrefix() + "server.trustStore.passwordPath"; + private final String sslServerTruststoreTypeProperty = getConfigPrefix() + "server.trustStore.type"; private final String sslContextSupplierClassProperty = getConfigPrefix() + "context.supplier.class"; private final String sslHostnameVerificationEnabledProperty = getConfigPrefix() + "hostnameVerification"; private final String sslClientHostnameVerificationEnabledProperty = getConfigPrefix() + "clientHostnameVerification"; @@ -185,10 +193,15 @@ public io.netty.handler.ssl.ClientAuth toNettyClientAuth() { private final AtomicReference defaultSSLContextAndOptions = new AtomicReference<>(null); private FileChangeWatcher keyStoreFileWatcher; + private FileChangeWatcher clientKeyStoreFileWatcher; private FileChangeWatcher trustStoreFileWatcher; + private FileChangeWatcher serverTrustStoreFileWatcher; public X509Util() { - keyStoreFileWatcher = trustStoreFileWatcher = null; + keyStoreFileWatcher = null; + clientKeyStoreFileWatcher = null; + trustStoreFileWatcher = null; + serverTrustStoreFileWatcher = null; } protected abstract String getConfigPrefix(); @@ -227,6 +240,22 @@ public String getSslKeystoreTypeProperty() { return sslKeystoreTypeProperty; } + public String getSslClientKeystoreLocationProperty() { + return sslClientKeystoreLocationProperty; + } + + public String getSslClientKeystorePasswdProperty() { + return sslClientKeystorePasswdProperty; + } + + public String getSslClientKeystorePasswdPathProperty() { + return sslClientKeystorePasswdPathProperty; + } + + public String getSslClientKeystoreTypeProperty() { + return sslClientKeystoreTypeProperty; + } + public String getSslTruststoreLocationProperty() { return sslTruststoreLocationProperty; } @@ -243,6 +272,22 @@ public String getSslTruststoreTypeProperty() { return sslTruststoreTypeProperty; } + public String getSslServerTruststoreLocationProperty() { + return sslServerTruststoreLocationProperty; + } + + public String getSslServerTruststorePasswdProperty() { + return sslServerTruststorePasswdProperty; + } + + public String getSslServerTruststorePasswdPathProperty() { + return sslServerTruststorePasswdPathProperty; + } + + public String getSslServerTruststoreTypeProperty() { + return sslServerTruststoreTypeProperty; + } + public String getSslContextSupplierClassProperty() { return sslContextSupplierClassProperty; } @@ -412,10 +457,7 @@ public SSLContextAndOptions createSSLContextAndOptionsFromConfig(ZKConfig config TrustManager[] trustManagers = null; try { - KeyManager km = buildKeyManager(config); - if (km != null) { - keyManagers = new KeyManager[]{km}; - } + keyManagers = buildKeyManagers(config, keyManagers); } catch (KeyManagerException keyManagerException) { throw new SSLContextException("Failed to create KeyManager", keyManagerException); } catch (IllegalArgumentException e) { @@ -424,10 +466,7 @@ public SSLContextAndOptions createSSLContextAndOptionsFromConfig(ZKConfig config } try { - TrustManager tm = buildTrustManager(config); - if (tm != null) { - trustManagers = new TrustManager[]{tm}; - } + trustManagers = buildTrustManagers(config); } catch (TrustManagerException trustManagerException) { throw new SSLContextException("Failed to create TrustManager", trustManagerException); } catch (IllegalArgumentException e) { @@ -448,6 +487,24 @@ public SSLContextAndOptions createSSLContextAndOptionsFromConfig(ZKConfig config } } + private KeyManager[] buildKeyManagers(ZKConfig config, KeyManager[] keyManagers) throws KeyManagerException { + String clientKeyStoreLocation = config.getProperty(sslClientKeystoreLocationProperty, ""); + X509KeyManager serverKm = buildKeyManager(config); + if (!clientKeyStoreLocation.isEmpty()) { + X509KeyManager clientKm = buildClientKeyManager(config); + if (serverKm != null && clientKm != null) { + keyManagers = new KeyManager[]{new ClientServerX509KeyManager(clientKm, serverKm)}; + } else if (clientKm != null) { + keyManagers = new KeyManager[]{clientKm}; + } else if (serverKm != null) { + keyManagers = new KeyManager[]{serverKm}; + } + } else if (serverKm != null) { + keyManagers = new KeyManager[]{serverKm}; + } + return keyManagers; + } + public static KeyStore loadKeyStore( String keyStoreLocation, String keyStorePassword, @@ -506,6 +563,22 @@ public X509KeyManager buildKeyManager(ZKConfig config) throws KeyManagerExceptio return createKeyManager(keyStoreLocation, keyStorePassword, keyStoreType); } + /** + * Builds a key manager for the client role. If a dedicated client keystore + * is configured ({@code client.keyStore.location}), it is loaded and returned. + * Otherwise, falls back to the shared keystore via {@link #buildKeyManager(ZKConfig)}. + */ + public X509KeyManager buildClientKeyManager(ZKConfig config) throws KeyManagerException { + String clientKeyStoreLocation = config.getProperty(getSslClientKeystoreLocationProperty(), ""); + if (!clientKeyStoreLocation.isEmpty()) { + String clientKeyStorePassword = getPasswordFromConfigPropertyOrFile(config, + getSslClientKeystorePasswdProperty(), getSslClientKeystorePasswdPathProperty()); + String clientKeyStoreType = config.getProperty(getSslClientKeystoreTypeProperty()); + return createKeyManager(clientKeyStoreLocation, clientKeyStorePassword, clientKeyStoreType); + } + return buildKeyManager(config); + } + /** * Creates a key manager by loading the key store from the given file of * the given type, optionally decrypting it using the given password. @@ -548,6 +621,22 @@ public X509TrustManager buildTrustManager(ZKConfig config) throws TrustManagerEx return null; } + return buildX509TrustManager(config, trustStoreLocationProp, sslTruststorePasswdProperty, sslTruststorePasswdPathProperty, sslTruststoreTypeProperty); + } + + public X509TrustManager buildServerTrustManager(ZKConfig config) throws TrustManagerException { + String serverTrustStoreLocation = config.getProperty(sslServerTruststoreLocationProperty, ""); + if (serverTrustStoreLocation.isEmpty()) { + return buildTrustManager(config); + } + + return buildX509TrustManager(config, serverTrustStoreLocation, sslServerTruststorePasswdProperty, sslServerTruststorePasswdPathProperty, sslServerTruststoreTypeProperty); + } + + private X509TrustManager buildX509TrustManager(ZKConfig config, String trustStoreLocationProp, + String sslTruststorePasswdProperty, + String sslTruststorePasswdPathProperty, + String sslTruststoreTypeProperty) throws TrustManagerException { String trustStorePasswordProp = getPasswordFromConfigPropertyOrFile(config, sslTruststorePasswdProperty, sslTruststorePasswdPathProperty); String trustStoreTypeProp = config.getProperty(sslTruststoreTypeProperty); @@ -571,6 +660,25 @@ public X509TrustManager buildTrustManager(ZKConfig config) throws TrustManagerEx fipsMode); } + private TrustManager[] buildTrustManagers(ZKConfig config) throws TrustManagerException { + String serverTrustStoreLocation = config.getProperty(sslServerTruststoreLocationProperty, ""); + X509TrustManager clientTm = buildTrustManager(config); + if (!serverTrustStoreLocation.isEmpty()) { + X509TrustManager serverTm = buildServerTrustManager(config); + if (clientTm != null && serverTm != null) { + return new TrustManager[]{new ClientServerX509TrustManager( + (X509ExtendedTrustManager) clientTm, (X509ExtendedTrustManager) serverTm)}; + } else if (serverTm != null) { + return new TrustManager[]{serverTm}; + } else if (clientTm != null) { + return new TrustManager[]{clientTm}; + } + } else if (clientTm != null) { + return new TrustManager[]{clientTm}; + } + return null; + } + // @VisibleForTesting protected X509TrustManager createTrustManagerInternal( String trustStoreLocation, @@ -743,6 +851,15 @@ public void enableCertFileReloading() throws IOException { keyStoreFileWatcher = newKeyStoreFileWatcher; keyStoreFileWatcher.start(); } + FileChangeWatcher newClientKeyStoreFileWatcher = newFileChangeWatcher(config.getProperty(sslClientKeystoreLocationProperty)); + if (newClientKeyStoreFileWatcher != null) { + // stop old watcher if there is one + if (clientKeyStoreFileWatcher != null) { + clientKeyStoreFileWatcher.stop(); + } + clientKeyStoreFileWatcher = newClientKeyStoreFileWatcher; + clientKeyStoreFileWatcher.start(); + } FileChangeWatcher newTrustStoreFileWatcher = newFileChangeWatcher(config.getProperty(sslTruststoreLocationProperty)); if (newTrustStoreFileWatcher != null) { // stop old watcher if there is one @@ -752,6 +869,15 @@ public void enableCertFileReloading() throws IOException { trustStoreFileWatcher = newTrustStoreFileWatcher; trustStoreFileWatcher.start(); } + FileChangeWatcher newServerTrustStoreFileWatcher = newFileChangeWatcher(config.getProperty(sslServerTruststoreLocationProperty)); + if (newServerTrustStoreFileWatcher != null) { + // stop old watcher if there is one + if (serverTrustStoreFileWatcher != null) { + serverTrustStoreFileWatcher.stop(); + } + serverTrustStoreFileWatcher = newServerTrustStoreFileWatcher; + serverTrustStoreFileWatcher.start(); + } } /** @@ -765,10 +891,18 @@ public void close() { keyStoreFileWatcher.stop(); keyStoreFileWatcher = null; } + if (clientKeyStoreFileWatcher != null) { + clientKeyStoreFileWatcher.stop(); + clientKeyStoreFileWatcher = null; + } if (trustStoreFileWatcher != null) { trustStoreFileWatcher.stop(); trustStoreFileWatcher = null; } + if (serverTrustStoreFileWatcher != null) { + serverTrustStoreFileWatcher.stop(); + serverTrustStoreFileWatcher = null; + } } /** 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 442d8e7221d..a40bb3c533c 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 @@ -146,10 +146,18 @@ private void putSSLProperties(X509Util x509Util) { properties.put(x509Util.getSslKeystorePasswdProperty(), System.getProperty(x509Util.getSslKeystorePasswdProperty())); properties.put(x509Util.getSslKeystorePasswdPathProperty(), System.getProperty(x509Util.getSslKeystorePasswdPathProperty())); properties.put(x509Util.getSslKeystoreTypeProperty(), System.getProperty(x509Util.getSslKeystoreTypeProperty())); + properties.put(x509Util.getSslClientKeystoreLocationProperty(), System.getProperty(x509Util.getSslClientKeystoreLocationProperty())); + properties.put(x509Util.getSslClientKeystorePasswdProperty(), System.getProperty(x509Util.getSslClientKeystorePasswdProperty())); + properties.put(x509Util.getSslClientKeystorePasswdPathProperty(), System.getProperty(x509Util.getSslClientKeystorePasswdPathProperty())); + properties.put(x509Util.getSslClientKeystoreTypeProperty(), System.getProperty(x509Util.getSslClientKeystoreTypeProperty())); properties.put(x509Util.getSslTruststoreLocationProperty(), System.getProperty(x509Util.getSslTruststoreLocationProperty())); properties.put(x509Util.getSslTruststorePasswdProperty(), System.getProperty(x509Util.getSslTruststorePasswdProperty())); properties.put(x509Util.getSslTruststorePasswdPathProperty(), System.getProperty(x509Util.getSslTruststorePasswdPathProperty())); properties.put(x509Util.getSslTruststoreTypeProperty(), System.getProperty(x509Util.getSslTruststoreTypeProperty())); + properties.put(x509Util.getSslServerTruststoreLocationProperty(), System.getProperty(x509Util.getSslServerTruststoreLocationProperty())); + properties.put(x509Util.getSslServerTruststorePasswdProperty(), System.getProperty(x509Util.getSslServerTruststorePasswdProperty())); + properties.put(x509Util.getSslServerTruststorePasswdPathProperty(), System.getProperty(x509Util.getSslServerTruststorePasswdPathProperty())); + properties.put(x509Util.getSslServerTruststoreTypeProperty(), System.getProperty(x509Util.getSslServerTruststoreTypeProperty())); properties.put(x509Util.getSslContextSupplierClassProperty(), System.getProperty(x509Util.getSslContextSupplierClassProperty())); properties.put(x509Util.getSslClientHostnameVerificationEnabledProperty(), System.getProperty(x509Util.getSslClientHostnameVerificationEnabledProperty())); properties.put(x509Util.getSslHostnameVerificationEnabledProperty(), System.getProperty(x509Util.getSslHostnameVerificationEnabledProperty())); diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509KeyManagerTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509KeyManagerTest.java new file mode 100644 index 00000000000..5e04b0b8a42 --- /dev/null +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509KeyManagerTest.java @@ -0,0 +1,308 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import java.net.Socket; +import java.security.KeyPair; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ClientServerX509KeyManagerTest { + + private static KeyPair clientKeyPair; + private static X509Certificate clientCert; + private static KeyPair serverKeyPair; + private static X509Certificate serverCert; + + @BeforeAll + public static void setUp() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + KeyPair caKeyPair = X509TestHelpers.generateKeyPair(X509KeyType.RSA); + X509Certificate caCert = X509TestHelpers.newSelfSignedCACert( + new org.bouncycastle.asn1.x500.X500NameBuilder(org.bouncycastle.asn1.x500.style.BCStyle.INSTANCE) + .addRDN(org.bouncycastle.asn1.x500.style.BCStyle.CN, "Test CA") + .build(), + caKeyPair, 86400000L); + clientKeyPair = X509TestHelpers.generateKeyPair(X509KeyType.RSA); + clientCert = X509TestHelpers.newClientOnlyCert(caCert, caKeyPair, "client", clientKeyPair.getPublic()); + serverKeyPair = X509TestHelpers.generateKeyPair(X509KeyType.RSA); + serverCert = X509TestHelpers.newServerOnlyCert(caCert, caKeyPair, "server", serverKeyPair.getPublic()); + } + + @AfterAll + public static void tearDown() { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + } + + @Test + public void testChooseClientAliasDelegatesToClientKeyManager() { + X509KeyManager clientKm = new StubKeyManager("clientAlias", clientCert, clientKeyPair.getPrivate()); + X509KeyManager serverKm = new StubKeyManager("serverAlias", serverCert, serverKeyPair.getPrivate()); + ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); + + String alias = keyManager.chooseClientAlias(new String[]{"RSA"}, null, null); + assertNotNull(alias); + assertEquals("client:clientAlias", alias); + } + + @Test + public void testChooseServerAliasDelegatesToServerKeyManager() { + X509KeyManager clientKm = new StubKeyManager("clientAlias", clientCert, clientKeyPair.getPrivate()); + X509KeyManager serverKm = new StubKeyManager("serverAlias", serverCert, serverKeyPair.getPrivate()); + ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); + + String alias = keyManager.chooseServerAlias("RSA", null, null); + assertNotNull(alias); + assertEquals("server:serverAlias", alias); + } + + @Test + public void testGetCertificateChainRoutesToClientManager() { + X509KeyManager clientKm = new StubKeyManager("myAlias", clientCert, clientKeyPair.getPrivate()); + X509KeyManager serverKm = new StubKeyManager("myAlias", serverCert, serverKeyPair.getPrivate()); + ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); + + X509Certificate[] chain = keyManager.getCertificateChain("client:myAlias"); + assertNotNull(chain); + assertEquals(1, chain.length); + assertEquals(clientCert, chain[0]); + } + + @Test + public void testGetCertificateChainRoutesToServerManager() { + X509KeyManager clientKm = new StubKeyManager("myAlias", clientCert, clientKeyPair.getPrivate()); + X509KeyManager serverKm = new StubKeyManager("myAlias", serverCert, serverKeyPair.getPrivate()); + ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); + + X509Certificate[] chain = keyManager.getCertificateChain("server:myAlias"); + assertNotNull(chain); + assertEquals(1, chain.length); + assertEquals(serverCert, chain[0]); + } + + @Test + public void testGetPrivateKeyRoutesToClientManager() { + X509KeyManager clientKm = new StubKeyManager("myAlias", clientCert, clientKeyPair.getPrivate()); + X509KeyManager serverKm = new StubKeyManager("myAlias", serverCert, serverKeyPair.getPrivate()); + ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); + + PrivateKey key = keyManager.getPrivateKey("client:myAlias"); + assertEquals(clientKeyPair.getPrivate(), key); + } + + @Test + public void testGetPrivateKeyRoutesToServerManager() { + X509KeyManager clientKm = new StubKeyManager("myAlias", clientCert, clientKeyPair.getPrivate()); + X509KeyManager serverKm = new StubKeyManager("myAlias", serverCert, serverKeyPair.getPrivate()); + ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); + + PrivateKey key = keyManager.getPrivateKey("server:myAlias"); + assertEquals(serverKeyPair.getPrivate(), key); + } + + @Test + public void testGetClientAliasesPrefixed() { + X509KeyManager clientKm = new StubKeyManager("a1", clientCert, clientKeyPair.getPrivate()); + X509KeyManager serverKm = new StubKeyManager("a2", serverCert, serverKeyPair.getPrivate()); + ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); + + String[] aliases = keyManager.getClientAliases("RSA", null); + assertNotNull(aliases); + assertEquals(1, aliases.length); + assertEquals("client:a1", aliases[0]); + } + + @Test + public void testGetServerAliasesPrefixed() { + X509KeyManager clientKm = new StubKeyManager("a1", clientCert, clientKeyPair.getPrivate()); + X509KeyManager serverKm = new StubKeyManager("a2", serverCert, serverKeyPair.getPrivate()); + ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); + + String[] aliases = keyManager.getServerAliases("RSA", null); + assertNotNull(aliases); + assertEquals(1, aliases.length); + assertEquals("server:a2", aliases[0]); + } + + @Test + public void testNullAliasReturnsNull() { + X509KeyManager clientKm = new StubKeyManager(null, clientCert, clientKeyPair.getPrivate()); + X509KeyManager serverKm = new StubKeyManager(null, serverCert, serverKeyPair.getPrivate()); + ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); + + assertNull(keyManager.chooseClientAlias(new String[]{"RSA"}, null, null)); + assertNull(keyManager.chooseServerAlias("RSA", null, null)); + assertNull(keyManager.getCertificateChain(null)); + assertNull(keyManager.getPrivateKey(null)); + } + + @Test + public void testUnprefixedAliasFallsBackToServerManager() { + X509KeyManager clientKm = new StubKeyManager("myAlias", clientCert, clientKeyPair.getPrivate()); + X509KeyManager serverKm = new StubKeyManager("myAlias", serverCert, serverKeyPair.getPrivate()); + ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); + + X509Certificate[] chain = keyManager.getCertificateChain("myAlias"); + assertNotNull(chain); + assertEquals(serverCert, chain[0]); + + PrivateKey key = keyManager.getPrivateKey("myAlias"); + assertEquals(serverKeyPair.getPrivate(), key); + } + + @Test + public void testChooseEngineClientAliasDelegatesToExtendedKeyManager() { + ExtendedStubKeyManager clientKm = new ExtendedStubKeyManager("engineClient", clientCert, clientKeyPair.getPrivate()); + X509KeyManager serverKm = new StubKeyManager("serverAlias", serverCert, serverKeyPair.getPrivate()); + ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); + + String alias = keyManager.chooseEngineClientAlias(new String[]{"RSA"}, null, null); + assertNotNull(alias); + assertEquals("client:engineClient", alias); + } + + @Test + public void testChooseEngineServerAliasDelegatesToExtendedKeyManager() { + X509KeyManager clientKm = new StubKeyManager("clientAlias", clientCert, clientKeyPair.getPrivate()); + ExtendedStubKeyManager serverKm = new ExtendedStubKeyManager("engineServer", serverCert, serverKeyPair.getPrivate()); + ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); + + String alias = keyManager.chooseEngineServerAlias("RSA", null, null); + assertNotNull(alias); + assertEquals("server:engineServer", alias); + } + + private static class StubKeyManager implements X509KeyManager { + private final String alias; + private final X509Certificate cert; + private final PrivateKey key; + + StubKeyManager(String alias, X509Certificate cert, PrivateKey key) { + this.alias = alias; + this.cert = cert; + this.key = key; + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return alias != null ? new String[]{alias} : null; + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return alias; + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return alias != null ? new String[]{alias} : null; + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return alias; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + if (this.alias != null && this.alias.equals(alias)) { + return new X509Certificate[]{cert}; + } + return null; + } + + @Override + public PrivateKey getPrivateKey(String alias) { + if (this.alias != null && this.alias.equals(alias)) { + return key; + } + return null; + } + } + + private static class ExtendedStubKeyManager extends X509ExtendedKeyManager { + private final String alias; + private final X509Certificate cert; + private final PrivateKey key; + + ExtendedStubKeyManager(String alias, X509Certificate cert, PrivateKey key) { + this.alias = alias; + this.cert = cert; + this.key = key; + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return alias != null ? new String[]{alias} : null; + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return alias; + } + + @Override + public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) { + return alias; + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return alias != null ? new String[]{alias} : null; + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return alias; + } + + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) { + return alias; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + if (this.alias != null && this.alias.equals(alias)) { + return new X509Certificate[]{cert}; + } + return null; + } + + @Override + public PrivateKey getPrivateKey(String alias) { + if (this.alias != null && this.alias.equals(alias)) { + return key; + } + return null; + } + } +} diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509TrustManagerTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509TrustManagerTest.java new file mode 100644 index 00000000000..24d903aef19 --- /dev/null +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509TrustManagerTest.java @@ -0,0 +1,240 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.net.Socket; +import java.security.KeyPair; +import java.security.Security; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ClientServerX509TrustManagerTest { + + private static X509Certificate clientCaCert; + private static X509Certificate clientCert; + + private static X509Certificate serverCaCert; + private static X509Certificate serverCert; + + @BeforeAll + public static void setUp() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + + KeyPair clientCaKeyPair = X509TestHelpers.generateKeyPair(X509KeyType.RSA); + clientCaCert = X509TestHelpers.newSelfSignedCACert( + new X500NameBuilder(BCStyle.INSTANCE).addRDN(BCStyle.CN, "Client CA").build(), + clientCaKeyPair, 86400000L); + + KeyPair clientKeyPair = X509TestHelpers.generateKeyPair(X509KeyType.RSA); + clientCert = X509TestHelpers.newClientOnlyCert(clientCaCert, clientCaKeyPair, "client", clientKeyPair.getPublic()); + + KeyPair serverCaKeyPair = X509TestHelpers.generateKeyPair(X509KeyType.RSA); + serverCaCert = X509TestHelpers.newSelfSignedCACert( + new X500NameBuilder(BCStyle.INSTANCE).addRDN(BCStyle.CN, "Server CA").build(), + serverCaKeyPair, 86400000L); + + KeyPair serverKeyPair = X509TestHelpers.generateKeyPair(X509KeyType.RSA); + serverCert = X509TestHelpers.newServerOnlyCert(serverCaCert, serverCaKeyPair, "server", serverKeyPair.getPublic()); + } + + @AfterAll + public static void tearDown() { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + } + + @Test + public void testCheckServerTrustedDelegatesToClientTrustManager() { + StubTrustManager clientTm = new StubTrustManager(serverCaCert); + StubTrustManager serverTm = new StubTrustManager(clientCaCert); + ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); + + assertDoesNotThrow(() -> trustManager.checkServerTrusted( + new X509Certificate[]{serverCert, serverCaCert}, "RSA")); + assertTrue(clientTm.checkServerTrustedCalled); + } + + @Test + public void testCheckClientTrustedDelegatesToServerTrustManager() { + StubTrustManager clientTm = new StubTrustManager(serverCaCert); + StubTrustManager serverTm = new StubTrustManager(clientCaCert); + ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); + + assertDoesNotThrow(() -> trustManager.checkClientTrusted( + new X509Certificate[]{clientCert, clientCaCert}, "RSA")); + assertTrue(serverTm.checkClientTrustedCalled); + } + + @Test + public void testCheckServerTrustedFailsWithWrongTrustManager() { + StubTrustManager clientTm = new StubTrustManager(clientCaCert); + StubTrustManager serverTm = new StubTrustManager(serverCaCert); + ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); + + assertThrows(CertificateException.class, () -> trustManager.checkServerTrusted( + new X509Certificate[]{serverCert, serverCaCert}, "RSA")); + } + + @Test + public void testCheckClientTrustedFailsWithWrongTrustManager() { + StubTrustManager clientTm = new StubTrustManager(clientCaCert); + StubTrustManager serverTm = new StubTrustManager(serverCaCert); + ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); + + assertThrows(CertificateException.class, () -> trustManager.checkClientTrusted( + new X509Certificate[]{clientCert, clientCaCert}, "RSA")); + } + + @Test + public void testCheckServerTrustedWithSocket() { + StubTrustManager clientTm = new StubTrustManager(serverCaCert); + StubTrustManager serverTm = new StubTrustManager(clientCaCert); + ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); + + assertDoesNotThrow(() -> trustManager.checkServerTrusted( + new X509Certificate[]{serverCert, serverCaCert}, "RSA", (Socket) null)); + assertTrue(clientTm.checkServerTrustedSocketCalled); + } + + @Test + public void testCheckClientTrustedWithSocket() { + StubTrustManager clientTm = new StubTrustManager(serverCaCert); + StubTrustManager serverTm = new StubTrustManager(clientCaCert); + ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); + + assertDoesNotThrow(() -> trustManager.checkClientTrusted( + new X509Certificate[]{clientCert, clientCaCert}, "RSA", (Socket) null)); + assertTrue(serverTm.checkClientTrustedSocketCalled); + } + + @Test + public void testCheckServerTrustedWithEngine() { + StubTrustManager clientTm = new StubTrustManager(serverCaCert); + StubTrustManager serverTm = new StubTrustManager(clientCaCert); + ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); + + assertDoesNotThrow(() -> trustManager.checkServerTrusted( + new X509Certificate[]{serverCert, serverCaCert}, "RSA", (SSLEngine) null)); + assertTrue(clientTm.checkServerTrustedEngineCalled); + } + + @Test + public void testCheckClientTrustedWithEngine() { + StubTrustManager clientTm = new StubTrustManager(serverCaCert); + StubTrustManager serverTm = new StubTrustManager(clientCaCert); + ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); + + assertDoesNotThrow(() -> trustManager.checkClientTrusted( + new X509Certificate[]{clientCert, clientCaCert}, "RSA", (SSLEngine) null)); + assertTrue(serverTm.checkClientTrustedEngineCalled); + } + + @Test + public void testGetAcceptedIssuersReturnsCombined() { + StubTrustManager clientTm = new StubTrustManager(serverCaCert); + StubTrustManager serverTm = new StubTrustManager(clientCaCert); + ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); + + X509Certificate[] issuers = trustManager.getAcceptedIssuers(); + assertEquals(2, issuers.length); + assertEquals(serverCaCert, issuers[0]); + assertEquals(clientCaCert, issuers[1]); + } + + private static class StubTrustManager extends X509ExtendedTrustManager { + private final X509Certificate trustedCa; + boolean checkClientTrustedCalled; + boolean checkServerTrustedCalled; + boolean checkClientTrustedSocketCalled; + boolean checkServerTrustedSocketCalled; + boolean checkClientTrustedEngineCalled; + boolean checkServerTrustedEngineCalled; + + StubTrustManager(X509Certificate trustedCa) { + this.trustedCa = trustedCa; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + checkClientTrustedCalled = true; + verifyChain(chain); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + checkServerTrustedCalled = true; + verifyChain(chain); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + checkClientTrustedSocketCalled = true; + verifyChain(chain); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + checkServerTrustedSocketCalled = true; + verifyChain(chain); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { + checkClientTrustedEngineCalled = true; + verifyChain(chain); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { + checkServerTrustedEngineCalled = true; + verifyChain(chain); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[]{trustedCa}; + } + + private void verifyChain(X509Certificate[] chain) throws CertificateException { + for (X509Certificate cert : chain) { + if (cert.equals(trustedCa)) { + return; + } + try { + cert.verify(trustedCa.getPublicKey()); + return; + } catch (Exception e) { + // continue checking + } + } + throw new CertificateException("Certificate chain not trusted"); + } + } +} diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java index 80632e45c01..45fad846d3d 100644 --- a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java @@ -393,10 +393,18 @@ public void clearSystemProperties(X509Util x509Util) { System.clearProperty(x509Util.getSslKeystorePasswdProperty()); System.clearProperty(x509Util.getSslKeystorePasswdPathProperty()); System.clearProperty(x509Util.getSslKeystoreTypeProperty()); + System.clearProperty(x509Util.getSslClientKeystoreLocationProperty()); + System.clearProperty(x509Util.getSslClientKeystorePasswdProperty()); + System.clearProperty(x509Util.getSslClientKeystorePasswdPathProperty()); + System.clearProperty(x509Util.getSslClientKeystoreTypeProperty()); System.clearProperty(x509Util.getSslTruststoreLocationProperty()); System.clearProperty(x509Util.getSslTruststorePasswdProperty()); System.clearProperty(x509Util.getSslTruststorePasswdPathProperty()); System.clearProperty(x509Util.getSslTruststoreTypeProperty()); + System.clearProperty(x509Util.getSslServerTruststoreLocationProperty()); + System.clearProperty(x509Util.getSslServerTruststorePasswdProperty()); + System.clearProperty(x509Util.getSslServerTruststorePasswdPathProperty()); + System.clearProperty(x509Util.getSslServerTruststoreTypeProperty()); System.clearProperty(x509Util.getSslHostnameVerificationEnabledProperty()); } diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java index 761c718b865..192ad7ae5db 100644 --- a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java @@ -165,6 +165,13 @@ public static X509Certificate newCert( public static X509Certificate newCert( X509Certificate caCert, KeyPair caKeyPair, X500Name certSubject, PublicKey certPublicKey, long expirationMillis, CertificateCustomization customization) throws Exception { + return newCert(caCert, caKeyPair, certSubject, certPublicKey, expirationMillis, + new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth}, customization); + } + + public static X509Certificate newCert( + X509Certificate caCert, KeyPair caKeyPair, X500Name certSubject, PublicKey certPublicKey, + long expirationMillis, KeyPurposeId[] keyPurposes, CertificateCustomization customization) throws Exception { if (!caKeyPair.getPublic().equals(caCert.getPublicKey())) { throw new IllegalArgumentException("CA private key does not match the public key in the CA cert"); } @@ -175,7 +182,7 @@ public static X509Certificate newCert( builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); // not a CA builder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment)); - builder.addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth})); + builder.addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(keyPurposes)); builder.addExtension(Extension.subjectAlternativeName, false, getLocalhostSubjectAltNames()); if (customization != null) { @@ -184,6 +191,25 @@ public static X509Certificate newCert( return buildAndSignCertificate(caKeyPair.getPrivate(), builder); } + public static X509Certificate newServerOnlyCert(X509Certificate caCert, KeyPair caKeyPair, + String name, PublicKey certPublicKey) throws Exception { + return createSinglePurposeCert(name, caCert, caKeyPair, certPublicKey, KeyPurposeId.id_kp_serverAuth); + } + + public static X509Certificate newClientOnlyCert(X509Certificate caCert, KeyPair caKeyPair, + String name, PublicKey certPublicKey) throws Exception { + return createSinglePurposeCert(name, caCert, caKeyPair, certPublicKey, KeyPurposeId.id_kp_clientAuth); + } + + private static X509Certificate createSinglePurposeCert( + String name, X509Certificate caCert, KeyPair caKeyPair, PublicKey certPublicKey, KeyPurposeId keyPurposeId) + throws Exception { + X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); + nameBuilder.addRDN(BCStyle.CN, name); + return newCert(caCert, caKeyPair, nameBuilder.build(), certPublicKey, Duration.ofDays(1).toMillis(), + new KeyPurposeId[]{keyPurposeId}, null); + } + /** * Returns subject alternative names for "localhost". * @return the subject alternative names for "localhost". 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 f76b91ddf9f..23c47b67466 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 @@ -23,18 +23,23 @@ import static org.apache.zookeeper.common.X509Util.TLS_1_3; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; 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.File; +import java.io.FileOutputStream; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.nio.file.Path; +import java.security.KeyPair; import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; @@ -953,4 +958,355 @@ private void testCreateSSLContext_withWrongPasswordFromFile(final String keyPass x509Util.getDefaultSSLContext(); }); } + + @ParameterizedTest + @MethodSource("data") + @Timeout(value = 5) + public void testSeparateClientKeyStore_backwardCompat( + X509KeyType caKeyType, X509KeyType certKeyType, String keyPassword, Integer paramIndex) + throws Exception { + init(caKeyType, certKeyType, keyPassword, paramIndex); + // No clientKeyStore properties set — should behave identically to existing code + SSLContext ctx = x509Util.getDefaultSSLContext(); + assertNotNull(ctx); + } + + @ParameterizedTest + @MethodSource("data") + @Timeout(value = 15) + public void testSeparateClientKeyStore_singleEkuCertsHandshake( + X509KeyType caKeyType, X509KeyType certKeyType, String keyPassword, Integer paramIndex) + throws Exception { + init(caKeyType, certKeyType, keyPassword, paramIndex); + // Generate separate server-only and client-only certs + KeyPair serverKeyPair = X509TestHelpers.generateKeyPair(certKeyType); + X509Certificate serverCert = X509TestHelpers.newServerOnlyCert( + x509TestContext.getTrustStoreCertificates().get(0), + x509TestContext.getTrustStoreKeyPair(), + "server", serverKeyPair.getPublic()); + + KeyPair clientKeyPair = X509TestHelpers.generateKeyPair(certKeyType); + X509Certificate clientCert = X509TestHelpers.newClientOnlyCert( + x509TestContext.getTrustStoreCertificates().get(0), + x509TestContext.getTrustStoreKeyPair(), + "client", clientKeyPair.getPublic()); + + // Write server keystore + File serverKsFile = File.createTempFile("server_ks", ".jks", x509TestContext.getTempDir()); + serverKsFile.deleteOnExit(); + try (FileOutputStream fos = new FileOutputStream(serverKsFile)) { + fos.write(X509TestHelpers.certAndPrivateKeyToJavaKeyStoreBytes( + serverCert, serverKeyPair.getPrivate(), keyPassword)); + } + + // Write client keystore + File clientKsFile = File.createTempFile("client_ks", ".jks", x509TestContext.getTempDir()); + clientKsFile.deleteOnExit(); + try (FileOutputStream fos = new FileOutputStream(clientKsFile)) { + fos.write(X509TestHelpers.certAndPrivateKeyToJavaKeyStoreBytes( + clientCert, clientKeyPair.getPrivate(), keyPassword)); + } + + // Set properties: server keystore as keyStore, client keystore as clientKeyStore + System.setProperty(x509Util.getSslKeystoreLocationProperty(), serverKsFile.getAbsolutePath()); + System.setProperty(x509Util.getSslKeystorePasswdProperty(), keyPassword); + System.setProperty(x509Util.getSslKeystoreTypeProperty(), "JKS"); + System.setProperty(x509Util.getSslClientKeystoreLocationProperty(), clientKsFile.getAbsolutePath()); + System.setProperty(x509Util.getSslClientKeystorePasswdProperty(), keyPassword); + System.setProperty(x509Util.getSslClientKeystoreTypeProperty(), "JKS"); + + x509Util.close(); // reset cached context + x509Util = new ClientX509Util(); + + // Create server socket and client socket, perform handshake + int port = PortAssignment.unique(); + SSLServerSocket serverSocket = x509Util.createSSLServerSocket(port); + serverSocket.setSoTimeout(5000); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + Future serverFuture = executor.submit(() -> { + try (SSLSocket accepted = (SSLSocket) serverSocket.accept()) { + accepted.startHandshake(); + } + return null; + }); + + SSLSocket clientSocket = x509Util.createSSLSocket(); + clientSocket.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), port), 3000); + clientSocket.startHandshake(); + clientSocket.close(); + + serverFuture.get(5, TimeUnit.SECONDS); + } finally { + serverSocket.close(); + executor.shutdownNow(); + System.clearProperty(x509Util.getSslClientKeystoreLocationProperty()); + System.clearProperty(x509Util.getSslClientKeystorePasswdProperty()); + System.clearProperty(x509Util.getSslClientKeystoreTypeProperty()); + } + } + + @ParameterizedTest + @MethodSource("data") + @Timeout(value = 5) + public void testSeparateClientKeyStore_nettyClientUsesClientKeyStore( + X509KeyType caKeyType, X509KeyType certKeyType, String keyPassword, Integer paramIndex) + throws Exception { + init(caKeyType, certKeyType, keyPassword, paramIndex); + // Generate separate keystores + KeyPair clientKeyPair = X509TestHelpers.generateKeyPair(certKeyType); + X509Certificate clientCert = X509TestHelpers.newClientOnlyCert( + x509TestContext.getTrustStoreCertificates().get(0), + x509TestContext.getTrustStoreKeyPair(), + "client", clientKeyPair.getPublic()); + + File clientKsFile = File.createTempFile("client_ks", ".jks", x509TestContext.getTempDir()); + clientKsFile.deleteOnExit(); + try (FileOutputStream fos = new FileOutputStream(clientKsFile)) { + fos.write(X509TestHelpers.certAndPrivateKeyToJavaKeyStoreBytes( + clientCert, clientKeyPair.getPrivate(), keyPassword)); + } + + System.setProperty(x509Util.getSslClientKeystoreLocationProperty(), clientKsFile.getAbsolutePath()); + System.setProperty(x509Util.getSslClientKeystorePasswdProperty(), keyPassword); + System.setProperty(x509Util.getSslClientKeystoreTypeProperty(), "JKS"); + + try { + // Netty client context should load from clientKeyStore + SslContext clientCtx = ((ClientX509Util) x509Util).createNettySslContextForClient(new ZKConfig()); + assertNotNull(clientCtx); + + // Netty server context should still load from keyStore (the main one) + SslContext serverCtx = ((ClientX509Util) x509Util).createNettySslContextForServer(new ZKConfig()); + assertNotNull(serverCtx); + } finally { + System.clearProperty(x509Util.getSslClientKeystoreLocationProperty()); + System.clearProperty(x509Util.getSslClientKeystorePasswdProperty()); + System.clearProperty(x509Util.getSslClientKeystoreTypeProperty()); + } + } + + @ParameterizedTest + @MethodSource("data") + @Timeout(value = 5) + public void testSeparateClientKeyStore_clientServerKeyManagerUsed( + X509KeyType caKeyType, X509KeyType certKeyType, String keyPassword, Integer paramIndex) + throws Exception { + init(caKeyType, certKeyType, keyPassword, paramIndex); + // Generate a separate client keystore + KeyPair clientKeyPair = X509TestHelpers.generateKeyPair(certKeyType); + X509Certificate clientCert = X509TestHelpers.newClientOnlyCert( + x509TestContext.getTrustStoreCertificates().get(0), + x509TestContext.getTrustStoreKeyPair(), + "client", clientKeyPair.getPublic()); + + File clientKsFile = File.createTempFile("client_ks", ".jks", x509TestContext.getTempDir()); + clientKsFile.deleteOnExit(); + try (FileOutputStream fos = new FileOutputStream(clientKsFile)) { + fos.write(X509TestHelpers.certAndPrivateKeyToJavaKeyStoreBytes( + clientCert, clientKeyPair.getPrivate(), keyPassword)); + } + + System.setProperty(x509Util.getSslClientKeystoreLocationProperty(), clientKsFile.getAbsolutePath()); + System.setProperty(x509Util.getSslClientKeystorePasswdProperty(), keyPassword); + System.setProperty(x509Util.getSslClientKeystoreTypeProperty(), "JKS"); + + x509Util.close(); + x509Util = new ClientX509Util(); + + try { + SSLContextAndOptions ctxAndOpts = x509Util.getDefaultSSLContextAndOptions(); + assertNotNull(ctxAndOpts); + // The SSLContext should be functional + assertNotNull(ctxAndOpts.getSSLContext()); + } finally { + System.clearProperty(x509Util.getSslClientKeystoreLocationProperty()); + System.clearProperty(x509Util.getSslClientKeystorePasswdProperty()); + System.clearProperty(x509Util.getSslClientKeystoreTypeProperty()); + } + } + + @ParameterizedTest + @MethodSource("data") + @Timeout(value = 5) + public void testSeparateServerTrustStore_backwardCompat( + X509KeyType caKeyType, X509KeyType certKeyType, String keyPassword, Integer paramIndex) + throws Exception { + init(caKeyType, certKeyType, keyPassword, paramIndex); + // Without server.trustStore configured, existing trustStore is used for both roles + x509Util.close(); + x509Util = new ClientX509Util(); + + int port = PortAssignment.unique(); + SSLServerSocket serverSocket = x509Util.createSSLServerSocket(port); + serverSocket.setSoTimeout(5000); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + Future serverFuture = executor.submit(() -> { + try (SSLSocket accepted = (SSLSocket) serverSocket.accept()) { + accepted.startHandshake(); + } + return null; + }); + + SSLSocket clientSocket = x509Util.createSSLSocket(); + clientSocket.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), port), 3000); + clientSocket.startHandshake(); + clientSocket.close(); + + serverFuture.get(5, TimeUnit.SECONDS); + } finally { + serverSocket.close(); + executor.shutdownNow(); + } + } + + @ParameterizedTest + @MethodSource("data") + @Timeout(value = 5) + public void testSeparateServerTrustStore_separateCAs( + X509KeyType caKeyType, X509KeyType certKeyType, String keyPassword, Integer paramIndex) + throws Exception { + init(caKeyType, certKeyType, keyPassword, paramIndex); + + // Create a separate CA for client certs + KeyPair clientCaKeyPair = X509TestHelpers.generateKeyPair(caKeyType); + X509Certificate clientCaCert = X509TestHelpers.newSelfSignedCACert( + new org.bouncycastle.asn1.x500.X500NameBuilder(org.bouncycastle.asn1.x500.style.BCStyle.INSTANCE) + .addRDN(org.bouncycastle.asn1.x500.style.BCStyle.CN, "Client CA") + .build(), + clientCaKeyPair, 86400000L); + + // The existing CA is used for server certs + KeyPair serverCaKeyPair = x509TestContext.getTrustStoreKeyPair(); + X509Certificate serverCaCert = x509TestContext.getTrustStoreCertificates().get(0); + + // Generate server cert signed by server CA + KeyPair serverKeyPair = X509TestHelpers.generateKeyPair(certKeyType); + X509Certificate serverCert = X509TestHelpers.newServerOnlyCert( + serverCaCert, serverCaKeyPair, "server", serverKeyPair.getPublic()); + + // Generate client cert signed by client CA + KeyPair clientKeyPair = X509TestHelpers.generateKeyPair(certKeyType); + X509Certificate clientCert = X509TestHelpers.newClientOnlyCert( + clientCaCert, clientCaKeyPair, "client", clientKeyPair.getPublic()); + + // Server keystore with server cert + File serverKsFile = File.createTempFile("server_ks", ".jks", x509TestContext.getTempDir()); + serverKsFile.deleteOnExit(); + try (FileOutputStream fos = new FileOutputStream(serverKsFile)) { + fos.write(X509TestHelpers.certAndPrivateKeyToJavaKeyStoreBytes( + serverCert, serverKeyPair.getPrivate(), keyPassword)); + } + + // Client keystore with client cert + File clientKsFile = File.createTempFile("client_ks", ".jks", x509TestContext.getTempDir()); + clientKsFile.deleteOnExit(); + try (FileOutputStream fos = new FileOutputStream(clientKsFile)) { + fos.write(X509TestHelpers.certAndPrivateKeyToJavaKeyStoreBytes( + clientCert, clientKeyPair.getPrivate(), keyPassword)); + } + + // Client truststore: trusts server CA (validates servers we connect to) + File clientTsFile = File.createTempFile("client_ts", ".jks", x509TestContext.getTempDir()); + clientTsFile.deleteOnExit(); + try (FileOutputStream fos = new FileOutputStream(clientTsFile)) { + fos.write(X509TestHelpers.certToJavaTrustStoreBytes(serverCaCert, keyPassword)); + } + + // Server truststore: trusts client CA (validates clients connecting to us) + File serverTsFile = File.createTempFile("server_ts", ".jks", x509TestContext.getTempDir()); + serverTsFile.deleteOnExit(); + try (FileOutputStream fos = new FileOutputStream(serverTsFile)) { + fos.write(X509TestHelpers.certToJavaTrustStoreBytes(clientCaCert, keyPassword)); + } + + System.setProperty(x509Util.getSslKeystoreLocationProperty(), serverKsFile.getAbsolutePath()); + System.setProperty(x509Util.getSslKeystorePasswdProperty(), keyPassword); + System.setProperty(x509Util.getSslKeystoreTypeProperty(), "JKS"); + System.setProperty(x509Util.getSslClientKeystoreLocationProperty(), clientKsFile.getAbsolutePath()); + System.setProperty(x509Util.getSslClientKeystorePasswdProperty(), keyPassword); + System.setProperty(x509Util.getSslClientKeystoreTypeProperty(), "JKS"); + System.setProperty(x509Util.getSslTruststoreLocationProperty(), clientTsFile.getAbsolutePath()); + System.setProperty(x509Util.getSslTruststorePasswdProperty(), keyPassword); + System.setProperty(x509Util.getSslTruststoreTypeProperty(), "JKS"); + System.setProperty(x509Util.getSslServerTruststoreLocationProperty(), serverTsFile.getAbsolutePath()); + System.setProperty(x509Util.getSslServerTruststorePasswdProperty(), keyPassword); + System.setProperty(x509Util.getSslServerTruststoreTypeProperty(), "JKS"); + + x509Util.close(); + x509Util = new ClientX509Util(); + + int port = PortAssignment.unique(); + SSLServerSocket serverSocket = x509Util.createSSLServerSocket(port); + serverSocket.setSoTimeout(5000); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + Future serverFuture = executor.submit(() -> { + try (SSLSocket accepted = (SSLSocket) serverSocket.accept()) { + accepted.startHandshake(); + } + return null; + }); + + SSLSocket clientSocket = x509Util.createSSLSocket(); + clientSocket.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), port), 3000); + clientSocket.startHandshake(); + clientSocket.close(); + + serverFuture.get(5, TimeUnit.SECONDS); + } finally { + serverSocket.close(); + executor.shutdownNow(); + System.clearProperty(x509Util.getSslClientKeystoreLocationProperty()); + System.clearProperty(x509Util.getSslClientKeystorePasswdProperty()); + System.clearProperty(x509Util.getSslClientKeystoreTypeProperty()); + System.clearProperty(x509Util.getSslServerTruststoreLocationProperty()); + System.clearProperty(x509Util.getSslServerTruststorePasswdProperty()); + System.clearProperty(x509Util.getSslServerTruststoreTypeProperty()); + } + } + + @ParameterizedTest + @MethodSource("data") + @Timeout(value = 5) + public void testSeparateServerTrustStore_clientServerTrustManagerUsed( + X509KeyType caKeyType, X509KeyType certKeyType, String keyPassword, Integer paramIndex) + throws Exception { + init(caKeyType, certKeyType, keyPassword, paramIndex); + + // Create a separate truststore for server role + KeyPair serverCaKeyPair = X509TestHelpers.generateKeyPair(caKeyType); + X509Certificate serverCaCert = X509TestHelpers.newSelfSignedCACert( + new org.bouncycastle.asn1.x500.X500NameBuilder(org.bouncycastle.asn1.x500.style.BCStyle.INSTANCE) + .addRDN(org.bouncycastle.asn1.x500.style.BCStyle.CN, "Server Trust CA") + .build(), + serverCaKeyPair, 86400000L); + + File serverTsFile = File.createTempFile("server_ts", ".jks", x509TestContext.getTempDir()); + serverTsFile.deleteOnExit(); + try (FileOutputStream fos = new FileOutputStream(serverTsFile)) { + fos.write(X509TestHelpers.certToJavaTrustStoreBytes(serverCaCert, keyPassword)); + } + + System.setProperty(x509Util.getSslServerTruststoreLocationProperty(), serverTsFile.getAbsolutePath()); + System.setProperty(x509Util.getSslServerTruststorePasswdProperty(), keyPassword); + System.setProperty(x509Util.getSslServerTruststoreTypeProperty(), "JKS"); + + x509Util.close(); + x509Util = new ClientX509Util(); + + try { + SSLContextAndOptions ctxAndOpts = x509Util.getDefaultSSLContextAndOptions(); + assertNotNull(ctxAndOpts); + assertNotNull(ctxAndOpts.getSSLContext()); + } finally { + System.clearProperty(x509Util.getSslServerTruststoreLocationProperty()); + System.clearProperty(x509Util.getSslServerTruststorePasswdProperty()); + System.clearProperty(x509Util.getSslServerTruststoreTypeProperty()); + } + } } From d056eeb017f3bb5b34fe85e5ca3946c6fa094f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Paksy?= Date: Tue, 18 Aug 2026 15:50:01 +0200 Subject: [PATCH 2/2] ZOOKEEPER-5070: Use two SSLContexts for FIPS-compatible single-EKU support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace custom ClientServerX509KeyManager and ClientServerX509TrustManager wrappers with two separate SSLContext instances — one for client role (outgoing connections) and one for server role (incoming connections). The custom wrappers were passed to SSLContext.init(), which is rejected by JVM-level FIPS providers that only accept their own validated manager implementations. The new approach initializes each context with standard PKIX managers from their respective keystores, making single-EKU certificate support fully FIPS-compatible. Co-Authored-By: Claude Opus 4.6 --- .../common/ClientServerX509KeyManager.java | 131 -------- .../common/ClientServerX509TrustManager.java | 97 ------ .../common/SSLContextAndOptions.java | 44 ++- .../org/apache/zookeeper/common/X509Util.java | 103 +++--- .../ClientServerX509KeyManagerTest.java | 308 ------------------ .../ClientServerX509TrustManagerTest.java | 240 -------------- .../apache/zookeeper/common/X509UtilTest.java | 79 ----- 7 files changed, 84 insertions(+), 918 deletions(-) delete mode 100644 zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509KeyManager.java delete mode 100644 zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509TrustManager.java delete mode 100644 zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509KeyManagerTest.java delete mode 100644 zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509TrustManagerTest.java diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509KeyManager.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509KeyManager.java deleted file mode 100644 index 9c0f1c66652..00000000000 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509KeyManager.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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 java.net.Socket; -import java.security.Principal; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.X509ExtendedKeyManager; -import javax.net.ssl.X509KeyManager; - -/** - * An {@link X509ExtendedKeyManager} that delegates client-mode and server-mode - * key selection to separate underlying key managers. This allows a ZooKeeper - * node to use different keystores (and therefore different certificates) for - * its client role (outgoing connections) and server role (incoming connections), - * enabling the use of certificates with a single Extended Key Usage (EKU). - */ -public class ClientServerX509KeyManager extends X509ExtendedKeyManager { - - private static final String CLIENT_PREFIX = "client:"; - private static final String SERVER_PREFIX = "server:"; - - private final X509KeyManager clientKeyManager; - private final X509KeyManager serverKeyManager; - - public ClientServerX509KeyManager(X509KeyManager clientKeyManager, X509KeyManager serverKeyManager) { - this.clientKeyManager = clientKeyManager; - this.serverKeyManager = serverKeyManager; - } - - @Override - public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { - String alias = clientKeyManager.chooseClientAlias(keyType, issuers, socket); - return alias != null ? CLIENT_PREFIX + alias : null; - } - - @Override - public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { - String alias = serverKeyManager.chooseServerAlias(keyType, issuers, socket); - return alias != null ? SERVER_PREFIX + alias : null; - } - - @Override - public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) { - if (clientKeyManager instanceof X509ExtendedKeyManager) { - String alias = ((X509ExtendedKeyManager) clientKeyManager) - .chooseEngineClientAlias(keyType, issuers, engine); - return alias != null ? CLIENT_PREFIX + alias : null; - } - return chooseClientAlias(keyType, issuers, null); - } - - @Override - public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) { - if (serverKeyManager instanceof X509ExtendedKeyManager) { - String alias = ((X509ExtendedKeyManager) serverKeyManager) - .chooseEngineServerAlias(keyType, issuers, engine); - return alias != null ? SERVER_PREFIX + alias : null; - } - return chooseServerAlias(keyType, issuers, null); - } - - @Override - public X509Certificate[] getCertificateChain(String alias) { - if (alias == null) { - return null; - } - if (alias.startsWith(CLIENT_PREFIX)) { - return clientKeyManager.getCertificateChain(alias.substring(CLIENT_PREFIX.length())); - } - if (alias.startsWith(SERVER_PREFIX)) { - return serverKeyManager.getCertificateChain(alias.substring(SERVER_PREFIX.length())); - } - return serverKeyManager.getCertificateChain(alias); - } - - @Override - public PrivateKey getPrivateKey(String alias) { - if (alias == null) { - return null; - } - if (alias.startsWith(CLIENT_PREFIX)) { - return clientKeyManager.getPrivateKey(alias.substring(CLIENT_PREFIX.length())); - } - if (alias.startsWith(SERVER_PREFIX)) { - return serverKeyManager.getPrivateKey(alias.substring(SERVER_PREFIX.length())); - } - return serverKeyManager.getPrivateKey(alias); - } - - @Override - public String[] getClientAliases(String keyType, Principal[] issuers) { - String[] aliases = clientKeyManager.getClientAliases(keyType, issuers); - return prefixAliases(aliases, CLIENT_PREFIX); - } - - @Override - public String[] getServerAliases(String keyType, Principal[] issuers) { - String[] aliases = serverKeyManager.getServerAliases(keyType, issuers); - return prefixAliases(aliases, SERVER_PREFIX); - } - - private static String[] prefixAliases(String[] aliases, String prefix) { - if (aliases == null) { - return null; - } - String[] prefixed = new String[aliases.length]; - for (int i = 0; i < aliases.length; i++) { - prefixed[i] = prefix + aliases[i]; - } - return prefixed; - } -} diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509TrustManager.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509TrustManager.java deleted file mode 100644 index 81a60b13f9c..00000000000 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/ClientServerX509TrustManager.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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 java.net.Socket; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.X509ExtendedTrustManager; - -/** - * An {@link X509ExtendedTrustManager} that delegates to separate trust managers for - * client and server certificate validation. This allows configuring different truststores - * for validating client certificates (when acting as a server) vs. server certificates - * (when acting as a client). - * - *
    - *
  • {@code checkServerTrusted} — delegates to the client trust manager - * (validates server certs when this node connects as a client)
  • - *
  • {@code checkClientTrusted} — delegates to the server trust manager - * (validates client certs when this node accepts connections as a server)
  • - *
- */ -public class ClientServerX509TrustManager extends X509ExtendedTrustManager { - - private final X509ExtendedTrustManager clientTrustManager; - private final X509ExtendedTrustManager serverTrustManager; - - /** - * @param clientTrustManager used to validate server certificates (when acting as TLS client) - * @param serverTrustManager used to validate client certificates (when acting as TLS server) - */ - public ClientServerX509TrustManager(X509ExtendedTrustManager clientTrustManager, - X509ExtendedTrustManager serverTrustManager) { - this.clientTrustManager = clientTrustManager; - this.serverTrustManager = serverTrustManager; - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - serverTrustManager.checkClientTrusted(chain, authType); - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { - serverTrustManager.checkClientTrusted(chain, authType, socket); - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { - serverTrustManager.checkClientTrusted(chain, authType, engine); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - clientTrustManager.checkServerTrusted(chain, authType); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { - clientTrustManager.checkServerTrusted(chain, authType, socket); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { - clientTrustManager.checkServerTrusted(chain, authType, engine); - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - X509Certificate[] clientIssuers = clientTrustManager.getAcceptedIssuers(); - X509Certificate[] serverIssuers = serverTrustManager.getAcceptedIssuers(); - List combined = new ArrayList<>(clientIssuers.length + serverIssuers.length); - combined.addAll(Arrays.asList(clientIssuers)); - combined.addAll(Arrays.asList(serverIssuers)); - return combined.toArray(new X509Certificate[0]); - } -} diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/SSLContextAndOptions.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/SSLContextAndOptions.java index c712f6acb6c..f45761fa396 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/SSLContextAndOptions.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/SSLContextAndOptions.java @@ -31,7 +31,7 @@ import org.slf4j.LoggerFactory; /** - * Wrapper class for an SSLContext + some config options that can't be set on the context when it is created but + * Wrapper class for SSLContexts and config options that can't be set on the context when it is created but * must be set on a secure socket created by the context after the socket creation. By wrapping the options in this * class we avoid reading from global system properties during socket configuration. This makes testing easier * since we can create different X509Util instances with different configurations in a single test process, and @@ -45,52 +45,68 @@ public class SSLContextAndOptions { private final String[] enabledProtocols; private final String[] cipherSuites; private final X509Util.ClientAuth clientAuth; - private final SSLContext sslContext; + private final SSLContext clientSSLContext; + private final SSLContext serverSSLContext; private final int handshakeDetectionTimeoutMillis; - /** + * Creates an SSLContextAndOptions with separate client and server contexts. * Note: constructor is intentionally package-private, only the X509Util class should be creating instances of this * class. * @param x509Util the X509Util that created this object. * @param config a ZKConfig that holds config properties. - * @param sslContext the SSLContext. + * @param clientSSLContext the SSLContext for connecting as a client. + * @param serverSSLContext the SSLContext for accepting connections as a server. */ - SSLContextAndOptions(final X509Util x509Util, final ZKConfig config, final SSLContext sslContext) { + SSLContextAndOptions(final X509Util x509Util, final ZKConfig config, + final SSLContext clientSSLContext, final SSLContext serverSSLContext) { this.x509Util = requireNonNull(x509Util); - this.sslContext = requireNonNull(sslContext); - this.enabledProtocols = getEnabledProtocols(requireNonNull(config), sslContext); + this.clientSSLContext = requireNonNull(clientSSLContext); + this.serverSSLContext = requireNonNull(serverSSLContext); + this.enabledProtocols = getEnabledProtocols(requireNonNull(config), clientSSLContext); this.cipherSuites = getCipherSuites(config); this.clientAuth = getClientAuth(config); this.handshakeDetectionTimeoutMillis = getHandshakeDetectionTimeoutMillis(config); } - public SSLContext getSSLContext() { - return sslContext; + /** + * Creates an SSLContextAndOptions with a single context used for both roles. + * Note: constructor is intentionally package-private, only the X509Util class should be creating instances of this + * class. + * @param x509Util the X509Util that created this object. + * @param config a ZKConfig that holds config properties. + * @param sslContext the SSLContext. + */ + SSLContextAndOptions(final X509Util x509Util, final ZKConfig config, final SSLContext sslContext) { + this(x509Util, config, sslContext, sslContext); + } + + public SSLContext getClientSSLContext() { + return clientSSLContext; } public SSLSocket createSSLSocket() throws IOException { - return configureSSLSocket((SSLSocket) sslContext.getSocketFactory().createSocket(), true); + return configureSSLSocket((SSLSocket) clientSSLContext.getSocketFactory().createSocket(), true); } public SSLSocket createSSLSocket(Socket socket, byte[] pushbackBytes) throws IOException { SSLSocket sslSocket; if (pushbackBytes != null && pushbackBytes.length > 0) { - sslSocket = (SSLSocket) sslContext.getSocketFactory() + sslSocket = (SSLSocket) serverSSLContext.getSocketFactory() .createSocket(socket, new ByteArrayInputStream(pushbackBytes), true); } else { - sslSocket = (SSLSocket) sslContext.getSocketFactory().createSocket(socket, null, socket.getPort(), true); + sslSocket = (SSLSocket) serverSSLContext.getSocketFactory().createSocket(socket, null, socket.getPort(), true); } return configureSSLSocket(sslSocket, false); } public SSLServerSocket createSSLServerSocket() throws IOException { - SSLServerSocket sslServerSocket = (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket(); + SSLServerSocket sslServerSocket = (SSLServerSocket) serverSSLContext.getServerSocketFactory().createServerSocket(); return configureSSLServerSocket(sslServerSocket); } public SSLServerSocket createSSLServerSocket(int port) throws IOException { - SSLServerSocket sslServerSocket = (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket(port); + SSLServerSocket sslServerSocket = (SSLServerSocket) serverSSLContext.getServerSocketFactory().createServerSocket(port); return configureSSLServerSocket(sslServerSocket); } 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 5156521466b..eb066048b85 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 @@ -348,11 +348,11 @@ public boolean allowReverseDnsLookup(ZKConfig config) { } public SSLContext getDefaultSSLContext() throws X509Exception.SSLContextException { - return getDefaultSSLContextAndOptions().getSSLContext(); + return getDefaultSSLContextAndOptions().getClientSSLContext(); } public SSLContext createSSLContext(ZKConfig config) throws SSLContextException { - return createSSLContextAndOptions(config).getSSLContext(); + return createSSLContextAndOptions(config).getClientSSLContext(); } public SSLContextAndOptions getDefaultSSLContextAndOptions() throws X509Exception.SSLContextException { @@ -453,11 +453,18 @@ public SSLContextAndOptions createSSLContextAndOptionsFromConfig(ZKConfig config // 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. - KeyManager[] keyManagers = null; - TrustManager[] trustManagers = null; + String defaultTlsProtocol = defaultTlsProtocol(config); + String protocol = config.getProperty(sslProtocolProperty, defaultTlsProtocol); + + // Client context — used for outgoing connections (client role) + KeyManager[] clientKeyManagers = null; + TrustManager[] clientTrustManagers = null; try { - keyManagers = buildKeyManagers(config, keyManagers); + X509KeyManager clientKm = buildClientKeyManager(config); + if (clientKm != null) { + clientKeyManagers = new KeyManager[]{clientKm}; + } } catch (KeyManagerException keyManagerException) { throw new SSLContextException("Failed to create KeyManager", keyManagerException); } catch (IllegalArgumentException e) { @@ -466,45 +473,61 @@ public SSLContextAndOptions createSSLContextAndOptionsFromConfig(ZKConfig config } try { - trustManagers = buildTrustManagers(config); + X509TrustManager clientTm = buildTrustManager(config); + if (clientTm != null) { + clientTrustManagers = new TrustManager[]{clientTm}; + } } catch (TrustManagerException trustManagerException) { throw new SSLContextException("Failed to create TrustManager", trustManagerException); } catch (IllegalArgumentException e) { String trustStoreTypeProp = config.getProperty(sslTruststoreTypeProperty); throw new SSLContextException("Bad value for " - + sslTruststoreTypeProperty - + ": " - + trustStoreTypeProp, e); + + sslTruststoreTypeProperty + ": " + trustStoreTypeProp, e); } - String defaultTlsProtocol = defaultTlsProtocol(config); - String protocol = config.getProperty(sslProtocolProperty, defaultTlsProtocol); + + // Server context — used for incoming connections (server role) + KeyManager[] serverKeyManagers = null; + TrustManager[] serverTrustManagers = null; + try { - SSLContext sslContext = SSLContext.getInstance(protocol); - sslContext.init(keyManagers, trustManagers, null); - return new SSLContextAndOptions(this, config, sslContext); - } catch (NoSuchAlgorithmException | KeyManagementException sslContextInitException) { - throw new SSLContextException(sslContextInitException); + X509KeyManager serverKm = buildKeyManager(config); + if (serverKm != null) { + serverKeyManagers = new KeyManager[]{serverKm}; + } + } catch (KeyManagerException keyManagerException) { + throw new SSLContextException("Failed to create KeyManager", keyManagerException); + } catch (IllegalArgumentException e) { + String keyStoreTypeProp = config.getProperty(sslKeystoreTypeProperty); + throw new SSLContextException("Bad value for " + sslKeystoreTypeProperty + ": " + keyStoreTypeProp, e); } - } - private KeyManager[] buildKeyManagers(ZKConfig config, KeyManager[] keyManagers) throws KeyManagerException { - String clientKeyStoreLocation = config.getProperty(sslClientKeystoreLocationProperty, ""); - X509KeyManager serverKm = buildKeyManager(config); - if (!clientKeyStoreLocation.isEmpty()) { - X509KeyManager clientKm = buildClientKeyManager(config); - if (serverKm != null && clientKm != null) { - keyManagers = new KeyManager[]{new ClientServerX509KeyManager(clientKm, serverKm)}; - } else if (clientKm != null) { - keyManagers = new KeyManager[]{clientKm}; - } else if (serverKm != null) { - keyManagers = new KeyManager[]{serverKm}; + try { + X509TrustManager serverTm = buildServerTrustManager(config); + if (serverTm != null) { + serverTrustManagers = new TrustManager[]{serverTm}; } - } else if (serverKm != null) { - keyManagers = new KeyManager[]{serverKm}; + } catch (TrustManagerException trustManagerException) { + throw new SSLContextException("Failed to create TrustManager", trustManagerException); + } catch (IllegalArgumentException e) { + String trustStoreTypeProp = config.getProperty(sslServerTruststoreTypeProperty); + throw new SSLContextException("Bad value for " + + sslServerTruststoreTypeProperty + ": " + trustStoreTypeProp, e); + } + + try { + SSLContext clientCtx = SSLContext.getInstance(protocol); + clientCtx.init(clientKeyManagers, clientTrustManagers, null); + + SSLContext serverCtx = SSLContext.getInstance(protocol); + serverCtx.init(serverKeyManagers, serverTrustManagers, null); + + return new SSLContextAndOptions(this, config, clientCtx, serverCtx); + } catch (NoSuchAlgorithmException | KeyManagementException sslContextInitException) { + throw new SSLContextException(sslContextInitException); } - return keyManagers; } + public static KeyStore loadKeyStore( String keyStoreLocation, String keyStorePassword, @@ -660,24 +683,6 @@ private X509TrustManager buildX509TrustManager(ZKConfig config, String trustStor fipsMode); } - private TrustManager[] buildTrustManagers(ZKConfig config) throws TrustManagerException { - String serverTrustStoreLocation = config.getProperty(sslServerTruststoreLocationProperty, ""); - X509TrustManager clientTm = buildTrustManager(config); - if (!serverTrustStoreLocation.isEmpty()) { - X509TrustManager serverTm = buildServerTrustManager(config); - if (clientTm != null && serverTm != null) { - return new TrustManager[]{new ClientServerX509TrustManager( - (X509ExtendedTrustManager) clientTm, (X509ExtendedTrustManager) serverTm)}; - } else if (serverTm != null) { - return new TrustManager[]{serverTm}; - } else if (clientTm != null) { - return new TrustManager[]{clientTm}; - } - } else if (clientTm != null) { - return new TrustManager[]{clientTm}; - } - return null; - } // @VisibleForTesting protected X509TrustManager createTrustManagerInternal( diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509KeyManagerTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509KeyManagerTest.java deleted file mode 100644 index 5e04b0b8a42..00000000000 --- a/zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509KeyManagerTest.java +++ /dev/null @@ -1,308 +0,0 @@ -/* - * 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 static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import java.net.Socket; -import java.security.KeyPair; -import java.security.Principal; -import java.security.PrivateKey; -import java.security.Security; -import java.security.cert.X509Certificate; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.X509ExtendedKeyManager; -import javax.net.ssl.X509KeyManager; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -public class ClientServerX509KeyManagerTest { - - private static KeyPair clientKeyPair; - private static X509Certificate clientCert; - private static KeyPair serverKeyPair; - private static X509Certificate serverCert; - - @BeforeAll - public static void setUp() throws Exception { - Security.addProvider(new BouncyCastleProvider()); - KeyPair caKeyPair = X509TestHelpers.generateKeyPair(X509KeyType.RSA); - X509Certificate caCert = X509TestHelpers.newSelfSignedCACert( - new org.bouncycastle.asn1.x500.X500NameBuilder(org.bouncycastle.asn1.x500.style.BCStyle.INSTANCE) - .addRDN(org.bouncycastle.asn1.x500.style.BCStyle.CN, "Test CA") - .build(), - caKeyPair, 86400000L); - clientKeyPair = X509TestHelpers.generateKeyPair(X509KeyType.RSA); - clientCert = X509TestHelpers.newClientOnlyCert(caCert, caKeyPair, "client", clientKeyPair.getPublic()); - serverKeyPair = X509TestHelpers.generateKeyPair(X509KeyType.RSA); - serverCert = X509TestHelpers.newServerOnlyCert(caCert, caKeyPair, "server", serverKeyPair.getPublic()); - } - - @AfterAll - public static void tearDown() { - Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); - } - - @Test - public void testChooseClientAliasDelegatesToClientKeyManager() { - X509KeyManager clientKm = new StubKeyManager("clientAlias", clientCert, clientKeyPair.getPrivate()); - X509KeyManager serverKm = new StubKeyManager("serverAlias", serverCert, serverKeyPair.getPrivate()); - ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); - - String alias = keyManager.chooseClientAlias(new String[]{"RSA"}, null, null); - assertNotNull(alias); - assertEquals("client:clientAlias", alias); - } - - @Test - public void testChooseServerAliasDelegatesToServerKeyManager() { - X509KeyManager clientKm = new StubKeyManager("clientAlias", clientCert, clientKeyPair.getPrivate()); - X509KeyManager serverKm = new StubKeyManager("serverAlias", serverCert, serverKeyPair.getPrivate()); - ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); - - String alias = keyManager.chooseServerAlias("RSA", null, null); - assertNotNull(alias); - assertEquals("server:serverAlias", alias); - } - - @Test - public void testGetCertificateChainRoutesToClientManager() { - X509KeyManager clientKm = new StubKeyManager("myAlias", clientCert, clientKeyPair.getPrivate()); - X509KeyManager serverKm = new StubKeyManager("myAlias", serverCert, serverKeyPair.getPrivate()); - ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); - - X509Certificate[] chain = keyManager.getCertificateChain("client:myAlias"); - assertNotNull(chain); - assertEquals(1, chain.length); - assertEquals(clientCert, chain[0]); - } - - @Test - public void testGetCertificateChainRoutesToServerManager() { - X509KeyManager clientKm = new StubKeyManager("myAlias", clientCert, clientKeyPair.getPrivate()); - X509KeyManager serverKm = new StubKeyManager("myAlias", serverCert, serverKeyPair.getPrivate()); - ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); - - X509Certificate[] chain = keyManager.getCertificateChain("server:myAlias"); - assertNotNull(chain); - assertEquals(1, chain.length); - assertEquals(serverCert, chain[0]); - } - - @Test - public void testGetPrivateKeyRoutesToClientManager() { - X509KeyManager clientKm = new StubKeyManager("myAlias", clientCert, clientKeyPair.getPrivate()); - X509KeyManager serverKm = new StubKeyManager("myAlias", serverCert, serverKeyPair.getPrivate()); - ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); - - PrivateKey key = keyManager.getPrivateKey("client:myAlias"); - assertEquals(clientKeyPair.getPrivate(), key); - } - - @Test - public void testGetPrivateKeyRoutesToServerManager() { - X509KeyManager clientKm = new StubKeyManager("myAlias", clientCert, clientKeyPair.getPrivate()); - X509KeyManager serverKm = new StubKeyManager("myAlias", serverCert, serverKeyPair.getPrivate()); - ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); - - PrivateKey key = keyManager.getPrivateKey("server:myAlias"); - assertEquals(serverKeyPair.getPrivate(), key); - } - - @Test - public void testGetClientAliasesPrefixed() { - X509KeyManager clientKm = new StubKeyManager("a1", clientCert, clientKeyPair.getPrivate()); - X509KeyManager serverKm = new StubKeyManager("a2", serverCert, serverKeyPair.getPrivate()); - ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); - - String[] aliases = keyManager.getClientAliases("RSA", null); - assertNotNull(aliases); - assertEquals(1, aliases.length); - assertEquals("client:a1", aliases[0]); - } - - @Test - public void testGetServerAliasesPrefixed() { - X509KeyManager clientKm = new StubKeyManager("a1", clientCert, clientKeyPair.getPrivate()); - X509KeyManager serverKm = new StubKeyManager("a2", serverCert, serverKeyPair.getPrivate()); - ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); - - String[] aliases = keyManager.getServerAliases("RSA", null); - assertNotNull(aliases); - assertEquals(1, aliases.length); - assertEquals("server:a2", aliases[0]); - } - - @Test - public void testNullAliasReturnsNull() { - X509KeyManager clientKm = new StubKeyManager(null, clientCert, clientKeyPair.getPrivate()); - X509KeyManager serverKm = new StubKeyManager(null, serverCert, serverKeyPair.getPrivate()); - ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); - - assertNull(keyManager.chooseClientAlias(new String[]{"RSA"}, null, null)); - assertNull(keyManager.chooseServerAlias("RSA", null, null)); - assertNull(keyManager.getCertificateChain(null)); - assertNull(keyManager.getPrivateKey(null)); - } - - @Test - public void testUnprefixedAliasFallsBackToServerManager() { - X509KeyManager clientKm = new StubKeyManager("myAlias", clientCert, clientKeyPair.getPrivate()); - X509KeyManager serverKm = new StubKeyManager("myAlias", serverCert, serverKeyPair.getPrivate()); - ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); - - X509Certificate[] chain = keyManager.getCertificateChain("myAlias"); - assertNotNull(chain); - assertEquals(serverCert, chain[0]); - - PrivateKey key = keyManager.getPrivateKey("myAlias"); - assertEquals(serverKeyPair.getPrivate(), key); - } - - @Test - public void testChooseEngineClientAliasDelegatesToExtendedKeyManager() { - ExtendedStubKeyManager clientKm = new ExtendedStubKeyManager("engineClient", clientCert, clientKeyPair.getPrivate()); - X509KeyManager serverKm = new StubKeyManager("serverAlias", serverCert, serverKeyPair.getPrivate()); - ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); - - String alias = keyManager.chooseEngineClientAlias(new String[]{"RSA"}, null, null); - assertNotNull(alias); - assertEquals("client:engineClient", alias); - } - - @Test - public void testChooseEngineServerAliasDelegatesToExtendedKeyManager() { - X509KeyManager clientKm = new StubKeyManager("clientAlias", clientCert, clientKeyPair.getPrivate()); - ExtendedStubKeyManager serverKm = new ExtendedStubKeyManager("engineServer", serverCert, serverKeyPair.getPrivate()); - ClientServerX509KeyManager keyManager = new ClientServerX509KeyManager(clientKm, serverKm); - - String alias = keyManager.chooseEngineServerAlias("RSA", null, null); - assertNotNull(alias); - assertEquals("server:engineServer", alias); - } - - private static class StubKeyManager implements X509KeyManager { - private final String alias; - private final X509Certificate cert; - private final PrivateKey key; - - StubKeyManager(String alias, X509Certificate cert, PrivateKey key) { - this.alias = alias; - this.cert = cert; - this.key = key; - } - - @Override - public String[] getClientAliases(String keyType, Principal[] issuers) { - return alias != null ? new String[]{alias} : null; - } - - @Override - public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { - return alias; - } - - @Override - public String[] getServerAliases(String keyType, Principal[] issuers) { - return alias != null ? new String[]{alias} : null; - } - - @Override - public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { - return alias; - } - - @Override - public X509Certificate[] getCertificateChain(String alias) { - if (this.alias != null && this.alias.equals(alias)) { - return new X509Certificate[]{cert}; - } - return null; - } - - @Override - public PrivateKey getPrivateKey(String alias) { - if (this.alias != null && this.alias.equals(alias)) { - return key; - } - return null; - } - } - - private static class ExtendedStubKeyManager extends X509ExtendedKeyManager { - private final String alias; - private final X509Certificate cert; - private final PrivateKey key; - - ExtendedStubKeyManager(String alias, X509Certificate cert, PrivateKey key) { - this.alias = alias; - this.cert = cert; - this.key = key; - } - - @Override - public String[] getClientAliases(String keyType, Principal[] issuers) { - return alias != null ? new String[]{alias} : null; - } - - @Override - public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { - return alias; - } - - @Override - public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) { - return alias; - } - - @Override - public String[] getServerAliases(String keyType, Principal[] issuers) { - return alias != null ? new String[]{alias} : null; - } - - @Override - public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { - return alias; - } - - @Override - public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) { - return alias; - } - - @Override - public X509Certificate[] getCertificateChain(String alias) { - if (this.alias != null && this.alias.equals(alias)) { - return new X509Certificate[]{cert}; - } - return null; - } - - @Override - public PrivateKey getPrivateKey(String alias) { - if (this.alias != null && this.alias.equals(alias)) { - return key; - } - return null; - } - } -} diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509TrustManagerTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509TrustManagerTest.java deleted file mode 100644 index 24d903aef19..00000000000 --- a/zookeeper-server/src/test/java/org/apache/zookeeper/common/ClientServerX509TrustManagerTest.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * 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 static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import java.net.Socket; -import java.security.KeyPair; -import java.security.Security; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.X509ExtendedTrustManager; -import org.bouncycastle.asn1.x500.X500NameBuilder; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -public class ClientServerX509TrustManagerTest { - - private static X509Certificate clientCaCert; - private static X509Certificate clientCert; - - private static X509Certificate serverCaCert; - private static X509Certificate serverCert; - - @BeforeAll - public static void setUp() throws Exception { - Security.addProvider(new BouncyCastleProvider()); - - KeyPair clientCaKeyPair = X509TestHelpers.generateKeyPair(X509KeyType.RSA); - clientCaCert = X509TestHelpers.newSelfSignedCACert( - new X500NameBuilder(BCStyle.INSTANCE).addRDN(BCStyle.CN, "Client CA").build(), - clientCaKeyPair, 86400000L); - - KeyPair clientKeyPair = X509TestHelpers.generateKeyPair(X509KeyType.RSA); - clientCert = X509TestHelpers.newClientOnlyCert(clientCaCert, clientCaKeyPair, "client", clientKeyPair.getPublic()); - - KeyPair serverCaKeyPair = X509TestHelpers.generateKeyPair(X509KeyType.RSA); - serverCaCert = X509TestHelpers.newSelfSignedCACert( - new X500NameBuilder(BCStyle.INSTANCE).addRDN(BCStyle.CN, "Server CA").build(), - serverCaKeyPair, 86400000L); - - KeyPair serverKeyPair = X509TestHelpers.generateKeyPair(X509KeyType.RSA); - serverCert = X509TestHelpers.newServerOnlyCert(serverCaCert, serverCaKeyPair, "server", serverKeyPair.getPublic()); - } - - @AfterAll - public static void tearDown() { - Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); - } - - @Test - public void testCheckServerTrustedDelegatesToClientTrustManager() { - StubTrustManager clientTm = new StubTrustManager(serverCaCert); - StubTrustManager serverTm = new StubTrustManager(clientCaCert); - ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); - - assertDoesNotThrow(() -> trustManager.checkServerTrusted( - new X509Certificate[]{serverCert, serverCaCert}, "RSA")); - assertTrue(clientTm.checkServerTrustedCalled); - } - - @Test - public void testCheckClientTrustedDelegatesToServerTrustManager() { - StubTrustManager clientTm = new StubTrustManager(serverCaCert); - StubTrustManager serverTm = new StubTrustManager(clientCaCert); - ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); - - assertDoesNotThrow(() -> trustManager.checkClientTrusted( - new X509Certificate[]{clientCert, clientCaCert}, "RSA")); - assertTrue(serverTm.checkClientTrustedCalled); - } - - @Test - public void testCheckServerTrustedFailsWithWrongTrustManager() { - StubTrustManager clientTm = new StubTrustManager(clientCaCert); - StubTrustManager serverTm = new StubTrustManager(serverCaCert); - ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); - - assertThrows(CertificateException.class, () -> trustManager.checkServerTrusted( - new X509Certificate[]{serverCert, serverCaCert}, "RSA")); - } - - @Test - public void testCheckClientTrustedFailsWithWrongTrustManager() { - StubTrustManager clientTm = new StubTrustManager(clientCaCert); - StubTrustManager serverTm = new StubTrustManager(serverCaCert); - ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); - - assertThrows(CertificateException.class, () -> trustManager.checkClientTrusted( - new X509Certificate[]{clientCert, clientCaCert}, "RSA")); - } - - @Test - public void testCheckServerTrustedWithSocket() { - StubTrustManager clientTm = new StubTrustManager(serverCaCert); - StubTrustManager serverTm = new StubTrustManager(clientCaCert); - ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); - - assertDoesNotThrow(() -> trustManager.checkServerTrusted( - new X509Certificate[]{serverCert, serverCaCert}, "RSA", (Socket) null)); - assertTrue(clientTm.checkServerTrustedSocketCalled); - } - - @Test - public void testCheckClientTrustedWithSocket() { - StubTrustManager clientTm = new StubTrustManager(serverCaCert); - StubTrustManager serverTm = new StubTrustManager(clientCaCert); - ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); - - assertDoesNotThrow(() -> trustManager.checkClientTrusted( - new X509Certificate[]{clientCert, clientCaCert}, "RSA", (Socket) null)); - assertTrue(serverTm.checkClientTrustedSocketCalled); - } - - @Test - public void testCheckServerTrustedWithEngine() { - StubTrustManager clientTm = new StubTrustManager(serverCaCert); - StubTrustManager serverTm = new StubTrustManager(clientCaCert); - ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); - - assertDoesNotThrow(() -> trustManager.checkServerTrusted( - new X509Certificate[]{serverCert, serverCaCert}, "RSA", (SSLEngine) null)); - assertTrue(clientTm.checkServerTrustedEngineCalled); - } - - @Test - public void testCheckClientTrustedWithEngine() { - StubTrustManager clientTm = new StubTrustManager(serverCaCert); - StubTrustManager serverTm = new StubTrustManager(clientCaCert); - ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); - - assertDoesNotThrow(() -> trustManager.checkClientTrusted( - new X509Certificate[]{clientCert, clientCaCert}, "RSA", (SSLEngine) null)); - assertTrue(serverTm.checkClientTrustedEngineCalled); - } - - @Test - public void testGetAcceptedIssuersReturnsCombined() { - StubTrustManager clientTm = new StubTrustManager(serverCaCert); - StubTrustManager serverTm = new StubTrustManager(clientCaCert); - ClientServerX509TrustManager trustManager = new ClientServerX509TrustManager(clientTm, serverTm); - - X509Certificate[] issuers = trustManager.getAcceptedIssuers(); - assertEquals(2, issuers.length); - assertEquals(serverCaCert, issuers[0]); - assertEquals(clientCaCert, issuers[1]); - } - - private static class StubTrustManager extends X509ExtendedTrustManager { - private final X509Certificate trustedCa; - boolean checkClientTrustedCalled; - boolean checkServerTrustedCalled; - boolean checkClientTrustedSocketCalled; - boolean checkServerTrustedSocketCalled; - boolean checkClientTrustedEngineCalled; - boolean checkServerTrustedEngineCalled; - - StubTrustManager(X509Certificate trustedCa) { - this.trustedCa = trustedCa; - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - checkClientTrustedCalled = true; - verifyChain(chain); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - checkServerTrustedCalled = true; - verifyChain(chain); - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { - checkClientTrustedSocketCalled = true; - verifyChain(chain); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { - checkServerTrustedSocketCalled = true; - verifyChain(chain); - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { - checkClientTrustedEngineCalled = true; - verifyChain(chain); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { - checkServerTrustedEngineCalled = true; - verifyChain(chain); - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[]{trustedCa}; - } - - private void verifyChain(X509Certificate[] chain) throws CertificateException { - for (X509Certificate cert : chain) { - if (cert.equals(trustedCa)) { - return; - } - try { - cert.verify(trustedCa.getPublicKey()); - return; - } catch (Exception e) { - // continue checking - } - } - throw new CertificateException("Certificate chain not trusted"); - } - } -} 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 23c47b67466..d7f1354d2e8 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 @@ -1087,46 +1087,6 @@ public void testSeparateClientKeyStore_nettyClientUsesClientKeyStore( } } - @ParameterizedTest - @MethodSource("data") - @Timeout(value = 5) - public void testSeparateClientKeyStore_clientServerKeyManagerUsed( - X509KeyType caKeyType, X509KeyType certKeyType, String keyPassword, Integer paramIndex) - throws Exception { - init(caKeyType, certKeyType, keyPassword, paramIndex); - // Generate a separate client keystore - KeyPair clientKeyPair = X509TestHelpers.generateKeyPair(certKeyType); - X509Certificate clientCert = X509TestHelpers.newClientOnlyCert( - x509TestContext.getTrustStoreCertificates().get(0), - x509TestContext.getTrustStoreKeyPair(), - "client", clientKeyPair.getPublic()); - - File clientKsFile = File.createTempFile("client_ks", ".jks", x509TestContext.getTempDir()); - clientKsFile.deleteOnExit(); - try (FileOutputStream fos = new FileOutputStream(clientKsFile)) { - fos.write(X509TestHelpers.certAndPrivateKeyToJavaKeyStoreBytes( - clientCert, clientKeyPair.getPrivate(), keyPassword)); - } - - System.setProperty(x509Util.getSslClientKeystoreLocationProperty(), clientKsFile.getAbsolutePath()); - System.setProperty(x509Util.getSslClientKeystorePasswdProperty(), keyPassword); - System.setProperty(x509Util.getSslClientKeystoreTypeProperty(), "JKS"); - - x509Util.close(); - x509Util = new ClientX509Util(); - - try { - SSLContextAndOptions ctxAndOpts = x509Util.getDefaultSSLContextAndOptions(); - assertNotNull(ctxAndOpts); - // The SSLContext should be functional - assertNotNull(ctxAndOpts.getSSLContext()); - } finally { - System.clearProperty(x509Util.getSslClientKeystoreLocationProperty()); - System.clearProperty(x509Util.getSslClientKeystorePasswdProperty()); - System.clearProperty(x509Util.getSslClientKeystoreTypeProperty()); - } - } - @ParameterizedTest @MethodSource("data") @Timeout(value = 5) @@ -1270,43 +1230,4 @@ public void testSeparateServerTrustStore_separateCAs( } } - @ParameterizedTest - @MethodSource("data") - @Timeout(value = 5) - public void testSeparateServerTrustStore_clientServerTrustManagerUsed( - X509KeyType caKeyType, X509KeyType certKeyType, String keyPassword, Integer paramIndex) - throws Exception { - init(caKeyType, certKeyType, keyPassword, paramIndex); - - // Create a separate truststore for server role - KeyPair serverCaKeyPair = X509TestHelpers.generateKeyPair(caKeyType); - X509Certificate serverCaCert = X509TestHelpers.newSelfSignedCACert( - new org.bouncycastle.asn1.x500.X500NameBuilder(org.bouncycastle.asn1.x500.style.BCStyle.INSTANCE) - .addRDN(org.bouncycastle.asn1.x500.style.BCStyle.CN, "Server Trust CA") - .build(), - serverCaKeyPair, 86400000L); - - File serverTsFile = File.createTempFile("server_ts", ".jks", x509TestContext.getTempDir()); - serverTsFile.deleteOnExit(); - try (FileOutputStream fos = new FileOutputStream(serverTsFile)) { - fos.write(X509TestHelpers.certToJavaTrustStoreBytes(serverCaCert, keyPassword)); - } - - System.setProperty(x509Util.getSslServerTruststoreLocationProperty(), serverTsFile.getAbsolutePath()); - System.setProperty(x509Util.getSslServerTruststorePasswdProperty(), keyPassword); - System.setProperty(x509Util.getSslServerTruststoreTypeProperty(), "JKS"); - - x509Util.close(); - x509Util = new ClientX509Util(); - - try { - SSLContextAndOptions ctxAndOpts = x509Util.getDefaultSSLContextAndOptions(); - assertNotNull(ctxAndOpts); - assertNotNull(ctxAndOpts.getSSLContext()); - } finally { - System.clearProperty(x509Util.getSslServerTruststoreLocationProperty()); - System.clearProperty(x509Util.getSslServerTruststorePasswdProperty()); - System.clearProperty(x509Util.getSslServerTruststoreTypeProperty()); - } - } }