ClientSSLTest.java
/*
* 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
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* 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.test;
import static org.apache.zookeeper.test.ClientBase.CONNECTION_TIMEOUT;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import io.netty.handler.ssl.SslProvider;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.stream.Stream;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.PortAssignment;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.client.ZKClientConfig;
import org.apache.zookeeper.common.ClientX509Util;
import org.apache.zookeeper.common.SecretUtilsTest;
import org.apache.zookeeper.server.NettyServerCnxnFactory;
import org.apache.zookeeper.server.ServerCnxnFactory;
import org.apache.zookeeper.server.auth.ProviderRegistry;
import org.apache.zookeeper.server.quorum.QuorumPeerTestBase;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
public class ClientSSLTest extends QuorumPeerTestBase {
private ClientX509Util clientX509Util;
public static Stream<Arguments> positiveTestData() {
ArrayList<Arguments> result = new ArrayList<>();
for (SslProvider sslProvider : SslProvider.values()) {
for (String fipsEnabled : new String[] { "true", "false" }) {
for (String hostnameverification : new String[] { "true", "false" }) {
result.add(Arguments.of(sslProvider, fipsEnabled, hostnameverification));
}
}
}
return result.stream();
}
public static Stream<Arguments> negativeTestData() {
ArrayList<Arguments> result = new ArrayList<>();
for (SslProvider sslProvider : SslProvider.values()) {
for (String fipsEnabled : new String[] { "true", "false" }) {
result.add(Arguments.of(sslProvider, fipsEnabled));
}
}
return result.stream();
}
@BeforeEach
public void setup() {
System.setProperty(NettyServerCnxnFactory.PORT_UNIFICATION_KEY, Boolean.TRUE.toString());
clientX509Util = new ClientX509Util();
String testDataPath = System.getProperty("test.data.dir", "src/test/resources/data");
System.setProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY, "org.apache.zookeeper.server.NettyServerCnxnFactory");
System.setProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET, "org.apache.zookeeper.ClientCnxnSocketNetty");
System.setProperty(ZKClientConfig.SECURE_CLIENT, "true");
System.setProperty(clientX509Util.getSslKeystoreLocationProperty(), testDataPath + "/ssl/testKeyStore.jks");
System.setProperty(clientX509Util.getSslKeystorePasswdProperty(), "testpass");
System.setProperty(clientX509Util.getSslTruststoreLocationProperty(), testDataPath + "/ssl/testTrustStore.jks");
System.setProperty(clientX509Util.getSslTruststorePasswdProperty(), "testpass");
}
@AfterEach
public void teardown() {
System.clearProperty(NettyServerCnxnFactory.PORT_UNIFICATION_KEY);
System.clearProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY);
System.clearProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET);
System.clearProperty(ZKClientConfig.SECURE_CLIENT);
System.clearProperty(clientX509Util.getSslKeystoreLocationProperty());
System.clearProperty(clientX509Util.getSslKeystorePasswdProperty());
System.clearProperty(clientX509Util.getSslKeystorePasswdPathProperty());
System.clearProperty(clientX509Util.getSslTruststoreLocationProperty());
System.clearProperty(clientX509Util.getSslTruststorePasswdProperty());
System.clearProperty(clientX509Util.getSslTruststorePasswdPathProperty());
System.clearProperty(clientX509Util.getFipsModeProperty());
System.clearProperty(clientX509Util.getSslHostnameVerificationEnabledProperty());
System.clearProperty(clientX509Util.getSslProviderProperty());
clientX509Util.close();
}
/**
* This test checks that client SSL connections work in the absence of a
* secure port when port unification is set up for the plaintext port.
*
* This single client port will be tested for handling both plaintext
* and SSL traffic.
*/
@Test
public void testClientServerUnifiedPort() throws Exception {
testClientServerSSL(false);
}
@Test
public void testClientServerUnifiedPortWithCnxnClassName() throws Exception {
System.setProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET, "ClientCnxnSocketNIO");
testClientServerSSL(false);
}
@Test
public void testClientServerSSLWithCnxnClassName() throws Exception {
System.setProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET, "ClientCnxnSocketNetty");
testClientServerSSL(true);
}
/**
* This test checks that client - server SSL works in cluster setup of ZK servers, which includes:
* 1. setting "secureClientPort" in "zoo.cfg" file.
* 2. setting jvm flags for serverCnxn, keystore, truststore.
* Finally, a zookeeper client should be able to connect to the secure port and
* communicate with server via secure connection.
* <p/>
* Note that in this test a ZK server has two ports -- clientPort and secureClientPort.
* <p/>
* This test covers the positive scenarios for hostname verification.
*/
@ParameterizedTest(name = "sslProvider={0}, fipsEnabled={1}, hostnameVerification={2}")
@MethodSource("positiveTestData")
public void testClientServerSSL_positive(SslProvider sslProvider, String fipsEnabled, String hostnameVerification) throws Exception {
//Skipping this test for s390x arch as netty-tc-native is not supported
assumeFalse(System.getProperty("os.arch").contains("s390x"), " Skipping for s390x arch as netty-tcnative is not yet supported.");
// Arrange
System.setProperty(clientX509Util.getSslProviderProperty(), sslProvider.toString());
System.setProperty(clientX509Util.getFipsModeProperty(), fipsEnabled);
System.setProperty(clientX509Util.getSslHostnameVerificationEnabledProperty(), hostnameVerification);
// Act & Assert
testClientServerSSL(hostnameVerification.equals("true") ? "localhost" : InetAddress.getLocalHost().getHostName(),
true, CONNECTION_TIMEOUT);
}
/**
* This test covers the negative scenarios for hostname verification.
*/
@ParameterizedTest(name = "sslProvider={0}, fipsEnabled={1}")
@MethodSource("negativeTestData")
public void testClientServerSSL_negative(SslProvider sslProvider, boolean fipsEnabled) {
// Arrange
System.setProperty(clientX509Util.getSslProviderProperty(), sslProvider.toString());
System.setProperty(clientX509Util.getFipsModeProperty(), Boolean.toString(fipsEnabled));
System.setProperty(clientX509Util.getSslHostnameVerificationEnabledProperty(), "true");
// Act & Assert
assertThrows(AssertionError.class, () ->
testClientServerSSL(InetAddress.getLocalHost().getHostName(), true, 5000));
}
@Test
public void testClientServerSSL_withPasswordFromFile() throws Exception {
final Path secretFile = SecretUtilsTest.createSecretFile("testpass");
System.clearProperty(clientX509Util.getSslKeystorePasswdProperty());
System.setProperty(clientX509Util.getSslKeystorePasswdPathProperty(), secretFile.toString());
System.clearProperty(clientX509Util.getSslTruststorePasswdProperty());
System.setProperty(clientX509Util.getSslTruststorePasswdPathProperty(), secretFile.toString());
testClientServerSSL(true);
}
public void testClientServerSSL(boolean useSecurePort) throws Exception {
testClientServerSSL("localhost", useSecurePort, CONNECTION_TIMEOUT);
}
public void testClientServerSSL(String hostname, boolean useSecurePort, long connectTimeout) throws Exception {
final int SERVER_COUNT = 3;
final int[] clientPorts = new int[SERVER_COUNT];
final Integer[] secureClientPorts = new Integer[SERVER_COUNT];
StringBuilder sb = new StringBuilder();
for (int i = 0; i < SERVER_COUNT; i++) {
clientPorts[i] = PortAssignment.unique();
secureClientPorts[i] = PortAssignment.unique();
String server = String.format("server.%d=127.0.0.1:%d:%d:participant;127.0.0.1:%d%n", i, PortAssignment.unique(), PortAssignment.unique(), clientPorts[i]);
sb.append(server);
}
String quorumCfg = sb.toString();
MainThread[] mt = new MainThread[SERVER_COUNT];
for (int i = 0; i < SERVER_COUNT; i++) {
if (useSecurePort) {
mt[i] = new MainThread(i, quorumCfg, secureClientPorts[i], true);
} else {
mt[i] = new MainThread(i, quorumCfg, true);
}
mt[i].start();
}
// Add some timing margin for the quorum to elect a leader
// (without this margin, timeouts have been observed in parallel test runs)
ClientBase.waitForServerUp("127.0.0.1:" + clientPorts[0], 2 * TIMEOUT);
// Servers have been set up. Now go test if secure connection is successful.
for (int i = 0; i < SERVER_COUNT; i++) {
assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPorts[i], TIMEOUT),
"waiting for server " + i + " being up");
final int port = useSecurePort ? secureClientPorts[i] : clientPorts[i];
try (ZooKeeper zk = ClientBase.createZKClient(hostname + ":" + port, TIMEOUT, connectTimeout)) {
// Do a simple operation to make sure the connection is fine.
zk.create("/test", "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
zk.delete("/test", -1);
}
}
for (int i = 0; i < mt.length; i++) {
mt[i].shutdown();
}
}
/**
* Developers might use standalone mode (which is the default for one server).
* This test checks SSL works in standalone mode of ZK server.
* <p>
* Note that in this test the Zk server has only secureClientPort
*/
@Test
public void testSecureStandaloneServer() throws Exception {
Integer secureClientPort = PortAssignment.unique();
MainThread mt = new MainThread(MainThread.UNSET_MYID, "", secureClientPort, false);
mt.start();
ZooKeeper zk = ClientBase.createZKClient("127.0.0.1:" + secureClientPort, TIMEOUT);
zk.create("/test", "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
zk.delete("/test", -1);
zk.close();
mt.shutdown();
}
@Test
public void testSecureStandaloneServerAuthFail() throws IOException {
try {
System.setProperty(ProviderRegistry.AUTHPROVIDER_PROPERTY_PREFIX + "authfail",
AuthFailX509AuthenticationProvider.class.getName());
System.setProperty(clientX509Util.getSslAuthProviderProperty(), "authfail");
Integer secureClientPort = PortAssignment.unique();
MainThread mt = new MainThread(MainThread.UNSET_MYID, "", secureClientPort, false);
mt.start();
AssertionError ex = assertThrows("Client should not able to connect when authentication fails", AssertionError.class,
() -> {
ClientBase.createZKClient("localhost:" + secureClientPort, TIMEOUT, 3000);
});
assertThat("Exception message does not match (different exception caught?)",
ex.getMessage(), startsWith("ZooKeeper client can not connect to"));
} finally {
System.clearProperty(ProviderRegistry.AUTHPROVIDER_PROPERTY_PREFIX + "authfail");
System.clearProperty(clientX509Util.getSslAuthProviderProperty());
}
}
}