This is an automated email from the ASF dual-hosted git repository.
anmolnar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zookeeper.git
The following commit(s) were added to refs/heads/master by this push:
new ebe5fd79f ZOOKEEPER-5070: Support Single EKU certificates
ebe5fd79f is described below
commit ebe5fd79f9f6a29345f5de12982184537261cceb
Author: Dávid Paksy <[email protected]>
AuthorDate: Fri Sep 11 18:10:48 2026 +0200
ZOOKEEPER-5070: Support Single EKU certificates
Reviewers: anmolnar
Author: PDavid
Closes #2429 from PDavid/ZOOKEEPER-5070-single-eku
---
.../apache/zookeeper/common/ClientX509Util.java | 4 +-
.../zookeeper/common/SSLContextAndOptions.java | 43 +++-
.../java/org/apache/zookeeper/common/X509Util.java | 207 +++++++++++++--
.../java/org/apache/zookeeper/common/ZKConfig.java | 8 +
.../server/auth/X509AuthenticationProvider.java | 2 +-
.../apache/zookeeper/common/X509TestContext.java | 8 +
.../apache/zookeeper/common/X509TestHelpers.java | 28 ++-
.../org/apache/zookeeper/common/X509UtilTest.java | 280 +++++++++++++++++++++
.../org/apache/zookeeper/test/X509AuthTest.java | 80 ++++++
.../configuration-parameters.mdx | 107 +++++++-
10 files changed, 721 insertions(+), 46 deletions(-)
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 82b1bb3fd..1c1d6ca88 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/SSLContextAndOptions.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/common/SSLContextAndOptions.java
index 627cc17b3..3217ae2f6 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,53 +45,70 @@ 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;
private final ZKConfig zkConfig;
/**
+ * 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.clientSSLContext = requireNonNull(clientSSLContext);
+ this.serverSSLContext = requireNonNull(serverSSLContext);
this.zkConfig = requireNonNull(config);
- this.enabledProtocols = getEnabledProtocols(zkConfig, sslContext);
+ this.enabledProtocols = getEnabledProtocols(zkConfig,
clientSSLContext);
this.cipherSuites = getCipherSuites(zkConfig);
this.clientAuth = getClientAuth(zkConfig);
this.handshakeDetectionTimeoutMillis =
getHandshakeDetectionTimeoutMillis(zkConfig);
}
- 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 2a91e87d2..993404f78 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<SSLContextAndOptions>
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;
}
@@ -303,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 {
@@ -408,14 +453,16 @@ 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);
+
+ // Shared (common) key and trust managers — built once and reused as
+ // the fallback when no dedicated client/server stores are configured.
+ X509KeyManager sharedKeyManager;
+ X509TrustManager sharedTrustManager;
try {
- KeyManager km = buildKeyManager(config);
- if (km != null) {
- keyManagers = new KeyManager[]{km};
- }
+ sharedKeyManager = buildKeyManager(config);
} catch (KeyManagerException keyManagerException) {
throw new SSLContextException("Failed to create KeyManager",
keyManagerException);
} catch (IllegalArgumentException e) {
@@ -424,30 +471,70 @@ public SSLContextAndOptions
createSSLContextAndOptionsFromConfig(ZKConfig config
}
try {
- TrustManager tm = buildTrustManager(config);
- if (tm != null) {
- trustManagers = new TrustManager[]{tm};
- }
+ sharedTrustManager = buildTrustManager(config);
} 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);
+
+ // Client context - used for outgoing connections (client role)
+ KeyManager[] clientKeyManagers = null;
+ TrustManager[] clientTrustManagers = null;
+
+ try {
+ X509KeyManager clientKm = buildClientKeyManager(config,
sharedKeyManager);
+ if (clientKm != null) {
+ clientKeyManagers = new KeyManager[]{clientKm};
+ }
+ } catch (KeyManagerException keyManagerException) {
+ throw new SSLContextException("Failed to create KeyManager",
keyManagerException);
+ } catch (IllegalArgumentException e) {
+ String keyStoreTypeProp =
config.getProperty(sslClientKeystoreTypeProperty);
+ throw new SSLContextException("Bad value for " +
sslClientKeystoreTypeProperty + ": " + keyStoreTypeProp, e);
+ }
+
+ if (sharedTrustManager != null) {
+ clientTrustManagers = new TrustManager[]{sharedTrustManager};
+ }
+
+ // Server context - used for incoming connections (server role)
+ KeyManager[] serverKeyManagers = null;
+ TrustManager[] serverTrustManagers = null;
+
+ if (sharedKeyManager != null) {
+ serverKeyManagers = new KeyManager[]{sharedKeyManager};
+ }
+
try {
- SSLContext sslContext = SSLContext.getInstance(protocol);
- sslContext.init(keyManagers, trustManagers, null);
- return new SSLContextAndOptions(this, config, sslContext);
+ X509TrustManager serverTm = buildServerTrustManager(config,
sharedTrustManager);
+ if (serverTm != null) {
+ serverTrustManagers = new TrustManager[]{serverTm};
+ }
+ } 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);
}
}
+
public static KeyStore loadKeyStore(
String keyStoreLocation,
String keyStorePassword,
@@ -503,9 +590,38 @@ public X509KeyManager buildKeyManager(ZKConfig config)
throws KeyManagerExceptio
String keyStorePassword = getPasswordFromConfigPropertyOrFile(config,
getSslKeystorePasswdProperty(),
getSslKeystorePasswdPathProperty());
String keyStoreType = config.getProperty(getSslKeystoreTypeProperty());
+ LOG.debug("Using SSL keystore {}", keyStoreLocation);
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 {
+ return buildClientKeyManager(config, null);
+ }
+
+ /**
+ * 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, returns the pre-built {@code sharedKeyManager} (or falls back
+ * to {@link #buildKeyManager(ZKConfig)} when {@code sharedKeyManager} is
null).
+ */
+ public X509KeyManager buildClientKeyManager(ZKConfig config,
X509KeyManager sharedKeyManager) throws KeyManagerException {
+ String clientKeyStoreLocation =
config.getProperty(getSslClientKeystoreLocationProperty(), "");
+ if (!clientKeyStoreLocation.isEmpty()) {
+ String clientKeyStorePassword =
getPasswordFromConfigPropertyOrFile(config,
+ getSslClientKeystorePasswdProperty(),
getSslClientKeystorePasswdPathProperty());
+ String clientKeyStoreType =
config.getProperty(getSslClientKeystoreTypeProperty());
+ LOG.debug("Using SSL client keystore {}", clientKeyStoreLocation);
+ return createKeyManager(clientKeyStoreLocation,
clientKeyStorePassword, clientKeyStoreType);
+ }
+ LOG.debug("{} not specified for X509KeyManager, falling back to common
property", getSslClientKeystoreLocationProperty());
+ return sharedKeyManager != null ? sharedKeyManager :
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 +664,28 @@ public X509TrustManager buildTrustManager(ZKConfig config)
throws TrustManagerEx
return null;
}
+ LOG.debug("Using SSL trust store {}", trustStoreLocationProp);
+ return buildX509TrustManager(config, trustStoreLocationProp,
sslTruststorePasswdProperty, sslTruststorePasswdPathProperty,
sslTruststoreTypeProperty);
+ }
+
+ public X509TrustManager buildServerTrustManager(ZKConfig config) throws
TrustManagerException {
+ return buildServerTrustManager(config, null);
+ }
+
+ public X509TrustManager buildServerTrustManager(ZKConfig config,
X509TrustManager sharedTrustManager) throws TrustManagerException {
+ String serverTrustStoreLocation =
config.getProperty(sslServerTruststoreLocationProperty, "");
+ if (serverTrustStoreLocation.isEmpty()) {
+ LOG.debug("{} not specified for X509TrustManager, falling back to
common property", sslServerTruststoreLocationProperty);
+ return sharedTrustManager != null ? sharedTrustManager :
buildTrustManager(config);
+ }
+ LOG.debug("Using SSL server trust store {}", serverTrustStoreLocation);
+ 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 +709,7 @@ public X509TrustManager buildTrustManager(ZKConfig config)
throws TrustManagerEx
fipsMode);
}
+
// @VisibleForTesting
protected X509TrustManager createTrustManagerInternal(
String trustStoreLocation,
@@ -743,6 +882,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 +900,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 +922,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 442d8e722..a40bb3c53 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/main/java/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java
index eb9780053..48ec99450 100644
---
a/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java
+++
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java
@@ -89,7 +89,7 @@ public X509AuthenticationProvider() throws X509Exception {
}
try {
- tm = x509Util.buildTrustManager(config);
+ tm = x509Util.buildServerTrustManager(config);
} catch (TrustManagerException e) {
LOG.error("Failed to create trust manager", e);
}
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 80632e45c..45fad846d 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 761c718b8..192ad7ae5 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 f76b91ddf..9999e9fd6 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,279 @@ 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<Void> 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 {
+ executor.shutdownNow();
+ System.clearProperty(x509Util.getSslKeystoreLocationProperty());
+ System.clearProperty(x509Util.getSslKeystorePasswdProperty());
+ System.clearProperty(x509Util.getSslKeystoreTypeProperty());
+
System.clearProperty(x509Util.getSslClientKeystoreLocationProperty());
+
System.clearProperty(x509Util.getSslClientKeystorePasswdProperty());
+ System.clearProperty(x509Util.getSslClientKeystoreTypeProperty());
+ serverSocket.close();
+ }
+ }
+
+ @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 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<Void> 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<Void> 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());
+ }
+ }
+
}
diff --git
a/zookeeper-server/src/test/java/org/apache/zookeeper/test/X509AuthTest.java
b/zookeeper-server/src/test/java/org/apache/zookeeper/test/X509AuthTest.java
index ac097c99e..8281c6127 100644
--- a/zookeeper-server/src/test/java/org/apache/zookeeper/test/X509AuthTest.java
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/test/X509AuthTest.java
@@ -24,12 +24,16 @@
import jakarta.servlet.http.HttpServletRequest;
import java.math.BigInteger;
import java.net.Socket;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.security.InvalidKeyException;
+import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.PublicKey;
+import java.security.Security;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
@@ -45,11 +49,21 @@
import javax.security.auth.x500.X500Principal;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZKTestCase;
+import org.apache.zookeeper.common.ClientX509Util;
+import org.apache.zookeeper.common.X509KeyType;
+import org.apache.zookeeper.common.X509TestHelpers;
import org.apache.zookeeper.data.Id;
import org.apache.zookeeper.server.MockServerCnxn;
import org.apache.zookeeper.server.auth.X509AuthenticationProvider;
+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.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mockito;
public class X509AuthTest extends ZKTestCase {
@@ -58,6 +72,19 @@ public class X509AuthTest extends ZKTestCase {
private static TestCertificate superCert;
private static TestCertificate unknownCert;
+ @TempDir
+ public Path tempDir;
+
+ @BeforeAll
+ public static void setUpClass() {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ @AfterAll
+ public static void tearDownClass() {
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+ }
+
@BeforeEach
public void setUp() {
System.setProperty("zookeeper.X509AuthenticationProvider.superUser",
"CN=SUPER");
@@ -69,6 +96,18 @@ public void setUp() {
unknownCert = new TestCertificate("UNKNOWN");
}
+ @AfterEach
+ public void tearDown() {
+ try (ClientX509Util x509Util = new ClientX509Util()) {
+ System.clearProperty(x509Util.getSslTruststoreLocationProperty());
+ System.clearProperty(x509Util.getSslTruststorePasswdProperty());
+ System.clearProperty(x509Util.getSslTruststoreTypeProperty());
+
System.clearProperty(x509Util.getSslServerTruststoreLocationProperty());
+
System.clearProperty(x509Util.getSslServerTruststorePasswdProperty());
+
System.clearProperty(x509Util.getSslServerTruststoreTypeProperty());
+ }
+ }
+
@Test
public void testTrustedAuth() {
X509AuthenticationProvider provider = createProvider(clientCert);
@@ -123,6 +162,47 @@ public void testUntrustedAuth_HttpServletRequest() {
assertTrue(provider.handleAuthentication(mockRequest, null).isEmpty());
}
+ @Test
+ public void
testDefaultProviderUsesServerTrustStoreForClientAuthentication() throws
Exception {
+ final String password = "password";
+
+ KeyPair serverCaKeyPair =
X509TestHelpers.generateKeyPair(X509KeyType.RSA);
+ X509Certificate serverCaCert = X509TestHelpers.newSelfSignedCACert(
+ new X500NameBuilder(BCStyle.INSTANCE).addRDN(BCStyle.CN,
"Server CA").build(),
+ serverCaKeyPair,
+ 86400000L);
+
+ KeyPair clientCaKeyPair =
X509TestHelpers.generateKeyPair(X509KeyType.RSA);
+ X509Certificate clientCaCert = X509TestHelpers.newSelfSignedCACert(
+ new X500NameBuilder(BCStyle.INSTANCE).addRDN(BCStyle.CN,
"Client CA").build(),
+ clientCaKeyPair,
+ 86400000L);
+ KeyPair clientKeyPair =
X509TestHelpers.generateKeyPair(X509KeyType.RSA);
+ X509Certificate clientOnlyCert = X509TestHelpers.newClientOnlyCert(
+ clientCaCert, clientCaKeyPair, "CLIENT",
clientKeyPair.getPublic());
+
+ Path clientTrustStore = tempDir.resolve("client-truststore.jks");
+ Files.write(clientTrustStore,
X509TestHelpers.certToJavaTrustStoreBytes(serverCaCert, password));
+ Path serverTrustStore = tempDir.resolve("server-truststore.jks");
+ Files.write(serverTrustStore,
X509TestHelpers.certToJavaTrustStoreBytes(clientCaCert, password));
+
+ try (ClientX509Util x509Util = new ClientX509Util()) {
+ System.setProperty(x509Util.getSslTruststoreLocationProperty(),
clientTrustStore.toString());
+ System.setProperty(x509Util.getSslTruststorePasswdProperty(),
password);
+ System.setProperty(x509Util.getSslTruststoreTypeProperty(), "JKS");
+
System.setProperty(x509Util.getSslServerTruststoreLocationProperty(),
serverTrustStore.toString());
+
System.setProperty(x509Util.getSslServerTruststorePasswdProperty(), password);
+ System.setProperty(x509Util.getSslServerTruststoreTypeProperty(),
"JKS");
+ }
+
+ X509AuthenticationProvider provider = new X509AuthenticationProvider();
+ MockServerCnxn cnxn = new MockServerCnxn();
+ cnxn.clientChain = new X509Certificate[]{clientOnlyCert, clientCaCert};
+
+ assertEquals(KeeperException.Code.OK,
provider.handleAuthentication(cnxn, null));
+ assertEquals(Arrays.asList(new Id("x509", "CN=CLIENT")),
cnxn.getAuthInfo());
+ }
+
private static class TestPublicKey implements PublicKey {
private static final long serialVersionUID = 1L;
diff --git
a/zookeeper-website/app/pages/_docs/docs/_mdx/admin-ops/administrators-guide/configuration-parameters.mdx
b/zookeeper-website/app/pages/_docs/docs/_mdx/admin-ops/administrators-guide/configuration-parameters.mdx
index c493dc02a..7058b8426 100644
---
a/zookeeper-website/app/pages/_docs/docs/_mdx/admin-ops/administrators-guide/configuration-parameters.mdx
+++
b/zookeeper-website/app/pages/_docs/docs/_mdx/admin-ops/administrators-guide/configuration-parameters.mdx
@@ -1178,12 +1178,17 @@ server if this feature is enabled with sasl as
authentication scheme.
Enables encrypted quorum communication. Default is `false`. When enabling
this feature, please also consider enabling _leader.closeSocketAsync_
and _learner.closeSocketAsync_ to avoid issues associated with the
potentially long socket closing time when shutting down an SSL connection.
-- _ssl.keyStore.location and ssl.keyStore.password_ and
_ssl.quorum.keyStore.location_ and _ssl.quorum.keyStore.password_ :
- (Java system properties: **zookeeper.ssl.keyStore.location** and
**zookeeper.ssl.keyStore.password** and
**zookeeper.ssl.quorum.keyStore.location** and
**zookeeper.ssl.quorum.keyStore.password**)
+- _ssl.keyStore.location_ and _ssl.keyStore.password_ and
_ssl.quorum.keyStore.location_ and _ssl.quorum.keyStore.password_ :
+ (Java system properties: **zookeeper.ssl.keyStore.location** and
**zookeeper.ssl.keyStore.password** and
+ **zookeeper.ssl.quorum.keyStore.location** and
**zookeeper.ssl.quorum.keyStore.password**)
**New in 3.5.5:**
Specifies the file path to a Java keystore containing the local
- credentials to be used for client and quorum TLS connections, and the
- password to unlock the file.
+ credentials to be used for client-server and quorum TLS connections, and the
+ password to unlock the file. This keystore is used for both the server role
+ (accepting incoming connections) and the client role (initiating outgoing
+ connections). When separate `client.keyStore` properties are configured (see
+ below), this keystore is used only for the server role and the client role
+ uses the dedicated client keystore instead.
- _ssl.keyStore.passwordPath_ and _ssl.quorum.keyStore.passwordPath_ :
(Java system properties: **zookeeper.ssl.keyStore.passwordPath** and
**zookeeper.ssl.quorum.keyStore.passwordPath**)
@@ -1194,17 +1199,46 @@ server if this feature is enabled with sasl as
authentication scheme.
- _ssl.keyStore.type_ and _ssl.quorum.keyStore.type_ :
(Java system properties: **zookeeper.ssl.keyStore.type** and
**zookeeper.ssl.quorum.keyStore.type**)
**New in 3.5.5:**
- Specifies the file format of client and quorum keystores. Values: JKS, PEM,
PKCS12 or null (detect by filename).
+ Specifies the file format of client-server and quorum keystores. Values:
JKS, PEM, PKCS12 or null (detect by filename).
Default: null.
**New in 3.5.10, 3.6.3, 3.7.0:**
The format BCFKS was added.
+- _ssl.client.keyStore.location_ and _ssl.client.keyStore.password_ and
_ssl.quorum.client.keyStore.location_ and _ssl.quorum.client.keyStore.password_
:
+ (Java system properties: **zookeeper.ssl.client.keyStore.location** and
**zookeeper.ssl.client.keyStore.password** and
+ **zookeeper.ssl.quorum.client.keyStore.location** and
**zookeeper.ssl.quorum.client.keyStore.password**)
+ **New in 3.10.0:**
+ Specifies the file path to a Java keystore containing the credentials to
+ be used when initiating outgoing connections (i.e. the client role in
+ client-server and quorum TLS connections), and the password to unlock the
+ file. This is only needed when using certificates with a single Extended Key
+ Usage (EKU): configure a `clientAuth`-only certificate here and a
+ `serverAuth`-only certificate in `ssl.keyStore.location`. When this property
+ is not set, the shared `ssl.keyStore.location` keystore is used for both
+ roles. See [Single EKU certificate support](#single-eku-certificate-support).
+
+- _ssl.client.keyStore.passwordPath_ and
_ssl.quorum.client.keyStore.passwordPath_ :
+ (Java system properties: **zookeeper.ssl.client.keyStore.passwordPath** and
**zookeeper.ssl.quorum.client.keyStore.passwordPath**)
+ **New in 3.10.0:**
+ Specifies the file path that contains the client keystore password. Reading
the password from a file takes precedence over
+ the explicit password property. See [Single EKU certificate
support](#single-eku-certificate-support).
+
+- _ssl.client.keyStore.type_ and _ssl.quorum.client.keyStore.type_ :
+ (Java system properties: **zookeeper.ssl.client.keyStore.type** and
**zookeeper.ssl.quorum.client.keyStore.type**)
+ **New in 3.10.0:**
+ Specifies the file format of the client keystores. Values: JKS, PEM, PKCS12,
BCFKS or null (detect by filename).
+ Default: null. See [Single EKU certificate
support](#single-eku-certificate-support).
+
- _ssl.trustStore.location_ and _ssl.trustStore.password_ and
_ssl.quorum.trustStore.location_ and _ssl.quorum.trustStore.password_ :
(Java system properties: **zookeeper.ssl.trustStore.location** and
**zookeeper.ssl.trustStore.password** and
**zookeeper.ssl.quorum.trustStore.location** and
**zookeeper.ssl.quorum.trustStore.password**)
**New in 3.5.5:**
Specifies the file path to a Java truststore containing the remote
- credentials to be used for client and quorum TLS connections, and the
- password to unlock the file.
+ credentials to be used for client-server and quorum TLS connections, and the
+ password to unlock the file. This truststore is used for both the client
+ role (verifying servers) and the server role (verifying clients during
+ mTLS). When separate `server.trustStore` properties are configured (see
+ below), this truststore is used only for the client role and the server role
+ uses the dedicated server truststore instead.
- _ssl.trustStore.passwordPath_ and _ssl.quorum.trustStore.passwordPath_ :
(Java system properties: **zookeeper.ssl.trustStore.passwordPath** and
**zookeeper.ssl.quorum.trustStore.passwordPath**)
@@ -1215,11 +1249,38 @@ server if this feature is enabled with sasl as
authentication scheme.
- _ssl.trustStore.type_ and _ssl.quorum.trustStore.type_ :
(Java system properties: **zookeeper.ssl.trustStore.type** and
**zookeeper.ssl.quorum.trustStore.type**)
**New in 3.5.5:**
- Specifies the file format of client and quorum trustStores. Values: JKS,
PEM, PKCS12 or null (detect by filename).
+ Specifies the file format of client-server and quorum trustStores. Values:
JKS, PEM, PKCS12 or null (detect by filename).
Default: null.
**New in 3.5.10, 3.6.3, 3.7.0:**
The format BCFKS was added.
+- _ssl.server.trustStore.location_ and _ssl.server.trustStore.password_ and
_ssl.quorum.server.trustStore.location_ and
_ssl.quorum.server.trustStore.password_ :
+ (Java system properties: **zookeeper.ssl.server.trustStore.location** and
**zookeeper.ssl.server.trustStore.password**
+ and **zookeeper.ssl.quorum.server.trustStore.location** and
**zookeeper.ssl.quorum.server.trustStore.password**)
+ **New in 3.10.0:**
+ Specifies the file path to a Java truststore containing the credentials to
+ be used when accepting incoming connections (i.e. the server role in
+ client-server and quorum TLS connections), and the password to unlock the
+ file. This is only needed when using certificates with a single Extended Key
+ Usage (EKU): configure a truststore containing `serverAuth`-only CA
+ certificates here and a truststore containing `clientAuth`-only CA
+ certificates in `ssl.trustStore.location`. When this property is not set,
+ the shared `ssl.trustStore.location` truststore is used for both roles.
+ See [Single EKU certificate support](#single-eku-certificate-support).
+
+- _ssl.server.trustStore.passwordPath_ and
_ssl.quorum.server.trustStore.passwordPath_ :
+ (Java system properties: **zookeeper.ssl.server.trustStore.passwordPath**
and **zookeeper.ssl.quorum.server.trustStore.passwordPath**)
+ **New in 3.10.0:**
+ Specifies the file path that contains the server truststore password.
Reading the password from a file takes precedence over
+ the explicit password property. See [Single EKU certificate
support](#single-eku-certificate-support).
+
+- _ssl.server.trustStore.type_ and _ssl.quorum.server.trustStore.type_ :
+ (Java system properties: **zookeeper.ssl.server.trustStore.type** and
**zookeeper.ssl.quorum.server.trustStore.type**)
+ **New in 3.10.0:**
+ Specifies the file format of the server truststores. Values: JKS, PEM,
PKCS12, BCFKS or null (detect by filename).
+ Default: null.
+ See [Single EKU certificate support](#single-eku-certificate-support).
+
- _ssl.protocol_ and _ssl.quorum.protocol_ :
(Java system properties: **zookeeper.ssl.protocol** and
**zookeeper.ssl.quorum.protocol**)
**New in 3.5.5:**
@@ -1387,6 +1448,36 @@ server if this feature is enabled with sasl as
authentication scheme.
Default: **true** (3.9.0+), **false** (3.8.x)
+## Single EKU certificate support
+
+**New in 3.10.0.**
+When TLS is configured, by default, ZooKeeper uses a single keystore and a
single truststore for
+both sides of every TLS connection. This works when each certificate carries
+both the `clientAuth` and `serverAuth` Extended Key Usage (EKU) extensions
+(or no EKU restriction at all).
+
+In order to support **single EKU** certificates: one certificate for the
client role
+(`clientAuth` only) and a separate certificate for the server role
+(`serverAuth` only), ZooKeeper allows configuring dedicated stores for each
role.
+
+**Client-server TLS** (`ssl.` prefix):
+
+| Role | KeyStore property |
TrustStore property | Certificate EKU |
+| ----------------------------------- | ------------------------------ |
-------------------------------- | --------------- |
+| **Server** (accepting connections) | `ssl.keyStore.location` |
`ssl.server.trustStore.location` | `serverAuth` |
+| **Client** (initiating connections) | `ssl.client.keyStore.location` |
`ssl.trustStore.location` | `clientAuth` |
+
+**Quorum TLS** (`ssl.quorum.` prefix):
+
+| Role | KeyStore property
| TrustStore property | Certificate EKU |
+| ----------------------------------- | -------------------------------------
| --------------------------------------- | --------------- |
+| **Server** (accepting connections) | `ssl.quorum.keyStore.location`
| `ssl.quorum.server.trustStore.location` | `serverAuth` |
+| **Client** (initiating connections) | `ssl.quorum.client.keyStore.location`
| `ssl.quorum.trustStore.location` | `clientAuth` |
+
+When the dedicated `client.keyStore` or `server.trustStore` properties are
+not set, ZooKeeper falls back to the shared `keyStore` and `trustStore`
+for both roles, which is the pre-3.10.0 behavior.
+
## TLS Cipher Suites
From 3.5.5 to 3.9 a hard coded default cipher list was used, with the ordering