TestSecureZKCuratorManager.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.hadoop.util.curator;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.apache.hadoop.security.SecurityUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import org.apache.curator.test.InstanceSpec;
import org.apache.curator.test.TestingServer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.CommonConfigurationKeys;
import org.apache.zookeeper.ClientCnxnSocketNetty;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.client.ZKClientConfig;
import org.apache.zookeeper.common.ClientX509Util;
import org.apache.zookeeper.server.NettyServerCnxnFactory;

import static org.apache.hadoop.fs.FileContext.LOG;
import static org.junit.Assert.assertEquals;

/**
 * Test the manager for ZooKeeper Curator when SSL/TLS is enabled for the ZK server-client
 * connection.
 */
public class TestSecureZKCuratorManager {

  public static final boolean DELETE_DATA_DIRECTORY_ON_CLOSE = true;
  private TestingServer server;
  private ZKCuratorManager curator;
  private Configuration hadoopConf;
  static final int SECURE_CLIENT_PORT = 2281;
  static final int JUTE_MAXBUFFER = 400000000;
  static final File ZK_DATA_DIR = new File("testZkSSLClientConnectionDataDir");
  private static final int SERVER_ID = 1;
  private static final int TICK_TIME = 100;
  private static final int MAX_CLIENT_CNXNS = 10;
  public static final int ELECTION_PORT = -1;
  public static final int QUORUM_PORT = -1;

  @Before
  public void setup() throws Exception {
    // inject values to the ZK configuration file for secure connection
    Map<String, Object> customConfiguration = new HashMap<>();
    customConfiguration.put("secureClientPort", String.valueOf(SECURE_CLIENT_PORT));
    customConfiguration.put("audit.enable", true);
    this.hadoopConf = setUpSecureConfig();
    InstanceSpec spec =
        new InstanceSpec(ZK_DATA_DIR, SECURE_CLIENT_PORT, ELECTION_PORT, QUORUM_PORT,
            DELETE_DATA_DIRECTORY_ON_CLOSE, SERVER_ID, TICK_TIME, MAX_CLIENT_CNXNS,
            customConfiguration);
    this.server = new TestingServer(spec, true);
    String zkHostPort = this.server.getConnectString();
    this.curator = new ZKCuratorManager(this.hadoopConf);
    this.curator.start(new ArrayList<>(), true, zkHostPort);
  }

  /**
   * A static method to configure the test ZK server to accept secure client connection.
   * The self-signed certificates were generated for testing purposes as described below.
   * For the ZK client to connect with the ZK server, the ZK server's keystore and truststore
   * should be used.
   * For testing purposes the keystore and truststore were generated using default values.
   * 1. to generate the keystore.jks file:
   * # keytool -genkey -alias mockcert -keyalg RSA -keystore keystore.jks -keysize 2048
   * 2. generate the ca-cert and the ca-key:
   * # openssl req -new -x509 -keyout ca-key -out ca-cert
   * 3. to generate the certificate signing request (cert-file):
   * # keytool -keystore keystore.jks -alias mockcert -certreq -file certificate-request
   * 4. to generate the ca-cert.srl file and make the cert valid for 10 years:
   * # openssl x509 -req -CA ca-cert -CAkey ca-key -in certificate-request -out cert-signed
   * -days 3650 -CAcreateserial -passin pass:password
   * 5. add the ca-cert to the keystore.jks:
   * # keytool -keystore keystore.jks -alias mockca -import -file ca-cert
   * 6. install the signed certificate to the keystore:
   * # keytool -keystore keystore.jks -alias mockcert -import -file cert-signed
   * 7. add the certificate to the truststore:
   * # keytool -keystore truststore.jks -alias mockcert -import -file ca-cert
   * For our purpose, we only need the end result of this process: the keystore.jks and the
   * truststore.jks files.
   *
   * @return conf The method returns the updated Configuration.
   */
  public static Configuration setUpSecureConfig() {
    return setUpSecureConfig(new Configuration(),
        "src/test/java/org/apache/hadoop/util/curator" + "/resources/data");
  }

  public static Configuration setUpSecureConfig(Configuration conf, String testDataPath) {
    System.setProperty("zookeeper.serverCnxnFactory",
        NettyServerCnxnFactory.class.getCanonicalName());

    System.setProperty("zookeeper.ssl.keyStore.location", testDataPath + "keystore.jks");
    System.setProperty("zookeeper.ssl.keyStore.password", "password");
    System.setProperty("zookeeper.ssl.trustStore.location", testDataPath + "truststore.jks");
    System.setProperty("zookeeper.ssl.trustStore.password", "password");
    System.setProperty("zookeeper.request.timeout", "12345");

    System.setProperty("jute.maxbuffer", String.valueOf(JUTE_MAXBUFFER));

    System.setProperty("javax.net.debug", "ssl");
    System.setProperty("zookeeper.authProvider.x509",
        "org.apache.zookeeper.server.auth.X509AuthenticationProvider");

    conf.set(CommonConfigurationKeys.ZK_SSL_KEYSTORE_LOCATION,
        testDataPath + "/ssl/keystore.jks");
    conf.set(CommonConfigurationKeys.ZK_SSL_KEYSTORE_PASSWORD, "password");
    conf.set(CommonConfigurationKeys.ZK_SSL_TRUSTSTORE_LOCATION,
        testDataPath + "/ssl/truststore.jks");
    conf.set(CommonConfigurationKeys.ZK_SSL_TRUSTSTORE_PASSWORD, "password");
    return conf;
  }

  @After
  public void teardown() throws Exception {
    this.curator.close();
    if (this.server != null) {
      this.server.close();
      this.server = null;
    }
  }

  @Test
  public void testSecureZKConfiguration() throws Exception {
    LOG.info("Entered to the testSecureZKConfiguration test case.");
    // Validate that HadoopZooKeeperFactory will set ZKConfig with given principals
    ZKCuratorManager.HadoopZookeeperFactory factory =
        new ZKCuratorManager.HadoopZookeeperFactory(null, null, null, true,
            new SecurityUtil.TruststoreKeystore(hadoopConf));
    ZooKeeper zk = factory.newZooKeeper(this.server.getConnectString(), 1000, null, false);
    validateSSLConfiguration(this.hadoopConf.get(CommonConfigurationKeys.ZK_SSL_KEYSTORE_LOCATION),
        this.hadoopConf.get(CommonConfigurationKeys.ZK_SSL_KEYSTORE_PASSWORD),
        this.hadoopConf.get(CommonConfigurationKeys.ZK_SSL_TRUSTSTORE_LOCATION),
        this.hadoopConf.get(CommonConfigurationKeys.ZK_SSL_TRUSTSTORE_PASSWORD), zk);
  }

  private void validateSSLConfiguration(String keystoreLocation, String keystorePassword,
      String truststoreLocation, String truststorePassword, ZooKeeper zk) {
    try (ClientX509Util x509Util = new ClientX509Util()) {
      //testing if custom values are set properly
      assertEquals("Validate that expected clientConfig is set in ZK config", keystoreLocation,
          zk.getClientConfig().getProperty(x509Util.getSslKeystoreLocationProperty()));
      assertEquals("Validate that expected clientConfig is set in ZK config", keystorePassword,
          zk.getClientConfig().getProperty(x509Util.getSslKeystorePasswdProperty()));
      assertEquals("Validate that expected clientConfig is set in ZK config", truststoreLocation,
          zk.getClientConfig().getProperty(x509Util.getSslTruststoreLocationProperty()));
      assertEquals("Validate that expected clientConfig is set in ZK config", truststorePassword,
          zk.getClientConfig().getProperty(x509Util.getSslTruststorePasswdProperty()));
    }
    //testing if constant values hardcoded into the code are set properly
    assertEquals("Validate that expected clientConfig is set in ZK config", Boolean.TRUE.toString(),
        zk.getClientConfig().getProperty(ZKClientConfig.SECURE_CLIENT));
    assertEquals("Validate that expected clientConfig is set in ZK config",
        ClientCnxnSocketNetty.class.getCanonicalName(),
        zk.getClientConfig().getProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET));
  }

  @Test
  public void testTruststoreKeystoreConfiguration() {
    LOG.info("Entered to the testTruststoreKeystoreConfiguration test case.");
    /*
      By default the truststore/keystore configurations are not set, hence the values are null.
      Validate that the null values are converted into empty strings by the class.
     */
    Configuration conf = new Configuration();
    SecurityUtil.TruststoreKeystore truststoreKeystore =
        new SecurityUtil.TruststoreKeystore(conf);

    assertEquals("Validate that null value is converted to empty string.", "",
        truststoreKeystore.getKeystoreLocation());
    assertEquals("Validate that null value is converted to empty string.", "",
        truststoreKeystore.getKeystorePassword());
    assertEquals("Validate that null value is converted to empty string.", "",
        truststoreKeystore.getTruststoreLocation());
    assertEquals("Validate that null value is converted to empty string.", "",
        truststoreKeystore.getTruststorePassword());

    //Validate that non-null values will remain intact
    conf.set(CommonConfigurationKeys.ZK_SSL_KEYSTORE_LOCATION, "/keystore.jks");
    conf.set(CommonConfigurationKeys.ZK_SSL_KEYSTORE_PASSWORD, "keystorePassword");
    conf.set(CommonConfigurationKeys.ZK_SSL_TRUSTSTORE_LOCATION, "/truststore.jks");
    conf.set(CommonConfigurationKeys.ZK_SSL_TRUSTSTORE_PASSWORD, "truststorePassword");
    SecurityUtil.TruststoreKeystore truststoreKeystore1 =
        new SecurityUtil.TruststoreKeystore(conf);
    assertEquals("Validate that non-null value kept intact.", "/keystore.jks",
        truststoreKeystore1.getKeystoreLocation());
    assertEquals("Validate that null value is converted to empty string.", "keystorePassword",
        truststoreKeystore1.getKeystorePassword());
    assertEquals("Validate that null value is converted to empty string.", "/truststore.jks",
        truststoreKeystore1.getTruststoreLocation());
    assertEquals("Validate that null value is converted to empty string.", "truststorePassword",
        truststoreKeystore1.getTruststorePassword());
  }
}