From a9b25f74518c1ea42d83b2acd7a493661341dd4e Mon Sep 17 00:00:00 2001 From: Kezhu Wang Date: Sun, 17 Aug 2025 14:40:46 +0800 Subject: [PATCH 1/2] ZOOKEEPER-4962: Export bound client port for ZooKeeperServerEmbedded Since, `ZooKeeperServerEmbedded` could bind to port 0 and get unused port from system. It would be good to export the bound client port in addition to `getConnectionString`. --- .../zookeeper/server/ZooKeeperServerMain.java | 2 -- .../embedded/ZooKeeperServerEmbedded.java | 10 ++++++++++ .../embedded/ZooKeeperServerEmbeddedImpl.java | 18 ++++++++++++++---- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServerMain.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServerMain.java index 24e7f5b180e..b63280f2971 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServerMain.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServerMain.java @@ -242,7 +242,6 @@ ServerCnxnFactory getSecureCnxnFactory() { return secureCnxnFactory; } - // VisibleForTesting public int getClientPort() { if (cnxnFactory != null) { return cnxnFactory.getLocalPort(); @@ -250,7 +249,6 @@ public int getClientPort() { return 0; } - // VisibleForTesting public int getSecureClientPort() { if (secureCnxnFactory != null) { return secureCnxnFactory.getLocalPort(); diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/embedded/ZooKeeperServerEmbedded.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/embedded/ZooKeeperServerEmbedded.java index 6fcd7038504..f73a1ee3d99 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/embedded/ZooKeeperServerEmbedded.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/embedded/ZooKeeperServerEmbedded.java @@ -116,6 +116,16 @@ static ZookKeeperServerEmbeddedBuilder builder() { */ void start(long startupTimeout) throws Exception; + /** + * Get client port for no secure connection. + */ + int getClientPort(); + + /** + * Get client port for secure connection. + */ + int getSecureClientPort(); + /** * Get a connection string useful for the client. * @return the connection string diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/embedded/ZooKeeperServerEmbeddedImpl.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/embedded/ZooKeeperServerEmbeddedImpl.java index fb9e5eb6c9f..14e7cbbb7e9 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/embedded/ZooKeeperServerEmbeddedImpl.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/embedded/ZooKeeperServerEmbeddedImpl.java @@ -107,8 +107,8 @@ protected QuorumPeer getQuorumPeer() throws SaslException { @Override public void start() { super.start(); - boundClientPort = getClientPort(); - boundSecureClientPort = getSecureClientPort(); + boundClientPort = super.getClientPort(); + boundSecureClientPort = super.getSecureClientPort(); LOG.info("ZK Server {} started", this); started.complete(null); } @@ -148,8 +148,8 @@ public void run() { @Override public void serverStarted() { LOG.info("ZK Server started"); - boundClientPort = getClientPort(); - boundSecureClientPort = getSecureClientPort(); + boundClientPort = super.getClientPort(); + boundSecureClientPort = super.getSecureClientPort(); started.complete(null); } }; @@ -190,6 +190,16 @@ public void run() { } } + @Override + public int getClientPort() { + return boundClientPort; + } + + @Override + public int getSecureClientPort() { + return boundSecureClientPort; + } + @Override public String getConnectionString() { return prettifyConnectionString(config.getClientPortAddress(), boundClientPort); From 6f539802c209e78ffdb781f15bdb59fc924d07e5 Mon Sep 17 00:00:00 2001 From: Kezhu Wang Date: Sun, 17 Aug 2025 15:08:11 +0800 Subject: [PATCH 2/2] ZOOKEEPER-4958: Fix client hostname verification ignored in server if ssl.authProvider configured `NettyServerCnxnFactory` uses `TrustManager` from `X509AuthenticationProvider` if `ssl.authProvider` is configured. But `clientHostnameVerificationEnabled` is explicitly set to `false` in construction of `X509AuthenticationProvider`. This cause the server skip hostname verification agaist client certificate. This is reproducible in case of following server configs: * zookeeper.ssl.hostnameVerification: true * zookeeper.ssl.clientHostnameVerification: true * zookeeper.fips-mode: false * zookeeper.ssl.authProvider: x509 --- .../auth/KeyAuthenticationProvider.java | 2 +- .../auth/X509AuthenticationProvider.java | 3 +- .../server/SSLHostnameVerificationTest.java | 459 ++++++++++++++++++ 3 files changed, 462 insertions(+), 2 deletions(-) create mode 100644 zookeeper-server/src/test/java/org/apache/zookeeper/server/SSLHostnameVerificationTest.java diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/KeyAuthenticationProvider.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/KeyAuthenticationProvider.java index 92bb380fb46..cf252b9b08a 100644 --- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/KeyAuthenticationProvider.java +++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/auth/KeyAuthenticationProvider.java @@ -37,7 +37,7 @@ * See the "Pluggable ZooKeeper authentication" section of the * "Zookeeper Programmer's Guide" for general details of implementing an * authentication plugin. e.g. - * http://zookeeper.apache.org/doc/trunk/zookeeperProgrammers.html#sc_ZooKeeperPluggableAuthentication + * http://zookeeper.apache.org/doc/current/zookeeperProgrammers.html#sc_ZooKeeperPluggableAuthentication * * This class looks for a numeric "key" under the /key node. * Authorization is granted if the user passes in as authorization a number 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 8286c061c9c..53690c6d0d2 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,6 +89,7 @@ public X509AuthenticationProvider() throws X509Exception { boolean crlEnabled = config.getBoolean(x509Util.getSslCrlEnabledProperty(), Boolean.getBoolean("com.sun.net.ssl.checkRevocation")); boolean ocspEnabled = config.getBoolean(x509Util.getSslOcspEnabledProperty(), Boolean.parseBoolean(Security.getProperty("ocsp.enable"))); boolean hostnameVerificationEnabled = Boolean.parseBoolean(config.getProperty(x509Util.getSslHostnameVerificationEnabledProperty())); + boolean clientHostnameVerificationEnabled = x509Util.isClientHostnameVerificationEnabled(config); X509KeyManager km = null; X509TrustManager tm = null; @@ -120,7 +121,7 @@ public X509AuthenticationProvider() throws X509Exception { crlEnabled, ocspEnabled, hostnameVerificationEnabled, - false, + clientHostnameVerificationEnabled, fipsMode); } catch (TrustManagerException e) { LOG.error("Failed to create trust manager", e); diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/server/SSLHostnameVerificationTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/server/SSLHostnameVerificationTest.java new file mode 100644 index 00000000000..6c58774f77f --- /dev/null +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/server/SSLHostnameVerificationTest.java @@ -0,0 +1,459 @@ +/* + * 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.server; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.Security; +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.apache.zookeeper.WatchedEvent; +import org.apache.zookeeper.Watcher; +import org.apache.zookeeper.ZooKeeper; +import org.apache.zookeeper.client.ZKClientConfig; +import org.apache.zookeeper.common.ssl.Ca; +import org.apache.zookeeper.common.ssl.Cert; +import org.apache.zookeeper.server.embedded.ExitHandler; +import org.apache.zookeeper.server.embedded.ZooKeeperServerEmbedded; +import org.apache.zookeeper.test.ClientBase; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.burningwave.tools.net.HostResolutionRequestInterceptor; +import org.burningwave.tools.net.MappedHostResolver; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +public class SSLHostnameVerificationTest { + @BeforeAll + public static void setupDNSMocks() { + Map hostAliases = new LinkedHashMap<>(); + + // avoid resolving "localhost" to ipv6 address "::1" + hostAliases.put("localhost", "127.0.0.1"); + HostResolutionRequestInterceptor.INSTANCE.install(new MappedHostResolver(hostAliases)); + HostResolutionRequestInterceptor.INSTANCE.clearCache(); + } + + @AfterAll + public static void clearDNSMocks() { + HostResolutionRequestInterceptor.INSTANCE.uninstall(); + } + + @BeforeAll + public static void setup() { + Security.addProvider(new BouncyCastleProvider()); + } + + @AfterAll + public static void cleanup() { + Security.removeProvider("BC"); + } + + Watcher.Event.KeeperState checkConnectState(String connectString, ZKClientConfig clientConfig) throws Exception { + Duration timeout = Duration.ofSeconds(1); + Watcher.Event.KeeperState state; + CompletableFuture future = new CompletableFuture<>(); + try (ZooKeeper zk = new ZooKeeper(connectString, (int) timeout.toMillis(), future::complete, clientConfig)) { + try { + WatchedEvent event = future.get(timeout.toMillis() * 2, TimeUnit.MILLISECONDS); + state = event.getState(); + } catch (TimeoutException ignored) { + // See: ZOOKEEPER-4508, ZOOKEEPER-4921, ZOOKEEPER-4923 + state = Watcher.Event.KeeperState.Expired; + } + } + return state; + } + + @ParameterizedTest(name = "{0}, fips-mode: {1}") + @CsvSource({ + "localhost, true", + "localhost, false", + "127.0.0.1, true", + "127.0.0.1, false", + }) + public void testClientHostnameVerificationWithMismatchNames(String serverHost, boolean fipsEnabled, @TempDir Path tmpDir) throws Exception { + try (Ca ca = Ca.create(tmpDir)) { + // given: server with cert mismatching cn/dns/ip + Cert server1Cert = ca.signer("abc0").withDnsName("abc1").withIpAddress("192.168.0.10").sign(); + Properties config = server1Cert.buildServerProperties(ca); + config.put("ssl.hostnameVerification", "false"); + config.put("secureClientPortAddress", serverHost); + + try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded + .builder() + .baseDir(Files.createTempDirectory(tmpDir, "server.data")) + .configuration(config) + .exitHandler(ExitHandler.LOG_ONLY) + .build()) { + server.start(); + + // server ready + assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000)); + + Cert clientCert = ca.sign("client"); + ZKClientConfig clientConfig = clientCert.buildClientConfig(ca); + clientConfig.setProperty("zookeeper.sasl.client", "false"); + clientConfig.setProperty("zookeeper.fips-mode", Boolean.toString(fipsEnabled)); + clientConfig.setProperty("zookeeper.ssl.hostnameVerification", "true"); + + // when: connect using mismatched dns/ip + String connectionString = server.getSecureConnectionString(); + // then: connection rejected by us as no matching name + assertEquals(Watcher.Event.KeeperState.Expired, checkConnectState(server.getSecureConnectionString(), clientConfig)); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"localhost", "127.0.0.1"}) + public void testClientHostnameVerificationWithMatchingCnName(String serverHost, @TempDir Path tmpDir) throws Exception { + try (Ca ca = Ca.create(tmpDir)) { + // given: server cert with cn name "localhost" + Cert server1Cert = ca.signer("localhost").sign(); + Properties config = server1Cert.buildServerProperties(ca); + config.put("ssl.hostnameVerification", "false"); + + try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded + .builder() + .baseDir(Files.createTempDirectory(tmpDir, "server.data")) + .configuration(config) + .exitHandler(ExitHandler.LOG_ONLY) + .build()) { + server.start(); + + // server ready + assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000)); + + Cert clientCert = ca.sign("client"); + ZKClientConfig clientConfig = clientCert.buildClientConfig(ca); + clientConfig.setProperty("zookeeper.sasl.client", "false"); + clientConfig.setProperty("zookeeper.ssl.hostnameVerification", "true"); + clientConfig.setProperty("zookeeper.fips-mode", "false"); + + // when: connect using matching dns/ip + String connectionString = serverHost + ":" + server.getSecureClientPort(); + // then: connected as there is no other sans + assertEquals(Watcher.Event.KeeperState.SyncConnected, checkConnectState(connectionString, clientConfig)); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"localhost", "127.0.0.1"}) + public void testClientHostnameVerificationWithMatchingCnNameButMismatchingSan(String serverHost, @TempDir Path tmpDir) throws Exception { + try (Ca ca = Ca.create(tmpDir)) { + // given: server with cert matching cn + Cert server1Cert = ca.signer("localhost").withDnsName("abc1").withIpAddress("192.168.0.10").sign(); + Properties config = server1Cert.buildServerProperties(ca); + config.put("ssl.hostnameVerification", "false"); + + try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded + .builder() + .baseDir(Files.createTempDirectory(tmpDir, "server.data")) + .configuration(config) + .exitHandler(ExitHandler.LOG_ONLY) + .build()) { + server.start(); + + // server ready + assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000)); + + Cert clientCert = ca.sign("client"); + ZKClientConfig clientConfig = clientCert.buildClientConfig(ca); + clientConfig.setProperty("zookeeper.ssl.hostnameVerification", "true"); + clientConfig.setProperty("zookeeper.sasl.client", "false"); + clientConfig.setProperty("zookeeper.fips-mode", "false"); + + // when: connect with dns or ip resolved to cn name + String connectionString = String.format("%s:%d", serverHost, server.getSecureClientPort()); + + // then: fail to connect + // + // CN matching has been deprecated by rfc2818 and can be used + // as fallback only when no subjectAlts are available + assertEquals(Watcher.Event.KeeperState.Expired, checkConnectState(connectionString, clientConfig)); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"localhost", "127.0.0.1"}) + public void testClientHostnameVerificationWithMatchingIpAddress(String serverHost, @TempDir Path tmpDir) throws Exception { + try (Ca ca = Ca.create(tmpDir)) { + // given: server with cert mismatching ip + Cert server1Cert = ca.signer("abc0").withDnsName("abc1").withIpAddress("127.0.0.1").sign(); + Properties config = server1Cert.buildServerProperties(ca); + config.put("ssl.hostnameVerification", "false"); + + try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded + .builder() + .baseDir(Files.createTempDirectory(tmpDir, "server.data")) + .configuration(config) + .exitHandler(ExitHandler.LOG_ONLY) + .build()) { + server.start(); + + // server ready + assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000)); + + Cert clientCert = ca.sign("client"); + ZKClientConfig clientConfig = clientCert.buildClientConfig(ca); + clientConfig.setProperty("zookeeper.ssl.hostnameVerification", "true"); + clientConfig.setProperty("zookeeper.sasl.client", "false"); + clientConfig.setProperty("zookeeper.fips-mode", "false"); + + // when: connect with matching ip or its dns + String connectionString = String.format("%s:%d", serverHost, server.getSecureClientPort()); + + // then: connected + assertEquals(Watcher.Event.KeeperState.SyncConnected, checkConnectState(connectionString, clientConfig)); + } + } + } + + @Test + public void testClientHostnameVerificationFipsModeWithIpAddress(@TempDir Path tmpDir) throws Exception { + try (Ca ca = Ca.create(tmpDir)) { + // given: server with cert mismatching ip + Cert server1Cert = ca.signer("abc0").withDnsName("abc1").withIpAddress("127.0.0.1").sign(); + Properties config = server1Cert.buildServerProperties(ca); + config.put("ssl.hostnameVerification", "false"); + + try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded + .builder() + .baseDir(Files.createTempDirectory(tmpDir, "server.data")) + .configuration(config) + .exitHandler(ExitHandler.LOG_ONLY) + .build()) { + server.start(); + + // server ready + assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000)); + + Cert clientCert = ca.sign("client"); + ZKClientConfig clientConfig = clientCert.buildClientConfig(ca); + clientConfig.setProperty("zookeeper.ssl.hostnameVerification", "true"); + clientConfig.setProperty("zookeeper.sasl.client", "false"); + clientConfig.setProperty("zookeeper.fips-mode", "true"); + + // when: connect with ip's dns + String connectionString = String.format("localhost:%d", server.getSecureClientPort()); + // then: rejected as fips-mode don't do dns lookup + assertEquals(Watcher.Event.KeeperState.Expired, checkConnectState(connectionString, clientConfig)); + + // when: connect with ip address + connectionString = String.format("127.0.0.1:%d", server.getSecureClientPort()); + // then: connected + assertEquals(Watcher.Event.KeeperState.SyncConnected, checkConnectState(connectionString, clientConfig)); + } + } + } + + @Test + public void testClientHostnameVerificationFipsModeWithDns(@TempDir Path tmpDir) throws Exception { + try (Ca ca = Ca.create(tmpDir)) { + // given: server cert with dns "localhost" + Cert server1Cert = ca.signer("abc0").withDnsName("localhost").sign(); + Properties config = server1Cert.buildServerProperties(ca); + config.put("ssl.hostnameVerification", "false"); + + try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded + .builder() + .baseDir(Files.createTempDirectory(tmpDir, "server.data")) + .configuration(config) + .exitHandler(ExitHandler.LOG_ONLY) + .build()) { + server.start(); + + // server ready + assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000)); + + Cert clientCert = ca.sign("client"); + ZKClientConfig clientConfig = clientCert.buildClientConfig(ca); + clientConfig.setProperty("zookeeper.ssl.hostnameVerification", "true"); + clientConfig.setProperty("zookeeper.sasl.client", "false"); + clientConfig.setProperty("zookeeper.fips-mode", "true"); + + // when: connect with ip address + String connectionString = String.format("127.0.0.1:%d", server.getSecureClientPort()); + // then: fail as fips-mode won't do reverse dns lookup + assertEquals(Watcher.Event.KeeperState.Expired, checkConnectState(connectionString, clientConfig)); + + // when: connect with "localhost" + connectionString = String.format("localhost:%d", server.getSecureClientPort()); + // then: succeed + assertEquals(Watcher.Event.KeeperState.SyncConnected, checkConnectState(connectionString, clientConfig)); + } + } + } + + @Test + public void testClientHostnameVerificationWithMatchingDnsName(@TempDir Path tmpDir) throws Exception { + try (Ca ca = Ca.create(tmpDir)) { + // given: server cert with dns name "localhost" + Cert server1Cert = ca.signer("abc0").withDnsName("localhost").withIpAddress("192.168.0.10").sign(); + Properties config = server1Cert.buildServerProperties(ca); + config.put("ssl.hostnameVerification", "false"); + + try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded + .builder() + .baseDir(Files.createTempDirectory(tmpDir, "server.data")) + .configuration(config) + .exitHandler(ExitHandler.LOG_ONLY) + .build()) { + server.start(); + + // server ready + assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000)); + + Cert clientCert = ca.sign("client"); + ZKClientConfig clientConfig = clientCert.buildClientConfig(ca); + clientConfig.setProperty("zookeeper.sasl.client", "false"); + clientConfig.setProperty("zookeeper.fips-mode", "false"); + clientConfig.setProperty("zookeeper.ssl.hostnameVerification", "true"); + + // when: connect to "127.0.0.1" + String connectionString = String.format("127.0.0.1:%d", server.getSecureClientPort()); + // then: connected as ZKHostnameVerifier will do reverse dns lookup + assertEquals(Watcher.Event.KeeperState.SyncConnected, checkConnectState(connectionString, clientConfig)); + + // when: connect to "localhost" + connectionString = String.format("localhost:%d", server.getSecureClientPort()); + // then: connected as dns match + assertEquals(Watcher.Event.KeeperState.SyncConnected, checkConnectState(connectionString, clientConfig)); + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"x509", ""}) + public void testServerHostnameVerification(String authProvider, @TempDir Path tmpDir) throws Exception { + try (Ca ca = Ca.create(tmpDir)) { + Cert cert = ca.sign("server"); + + // given: server with client hostname verification enabled + Properties config = cert.buildServerProperties(ca); + config.put("ssl.hostnameVerification", "true"); + config.put("ssl.clientHostnameVerification", "true"); + config.put("fips-mode", "false"); + if (!authProvider.isEmpty()) { + config.put("ssl.authProvider", "x509"); + } + + try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded + .builder() + .baseDir(Files.createTempDirectory(tmpDir, "server.data")) + .configuration(config) + .exitHandler(ExitHandler.LOG_ONLY) + .build()) { + server.start(); + + // server ready + assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000)); + + // // when: connect with matching dns name + Cert client1Cert = ca.signer("client1").withDnsName("localhost").sign(); + ZKClientConfig client1Config = client1Cert.buildClientConfig(ca); + client1Config.setProperty("zookeeper.ssl.hostnameVerification", "false"); + + // then: connected + assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, true, client1Config)); + + // when: connect with matching ip address + Cert client2Cert = ca.signer("client2").withIpAddress("127.0.0.1").sign(); + ZKClientConfig client2Config = client2Cert.buildClientConfig(ca); + client2Config.setProperty("zookeeper.ssl.hostnameVerification", "false"); + + // then: connected + assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, true, client2Config)); + + // when: connect with matching cn name + Cert client3Cert = ca.signer("localhost").sign(); + ZKClientConfig client3Config = client3Cert.buildClientConfig(ca); + client3Config.setProperty("zookeeper.ssl.hostnameVerification", "false"); + + // then: connected + assertTrue(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, true, client3Config)); + + // when: connect with mismatching cert name + Cert client4Cert = ca.signer("client4").withDnsName("abc").sign(); + ZKClientConfig client4Config = client4Cert.buildClientConfig(ca); + client4Config.setProperty("zookeeper.ssl.hostnameVerification", "false"); + + // then: fail to connect + assertFalse(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, true, client4Config)); + } + } + } + + /** + * FIPS mode disallow custom trust manager so server has no way to validate against client's endpoint. + */ + @ParameterizedTest + @ValueSource(strings = {"x509", ""}) + public void testServerHostnameVerificationFipsMode(String authProvider, @TempDir Path tmpDir) throws Exception { + try (Ca ca = Ca.create(tmpDir)) { + Cert cert = ca.sign("server"); + + Properties config = cert.buildServerProperties(ca); + + // given: server in fips mode with client hostname verification enabled + config.put("ssl.hostnameVerification", "true"); + config.put("ssl.clientHostnameVerification", "true"); + config.put("fips-mode", "true"); + + if (!authProvider.isEmpty()) { + config.put("ssl.authProvider", "x509"); + } + + try (ZooKeeperServerEmbedded server = ZooKeeperServerEmbedded + .builder() + .baseDir(Files.createTempDirectory(tmpDir, "server.data")) + .configuration(config) + .exitHandler(ExitHandler.LOG_ONLY) + .build()) { + server.start(); + + // server ready + assertTrue(ClientBase.waitForServerUp(server.getConnectionString(), 60000)); + + // // when: connect with matching dns name + Cert client1Cert = ca.signer("localhost").withResolvedDns("localhost").sign(); + ZKClientConfig client1Config = client1Cert.buildClientConfig(ca); + client1Config.setProperty("zookeeper.ssl.hostnameVerification", "false"); + + // then: fail to connect + assertFalse(ClientBase.waitForServerUp(server.getSecureConnectionString(), 6000, true, client1Config)); + } + } + } +}