org.apache.zookeeper.server.quorum.QuorumSSLTest.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.zookeeper.server.quorum.QuorumSSLTest.java

Source

/**
 * 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.quorum;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import org.apache.zookeeper.PortAssignment;
import org.apache.zookeeper.client.ZKClientConfig;
import org.apache.zookeeper.common.QuorumX509Util;
import org.apache.zookeeper.server.ServerCnxnFactory;
import org.apache.zookeeper.test.ClientBase;
import org.bouncycastle.asn1.ocsp.OCSPResponse;
import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.CRLNumber;
import org.bouncycastle.asn1.x509.CRLReason;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x509.X509ObjectIdentifiers;
import org.bouncycastle.cert.X509CRLHolder;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509ExtensionUtils;
import org.bouncycastle.cert.X509v2CRLBuilder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.bc.BcX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v2CRLBuilder;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.BasicOCSPRespBuilder;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.CertificateStatus;
import org.bouncycastle.cert.ocsp.OCSPException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.cert.ocsp.OCSPRespBuilder;
import org.bouncycastle.cert.ocsp.Req;
import org.bouncycastle.cert.ocsp.UnknownStatus;
import org.bouncycastle.cert.ocsp.jcajce.JcaBasicOCSPRespBuilder;
import org.bouncycastle.cert.ocsp.jcajce.JcaCertificateID;
import org.bouncycastle.crypto.util.PublicKeyFactory;
import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.MiscPEMGenerator;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DigestCalculator;
import org.bouncycastle.operator.OperatorException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.io.pem.PemWriter;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;

import javax.net.ssl.SSLServerSocketFactory;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import static org.apache.zookeeper.test.ClientBase.CONNECTION_TIMEOUT;
import static org.apache.zookeeper.test.ClientBase.createTmpDir;
import static org.junit.Assert.fail;

public class QuorumSSLTest extends QuorumPeerTestBase {

    private static final String SSL_QUORUM_ENABLED = "sslQuorum=true\n";
    private static final String PORT_UNIFICATION_ENABLED = "portUnification=true\n";
    private static final String PORT_UNIFICATION_DISABLED = "portUnification=false\n";

    private static final char[] PASSWORD = "testpass".toCharArray();
    private static final String HOSTNAME = "localhost";

    private QuorumX509Util quorumX509Util;

    private MainThread q1;
    private MainThread q2;
    private MainThread q3;

    private int clientPortQp1;
    private int clientPortQp2;
    private int clientPortQp3;

    private String tmpDir;

    private String quorumConfiguration;
    private String validKeystorePath;
    private String truststorePath;

    private KeyPair rootKeyPair;
    private X509Certificate rootCertificate;

    private KeyPair defaultKeyPair;

    private ContentSigner contentSigner;

    private Date certStartTime;
    private Date certEndTime;

    @Rule
    public Timeout timeout = Timeout.builder().withTimeout(5, TimeUnit.MINUTES).withLookingForStuckThread(true)
            .build();

    @Before
    public void setup() throws Exception {
        quorumX509Util = new QuorumX509Util();
        ClientBase.setupTestEnv();

        tmpDir = createTmpDir().getAbsolutePath();

        clientPortQp1 = PortAssignment.unique();
        clientPortQp2 = PortAssignment.unique();
        clientPortQp3 = PortAssignment.unique();

        validKeystorePath = tmpDir + "/valid.jks";
        truststorePath = tmpDir + "/truststore.jks";

        quorumConfiguration = generateQuorumConfiguration();

        Security.addProvider(new BouncyCastleProvider());

        certStartTime = new Date();
        Calendar cal = Calendar.getInstance();
        cal.setTime(certStartTime);
        cal.add(Calendar.YEAR, 1);
        certEndTime = cal.getTime();

        rootKeyPair = createKeyPair();
        contentSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(rootKeyPair.getPrivate());
        rootCertificate = createSelfSignedCertifcate(rootKeyPair);

        // Write the truststore
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(null, PASSWORD);
        trustStore.setCertificateEntry(rootCertificate.getSubjectDN().toString(), rootCertificate);
        FileOutputStream outputStream = new FileOutputStream(truststorePath);
        trustStore.store(outputStream, PASSWORD);
        outputStream.flush();
        outputStream.close();

        defaultKeyPair = createKeyPair();
        X509Certificate validCertificate = buildEndEntityCert(defaultKeyPair, rootCertificate,
                rootKeyPair.getPrivate(), HOSTNAME, "127.0.0.1", null, null);
        writeKeystore(validCertificate, defaultKeyPair, validKeystorePath);

        setSSLSystemProperties();
    }

    private void writeKeystore(X509Certificate certificate, KeyPair entityKeyPair, String path) throws Exception {
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, PASSWORD);
        keyStore.setKeyEntry("alias", entityKeyPair.getPrivate(), PASSWORD, new Certificate[] { certificate });
        FileOutputStream outputStream = new FileOutputStream(path);
        keyStore.store(outputStream, PASSWORD);
        outputStream.flush();
        outputStream.close();
    }

    private class OCSPHandler implements HttpHandler {

        private X509Certificate revokedCert;

        // Builds an OCSPHandler that responds with a good status for all certificates
        // except revokedCert.
        public OCSPHandler(X509Certificate revokedCert) {
            this.revokedCert = revokedCert;
        }

        @Override
        public void handle(com.sun.net.httpserver.HttpExchange httpExchange) throws IOException {
            byte[] responseBytes;
            try {
                InputStream request = httpExchange.getRequestBody();
                byte[] requestBytes = new byte[10000];
                request.read(requestBytes);

                OCSPReq ocspRequest = new OCSPReq(requestBytes);
                Req[] requestList = ocspRequest.getRequestList();

                DigestCalculator digestCalculator = new JcaDigestCalculatorProviderBuilder().build()
                        .get(CertificateID.HASH_SHA1);

                BasicOCSPRespBuilder responseBuilder = new JcaBasicOCSPRespBuilder(rootKeyPair.getPublic(),
                        digestCalculator);
                for (Req req : requestList) {
                    CertificateID certId = req.getCertID();
                    CertificateID revokedCertId = new JcaCertificateID(digestCalculator, rootCertificate,
                            revokedCert.getSerialNumber());
                    CertificateStatus certificateStatus;
                    if (revokedCertId.equals(certId)) {
                        certificateStatus = new UnknownStatus();
                    } else {
                        certificateStatus = CertificateStatus.GOOD;
                    }

                    responseBuilder.addResponse(certId, certificateStatus, null);
                }

                X509CertificateHolder[] chain = new X509CertificateHolder[] {
                        new JcaX509CertificateHolder(rootCertificate) };
                ContentSigner signer = new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC")
                        .build(rootKeyPair.getPrivate());
                BasicOCSPResp ocspResponse = responseBuilder.build(signer, chain, Calendar.getInstance().getTime());

                responseBytes = new OCSPRespBuilder().build(OCSPRespBuilder.SUCCESSFUL, ocspResponse).getEncoded();
            } catch (OperatorException | CertificateEncodingException | OCSPException exception) {
                responseBytes = new OCSPResp(
                        new OCSPResponse(new OCSPResponseStatus(OCSPRespBuilder.INTERNAL_ERROR), null))
                                .getEncoded();
            }

            Headers rh = httpExchange.getResponseHeaders();
            rh.set("Content-Type", "application/ocsp-response");
            httpExchange.sendResponseHeaders(200, responseBytes.length);

            OutputStream os = httpExchange.getResponseBody();
            os.write(responseBytes);
            os.close();
        }
    }

    private X509Certificate createSelfSignedCertifcate(KeyPair keyPair) throws Exception {
        X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
        nameBuilder.addRDN(BCStyle.CN, HOSTNAME);
        BigInteger serialNumber = new BigInteger(128, new Random());

        X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(nameBuilder.build(),
                serialNumber, certStartTime, certEndTime, nameBuilder.build(), keyPair.getPublic())
                        .addExtension(Extension.basicConstraints, true, new BasicConstraints(0))
                        .addExtension(Extension.keyUsage, true,
                                new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign));

        return new JcaX509CertificateConverter().getCertificate(certificateBuilder.build(contentSigner));
    }

    private void buildCRL(X509Certificate x509Certificate, String crlPath) throws Exception {
        X509v2CRLBuilder builder = new JcaX509v2CRLBuilder(x509Certificate.getIssuerX500Principal(), certStartTime);
        builder.addCRLEntry(x509Certificate.getSerialNumber(), certStartTime, CRLReason.cACompromise);
        builder.setNextUpdate(certEndTime);
        builder.addExtension(Extension.authorityKeyIdentifier, false,
                new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(rootCertificate));
        builder.addExtension(Extension.cRLNumber, false, new CRLNumber(new BigInteger("1000")));

        X509CRLHolder cRLHolder = builder.build(contentSigner);

        PemWriter pemWriter = new PemWriter(new FileWriter(crlPath));
        pemWriter.writeObject(new MiscPEMGenerator(cRLHolder));
        pemWriter.flush();
        pemWriter.close();
    }

    public X509Certificate buildEndEntityCert(KeyPair keyPair, X509Certificate caCert, PrivateKey caPrivateKey,
            String hostname, String ipAddress, String crlPath, Integer ocspPort) throws Exception {
        X509CertificateHolder holder = new JcaX509CertificateHolder(caCert);
        ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(caPrivateKey);

        List<GeneralName> generalNames = new ArrayList<>();
        if (hostname != null) {
            generalNames.add(new GeneralName(GeneralName.dNSName, hostname));
        }

        if (ipAddress != null) {
            generalNames.add(new GeneralName(GeneralName.iPAddress, ipAddress));
        }

        SubjectPublicKeyInfo entityKeyInfo = SubjectPublicKeyInfoFactory
                .createSubjectPublicKeyInfo(PublicKeyFactory.createKey(keyPair.getPublic().getEncoded()));
        X509ExtensionUtils extensionUtils = new BcX509ExtensionUtils();
        X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(holder.getSubject(),
                new BigInteger(128, new Random()), certStartTime, certEndTime,
                new X500Name("CN=Test End Entity Certificate"), keyPair.getPublic())
                        .addExtension(Extension.authorityKeyIdentifier, false,
                                extensionUtils.createAuthorityKeyIdentifier(holder))
                        .addExtension(Extension.subjectKeyIdentifier, false,
                                extensionUtils.createSubjectKeyIdentifier(entityKeyInfo))
                        .addExtension(Extension.basicConstraints, true, new BasicConstraints(false))
                        .addExtension(Extension.keyUsage, true,
                                new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment));

        if (!generalNames.isEmpty()) {
            certificateBuilder.addExtension(Extension.subjectAlternativeName, true,
                    new GeneralNames(generalNames.toArray(new GeneralName[] {})));
        }

        if (crlPath != null) {
            DistributionPointName distPointOne = new DistributionPointName(
                    new GeneralNames(new GeneralName(GeneralName.uniformResourceIdentifier, "file://" + crlPath)));

            certificateBuilder.addExtension(Extension.cRLDistributionPoints, false,
                    new CRLDistPoint(new DistributionPoint[] { new DistributionPoint(distPointOne, null, null) }));
        }

        if (ocspPort != null) {
            certificateBuilder.addExtension(Extension.authorityInfoAccess, false, new AuthorityInformationAccess(
                    X509ObjectIdentifiers.ocspAccessMethod,
                    new GeneralName(GeneralName.uniformResourceIdentifier, "http://" + hostname + ":" + ocspPort)));
        }

        return new JcaX509CertificateConverter().getCertificate(certificateBuilder.build(signer));
    }

    private KeyPair createKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
        keyPairGenerator.initialize(4096);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        return keyPair;
    }

    private String generateQuorumConfiguration() {
        int portQp1 = PortAssignment.unique();
        int portQp2 = PortAssignment.unique();
        int portQp3 = PortAssignment.unique();

        int portLe1 = PortAssignment.unique();
        int portLe2 = PortAssignment.unique();
        int portLe3 = PortAssignment.unique();

        return "server.1=127.0.0.1:" + (portQp1) + ":" + (portLe1) + ";" + clientPortQp1 + "\n"
                + "server.2=127.0.0.1:" + (portQp2) + ":" + (portLe2) + ";" + clientPortQp2 + "\n"
                + "server.3=127.0.0.1:" + (portQp3) + ":" + (portLe3) + ";" + clientPortQp3;
    }

    public void setSSLSystemProperties() {
        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(quorumX509Util.getSslKeystoreLocationProperty(), validKeystorePath);
        System.setProperty(quorumX509Util.getSslKeystorePasswdProperty(), "testpass");
        System.setProperty(quorumX509Util.getSslTruststoreLocationProperty(), truststorePath);
        System.setProperty(quorumX509Util.getSslTruststorePasswdProperty(), "testpass");
    }

    @After
    public void cleanUp() throws Exception {
        clearSSLSystemProperties();
        if (q1 != null) {
            q1.shutdown();
        }
        if (q2 != null) {
            q2.shutdown();
        }
        if (q3 != null) {
            q3.shutdown();
        }

        Security.removeProvider("BC");
        quorumX509Util.close();
    }

    private void clearSSLSystemProperties() {
        System.clearProperty(quorumX509Util.getSslKeystoreLocationProperty());
        System.clearProperty(quorumX509Util.getSslKeystorePasswdProperty());
        System.clearProperty(quorumX509Util.getSslTruststoreLocationProperty());
        System.clearProperty(quorumX509Util.getSslTruststorePasswdProperty());
        System.clearProperty(quorumX509Util.getSslHostnameVerificationEnabledProperty());
        System.clearProperty(quorumX509Util.getSslOcspEnabledProperty());
        System.clearProperty(quorumX509Util.getSslCrlEnabledProperty());
        System.clearProperty(quorumX509Util.getCipherSuitesProperty());
        System.clearProperty(quorumX509Util.getSslProtocolProperty());
    }

    @Test
    public void testQuorumSSL() throws Exception {
        q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED);
        q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED);

        q1.start();
        q2.start();

        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT));
        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT));

        clearSSLSystemProperties();

        // This server should fail to join the quorum as it is not using ssl.
        q3 = new MainThread(3, clientPortQp3, quorumConfiguration);
        q3.start();

        Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT));
    }

    @Test
    public void testRollingUpgrade() throws Exception {
        // Form a quorum without ssl
        q1 = new MainThread(1, clientPortQp1, quorumConfiguration);
        q2 = new MainThread(2, clientPortQp2, quorumConfiguration);
        q3 = new MainThread(3, clientPortQp3, quorumConfiguration);

        Map<Integer, MainThread> members = new HashMap<>();
        members.put(clientPortQp1, q1);
        members.put(clientPortQp2, q2);
        members.put(clientPortQp3, q3);

        for (MainThread member : members.values()) {
            member.start();
        }

        for (int clientPort : members.keySet()) {
            Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPort, CONNECTION_TIMEOUT));
        }

        // Set SSL system properties and port unification, begin restarting servers
        setSSLSystemProperties();

        stopAppendConfigRestartAll(members, PORT_UNIFICATION_ENABLED);
        stopAppendConfigRestartAll(members, SSL_QUORUM_ENABLED);
        stopAppendConfigRestartAll(members, PORT_UNIFICATION_DISABLED);
    }

    private void stopAppendConfigRestartAll(Map<Integer, MainThread> members, String config) throws Exception {
        for (Map.Entry<Integer, MainThread> entry : members.entrySet()) {
            int clientPort = entry.getKey();
            MainThread member = entry.getValue();

            member.shutdown();
            Assert.assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPort, CONNECTION_TIMEOUT));

            FileWriter fileWriter = new FileWriter(member.getConfFile(), true);
            fileWriter.write(config);
            fileWriter.flush();
            fileWriter.close();

            member.start();

            Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPort, CONNECTION_TIMEOUT));
        }
    }

    @Test
    public void testHostnameVerificationWithInvalidHostname() throws Exception {
        String badhostnameKeystorePath = tmpDir + "/badhost.jks";
        X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(),
                "bleepbloop", null, null, null);
        writeKeystore(badHostCert, defaultKeyPair, badhostnameKeystorePath);

        testHostnameVerification(badhostnameKeystorePath, false);
    }

    @Test
    public void testHostnameVerificationWithInvalidIPAddress() throws Exception {
        String badhostnameKeystorePath = tmpDir + "/badhost.jks";
        X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(),
                null, "140.211.11.105", null, null);
        writeKeystore(badHostCert, defaultKeyPair, badhostnameKeystorePath);

        testHostnameVerification(badhostnameKeystorePath, false);
    }

    @Test
    public void testHostnameVerificationWithInvalidIpAddressAndInvalidHostname() throws Exception {
        String badhostnameKeystorePath = tmpDir + "/badhost.jks";
        X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(),
                "bleepbloop", "140.211.11.105", null, null);
        writeKeystore(badHostCert, defaultKeyPair, badhostnameKeystorePath);

        testHostnameVerification(badhostnameKeystorePath, false);
    }

    @Test
    public void testHostnameVerificationWithInvalidIpAddressAndValidHostname() throws Exception {
        String badhostnameKeystorePath = tmpDir + "/badhost.jks";
        X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(),
                "localhost", "140.211.11.105", null, null);
        writeKeystore(badHostCert, defaultKeyPair, badhostnameKeystorePath);

        testHostnameVerification(badhostnameKeystorePath, true);
    }

    @Test
    public void testHostnameVerificationWithValidIpAddressAndInvalidHostname() throws Exception {
        String badhostnameKeystorePath = tmpDir + "/badhost.jks";
        X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(),
                "bleepbloop", "127.0.0.1", null, null);
        writeKeystore(badHostCert, defaultKeyPair, badhostnameKeystorePath);

        testHostnameVerification(badhostnameKeystorePath, true);
    }

    /**
     * @param keystorePath The keystore to use
     * @param expectSuccess True for expecting the keystore to pass hostname verification, false for expecting failure
     * @throws Exception
     */
    private void testHostnameVerification(String keystorePath, boolean expectSuccess) throws Exception {
        System.setProperty(quorumX509Util.getSslHostnameVerificationEnabledProperty(), "false");

        q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED);
        q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED);

        q1.start();
        q2.start();

        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT));
        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT));

        System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), keystorePath);

        // This server should join successfully
        q3 = new MainThread(3, clientPortQp3, quorumConfiguration, SSL_QUORUM_ENABLED);
        q3.start();

        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT));

        q1.shutdown();
        q2.shutdown();
        q3.shutdown();

        Assert.assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT));
        Assert.assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT));
        Assert.assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT));

        setSSLSystemProperties();
        System.clearProperty(quorumX509Util.getSslHostnameVerificationEnabledProperty());

        q1.start();
        q2.start();

        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT));
        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT));

        System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), keystorePath);
        q3.start();

        Assert.assertEquals(expectSuccess,
                ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT));
    }

    @Test
    public void testCertificateRevocationList() throws Exception {
        q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED);
        q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED);

        q1.start();
        q2.start();

        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT));
        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT));

        String revokedInCRLKeystorePath = tmpDir + "/crl_revoked.jks";
        String crlPath = tmpDir + "/crl.pem";
        X509Certificate revokedInCRLCert = buildEndEntityCert(defaultKeyPair, rootCertificate,
                rootKeyPair.getPrivate(), HOSTNAME, null, crlPath, null);
        writeKeystore(revokedInCRLCert, defaultKeyPair, revokedInCRLKeystorePath);
        buildCRL(revokedInCRLCert, crlPath);

        System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), revokedInCRLKeystorePath);

        // This server should join successfully
        q3 = new MainThread(3, clientPortQp3, quorumConfiguration, SSL_QUORUM_ENABLED);
        q3.start();

        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT));

        q1.shutdown();
        q2.shutdown();
        q3.shutdown();

        Assert.assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT));
        Assert.assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT));
        Assert.assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT));

        setSSLSystemProperties();
        System.setProperty(quorumX509Util.getSslCrlEnabledProperty(), "true");

        X509Certificate validCertificate = buildEndEntityCert(defaultKeyPair, rootCertificate,
                rootKeyPair.getPrivate(), HOSTNAME, null, crlPath, null);
        writeKeystore(validCertificate, defaultKeyPair, validKeystorePath);

        q1.start();
        q2.start();

        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT));
        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT));

        System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), revokedInCRLKeystorePath);
        q3.start();

        Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT));
    }

    @Test
    public void testOCSP() throws Exception {
        Integer ocspPort = PortAssignment.unique();

        q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED);
        q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED);

        q1.start();
        q2.start();

        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT));
        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT));

        String revokedInOCSPKeystorePath = tmpDir + "/ocsp_revoked.jks";
        X509Certificate revokedInOCSPCert = buildEndEntityCert(defaultKeyPair, rootCertificate,
                rootKeyPair.getPrivate(), HOSTNAME, null, null, ocspPort);
        writeKeystore(revokedInOCSPCert, defaultKeyPair, revokedInOCSPKeystorePath);

        HttpServer ocspServer = HttpServer.create(new InetSocketAddress(ocspPort), 0);
        try {
            ocspServer.createContext("/", new OCSPHandler(revokedInOCSPCert));
            ocspServer.start();

            System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), revokedInOCSPKeystorePath);

            // This server should join successfully
            q3 = new MainThread(3, clientPortQp3, quorumConfiguration, SSL_QUORUM_ENABLED);
            q3.start();

            Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT));

            q1.shutdown();
            q2.shutdown();
            q3.shutdown();

            Assert.assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT));
            Assert.assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT));
            Assert.assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT));

            setSSLSystemProperties();
            System.setProperty(quorumX509Util.getSslOcspEnabledProperty(), "true");

            X509Certificate validCertificate = buildEndEntityCert(defaultKeyPair, rootCertificate,
                    rootKeyPair.getPrivate(), HOSTNAME, null, null, ocspPort);
            writeKeystore(validCertificate, defaultKeyPair, validKeystorePath);

            q1.start();
            q2.start();

            Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT));
            Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT));

            System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), revokedInOCSPKeystorePath);
            q3.start();

            Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT));
        } finally {
            ocspServer.stop(0);
        }
    }

    @Test
    public void testCipherSuites() throws Exception {
        // Get default cipher suites from JDK
        SSLServerSocketFactory ssf = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
        List<String> defaultCiphers = new ArrayList<String>();
        for (String cipher : ssf.getDefaultCipherSuites()) {
            if (!cipher.matches(".*EMPTY.*") && cipher.startsWith("TLS") && cipher.contains("RSA")) {
                defaultCiphers.add(cipher);
            }
        }

        if (defaultCiphers.size() < 2) {
            fail("JDK has to support at least 2 valid (RSA) cipher suites for this test to run");
        }

        // Use them all except one to build the ensemble
        String suitesOfEnsemble = String.join(",", defaultCiphers.subList(1, defaultCiphers.size()));
        System.setProperty(quorumX509Util.getCipherSuitesProperty(), suitesOfEnsemble);

        q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED);
        q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED);

        q1.start();
        q2.start();

        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT));
        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT));

        // Use the odd one out for the client
        String suiteOfClient = defaultCiphers.get(0);
        System.setProperty(quorumX509Util.getCipherSuitesProperty(), suiteOfClient);

        // This server should fail to join the quorum as it is not using one of the supported suites from the other
        // quorum members
        q3 = new MainThread(3, clientPortQp3, quorumConfiguration, SSL_QUORUM_ENABLED);
        q3.start();

        Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT));
    }

    @Test
    public void testProtocolVersion() throws Exception {
        System.setProperty(quorumX509Util.getSslProtocolProperty(), "TLSv1.2");

        q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED);
        q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED);

        q1.start();
        q2.start();

        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT));
        Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT));

        System.setProperty(quorumX509Util.getSslProtocolProperty(), "TLSv1.1");

        // This server should fail to join the quorum as it is not using TLSv1.2
        q3 = new MainThread(3, clientPortQp3, quorumConfiguration, SSL_QUORUM_ENABLED);
        q3.start();

        Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT));
    }
}