| /* |
| * 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 static org.apache.zookeeper.test.ClientBase.CONNECTION_TIMEOUT; |
| import static org.apache.zookeeper.test.ClientBase.createTmpDir; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| import com.sun.net.httpserver.Headers; |
| import com.sun.net.httpserver.HttpHandler; |
| import com.sun.net.httpserver.HttpServer; |
| 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 javax.net.ssl.SSLServerSocketFactory; |
| 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.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.Timeout; |
| |
| 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()); |
| |
| JcaX509v3CertificateBuilder jcaX509v3CertificateBuilder = new JcaX509v3CertificateBuilder( |
| nameBuilder.build(), |
| serialNumber, |
| certStartTime, |
| certEndTime, |
| nameBuilder.build(), |
| keyPair.getPublic()); |
| X509v3CertificateBuilder certificateBuilder = jcaX509v3CertificateBuilder |
| .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(); |
| JcaX509v3CertificateBuilder jcaX509v3CertificateBuilder = new JcaX509v3CertificateBuilder( |
| holder.getSubject(), |
| new BigInteger(128, new Random()), |
| certStartTime, |
| certEndTime, |
| new X500Name("CN=Test End Entity Certificate"), |
| keyPair.getPublic()); |
| X509v3CertificateBuilder certificateBuilder = jcaX509v3CertificateBuilder |
| .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() { |
| StringBuilder sb = new StringBuilder(); |
| |
| int portQp1 = PortAssignment.unique(); |
| int portQp2 = PortAssignment.unique(); |
| int portQp3 = PortAssignment.unique(); |
| |
| int portLe1 = PortAssignment.unique(); |
| int portLe2 = PortAssignment.unique(); |
| int portLe3 = PortAssignment.unique(); |
| |
| sb.append(String.format("server.1=127.0.0.1:%d:%d;%d\n", portQp1, portLe1, clientPortQp1)); |
| sb.append(String.format("server.2=127.0.0.1:%d:%d;%d\n", portQp2, portLe2, clientPortQp2)); |
| sb.append(String.format("server.3=127.0.0.1:%d:%d;%d\n", portQp3, portLe3, clientPortQp3)); |
| |
| return sb.toString(); |
| } |
| |
| private String generateMultiAddressQuorumConfiguration() { |
| StringBuilder sb = new StringBuilder(); |
| |
| int portQp1a = PortAssignment.unique(); |
| int portQp1b = PortAssignment.unique(); |
| int portQp2a = PortAssignment.unique(); |
| int portQp2b = PortAssignment.unique(); |
| int portQp3a = PortAssignment.unique(); |
| int portQp3b = PortAssignment.unique(); |
| |
| int portLe1a = PortAssignment.unique(); |
| int portLe1b = PortAssignment.unique(); |
| int portLe2a = PortAssignment.unique(); |
| int portLe2b = PortAssignment.unique(); |
| int portLe3a = PortAssignment.unique(); |
| int portLe3b = PortAssignment.unique(); |
| |
| sb.append(String.format("server.1=127.0.0.1:%d:%d|127.0.0.1:%d:%d;%d\n", portQp1a, portLe1a, portQp1b, portLe1b, clientPortQp1)); |
| sb.append(String.format("server.2=127.0.0.1:%d:%d|127.0.0.1:%d:%d;%d\n", portQp2a, portLe2a, portQp2b, portLe2b, clientPortQp2)); |
| sb.append(String.format("server.3=127.0.0.1:%d:%d|127.0.0.1:%d:%d;%d\n", portQp3a, portLe3a, portQp3b, portLe3b, clientPortQp3)); |
| |
| return sb.toString(); |
| } |
| |
| 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(); |
| |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); |
| 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(); |
| |
| assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); |
| } |
| |
| |
| @Test |
| public void testQuorumSSLWithMultipleAddresses() throws Exception { |
| quorumConfiguration = generateMultiAddressQuorumConfiguration(); |
| |
| q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED); |
| q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED); |
| |
| q1.start(); |
| q2.start(); |
| |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); |
| 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(); |
| |
| 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()) { |
| 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(); |
| 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(); |
| |
| 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 testHostnameVerificationForInvalidMultiAddressServerConfig() throws Exception { |
| quorumConfiguration = generateMultiAddressQuorumConfiguration(); |
| |
| 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(); |
| |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); |
| 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(); |
| |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); |
| |
| q1.shutdown(); |
| q2.shutdown(); |
| q3.shutdown(); |
| |
| assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); |
| assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT)); |
| assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); |
| |
| setSSLSystemProperties(); |
| System.clearProperty(quorumX509Util.getSslHostnameVerificationEnabledProperty()); |
| |
| q1.start(); |
| q2.start(); |
| |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT)); |
| |
| System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), keystorePath); |
| q3.start(); |
| |
| 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(); |
| |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); |
| 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(); |
| |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); |
| |
| q1.shutdown(); |
| q2.shutdown(); |
| q3.shutdown(); |
| |
| assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); |
| assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT)); |
| 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(); |
| |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT)); |
| |
| System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), revokedInCRLKeystorePath); |
| q3.start(); |
| |
| 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(); |
| |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); |
| 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(); |
| |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); |
| |
| q1.shutdown(); |
| q2.shutdown(); |
| q3.shutdown(); |
| |
| assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); |
| assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT)); |
| 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(); |
| |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT)); |
| |
| System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), revokedInOCSPKeystorePath); |
| q3.start(); |
| |
| 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(); |
| |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); |
| 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(); |
| |
| 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(); |
| |
| assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); |
| 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(); |
| |
| assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); |
| } |
| |
| } |