[ https://issues.apache.org/jira/browse/ZOOKEEPER-236?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=15968204#comment-15968204 ]
ASF GitHub Bot commented on ZOOKEEPER-236: ------------------------------------------ Github user hanm commented on a diff in the pull request: https://github.com/apache/zookeeper/pull/184#discussion_r111487159 --- Diff: src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java --- @@ -0,0 +1,668 @@ +/** + * 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 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.server.quorum.QuorumPeerTestBase; +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.Test; + +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 static org.apache.zookeeper.test.ClientBase.CONNECTION_TIMEOUT; +import static org.apache.zookeeper.test.ClientBase.createTmpDir; + +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 = new 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; + + @Before + public void setup() throws Exception { + 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()); + + 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, null, 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); + Date notBefore = new Date(); // time from which certificate is valid + Calendar cal = Calendar.getInstance(); + cal.setTime(notBefore); + cal.add(Calendar.YEAR, 1); + Date notAfter = cal.getTime(); + BigInteger serialNumber = new BigInteger(128, new Random()); + + X509v3CertificateBuilder certificateBuilder = + new JcaX509v3CertificateBuilder(nameBuilder.build(), serialNumber, notBefore, notAfter, 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(), new Date()); + Date notBefore = new Date(); + Calendar cal = Calendar.getInstance(); + cal.setTime(notBefore); + cal.add(Calendar.YEAR, 1); + Date notAfter = cal.getTime(); + builder.setNextUpdate(notAfter); + builder.addCRLEntry(x509Certificate.getSerialNumber(), new Date(), CRLReason.cACompromise); + 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()), + new Date(System.currentTimeMillis()), new Date(System.currentTimeMillis() + 100000), + 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"); + System.setProperty(quorumX509Util.getSslHostnameVerificationEnabledProperty(), "false"); + } + + @After + public void cleanUp() throws Exception { + clearSSLSystemProperties(); + q1.shutdown(); + q2.shutdown(); + q3.shutdown(); + + Security.removeProvider("BC"); + } + + 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()); + } + + @Test(timeout = 300000) + 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(timeout = 300000) + public void testRollingUpgrade() throws Exception { --- End diff -- This test is pretty flaky - please check internal Jenkins to diagnose. > SSL Support for Atomic Broadcast protocol > ----------------------------------------- > > Key: ZOOKEEPER-236 > URL: https://issues.apache.org/jira/browse/ZOOKEEPER-236 > Project: ZooKeeper > Issue Type: New Feature > Components: quorum, server > Reporter: Benjamin Reed > Assignee: Abraham Fine > Priority: Minor > > We should have the ability to use SSL to authenticate and encrypt the traffic > between ZooKeeper servers. For the most part this is a very easy change. We > would probably only want to support this for TCP based leader elections. -- This message was sent by Atlassian JIRA (v6.3.15#6346)