From f0277f2e88906c58907e495d445c41ce7fbff075 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] 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 1e50b84257c..0306848e833 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 @@ -65,7 +65,7 @@ public SslContext createNettySslContextForClient(ZKConfig config) throws X509Exception.KeyManagerException, X509Exception.TrustManagerException, SSLException { SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); - KeyManager km = buildKeyManager(config); + KeyManager km = buildClientKeyManager(config); if (km != null) { sslContextBuilder.keyManager(km); } @@ -102,7 +102,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 460a6f3ac55..8a0dd5373c5 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; } @@ -395,10 +440,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) { @@ -407,10 +449,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) { @@ -431,6 +470,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, @@ -489,6 +546,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. @@ -531,6 +604,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); @@ -554,6 +643,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, @@ -726,6 +834,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 @@ -735,6 +852,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(); + } } /** @@ -748,10 +874,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 660ca64bfdb..ca22afd91d9 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,17 +23,22 @@ 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.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; @@ -892,4 +897,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()); + } + } }