From 2ab6c5e1b950c42c1be8d4f6c2bb0c98666e3a36 Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Mon, 6 Mar 2017 15:12:59 -0800 Subject: [PATCH 01/25] ZOOKEEPER-236: SSL Support for Atomic Broadcast protocol --- .../org/apache/zookeeper/common/X509Util.java | 46 ++++++++++++++++ .../apache/zookeeper/server/ServerConfig.java | 2 + .../server/quorum/BufferedServerSocket.java | 48 +++++++++++++++++ .../server/quorum/BufferedSocket.java | 54 +++++++++++++++++++ .../zookeeper/server/quorum/Leader.java | 32 +++++++++-- .../zookeeper/server/quorum/Learner.java | 28 +++++++--- .../server/quorum/QuorumCnxManager.java | 41 ++++++++++++-- .../zookeeper/server/quorum/QuorumPeer.java | 22 +++++++- .../server/quorum/QuorumPeerConfig.java | 13 +++++ .../server/quorum/QuorumPeerMain.java | 2 + .../server/quorum/RaceConditionTest.java | 3 +- .../zookeeper/server/quorum/Zab1_0Test.java | 7 +-- 12 files changed, 277 insertions(+), 21 deletions(-) create mode 100644 src/java/main/org/apache/zookeeper/server/quorum/BufferedServerSocket.java create mode 100644 src/java/main/org/apache/zookeeper/server/quorum/BufferedSocket.java diff --git a/src/java/main/org/apache/zookeeper/common/X509Util.java b/src/java/main/org/apache/zookeeper/common/X509Util.java index c48d6941a06..a0a1ab51e44 100644 --- a/src/java/main/org/apache/zookeeper/common/X509Util.java +++ b/src/java/main/org/apache/zookeeper/common/X509Util.java @@ -21,6 +21,8 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509KeyManager; @@ -28,8 +30,12 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.net.Socket; import java.security.KeyStore; +import org.apache.zookeeper.server.quorum.BufferedSocket; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.handler.ssl.SslHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -199,4 +205,44 @@ public static X509TrustManager createTrustManager(String trustStoreLocation, Str } } } + + public static SSLSocket createSSLSocket() throws X509Exception, IOException { + SSLSocket sslSocket = (SSLSocket) createSSLContext().getSocketFactory().createSocket(); + sslSocket.setNeedClientAuth(true); + return sslSocket; + } + + + public static SSLServerSocket createSSLServerSocket() throws X509Exception, IOException { + SSLServerSocket sslServerSocket = (SSLServerSocket) createSSLContext().getServerSocketFactory().createServerSocket(); + sslServerSocket.setNeedClientAuth(true); + return sslServerSocket; + } + + public static SSLServerSocket createSSLServerSocket(int port) throws X509Exception, IOException { + SSLServerSocket sslServerSocket = (SSLServerSocket) createSSLContext().getServerSocketFactory().createServerSocket(port); + sslServerSocket.setNeedClientAuth(true); + return sslServerSocket; + } + + public static Socket createUnifiedSocket(BufferedSocket socket) throws X509Exception, IOException { + socket.getInputStream().mark(6); + + byte[] litmus = new byte[5]; + socket.getInputStream().read(litmus, 0, 5); + + socket.getInputStream().reset(); + + boolean isSsl = SslHandler.isEncrypted(ChannelBuffers.wrappedBuffer(litmus)); + if (isSsl) { + LOG.info(socket.getInetAddress() + " attempting to connect over ssl"); + SSLSocket sslSocket = (SSLSocket) createSSLContext().getSocketFactory().createSocket(socket, null, socket.getPort(), false); + sslSocket.setUseClientMode(false); + return sslSocket; + } else { + LOG.info(socket.getInetAddress() + " attempting to connect without ssl"); + return socket; + } + + } } \ No newline at end of file diff --git a/src/java/main/org/apache/zookeeper/server/ServerConfig.java b/src/java/main/org/apache/zookeeper/server/ServerConfig.java index 444e126ae97..d9fe8840ac7 100644 --- a/src/java/main/org/apache/zookeeper/server/ServerConfig.java +++ b/src/java/main/org/apache/zookeeper/server/ServerConfig.java @@ -46,6 +46,7 @@ public class ServerConfig { protected int minSessionTimeout = -1; /** defaults to -1 if not set explicitly */ protected int maxSessionTimeout = -1; + protected boolean sslQuorum; /** * Parse arguments for server configuration @@ -97,6 +98,7 @@ public void readFrom(QuorumPeerConfig config) { maxClientCnxns = config.getMaxClientCnxns(); minSessionTimeout = config.getMinSessionTimeout(); maxSessionTimeout = config.getMaxSessionTimeout(); + sslQuorum = config.isSslQuorum(); } public InetSocketAddress getClientPortAddress() { diff --git a/src/java/main/org/apache/zookeeper/server/quorum/BufferedServerSocket.java b/src/java/main/org/apache/zookeeper/server/quorum/BufferedServerSocket.java new file mode 100644 index 00000000000..95fcc157570 --- /dev/null +++ b/src/java/main/org/apache/zookeeper/server/quorum/BufferedServerSocket.java @@ -0,0 +1,48 @@ +/** + * 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 java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; + +public class BufferedServerSocket extends ServerSocket { + + public BufferedServerSocket() throws IOException { + super(); + } + + public BufferedServerSocket(int port) throws IOException { + super(port); + } + + @Override + public Socket accept() throws IOException { + if (isClosed()) { + throw new SocketException("Socket is closed"); + } + if (!isBound()) { + throw new SocketException("Socket is not bound yet"); + } + final Socket bufferedSocket = new BufferedSocket(null); + implAccept(bufferedSocket); + return bufferedSocket; + } +} \ No newline at end of file diff --git a/src/java/main/org/apache/zookeeper/server/quorum/BufferedSocket.java b/src/java/main/org/apache/zookeeper/server/quorum/BufferedSocket.java new file mode 100644 index 00000000000..7852b06afc2 --- /dev/null +++ b/src/java/main/org/apache/zookeeper/server/quorum/BufferedSocket.java @@ -0,0 +1,54 @@ +/** + * 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 java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.net.Socket; +import java.net.SocketImpl; + +public class BufferedSocket extends Socket { + + private BufferedInputStream bufferedInputStream; + private BufferedOutputStream bufferedOutputStream; + + public BufferedSocket(SocketImpl base) throws IOException { + super(base); + } + + @Override + public BufferedInputStream getInputStream() throws IOException { + if (bufferedInputStream == null) { + bufferedInputStream = new BufferedInputStream(super.getInputStream()); + } + + return bufferedInputStream; + } + + @Override + public BufferedOutputStream getOutputStream() throws IOException { + if (bufferedOutputStream == null) { + bufferedOutputStream = new BufferedOutputStream(super.getOutputStream()); + } + + return bufferedOutputStream; + } + +} \ No newline at end of file diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Leader.java b/src/java/main/org/apache/zookeeper/server/quorum/Leader.java index 13f7ec97183..10fe83101cb 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Leader.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Leader.java @@ -41,6 +41,8 @@ import org.apache.jute.BinaryOutputArchive; import org.apache.zookeeper.ZooDefs.OpCode; import org.apache.zookeeper.common.Time; +import org.apache.zookeeper.common.X509Exception; +import org.apache.zookeeper.common.X509Util; import org.apache.zookeeper.server.FinalRequestProcessor; import org.apache.zookeeper.server.Request; import org.apache.zookeeper.server.RequestProcessor; @@ -216,18 +218,35 @@ public boolean isQuorumSynced(QuorumVerifier qv) { private final ServerSocket ss; - Leader(QuorumPeer self,LeaderZooKeeperServer zk) throws IOException { + Leader(QuorumPeer self,LeaderZooKeeperServer zk) throws IOException, X509Exception { this.self = self; try { - if (self.getQuorumListenOnAllIPs()) { - ss = new ServerSocket(self.getQuorumAddress().getPort()); + if (self.shouldUsePortUnification()) { + if (self.getQuorumListenOnAllIPs()) { + ss = new BufferedServerSocket(self.getQuorumAddress().getPort()); + } else { + ss = new BufferedServerSocket(); + } + } else if (self.isSslQuorum()) { + if (self.getQuorumListenOnAllIPs()) { + ss = X509Util.createSSLServerSocket(self.getQuorumAddress().getPort()); + } else { + ss = X509Util.createSSLServerSocket(); + } } else { - ss = new ServerSocket(); + if (self.getQuorumListenOnAllIPs()) { + ss = new ServerSocket(self.getQuorumAddress().getPort()); + } else { + ss = new ServerSocket(); + } } ss.setReuseAddress(true); if (!self.getQuorumListenOnAllIPs()) { ss.bind(self.getQuorumAddress()); } + } catch (X509Exception e) { + LOG.error("failed to setup ssl server socket", e); + throw e; } catch (BindException e) { if (self.getQuorumListenOnAllIPs()) { LOG.error("Couldn't bind to port " + self.getQuorumAddress().getPort(), e); @@ -362,6 +381,11 @@ public void run() { while (!stop) { try{ Socket s = ss.accept(); + + if (self.shouldUsePortUnification()) { + s = X509Util.createUnifiedSocket((BufferedSocket) s); + } + // start with the initLimit, once the ack is processed // in LearnerHandler switch to the syncLimit s.setSoTimeout(self.tickTime * self.initLimit); diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Learner.java b/src/java/main/org/apache/zookeeper/server/quorum/Learner.java index f048da8a4d9..421dccc7a6b 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Learner.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Learner.java @@ -38,6 +38,8 @@ import org.apache.jute.InputArchive; import org.apache.jute.OutputArchive; import org.apache.jute.Record; +import org.apache.zookeeper.common.X509Exception; +import org.apache.zookeeper.common.X509Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.zookeeper.ZooDefs.OpCode; @@ -51,6 +53,8 @@ import org.apache.zookeeper.txn.SetDataTxn; import org.apache.zookeeper.txn.TxnHeader; +import javax.net.ssl.SSLSocket; + /** * This class is the superclass of two of the three main actors in a ZK * ensemble: Followers and Observers. Both Followers and Observers share @@ -236,9 +240,8 @@ protected void sockConnect(Socket sock, InetSocketAddress addr, int timeout) * @throws InterruptedException */ protected void connectToLeader(InetSocketAddress addr) - throws IOException, ConnectException, InterruptedException { - sock = new Socket(); - sock.setSoTimeout(self.tickTime * self.initLimit); + throws IOException, InterruptedException, X509Exception { + createSocket(); int initLimitTime = self.tickTime * self.initLimit; int remainingInitLimitTime = initLimitTime; @@ -254,6 +257,9 @@ protected void connectToLeader(InetSocketAddress addr) } sockConnect(sock, addr, Math.min(self.tickTime * self.syncLimit, remainingInitLimitTime)); + if (self.isSslQuorum()) { + ((SSLSocket) sock).startHandshake(); + } sock.setTcpNoDelay(nodelay); break; } catch (IOException e) { @@ -273,8 +279,7 @@ protected void connectToLeader(InetSocketAddress addr) LOG.warn("Unexpected exception, tries=" + tries + ", remaining init limit=" + remainingInitLimitTime + ", connecting to " + addr,e); - sock = new Socket(); - sock.setSoTimeout(self.tickTime * self.initLimit); + createSocket(); } } Thread.sleep(1000); @@ -283,8 +288,17 @@ protected void connectToLeader(InetSocketAddress addr) sock.getInputStream())); bufferedOutput = new BufferedOutputStream(sock.getOutputStream()); leaderOs = BinaryOutputArchive.getArchive(bufferedOutput); - } - + } + + private void createSocket() throws X509Exception, IOException { + if (self.isSslQuorum()) { + sock = X509Util.createSSLSocket(); + } else { + sock = new Socket(); + } + sock.setSoTimeout(self.tickTime * self.initLimit); + } + /** * Once connected to the leader, perform the handshake protocol to * establish a following / observing connection. diff --git a/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java b/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java index 57cf8d91575..7e03e738629 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java @@ -37,11 +37,15 @@ import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicInteger; +import org.apache.zookeeper.common.X509Exception; +import org.apache.zookeeper.common.X509Util; import org.apache.zookeeper.server.ZooKeeperThread; import org.apache.zookeeper.server.quorum.flexible.QuorumVerifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLSocket; + /** * This class implements a connection manager for leader election using TCP. It * maintains one connection for every pair of servers. The tricky part is to @@ -438,9 +442,18 @@ synchronized private boolean connectOne(long sid, InetSocketAddress electionAddr Socket sock = null; try { LOG.debug("Opening channel to server " + sid); - sock = new Socket(); - setSockOpts(sock); - sock.connect(electionAddr, cnxTO); + if (self.isSslQuorum()) { + SSLSocket sslSock = (SSLSocket) X509Util.createSSLContext().getSocketFactory().createSocket(); + setSockOpts(sslSock); + sslSock.connect(electionAddr, cnxTO); + sslSock.startHandshake(); + sock = sslSock; + } else { + sock = new Socket(); + setSockOpts(sock); + sock.connect(electionAddr, cnxTO); + } + LOG.debug("Connected to server " + sid); initiateConnection(sock, sid); return true; @@ -453,6 +466,11 @@ synchronized private boolean connectOne(long sid, InetSocketAddress electionAddr + " at election address " + electionAddr, e); closeSocket(sock); throw e; + } catch (X509Exception e) { + LOG.warn("Cannot open secure channel to " + sid + + " at election address " + electionAddr, e); + closeSocket(sock); + return false; } catch (IOException e) { LOG.warn("Cannot open channel to " + sid + " at election address " + electionAddr, @@ -622,8 +640,16 @@ public void run() { Socket client = null; while((!shutdown) && (numRetries < 3)){ try { - ss = new ServerSocket(); + if (self.shouldUsePortUnification()) { + ss = new BufferedServerSocket(); + } else if (self.isSslQuorum()) { + ss = X509Util.createSSLServerSocket(); + } else { + ss = new ServerSocket(); + } + ss.setReuseAddress(true); + if (self.getQuorumListenOnAllIPs()) { int port = self.getElectionAddress().getPort(); addr = new InetSocketAddress(port); @@ -638,13 +664,18 @@ public void run() { ss.bind(addr); while (!shutdown) { client = ss.accept(); + + if (self.shouldUsePortUnification()) { + client = X509Util.createUnifiedSocket((BufferedSocket) client); + } + setSockOpts(client); LOG.info("Received connection request " + client.getRemoteSocketAddress()); receiveConnection(client); numRetries = 0; } - } catch (IOException e) { + } catch (IOException|X509Exception e) { if (shutdown) { break; } diff --git a/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeer.java b/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeer.java index 38b02993739..136eeea018f 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeer.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeer.java @@ -47,6 +47,7 @@ import org.apache.zookeeper.common.AtomicFileWritingIdiom; import org.apache.zookeeper.common.AtomicFileWritingIdiom.WriterStatement; import org.apache.zookeeper.common.Time; +import org.apache.zookeeper.common.X509Exception; import org.apache.zookeeper.jmx.MBeanRegistry; import org.apache.zookeeper.jmx.ZKMBeanInfo; import org.apache.zookeeper.server.ServerCnxnFactory; @@ -451,6 +452,17 @@ public long getId() { return myid; } + private boolean sslQuorum; + private boolean shouldUsePortUnification; + + public boolean isSslQuorum() { + return sslQuorum; + } + + public boolean shouldUsePortUnification() { + return shouldUsePortUnification; + } + /** * This is who I think the leader currently is. */ @@ -938,7 +950,7 @@ protected Follower makeFollower(FileTxnSnapLog logFactory) throws IOException { return new Follower(this, new FollowerZooKeeperServer(logFactory, this, this.zkDb)); } - protected Leader makeLeader(FileTxnSnapLog logFactory) throws IOException { + protected Leader makeLeader(FileTxnSnapLog logFactory) throws IOException, X509Exception { return new Leader(this, new LeaderZooKeeperServer(logFactory, this, this.zkDb)); } @@ -1615,6 +1627,14 @@ public void setSecureCnxnFactory(ServerCnxnFactory secureCnxnFactory) { this.secureCnxnFactory = secureCnxnFactory; } + public void setSslQuorum(boolean sslQuorum) { + this.sslQuorum = sslQuorum; + } + + public void setUsePortUnification(boolean shouldUsePortUnification) { + this.shouldUsePortUnification = shouldUsePortUnification; + } + private void startServerCnxnFactory() { if (cnxnFactory != null) { cnxnFactory.start(); diff --git a/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java b/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java index 3dbc30ac0ee..8066deb1342 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java @@ -64,6 +64,8 @@ public class QuorumPeerConfig { protected InetSocketAddress clientPortAddress; protected InetSocketAddress secureClientPortAddress; + protected boolean sslQuorum = false; + protected boolean shouldUsePortUnification = false; protected File dataDir; protected File dataLogDir; protected String dynamicConfigFileStr = null; @@ -290,6 +292,10 @@ public void parseProperties(Properties zkProp) } else { throw new ConfigException("Invalid option " + value + " for reconfigEnabled flag. Choose 'true' or 'false.'"); } + } else if (key.equals("sslQuorum")){ + sslQuorum = Boolean.parseBoolean(value); + } else if (key.equals("portUnification")){ + shouldUsePortUnification = Boolean.parseBoolean(value); } else if ((key.startsWith("server.") || key.startsWith("group") || key.startsWith("weight")) && zkProp.containsKey("dynamicConfigFile")) { throw new ConfigException("parameter: " + key + " must be in a separate dynamic config file"); } else { @@ -686,6 +692,13 @@ public void checkValidity() throws IOException, ConfigException{ public boolean isLocalSessionsUpgradingEnabled() { return localSessionsUpgradingEnabled; } + public boolean isSslQuorum() { + return sslQuorum; + } + + public boolean shouldUsePortUnification() { + return shouldUsePortUnification; + } public int getInitLimit() { return initLimit; } public int getSyncLimit() { return syncLimit; } diff --git a/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerMain.java b/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerMain.java index cde193e8685..4633771a1a0 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerMain.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerMain.java @@ -178,6 +178,8 @@ public void runFromConfig(QuorumPeerConfig config) quorumPeer.initConfigInZKDatabase(); quorumPeer.setCnxnFactory(cnxnFactory); quorumPeer.setSecureCnxnFactory(secureCnxnFactory); + quorumPeer.setSslQuorum(config.isSslQuorum()); + quorumPeer.setUsePortUnification(config.shouldUsePortUnification()); quorumPeer.setLearnerType(config.getPeerType()); quorumPeer.setSyncEnabled(config.getSyncEnabled()); quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs()); diff --git a/src/java/test/org/apache/zookeeper/server/quorum/RaceConditionTest.java b/src/java/test/org/apache/zookeeper/server/quorum/RaceConditionTest.java index c65a7944d49..d68f6c6155b 100644 --- a/src/java/test/org/apache/zookeeper/server/quorum/RaceConditionTest.java +++ b/src/java/test/org/apache/zookeeper/server/quorum/RaceConditionTest.java @@ -27,6 +27,7 @@ import org.apache.zookeeper.PortAssignment; import org.apache.zookeeper.ZooDefs; +import org.apache.zookeeper.common.X509Exception; import org.apache.zookeeper.server.FinalRequestProcessor; import org.apache.zookeeper.server.PrepRequestProcessor; import org.apache.zookeeper.server.Request; @@ -177,7 +178,7 @@ protected void processPacket(QuorumPacket qp) throws Exception { } @Override - protected Leader makeLeader(FileTxnSnapLog logFactory) throws IOException { + protected Leader makeLeader(FileTxnSnapLog logFactory) throws IOException, X509Exception { LeaderZooKeeperServer zk = new LeaderZooKeeperServer(logFactory, this, this.getZkDb()) { @Override protected void setupRequestProcessors() { diff --git a/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java b/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java index 778ea1e2989..51ed5298a3f 100644 --- a/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java +++ b/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java @@ -49,6 +49,7 @@ import org.apache.zookeeper.Watcher; import org.apache.zookeeper.Watcher.Event.EventType; import org.apache.zookeeper.ZooDefs; +import org.apache.zookeeper.common.X509Exception; import org.apache.zookeeper.data.Stat; import org.apache.zookeeper.server.ByteBufferInputStream; import org.apache.zookeeper.server.ByteBufferOutputStream; @@ -112,7 +113,7 @@ public void run() { private static final class MockLeader extends Leader { MockLeader(QuorumPeer qp, LeaderZooKeeperServer zk) - throws IOException { + throws IOException, X509Exception { super(qp, zk); } @@ -1245,13 +1246,13 @@ public void converseWithLeader(InputArchive ia, OutputArchive oa, Leader l) } private Leader createLeader(File tmpDir, QuorumPeer peer) - throws IOException, NoSuchFieldException, IllegalAccessException { + throws IOException, NoSuchFieldException, IllegalAccessException, X509Exception { LeaderZooKeeperServer zk = prepareLeader(tmpDir, peer); return new Leader(peer, zk); } private Leader createMockLeader(File tmpDir, QuorumPeer peer) - throws IOException, NoSuchFieldException, IllegalAccessException { + throws IOException, NoSuchFieldException, IllegalAccessException, X509Exception { LeaderZooKeeperServer zk = prepareLeader(tmpDir, peer); return new MockLeader(peer, zk); } From 54d2f1093b69a4d884f0681d827e5cacdc87a99c Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Tue, 14 Mar 2017 10:56:03 -0700 Subject: [PATCH 02/25] hostname verification --- .../org/apache/zookeeper/common/X509Util.java | 31 + .../org/apache/zookeeper/common/ZKConfig.java | 2 + .../zookeeper/common/ZKHostnameVerifier.java | 561 ++++++++++++++++++ .../zookeeper/server/quorum/Follower.java | 4 +- .../zookeeper/server/quorum/Learner.java | 25 +- .../server/quorum/LearnerHandler.java | 10 + .../zookeeper/server/quorum/Observer.java | 6 +- .../server/quorum/QuorumCnxManager.java | 2 +- .../zookeeper/server/quorum/QuorumPeer.java | 13 +- .../org/apache/zookeeper/test/SSLTest.java | 4 +- 10 files changed, 631 insertions(+), 27 deletions(-) create mode 100644 src/java/main/org/apache/zookeeper/common/ZKHostnameVerifier.java diff --git a/src/java/main/org/apache/zookeeper/common/X509Util.java b/src/java/main/org/apache/zookeeper/common/X509Util.java index a0a1ab51e44..cf1db30efe1 100644 --- a/src/java/main/org/apache/zookeeper/common/X509Util.java +++ b/src/java/main/org/apache/zookeeper/common/X509Util.java @@ -18,6 +18,7 @@ package org.apache.zookeeper.common; +import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; @@ -34,6 +35,7 @@ import java.security.KeyStore; import org.apache.zookeeper.server.quorum.BufferedSocket; +import org.apache.zookeeper.server.quorum.QuorumPeer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.handler.ssl.SslHandler; import org.slf4j.Logger; @@ -245,4 +247,33 @@ public static Socket createUnifiedSocket(BufferedSocket socket) throws X509Excep } } + + private static HostnameVerifier getHostnameVerifier(ZKConfig config) throws SSLContextException { + String verifier = config.getProperty(ZKConfig.SSL_HOSTNAME_VERIFIER); + if (verifier == null || verifier.equals("ALLOW_ALL")) { + return ZKHostnameVerifier.ALLOW_ALL; + } else if (verifier.equals("DEFAULT")) { + return ZKHostnameVerifier.DEFAULT; + } else if (verifier.equals("DEFAULT_AND_LOCALHOST")) { + return ZKHostnameVerifier.DEFAULT_AND_LOCALHOST; + } else if (verifier.equals("STRICT")) { + return ZKHostnameVerifier.STRICT; + } else if (verifier.equals("STRICT_IE6")) { + return ZKHostnameVerifier.STRICT_IE6; + } else { + throw new SSLContextException("Invalid hostname verifier: " + verifier); + } + } + + public static void performHandshakeAndHostnameVerification(SSLSocket socket, QuorumPeer.QuorumServer server) throws IOException, SSLContextException { + socket.startHandshake(); + performHostnameVerification(socket, server); + } + + public static void performHostnameVerification(SSLSocket socket, QuorumPeer.QuorumServer server) throws IOException, SSLContextException { + HostnameVerifier hostnameVerifier = getHostnameVerifier(new ZKConfig()); + if (!hostnameVerifier.verify(server.hostname, socket.getSession())) { + throw new IOException("Hostname verification failure: verifier: " + hostnameVerifier + " hostname from configuration: " + server.hostname); + } + } } \ No newline at end of file diff --git a/src/java/main/org/apache/zookeeper/common/ZKConfig.java b/src/java/main/org/apache/zookeeper/common/ZKConfig.java index 8d9c001328d..9c381e094dd 100644 --- a/src/java/main/org/apache/zookeeper/common/ZKConfig.java +++ b/src/java/main/org/apache/zookeeper/common/ZKConfig.java @@ -53,6 +53,7 @@ public class ZKConfig { @SuppressWarnings("deprecation") public static final String SSL_AUTHPROVIDER = X509Util.SSL_AUTHPROVIDER; public static final String JUTE_MAXBUFFER = "jute.maxbuffer"; + public static final String SSL_HOSTNAME_VERIFIER = "zookeeper.ssl.hostnameVerifier"; /** * Path to a kinit binary: {@value}. Defaults to * "/usr/bin/kinit" @@ -115,6 +116,7 @@ protected void handleBackwardCompatibility() { properties.put(JUTE_MAXBUFFER, System.getProperty(JUTE_MAXBUFFER)); properties.put(KINIT_COMMAND, System.getProperty(KINIT_COMMAND)); properties.put(JGSS_NATIVE, System.getProperty(JGSS_NATIVE)); + properties.put(SSL_HOSTNAME_VERIFIER, System.getProperty(SSL_HOSTNAME_VERIFIER)); } /** diff --git a/src/java/main/org/apache/zookeeper/common/ZKHostnameVerifier.java b/src/java/main/org/apache/zookeeper/common/ZKHostnameVerifier.java new file mode 100644 index 00000000000..a91a107f699 --- /dev/null +++ b/src/java/main/org/apache/zookeeper/common/ZKHostnameVerifier.java @@ -0,0 +1,561 @@ +/** + * 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.common; + +import javax.naming.InvalidNameException; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.net.ssl.*; +import javax.security.auth.x500.X500Principal; +import java.io.IOException; +import java.io.InputStream; +import java.security.cert.Certificate; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.TreeSet; + +/** + * Copied from the not-yet-commons-ssl project at + * http://juliusdavies.ca/commons-ssl/ + *

+ * Interface for checking if a hostname matches the names stored inside the + * server's X.509 certificate. Correctly implements + * javax.net.ssl.HostnameVerifier, but that interface is not recommended. + * Instead we added several check() methods that take SSLSocket, + * or X509Certificate, or ultimately (they all end up calling this one), + * String. (It's easier to supply JUnit with Strings instead of mock + * SSLSession objects!) + *

Our check() methods throw exceptions if the name is + * invalid, whereas javax.net.ssl.HostnameVerifier just returns true/false. + *

+ * We provide the HostnameVerifier.DEFAULT, HostnameVerifier.STRICT, and + * HostnameVerifier.ALLOW_ALL implementations. We also provide the more + * specialized HostnameVerifier.DEFAULT_AND_LOCALHOST, as well as + * HostnameVerifier.STRICT_IE6. But feel free to define your own + * implementations! + *

+ * Inspired by Sebastian Hauer's original StrictSSLProtocolSocketFactory in the + * HttpClient "contrib" repository. + * + * @author Julius Davies + * @author Sebastian Hauer + * @since 8-Dec-2006 + */ +public interface ZKHostnameVerifier extends HostnameVerifier { + + boolean verify(String host, SSLSession session); + + void check(String host, SSLSocket ssl) throws IOException; + + void check(String host, X509Certificate cert) throws SSLException; + + void check(String host, String[] cns, String[] subjectAlts) + throws SSLException; + + void check(String[] hosts, SSLSocket ssl) throws IOException; + + void check(String[] hosts, X509Certificate cert) throws SSLException; + + + /** + * Checks to see if the supplied hostname matches any of the supplied CNs + * or "DNS" Subject-Alts. Most implementations only look at the first CN, + * and ignore any additional CNs. Most implementations do look at all of + * the "DNS" Subject-Alts. The CNs or Subject-Alts may contain wildcards + * according to RFC 2818. + * + * @param cns CN fields, in order, as extracted from the X.509 + * certificate. + * @param subjectAlts Subject-Alt fields of type 2 ("DNS"), as extracted + * from the X.509 certificate. + * @param hosts The array of hostnames to verify. + * @throws SSLException If verification failed. + */ + void check(String[] hosts, String[] cns, String[] subjectAlts) + throws SSLException; + + + /** + * The DEFAULT HostnameVerifier works the same way as Curl and Firefox. + *

+ * The hostname must match either the first CN, or any of the subject-alts. + * A wildcard can occur in the CN, and in any of the subject-alts. + *

+ * The only difference between DEFAULT and STRICT is that a wildcard (such + * as "*.foo.com") with DEFAULT matches all subdomains, including + * "a.b.foo.com". + */ + public final static ZKHostnameVerifier DEFAULT = + new AbstractVerifier() { + public final void check(final String[] hosts, final String[] cns, + final String[] subjectAlts) + throws SSLException { + check(hosts, cns, subjectAlts, false, false); + } + + public final String toString() { + return "DEFAULT"; + } + }; + + + /** + * The DEFAULT_AND_LOCALHOST HostnameVerifier works like the DEFAULT + * one with one additional relaxation: a host of "localhost", + * "localhost.localdomain", "127.0.0.1", "::1" will always pass, no matter + * what is in the server's certificate. + */ + public final static ZKHostnameVerifier DEFAULT_AND_LOCALHOST = + new AbstractVerifier() { + public final void check(final String[] hosts, final String[] cns, + final String[] subjectAlts) + throws SSLException { + if (isLocalhost(hosts[0])) { + return; + } + check(hosts, cns, subjectAlts, false, false); + } + + public final String toString() { + return "DEFAULT_AND_LOCALHOST"; + } + }; + + /** + * The STRICT HostnameVerifier works the same way as java.net.URL in Sun + * Java 1.4, Sun Java 5, Sun Java 6. It's also pretty close to IE6. + * This implementation appears to be compliant with RFC 2818 for dealing + * with wildcards. + *

+ * The hostname must match either the first CN, or any of the subject-alts. + * A wildcard can occur in the CN, and in any of the subject-alts. The + * one divergence from IE6 is how we only check the first CN. IE6 allows + * a match against any of the CNs present. We decided to follow in + * Sun Java 1.4's footsteps and only check the first CN. + *

+ * A wildcard such as "*.foo.com" matches only subdomains in the same + * level, for example "a.foo.com". It does not match deeper subdomains + * such as "a.b.foo.com". + */ + public final static ZKHostnameVerifier STRICT = + new AbstractVerifier() { + public final void check(final String[] host, final String[] cns, + final String[] subjectAlts) + throws SSLException { + check(host, cns, subjectAlts, false, true); + } + + public final String toString() { + return "STRICT"; + } + }; + + /** + * The STRICT_IE6 HostnameVerifier works just like the STRICT one with one + * minor variation: the hostname can match against any of the CN's in the + * server's certificate, not just the first one. This behaviour is + * identical to IE6's behaviour. + */ + public final static ZKHostnameVerifier STRICT_IE6 = + new AbstractVerifier() { + public final void check(final String[] host, final String[] cns, + final String[] subjectAlts) + throws SSLException { + check(host, cns, subjectAlts, true, true); + } + + public final String toString() { + return "STRICT_IE6"; + } + }; + + /** + * The ALLOW_ALL HostnameVerifier essentially turns hostname verification + * off. This implementation is a no-op, and never throws the SSLException. + */ + public final static ZKHostnameVerifier ALLOW_ALL = + new AbstractVerifier() { + public final void check(final String[] host, final String[] cns, + final String[] subjectAlts) { + // Allow everything - so never blowup. + } + + public final String toString() { + return "ALLOW_ALL"; + } + }; + + abstract class AbstractVerifier implements ZKHostnameVerifier { + + /** + * This contains a list of 2nd-level domains that aren't allowed to + * have wildcards when combined with country-codes. + * For example: [*.co.uk]. + *

+ * The [*.co.uk] problem is an interesting one. Should we just hope + * that CA's would never foolishly allow such a certificate to happen? + * Looks like we're the only implementation guarding against this. + * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check. + */ + private final static String[] BAD_COUNTRY_2LDS = + {"ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info", + "lg", "ne", "net", "or", "org"}; + + private final static String[] LOCALHOSTS = {"::1", "127.0.0.1", + "localhost", + "localhost.localdomain"}; + + + static { + // Just in case developer forgot to manually sort the array. :-) + Arrays.sort(BAD_COUNTRY_2LDS); + Arrays.sort(LOCALHOSTS); + } + + protected AbstractVerifier() { + } + + /** + * The javax.net.ssl.HostnameVerifier contract. + * + * @param host 'hostname' we used to create our socket + * @param session SSLSession with the remote server + * @return true if the host matched the one in the certificate. + */ + public boolean verify(String host, SSLSession session) { + try { + Certificate[] certs = session.getPeerCertificates(); + X509Certificate x509 = (X509Certificate) certs[0]; + check(new String[]{host}, x509); + return true; + } catch (SSLException e) { + return false; + } + } + + public void check(String host, SSLSocket ssl) throws IOException { + check(new String[]{host}, ssl); + } + + public void check(String host, X509Certificate cert) + throws SSLException { + check(new String[]{host}, cert); + } + + public void check(String host, String[] cns, String[] subjectAlts) + throws SSLException { + check(new String[]{host}, cns, subjectAlts); + } + + public void check(String host[], SSLSocket ssl) + throws IOException { + if (host == null) { + throw new NullPointerException("host to verify is null"); + } + + SSLSession session = ssl.getSession(); + if (session == null) { + // In our experience this only happens under IBM 1.4.x when + // spurious (unrelated) certificates show up in the server' + // chain. Hopefully this will unearth the real problem: + InputStream in = ssl.getInputStream(); + in.available(); + /* + If you're looking at the 2 lines of code above because + you're running into a problem, you probably have two + options: + + #1. Clean up the certificate chain that your server + is presenting (e.g. edit "/etc/apache2/server.crt" + or wherever it is your server's certificate chain + is defined). + + OR + + #2. Upgrade to an IBM 1.5.x or greater JVM, or switch + to a non-IBM JVM. + */ + + // If ssl.getInputStream().available() didn't cause an + // exception, maybe at least now the session is available? + session = ssl.getSession(); + if (session == null) { + // If it's still null, probably a startHandshake() will + // unearth the real problem. + ssl.startHandshake(); + + // Okay, if we still haven't managed to cause an exception, + // might as well go for the NPE. Or maybe we're okay now? + session = ssl.getSession(); + } + } + Certificate[] certs; + try { + certs = session.getPeerCertificates(); + } catch (SSLPeerUnverifiedException spue) { + InputStream in = ssl.getInputStream(); + in.available(); + // Didn't trigger anything interesting? Okay, just throw + // original. + throw spue; + } + X509Certificate x509 = (X509Certificate) certs[0]; + check(host, x509); + } + + public void check(String[] host, X509Certificate cert) + throws SSLException { + String[] cns = getCNs(cert); + String[] subjectAlts = getDNSSubjectAlts(cert); + check(host, cns, subjectAlts); + } + + public void check(final String[] hosts, final String[] cns, + final String[] subjectAlts, final boolean ie6, + final boolean strictWithSubDomains) + throws SSLException { + // Build up lists of allowed hosts For logging/debugging purposes. + StringBuffer buf = new StringBuffer(32); + buf.append('<'); + for (int i = 0; i < hosts.length; i++) { + String h = hosts[i]; + h = h != null ? h.trim().toLowerCase() : ""; + hosts[i] = h; + if (i > 0) { + buf.append('/'); + } + buf.append(h); + } + buf.append('>'); + String hostnames = buf.toString(); + // Build the list of names we're going to check. Our DEFAULT and + // STRICT implementations of the HostnameVerifier only use the + // first CN provided. All other CNs are ignored. + // (Firefox, wget, curl, Sun Java 1.4, 5, 6 all work this way). + TreeSet names = new TreeSet(); + if (cns != null && cns.length > 0 && cns[0] != null) { + names.add(cns[0]); + if (ie6) { + for (int i = 1; i < cns.length; i++) { + names.add(cns[i]); + } + } + } + if (subjectAlts != null) { + for (int i = 0; i < subjectAlts.length; i++) { + if (subjectAlts[i] != null) { + names.add(subjectAlts[i]); + } + } + } + if (names.isEmpty()) { + String msg = "Certificate for " + hosts[0] + " doesn't contain CN or DNS subjectAlt"; + throw new SSLException(msg); + } + + // StringBuffer for building the error message. + buf = new StringBuffer(); + + boolean match = false; + out: + for (Iterator it = names.iterator(); it.hasNext(); ) { + // Don't trim the CN, though! + String cn = (String) it.next(); + cn = cn.toLowerCase(); + // Store CN in StringBuffer in case we need to report an error. + buf.append(" <"); + buf.append(cn); + buf.append('>'); + if (it.hasNext()) { + buf.append(" OR"); + } + + // The CN better have at least two dots if it wants wildcard + // action. It also can't be [*.co.uk] or [*.co.jp] or + // [*.org.uk], etc... + boolean doWildcard = cn.startsWith("*.") && + cn.lastIndexOf('.') >= 0 && + !isIP4Address(cn) && + acceptableCountryWildcard(cn); + + for (int i = 0; i < hosts.length; i++) { + final String hostName = hosts[i].trim().toLowerCase(); + if (doWildcard) { + match = hostName.endsWith(cn.substring(1)); + if (match && strictWithSubDomains) { + // If we're in strict mode, then [*.foo.com] is not + // allowed to match [a.b.foo.com] + match = countDots(hostName) == countDots(cn); + } + } else { + match = hostName.equals(cn); + } + if (match) { + break out; + } + } + } + if (!match) { + throw new SSLException("hostname in certificate didn't match: " + hostnames + " !=" + buf); + } + } + + public static boolean isIP4Address(final String cn) { + boolean isIP4 = true; + String tld = cn; + int x = cn.lastIndexOf('.'); + // We only bother analyzing the characters after the final dot + // in the name. + if (x >= 0 && x + 1 < cn.length()) { + tld = cn.substring(x + 1); + } + for (int i = 0; i < tld.length(); i++) { + if (!Character.isDigit(tld.charAt(0))) { + isIP4 = false; + break; + } + } + return isIP4; + } + + public static boolean acceptableCountryWildcard(final String cn) { + int cnLen = cn.length(); + if (cnLen >= 7 && cnLen <= 9) { + // Look for the '.' in the 3rd-last position: + if (cn.charAt(cnLen - 3) == '.') { + // Trim off the [*.] and the [.XX]. + String s = cn.substring(2, cnLen - 3); + // And test against the sorted array of bad 2lds: + int x = Arrays.binarySearch(BAD_COUNTRY_2LDS, s); + return x < 0; + } + } + return true; + } + + public static boolean isLocalhost(String host) { + host = host != null ? host.trim().toLowerCase() : ""; + if (host.startsWith("::1")) { + int x = host.lastIndexOf('%'); + if (x >= 0) { + host = host.substring(0, x); + } + } + int x = Arrays.binarySearch(LOCALHOSTS, host); + return x >= 0; + } + + /** + * Counts the number of dots "." in a string. + * + * @param s string to count dots from + * @return number of dots + */ + public static int countDots(final String s) { + int count = 0; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '.') { + count++; + } + } + return count; + } + + public static String[] getCNs(X509Certificate cert) { + try { + final String subjectPrincipal = cert.getSubjectX500Principal().getName(X500Principal.RFC2253); + final LinkedList cnList = new LinkedList(); + final LdapName subjectDN = new LdapName(subjectPrincipal); + for (final Rdn rds : subjectDN.getRdns()) { + final Attributes attributes = rds.toAttributes(); + final Attribute cn = attributes.get("cn"); + if (cn != null) { + try { + final Object value = cn.get(); + if (value != null) { + cnList.add(value.toString()); + } + } catch (NoSuchElementException ignore) { + } catch (NamingException ignore) { + } + } + } + if (!cnList.isEmpty()) { + return cnList.toArray(new String[cnList.size()]); + } + } catch (InvalidNameException ignore) { + } + return null; + } + + /** + * Extracts the array of SubjectAlt DNS names from an X509Certificate. + * Returns null if there aren't any. + *

+ * Note: Java doesn't appear able to extract international characters + * from the SubjectAlts. It can only extract international characters + * from the CN field. + *

+ * (Or maybe the version of OpenSSL I'm using to test isn't storing the + * international characters correctly in the SubjectAlts?). + * + * @param cert X509Certificate + * @return Array of SubjectALT DNS names stored in the certificate. + */ + public static String[] getDNSSubjectAlts(X509Certificate cert) { + LinkedList subjectAltList = new LinkedList(); + Collection c = null; + try { + c = cert.getSubjectAlternativeNames(); + } catch (CertificateParsingException cpe) { + // Should probably log.debug() this? + cpe.printStackTrace(); + } + if (c != null) { + Iterator it = c.iterator(); + while (it.hasNext()) { + List list = (List) it.next(); + int type = ((Integer) list.get(0)).intValue(); + // If type is 2, then we've got a dNSName + if (type == 2) { + String s = (String) list.get(1); + subjectAltList.add(s); + } + } + } + if (!subjectAltList.isEmpty()) { + String[] subjectAlts = new String[subjectAltList.size()]; + subjectAltList.toArray(subjectAlts); + return subjectAlts; + } else { + return null; + } + } + } + +} \ No newline at end of file diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Follower.java b/src/java/main/org/apache/zookeeper/server/quorum/Follower.java index 1cdc2025c7e..f1fc646e87e 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Follower.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Follower.java @@ -71,9 +71,9 @@ void followLeader() throws InterruptedException { self.end_fle = 0; fzk.registerJMX(new FollowerBean(this, zk), self.jmxLocalPeerBean); try { - InetSocketAddress addr = findLeader(); + QuorumPeer.QuorumServer leader = findLeader(); try { - connectToLeader(addr); + connectToLeader(leader); long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO); if (self.isReconfigStateChange()) throw new Exception("learned about role change"); diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Learner.java b/src/java/main/org/apache/zookeeper/server/quorum/Learner.java index 421dccc7a6b..7dd8df30638 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Learner.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Learner.java @@ -197,21 +197,18 @@ void request(Request request) throws IOException { /** * Returns the address of the node we think is the leader. */ - protected InetSocketAddress findLeader() { + protected QuorumServer findLeader() { InetSocketAddress addr = null; // Find the leader by id Vote current = self.getCurrentVote(); for (QuorumServer s : self.getView().values()) { if (s.id == current.getId()) { - addr = s.addr; - break; + return s; } } - if (addr == null) { - LOG.warn("Couldn't find the leader with id = " + LOG.warn("Couldn't find the leader with id = " + current.getId()); - } - return addr; + return null; } /** @@ -234,12 +231,12 @@ protected void sockConnect(Socket sock, InetSocketAddress addr, int timeout) /** * Establish a connection with the Leader found by findLeader. Retries * until either initLimit time has elapsed or 5 tries have happened. - * @param addr - the address of the Leader to connect to. + * @param leader - the QuorumServer elected leader * @throws IOException - if the socket connection fails on the 5th attempt * @throws ConnectException * @throws InterruptedException */ - protected void connectToLeader(InetSocketAddress addr) + protected void connectToLeader(QuorumServer leader) throws IOException, InterruptedException, X509Exception { createSocket(); @@ -256,9 +253,9 @@ protected void connectToLeader(InetSocketAddress addr) throw new IOException("initLimit exceeded on retries."); } - sockConnect(sock, addr, Math.min(self.tickTime * self.syncLimit, remainingInitLimitTime)); + sockConnect(sock, leader.addr, Math.min(self.tickTime * self.syncLimit, remainingInitLimitTime)); if (self.isSslQuorum()) { - ((SSLSocket) sock).startHandshake(); + X509Util.performHandshakeAndHostnameVerification((SSLSocket) sock, leader); } sock.setTcpNoDelay(nodelay); break; @@ -268,17 +265,17 @@ protected void connectToLeader(InetSocketAddress addr) if (remainingInitLimitTime <= 1000) { LOG.error("Unexpected exception, initLimit exceeded. tries=" + tries + ", remaining init limit=" + remainingInitLimitTime + - ", connecting to " + addr,e); + ", connecting to " + leader.addr,e); throw e; } else if (tries >= 4) { LOG.error("Unexpected exception, retries exceeded. tries=" + tries + ", remaining init limit=" + remainingInitLimitTime + - ", connecting to " + addr,e); + ", connecting to " + leader.addr,e); throw e; } else { LOG.warn("Unexpected exception, tries=" + tries + ", remaining init limit=" + remainingInitLimitTime + - ", connecting to " + addr,e); + ", connecting to " + leader.addr,e); createSocket(); } } diff --git a/src/java/main/org/apache/zookeeper/server/quorum/LearnerHandler.java b/src/java/main/org/apache/zookeeper/server/quorum/LearnerHandler.java index dcd1b47c7af..1e252019173 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/LearnerHandler.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/LearnerHandler.java @@ -38,6 +38,8 @@ import org.apache.jute.Record; import org.apache.zookeeper.KeeperException.SessionExpiredException; import org.apache.zookeeper.ZooDefs.OpCode; +import org.apache.zookeeper.common.X509Exception; +import org.apache.zookeeper.common.X509Util; import org.apache.zookeeper.server.Request; import org.apache.zookeeper.server.TxnLogProposalIterator; import org.apache.zookeeper.server.ZKDatabase; @@ -51,6 +53,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLSocket; + /** * There will be an instance of this class created by the Leader for each * learner. All communication with a learner is handled by this @@ -381,8 +385,12 @@ public void run() { if (leader.self.getView().containsKey(this.sid)) { LOG.info("Follower sid: " + this.sid + " : info : " + leader.self.getView().get(this.sid).toString()); + if (sock instanceof SSLSocket) { + X509Util.performHostnameVerification((SSLSocket) sock, leader.self.getView().get(this.sid)); + } } else { LOG.info("Follower sid: " + this.sid + " not in the current config " + Long.toHexString(leader.self.getQuorumVerifier().getVersion())); + //TODO: Hostname verification possible } if (qp.getType() == Leader.OBSERVERINFO) { @@ -615,6 +623,8 @@ public void run() { LOG.error("Unexpected exception causing shutdown", e); } catch (SnapshotThrottleException e) { LOG.error("too many concurrent snapshots: " + e); + } catch (X509Exception.SSLContextException e) { + LOG.error("Learner failed hostname verification", e); } finally { LOG.warn("******* GOODBYE " + (sock != null ? sock.getRemoteSocketAddress() : "") diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Observer.java b/src/java/main/org/apache/zookeeper/server/quorum/Observer.java index 27368c7527f..dc048147370 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Observer.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Observer.java @@ -63,10 +63,10 @@ void observeLeader() throws Exception { zk.registerJMX(new ObserverBean(this, zk), self.jmxLocalPeerBean); try { - InetSocketAddress addr = findLeader(); - LOG.info("Observing " + addr); + QuorumPeer.QuorumServer leader = findLeader(); + LOG.info("Observing " + leader.addr); try { - connectToLeader(addr); + connectToLeader(leader); long newLeaderZxid = registerWithLeader(Leader.OBSERVERINFO); if (self.isReconfigStateChange()) throw new Exception("learned about role change"); diff --git a/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java b/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java index 7e03e738629..584cf1bfab9 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java @@ -446,7 +446,7 @@ synchronized private boolean connectOne(long sid, InetSocketAddress electionAddr SSLSocket sslSock = (SSLSocket) X509Util.createSSLContext().getSocketFactory().createSocket(); setSockOpts(sslSock); sslSock.connect(electionAddr, cnxTO); - sslSock.startHandshake(); + X509Util.performHandshakeAndHostnameVerification(sslSock, self.getView().get(sid)); sock = sslSock; } else { sock = new Socket(); diff --git a/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeer.java b/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeer.java index 136eeea018f..bda5d381953 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeer.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeer.java @@ -111,6 +111,8 @@ public class QuorumPeer extends ZooKeeperThread implements QuorumStats.Provider private ZKDatabase zkDb; public static class QuorumServer { + public String hostname = null; + public InetSocketAddress addr = null; public InetSocketAddress electionAddr = null; @@ -219,7 +221,7 @@ public QuorumServer(long sid, String addressStr) throws ConfigException { } // is client_config a host:port or just a port - String hostname = (clientParts.length == 2) ? clientParts[0] : "0.0.0.0"; + hostname = (clientParts.length == 2) ? clientParts[0] : "0.0.0.0"; try { clientAddr = new InetSocketAddress(hostname, Integer.parseInt(clientParts[clientParts.length - 1])); @@ -230,17 +232,18 @@ public QuorumServer(long sid, String addressStr) throws ConfigException { } // server_config should be either host:port:port or host:port:port:type + hostname = serverParts[0]; try { - addr = new InetSocketAddress(serverParts[0], + addr = new InetSocketAddress(hostname, Integer.parseInt(serverParts[1])); } catch (NumberFormatException e) { - throw new ConfigException("Address unresolved: " + serverParts[0] + ":" + serverParts[1]); + throw new ConfigException("Address unresolved: " + hostname + ":" + serverParts[1]); } try { - electionAddr = new InetSocketAddress(serverParts[0], + electionAddr = new InetSocketAddress(hostname, Integer.parseInt(serverParts[2])); } catch (NumberFormatException e) { - throw new ConfigException("Address unresolved: " + serverParts[0] + ":" + serverParts[2]); + throw new ConfigException("Address unresolved: " + hostname + ":" + serverParts[2]); } if (serverParts.length == 4) { diff --git a/src/java/test/org/apache/zookeeper/test/SSLTest.java b/src/java/test/org/apache/zookeeper/test/SSLTest.java index 16911b735af..b2fcd706b81 100644 --- a/src/java/test/org/apache/zookeeper/test/SSLTest.java +++ b/src/java/test/org/apache/zookeeper/test/SSLTest.java @@ -61,7 +61,7 @@ public void teardown() throws Exception { } /** - * This test checks that SSL works in cluster setup of ZK servers, which includes: + * This test checks that client <-> server SSL works in cluster setup of ZK servers, which includes: * 1. setting "secureClientPort" in "zoo.cfg" file. * 2. setting jvm flags for serverCnxn, keystore, truststore. * Finally, a zookeeper client should be able to connect to the secure port and @@ -70,7 +70,7 @@ public void teardown() throws Exception { * Note that in this test a ZK server has two ports -- clientPort and secureClientPort. */ @Test - public void testSecureQuorumServer() throws Exception { + public void testClientServerSSL() throws Exception { final int SERVER_COUNT = 3; final int clientPorts[] = new int[SERVER_COUNT]; final Integer secureClientPorts[] = new Integer[SERVER_COUNT]; From a1ac5dbb204ad4e3c7c27f2a3bb5906f7e2bf7bf Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Tue, 14 Mar 2017 15:27:05 -0700 Subject: [PATCH 03/25] certificate revocation checking --- .../org/apache/zookeeper/common/X509Util.java | 26 ++++++++++++++++--- .../org/apache/zookeeper/common/ZKConfig.java | 4 +++ .../auth/X509AuthenticationProvider.java | 7 ++++- .../server/quorum/QuorumCnxManager.java | 10 ++++++- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/java/main/org/apache/zookeeper/common/X509Util.java b/src/java/main/org/apache/zookeeper/common/X509Util.java index cf1db30efe1..4fb42ba68b0 100644 --- a/src/java/main/org/apache/zookeeper/common/X509Util.java +++ b/src/java/main/org/apache/zookeeper/common/X509Util.java @@ -18,6 +18,7 @@ package org.apache.zookeeper.common; +import javax.net.ssl.CertPathTrustManagerParameters; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; @@ -33,6 +34,9 @@ import java.io.IOException; import java.net.Socket; import java.security.KeyStore; +import java.security.Security; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.X509CertSelector; import org.apache.zookeeper.server.quorum.BufferedSocket; import org.apache.zookeeper.server.quorum.QuorumPeer; @@ -133,7 +137,7 @@ public static SSLContext createSSLContext(ZKConfig config) throws SSLContextExce } try { trustManagers = new TrustManager[]{ - createTrustManager(trustStoreLocationProp, trustStorePasswordProp)}; + createTrustManager(trustStoreLocationProp, trustStorePasswordProp, config.getBoolean(ZKConfig.SSL_CRL_ENABLED), config.getBoolean(ZKConfig.SSL_OCSP_ENABLED))}; } catch (TrustManagerException e) { throw new SSLContextException("Failed to create KeyManager", e); } @@ -179,7 +183,7 @@ public static X509KeyManager createKeyManager(String keyStoreLocation, String ke } } - public static X509TrustManager createTrustManager(String trustStoreLocation, String trustStorePassword) + public static X509TrustManager createTrustManager(String trustStoreLocation, String trustStorePassword, boolean crlEnabled, boolean ocspEnabled) throws TrustManagerException { FileInputStream inputStream = null; try { @@ -188,8 +192,22 @@ public static X509TrustManager createTrustManager(String trustStoreLocation, Str KeyStore ts = KeyStore.getInstance("JKS"); inputStream = new FileInputStream(trustStoreFile); ts.load(inputStream, trustStorePasswordChars); - TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); - tmf.init(ts); + + PKIXBuilderParameters pbParams = new PKIXBuilderParameters(ts, new X509CertSelector()); + if (crlEnabled || ocspEnabled) { + pbParams.setRevocationEnabled(true); + System.setProperty("com.sun.net.ssl.checkRevocation", "true"); + System.setProperty("com.sun.security.enableCRLDP", "true"); + if (ocspEnabled) { + Security.setProperty("ocsp.enable", "true"); + } + + } else { + pbParams.setRevocationEnabled(false); + } + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + tmf.init(new CertPathTrustManagerParameters(pbParams)); for (TrustManager tm : tmf.getTrustManagers()) { if (tm instanceof X509TrustManager) { diff --git a/src/java/main/org/apache/zookeeper/common/ZKConfig.java b/src/java/main/org/apache/zookeeper/common/ZKConfig.java index 9c381e094dd..e6ace316490 100644 --- a/src/java/main/org/apache/zookeeper/common/ZKConfig.java +++ b/src/java/main/org/apache/zookeeper/common/ZKConfig.java @@ -54,6 +54,8 @@ public class ZKConfig { public static final String SSL_AUTHPROVIDER = X509Util.SSL_AUTHPROVIDER; public static final String JUTE_MAXBUFFER = "jute.maxbuffer"; public static final String SSL_HOSTNAME_VERIFIER = "zookeeper.ssl.hostnameVerifier"; + public static final String SSL_CRL_ENABLED = "zookeeper.ssl.crl"; + public static final String SSL_OCSP_ENABLED = "zookeeper.ssl.ocsp"; /** * Path to a kinit binary: {@value}. Defaults to * "/usr/bin/kinit" @@ -117,6 +119,8 @@ protected void handleBackwardCompatibility() { properties.put(KINIT_COMMAND, System.getProperty(KINIT_COMMAND)); properties.put(JGSS_NATIVE, System.getProperty(JGSS_NATIVE)); properties.put(SSL_HOSTNAME_VERIFIER, System.getProperty(SSL_HOSTNAME_VERIFIER)); + properties.put(SSL_CRL_ENABLED, System.getProperty(SSL_CRL_ENABLED)); + properties.put(SSL_OCSP_ENABLED, System.getProperty(SSL_OCSP_ENABLED)); } /** diff --git a/src/java/main/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java b/src/java/main/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java index 902b3078710..60cd9a6c64a 100644 --- a/src/java/main/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java +++ b/src/java/main/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java @@ -70,6 +70,11 @@ public X509AuthenticationProvider() { String keyStorePasswordProp = System.getProperty( ZKConfig.SSL_KEYSTORE_PASSWD); + String ocspEnabledProperty = System.getProperty( + ZKConfig.SSL_OCSP_ENABLED); + String crlEnabledProperty = System.getProperty( + ZKConfig.SSL_CRL_ENABLED); + X509KeyManager km = null; X509TrustManager tm = null; try { @@ -86,7 +91,7 @@ public X509AuthenticationProvider() { try { tm = X509Util.createTrustManager( - trustStoreLocationProp, trustStorePasswordProp); + trustStoreLocationProp, trustStorePasswordProp, Boolean.parseBoolean(crlEnabledProperty), Boolean.parseBoolean(ocspEnabledProperty)); } catch (TrustManagerException e) { LOG.error("Failed to create trust manager", e); } diff --git a/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java b/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java index 584cf1bfab9..c0ce01dfeaa 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java @@ -344,13 +344,21 @@ public void receiveConnection(Socket sock) { sid = observerCounter--; LOG.info("Setting arbitrary identifier to observer: {}", sid); + } else { + if (sock instanceof SSLSocket) { + X509Util.performHostnameVerification((SSLSocket) sock, self.getView().get(sid)); + } } } catch (IOException e) { closeSocket(sock); LOG.warn("Exception reading or writing challenge: {}", e.toString()); return; + } catch (X509Exception.SSLContextException e) { + closeSocket(sock); + LOG.warn("Hostname verification failed", e); + return; } - + //If wins the challenge, then close the new connection. if (sid < self.getId()) { /* From 10b580dd7a8781e02dce44f5d021d62105d1d09e Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Fri, 17 Mar 2017 15:18:47 -0700 Subject: [PATCH 04/25] cleaner hostname verification --- .../org/apache/zookeeper/common/X509Util.java | 66 +-- .../org/apache/zookeeper/common/ZKConfig.java | 4 +- .../zookeeper/common/ZKHostnameVerifier.java | 561 ------------------ .../zookeeper/server/quorum/Learner.java | 2 +- .../server/quorum/LearnerHandler.java | 5 - .../server/quorum/QuorumCnxManager.java | 12 +- 6 files changed, 31 insertions(+), 619 deletions(-) delete mode 100644 src/java/main/org/apache/zookeeper/common/ZKHostnameVerifier.java diff --git a/src/java/main/org/apache/zookeeper/common/X509Util.java b/src/java/main/org/apache/zookeeper/common/X509Util.java index 4fb42ba68b0..a3972f3fe4b 100644 --- a/src/java/main/org/apache/zookeeper/common/X509Util.java +++ b/src/java/main/org/apache/zookeeper/common/X509Util.java @@ -18,11 +18,17 @@ package org.apache.zookeeper.common; +import org.apache.zookeeper.server.quorum.BufferedSocket; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.handler.ssl.SslHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import javax.net.ssl.CertPathTrustManagerParameters; -import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; @@ -38,13 +44,6 @@ import java.security.cert.PKIXBuilderParameters; import java.security.cert.X509CertSelector; -import org.apache.zookeeper.server.quorum.BufferedSocket; -import org.apache.zookeeper.server.quorum.QuorumPeer; -import org.jboss.netty.buffer.ChannelBuffers; -import org.jboss.netty.handler.ssl.SslHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import static org.apache.zookeeper.common.X509Exception.KeyManagerException; import static org.apache.zookeeper.common.X509Exception.SSLContextException; import static org.apache.zookeeper.common.X509Exception.TrustManagerException; @@ -228,20 +227,36 @@ public static X509TrustManager createTrustManager(String trustStoreLocation, Str public static SSLSocket createSSLSocket() throws X509Exception, IOException { SSLSocket sslSocket = (SSLSocket) createSSLContext().getSocketFactory().createSocket(); - sslSocket.setNeedClientAuth(true); + SSLParameters sslParameters = sslSocket.getSSLParameters(); + sslParameters.setNeedClientAuth(true); + sslParameters.setEndpointIdentificationAlgorithm("https"); + + sslSocket.setSSLParameters(sslParameters); + return sslSocket; } public static SSLServerSocket createSSLServerSocket() throws X509Exception, IOException { SSLServerSocket sslServerSocket = (SSLServerSocket) createSSLContext().getServerSocketFactory().createServerSocket(); - sslServerSocket.setNeedClientAuth(true); + SSLParameters sslParameters = sslServerSocket.getSSLParameters(); + sslParameters.setNeedClientAuth(true); + + + sslParameters.setEndpointIdentificationAlgorithm("https"); + + sslServerSocket.setSSLParameters(sslParameters); return sslServerSocket; } public static SSLServerSocket createSSLServerSocket(int port) throws X509Exception, IOException { SSLServerSocket sslServerSocket = (SSLServerSocket) createSSLContext().getServerSocketFactory().createServerSocket(port); - sslServerSocket.setNeedClientAuth(true); + SSLParameters sslParameters = sslServerSocket.getSSLParameters(); + sslParameters.setNeedClientAuth(true); + sslParameters.setEndpointIdentificationAlgorithm("https"); + + sslServerSocket.setSSLParameters(sslParameters); + return sslServerSocket; } @@ -265,33 +280,4 @@ public static Socket createUnifiedSocket(BufferedSocket socket) throws X509Excep } } - - private static HostnameVerifier getHostnameVerifier(ZKConfig config) throws SSLContextException { - String verifier = config.getProperty(ZKConfig.SSL_HOSTNAME_VERIFIER); - if (verifier == null || verifier.equals("ALLOW_ALL")) { - return ZKHostnameVerifier.ALLOW_ALL; - } else if (verifier.equals("DEFAULT")) { - return ZKHostnameVerifier.DEFAULT; - } else if (verifier.equals("DEFAULT_AND_LOCALHOST")) { - return ZKHostnameVerifier.DEFAULT_AND_LOCALHOST; - } else if (verifier.equals("STRICT")) { - return ZKHostnameVerifier.STRICT; - } else if (verifier.equals("STRICT_IE6")) { - return ZKHostnameVerifier.STRICT_IE6; - } else { - throw new SSLContextException("Invalid hostname verifier: " + verifier); - } - } - - public static void performHandshakeAndHostnameVerification(SSLSocket socket, QuorumPeer.QuorumServer server) throws IOException, SSLContextException { - socket.startHandshake(); - performHostnameVerification(socket, server); - } - - public static void performHostnameVerification(SSLSocket socket, QuorumPeer.QuorumServer server) throws IOException, SSLContextException { - HostnameVerifier hostnameVerifier = getHostnameVerifier(new ZKConfig()); - if (!hostnameVerifier.verify(server.hostname, socket.getSession())) { - throw new IOException("Hostname verification failure: verifier: " + hostnameVerifier + " hostname from configuration: " + server.hostname); - } - } } \ No newline at end of file diff --git a/src/java/main/org/apache/zookeeper/common/ZKConfig.java b/src/java/main/org/apache/zookeeper/common/ZKConfig.java index e6ace316490..571862f8f50 100644 --- a/src/java/main/org/apache/zookeeper/common/ZKConfig.java +++ b/src/java/main/org/apache/zookeeper/common/ZKConfig.java @@ -53,7 +53,7 @@ public class ZKConfig { @SuppressWarnings("deprecation") public static final String SSL_AUTHPROVIDER = X509Util.SSL_AUTHPROVIDER; public static final String JUTE_MAXBUFFER = "jute.maxbuffer"; - public static final String SSL_HOSTNAME_VERIFIER = "zookeeper.ssl.hostnameVerifier"; +// public static final String SSL_HOSTNAME_VERIFIER = "zookeeper.ssl.hostnameVerification"; public static final String SSL_CRL_ENABLED = "zookeeper.ssl.crl"; public static final String SSL_OCSP_ENABLED = "zookeeper.ssl.ocsp"; /** @@ -118,7 +118,7 @@ protected void handleBackwardCompatibility() { properties.put(JUTE_MAXBUFFER, System.getProperty(JUTE_MAXBUFFER)); properties.put(KINIT_COMMAND, System.getProperty(KINIT_COMMAND)); properties.put(JGSS_NATIVE, System.getProperty(JGSS_NATIVE)); - properties.put(SSL_HOSTNAME_VERIFIER, System.getProperty(SSL_HOSTNAME_VERIFIER)); +// properties.put(SSL_HOSTNAME_VERIFIER, System.getProperty(SSL_HOSTNAME_VERIFIER)); properties.put(SSL_CRL_ENABLED, System.getProperty(SSL_CRL_ENABLED)); properties.put(SSL_OCSP_ENABLED, System.getProperty(SSL_OCSP_ENABLED)); } diff --git a/src/java/main/org/apache/zookeeper/common/ZKHostnameVerifier.java b/src/java/main/org/apache/zookeeper/common/ZKHostnameVerifier.java deleted file mode 100644 index a91a107f699..00000000000 --- a/src/java/main/org/apache/zookeeper/common/ZKHostnameVerifier.java +++ /dev/null @@ -1,561 +0,0 @@ -/** - * 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.common; - -import javax.naming.InvalidNameException; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; -import javax.net.ssl.*; -import javax.security.auth.x500.X500Principal; -import java.io.IOException; -import java.io.InputStream; -import java.security.cert.Certificate; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.TreeSet; - -/** - * Copied from the not-yet-commons-ssl project at - * http://juliusdavies.ca/commons-ssl/ - *

- * Interface for checking if a hostname matches the names stored inside the - * server's X.509 certificate. Correctly implements - * javax.net.ssl.HostnameVerifier, but that interface is not recommended. - * Instead we added several check() methods that take SSLSocket, - * or X509Certificate, or ultimately (they all end up calling this one), - * String. (It's easier to supply JUnit with Strings instead of mock - * SSLSession objects!) - *

Our check() methods throw exceptions if the name is - * invalid, whereas javax.net.ssl.HostnameVerifier just returns true/false. - *

- * We provide the HostnameVerifier.DEFAULT, HostnameVerifier.STRICT, and - * HostnameVerifier.ALLOW_ALL implementations. We also provide the more - * specialized HostnameVerifier.DEFAULT_AND_LOCALHOST, as well as - * HostnameVerifier.STRICT_IE6. But feel free to define your own - * implementations! - *

- * Inspired by Sebastian Hauer's original StrictSSLProtocolSocketFactory in the - * HttpClient "contrib" repository. - * - * @author Julius Davies - * @author Sebastian Hauer - * @since 8-Dec-2006 - */ -public interface ZKHostnameVerifier extends HostnameVerifier { - - boolean verify(String host, SSLSession session); - - void check(String host, SSLSocket ssl) throws IOException; - - void check(String host, X509Certificate cert) throws SSLException; - - void check(String host, String[] cns, String[] subjectAlts) - throws SSLException; - - void check(String[] hosts, SSLSocket ssl) throws IOException; - - void check(String[] hosts, X509Certificate cert) throws SSLException; - - - /** - * Checks to see if the supplied hostname matches any of the supplied CNs - * or "DNS" Subject-Alts. Most implementations only look at the first CN, - * and ignore any additional CNs. Most implementations do look at all of - * the "DNS" Subject-Alts. The CNs or Subject-Alts may contain wildcards - * according to RFC 2818. - * - * @param cns CN fields, in order, as extracted from the X.509 - * certificate. - * @param subjectAlts Subject-Alt fields of type 2 ("DNS"), as extracted - * from the X.509 certificate. - * @param hosts The array of hostnames to verify. - * @throws SSLException If verification failed. - */ - void check(String[] hosts, String[] cns, String[] subjectAlts) - throws SSLException; - - - /** - * The DEFAULT HostnameVerifier works the same way as Curl and Firefox. - *

- * The hostname must match either the first CN, or any of the subject-alts. - * A wildcard can occur in the CN, and in any of the subject-alts. - *

- * The only difference between DEFAULT and STRICT is that a wildcard (such - * as "*.foo.com") with DEFAULT matches all subdomains, including - * "a.b.foo.com". - */ - public final static ZKHostnameVerifier DEFAULT = - new AbstractVerifier() { - public final void check(final String[] hosts, final String[] cns, - final String[] subjectAlts) - throws SSLException { - check(hosts, cns, subjectAlts, false, false); - } - - public final String toString() { - return "DEFAULT"; - } - }; - - - /** - * The DEFAULT_AND_LOCALHOST HostnameVerifier works like the DEFAULT - * one with one additional relaxation: a host of "localhost", - * "localhost.localdomain", "127.0.0.1", "::1" will always pass, no matter - * what is in the server's certificate. - */ - public final static ZKHostnameVerifier DEFAULT_AND_LOCALHOST = - new AbstractVerifier() { - public final void check(final String[] hosts, final String[] cns, - final String[] subjectAlts) - throws SSLException { - if (isLocalhost(hosts[0])) { - return; - } - check(hosts, cns, subjectAlts, false, false); - } - - public final String toString() { - return "DEFAULT_AND_LOCALHOST"; - } - }; - - /** - * The STRICT HostnameVerifier works the same way as java.net.URL in Sun - * Java 1.4, Sun Java 5, Sun Java 6. It's also pretty close to IE6. - * This implementation appears to be compliant with RFC 2818 for dealing - * with wildcards. - *

- * The hostname must match either the first CN, or any of the subject-alts. - * A wildcard can occur in the CN, and in any of the subject-alts. The - * one divergence from IE6 is how we only check the first CN. IE6 allows - * a match against any of the CNs present. We decided to follow in - * Sun Java 1.4's footsteps and only check the first CN. - *

- * A wildcard such as "*.foo.com" matches only subdomains in the same - * level, for example "a.foo.com". It does not match deeper subdomains - * such as "a.b.foo.com". - */ - public final static ZKHostnameVerifier STRICT = - new AbstractVerifier() { - public final void check(final String[] host, final String[] cns, - final String[] subjectAlts) - throws SSLException { - check(host, cns, subjectAlts, false, true); - } - - public final String toString() { - return "STRICT"; - } - }; - - /** - * The STRICT_IE6 HostnameVerifier works just like the STRICT one with one - * minor variation: the hostname can match against any of the CN's in the - * server's certificate, not just the first one. This behaviour is - * identical to IE6's behaviour. - */ - public final static ZKHostnameVerifier STRICT_IE6 = - new AbstractVerifier() { - public final void check(final String[] host, final String[] cns, - final String[] subjectAlts) - throws SSLException { - check(host, cns, subjectAlts, true, true); - } - - public final String toString() { - return "STRICT_IE6"; - } - }; - - /** - * The ALLOW_ALL HostnameVerifier essentially turns hostname verification - * off. This implementation is a no-op, and never throws the SSLException. - */ - public final static ZKHostnameVerifier ALLOW_ALL = - new AbstractVerifier() { - public final void check(final String[] host, final String[] cns, - final String[] subjectAlts) { - // Allow everything - so never blowup. - } - - public final String toString() { - return "ALLOW_ALL"; - } - }; - - abstract class AbstractVerifier implements ZKHostnameVerifier { - - /** - * This contains a list of 2nd-level domains that aren't allowed to - * have wildcards when combined with country-codes. - * For example: [*.co.uk]. - *

- * The [*.co.uk] problem is an interesting one. Should we just hope - * that CA's would never foolishly allow such a certificate to happen? - * Looks like we're the only implementation guarding against this. - * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check. - */ - private final static String[] BAD_COUNTRY_2LDS = - {"ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info", - "lg", "ne", "net", "or", "org"}; - - private final static String[] LOCALHOSTS = {"::1", "127.0.0.1", - "localhost", - "localhost.localdomain"}; - - - static { - // Just in case developer forgot to manually sort the array. :-) - Arrays.sort(BAD_COUNTRY_2LDS); - Arrays.sort(LOCALHOSTS); - } - - protected AbstractVerifier() { - } - - /** - * The javax.net.ssl.HostnameVerifier contract. - * - * @param host 'hostname' we used to create our socket - * @param session SSLSession with the remote server - * @return true if the host matched the one in the certificate. - */ - public boolean verify(String host, SSLSession session) { - try { - Certificate[] certs = session.getPeerCertificates(); - X509Certificate x509 = (X509Certificate) certs[0]; - check(new String[]{host}, x509); - return true; - } catch (SSLException e) { - return false; - } - } - - public void check(String host, SSLSocket ssl) throws IOException { - check(new String[]{host}, ssl); - } - - public void check(String host, X509Certificate cert) - throws SSLException { - check(new String[]{host}, cert); - } - - public void check(String host, String[] cns, String[] subjectAlts) - throws SSLException { - check(new String[]{host}, cns, subjectAlts); - } - - public void check(String host[], SSLSocket ssl) - throws IOException { - if (host == null) { - throw new NullPointerException("host to verify is null"); - } - - SSLSession session = ssl.getSession(); - if (session == null) { - // In our experience this only happens under IBM 1.4.x when - // spurious (unrelated) certificates show up in the server' - // chain. Hopefully this will unearth the real problem: - InputStream in = ssl.getInputStream(); - in.available(); - /* - If you're looking at the 2 lines of code above because - you're running into a problem, you probably have two - options: - - #1. Clean up the certificate chain that your server - is presenting (e.g. edit "/etc/apache2/server.crt" - or wherever it is your server's certificate chain - is defined). - - OR - - #2. Upgrade to an IBM 1.5.x or greater JVM, or switch - to a non-IBM JVM. - */ - - // If ssl.getInputStream().available() didn't cause an - // exception, maybe at least now the session is available? - session = ssl.getSession(); - if (session == null) { - // If it's still null, probably a startHandshake() will - // unearth the real problem. - ssl.startHandshake(); - - // Okay, if we still haven't managed to cause an exception, - // might as well go for the NPE. Or maybe we're okay now? - session = ssl.getSession(); - } - } - Certificate[] certs; - try { - certs = session.getPeerCertificates(); - } catch (SSLPeerUnverifiedException spue) { - InputStream in = ssl.getInputStream(); - in.available(); - // Didn't trigger anything interesting? Okay, just throw - // original. - throw spue; - } - X509Certificate x509 = (X509Certificate) certs[0]; - check(host, x509); - } - - public void check(String[] host, X509Certificate cert) - throws SSLException { - String[] cns = getCNs(cert); - String[] subjectAlts = getDNSSubjectAlts(cert); - check(host, cns, subjectAlts); - } - - public void check(final String[] hosts, final String[] cns, - final String[] subjectAlts, final boolean ie6, - final boolean strictWithSubDomains) - throws SSLException { - // Build up lists of allowed hosts For logging/debugging purposes. - StringBuffer buf = new StringBuffer(32); - buf.append('<'); - for (int i = 0; i < hosts.length; i++) { - String h = hosts[i]; - h = h != null ? h.trim().toLowerCase() : ""; - hosts[i] = h; - if (i > 0) { - buf.append('/'); - } - buf.append(h); - } - buf.append('>'); - String hostnames = buf.toString(); - // Build the list of names we're going to check. Our DEFAULT and - // STRICT implementations of the HostnameVerifier only use the - // first CN provided. All other CNs are ignored. - // (Firefox, wget, curl, Sun Java 1.4, 5, 6 all work this way). - TreeSet names = new TreeSet(); - if (cns != null && cns.length > 0 && cns[0] != null) { - names.add(cns[0]); - if (ie6) { - for (int i = 1; i < cns.length; i++) { - names.add(cns[i]); - } - } - } - if (subjectAlts != null) { - for (int i = 0; i < subjectAlts.length; i++) { - if (subjectAlts[i] != null) { - names.add(subjectAlts[i]); - } - } - } - if (names.isEmpty()) { - String msg = "Certificate for " + hosts[0] + " doesn't contain CN or DNS subjectAlt"; - throw new SSLException(msg); - } - - // StringBuffer for building the error message. - buf = new StringBuffer(); - - boolean match = false; - out: - for (Iterator it = names.iterator(); it.hasNext(); ) { - // Don't trim the CN, though! - String cn = (String) it.next(); - cn = cn.toLowerCase(); - // Store CN in StringBuffer in case we need to report an error. - buf.append(" <"); - buf.append(cn); - buf.append('>'); - if (it.hasNext()) { - buf.append(" OR"); - } - - // The CN better have at least two dots if it wants wildcard - // action. It also can't be [*.co.uk] or [*.co.jp] or - // [*.org.uk], etc... - boolean doWildcard = cn.startsWith("*.") && - cn.lastIndexOf('.') >= 0 && - !isIP4Address(cn) && - acceptableCountryWildcard(cn); - - for (int i = 0; i < hosts.length; i++) { - final String hostName = hosts[i].trim().toLowerCase(); - if (doWildcard) { - match = hostName.endsWith(cn.substring(1)); - if (match && strictWithSubDomains) { - // If we're in strict mode, then [*.foo.com] is not - // allowed to match [a.b.foo.com] - match = countDots(hostName) == countDots(cn); - } - } else { - match = hostName.equals(cn); - } - if (match) { - break out; - } - } - } - if (!match) { - throw new SSLException("hostname in certificate didn't match: " + hostnames + " !=" + buf); - } - } - - public static boolean isIP4Address(final String cn) { - boolean isIP4 = true; - String tld = cn; - int x = cn.lastIndexOf('.'); - // We only bother analyzing the characters after the final dot - // in the name. - if (x >= 0 && x + 1 < cn.length()) { - tld = cn.substring(x + 1); - } - for (int i = 0; i < tld.length(); i++) { - if (!Character.isDigit(tld.charAt(0))) { - isIP4 = false; - break; - } - } - return isIP4; - } - - public static boolean acceptableCountryWildcard(final String cn) { - int cnLen = cn.length(); - if (cnLen >= 7 && cnLen <= 9) { - // Look for the '.' in the 3rd-last position: - if (cn.charAt(cnLen - 3) == '.') { - // Trim off the [*.] and the [.XX]. - String s = cn.substring(2, cnLen - 3); - // And test against the sorted array of bad 2lds: - int x = Arrays.binarySearch(BAD_COUNTRY_2LDS, s); - return x < 0; - } - } - return true; - } - - public static boolean isLocalhost(String host) { - host = host != null ? host.trim().toLowerCase() : ""; - if (host.startsWith("::1")) { - int x = host.lastIndexOf('%'); - if (x >= 0) { - host = host.substring(0, x); - } - } - int x = Arrays.binarySearch(LOCALHOSTS, host); - return x >= 0; - } - - /** - * Counts the number of dots "." in a string. - * - * @param s string to count dots from - * @return number of dots - */ - public static int countDots(final String s) { - int count = 0; - for (int i = 0; i < s.length(); i++) { - if (s.charAt(i) == '.') { - count++; - } - } - return count; - } - - public static String[] getCNs(X509Certificate cert) { - try { - final String subjectPrincipal = cert.getSubjectX500Principal().getName(X500Principal.RFC2253); - final LinkedList cnList = new LinkedList(); - final LdapName subjectDN = new LdapName(subjectPrincipal); - for (final Rdn rds : subjectDN.getRdns()) { - final Attributes attributes = rds.toAttributes(); - final Attribute cn = attributes.get("cn"); - if (cn != null) { - try { - final Object value = cn.get(); - if (value != null) { - cnList.add(value.toString()); - } - } catch (NoSuchElementException ignore) { - } catch (NamingException ignore) { - } - } - } - if (!cnList.isEmpty()) { - return cnList.toArray(new String[cnList.size()]); - } - } catch (InvalidNameException ignore) { - } - return null; - } - - /** - * Extracts the array of SubjectAlt DNS names from an X509Certificate. - * Returns null if there aren't any. - *

- * Note: Java doesn't appear able to extract international characters - * from the SubjectAlts. It can only extract international characters - * from the CN field. - *

- * (Or maybe the version of OpenSSL I'm using to test isn't storing the - * international characters correctly in the SubjectAlts?). - * - * @param cert X509Certificate - * @return Array of SubjectALT DNS names stored in the certificate. - */ - public static String[] getDNSSubjectAlts(X509Certificate cert) { - LinkedList subjectAltList = new LinkedList(); - Collection c = null; - try { - c = cert.getSubjectAlternativeNames(); - } catch (CertificateParsingException cpe) { - // Should probably log.debug() this? - cpe.printStackTrace(); - } - if (c != null) { - Iterator it = c.iterator(); - while (it.hasNext()) { - List list = (List) it.next(); - int type = ((Integer) list.get(0)).intValue(); - // If type is 2, then we've got a dNSName - if (type == 2) { - String s = (String) list.get(1); - subjectAltList.add(s); - } - } - } - if (!subjectAltList.isEmpty()) { - String[] subjectAlts = new String[subjectAltList.size()]; - subjectAltList.toArray(subjectAlts); - return subjectAlts; - } else { - return null; - } - } - } - -} \ No newline at end of file diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Learner.java b/src/java/main/org/apache/zookeeper/server/quorum/Learner.java index 7dd8df30638..8f2e2602f71 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Learner.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Learner.java @@ -255,7 +255,7 @@ protected void connectToLeader(QuorumServer leader) sockConnect(sock, leader.addr, Math.min(self.tickTime * self.syncLimit, remainingInitLimitTime)); if (self.isSslQuorum()) { - X509Util.performHandshakeAndHostnameVerification((SSLSocket) sock, leader); + ((SSLSocket) sock).startHandshake(); } sock.setTcpNoDelay(nodelay); break; diff --git a/src/java/main/org/apache/zookeeper/server/quorum/LearnerHandler.java b/src/java/main/org/apache/zookeeper/server/quorum/LearnerHandler.java index 1e252019173..39874c4e4c9 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/LearnerHandler.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/LearnerHandler.java @@ -385,9 +385,6 @@ public void run() { if (leader.self.getView().containsKey(this.sid)) { LOG.info("Follower sid: " + this.sid + " : info : " + leader.self.getView().get(this.sid).toString()); - if (sock instanceof SSLSocket) { - X509Util.performHostnameVerification((SSLSocket) sock, leader.self.getView().get(this.sid)); - } } else { LOG.info("Follower sid: " + this.sid + " not in the current config " + Long.toHexString(leader.self.getQuorumVerifier().getVersion())); //TODO: Hostname verification possible @@ -623,8 +620,6 @@ public void run() { LOG.error("Unexpected exception causing shutdown", e); } catch (SnapshotThrottleException e) { LOG.error("too many concurrent snapshots: " + e); - } catch (X509Exception.SSLContextException e) { - LOG.error("Learner failed hostname verification", e); } finally { LOG.warn("******* GOODBYE " + (sock != null ? sock.getRemoteSocketAddress() : "") diff --git a/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java b/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java index c0ce01dfeaa..3a925279b77 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java @@ -344,19 +344,11 @@ public void receiveConnection(Socket sock) { sid = observerCounter--; LOG.info("Setting arbitrary identifier to observer: {}", sid); - } else { - if (sock instanceof SSLSocket) { - X509Util.performHostnameVerification((SSLSocket) sock, self.getView().get(sid)); - } } } catch (IOException e) { closeSocket(sock); LOG.warn("Exception reading or writing challenge: {}", e.toString()); return; - } catch (X509Exception.SSLContextException e) { - closeSocket(sock); - LOG.warn("Hostname verification failed", e); - return; } //If wins the challenge, then close the new connection. @@ -451,10 +443,10 @@ synchronized private boolean connectOne(long sid, InetSocketAddress electionAddr try { LOG.debug("Opening channel to server " + sid); if (self.isSslQuorum()) { - SSLSocket sslSock = (SSLSocket) X509Util.createSSLContext().getSocketFactory().createSocket(); + SSLSocket sslSock = X509Util.createSSLSocket(); setSockOpts(sslSock); sslSock.connect(electionAddr, cnxTO); - X509Util.performHandshakeAndHostnameVerification(sslSock, self.getView().get(sid)); + sslSock.startHandshake(); sock = sslSock; } else { sock = new Socket(); From f1a9ff957c01c9e5e8498e3ad75c1fb6a3670278 Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Mon, 20 Mar 2017 15:43:55 -0700 Subject: [PATCH 05/25] cleaner hostname verification --- .../org/apache/zookeeper/common/X509Util.java | 54 ++++++++++++++++--- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/src/java/main/org/apache/zookeeper/common/X509Util.java b/src/java/main/org/apache/zookeeper/common/X509Util.java index a3972f3fe4b..b64e5f61015 100644 --- a/src/java/main/org/apache/zookeeper/common/X509Util.java +++ b/src/java/main/org/apache/zookeeper/common/X509Util.java @@ -23,16 +23,19 @@ import org.jboss.netty.handler.ssl.SslHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sun.security.util.HostnameChecker; import javax.net.ssl.CertPathTrustManagerParameters; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; import java.io.File; @@ -41,8 +44,10 @@ import java.net.Socket; import java.security.KeyStore; import java.security.Security; +import java.security.cert.CertificateException; import java.security.cert.PKIXBuilderParameters; import java.security.cert.X509CertSelector; +import java.security.cert.X509Certificate; import static org.apache.zookeeper.common.X509Exception.KeyManagerException; import static org.apache.zookeeper.common.X509Exception.SSLContextException; @@ -208,9 +213,48 @@ public static X509TrustManager createTrustManager(String trustStoreLocation, Str TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); tmf.init(new CertPathTrustManagerParameters(pbParams)); - for (TrustManager tm : tmf.getTrustManagers()) { + for (final TrustManager tm : tmf.getTrustManagers()) { if (tm instanceof X509TrustManager) { - return (X509TrustManager) tm; + return new X509ExtendedTrustManager() { + HostnameChecker hostnameChecker = HostnameChecker.getInstance(HostnameChecker.TYPE_TLS); + + @Override + public X509Certificate[] getAcceptedIssuers() { + return ((X509ExtendedTrustManager) tm).getAcceptedIssuers(); + } + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException { + hostnameChecker.match(socket.getInetAddress().getHostName(), x509Certificates[0]); + ((X509ExtendedTrustManager) tm).checkClientTrusted(x509Certificates, s, socket); + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException { + hostnameChecker.match(((SSLSocket) socket).getHandshakeSession().getPeerHost(), x509Certificates[0]); + ((X509ExtendedTrustManager) tm).checkServerTrusted(x509Certificates, s, socket); + } + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException { + throw new RuntimeException("sslengine should not be in use"); + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException { + throw new RuntimeException("sslengine should not be in use"); + } + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + throw new RuntimeException("expecting a socket"); + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + throw new RuntimeException("expecting a socket"); + } + }; } } throw new TrustManagerException("Couldn't find X509TrustManager"); @@ -229,7 +273,6 @@ public static SSLSocket createSSLSocket() throws X509Exception, IOException { SSLSocket sslSocket = (SSLSocket) createSSLContext().getSocketFactory().createSocket(); SSLParameters sslParameters = sslSocket.getSSLParameters(); sslParameters.setNeedClientAuth(true); - sslParameters.setEndpointIdentificationAlgorithm("https"); sslSocket.setSSLParameters(sslParameters); @@ -241,10 +284,6 @@ public static SSLServerSocket createSSLServerSocket() throws X509Exception, IOEx SSLServerSocket sslServerSocket = (SSLServerSocket) createSSLContext().getServerSocketFactory().createServerSocket(); SSLParameters sslParameters = sslServerSocket.getSSLParameters(); sslParameters.setNeedClientAuth(true); - - - sslParameters.setEndpointIdentificationAlgorithm("https"); - sslServerSocket.setSSLParameters(sslParameters); return sslServerSocket; } @@ -253,7 +292,6 @@ public static SSLServerSocket createSSLServerSocket(int port) throws X509Excepti SSLServerSocket sslServerSocket = (SSLServerSocket) createSSLContext().getServerSocketFactory().createServerSocket(port); SSLParameters sslParameters = sslServerSocket.getSSLParameters(); sslParameters.setNeedClientAuth(true); - sslParameters.setEndpointIdentificationAlgorithm("https"); sslServerSocket.setSSLParameters(sslParameters); From aed9c01f90a68dbb7b396820d755a2389ee14be8 Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Fri, 24 Mar 2017 14:24:58 -0700 Subject: [PATCH 06/25] more cleanup and switch to hostname verifier from apache httpcomponents --- ivy.xml | 4 + .../zookeeper/ClientCnxnSocketNetty.java | 2 +- .../zookeeper/client/FourLetterWordMain.java | 2 +- .../ClientX509Util.java} | 38 ++-- .../zookeeper/common/QuorumX509Util.java | 32 +++ .../org/apache/zookeeper/common/X509Util.java | 197 ++++++++++-------- .../org/apache/zookeeper/common/ZKConfig.java | 46 ++-- .../server/NettyServerCnxnFactory.java | 8 +- .../auth/X509AuthenticationProvider.java | 29 +-- .../zookeeper/server/quorum/Leader.java | 12 +- .../zookeeper/server/quorum/Learner.java | 2 +- .../server/quorum/QuorumCnxManager.java | 10 +- .../server/quorum/QuorumPeerConfig.java | 5 +- .../server/quorum/UnifiedServerSocket.java | 79 +++++++ 14 files changed, 291 insertions(+), 175 deletions(-) rename src/java/main/org/apache/zookeeper/{server/quorum/BufferedServerSocket.java => common/ClientX509Util.java} (53%) create mode 100644 src/java/main/org/apache/zookeeper/common/QuorumX509Util.java create mode 100644 src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java diff --git a/ivy.xml b/ivy.xml index e56ecd846f5..d3ef5c0671c 100644 --- a/ivy.xml +++ b/ivy.xml @@ -55,6 +55,10 @@ + + + + diff --git a/src/java/main/org/apache/zookeeper/ClientCnxnSocketNetty.java b/src/java/main/org/apache/zookeeper/ClientCnxnSocketNetty.java index 97af9da698b..24eed08a5ce 100644 --- a/src/java/main/org/apache/zookeeper/ClientCnxnSocketNetty.java +++ b/src/java/main/org/apache/zookeeper/ClientCnxnSocketNetty.java @@ -362,7 +362,7 @@ public ChannelPipeline getPipeline() throws Exception { // Basically we only need to create it once. private synchronized void initSSL(ChannelPipeline pipeline) throws SSLContextException { if (sslContext == null || sslEngine == null) { - sslContext = X509Util.createSSLContext(clientConfig); + sslContext = X509Util.CLIENT_X509UTIL.createSSLContext(clientConfig); sslEngine = sslContext.createSSLEngine(); sslEngine.setUseClientMode(true); } diff --git a/src/java/main/org/apache/zookeeper/client/FourLetterWordMain.java b/src/java/main/org/apache/zookeeper/client/FourLetterWordMain.java index 19b45ba13e9..d78ecda5fda 100644 --- a/src/java/main/org/apache/zookeeper/client/FourLetterWordMain.java +++ b/src/java/main/org/apache/zookeeper/client/FourLetterWordMain.java @@ -88,7 +88,7 @@ public static String send4LetterWord(String host, int port, String cmd, boolean new InetSocketAddress(InetAddress.getByName(null), port); if (secure) { LOG.info("using secure socket"); - SSLContext sslContext = X509Util.createSSLContext(); + SSLContext sslContext = X509Util.CLIENT_X509UTIL.getDefaultSSLContext(); SSLSocketFactory socketFactory = sslContext.getSocketFactory(); SSLSocket sslSock = (SSLSocket) socketFactory.createSocket(); sslSock.connect(hostaddress, timeout); diff --git a/src/java/main/org/apache/zookeeper/server/quorum/BufferedServerSocket.java b/src/java/main/org/apache/zookeeper/common/ClientX509Util.java similarity index 53% rename from src/java/main/org/apache/zookeeper/server/quorum/BufferedServerSocket.java rename to src/java/main/org/apache/zookeeper/common/ClientX509Util.java index 95fcc157570..7de7d2a4179 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/BufferedServerSocket.java +++ b/src/java/main/org/apache/zookeeper/common/ClientX509Util.java @@ -16,33 +16,23 @@ * limitations under the License. */ -package org.apache.zookeeper.server.quorum; +package org.apache.zookeeper.common; -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketException; +public class ClientX509Util extends X509Util { -public class BufferedServerSocket extends ServerSocket { + private String sslAuthProviderProperty = getConfigPrefix() + "authProvider"; - public BufferedServerSocket() throws IOException { - super(); - } - - public BufferedServerSocket(int port) throws IOException { - super(port); - } + @Override + protected String getConfigPrefix() { + return "zookeeper.ssl."; + } - @Override - public Socket accept() throws IOException { - if (isClosed()) { - throw new SocketException("Socket is closed"); + @Override + protected boolean shouldVerifyClientHostname() { + return false; } - if (!isBound()) { - throw new SocketException("Socket is not bound yet"); + + public String getSslAuthProviderProperty() { + return sslAuthProviderProperty; } - final Socket bufferedSocket = new BufferedSocket(null); - implAccept(bufferedSocket); - return bufferedSocket; - } -} \ No newline at end of file +} diff --git a/src/java/main/org/apache/zookeeper/common/QuorumX509Util.java b/src/java/main/org/apache/zookeeper/common/QuorumX509Util.java new file mode 100644 index 00000000000..abe90a5bf95 --- /dev/null +++ b/src/java/main/org/apache/zookeeper/common/QuorumX509Util.java @@ -0,0 +1,32 @@ +/** + * 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.common; + +public class QuorumX509Util extends X509Util { + + @Override + protected String getConfigPrefix() { + return "zookeeper.ssl.quorum."; + } + + @Override + protected boolean shouldVerifyClientHostname() { + return true; + } +} diff --git a/src/java/main/org/apache/zookeeper/common/X509Util.java b/src/java/main/org/apache/zookeeper/common/X509Util.java index b64e5f61015..e1877253615 100644 --- a/src/java/main/org/apache/zookeeper/common/X509Util.java +++ b/src/java/main/org/apache/zookeeper/common/X509Util.java @@ -18,14 +18,12 @@ package org.apache.zookeeper.common; -import org.apache.zookeeper.server.quorum.BufferedSocket; -import org.jboss.netty.buffer.ChannelBuffers; -import org.jboss.netty.handler.ssl.SslHandler; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sun.security.util.HostnameChecker; import javax.net.ssl.CertPathTrustManagerParameters; +import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; @@ -49,48 +47,65 @@ import java.security.cert.X509CertSelector; import java.security.cert.X509Certificate; -import static org.apache.zookeeper.common.X509Exception.KeyManagerException; -import static org.apache.zookeeper.common.X509Exception.SSLContextException; -import static org.apache.zookeeper.common.X509Exception.TrustManagerException; +import static org.apache.zookeeper.common.X509Exception.*; /** * Utility code for X509 handling */ -public class X509Util { +public abstract class X509Util { private static final Logger LOG = LoggerFactory.getLogger(X509Util.class); - /** - * @deprecated Use {@link ZKConfig#SSL_KEYSTORE_LOCATION} - * instead. - */ - @Deprecated - public static final String SSL_KEYSTORE_LOCATION = "zookeeper.ssl.keyStore.location"; - /** - * @deprecated Use {@link ZKConfig#SSL_KEYSTORE_PASSWD} - * instead. - */ - @Deprecated - public static final String SSL_KEYSTORE_PASSWD = "zookeeper.ssl.keyStore.password"; - /** - * @deprecated Use {@link ZKConfig#SSL_TRUSTSTORE_LOCATION} - * instead. - */ - @Deprecated - public static final String SSL_TRUSTSTORE_LOCATION = "zookeeper.ssl.trustStore.location"; - /** - * @deprecated Use {@link ZKConfig#SSL_TRUSTSTORE_PASSWD} - * instead. - */ - @Deprecated - public static final String SSL_TRUSTSTORE_PASSWD = "zookeeper.ssl.trustStore.password"; - /** - * @deprecated Use {@link ZKConfig#SSL_AUTHPROVIDER} - * instead. - */ - @Deprecated - public static final String SSL_AUTHPROVIDER = "zookeeper.ssl.authProvider"; - - public static SSLContext createSSLContext() throws SSLContextException { + private String sslKeystoreLocationProperty = getConfigPrefix() + "keyStore.location"; + private String sslKeystorePasswdProperty = getConfigPrefix() + "keyStore.password"; + private String sslTruststoreLocationProperty = getConfigPrefix() + "trustStore.location"; + private String sslTruststorePasswdProperty = getConfigPrefix() + "trustStore.password"; + private String sslHostnameVerificationEnabledProperty = getConfigPrefix() + "hostnameVerification"; + private String sslCrlEnabledProperty = getConfigPrefix() + "ssl.crl"; + private String sslOcspEnabledProperty = getConfigPrefix() + "ssl.ocsp"; + + public static final QuorumX509Util QUORUM_X509UTIL = new QuorumX509Util(); + public static final ClientX509Util CLIENT_X509UTIL = new ClientX509Util(); + + private volatile SSLContext defaultSSLContext; + + protected abstract String getConfigPrefix(); + protected abstract boolean shouldVerifyClientHostname(); + + public String getSslKeystoreLocationProperty() { + return sslKeystoreLocationProperty; + } + + public String getSslKeystorePasswdProperty() { + return sslKeystorePasswdProperty; + } + + public String getSslTruststoreLocationProperty() { + return sslTruststoreLocationProperty; + } + + public String getSslTruststorePasswdProperty() { + return sslTruststorePasswdProperty; + } + + public String getSslHostnameVerificationEnabledProperty() { + return sslHostnameVerificationEnabledProperty; + } + public String getSslCrlEnabledProperty() { + return sslCrlEnabledProperty; + } + + public String getSslOcspEnabledProperty() { + return sslOcspEnabledProperty; + } + + public synchronized SSLContext getDefaultSSLContext() throws SSLContextException { + if (defaultSSLContext == null) { + defaultSSLContext = createSSLContext(); + } + return defaultSSLContext; + } + + public SSLContext createSSLContext() throws SSLContextException { /** * Since Configuration initializes the key store and trust store related * configuration from system property. Reading property from @@ -100,12 +115,12 @@ public static SSLContext createSSLContext() throws SSLContextException { return createSSLContext(config); } - public static SSLContext createSSLContext(ZKConfig config) throws SSLContextException { + public SSLContext createSSLContext(ZKConfig config) throws SSLContextException { KeyManager[] keyManagers = null; TrustManager[] trustManagers = null; - String keyStoreLocationProp = config.getProperty(ZKConfig.SSL_KEYSTORE_LOCATION); - String keyStorePasswordProp = config.getProperty(ZKConfig.SSL_KEYSTORE_PASSWD); + String keyStoreLocationProp = config.getProperty(sslKeystoreLocationProperty); + String keyStorePasswordProp = config.getProperty(sslKeystorePasswdProperty); // There are legal states in some use cases for null KeyManager or TrustManager. // But if a user wanna specify one, location and password are required. @@ -127,8 +142,12 @@ public static SSLContext createSSLContext(ZKConfig config) throws SSLContextExce } } - String trustStoreLocationProp = config.getProperty(ZKConfig.SSL_TRUSTSTORE_LOCATION); - String trustStorePasswordProp = config.getProperty(ZKConfig.SSL_TRUSTSTORE_PASSWD); + String trustStoreLocationProp = config.getProperty(sslTruststoreLocationProperty); + String trustStorePasswordProp = config.getProperty(sslTruststorePasswdProperty); + + boolean sslCrlEnabled = config.getBoolean(this.sslCrlEnabledProperty); + boolean sslOcspEnabled = config.getBoolean(this.sslOcspEnabledProperty); + boolean sslServerHostnameVerificationEnabled = config.getBoolean(this.getSslHostnameVerificationEnabledProperty()); if (trustStoreLocationProp == null && trustStorePasswordProp == null) { LOG.warn("keystore not specified for client connection"); @@ -140,8 +159,9 @@ public static SSLContext createSSLContext(ZKConfig config) throws SSLContextExce throw new SSLContextException("keystore password not specified for client connection"); } try { + trustManagers = new TrustManager[]{ - createTrustManager(trustStoreLocationProp, trustStorePasswordProp, config.getBoolean(ZKConfig.SSL_CRL_ENABLED), config.getBoolean(ZKConfig.SSL_OCSP_ENABLED))}; + createTrustManager(trustStoreLocationProp, trustStorePasswordProp, sslCrlEnabled, sslOcspEnabled, sslServerHostnameVerificationEnabled, shouldVerifyClientHostname())}; } catch (TrustManagerException e) { throw new SSLContextException("Failed to create KeyManager", e); } @@ -187,7 +207,10 @@ public static X509KeyManager createKeyManager(String keyStoreLocation, String ke } } - public static X509TrustManager createTrustManager(String trustStoreLocation, String trustStorePassword, boolean crlEnabled, boolean ocspEnabled) + public static X509TrustManager createTrustManager(String trustStoreLocation, String trustStorePassword, + boolean crlEnabled, boolean ocspEnabled, + final boolean hostnameVerificationEnabled, + final boolean shouldVerifyClientHostname) throws TrustManagerException { FileInputStream inputStream = null; try { @@ -214,45 +237,56 @@ public static X509TrustManager createTrustManager(String trustStoreLocation, Str tmf.init(new CertPathTrustManagerParameters(pbParams)); for (final TrustManager tm : tmf.getTrustManagers()) { - if (tm instanceof X509TrustManager) { + if (tm instanceof X509ExtendedTrustManager) { return new X509ExtendedTrustManager() { - HostnameChecker hostnameChecker = HostnameChecker.getInstance(HostnameChecker.TYPE_TLS); + X509ExtendedTrustManager x509ExtendedTrustManager = (X509ExtendedTrustManager) tm; + HostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(); @Override public X509Certificate[] getAcceptedIssuers() { - return ((X509ExtendedTrustManager) tm).getAcceptedIssuers(); + return x509ExtendedTrustManager.getAcceptedIssuers(); } @Override - public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException { - hostnameChecker.match(socket.getInetAddress().getHostName(), x509Certificates[0]); - ((X509ExtendedTrustManager) tm).checkClientTrusted(x509Certificates, s, socket); + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + if (hostnameVerificationEnabled && shouldVerifyClientHostname) { + hostnameVerifier.verify(socket.getInetAddress().getHostName(), ((SSLSocket) socket).getSession()); + } + x509ExtendedTrustManager.checkClientTrusted(chain, authType, socket); } @Override - public void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException { - hostnameChecker.match(((SSLSocket) socket).getHandshakeSession().getPeerHost(), x509Certificates[0]); - ((X509ExtendedTrustManager) tm).checkServerTrusted(x509Certificates, s, socket); + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + if (hostnameVerificationEnabled) { + hostnameVerifier.verify(socket.getInetAddress().getHostName(), ((SSLSocket) socket).getSession()); + } + x509ExtendedTrustManager.checkServerTrusted(chain, authType, socket); } @Override - public void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException { - throw new RuntimeException("sslengine should not be in use"); + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { + if (hostnameVerificationEnabled && shouldVerifyClientHostname) { + hostnameVerifier.verify(engine.getPeerHost(), engine.getSession()); + } + x509ExtendedTrustManager.checkServerTrusted(chain, authType, engine); } @Override - public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException { - throw new RuntimeException("sslengine should not be in use"); + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { + if (hostnameVerificationEnabled) { + hostnameVerifier.verify(engine.getPeerHost(), engine.getSession()); + } + x509ExtendedTrustManager.checkServerTrusted(chain, authType, engine); } @Override - public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { - throw new RuntimeException("expecting a socket"); + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + x509ExtendedTrustManager.checkClientTrusted(chain, authType); } @Override - public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { - throw new RuntimeException("expecting a socket"); + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + x509ExtendedTrustManager.checkServerTrusted(chain, authType); } }; } @@ -269,8 +303,8 @@ public void checkServerTrusted(X509Certificate[] x509Certificates, String s) thr } } - public static SSLSocket createSSLSocket() throws X509Exception, IOException { - SSLSocket sslSocket = (SSLSocket) createSSLContext().getSocketFactory().createSocket(); + public SSLSocket createSSLSocket() throws X509Exception, IOException { + SSLSocket sslSocket = (SSLSocket) getDefaultSSLContext().getSocketFactory().createSocket(); SSLParameters sslParameters = sslSocket.getSSLParameters(); sslParameters.setNeedClientAuth(true); @@ -280,16 +314,16 @@ public static SSLSocket createSSLSocket() throws X509Exception, IOException { } - public static SSLServerSocket createSSLServerSocket() throws X509Exception, IOException { - SSLServerSocket sslServerSocket = (SSLServerSocket) createSSLContext().getServerSocketFactory().createServerSocket(); + public SSLServerSocket createSSLServerSocket() throws X509Exception, IOException { + SSLServerSocket sslServerSocket = (SSLServerSocket) getDefaultSSLContext().getServerSocketFactory().createServerSocket(); SSLParameters sslParameters = sslServerSocket.getSSLParameters(); sslParameters.setNeedClientAuth(true); sslServerSocket.setSSLParameters(sslParameters); return sslServerSocket; } - public static SSLServerSocket createSSLServerSocket(int port) throws X509Exception, IOException { - SSLServerSocket sslServerSocket = (SSLServerSocket) createSSLContext().getServerSocketFactory().createServerSocket(port); + public SSLServerSocket createSSLServerSocket(int port) throws X509Exception, IOException { + SSLServerSocket sslServerSocket = (SSLServerSocket) getDefaultSSLContext().getServerSocketFactory().createServerSocket(port); SSLParameters sslParameters = sslServerSocket.getSSLParameters(); sslParameters.setNeedClientAuth(true); @@ -297,25 +331,4 @@ public static SSLServerSocket createSSLServerSocket(int port) throws X509Excepti return sslServerSocket; } - - public static Socket createUnifiedSocket(BufferedSocket socket) throws X509Exception, IOException { - socket.getInputStream().mark(6); - - byte[] litmus = new byte[5]; - socket.getInputStream().read(litmus, 0, 5); - - socket.getInputStream().reset(); - - boolean isSsl = SslHandler.isEncrypted(ChannelBuffers.wrappedBuffer(litmus)); - if (isSsl) { - LOG.info(socket.getInetAddress() + " attempting to connect over ssl"); - SSLSocket sslSocket = (SSLSocket) createSSLContext().getSocketFactory().createSocket(socket, null, socket.getPort(), false); - sslSocket.setUseClientMode(false); - return sslSocket; - } else { - LOG.info(socket.getInetAddress() + " attempting to connect without ssl"); - return socket; - } - - } -} \ No newline at end of file +} diff --git a/src/java/main/org/apache/zookeeper/common/ZKConfig.java b/src/java/main/org/apache/zookeeper/common/ZKConfig.java index 571862f8f50..cfb8866f4df 100644 --- a/src/java/main/org/apache/zookeeper/common/ZKConfig.java +++ b/src/java/main/org/apache/zookeeper/common/ZKConfig.java @@ -42,20 +42,9 @@ public class ZKConfig { private static final Logger LOG = LoggerFactory.getLogger(ZKConfig.class); - @SuppressWarnings("deprecation") - public static final String SSL_KEYSTORE_LOCATION = X509Util.SSL_KEYSTORE_LOCATION; - @SuppressWarnings("deprecation") - public static final String SSL_KEYSTORE_PASSWD = X509Util.SSL_KEYSTORE_PASSWD; - @SuppressWarnings("deprecation") - public static final String SSL_TRUSTSTORE_LOCATION = X509Util.SSL_TRUSTSTORE_LOCATION; - @SuppressWarnings("deprecation") - public static final String SSL_TRUSTSTORE_PASSWD = X509Util.SSL_TRUSTSTORE_PASSWD; - @SuppressWarnings("deprecation") - public static final String SSL_AUTHPROVIDER = X509Util.SSL_AUTHPROVIDER; + public static final String JUTE_MAXBUFFER = "jute.maxbuffer"; -// public static final String SSL_HOSTNAME_VERIFIER = "zookeeper.ssl.hostnameVerification"; - public static final String SSL_CRL_ENABLED = "zookeeper.ssl.crl"; - public static final String SSL_OCSP_ENABLED = "zookeeper.ssl.ocsp"; + /** * Path to a kinit binary: {@value}. Defaults to * "/usr/bin/kinit" @@ -110,17 +99,32 @@ private void init() { * this configuration. */ protected void handleBackwardCompatibility() { - properties.put(SSL_KEYSTORE_LOCATION, System.getProperty(SSL_KEYSTORE_LOCATION)); - properties.put(SSL_KEYSTORE_PASSWD, System.getProperty(SSL_KEYSTORE_PASSWD)); - properties.put(SSL_TRUSTSTORE_LOCATION, System.getProperty(SSL_TRUSTSTORE_LOCATION)); - properties.put(SSL_TRUSTSTORE_PASSWD, System.getProperty(SSL_TRUSTSTORE_PASSWD)); - properties.put(SSL_AUTHPROVIDER, System.getProperty(SSL_AUTHPROVIDER)); properties.put(JUTE_MAXBUFFER, System.getProperty(JUTE_MAXBUFFER)); properties.put(KINIT_COMMAND, System.getProperty(KINIT_COMMAND)); properties.put(JGSS_NATIVE, System.getProperty(JGSS_NATIVE)); -// properties.put(SSL_HOSTNAME_VERIFIER, System.getProperty(SSL_HOSTNAME_VERIFIER)); - properties.put(SSL_CRL_ENABLED, System.getProperty(SSL_CRL_ENABLED)); - properties.put(SSL_OCSP_ENABLED, System.getProperty(SSL_OCSP_ENABLED)); + + putSSLProperties(X509Util.CLIENT_X509UTIL); + properties.put(X509Util.CLIENT_X509UTIL.getSslAuthProviderProperty(), + System.getProperty(X509Util.CLIENT_X509UTIL.getSslAuthProviderProperty())); + + putSSLProperties(X509Util.QUORUM_X509UTIL); + } + + private void putSSLProperties(X509Util x509Util) { + properties.put(x509Util.getSslKeystoreLocationProperty(), + System.getProperty(x509Util.getSslKeystoreLocationProperty())); + properties.put(x509Util.getSslKeystorePasswdProperty(), + System.getProperty(x509Util.getSslKeystorePasswdProperty())); + properties.put(x509Util.getSslTruststoreLocationProperty(), + System.getProperty(x509Util.getSslTruststoreLocationProperty())); + properties.put(x509Util.getSslTruststorePasswdProperty(), + System.getProperty(x509Util.getSslTruststorePasswdProperty())); + properties.put(x509Util.getSslHostnameVerificationEnabledProperty(), + System.getProperty(x509Util.getSslHostnameVerificationEnabledProperty())); + properties.put(x509Util.getSslCrlEnabledProperty(), + System.getProperty(x509Util.getSslCrlEnabledProperty())); + properties.put(x509Util.getSslOcspEnabledProperty(), + System.getProperty(x509Util.getSslOcspEnabledProperty())); } /** diff --git a/src/java/main/org/apache/zookeeper/server/NettyServerCnxnFactory.java b/src/java/main/org/apache/zookeeper/server/NettyServerCnxnFactory.java index a02468953a5..c6d9aa71a43 100644 --- a/src/java/main/org/apache/zookeeper/server/NettyServerCnxnFactory.java +++ b/src/java/main/org/apache/zookeeper/server/NettyServerCnxnFactory.java @@ -292,7 +292,7 @@ public void operationComplete(ChannelFuture future) cnxn.setClientCertificateChain(session.getPeerCertificates()); String authProviderProp - = System.getProperty(ZKConfig.SSL_AUTHPROVIDER, "x509"); + = System.getProperty(X509Util.CLIENT_X509UTIL.getSslAuthProviderProperty(), "x509"); X509AuthenticationProvider authProvider = (X509AuthenticationProvider) @@ -352,15 +352,15 @@ public ChannelPipeline getPipeline() throws Exception { private synchronized void initSSL(ChannelPipeline p) throws X509Exception, KeyManagementException, NoSuchAlgorithmException { - String authProviderProp = System.getProperty(ZKConfig.SSL_AUTHPROVIDER); + String authProviderProp = System.getProperty(X509Util.CLIENT_X509UTIL.getSslAuthProviderProperty()); SSLContext sslContext; if (authProviderProp == null) { - sslContext = X509Util.createSSLContext(); + sslContext = X509Util.CLIENT_X509UTIL.getDefaultSSLContext(); } else { sslContext = SSLContext.getInstance("TLSv1"); X509AuthenticationProvider authProvider = (X509AuthenticationProvider)ProviderRegistry.getProvider( - System.getProperty(ZKConfig.SSL_AUTHPROVIDER, + System.getProperty(X509Util.CLIENT_X509UTIL.getSslAuthProviderProperty(), "x509")); if (authProvider == null) diff --git a/src/java/main/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java b/src/java/main/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java index 60cd9a6c64a..ef72c6dd8f0 100644 --- a/src/java/main/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java +++ b/src/java/main/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java @@ -65,33 +65,34 @@ public class X509AuthenticationProvider implements AuthenticationProvider { *
zookeeper.ssl.trustStore.password */ public X509AuthenticationProvider() { - String keyStoreLocationProp = System.getProperty( - ZKConfig.SSL_KEYSTORE_LOCATION); - String keyStorePasswordProp = System.getProperty( - ZKConfig.SSL_KEYSTORE_PASSWD); + ZKConfig config = new ZKConfig(); + X509Util x509Util = X509Util.CLIENT_X509UTIL; + + String keyStoreLocation = config.getProperty(x509Util.getSslKeystoreLocationProperty()); + String keyStorePassword = System.getProperty(x509Util.getSslKeystorePasswdProperty()); + + boolean crlEnabled = Boolean.parseBoolean(System.getProperty(x509Util.getSslCrlEnabledProperty())); + boolean ocspEnabled = Boolean.parseBoolean(System.getProperty(x509Util.getSslOcspEnabledProperty())); + boolean hostnameVerificationEnabled = Boolean.parseBoolean(System.getProperty(x509Util.getSslHostnameVerificationEnabledProperty())); - String ocspEnabledProperty = System.getProperty( - ZKConfig.SSL_OCSP_ENABLED); - String crlEnabledProperty = System.getProperty( - ZKConfig.SSL_CRL_ENABLED); X509KeyManager km = null; X509TrustManager tm = null; try { - km = X509Util.createKeyManager( - keyStoreLocationProp, keyStorePasswordProp); + km = X509Util.createKeyManager(keyStoreLocation, keyStorePassword); } catch (KeyManagerException e) { LOG.error("Failed to create key manager", e); } String trustStoreLocationProp = System.getProperty( - ZKConfig.SSL_TRUSTSTORE_LOCATION); + x509Util.getSslTruststoreLocationProperty()); String trustStorePasswordProp = System.getProperty( - ZKConfig.SSL_TRUSTSTORE_PASSWD); + x509Util.getSslTruststorePasswdProperty()); + + try { - tm = X509Util.createTrustManager( - trustStoreLocationProp, trustStorePasswordProp, Boolean.parseBoolean(crlEnabledProperty), Boolean.parseBoolean(ocspEnabledProperty)); + tm = X509Util.createTrustManager(trustStoreLocationProp, trustStorePasswordProp, crlEnabled, ocspEnabled, hostnameVerificationEnabled, false); } catch (TrustManagerException e) { LOG.error("Failed to create trust manager", e); } diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Leader.java b/src/java/main/org/apache/zookeeper/server/quorum/Leader.java index 10fe83101cb..67043fc6d0a 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Leader.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Leader.java @@ -223,15 +223,15 @@ public boolean isQuorumSynced(QuorumVerifier qv) { try { if (self.shouldUsePortUnification()) { if (self.getQuorumListenOnAllIPs()) { - ss = new BufferedServerSocket(self.getQuorumAddress().getPort()); + ss = new UnifiedServerSocket(self.getQuorumAddress().getPort()); } else { - ss = new BufferedServerSocket(); + ss = new UnifiedServerSocket(); } } else if (self.isSslQuorum()) { if (self.getQuorumListenOnAllIPs()) { - ss = X509Util.createSSLServerSocket(self.getQuorumAddress().getPort()); + ss = X509Util.QUORUM_X509UTIL.createSSLServerSocket(self.getQuorumAddress().getPort()); } else { - ss = X509Util.createSSLServerSocket(); + ss = X509Util.QUORUM_X509UTIL.createSSLServerSocket(); } } else { if (self.getQuorumListenOnAllIPs()) { @@ -382,10 +382,6 @@ public void run() { try{ Socket s = ss.accept(); - if (self.shouldUsePortUnification()) { - s = X509Util.createUnifiedSocket((BufferedSocket) s); - } - // start with the initLimit, once the ack is processed // in LearnerHandler switch to the syncLimit s.setSoTimeout(self.tickTime * self.initLimit); diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Learner.java b/src/java/main/org/apache/zookeeper/server/quorum/Learner.java index 8f2e2602f71..443c9c9f396 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Learner.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Learner.java @@ -289,7 +289,7 @@ protected void connectToLeader(QuorumServer leader) private void createSocket() throws X509Exception, IOException { if (self.isSslQuorum()) { - sock = X509Util.createSSLSocket(); + sock = X509Util.QUORUM_X509UTIL.createSSLSocket(); } else { sock = new Socket(); } diff --git a/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java b/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java index 3a925279b77..c10cafd17b0 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java @@ -443,7 +443,7 @@ synchronized private boolean connectOne(long sid, InetSocketAddress electionAddr try { LOG.debug("Opening channel to server " + sid); if (self.isSslQuorum()) { - SSLSocket sslSock = X509Util.createSSLSocket(); + SSLSocket sslSock = X509Util.QUORUM_X509UTIL.createSSLSocket(); setSockOpts(sslSock); sslSock.connect(electionAddr, cnxTO); sslSock.startHandshake(); @@ -641,9 +641,9 @@ public void run() { while((!shutdown) && (numRetries < 3)){ try { if (self.shouldUsePortUnification()) { - ss = new BufferedServerSocket(); + ss = new UnifiedServerSocket(); } else if (self.isSslQuorum()) { - ss = X509Util.createSSLServerSocket(); + ss = X509Util.QUORUM_X509UTIL.createSSLServerSocket(); } else { ss = new ServerSocket(); } @@ -665,10 +665,6 @@ public void run() { while (!shutdown) { client = ss.accept(); - if (self.shouldUsePortUnification()) { - client = X509Util.createUnifiedSocket((BufferedSocket) client); - } - setSockOpts(client); LOG.info("Received connection request " + client.getRemoteSocketAddress()); diff --git a/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java b/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java index 8066deb1342..4a0de320b5c 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java @@ -37,6 +37,7 @@ import java.util.Map.Entry; import org.apache.zookeeper.common.StringUtils; +import org.apache.zookeeper.common.X509Util; import org.apache.zookeeper.common.ZKConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -381,14 +382,14 @@ public void parseProperties(Properties zkProp) * provider is not configured. */ private void configureSSLAuth() throws ConfigException { - String sslAuthProp = "zookeeper.authProvider." + System.getProperty(ZKConfig.SSL_AUTHPROVIDER, "x509"); + String sslAuthProp = "zookeeper.authProvider." + System.getProperty(X509Util.CLIENT_X509UTIL.getSslAuthProviderProperty(), "x509"); if (System.getProperty(sslAuthProp) == null) { if ("zookeeper.authProvider.x509".equals(sslAuthProp)) { System.setProperty("zookeeper.authProvider.x509", "org.apache.zookeeper.server.auth.X509AuthenticationProvider"); } else { throw new ConfigException("No auth provider configured for the SSL authentication scheme '" - + System.getProperty(ZKConfig.SSL_AUTHPROVIDER) + "'."); + + System.getProperty(X509Util.CLIENT_X509UTIL.getSslAuthProviderProperty()) + "'."); } } } diff --git a/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java b/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java new file mode 100644 index 00000000000..ecedf052fbc --- /dev/null +++ b/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java @@ -0,0 +1,79 @@ +/** + * 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 org.apache.zookeeper.common.X509Exception; +import org.apache.zookeeper.common.X509Util; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.handler.ssl.SslHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLSocket; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; + +public class UnifiedServerSocket extends ServerSocket { + private static final Logger LOG = LoggerFactory.getLogger(UnifiedServerSocket.class); + + public UnifiedServerSocket() throws IOException { + super(); + } + + public UnifiedServerSocket(int port) throws IOException { + super(port); + } + + @Override + public Socket accept() throws IOException { + if (isClosed()) { + throw new SocketException("Socket is closed"); + } + if (!isBound()) { + throw new SocketException("Socket is not bound yet"); + } + final Socket bufferedSocket = new BufferedSocket(null); + implAccept(bufferedSocket); + + bufferedSocket.getInputStream().mark(6); + + byte[] litmus = new byte[5]; + bufferedSocket.getInputStream().read(litmus, 0, 5); + + bufferedSocket.getInputStream().reset(); + + boolean isSsl = SslHandler.isEncrypted(ChannelBuffers.wrappedBuffer(litmus)); + if (isSsl) { + LOG.info(getInetAddress() + " attempting to connect over ssl"); + SSLSocket sslSocket; + try { + sslSocket = (SSLSocket) X509Util.QUORUM_X509UTIL.getDefaultSSLContext().getSocketFactory().createSocket(bufferedSocket, null, bufferedSocket.getPort(), false); + } catch (X509Exception.SSLContextException e) { + throw new IOException("failed to create SSL context", e); + } + sslSocket.setUseClientMode(false); + return sslSocket; + } else { + LOG.info(getInetAddress() + " attempting to connect without ssl"); + return bufferedSocket; + } + } +} \ No newline at end of file From 1909286f584d5d6b3f423430118da6ce6eb3501f Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Fri, 31 Mar 2017 15:46:32 -0700 Subject: [PATCH 07/25] add testing --- ivy.xml | 3 + .../zookeeper/ClientCnxnSocketNetty.java | 3 +- .../zookeeper/client/FourLetterWordMain.java | 3 +- .../org/apache/zookeeper/common/X509Util.java | 22 +- .../org/apache/zookeeper/common/ZKConfig.java | 9 +- .../server/NettyServerCnxnFactory.java | 53 +- .../auth/X509AuthenticationProvider.java | 3 +- .../zookeeper/server/quorum/Leader.java | 10 +- .../zookeeper/server/quorum/Learner.java | 10 +- .../server/quorum/QuorumCnxManager.java | 11 +- .../server/quorum/QuorumPeerConfig.java | 6 +- .../server/quorum/UnifiedServerSocket.java | 10 +- .../zookeeper/server/quorum/LearnerTest.java | 4 +- .../server/quorum/QuorumPeerConfigTest.java | 4 +- .../server/quorum/QuorumPeerTestBase.java | 7 + .../zookeeper/server/quorum/Zab1_0Test.java | 16 +- .../test/{SSLTest.java => ClientSSLTest.java} | 22 +- .../apache/zookeeper/test/QuorumSSLTest.java | 478 ++++++++++++++++++ .../apache/zookeeper/test/SSLAuthTest.java | 40 +- 19 files changed, 617 insertions(+), 97 deletions(-) rename src/java/test/org/apache/zookeeper/test/{SSLTest.java => ClientSSLTest.java} (84%) create mode 100644 src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java diff --git a/ivy.xml b/ivy.xml index d3ef5c0671c..c54f851d0f4 100644 --- a/ivy.xml +++ b/ivy.xml @@ -68,6 +68,9 @@ + + + >( ); InetSocketAddress localAddress; int maxClientCnxns = 60; + ClientX509Util x509Util; /** * This is an inner class since we need to extend SimpleChannelHandler, but @@ -292,7 +291,7 @@ public void operationComplete(ChannelFuture future) cnxn.setClientCertificateChain(session.getPeerCertificates()); String authProviderProp - = System.getProperty(X509Util.CLIENT_X509UTIL.getSslAuthProviderProperty(), "x509"); + = System.getProperty(x509Util.getSslAuthProviderProperty(), "x509"); X509AuthenticationProvider authProvider = (X509AuthenticationProvider) @@ -348,20 +347,20 @@ public ChannelPipeline getPipeline() throws Exception { return p; } }); + x509Util = new ClientX509Util(); } private synchronized void initSSL(ChannelPipeline p) throws X509Exception, KeyManagementException, NoSuchAlgorithmException { - String authProviderProp = System.getProperty(X509Util.CLIENT_X509UTIL.getSslAuthProviderProperty()); + String authProviderProp = System.getProperty(x509Util.getSslAuthProviderProperty()); SSLContext sslContext; if (authProviderProp == null) { - sslContext = X509Util.CLIENT_X509UTIL.getDefaultSSLContext(); + sslContext = x509Util.getDefaultSSLContext(); } else { sslContext = SSLContext.getInstance("TLSv1"); X509AuthenticationProvider authProvider = (X509AuthenticationProvider)ProviderRegistry.getProvider( - System.getProperty(X509Util.CLIENT_X509UTIL.getSslAuthProviderProperty(), - "x509")); + System.getProperty(x509Util.getSslAuthProviderProperty(), "x509")); if (authProvider == null) { diff --git a/src/java/main/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java b/src/java/main/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java index ef72c6dd8f0..fbf99df746d 100644 --- a/src/java/main/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java +++ b/src/java/main/org/apache/zookeeper/server/auth/X509AuthenticationProvider.java @@ -26,6 +26,7 @@ import javax.security.auth.x500.X500Principal; import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.common.ClientX509Util; import org.apache.zookeeper.common.ZKConfig; import org.apache.zookeeper.common.X509Exception.KeyManagerException; import org.apache.zookeeper.common.X509Exception.TrustManagerException; @@ -66,7 +67,7 @@ public class X509AuthenticationProvider implements AuthenticationProvider { */ public X509AuthenticationProvider() { ZKConfig config = new ZKConfig(); - X509Util x509Util = X509Util.CLIENT_X509UTIL; + X509Util x509Util = new ClientX509Util(); String keyStoreLocation = config.getProperty(x509Util.getSslKeystoreLocationProperty()); String keyStorePassword = System.getProperty(x509Util.getSslKeystorePasswdProperty()); diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Leader.java b/src/java/main/org/apache/zookeeper/server/quorum/Leader.java index 67043fc6d0a..f1a9c0b40b9 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Leader.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Leader.java @@ -40,6 +40,7 @@ import org.apache.jute.BinaryOutputArchive; import org.apache.zookeeper.ZooDefs.OpCode; +import org.apache.zookeeper.common.QuorumX509Util; import org.apache.zookeeper.common.Time; import org.apache.zookeeper.common.X509Exception; import org.apache.zookeeper.common.X509Util; @@ -222,16 +223,17 @@ public boolean isQuorumSynced(QuorumVerifier qv) { this.self = self; try { if (self.shouldUsePortUnification()) { + if (self.getQuorumListenOnAllIPs()) { - ss = new UnifiedServerSocket(self.getQuorumAddress().getPort()); + ss = new UnifiedServerSocket(new QuorumX509Util(), self.getQuorumAddress().getPort()); } else { - ss = new UnifiedServerSocket(); + ss = new UnifiedServerSocket(new QuorumX509Util()); } } else if (self.isSslQuorum()) { if (self.getQuorumListenOnAllIPs()) { - ss = X509Util.QUORUM_X509UTIL.createSSLServerSocket(self.getQuorumAddress().getPort()); + ss = new QuorumX509Util().createSSLServerSocket(self.getQuorumAddress().getPort()); } else { - ss = X509Util.QUORUM_X509UTIL.createSSLServerSocket(); + ss = new QuorumX509Util().createSSLServerSocket(); } } else { if (self.getQuorumListenOnAllIPs()) { diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Learner.java b/src/java/main/org/apache/zookeeper/server/quorum/Learner.java index 443c9c9f396..78dc58f6715 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Learner.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Learner.java @@ -38,6 +38,7 @@ import org.apache.jute.InputArchive; import org.apache.jute.OutputArchive; import org.apache.jute.Record; +import org.apache.zookeeper.common.QuorumX509Util; import org.apache.zookeeper.common.X509Exception; import org.apache.zookeeper.common.X509Util; import org.slf4j.Logger; @@ -238,7 +239,8 @@ protected void sockConnect(Socket sock, InetSocketAddress addr, int timeout) */ protected void connectToLeader(QuorumServer leader) throws IOException, InterruptedException, X509Exception { - createSocket(); + QuorumX509Util quorumX509Util = new QuorumX509Util(); + createSocket(quorumX509Util); int initLimitTime = self.tickTime * self.initLimit; int remainingInitLimitTime = initLimitTime; @@ -276,7 +278,7 @@ protected void connectToLeader(QuorumServer leader) LOG.warn("Unexpected exception, tries=" + tries + ", remaining init limit=" + remainingInitLimitTime + ", connecting to " + leader.addr,e); - createSocket(); + createSocket(quorumX509Util); } } Thread.sleep(1000); @@ -287,9 +289,9 @@ protected void connectToLeader(QuorumServer leader) leaderOs = BinaryOutputArchive.getArchive(bufferedOutput); } - private void createSocket() throws X509Exception, IOException { + private void createSocket(X509Util x509Util) throws X509Exception, IOException { if (self.isSslQuorum()) { - sock = X509Util.QUORUM_X509UTIL.createSSLSocket(); + sock = x509Util.createSSLSocket(); } else { sock = new Socket(); } diff --git a/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java b/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java index c10cafd17b0..1bd64bd8bb8 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/QuorumCnxManager.java @@ -37,6 +37,7 @@ import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicInteger; +import org.apache.zookeeper.common.QuorumX509Util; import org.apache.zookeeper.common.X509Exception; import org.apache.zookeeper.common.X509Util; import org.apache.zookeeper.server.ZooKeeperThread; @@ -143,6 +144,8 @@ public class QuorumCnxManager { */ private AtomicInteger threadCnt = new AtomicInteger(0); + private X509Util x509Util; + static public class Message { Message(ByteBuffer buffer, long sid) { this.buffer = buffer; @@ -235,6 +238,8 @@ public QuorumCnxManager(QuorumPeer self) { // Starts listener thread that waits for connection requests listener = new Listener(); listener.setName("QuorumPeerListener"); + + x509Util = new QuorumX509Util(); } /** @@ -443,7 +448,7 @@ synchronized private boolean connectOne(long sid, InetSocketAddress electionAddr try { LOG.debug("Opening channel to server " + sid); if (self.isSslQuorum()) { - SSLSocket sslSock = X509Util.QUORUM_X509UTIL.createSSLSocket(); + SSLSocket sslSock = x509Util.createSSLSocket(); setSockOpts(sslSock); sslSock.connect(electionAddr, cnxTO); sslSock.startHandshake(); @@ -641,9 +646,9 @@ public void run() { while((!shutdown) && (numRetries < 3)){ try { if (self.shouldUsePortUnification()) { - ss = new UnifiedServerSocket(); + ss = new UnifiedServerSocket(x509Util); } else if (self.isSslQuorum()) { - ss = X509Util.QUORUM_X509UTIL.createSSLServerSocket(); + ss = x509Util.createSSLServerSocket(); } else { ss = new ServerSocket(); } diff --git a/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java b/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java index 4a0de320b5c..0005a7525a4 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java @@ -36,6 +36,7 @@ import java.util.Properties; import java.util.Map.Entry; +import org.apache.zookeeper.common.ClientX509Util; import org.apache.zookeeper.common.StringUtils; import org.apache.zookeeper.common.X509Util; import org.apache.zookeeper.common.ZKConfig; @@ -382,14 +383,15 @@ public void parseProperties(Properties zkProp) * provider is not configured. */ private void configureSSLAuth() throws ConfigException { - String sslAuthProp = "zookeeper.authProvider." + System.getProperty(X509Util.CLIENT_X509UTIL.getSslAuthProviderProperty(), "x509"); + ClientX509Util clientX509Util = new ClientX509Util(); + String sslAuthProp = "zookeeper.authProvider." + System.getProperty(clientX509Util.getSslAuthProviderProperty(), "x509"); if (System.getProperty(sslAuthProp) == null) { if ("zookeeper.authProvider.x509".equals(sslAuthProp)) { System.setProperty("zookeeper.authProvider.x509", "org.apache.zookeeper.server.auth.X509AuthenticationProvider"); } else { throw new ConfigException("No auth provider configured for the SSL authentication scheme '" - + System.getProperty(X509Util.CLIENT_X509UTIL.getSslAuthProviderProperty()) + "'."); + + System.getProperty(clientX509Util.getSslAuthProviderProperty()) + "'."); } } } diff --git a/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java b/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java index ecedf052fbc..8c5599c3203 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java @@ -34,12 +34,16 @@ public class UnifiedServerSocket extends ServerSocket { private static final Logger LOG = LoggerFactory.getLogger(UnifiedServerSocket.class); - public UnifiedServerSocket() throws IOException { + private X509Util x509Util; + + public UnifiedServerSocket(X509Util x509Util) throws IOException { super(); + this.x509Util = x509Util; } - public UnifiedServerSocket(int port) throws IOException { + public UnifiedServerSocket(X509Util x509Util, int port) throws IOException { super(port); + this.x509Util = x509Util; } @Override @@ -65,7 +69,7 @@ public Socket accept() throws IOException { LOG.info(getInetAddress() + " attempting to connect over ssl"); SSLSocket sslSocket; try { - sslSocket = (SSLSocket) X509Util.QUORUM_X509UTIL.getDefaultSSLContext().getSocketFactory().createSocket(bufferedSocket, null, bufferedSocket.getPort(), false); + sslSocket = (SSLSocket) x509Util.getDefaultSSLContext().getSocketFactory().createSocket(bufferedSocket, null, bufferedSocket.getPort(), false); } catch (X509Exception.SSLContextException e) { throw new IOException("failed to create SSL context", e); } diff --git a/src/java/test/org/apache/zookeeper/server/quorum/LearnerTest.java b/src/java/test/org/apache/zookeeper/server/quorum/LearnerTest.java index 85284f6d42d..4b085ebfb16 100644 --- a/src/java/test/org/apache/zookeeper/server/quorum/LearnerTest.java +++ b/src/java/test/org/apache/zookeeper/server/quorum/LearnerTest.java @@ -110,7 +110,7 @@ public void connectionRetryTimeoutTest() throws Exception { InetSocketAddress addr = new InetSocketAddress(1111); // we expect this to throw an IOException since we're faking socket connect errors every time - learner.connectToLeader(addr); + learner.connectToLeader(new QuorumPeer.QuorumServer(0, addr)); } @Test public void connectionInitLimitTimeoutTest() throws Exception { @@ -130,7 +130,7 @@ public void connectionInitLimitTimeoutTest() throws Exception { // we expect this to throw an IOException since we're faking socket connect errors every time try { - learner.connectToLeader(addr); + learner.connectToLeader(new QuorumPeer.QuorumServer(0, addr)); Assert.fail("should have thrown IOException!"); } catch (IOException e) { //good, wanted to see that, let's make sure we ran out of time diff --git a/src/java/test/org/apache/zookeeper/server/quorum/QuorumPeerConfigTest.java b/src/java/test/org/apache/zookeeper/server/quorum/QuorumPeerConfigTest.java index b9cdce8461d..70de73dd985 100644 --- a/src/java/test/org/apache/zookeeper/server/quorum/QuorumPeerConfigTest.java +++ b/src/java/test/org/apache/zookeeper/server/quorum/QuorumPeerConfigTest.java @@ -26,6 +26,8 @@ import java.io.IOException; import java.util.Properties; +import org.apache.zookeeper.common.ClientX509Util; +import org.apache.zookeeper.common.X509Util; import org.apache.zookeeper.common.ZKConfig; import org.apache.zookeeper.server.quorum.QuorumPeerConfig.ConfigException; import org.junit.Test; @@ -91,7 +93,7 @@ public void testConfigureSSLAuthGetsConfiguredIfSecurePortConfigured() @Test public void testCustomSSLAuth() throws IOException{ - System.setProperty(ZKConfig.SSL_AUTHPROVIDER, "y509"); + System.setProperty(new ClientX509Util().getSslAuthProviderProperty(), "y509"); QuorumPeerConfig quorumPeerConfig = new QuorumPeerConfig(); try { Properties zkProp = getDefaultZKProperties(); diff --git a/src/java/test/org/apache/zookeeper/server/quorum/QuorumPeerTestBase.java b/src/java/test/org/apache/zookeeper/server/quorum/QuorumPeerTestBase.java index 51f444bc2b9..eb200d978ac 100644 --- a/src/java/test/org/apache/zookeeper/server/quorum/QuorumPeerTestBase.java +++ b/src/java/test/org/apache/zookeeper/server/quorum/QuorumPeerTestBase.java @@ -26,8 +26,11 @@ import java.io.FileWriter; import java.io.FilenameFilter; import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; import java.util.Properties; +import org.apache.zookeeper.common.X509Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.zookeeper.WatchedEvent; @@ -63,6 +66,10 @@ public void shutdown() { } public static class MainThread implements Runnable { + public File getConfFile() { + return confFile; + } + final File confFile; final File tmpDir; diff --git a/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java b/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java index 51ed5298a3f..2a0c04a9227 100644 --- a/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java +++ b/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java @@ -1272,14 +1272,14 @@ static class ConversableFollower extends Follower { super(self, zk); } - InetSocketAddress leaderAddr; + QuorumServer leader; public void setLeaderSocketAddress(InetSocketAddress addr) { - leaderAddr = addr; + leader.addr = addr; } @Override - protected InetSocketAddress findLeader() { - return leaderAddr; + protected QuorumServer findLeader() { + return leader; } } private ConversableFollower createFollower(File tmpDir, QuorumPeer peer) @@ -1298,14 +1298,14 @@ static class ConversableObserver extends Observer { super(self, zk); } - InetSocketAddress leaderAddr; + QuorumServer leader; public void setLeaderSocketAddress(InetSocketAddress addr) { - leaderAddr = addr; + leader.addr = addr; } @Override - protected InetSocketAddress findLeader() { - return leaderAddr; + protected QuorumServer findLeader() { + return leader; } } diff --git a/src/java/test/org/apache/zookeeper/test/SSLTest.java b/src/java/test/org/apache/zookeeper/test/ClientSSLTest.java similarity index 84% rename from src/java/test/org/apache/zookeeper/test/SSLTest.java rename to src/java/test/org/apache/zookeeper/test/ClientSSLTest.java index b2fcd706b81..08ffb4eef58 100644 --- a/src/java/test/org/apache/zookeeper/test/SSLTest.java +++ b/src/java/test/org/apache/zookeeper/test/ClientSSLTest.java @@ -27,7 +27,7 @@ import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.ZooKeeper; import org.apache.zookeeper.client.ZKClientConfig; -import org.apache.zookeeper.common.ZKConfig; +import org.apache.zookeeper.common.ClientX509Util; import org.apache.zookeeper.server.ServerCnxnFactory; import org.apache.zookeeper.server.quorum.QuorumPeerTestBase; import org.junit.After; @@ -35,7 +35,9 @@ import org.junit.Before; import org.junit.Test; -public class SSLTest extends QuorumPeerTestBase { +public class ClientSSLTest extends QuorumPeerTestBase { + + private ClientX509Util clientX509Util = new ClientX509Util(); @Before public void setup() { @@ -43,10 +45,10 @@ public void setup() { System.setProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY, "org.apache.zookeeper.server.NettyServerCnxnFactory"); System.setProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET, "org.apache.zookeeper.ClientCnxnSocketNetty"); System.setProperty(ZKClientConfig.SECURE_CLIENT, "true"); - System.setProperty(ZKConfig.SSL_KEYSTORE_LOCATION, testDataPath + "/ssl/testKeyStore.jks"); - System.setProperty(ZKConfig.SSL_KEYSTORE_PASSWD, "testpass"); - System.setProperty(ZKConfig.SSL_TRUSTSTORE_LOCATION, testDataPath + "/ssl/testTrustStore.jks"); - System.setProperty(ZKConfig.SSL_TRUSTSTORE_PASSWD, "testpass"); + System.setProperty(clientX509Util.getSslKeystoreLocationProperty(), testDataPath + "/ssl/testKeyStore.jks"); + System.setProperty(clientX509Util.getSslKeystorePasswdProperty(), "testpass"); + System.setProperty(clientX509Util.getSslTruststoreLocationProperty(), testDataPath + "/ssl/testTrustStore.jks"); + System.setProperty(clientX509Util.getSslTruststorePasswdProperty(), "testpass"); } @After @@ -54,10 +56,10 @@ public void teardown() throws Exception { System.clearProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY); System.clearProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET); System.clearProperty(ZKClientConfig.SECURE_CLIENT); - System.clearProperty(ZKConfig.SSL_KEYSTORE_LOCATION); - System.clearProperty(ZKConfig.SSL_KEYSTORE_PASSWD); - System.clearProperty(ZKConfig.SSL_TRUSTSTORE_LOCATION); - System.clearProperty(ZKConfig.SSL_TRUSTSTORE_PASSWD); + System.clearProperty(clientX509Util.getSslKeystoreLocationProperty()); + System.clearProperty(clientX509Util.getSslKeystorePasswdProperty()); + System.clearProperty(clientX509Util.getSslTruststoreLocationProperty()); + System.clearProperty(clientX509Util.getSslTruststorePasswdProperty()); } /** diff --git a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java new file mode 100644 index 00000000000..84322774fca --- /dev/null +++ b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java @@ -0,0 +1,478 @@ +/** + * 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.test; + +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.x500.X500Name; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x500.style.BCStyle; +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.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.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.jcajce.JcaContentSignerBuilder; +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.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.math.BigInteger; +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.X509Certificate; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +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 QuorumX509Util quorumX509Util = new QuorumX509Util(); + + private int clientPortQp1; + private int clientPortQp2; + private int clientPortQp3; + + private String quorumConfiguration; + private String validKeystorePath; + private String badhostnameKeystorePath; + private String revokedKeystorePath; + private String truststorePath; + private String crlPath; + + private KeyPair rootKeyPair; + private X509Certificate rootCertificate; + + @Before + public void setup() throws Exception { + ClientBase.setupTestEnv(); + + clientPortQp1 = PortAssignment.unique(); + clientPortQp2 = PortAssignment.unique(); + clientPortQp3 = PortAssignment.unique(); + + File tmpDir = createTmpDir(); + validKeystorePath = tmpDir.getAbsolutePath() + "/valid.jks"; + truststorePath = tmpDir.getAbsolutePath() + "/truststore.jks"; + crlPath = tmpDir.getAbsolutePath() + "/crl.pem"; + + quorumConfiguration = generateQuorumConfiguration(); + + + Security.addProvider(new BouncyCastleProvider()); + + rootKeyPair = createKeyPair("RSA", 4096); + rootCertificate = createSelfSignedCertifcate(rootKeyPair); + + KeyPair entityKeyPair = createKeyPair("RSA", 4096); + X509Certificate entityCertificate = buildEndEntityCert(entityKeyPair, rootCertificate, rootKeyPair.getPrivate(), "localhost"); + + KeyStore outStore = KeyStore.getInstance(KeyStore.getDefaultType()); + outStore.load(null, "testpass".toCharArray()); + outStore.setKeyEntry("mykey", entityKeyPair.getPrivate(), "testpass".toCharArray(), new Certificate[] { entityCertificate }); + FileOutputStream outputStream = new FileOutputStream(validKeystorePath); + outStore.store(outputStream, "testpass".toCharArray()); + outputStream.flush(); + outputStream.close(); + + + badhostnameKeystorePath = tmpDir + "/badhost.jks"; + X509Certificate badHostCert = buildEndEntityCert(entityKeyPair, rootCertificate, rootKeyPair.getPrivate(), "bleepbloop"); + outStore = KeyStore.getInstance(KeyStore.getDefaultType()); + outStore.load(null, "testpass".toCharArray()); + outStore.setKeyEntry("mykey", entityKeyPair.getPrivate(), "testpass".toCharArray(), new Certificate[] { badHostCert }); + outputStream = new FileOutputStream(badhostnameKeystorePath); + outStore.store(outputStream, "testpass".toCharArray()); + outputStream.flush(); + outputStream.close(); + + revokedKeystorePath = tmpDir + "/revoked.jks"; + X509Certificate revokedCert = buildEndEntityCert(entityKeyPair, rootCertificate, rootKeyPair.getPrivate(), "localhost"); + outStore = KeyStore.getInstance(KeyStore.getDefaultType()); + outStore.load(null, "testpass".toCharArray()); + outStore.setKeyEntry("mykey", entityKeyPair.getPrivate(), "testpass".toCharArray(), new Certificate[] { revokedCert }); + outputStream = new FileOutputStream(revokedKeystorePath); + outStore.store(outputStream, "testpass".toCharArray()); + outputStream.flush(); + outputStream.close(); + buildCRL(revokedCert); + + outStore = KeyStore.getInstance(KeyStore.getDefaultType()); + outStore.load(null, "testpass".toCharArray()); + outStore.setCertificateEntry(rootCertificate.getSubjectDN().toString(), rootCertificate); + outputStream = new FileOutputStream(truststorePath); + outStore.store(outputStream, "testpass".toCharArray()); + outputStream.flush(); + outputStream.close(); + + } + + private X509Certificate createSelfSignedCertifcate(KeyPair keyPair) throws Exception { + X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); + nameBuilder.addRDN(BCStyle.CN, "localhost"); + 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()); + certificateBuilder + .addExtension(Extension.basicConstraints, true, new BasicConstraints(0)) + .addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign)); + + ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.getPrivate()); + + + return new JcaX509CertificateConverter().getCertificate(certificateBuilder.build(contentSigner)); + } + + private void buildCRL(X509Certificate x509Certificate) throws Exception { + X509v2CRLBuilder builder = new JcaX509v2CRLBuilder(x509Certificate.getIssuerX500Principal(), new Date()); + 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(); + 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"))); + + JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption"); + + X509CRLHolder cRLHolder = builder.build(contentSignerBuilder.build(rootKeyPair.getPrivate())); + + + PemWriter pemWriter = new PemWriter(new FileWriter(crlPath)); + pemWriter.writeObject(new MiscPEMGenerator(cRLHolder)); + pemWriter.flush(); + pemWriter.close(); + } + + /** + * Build a sample V3 certificate to use as an end entity certificate + */ + public X509Certificate buildEndEntityCert(KeyPair keyPair, X509Certificate caCert, PrivateKey caPrivateKey, String hostname) + throws Exception + { + X509CertificateHolder holder = new JcaX509CertificateHolder(caCert); + ContentSigner signer =new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(caPrivateKey); + + + SubjectPublicKeyInfo entityKeyInfo = + SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(PublicKeyFactory.createKey(keyPair.getPublic().getEncoded())); + X509v3CertificateBuilder certBldr = 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()); + X509ExtensionUtils extUtils = new BcX509ExtensionUtils(); + certBldr.addExtension(Extension.authorityKeyIdentifier, + false, extUtils.createAuthorityKeyIdentifier(holder)) + .addExtension(Extension.subjectKeyIdentifier, + false, extUtils.createSubjectKeyIdentifier(entityKeyInfo)) + .addExtension(Extension.basicConstraints, + true, new BasicConstraints(false)) + .addExtension(Extension.keyUsage, + true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment)) + .addExtension(Extension.subjectAlternativeName, true, new GeneralNames(new GeneralName(GeneralName.dNSName, hostname))); + + DistributionPointName distPointOne = new DistributionPointName(new GeneralNames( + new GeneralName(GeneralName.uniformResourceIdentifier,"file://" + crlPath))); + + + certBldr.addExtension(Extension.cRLDistributionPoints, false, new CRLDistPoint(new DistributionPoint[] { new DistributionPoint(distPointOne, null, null) })); + return new JcaX509CertificateConverter().getCertificate(certBldr.build(signer)); + } + + + private KeyPair createKeyPair(String encryptionType, int byteCount) + throws NoSuchProviderException, NoSuchAlgorithmException + { + KeyPairGenerator keyPairGenerator = createKeyPairGenerator(encryptionType, byteCount); + KeyPair keyPair = keyPairGenerator.genKeyPair(); + return keyPair; + } + + + private KeyPairGenerator createKeyPairGenerator(String algorithmIdentifier, + int bitCount) throws NoSuchProviderException, + NoSuchAlgorithmException { + KeyPairGenerator kpg = KeyPairGenerator.getInstance( + algorithmIdentifier, BouncyCastleProvider.PROVIDER_NAME); + kpg.initialize(bitCount); + return kpg; + } + + 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 clearSSLSystemProperties() { + System.clearProperty(quorumX509Util.getSslKeystoreLocationProperty()); + System.clearProperty(quorumX509Util.getSslKeystorePasswdProperty()); + System.clearProperty(quorumX509Util.getSslTruststoreLocationProperty()); + System.clearProperty(quorumX509Util.getSslTruststorePasswdProperty()); + System.clearProperty(quorumX509Util.getSslHostnameVerificationEnabledProperty()); + } + + @Test + public void testQuorumSSL() throws Exception { + setSSLSystemProperties(); + + MainThread q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED); + MainThread 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. + MainThread 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 + Map members = new HashMap<>(); + members.put(clientPortQp1, new MainThread(1, clientPortQp1, quorumConfiguration)); + members.put(clientPortQp2, new MainThread(2, clientPortQp2, quorumConfiguration)); + members.put(clientPortQp3, new MainThread(3, clientPortQp3, quorumConfiguration)); + + 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); + // TODO: Is appending a new line to config a fair assumption + stopAppendConfigRestartAll(members, PORT_UNIFICATION_DISABLED); + } + + private void stopAppendConfigRestartAll(Map members, String config) throws Exception { + for (Map.Entry 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 testHostnameVerification() throws Exception { + setSSLSystemProperties(); + + MainThread q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED); + MainThread 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(), badhostnameKeystorePath); + + // This server should join successfully + MainThread 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.getSslHostnameVerificationEnabledProperty(), "true"); + + 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(), badhostnameKeystorePath); + q3.start(); + + Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); + } + + + @Test + public void testCertificateRevocabtionList() throws Exception { + System.setProperty("javax.net.debug", "all"); + System.setProperty("java.security.auth.debug", "all"); + System.setProperty("java.security.debug", "all"); + setSSLSystemProperties(); + + MainThread q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED); + MainThread 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(), revokedKeystorePath); + + // This server should join successfully + MainThread 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"); + System.setProperty("com.sun.net.ssl.checkRevocation", "true"); + System.setProperty("com.sun.security.enableCRLDP", "true"); + + 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(), revokedKeystorePath); + q3.start(); + + Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); + + + + } + + // ocsp + +} diff --git a/src/java/test/org/apache/zookeeper/test/SSLAuthTest.java b/src/java/test/org/apache/zookeeper/test/SSLAuthTest.java index 337c8f7df6c..8fd35bc6185 100644 --- a/src/java/test/org/apache/zookeeper/test/SSLAuthTest.java +++ b/src/java/test/org/apache/zookeeper/test/SSLAuthTest.java @@ -23,7 +23,8 @@ import org.apache.zookeeper.PortAssignment; import org.apache.zookeeper.TestableZooKeeper; import org.apache.zookeeper.client.ZKClientConfig; -import org.apache.zookeeper.common.ZKConfig; +import org.apache.zookeeper.common.ClientX509Util; +import org.apache.zookeeper.common.X509Util; import org.apache.zookeeper.server.ServerCnxnFactory; import org.junit.After; import org.junit.Assert; @@ -31,17 +32,20 @@ import org.junit.Test; public class SSLAuthTest extends ClientBase { + + private ClientX509Util clientX509Util = new ClientX509Util(); + @Before public void setUp() throws Exception { String testDataPath = System.getProperty("test.data.dir", "build/test/data"); System.setProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY, "org.apache.zookeeper.server.NettyServerCnxnFactory"); System.setProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET, "org.apache.zookeeper.ClientCnxnSocketNetty"); System.setProperty(ZKClientConfig.SECURE_CLIENT, "true"); - System.setProperty(ZKConfig.SSL_AUTHPROVIDER, "x509"); - System.setProperty(ZKConfig.SSL_KEYSTORE_LOCATION, testDataPath + "/ssl/testKeyStore.jks"); - System.setProperty(ZKConfig.SSL_KEYSTORE_PASSWD, "testpass"); - System.setProperty(ZKConfig.SSL_TRUSTSTORE_LOCATION, testDataPath + "/ssl/testTrustStore.jks"); - System.setProperty(ZKConfig.SSL_TRUSTSTORE_PASSWD, "testpass"); + System.setProperty(clientX509Util.getSslAuthProviderProperty(), "x509"); + System.setProperty(clientX509Util.getSslKeystoreLocationProperty(), testDataPath + "/ssl/testKeyStore.jks"); + System.setProperty(clientX509Util.getSslKeystorePasswdProperty(), "testpass"); + System.setProperty(clientX509Util.getSslTruststoreLocationProperty(), testDataPath + "/ssl/testTrustStore.jks"); + System.setProperty(clientX509Util.getSslTruststorePasswdProperty(), "testpass"); System.setProperty("javax.net.debug", "ssl"); System.setProperty("zookeeper.authProvider.x509", "org.apache.zookeeper.server.auth.X509AuthenticationProvider"); @@ -60,11 +64,11 @@ public void teardown() throws Exception { System.clearProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY); System.clearProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET); System.clearProperty(ZKClientConfig.SECURE_CLIENT); - System.clearProperty(ZKConfig.SSL_AUTHPROVIDER); - System.clearProperty(ZKConfig.SSL_KEYSTORE_LOCATION); - System.clearProperty(ZKConfig.SSL_KEYSTORE_PASSWD); - System.clearProperty(ZKConfig.SSL_TRUSTSTORE_LOCATION); - System.clearProperty(ZKConfig.SSL_TRUSTSTORE_PASSWD); + System.clearProperty(clientX509Util.getSslAuthProviderProperty()); + System.clearProperty(clientX509Util.getSslKeystoreLocationProperty()); + System.clearProperty(clientX509Util.getSslKeystorePasswdProperty()); + System.clearProperty(clientX509Util.getSslTruststoreLocationProperty()); + System.clearProperty(clientX509Util.getSslTruststorePasswdProperty()); System.clearProperty("javax.net.debug"); System.clearProperty("zookeeper.authProvider.x509"); } @@ -74,8 +78,8 @@ public void testRejection() throws Exception { String testDataPath = System.getProperty("test.data.dir", "build/test/data"); // Replace trusted keys with a valid key that is not trusted by the server - System.setProperty(ZKConfig.SSL_KEYSTORE_LOCATION, testDataPath + "/ssl/testUntrustedKeyStore.jks"); - System.setProperty(ZKConfig.SSL_KEYSTORE_PASSWD, "testpass"); + System.setProperty(clientX509Util.getSslKeystoreLocationProperty(), testDataPath + "/ssl/testUntrustedKeyStore.jks"); + System.setProperty(clientX509Util.getSslKeystorePasswdProperty(), "testpass"); CountdownWatcher watcher = new CountdownWatcher(); @@ -87,11 +91,11 @@ public void testRejection() throws Exception { @Test public void testMisconfiguration() throws Exception { - System.clearProperty(ZKConfig.SSL_AUTHPROVIDER); - System.clearProperty(ZKConfig.SSL_KEYSTORE_LOCATION); - System.clearProperty(ZKConfig.SSL_KEYSTORE_PASSWD); - System.clearProperty(ZKConfig.SSL_TRUSTSTORE_LOCATION); - System.clearProperty(ZKConfig.SSL_TRUSTSTORE_PASSWD); + System.clearProperty(clientX509Util.getSslAuthProviderProperty()); + System.clearProperty(clientX509Util.getSslKeystoreLocationProperty()); + System.clearProperty(clientX509Util.getSslKeystorePasswdProperty()); + System.clearProperty(clientX509Util.getSslTruststoreLocationProperty()); + System.clearProperty(clientX509Util.getSslTruststorePasswdProperty()); CountdownWatcher watcher = new CountdownWatcher(); new TestableZooKeeper(hostPort, CONNECTION_TIMEOUT, watcher); From c564ce839e2823dd69bfab8d26aed6c7284f2025 Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Mon, 3 Apr 2017 20:01:33 -0700 Subject: [PATCH 08/25] OCSP test working --- .../apache/zookeeper/test/QuorumSSLTest.java | 375 ++++++++++++------ 1 file changed, 249 insertions(+), 126 deletions(-) diff --git a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java index 84322774fca..f00fc29ec8e 100644 --- a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java +++ b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java @@ -17,14 +17,20 @@ */ 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; @@ -36,6 +42,7 @@ 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; @@ -47,22 +54,40 @@ 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.File; 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; @@ -71,6 +96,7 @@ 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.Calendar; import java.util.Date; @@ -87,89 +113,137 @@ public class QuorumSSLTest extends QuorumPeerTestBase { 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 badhostnameKeystorePath; - private String revokedKeystorePath; private String truststorePath; - private String crlPath; private KeyPair rootKeyPair; private X509Certificate rootCertificate; + private ContentSigner contentSigner; + @Before public void setup() throws Exception { ClientBase.setupTestEnv(); + tmpDir = createTmpDir().getAbsolutePath(); + clientPortQp1 = PortAssignment.unique(); clientPortQp2 = PortAssignment.unique(); clientPortQp3 = PortAssignment.unique(); - File tmpDir = createTmpDir(); - validKeystorePath = tmpDir.getAbsolutePath() + "/valid.jks"; - truststorePath = tmpDir.getAbsolutePath() + "/truststore.jks"; - crlPath = tmpDir.getAbsolutePath() + "/crl.pem"; + validKeystorePath = tmpDir + "/valid.jks"; + truststorePath = tmpDir + "/truststore.jks"; quorumConfiguration = generateQuorumConfiguration(); - Security.addProvider(new BouncyCastleProvider()); - rootKeyPair = createKeyPair("RSA", 4096); + rootKeyPair = createKeyPair(); + contentSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(rootKeyPair.getPrivate()); rootCertificate = createSelfSignedCertifcate(rootKeyPair); - KeyPair entityKeyPair = createKeyPair("RSA", 4096); - X509Certificate entityCertificate = buildEndEntityCert(entityKeyPair, rootCertificate, rootKeyPair.getPrivate(), "localhost"); - - KeyStore outStore = KeyStore.getInstance(KeyStore.getDefaultType()); - outStore.load(null, "testpass".toCharArray()); - outStore.setKeyEntry("mykey", entityKeyPair.getPrivate(), "testpass".toCharArray(), new Certificate[] { entityCertificate }); - FileOutputStream outputStream = new FileOutputStream(validKeystorePath); - outStore.store(outputStream, "testpass".toCharArray()); + // 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(); + KeyPair defaultKeyPair = createKeyPair(); + X509Certificate validCertificate = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), HOSTNAME, null, null); + writeKeystore(validCertificate, defaultKeyPair, validKeystorePath); - badhostnameKeystorePath = tmpDir + "/badhost.jks"; - X509Certificate badHostCert = buildEndEntityCert(entityKeyPair, rootCertificate, rootKeyPair.getPrivate(), "bleepbloop"); - outStore = KeyStore.getInstance(KeyStore.getDefaultType()); - outStore.load(null, "testpass".toCharArray()); - outStore.setKeyEntry("mykey", entityKeyPair.getPrivate(), "testpass".toCharArray(), new Certificate[] { badHostCert }); - outputStream = new FileOutputStream(badhostnameKeystorePath); - outStore.store(outputStream, "testpass".toCharArray()); - outputStream.flush(); - outputStream.close(); + setSSLSystemProperties(); + } - revokedKeystorePath = tmpDir + "/revoked.jks"; - X509Certificate revokedCert = buildEndEntityCert(entityKeyPair, rootCertificate, rootKeyPair.getPrivate(), "localhost"); - outStore = KeyStore.getInstance(KeyStore.getDefaultType()); - outStore.load(null, "testpass".toCharArray()); - outStore.setKeyEntry("mykey", entityKeyPair.getPrivate(), "testpass".toCharArray(), new Certificate[] { revokedCert }); - outputStream = new FileOutputStream(revokedKeystorePath); - outStore.store(outputStream, "testpass".toCharArray()); + 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(); - buildCRL(revokedCert); + } - outStore = KeyStore.getInstance(KeyStore.getDefaultType()); - outStore.load(null, "testpass".toCharArray()); - outStore.setCertificateEntry(rootCertificate.getSubjectDN().toString(), rootCertificate); - outputStream = new FileOutputStream(truststorePath); - outStore.store(outputStream, "testpass".toCharArray()); - 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, "localhost"); + nameBuilder.addRDN(BCStyle.CN, HOSTNAME); Date notBefore = new Date(); // time from which certificate is valid Calendar cal = Calendar.getInstance(); cal.setTime(notBefore); @@ -178,20 +252,16 @@ private X509Certificate createSelfSignedCertifcate(KeyPair keyPair) throws Excep BigInteger serialNumber = new BigInteger(128, new Random()); X509v3CertificateBuilder certificateBuilder = - new JcaX509v3CertificateBuilder(nameBuilder.build(), serialNumber, notBefore, notAfter, nameBuilder.build(), keyPair.getPublic()); - 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)); - ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.getPrivate()); - - return new JcaX509CertificateConverter().getCertificate(certificateBuilder.build(contentSigner)); } - private void buildCRL(X509Certificate x509Certificate) throws Exception { + private void buildCRL(X509Certificate x509Certificate, String crlPath) throws Exception { X509v2CRLBuilder builder = new JcaX509v2CRLBuilder(x509Certificate.getIssuerX500Principal(), new Date()); - Date notBefore = new Date(); // time from which certificate is valid + Date notBefore = new Date(); Calendar cal = Calendar.getInstance(); cal.setTime(notBefore); cal.add(Calendar.YEAR, 1); @@ -201,10 +271,7 @@ private void buildCRL(X509Certificate x509Certificate) throws Exception { builder.addExtension(Extension.authorityKeyIdentifier, false, new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(rootCertificate)); builder.addExtension(Extension.cRLNumber, false, new CRLNumber(new BigInteger("1000"))); - JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption"); - - X509CRLHolder cRLHolder = builder.build(contentSignerBuilder.build(rootKeyPair.getPrivate())); - + X509CRLHolder cRLHolder = builder.build(contentSigner); PemWriter pemWriter = new PemWriter(new FileWriter(crlPath)); pemWriter.writeObject(new MiscPEMGenerator(cRLHolder)); @@ -212,63 +279,48 @@ private void buildCRL(X509Certificate x509Certificate) throws Exception { pemWriter.close(); } - /** - * Build a sample V3 certificate to use as an end entity certificate - */ - public X509Certificate buildEndEntityCert(KeyPair keyPair, X509Certificate caCert, PrivateKey caPrivateKey, String hostname) - throws Exception - { + public X509Certificate buildEndEntityCert(KeyPair keyPair, X509Certificate caCert, PrivateKey caPrivateKey, + String hostname, String crlPath, Integer ocspPort) throws Exception { X509CertificateHolder holder = new JcaX509CertificateHolder(caCert); ContentSigner signer =new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(caPrivateKey); SubjectPublicKeyInfo entityKeyInfo = SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(PublicKeyFactory.createKey(keyPair.getPublic().getEncoded())); - X509v3CertificateBuilder certBldr = 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()); - X509ExtensionUtils extUtils = new BcX509ExtensionUtils(); - certBldr.addExtension(Extension.authorityKeyIdentifier, - false, extUtils.createAuthorityKeyIdentifier(holder)) - .addExtension(Extension.subjectKeyIdentifier, - false, extUtils.createSubjectKeyIdentifier(entityKeyInfo)) - .addExtension(Extension.basicConstraints, - true, new BasicConstraints(false)) - .addExtension(Extension.keyUsage, - true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment)) + 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)) .addExtension(Extension.subjectAlternativeName, true, new GeneralNames(new GeneralName(GeneralName.dNSName, hostname))); - DistributionPointName distPointOne = new DistributionPointName(new GeneralNames( - new GeneralName(GeneralName.uniformResourceIdentifier,"file://" + crlPath))); + 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))); + } - certBldr.addExtension(Extension.cRLDistributionPoints, false, new CRLDistPoint(new DistributionPoint[] { new DistributionPoint(distPointOne, null, null) })); - return new JcaX509CertificateConverter().getCertificate(certBldr.build(signer)); + return new JcaX509CertificateConverter().getCertificate(certificateBuilder.build(signer)); } - private KeyPair createKeyPair(String encryptionType, int byteCount) - throws NoSuchProviderException, NoSuchAlgorithmException - { - KeyPairGenerator keyPairGenerator = createKeyPairGenerator(encryptionType, byteCount); + private KeyPair createKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); + keyPairGenerator.initialize(4096); KeyPair keyPair = keyPairGenerator.genKeyPair(); return keyPair; } - - private KeyPairGenerator createKeyPairGenerator(String algorithmIdentifier, - int bitCount) throws NoSuchProviderException, - NoSuchAlgorithmException { - KeyPairGenerator kpg = KeyPairGenerator.getInstance( - algorithmIdentifier, BouncyCastleProvider.PROVIDER_NAME); - kpg.initialize(bitCount); - return kpg; - } - private String generateQuorumConfiguration() { int portQp1 = PortAssignment.unique(); int portQp2 = PortAssignment.unique(); @@ -297,20 +349,27 @@ public void setSSLSystemProperties() { } @After - public void clearSSLSystemProperties() { + public void cleanUp() throws Exception { + clearSSLSystemProperties(); + q1.shutdown(); + q2.shutdown(); + q3.shutdown(); + } + + 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 public void testQuorumSSL() throws Exception { - setSSLSystemProperties(); - - MainThread q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED); - MainThread q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED); + q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED); + q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED); q1.start(); @@ -322,21 +381,24 @@ public void testQuorumSSL() throws Exception { clearSSLSystemProperties(); // This server should fail to join the quorum as it is not using ssl. - MainThread q3 = new MainThread(3, clientPortQp3, quorumConfiguration); + 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 members = new HashMap<>(); - members.put(clientPortQp1, new MainThread(1, clientPortQp1, quorumConfiguration)); - members.put(clientPortQp2, new MainThread(2, clientPortQp2, quorumConfiguration)); - members.put(clientPortQp3, new MainThread(3, clientPortQp3, quorumConfiguration)); + members.put(clientPortQp1, q1); + members.put(clientPortQp2, q2); + members.put(clientPortQp3, q3); for (MainThread member : members.values()) { member.start(); @@ -351,7 +413,6 @@ public void testRollingUpgrade() throws Exception { stopAppendConfigRestartAll(members, PORT_UNIFICATION_ENABLED); stopAppendConfigRestartAll(members, SSL_QUORUM_ENABLED); - // TODO: Is appending a new line to config a fair assumption stopAppendConfigRestartAll(members, PORT_UNIFICATION_DISABLED); } @@ -376,10 +437,8 @@ private void stopAppendConfigRestartAll(Map members, String @Test public void testHostnameVerification() throws Exception { - setSSLSystemProperties(); - - MainThread q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED); - MainThread q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED); + q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED); + q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED); q1.start(); q2.start(); @@ -387,10 +446,15 @@ public void testHostnameVerification() throws Exception { Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT)); + String badhostnameKeystorePath = tmpDir + "/badhost.jks"; + KeyPair badHostnameKeyPair = createKeyPair(); + X509Certificate badHostCert = buildEndEntityCert(badHostnameKeyPair, rootCertificate, rootKeyPair.getPrivate(), "bleepbloop", null, null); + writeKeystore(badHostCert, badHostnameKeyPair, badhostnameKeystorePath); + System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), badhostnameKeystorePath); // This server should join successfully - MainThread q3 = new MainThread(3, clientPortQp3, quorumConfiguration, SSL_QUORUM_ENABLED); + q3 = new MainThread(3, clientPortQp3, quorumConfiguration, SSL_QUORUM_ENABLED); q3.start(); Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); @@ -421,14 +485,9 @@ public void testHostnameVerification() throws Exception { @Test - public void testCertificateRevocabtionList() throws Exception { - System.setProperty("javax.net.debug", "all"); - System.setProperty("java.security.auth.debug", "all"); - System.setProperty("java.security.debug", "all"); - setSSLSystemProperties(); - - MainThread q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED); - MainThread q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED); + 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(); @@ -436,10 +495,17 @@ public void testCertificateRevocabtionList() throws Exception { 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(), revokedKeystorePath); + String revokedInCRLKeystorePath = tmpDir + "/crl_revoked.jks"; + String crlPath = tmpDir + "/crl.pem"; + KeyPair keyPair = createKeyPair(); + X509Certificate revokedInCRLCert = buildEndEntityCert(keyPair, rootCertificate, rootKeyPair.getPrivate(), HOSTNAME, crlPath, null); + writeKeystore(revokedInCRLCert, keyPair, revokedInCRLKeystorePath); + buildCRL(revokedInCRLCert, crlPath); + + System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), revokedInCRLKeystorePath); // This server should join successfully - MainThread q3 = new MainThread(3, clientPortQp3, quorumConfiguration, SSL_QUORUM_ENABLED); + q3 = new MainThread(3, clientPortQp3, quorumConfiguration, SSL_QUORUM_ENABLED); q3.start(); Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); @@ -455,8 +521,10 @@ public void testCertificateRevocabtionList() throws Exception { setSSLSystemProperties(); System.setProperty(quorumX509Util.getSslCrlEnabledProperty(), "true"); - System.setProperty("com.sun.net.ssl.checkRevocation", "true"); - System.setProperty("com.sun.security.enableCRLDP", "true"); + + KeyPair defaultKeyPair = createKeyPair(); + X509Certificate validCertificate = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), HOSTNAME, crlPath, null); + writeKeystore(validCertificate, defaultKeyPair, validKeystorePath); q1.start(); q2.start(); @@ -464,15 +532,70 @@ public void testCertificateRevocabtionList() throws Exception { 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(), revokedKeystorePath); + 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"; + KeyPair keyPair = createKeyPair(); + X509Certificate revokedInOCSPCert = buildEndEntityCert(keyPair, rootCertificate, rootKeyPair.getPrivate(), HOSTNAME, null, ocspPort); + writeKeystore(revokedInOCSPCert, keyPair, 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)); - // ocsp + 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"); + + KeyPair defaultKeyPair = createKeyPair(); + X509Certificate validCertificate = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), HOSTNAME, 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); + } + } } From 3983a66199da294a491b20f55b1ae23278a27925 Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Mon, 3 Apr 2017 20:28:11 -0700 Subject: [PATCH 09/25] support passwordless truststores fix Zab1_0Test --- .../org/apache/zookeeper/common/X509Util.java | 18 +++++++----------- .../zookeeper/server/quorum/Zab1_0Test.java | 4 ++-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/java/main/org/apache/zookeeper/common/X509Util.java b/src/java/main/org/apache/zookeeper/common/X509Util.java index d893bcc4ea8..875c2476091 100644 --- a/src/java/main/org/apache/zookeeper/common/X509Util.java +++ b/src/java/main/org/apache/zookeeper/common/X509Util.java @@ -23,7 +23,6 @@ import org.slf4j.LoggerFactory; import javax.net.ssl.CertPathTrustManagerParameters; -import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; @@ -124,13 +123,13 @@ public SSLContext createSSLContext(ZKConfig config) throws SSLContextException { // But if a user wanna specify one, location and password are required. if (keyStoreLocationProp == null && keyStorePasswordProp == null) { - LOG.warn("keystore not specified for client connection"); + LOG.warn(getSslKeystoreLocationProperty() + " not specified"); } else { if (keyStoreLocationProp == null) { - throw new SSLContextException("keystore location not specified for client connection"); + throw new SSLContextException(getSslKeystoreLocationProperty() + " not specified"); } if (keyStorePasswordProp == null) { - throw new SSLContextException("keystore password not specified for client connection"); + throw new SSLContextException(getSslKeystorePasswdProperty() + " not specified"); } try { keyManagers = new KeyManager[]{ @@ -147,21 +146,18 @@ public SSLContext createSSLContext(ZKConfig config) throws SSLContextException { boolean sslOcspEnabled = config.getBoolean(this.sslOcspEnabledProperty); boolean sslServerHostnameVerificationEnabled = config.getBoolean(this.getSslHostnameVerificationEnabledProperty()); - if (trustStoreLocationProp == null && trustStorePasswordProp == null) { - LOG.warn("keystore not specified for client connection"); + if (trustStoreLocationProp == null) { + LOG.warn(getSslTruststoreLocationProperty() + " not specified"); } else { if (trustStoreLocationProp == null) { - throw new SSLContextException("keystore location not specified for client connection"); - } - if (trustStorePasswordProp == null) { - throw new SSLContextException("keystore password not specified for client connection"); + throw new SSLContextException(getSslTruststoreLocationProperty() + " not specified for client connection"); } try { trustManagers = new TrustManager[]{ createTrustManager(trustStoreLocationProp, trustStorePasswordProp, sslCrlEnabled, sslOcspEnabled, sslServerHostnameVerificationEnabled, shouldVerifyClientHostname())}; } catch (TrustManagerException e) { - throw new SSLContextException("Failed to create KeyManager", e); + throw new SSLContextException("Failed to create TrustManager", e); } } diff --git a/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java b/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java index 2a0c04a9227..3919e454271 100644 --- a/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java +++ b/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java @@ -1274,7 +1274,7 @@ static class ConversableFollower extends Follower { QuorumServer leader; public void setLeaderSocketAddress(InetSocketAddress addr) { - leader.addr = addr; + leader = new QuorumServer(0, addr); } @Override @@ -1300,7 +1300,7 @@ static class ConversableObserver extends Observer { QuorumServer leader; public void setLeaderSocketAddress(InetSocketAddress addr) { - leader.addr = addr; + leader = new QuorumServer(0, addr); } @Override From ae165f63204a1af1b979037b3692bfc25d2f09c0 Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Tue, 4 Apr 2017 15:31:56 -0700 Subject: [PATCH 10/25] UnifiedServerSocketTests and X509Util tests --- .../org/apache/zookeeper/common/X509Util.java | 54 +++- .../apache/zookeeper/common/X509UtilTest.java | 231 ++++++++++++++++++ .../quorum/UnifiedServerSocketTest.java | 133 ++++++++++ .../apache/zookeeper/test/QuorumSSLTest.java | 2 + 4 files changed, 413 insertions(+), 7 deletions(-) create mode 100644 src/java/main/org/apache/zookeeper/common/X509UtilTest.java create mode 100644 src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java diff --git a/src/java/main/org/apache/zookeeper/common/X509Util.java b/src/java/main/org/apache/zookeeper/common/X509Util.java index 875c2476091..ce272e0d6a0 100644 --- a/src/java/main/org/apache/zookeeper/common/X509Util.java +++ b/src/java/main/org/apache/zookeeper/common/X509Util.java @@ -40,7 +40,9 @@ import java.io.FileInputStream; import java.io.IOException; import java.net.Socket; +import java.security.KeyManagementException; import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; import java.security.Security; import java.security.cert.CertificateException; import java.security.cert.PKIXBuilderParameters; @@ -55,6 +57,10 @@ public abstract class X509Util { private static final Logger LOG = LoggerFactory.getLogger(X509Util.class); + public static final String DEFAULT_PROTOCOL = "TLSv1"; + + private String sslProtocolProperty = getConfigPrefix() + "protocol"; + private String cipherSuitesProperty = getConfigPrefix() + "ciphersuites"; private String sslKeystoreLocationProperty = getConfigPrefix() + "keyStore.location"; private String sslKeystorePasswdProperty = getConfigPrefix() + "keyStore.password"; private String sslTruststoreLocationProperty = getConfigPrefix() + "trustStore.location"; @@ -63,11 +69,30 @@ public abstract class X509Util { private String sslCrlEnabledProperty = getConfigPrefix() + "ssl.crl"; private String sslOcspEnabledProperty = getConfigPrefix() + "ssl.ocsp"; + private String[] cipherSuites; + private volatile SSLContext defaultSSLContext; + public X509Util() { + String cipherSuitesInput = System.getProperty(cipherSuitesProperty); + if (cipherSuitesInput == null) { + cipherSuites = null; + } else { + cipherSuites = cipherSuitesInput.split(","); + } + } + protected abstract String getConfigPrefix(); protected abstract boolean shouldVerifyClientHostname(); + public String getSslProtocolProperty() { + return sslProtocolProperty; + } + + public String getCipherSuitesProperty() { + return cipherSuitesProperty; + } + public String getSslKeystoreLocationProperty() { return sslKeystoreLocationProperty; } @@ -161,14 +186,14 @@ public SSLContext createSSLContext(ZKConfig config) throws SSLContextException { } } - SSLContext sslContext = null; + String protocol = System.getProperty(sslProtocolProperty, DEFAULT_PROTOCOL); try { - sslContext = SSLContext.getInstance("TLSv1"); + SSLContext sslContext = SSLContext.getInstance(protocol); sslContext.init(keyManagers, trustManagers, null); - } catch (Exception e) { + return sslContext; + } catch (NoSuchAlgorithmException|KeyManagementException e) { throw new SSLContextException(e); } - return sslContext; } public static X509KeyManager createKeyManager(String keyStoreLocation, String keyStorePassword) @@ -180,7 +205,7 @@ public static X509KeyManager createKeyManager(String keyStoreLocation, String ke KeyStore ks = KeyStore.getInstance("JKS"); inputStream = new FileInputStream(keyStoreFile); ks.load(inputStream, keyStorePasswordChars); - KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX"); kmf.init(ks, keyStorePasswordChars); for (KeyManager km : kmf.getKeyManagers()) { @@ -208,11 +233,15 @@ public static X509TrustManager createTrustManager(String trustStoreLocation, Str throws TrustManagerException { FileInputStream inputStream = null; try { - char[] trustStorePasswordChars = trustStorePassword.toCharArray(); File trustStoreFile = new File(trustStoreLocation); KeyStore ts = KeyStore.getInstance("JKS"); inputStream = new FileInputStream(trustStoreFile); - ts.load(inputStream, trustStorePasswordChars); + if (trustStorePassword != null) { + char[] trustStorePasswordChars = trustStorePassword.toCharArray(); + ts.load(inputStream, trustStorePasswordChars); + } else { + ts.load(inputStream, null); + } PKIXBuilderParameters pbParams = new PKIXBuilderParameters(ts, new X509CertSelector()); if (crlEnabled || ocspEnabled) { @@ -309,6 +338,9 @@ public SSLSocket createSSLSocket() throws X509Exception, IOException { SSLSocket sslSocket = (SSLSocket) getDefaultSSLContext().getSocketFactory().createSocket(); SSLParameters sslParameters = sslSocket.getSSLParameters(); sslParameters.setNeedClientAuth(true); + if (cipherSuites != null) { + sslParameters.setCipherSuites(cipherSuites); + } sslSocket.setSSLParameters(sslParameters); @@ -320,7 +352,12 @@ public SSLServerSocket createSSLServerSocket() throws X509Exception, IOException SSLServerSocket sslServerSocket = (SSLServerSocket) getDefaultSSLContext().getServerSocketFactory().createServerSocket(); SSLParameters sslParameters = sslServerSocket.getSSLParameters(); sslParameters.setNeedClientAuth(true); + if (cipherSuites != null) { + sslParameters.setCipherSuites(cipherSuites); + } + sslServerSocket.setSSLParameters(sslParameters); + return sslServerSocket; } @@ -328,6 +365,9 @@ public SSLServerSocket createSSLServerSocket(int port) throws X509Exception, IOE SSLServerSocket sslServerSocket = (SSLServerSocket) getDefaultSSLContext().getServerSocketFactory().createServerSocket(port); SSLParameters sslParameters = sslServerSocket.getSSLParameters(); sslParameters.setNeedClientAuth(true); + if (cipherSuites != null) { + sslParameters.setCipherSuites(cipherSuites); + } sslServerSocket.setSSLParameters(sslParameters); diff --git a/src/java/main/org/apache/zookeeper/common/X509UtilTest.java b/src/java/main/org/apache/zookeeper/common/X509UtilTest.java new file mode 100644 index 00000000000..50d5fff1ba5 --- /dev/null +++ b/src/java/main/org/apache/zookeeper/common/X509UtilTest.java @@ -0,0 +1,231 @@ +/** + * 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.common; + +import org.apache.zookeeper.ZKTestCase; +import org.apache.zookeeper.client.ZKClientConfig; +import org.apache.zookeeper.server.ServerCnxnFactory; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import java.io.FileOutputStream; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.Date; +import java.util.Random; + +import static org.apache.zookeeper.test.ClientBase.createTmpDir; + +public class X509UtilTest extends ZKTestCase { + + private static final char[] PASSWORD = "password".toCharArray(); + private X509Certificate rootCertificate; + + private String truststorePath; + private String keystorePath; + private static KeyPair rootKeyPair; + + private X509Util x509Util; + + @BeforeClass + public static void createKeyPair() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); + keyPairGenerator.initialize(4096); + rootKeyPair = keyPairGenerator.genKeyPair(); + } + + @AfterClass + public static void removeBouncyCastleProvider() throws Exception { + Security.removeProvider("BC"); + } + + @Before + public void setUp() throws Exception { + rootCertificate = createSelfSignedCertifcate(rootKeyPair); + + String tmpDir = createTmpDir().getAbsolutePath(); + truststorePath = tmpDir + "/truststore.jks"; + keystorePath = tmpDir + "/keystore.jks"; + + x509Util = new ClientX509Util(); + + writeKeystore(rootCertificate, rootKeyPair, keystorePath); + + 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(x509Util.getSslKeystoreLocationProperty(), keystorePath); + System.setProperty(x509Util.getSslKeystorePasswdProperty(), new String(PASSWORD)); + System.setProperty(x509Util.getSslTruststoreLocationProperty(), truststorePath); + System.setProperty(x509Util.getSslTruststorePasswdProperty(), new String(PASSWORD)); + System.setProperty(x509Util.getSslHostnameVerificationEnabledProperty(), "false"); + + writeTrustStore(PASSWORD); + } + + private void writeKeystore(X509Certificate certificate, KeyPair keyPair, String path) throws Exception { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, PASSWORD); + keyStore.setKeyEntry("alias", keyPair.getPrivate(), PASSWORD, new Certificate[] { certificate }); + FileOutputStream outputStream = new FileOutputStream(path); + keyStore.store(outputStream, PASSWORD); + outputStream.flush(); + outputStream.close(); + } + + private void writeTrustStore(char[] password) throws Exception { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, password); + trustStore.setCertificateEntry(rootCertificate.getSubjectDN().toString(), rootCertificate); + FileOutputStream outputStream = new FileOutputStream(truststorePath); + if (password == null) { + trustStore.store(outputStream, new char[0]); + } else { + trustStore.store(outputStream, password); + } + outputStream.flush(); + outputStream.close(); + } + + private X509Certificate createSelfSignedCertifcate(KeyPair keyPair) throws Exception { + X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); + nameBuilder.addRDN(BCStyle.CN, "localhost"); + 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)); + + ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.getPrivate()); + + return new JcaX509CertificateConverter().getCertificate(certificateBuilder.build(contentSigner)); + } + + @After + public void cleanUp() throws Exception { + System.clearProperty(x509Util.getSslKeystoreLocationProperty()); + System.clearProperty(x509Util.getSslKeystorePasswdProperty()); + System.clearProperty(x509Util.getSslTruststoreLocationProperty()); + System.clearProperty(x509Util.getSslTruststorePasswdProperty()); + System.clearProperty(x509Util.getSslHostnameVerificationEnabledProperty()); + System.clearProperty(x509Util.getSslOcspEnabledProperty()); + System.clearProperty(x509Util.getSslCrlEnabledProperty()); + System.clearProperty("com.sun.net.ssl.checkRevocation"); + System.clearProperty("com.sun.security.enableCRLDP"); + Security.setProperty("com.sun.security.enableCRLDP", "false"); + } + + @Test + public void testCreateSSLContextWithoutCustomProtocol() throws Exception { + SSLContext sslContext = x509Util.getDefaultSSLContext(); + Assert.assertEquals(X509Util.DEFAULT_PROTOCOL, sslContext.getProtocol()); + } + + @Test + public void testCreateSSLContextWithCustomProtocol() throws Exception { + final String protocol = "TLSv1.1"; + System.setProperty(x509Util.getSslProtocolProperty(), protocol); + SSLContext sslContext = x509Util.getDefaultSSLContext(); + Assert.assertEquals(protocol, sslContext.getProtocol()); + } + + @Test + public void testCreateSSLContextWithoutTrustStorePassword() throws Exception { + writeTrustStore(null); + System.clearProperty(x509Util.getSslTruststorePasswdProperty()); + x509Util.getDefaultSSLContext(); + } + + @Test(expected = X509Exception.SSLContextException.class) + public void testCreateSSLContextWithoutKeyStoreLocation() throws Exception { + System.clearProperty(x509Util.getSslKeystoreLocationProperty()); + x509Util.getDefaultSSLContext(); + } + + @Test(expected = X509Exception.SSLContextException.class) + public void testCreateSSLContextWithoutKeyStorePassword() throws Exception { + System.clearProperty(x509Util.getSslKeystorePasswdProperty()); + x509Util.getDefaultSSLContext(); + } + + @Test + public void testCreateSSLContextWithCustomCipherSuites() throws Exception { + String[] cipherSuites = {"SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA"}; + System.setProperty(x509Util.getCipherSuitesProperty(), cipherSuites[0] + "," + cipherSuites[1]); + x509Util = new ClientX509Util(); + SSLSocket sslSocket = x509Util.createSSLSocket(); + Assert.assertArrayEquals(cipherSuites, sslSocket.getEnabledCipherSuites()); + } + + // It would be great to test the value of PKIXBuilderParameters#setRevocationEnabled but it does not appear to be + // possible + @Test + public void testCRLEnabled() throws Exception { + System.setProperty(x509Util.getSslCrlEnabledProperty(), "true"); + x509Util.getDefaultSSLContext(); + Assert.assertTrue(Boolean.valueOf(System.getProperty("com.sun.net.ssl.checkRevocation"))); + Assert.assertTrue(Boolean.valueOf(System.getProperty("com.sun.security.enableCRLDP"))); + Assert.assertFalse(Boolean.valueOf(Security.getProperty("ocsp.enable"))); + } + + @Test + public void testCRLDisabled() throws Exception { + x509Util.getDefaultSSLContext(); + Assert.assertFalse(Boolean.valueOf(System.getProperty("com.sun.net.ssl.checkRevocation"))); + Assert.assertFalse(Boolean.valueOf(System.getProperty("com.sun.security.enableCRLDP"))); + Assert.assertFalse(Boolean.valueOf(Security.getProperty("ocsp.enable"))); + } + + @Test + public void testOCSPEnabled() throws Exception { + System.setProperty(x509Util.getSslOcspEnabledProperty(), "true"); + x509Util.getDefaultSSLContext(); + Assert.assertTrue(Boolean.valueOf(System.getProperty("com.sun.net.ssl.checkRevocation"))); + Assert.assertTrue(Boolean.valueOf(System.getProperty("com.sun.security.enableCRLDP"))); + Assert.assertTrue(Boolean.valueOf(Security.getProperty("ocsp.enable"))); + } +} diff --git a/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java b/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java new file mode 100644 index 00000000000..74886091858 --- /dev/null +++ b/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java @@ -0,0 +1,133 @@ +/** + * 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 org.apache.zookeeper.PortAssignment; +import org.apache.zookeeper.client.ZKClientConfig; +import org.apache.zookeeper.common.ClientX509Util; +import org.apache.zookeeper.common.Time; +import org.apache.zookeeper.common.X509Util; +import org.apache.zookeeper.server.ServerCnxnFactory; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import javax.net.ssl.HandshakeCompletedEvent; +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.SSLSocket; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; + +public class UnifiedServerSocketTest { + + private X509Util x509Util; + private int port; + private volatile boolean handshakeCompleted; + + @Before + public void setUp() throws Exception { + handshakeCompleted = false; + + port = PortAssignment.unique(); + + String testDataPath = System.getProperty("test.data.dir", "build/test/data"); + System.setProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY, "org.apache.zookeeper.server.NettyServerCnxnFactory"); + System.setProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET, "org.apache.zookeeper.ClientCnxnSocketNetty"); + System.setProperty(ZKClientConfig.SECURE_CLIENT, "true"); + + x509Util = new ClientX509Util(); + + System.setProperty(x509Util.getSslKeystoreLocationProperty(), testDataPath + "/ssl/testKeyStore.jks"); + System.setProperty(x509Util.getSslKeystorePasswdProperty(), "testpass"); + System.setProperty(x509Util.getSslTruststoreLocationProperty(), testDataPath + "/ssl/testTrustStore.jks"); + System.setProperty(x509Util.getSslTruststorePasswdProperty(), "testpass"); + } + + @Test + public void testConnectWithSSL() throws Exception { + class ServerThread extends Thread { + public void run() { + try { + Socket unifiedSocket = new UnifiedServerSocket(x509Util, port).accept(); + ((SSLSocket)unifiedSocket).getSession(); // block until handshake completes + } catch (IOException e) { + e.printStackTrace(); + } + } + } + ServerThread serverThread = new ServerThread(); + serverThread.start(); + + SSLSocket sslSocket = x509Util.createSSLSocket(); + sslSocket.connect(new InetSocketAddress(port), 1000); + sslSocket.addHandshakeCompletedListener(new HandshakeCompletedListener() { + @Override + public void handshakeCompleted(HandshakeCompletedEvent handshakeCompletedEvent) { + completeHandshake(); + } + }); + sslSocket.startHandshake(); + + serverThread.join(1000); + + long start = Time.currentElapsedTime(); + while (Time.currentElapsedTime() < start + 10000) { + if (handshakeCompleted) { + return; + } + } + + Assert.fail("failed to complete handshake"); + } + + private void completeHandshake() { + handshakeCompleted = true; + } + + @Test + public void testConnectWithoutSSL() throws Exception { + final byte[] testData = "hello there".getBytes(); + + class ServerThread extends Thread { + public void run() { + try { + Socket unifiedSocket = new UnifiedServerSocket(x509Util, port).accept(); + unifiedSocket.getOutputStream().write(testData); + unifiedSocket.getOutputStream().flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + ServerThread serverThread = new ServerThread(); + serverThread.start(); + + Socket socket = new Socket(); + socket.connect(new InetSocketAddress(port), 1000); + socket.getOutputStream().write("hello".getBytes()); + socket.getOutputStream().flush(); + + byte[] readBytes = new byte[testData.length]; + socket.getInputStream().read(readBytes, 0, testData.length); + + serverThread.join(1000); + + Assert.assertArrayEquals(testData, readBytes); + } +} diff --git a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java index f00fc29ec8e..b6509aad7f1 100644 --- a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java +++ b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java @@ -354,6 +354,8 @@ public void cleanUp() throws Exception { q1.shutdown(); q2.shutdown(); q3.shutdown(); + + Security.removeProvider("BC"); } private void clearSSLSystemProperties() { From 0396ac3403ccce8d04ece2be9962f8d102e936cb Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Wed, 5 Apr 2017 19:27:07 -0700 Subject: [PATCH 11/25] move X509UtilTest to the proper directory unneeded remove QuorumServer changes --- .../zookeeper/server/quorum/Follower.java | 4 ++-- .../zookeeper/server/quorum/Learner.java | 23 +++++++++++-------- .../zookeeper/server/quorum/Observer.java | 6 ++--- .../zookeeper/server/quorum/QuorumPeer.java | 13 ++++------- .../apache/zookeeper/common/X509UtilTest.java | 0 .../zookeeper/server/quorum/LearnerTest.java | 4 ++-- .../zookeeper/server/quorum/Zab1_0Test.java | 16 ++++++------- 7 files changed, 33 insertions(+), 33 deletions(-) rename src/java/{main => test}/org/apache/zookeeper/common/X509UtilTest.java (100%) diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Follower.java b/src/java/main/org/apache/zookeeper/server/quorum/Follower.java index f1fc646e87e..fa242f03cf3 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Follower.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Follower.java @@ -71,9 +71,9 @@ void followLeader() throws InterruptedException { self.end_fle = 0; fzk.registerJMX(new FollowerBean(this, zk), self.jmxLocalPeerBean); try { - QuorumPeer.QuorumServer leader = findLeader(); + InetSocketAddress addr = findLeader(); try { - connectToLeader(leader); + connectToLeader(addr); long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO); if (self.isReconfigStateChange()) throw new Exception("learned about role change"); diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Learner.java b/src/java/main/org/apache/zookeeper/server/quorum/Learner.java index 78dc58f6715..74b93261cb5 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Learner.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Learner.java @@ -198,18 +198,21 @@ void request(Request request) throws IOException { /** * Returns the address of the node we think is the leader. */ - protected QuorumServer findLeader() { + protected InetSocketAddress findLeader() { InetSocketAddress addr = null; // Find the leader by id Vote current = self.getCurrentVote(); for (QuorumServer s : self.getView().values()) { if (s.id == current.getId()) { - return s; + addr = s.addr; + break; } } - LOG.warn("Couldn't find the leader with id = " + if (addr == null) { + LOG.warn("Couldn't find the leader with id = " + current.getId()); - return null; + } + return addr; } /** @@ -232,12 +235,12 @@ protected void sockConnect(Socket sock, InetSocketAddress addr, int timeout) /** * Establish a connection with the Leader found by findLeader. Retries * until either initLimit time has elapsed or 5 tries have happened. - * @param leader - the QuorumServer elected leader + * @param addr - the address of the Leader to connect to. * @throws IOException - if the socket connection fails on the 5th attempt * @throws ConnectException * @throws InterruptedException */ - protected void connectToLeader(QuorumServer leader) + protected void connectToLeader(InetSocketAddress addr) throws IOException, InterruptedException, X509Exception { QuorumX509Util quorumX509Util = new QuorumX509Util(); createSocket(quorumX509Util); @@ -255,7 +258,7 @@ protected void connectToLeader(QuorumServer leader) throw new IOException("initLimit exceeded on retries."); } - sockConnect(sock, leader.addr, Math.min(self.tickTime * self.syncLimit, remainingInitLimitTime)); + sockConnect(sock, addr, Math.min(self.tickTime * self.syncLimit, remainingInitLimitTime)); if (self.isSslQuorum()) { ((SSLSocket) sock).startHandshake(); } @@ -267,17 +270,17 @@ protected void connectToLeader(QuorumServer leader) if (remainingInitLimitTime <= 1000) { LOG.error("Unexpected exception, initLimit exceeded. tries=" + tries + ", remaining init limit=" + remainingInitLimitTime + - ", connecting to " + leader.addr,e); + ", connecting to " + addr,e); throw e; } else if (tries >= 4) { LOG.error("Unexpected exception, retries exceeded. tries=" + tries + ", remaining init limit=" + remainingInitLimitTime + - ", connecting to " + leader.addr,e); + ", connecting to " + addr,e); throw e; } else { LOG.warn("Unexpected exception, tries=" + tries + ", remaining init limit=" + remainingInitLimitTime + - ", connecting to " + leader.addr,e); + ", connecting to " + addr,e); createSocket(quorumX509Util); } } diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Observer.java b/src/java/main/org/apache/zookeeper/server/quorum/Observer.java index dc048147370..27368c7527f 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Observer.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Observer.java @@ -63,10 +63,10 @@ void observeLeader() throws Exception { zk.registerJMX(new ObserverBean(this, zk), self.jmxLocalPeerBean); try { - QuorumPeer.QuorumServer leader = findLeader(); - LOG.info("Observing " + leader.addr); + InetSocketAddress addr = findLeader(); + LOG.info("Observing " + addr); try { - connectToLeader(leader); + connectToLeader(addr); long newLeaderZxid = registerWithLeader(Leader.OBSERVERINFO); if (self.isReconfigStateChange()) throw new Exception("learned about role change"); diff --git a/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeer.java b/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeer.java index bda5d381953..02cc266e934 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeer.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/QuorumPeer.java @@ -111,8 +111,6 @@ public class QuorumPeer extends ZooKeeperThread implements QuorumStats.Provider private ZKDatabase zkDb; public static class QuorumServer { - public String hostname = null; - public InetSocketAddress addr = null; public InetSocketAddress electionAddr = null; @@ -221,7 +219,7 @@ public QuorumServer(long sid, String addressStr) throws ConfigException { } // is client_config a host:port or just a port - hostname = (clientParts.length == 2) ? clientParts[0] : "0.0.0.0"; + String hostname = (clientParts.length == 2) ? clientParts[0] : "0.0.0.0"; try { clientAddr = new InetSocketAddress(hostname, Integer.parseInt(clientParts[clientParts.length - 1])); @@ -232,18 +230,17 @@ public QuorumServer(long sid, String addressStr) throws ConfigException { } // server_config should be either host:port:port or host:port:port:type - hostname = serverParts[0]; try { - addr = new InetSocketAddress(hostname, + addr = new InetSocketAddress(serverParts[0], Integer.parseInt(serverParts[1])); } catch (NumberFormatException e) { - throw new ConfigException("Address unresolved: " + hostname + ":" + serverParts[1]); + throw new ConfigException("Address unresolved: " + serverParts[0] + ":" + serverParts[1]); } try { - electionAddr = new InetSocketAddress(hostname, + electionAddr = new InetSocketAddress(serverParts[0], Integer.parseInt(serverParts[2])); } catch (NumberFormatException e) { - throw new ConfigException("Address unresolved: " + hostname + ":" + serverParts[2]); + throw new ConfigException("Address unresolved: " + serverParts[0] + ":" + serverParts[2]); } if (serverParts.length == 4) { diff --git a/src/java/main/org/apache/zookeeper/common/X509UtilTest.java b/src/java/test/org/apache/zookeeper/common/X509UtilTest.java similarity index 100% rename from src/java/main/org/apache/zookeeper/common/X509UtilTest.java rename to src/java/test/org/apache/zookeeper/common/X509UtilTest.java diff --git a/src/java/test/org/apache/zookeeper/server/quorum/LearnerTest.java b/src/java/test/org/apache/zookeeper/server/quorum/LearnerTest.java index 4b085ebfb16..85284f6d42d 100644 --- a/src/java/test/org/apache/zookeeper/server/quorum/LearnerTest.java +++ b/src/java/test/org/apache/zookeeper/server/quorum/LearnerTest.java @@ -110,7 +110,7 @@ public void connectionRetryTimeoutTest() throws Exception { InetSocketAddress addr = new InetSocketAddress(1111); // we expect this to throw an IOException since we're faking socket connect errors every time - learner.connectToLeader(new QuorumPeer.QuorumServer(0, addr)); + learner.connectToLeader(addr); } @Test public void connectionInitLimitTimeoutTest() throws Exception { @@ -130,7 +130,7 @@ public void connectionInitLimitTimeoutTest() throws Exception { // we expect this to throw an IOException since we're faking socket connect errors every time try { - learner.connectToLeader(new QuorumPeer.QuorumServer(0, addr)); + learner.connectToLeader(addr); Assert.fail("should have thrown IOException!"); } catch (IOException e) { //good, wanted to see that, let's make sure we ran out of time diff --git a/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java b/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java index 3919e454271..51ed5298a3f 100644 --- a/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java +++ b/src/java/test/org/apache/zookeeper/server/quorum/Zab1_0Test.java @@ -1272,14 +1272,14 @@ static class ConversableFollower extends Follower { super(self, zk); } - QuorumServer leader; + InetSocketAddress leaderAddr; public void setLeaderSocketAddress(InetSocketAddress addr) { - leader = new QuorumServer(0, addr); + leaderAddr = addr; } @Override - protected QuorumServer findLeader() { - return leader; + protected InetSocketAddress findLeader() { + return leaderAddr; } } private ConversableFollower createFollower(File tmpDir, QuorumPeer peer) @@ -1298,14 +1298,14 @@ static class ConversableObserver extends Observer { super(self, zk); } - QuorumServer leader; + InetSocketAddress leaderAddr; public void setLeaderSocketAddress(InetSocketAddress addr) { - leader = new QuorumServer(0, addr); + leaderAddr = addr; } @Override - protected QuorumServer findLeader() { - return leader; + protected InetSocketAddress findLeader() { + return leaderAddr; } } From 735b58b131ad65cf661c69375ce5fb39a1470ab2 Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Wed, 5 Apr 2017 19:53:37 -0700 Subject: [PATCH 12/25] remove unnecessary instantiation of X509Util in the learner --- .../org/apache/zookeeper/server/quorum/Leader.java | 2 -- .../org/apache/zookeeper/server/quorum/Learner.java | 12 ++++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Leader.java b/src/java/main/org/apache/zookeeper/server/quorum/Leader.java index f1a9c0b40b9..ac1e04b87c4 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Leader.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Leader.java @@ -43,7 +43,6 @@ import org.apache.zookeeper.common.QuorumX509Util; import org.apache.zookeeper.common.Time; import org.apache.zookeeper.common.X509Exception; -import org.apache.zookeeper.common.X509Util; import org.apache.zookeeper.server.FinalRequestProcessor; import org.apache.zookeeper.server.Request; import org.apache.zookeeper.server.RequestProcessor; @@ -223,7 +222,6 @@ public boolean isQuorumSynced(QuorumVerifier qv) { this.self = self; try { if (self.shouldUsePortUnification()) { - if (self.getQuorumListenOnAllIPs()) { ss = new UnifiedServerSocket(new QuorumX509Util(), self.getQuorumAddress().getPort()); } else { diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Learner.java b/src/java/main/org/apache/zookeeper/server/quorum/Learner.java index 74b93261cb5..03c3e321f19 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Learner.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Learner.java @@ -72,6 +72,8 @@ static class PacketInFlight { protected BufferedOutputStream bufferedOutput; protected Socket sock; + + protected X509Util x509Util; /** * Socket getter @@ -242,8 +244,7 @@ protected void sockConnect(Socket sock, InetSocketAddress addr, int timeout) */ protected void connectToLeader(InetSocketAddress addr) throws IOException, InterruptedException, X509Exception { - QuorumX509Util quorumX509Util = new QuorumX509Util(); - createSocket(quorumX509Util); + createSocket(); int initLimitTime = self.tickTime * self.initLimit; int remainingInitLimitTime = initLimitTime; @@ -281,7 +282,7 @@ protected void connectToLeader(InetSocketAddress addr) LOG.warn("Unexpected exception, tries=" + tries + ", remaining init limit=" + remainingInitLimitTime + ", connecting to " + addr,e); - createSocket(quorumX509Util); + createSocket(); } } Thread.sleep(1000); @@ -292,8 +293,11 @@ protected void connectToLeader(InetSocketAddress addr) leaderOs = BinaryOutputArchive.getArchive(bufferedOutput); } - private void createSocket(X509Util x509Util) throws X509Exception, IOException { + private void createSocket() throws X509Exception, IOException { if (self.isSslQuorum()) { + if (x509Util == null) { + x509Util = new QuorumX509Util(); + } sock = x509Util.createSSLSocket(); } else { sock = new Socket(); From 6babcd56e81a64266479b887ed20d5aba4ef8f14 Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Thu, 6 Apr 2017 13:41:28 -0700 Subject: [PATCH 13/25] change hostname verification to attempt just using the ip address before trying reverse dns --- .../org/apache/zookeeper/common/X509Util.java | 33 +++-- .../apache/zookeeper/test/QuorumSSLTest.java | 113 ++++++++++++++---- 2 files changed, 114 insertions(+), 32 deletions(-) diff --git a/src/java/main/org/apache/zookeeper/common/X509Util.java b/src/java/main/org/apache/zookeeper/common/X509Util.java index ce272e0d6a0..3c0437a9688 100644 --- a/src/java/main/org/apache/zookeeper/common/X509Util.java +++ b/src/java/main/org/apache/zookeeper/common/X509Util.java @@ -39,7 +39,9 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.net.InetAddress; import java.net.Socket; +import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; @@ -273,7 +275,7 @@ public X509Certificate[] getAcceptedIssuers() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { if (hostnameVerificationEnabled && shouldVerifyClientHostname) { - performHostnameVerification(socket.getInetAddress().getHostName(), chain[0]); + performHostVerification(socket.getInetAddress(), chain[0]); } x509ExtendedTrustManager.checkClientTrusted(chain, authType, socket); } @@ -281,7 +283,7 @@ public void checkClientTrusted(X509Certificate[] chain, String authType, Socket @Override public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { if (hostnameVerificationEnabled) { - performHostnameVerification(socket.getInetAddress().getHostName(), chain[0]); + performHostVerification(socket.getInetAddress(), chain[0]); } x509ExtendedTrustManager.checkServerTrusted(chain, authType, socket); } @@ -289,7 +291,11 @@ public void checkServerTrusted(X509Certificate[] chain, String authType, Socket @Override public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { if (hostnameVerificationEnabled && shouldVerifyClientHostname) { - performHostnameVerification(engine.getPeerHost(), chain[0]); + try { + performHostVerification(InetAddress.getByName(engine.getPeerHost()), chain[0]); + } catch (UnknownHostException e) { + throw new CertificateException("failed to verify host", e); + } } x509ExtendedTrustManager.checkServerTrusted(chain, authType, engine); } @@ -297,7 +303,11 @@ public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngi @Override public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { if (hostnameVerificationEnabled) { - performHostnameVerification(engine.getPeerHost(), chain[0]); + try { + performHostVerification(InetAddress.getByName(engine.getPeerHost()), chain[0]); + } catch (UnknownHostException e) { + throw new CertificateException("failed to verify host", e); + } } x509ExtendedTrustManager.checkServerTrusted(chain, authType, engine); } @@ -312,11 +322,18 @@ public void checkServerTrusted(X509Certificate[] chain, String authType) throws x509ExtendedTrustManager.checkServerTrusted(chain, authType); } - private void performHostnameVerification(String hostname, X509Certificate certificate) throws CertificateException { + private void performHostVerification(InetAddress inetAddress, X509Certificate certificate) throws CertificateException { try { - hostnameVerifier.verify(hostname, certificate); - } catch (SSLException e) { - throw new CertificateException("Failed to verify hostname", e); + hostnameVerifier.verify(inetAddress.getHostAddress(), certificate); + } catch (SSLException addressVerificationException) { + try { + LOG.debug("Failed to verify host address, attempting to verify host name with reverse dns lookup", addressVerificationException); + hostnameVerifier.verify(inetAddress.getHostName(), certificate); + } catch (SSLException hostnameVerificationException) { + LOG.error("Failed to verify host address", addressVerificationException); + LOG.error("Failed to verify hostname", hostnameVerificationException); + throw new CertificateException("Failed to verify both host address and host name", hostnameVerificationException); + } } } }; diff --git a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java index b6509aad7f1..382e1ffe786 100644 --- a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java +++ b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java @@ -98,9 +98,11 @@ 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; @@ -135,6 +137,8 @@ public class QuorumSSLTest extends QuorumPeerTestBase { private KeyPair rootKeyPair; private X509Certificate rootCertificate; + private KeyPair defaultKeyPair; + private ContentSigner contentSigner; @Before @@ -167,8 +171,9 @@ public void setup() throws Exception { outputStream.flush(); outputStream.close(); - KeyPair defaultKeyPair = createKeyPair(); - X509Certificate validCertificate = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), HOSTNAME, null, null); + defaultKeyPair = createKeyPair(); + X509Certificate validCertificate = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), + HOSTNAME, null, null, null); writeKeystore(validCertificate, defaultKeyPair, validKeystorePath); setSSLSystemProperties(); @@ -280,10 +285,18 @@ private void buildCRL(X509Certificate x509Certificate, String crlPath) throws Ex } public X509Certificate buildEndEntityCert(KeyPair keyPair, X509Certificate caCert, PrivateKey caPrivateKey, - String hostname, String crlPath, Integer ocspPort) throws Exception { + String hostname, String ipAddress, String crlPath, Integer ocspPort) throws Exception { X509CertificateHolder holder = new JcaX509CertificateHolder(caCert); ContentSigner signer =new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(caPrivateKey); + List 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())); @@ -294,8 +307,11 @@ public X509Certificate buildEndEntityCert(KeyPair keyPair, X509Certificate caCer .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)) - .addExtension(Extension.subjectAlternativeName, true, new GeneralNames(new GeneralName(GeneralName.dNSName, hostname))); + .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( @@ -438,7 +454,61 @@ private void stopAppendConfigRestartAll(Map members, String } @Test - public void testHostnameVerification() throws Exception { + 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 { q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED); q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED); @@ -448,12 +518,7 @@ public void testHostnameVerification() throws Exception { Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp1, CONNECTION_TIMEOUT)); Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT)); - String badhostnameKeystorePath = tmpDir + "/badhost.jks"; - KeyPair badHostnameKeyPair = createKeyPair(); - X509Certificate badHostCert = buildEndEntityCert(badHostnameKeyPair, rootCertificate, rootKeyPair.getPrivate(), "bleepbloop", null, null); - writeKeystore(badHostCert, badHostnameKeyPair, badhostnameKeystorePath); - - System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), badhostnameKeystorePath); + System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), keystorePath); // This server should join successfully q3 = new MainThread(3, clientPortQp3, quorumConfiguration, SSL_QUORUM_ENABLED); @@ -479,10 +544,10 @@ public void testHostnameVerification() throws Exception { 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(), badhostnameKeystorePath); + System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), keystorePath); q3.start(); - Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); + Assert.assertEquals(expectSuccess, ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); } @@ -499,9 +564,9 @@ public void testCertificateRevocationList() throws Exception { String revokedInCRLKeystorePath = tmpDir + "/crl_revoked.jks"; String crlPath = tmpDir + "/crl.pem"; - KeyPair keyPair = createKeyPair(); - X509Certificate revokedInCRLCert = buildEndEntityCert(keyPair, rootCertificate, rootKeyPair.getPrivate(), HOSTNAME, crlPath, null); - writeKeystore(revokedInCRLCert, keyPair, revokedInCRLKeystorePath); + X509Certificate revokedInCRLCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), + HOSTNAME, null, crlPath, null); + writeKeystore(revokedInCRLCert, defaultKeyPair, revokedInCRLKeystorePath); buildCRL(revokedInCRLCert, crlPath); System.setProperty(quorumX509Util.getSslKeystoreLocationProperty(), revokedInCRLKeystorePath); @@ -524,8 +589,8 @@ public void testCertificateRevocationList() throws Exception { setSSLSystemProperties(); System.setProperty(quorumX509Util.getSslCrlEnabledProperty(), "true"); - KeyPair defaultKeyPair = createKeyPair(); - X509Certificate validCertificate = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), HOSTNAME, crlPath, null); + X509Certificate validCertificate = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), + HOSTNAME, null, crlPath, null); writeKeystore(validCertificate, defaultKeyPair, validKeystorePath); q1.start(); @@ -554,9 +619,9 @@ public void testOCSP() throws Exception { Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp2, CONNECTION_TIMEOUT)); String revokedInOCSPKeystorePath = tmpDir + "/ocsp_revoked.jks"; - KeyPair keyPair = createKeyPair(); - X509Certificate revokedInOCSPCert = buildEndEntityCert(keyPair, rootCertificate, rootKeyPair.getPrivate(), HOSTNAME, null, ocspPort); - writeKeystore(revokedInOCSPCert, keyPair, revokedInOCSPKeystorePath); + X509Certificate revokedInOCSPCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), + HOSTNAME, null,null, ocspPort); + writeKeystore(revokedInOCSPCert, defaultKeyPair, revokedInOCSPKeystorePath); HttpServer ocspServer = HttpServer.create(new InetSocketAddress(ocspPort), 0); try { @@ -582,8 +647,8 @@ public void testOCSP() throws Exception { setSSLSystemProperties(); System.setProperty(quorumX509Util.getSslOcspEnabledProperty(), "true"); - KeyPair defaultKeyPair = createKeyPair(); - X509Certificate validCertificate = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), HOSTNAME, null, ocspPort); + X509Certificate validCertificate = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), + HOSTNAME, null,null, ocspPort); writeKeystore(validCertificate, defaultKeyPair, validKeystorePath); q1.start(); From 513439467230fb48a0ebeaa6fe6b93da2c3daf2c Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Thu, 6 Apr 2017 14:40:20 -0700 Subject: [PATCH 14/25] reduce flakyness in UnifiedServerSocketTest --- .../quorum/UnifiedServerSocketTest.java | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java b/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java index 74886091858..a1e00659dad 100644 --- a/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java +++ b/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java @@ -31,11 +31,15 @@ import javax.net.ssl.HandshakeCompletedListener; import javax.net.ssl.SSLSocket; import java.io.IOException; +import java.net.ConnectException; import java.net.InetSocketAddress; import java.net.Socket; public class UnifiedServerSocketTest { + private static final int MAX_RETRIES = 5; + private static final int TIMEOUT = 1000; + private X509Util x509Util; private int port; private volatile boolean handshakeCompleted; @@ -74,8 +78,21 @@ public void run() { ServerThread serverThread = new ServerThread(); serverThread.start(); - SSLSocket sslSocket = x509Util.createSSLSocket(); - sslSocket.connect(new InetSocketAddress(port), 1000); + SSLSocket sslSocket = null; + int retries = 0; + while (retries < MAX_RETRIES) { + try { + sslSocket = x509Util.createSSLSocket(); + sslSocket.setSoTimeout(TIMEOUT); + sslSocket.connect(new InetSocketAddress(port), TIMEOUT); + break; + } catch (ConnectException connectException) { + connectException.printStackTrace(); + Thread.sleep(TIMEOUT); + } + retries++; + } + sslSocket.addHandshakeCompletedListener(new HandshakeCompletedListener() { @Override public void handshakeCompleted(HandshakeCompletedEvent handshakeCompletedEvent) { @@ -118,8 +135,21 @@ public void run() { ServerThread serverThread = new ServerThread(); serverThread.start(); - Socket socket = new Socket(); - socket.connect(new InetSocketAddress(port), 1000); + Socket socket = null; + int retries = 0; + while (retries < MAX_RETRIES) { + try { + socket = new Socket(); + socket.setSoTimeout(TIMEOUT); + socket.connect(new InetSocketAddress(port), TIMEOUT); + break; + } catch (ConnectException connectException) { + connectException.printStackTrace(); + Thread.sleep(TIMEOUT); + } + retries++; + } + socket.getOutputStream().write("hello".getBytes()); socket.getOutputStream().flush(); From 8fca7d6b9391df672d1679a9dbb9ce6102f2af1f Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Thu, 6 Apr 2017 14:44:32 -0700 Subject: [PATCH 15/25] replace magic numbers UnifiedServerSocketTest --- .../zookeeper/server/quorum/UnifiedServerSocketTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java b/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java index a1e00659dad..d6b69ae7028 100644 --- a/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java +++ b/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java @@ -101,10 +101,10 @@ public void handshakeCompleted(HandshakeCompletedEvent handshakeCompletedEvent) }); sslSocket.startHandshake(); - serverThread.join(1000); + serverThread.join(TIMEOUT); long start = Time.currentElapsedTime(); - while (Time.currentElapsedTime() < start + 10000) { + while (Time.currentElapsedTime() < start + TIMEOUT) { if (handshakeCompleted) { return; } @@ -156,7 +156,7 @@ public void run() { byte[] readBytes = new byte[testData.length]; socket.getInputStream().read(readBytes, 0, testData.length); - serverThread.join(1000); + serverThread.join(TIMEOUT); Assert.assertArrayEquals(testData, readBytes); } From 8fd892d1bbe51c8c50c585c1c9348e6bd5776ac6 Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Fri, 7 Apr 2017 12:45:25 -0700 Subject: [PATCH 16/25] fix findbugs warnings --- .../org/apache/zookeeper/common/X509Util.java | 11 ++- .../apache/zookeeper/server/ServerConfig.java | 2 - .../server/quorum/UnifiedServerSocket.java | 76 +++++++++---------- 3 files changed, 42 insertions(+), 47 deletions(-) diff --git a/src/java/main/org/apache/zookeeper/common/X509Util.java b/src/java/main/org/apache/zookeeper/common/X509Util.java index 3c0437a9688..00a802261db 100644 --- a/src/java/main/org/apache/zookeeper/common/X509Util.java +++ b/src/java/main/org/apache/zookeeper/common/X509Util.java @@ -176,13 +176,10 @@ public SSLContext createSSLContext(ZKConfig config) throws SSLContextException { if (trustStoreLocationProp == null) { LOG.warn(getSslTruststoreLocationProperty() + " not specified"); } else { - if (trustStoreLocationProp == null) { - throw new SSLContextException(getSslTruststoreLocationProperty() + " not specified for client connection"); - } try { - trustManagers = new TrustManager[]{ - createTrustManager(trustStoreLocationProp, trustStorePasswordProp, sslCrlEnabled, sslOcspEnabled, sslServerHostnameVerificationEnabled, shouldVerifyClientHostname())}; + createTrustManager(trustStoreLocationProp, trustStorePasswordProp, sslCrlEnabled, sslOcspEnabled, + sslServerHostnameVerificationEnabled, shouldVerifyClientHostname())}; } catch (TrustManagerException e) { throw new SSLContextException("Failed to create TrustManager", e); } @@ -346,7 +343,9 @@ private void performHostVerification(InetAddress inetAddress, X509Certificate ce if (inputStream != null) { try { inputStream.close(); - } catch (IOException e) {} + } catch (IOException e) { + LOG.info("failed to close TrustStore input stream", e); + } } } } diff --git a/src/java/main/org/apache/zookeeper/server/ServerConfig.java b/src/java/main/org/apache/zookeeper/server/ServerConfig.java index d9fe8840ac7..444e126ae97 100644 --- a/src/java/main/org/apache/zookeeper/server/ServerConfig.java +++ b/src/java/main/org/apache/zookeeper/server/ServerConfig.java @@ -46,7 +46,6 @@ public class ServerConfig { protected int minSessionTimeout = -1; /** defaults to -1 if not set explicitly */ protected int maxSessionTimeout = -1; - protected boolean sslQuorum; /** * Parse arguments for server configuration @@ -98,7 +97,6 @@ public void readFrom(QuorumPeerConfig config) { maxClientCnxns = config.getMaxClientCnxns(); minSessionTimeout = config.getMinSessionTimeout(); maxSessionTimeout = config.getMaxSessionTimeout(); - sslQuorum = config.isSslQuorum(); } public InetSocketAddress getClientPortAddress() { diff --git a/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java b/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java index 8c5599c3203..9765bbd4520 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java @@ -32,52 +32,50 @@ import java.net.SocketException; public class UnifiedServerSocket extends ServerSocket { - private static final Logger LOG = LoggerFactory.getLogger(UnifiedServerSocket.class); + private static final Logger LOG = LoggerFactory.getLogger(UnifiedServerSocket.class); - private X509Util x509Util; + private X509Util x509Util; - public UnifiedServerSocket(X509Util x509Util) throws IOException { - super(); - this.x509Util = x509Util; - } - - public UnifiedServerSocket(X509Util x509Util, int port) throws IOException { - super(port); - this.x509Util = x509Util; - } - - @Override - public Socket accept() throws IOException { - if (isClosed()) { - throw new SocketException("Socket is closed"); + public UnifiedServerSocket(X509Util x509Util) throws IOException { + super(); + this.x509Util = x509Util; } - if (!isBound()) { - throw new SocketException("Socket is not bound yet"); + + public UnifiedServerSocket(X509Util x509Util, int port) throws IOException { + super(port); + this.x509Util = x509Util; } - final Socket bufferedSocket = new BufferedSocket(null); - implAccept(bufferedSocket); - bufferedSocket.getInputStream().mark(6); + @Override + public Socket accept() throws IOException { + if (isClosed()) { + throw new SocketException("Socket is closed"); + } + if (!isBound()) { + throw new SocketException("Socket is not bound yet"); + } + final Socket bufferedSocket = new BufferedSocket(null); + implAccept(bufferedSocket); - byte[] litmus = new byte[5]; - bufferedSocket.getInputStream().read(litmus, 0, 5); + bufferedSocket.getInputStream().mark(6); - bufferedSocket.getInputStream().reset(); + byte[] litmus = new byte[5]; + int bytesRead = bufferedSocket.getInputStream().read(litmus, 0, 5); + bufferedSocket.getInputStream().reset(); - boolean isSsl = SslHandler.isEncrypted(ChannelBuffers.wrappedBuffer(litmus)); - if (isSsl) { - LOG.info(getInetAddress() + " attempting to connect over ssl"); - SSLSocket sslSocket; - try { - sslSocket = (SSLSocket) x509Util.getDefaultSSLContext().getSocketFactory().createSocket(bufferedSocket, null, bufferedSocket.getPort(), false); - } catch (X509Exception.SSLContextException e) { - throw new IOException("failed to create SSL context", e); - } - sslSocket.setUseClientMode(false); - return sslSocket; - } else { - LOG.info(getInetAddress() + " attempting to connect without ssl"); - return bufferedSocket; + if (bytesRead == 5 && SslHandler.isEncrypted(ChannelBuffers.wrappedBuffer(litmus))) { + LOG.info(getInetAddress() + " attempting to connect over ssl"); + SSLSocket sslSocket; + try { + sslSocket = (SSLSocket) x509Util.getDefaultSSLContext().getSocketFactory().createSocket(bufferedSocket, null, bufferedSocket.getPort(), false); + } catch (X509Exception.SSLContextException e) { + throw new IOException("failed to create SSL context", e); + } + sslSocket.setUseClientMode(false); + return sslSocket; + } else { + LOG.info(getInetAddress() + " attempting to connect without ssl"); + return bufferedSocket; + } } - } } \ No newline at end of file From c00956aa2734a2c383961b5b3a6f2fd28f5715e5 Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Fri, 7 Apr 2017 15:10:17 -0700 Subject: [PATCH 17/25] pull out zktrustmanager and add testing --- .../org/apache/zookeeper/common/X509Util.java | 83 +----- .../zookeeper/common/ZKTrustManager.java | 137 ++++++++++ .../apache/zookeeper/common/X509UtilTest.java | 61 ++++- .../zookeeper/common/ZKTrustManagerTest.java | 248 ++++++++++++++++++ .../apache/zookeeper/test/QuorumSSLTest.java | 18 +- 5 files changed, 443 insertions(+), 104 deletions(-) create mode 100644 src/java/main/org/apache/zookeeper/common/ZKTrustManager.java create mode 100644 src/java/test/org/apache/zookeeper/common/ZKTrustManagerTest.java diff --git a/src/java/main/org/apache/zookeeper/common/X509Util.java b/src/java/main/org/apache/zookeeper/common/X509Util.java index 00a802261db..36cb536f1ac 100644 --- a/src/java/main/org/apache/zookeeper/common/X509Util.java +++ b/src/java/main/org/apache/zookeeper/common/X509Util.java @@ -18,7 +18,6 @@ package org.apache.zookeeper.common; -import org.apache.http.conn.ssl.DefaultHostnameVerifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,8 +25,6 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLException; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLSocket; @@ -39,17 +36,12 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.security.Security; -import java.security.cert.CertificateException; import java.security.cert.PKIXBuilderParameters; import java.security.cert.X509CertSelector; -import java.security.cert.X509Certificate; import static org.apache.zookeeper.common.X509Exception.*; @@ -260,80 +252,7 @@ public static X509TrustManager createTrustManager(String trustStoreLocation, Str for (final TrustManager tm : tmf.getTrustManagers()) { if (tm instanceof X509ExtendedTrustManager) { - return new X509ExtendedTrustManager() { - X509ExtendedTrustManager x509ExtendedTrustManager = (X509ExtendedTrustManager) tm; - DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(); - - @Override - public X509Certificate[] getAcceptedIssuers() { - return x509ExtendedTrustManager.getAcceptedIssuers(); - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { - if (hostnameVerificationEnabled && shouldVerifyClientHostname) { - performHostVerification(socket.getInetAddress(), chain[0]); - } - x509ExtendedTrustManager.checkClientTrusted(chain, authType, socket); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { - if (hostnameVerificationEnabled) { - performHostVerification(socket.getInetAddress(), chain[0]); - } - x509ExtendedTrustManager.checkServerTrusted(chain, authType, socket); - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { - if (hostnameVerificationEnabled && shouldVerifyClientHostname) { - try { - performHostVerification(InetAddress.getByName(engine.getPeerHost()), chain[0]); - } catch (UnknownHostException e) { - throw new CertificateException("failed to verify host", e); - } - } - x509ExtendedTrustManager.checkServerTrusted(chain, authType, engine); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { - if (hostnameVerificationEnabled) { - try { - performHostVerification(InetAddress.getByName(engine.getPeerHost()), chain[0]); - } catch (UnknownHostException e) { - throw new CertificateException("failed to verify host", e); - } - } - x509ExtendedTrustManager.checkServerTrusted(chain, authType, engine); - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - x509ExtendedTrustManager.checkClientTrusted(chain, authType); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - x509ExtendedTrustManager.checkServerTrusted(chain, authType); - } - - private void performHostVerification(InetAddress inetAddress, X509Certificate certificate) throws CertificateException { - try { - hostnameVerifier.verify(inetAddress.getHostAddress(), certificate); - } catch (SSLException addressVerificationException) { - try { - LOG.debug("Failed to verify host address, attempting to verify host name with reverse dns lookup", addressVerificationException); - hostnameVerifier.verify(inetAddress.getHostName(), certificate); - } catch (SSLException hostnameVerificationException) { - LOG.error("Failed to verify host address", addressVerificationException); - LOG.error("Failed to verify hostname", hostnameVerificationException); - throw new CertificateException("Failed to verify both host address and host name", hostnameVerificationException); - } - } - } - }; + return new ZKTrustManager((X509ExtendedTrustManager) tm, hostnameVerificationEnabled, shouldVerifyClientHostname); } } throw new TrustManagerException("Couldn't find X509TrustManager"); diff --git a/src/java/main/org/apache/zookeeper/common/ZKTrustManager.java b/src/java/main/org/apache/zookeeper/common/ZKTrustManager.java new file mode 100644 index 00000000000..33c8fe70088 --- /dev/null +++ b/src/java/main/org/apache/zookeeper/common/ZKTrustManager.java @@ -0,0 +1,137 @@ +/** + * 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.common; + +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.X509ExtendedTrustManager; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * A custom TrustManager that supports hostname verification via org.apache.http.conn.ssl.DefaultHostnameVerifier. + * + * We attempt to perform verification using just the IP address first and if that fails will attempt to perform a + * reverse DNS lookup and verify using the hostname. + */ +public class ZKTrustManager extends X509ExtendedTrustManager { + + private static final Logger LOG = LoggerFactory.getLogger(ZKTrustManager.class); + + private X509ExtendedTrustManager x509ExtendedTrustManager; + private boolean hostnameVerificationEnabled; + private boolean shouldVerifyClientHostname; + + private DefaultHostnameVerifier hostnameVerifier; + + /** + * Instantiate a new ZKTrustManager. + * + * @param x509ExtendedTrustManager The trustmanager to use for checkClientTrusted/checkServerTrusted logic + * @param hostnameVerificationEnabled If true, this TrustManager should verify hostnames. + * @param shouldVerifyClientHostname If true, and hostnameVerificationEnabled is true, the hostname of a client + * connecting to this machine will be verified in addition to the servers that this + * instance connects to. If false, and hostnameVerificationEnabled is true, only + * the hostnames of servers that this instance connects to will be verified. If + * hostnameVerificationEnabled is false, this argument is ignored. + */ + public ZKTrustManager(X509ExtendedTrustManager x509ExtendedTrustManager, boolean hostnameVerificationEnabled, boolean shouldVerifyClientHostname) { + this.x509ExtendedTrustManager = x509ExtendedTrustManager; + this.hostnameVerificationEnabled = hostnameVerificationEnabled; + this.shouldVerifyClientHostname = shouldVerifyClientHostname; + + hostnameVerifier = new DefaultHostnameVerifier(); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return x509ExtendedTrustManager.getAcceptedIssuers(); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + if (hostnameVerificationEnabled && shouldVerifyClientHostname) { + performHostVerification(socket.getInetAddress(), chain[0]); + } + x509ExtendedTrustManager.checkClientTrusted(chain, authType, socket); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + if (hostnameVerificationEnabled) { + performHostVerification(socket.getInetAddress(), chain[0]); + } + x509ExtendedTrustManager.checkServerTrusted(chain, authType, socket); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { + if (hostnameVerificationEnabled && shouldVerifyClientHostname) { + try { + performHostVerification(InetAddress.getByName(engine.getPeerHost()), chain[0]); + } catch (UnknownHostException e) { + throw new CertificateException("failed to verify host", e); + } + } + x509ExtendedTrustManager.checkServerTrusted(chain, authType, engine); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { + if (hostnameVerificationEnabled) { + try { + performHostVerification(InetAddress.getByName(engine.getPeerHost()), chain[0]); + } catch (UnknownHostException e) { + throw new CertificateException("failed to verify host", e); + } + } + x509ExtendedTrustManager.checkServerTrusted(chain, authType, engine); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + x509ExtendedTrustManager.checkClientTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + x509ExtendedTrustManager.checkServerTrusted(chain, authType); + } + + private void performHostVerification(InetAddress inetAddress, X509Certificate certificate) throws CertificateException { + try { + hostnameVerifier.verify(inetAddress.getHostAddress(), certificate); + } catch (SSLException addressVerificationException) { + try { + LOG.debug("Failed to verify host address, attempting to verify host name with reverse dns lookup", addressVerificationException); + hostnameVerifier.verify(inetAddress.getHostName(), certificate); + } catch (SSLException hostnameVerificationException) { + LOG.error("Failed to verify host address", addressVerificationException); + LOG.error("Failed to verify hostname", hostnameVerificationException); + throw new CertificateException("Failed to verify both host address and host name", hostnameVerificationException); + } + } + } +} diff --git a/src/java/test/org/apache/zookeeper/common/X509UtilTest.java b/src/java/test/org/apache/zookeeper/common/X509UtilTest.java index 50d5fff1ba5..fa04020c57e 100644 --- a/src/java/test/org/apache/zookeeper/common/X509UtilTest.java +++ b/src/java/test/org/apache/zookeeper/common/X509UtilTest.java @@ -17,6 +17,7 @@ */ package org.apache.zookeeper.common; +import org.apache.zookeeper.PortAssignment; import org.apache.zookeeper.ZKTestCase; import org.apache.zookeeper.client.ZKClientConfig; import org.apache.zookeeper.server.ServerCnxnFactory; @@ -39,6 +40,7 @@ import org.junit.Test; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLSocket; import java.io.FileOutputStream; import java.math.BigInteger; @@ -64,6 +66,7 @@ public class X509UtilTest extends ZKTestCase { private static KeyPair rootKeyPair; private X509Util x509Util; + private String[] customCipherSuites = new String[]{"SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA"}; @BeforeClass public static void createKeyPair() throws Exception { @@ -154,18 +157,19 @@ public void cleanUp() throws Exception { System.clearProperty(x509Util.getSslHostnameVerificationEnabledProperty()); System.clearProperty(x509Util.getSslOcspEnabledProperty()); System.clearProperty(x509Util.getSslCrlEnabledProperty()); + System.clearProperty(x509Util.getCipherSuitesProperty()); System.clearProperty("com.sun.net.ssl.checkRevocation"); System.clearProperty("com.sun.security.enableCRLDP"); Security.setProperty("com.sun.security.enableCRLDP", "false"); } - @Test + @Test(timeout = 1000) public void testCreateSSLContextWithoutCustomProtocol() throws Exception { SSLContext sslContext = x509Util.getDefaultSSLContext(); Assert.assertEquals(X509Util.DEFAULT_PROTOCOL, sslContext.getProtocol()); } - @Test + @Test(timeout = 1000) public void testCreateSSLContextWithCustomProtocol() throws Exception { final String protocol = "TLSv1.1"; System.setProperty(x509Util.getSslProtocolProperty(), protocol); @@ -173,37 +177,35 @@ public void testCreateSSLContextWithCustomProtocol() throws Exception { Assert.assertEquals(protocol, sslContext.getProtocol()); } - @Test + @Test(timeout = 1000) public void testCreateSSLContextWithoutTrustStorePassword() throws Exception { writeTrustStore(null); System.clearProperty(x509Util.getSslTruststorePasswdProperty()); x509Util.getDefaultSSLContext(); } - @Test(expected = X509Exception.SSLContextException.class) + @Test(timeout = 1000, expected = X509Exception.SSLContextException.class) public void testCreateSSLContextWithoutKeyStoreLocation() throws Exception { System.clearProperty(x509Util.getSslKeystoreLocationProperty()); x509Util.getDefaultSSLContext(); } - @Test(expected = X509Exception.SSLContextException.class) + @Test(timeout = 1000, expected = X509Exception.SSLContextException.class) public void testCreateSSLContextWithoutKeyStorePassword() throws Exception { System.clearProperty(x509Util.getSslKeystorePasswdProperty()); x509Util.getDefaultSSLContext(); } - @Test + @Test(timeout = 1000) public void testCreateSSLContextWithCustomCipherSuites() throws Exception { - String[] cipherSuites = {"SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA"}; - System.setProperty(x509Util.getCipherSuitesProperty(), cipherSuites[0] + "," + cipherSuites[1]); - x509Util = new ClientX509Util(); + setCustomCipherSuites(); SSLSocket sslSocket = x509Util.createSSLSocket(); - Assert.assertArrayEquals(cipherSuites, sslSocket.getEnabledCipherSuites()); + Assert.assertArrayEquals(customCipherSuites, sslSocket.getEnabledCipherSuites()); } // It would be great to test the value of PKIXBuilderParameters#setRevocationEnabled but it does not appear to be // possible - @Test + @Test(timeout = 1000) public void testCRLEnabled() throws Exception { System.setProperty(x509Util.getSslCrlEnabledProperty(), "true"); x509Util.getDefaultSSLContext(); @@ -212,7 +214,7 @@ public void testCRLEnabled() throws Exception { Assert.assertFalse(Boolean.valueOf(Security.getProperty("ocsp.enable"))); } - @Test + @Test(timeout = 1000) public void testCRLDisabled() throws Exception { x509Util.getDefaultSSLContext(); Assert.assertFalse(Boolean.valueOf(System.getProperty("com.sun.net.ssl.checkRevocation"))); @@ -220,7 +222,7 @@ public void testCRLDisabled() throws Exception { Assert.assertFalse(Boolean.valueOf(Security.getProperty("ocsp.enable"))); } - @Test + @Test(timeout = 1000) public void testOCSPEnabled() throws Exception { System.setProperty(x509Util.getSslOcspEnabledProperty(), "true"); x509Util.getDefaultSSLContext(); @@ -228,4 +230,37 @@ public void testOCSPEnabled() throws Exception { Assert.assertTrue(Boolean.valueOf(System.getProperty("com.sun.security.enableCRLDP"))); Assert.assertTrue(Boolean.valueOf(Security.getProperty("ocsp.enable"))); } + + @Test(timeout = 1000) + public void testCreateSSLSocket() throws Exception { + setCustomCipherSuites(); + SSLSocket sslSocket = x509Util.createSSLSocket(); + Assert.assertArrayEquals(customCipherSuites, sslSocket.getEnabledCipherSuites()); + Assert.assertTrue(sslSocket.getNeedClientAuth()); + } + + @Test(timeout = 1000) + public void testCreateSSLServerSocketWithoutPort() throws Exception { + setCustomCipherSuites(); + SSLServerSocket sslServerSocket = x509Util.createSSLServerSocket(); + Assert.assertArrayEquals(customCipherSuites, sslServerSocket.getEnabledCipherSuites()); + Assert.assertTrue(sslServerSocket.getNeedClientAuth()); + } + + @Test(timeout = 1000) + public void testCreateSSLServerSocketWithPort() throws Exception { + int port = PortAssignment.unique(); + setCustomCipherSuites(); + SSLServerSocket sslServerSocket = x509Util.createSSLServerSocket(port); + Assert.assertEquals(sslServerSocket.getLocalPort(), port); + Assert.assertArrayEquals(customCipherSuites, sslServerSocket.getEnabledCipherSuites()); + Assert.assertTrue(sslServerSocket.getNeedClientAuth()); + } + + // Warning: this will reset the x509Util + private void setCustomCipherSuites() { + System.setProperty(x509Util.getCipherSuitesProperty(), customCipherSuites[0] + "," + customCipherSuites[1]); + x509Util = new ClientX509Util(); + } + } diff --git a/src/java/test/org/apache/zookeeper/common/ZKTrustManagerTest.java b/src/java/test/org/apache/zookeeper/common/ZKTrustManagerTest.java new file mode 100644 index 00000000000..2212850644f --- /dev/null +++ b/src/java/test/org/apache/zookeeper/common/ZKTrustManagerTest.java @@ -0,0 +1,248 @@ +/** + * 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.common; + +import org.apache.zookeeper.ZKTestCase; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x509.BasicConstraints; +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.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import javax.net.ssl.X509ExtendedTrustManager; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.Socket; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Random; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +// We can only test calls to ZKTrustManager using Sockets (not SSLEngines). This can be fine since the logic is the same. +public class ZKTrustManagerTest extends ZKTestCase { + + private static KeyPair keyPair; + + private X509ExtendedTrustManager mockX509ExtendedTrustManager; + private static final String IP_ADDRESS = "127.0.0.1"; + private static final String HOSTNAME = "localhost"; + + private InetAddress mockInetAddress; + private Socket mockSocket; + + @BeforeClass + public static void createKeyPair() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); + keyPairGenerator.initialize(4096); + keyPair = keyPairGenerator.genKeyPair(); + } + + @AfterClass + public static void removeBouncyCastleProvider() throws Exception { + Security.removeProvider("BC"); + } + + @Before + public void setup() throws Exception { + mockX509ExtendedTrustManager = mock(X509ExtendedTrustManager.class); + + mockInetAddress = mock(InetAddress.class); + when(mockInetAddress.getHostAddress()).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocationOnMock) throws Throwable { + return IP_ADDRESS; + } + }); + + when(mockInetAddress.getHostName()).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocationOnMock) throws Throwable { + return HOSTNAME; + } + }); + + mockSocket = mock(Socket.class); + when(mockSocket.getInetAddress()).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocationOnMock) throws Throwable { + return mockInetAddress; + } + }); + } + + private X509Certificate[] createSelfSignedCertifcateChain(String ipAddress, String hostname) throws Exception { + X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); + nameBuilder.addRDN(BCStyle.CN, "NOT_LOCALHOST"); + Date notBefore = new Date(); + 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)); + + List generalNames = new ArrayList<>(); + if (ipAddress != null) { + generalNames.add(new GeneralName(GeneralName.iPAddress, ipAddress)); + } + if (hostname != null) { + generalNames.add(new GeneralName(GeneralName.dNSName, hostname)); + } + + if (!generalNames.isEmpty()) { + certificateBuilder.addExtension(Extension.subjectAlternativeName, true, new GeneralNames(generalNames.toArray(new GeneralName[] {}))); + } + + ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.getPrivate()); + + return new X509Certificate[] { new JcaX509CertificateConverter().getCertificate(certificateBuilder.build(contentSigner)) }; + } + + @Test + public void testServerHostnameVerificationWithHostnameVerificationDisabled() throws Exception { + ZKTrustManager zkTrustManager = new ZKTrustManager(mockX509ExtendedTrustManager, false, false); + + X509Certificate[] certificateChain = createSelfSignedCertifcateChain(IP_ADDRESS, HOSTNAME); + zkTrustManager.checkServerTrusted(certificateChain, null, mockSocket); + + verify(mockInetAddress, times(0)).getHostAddress(); + verify(mockInetAddress, times(0)).getHostName(); + + verify(mockX509ExtendedTrustManager, times(1)).checkServerTrusted(certificateChain, null, mockSocket); + } + + @Test + public void testServerHostnameVerificationWithHostnameVerificationDisabledAndClientHostnameVerificationEnabled() throws Exception { + ZKTrustManager zkTrustManager = new ZKTrustManager(mockX509ExtendedTrustManager, false, true); + + X509Certificate[] certificateChain = createSelfSignedCertifcateChain(IP_ADDRESS, HOSTNAME); + zkTrustManager.checkServerTrusted(certificateChain, null, mockSocket); + + verify(mockInetAddress, times(0)).getHostAddress(); + verify(mockInetAddress, times(0)).getHostName(); + + verify(mockX509ExtendedTrustManager, times(1)).checkServerTrusted(certificateChain, null, mockSocket); + } + + @Test + public void testServerHostnameVerificationWithIPAddress() throws Exception { + ZKTrustManager zkTrustManager = new ZKTrustManager(mockX509ExtendedTrustManager, true, false); + + X509Certificate[] certificateChain = createSelfSignedCertifcateChain(IP_ADDRESS, null); + zkTrustManager.checkServerTrusted(certificateChain, null, mockSocket); + + verify(mockInetAddress, times(1)).getHostAddress(); + verify(mockInetAddress, times(0)).getHostName(); + + verify(mockX509ExtendedTrustManager, times(1)).checkServerTrusted(certificateChain, null, mockSocket); + } + + @Test + public void testServerHostnameVerificationWithHostname() throws Exception { + ZKTrustManager zkTrustManager = new ZKTrustManager(mockX509ExtendedTrustManager, true, false); + + X509Certificate[] certificateChain = createSelfSignedCertifcateChain(null, HOSTNAME); + zkTrustManager.checkServerTrusted(certificateChain, null, mockSocket); + + verify(mockInetAddress, times(1)).getHostAddress(); + verify(mockInetAddress, times(1)).getHostName(); + + verify(mockX509ExtendedTrustManager, times(1)).checkServerTrusted(certificateChain, null, mockSocket); + } + + @Test + public void testClientHostnameVerificationWithHostnameVerificationDisabled() throws Exception { + ZKTrustManager zkTrustManager = new ZKTrustManager(mockX509ExtendedTrustManager, false, true); + + X509Certificate[] certificateChain = createSelfSignedCertifcateChain(null, HOSTNAME); + zkTrustManager.checkClientTrusted(certificateChain, null, mockSocket); + + verify(mockInetAddress, times(0)).getHostAddress(); + verify(mockInetAddress, times(0)).getHostName(); + + verify(mockX509ExtendedTrustManager, times(1)).checkClientTrusted(certificateChain, null, mockSocket); + } + + @Test + public void testClientHostnameVerificationWithClientHostnameVerificationDisabled() throws Exception { + ZKTrustManager zkTrustManager = new ZKTrustManager(mockX509ExtendedTrustManager, true, false); + + X509Certificate[] certificateChain = createSelfSignedCertifcateChain(null, HOSTNAME); + zkTrustManager.checkClientTrusted(certificateChain, null, mockSocket); + + verify(mockInetAddress, times(0)).getHostAddress(); + verify(mockInetAddress, times(0)).getHostName(); + + verify(mockX509ExtendedTrustManager, times(1)).checkClientTrusted(certificateChain, null, mockSocket); + } + + @Test + public void testClientHostnameVerificationWithIPAddress() throws Exception { + ZKTrustManager zkTrustManager = new ZKTrustManager(mockX509ExtendedTrustManager, true, true); + + X509Certificate[] certificateChain = createSelfSignedCertifcateChain(IP_ADDRESS, null); + zkTrustManager.checkClientTrusted(certificateChain, null, mockSocket); + + verify(mockInetAddress, times(1)).getHostAddress(); + verify(mockInetAddress, times(0)).getHostName(); + + verify(mockX509ExtendedTrustManager, times(1)).checkClientTrusted(certificateChain, null, mockSocket); + } + + @Test + public void testClientHostnameVerificationWithHostname() throws Exception { + ZKTrustManager zkTrustManager = new ZKTrustManager(mockX509ExtendedTrustManager, true, true); + + X509Certificate[] certificateChain = createSelfSignedCertifcateChain(null, HOSTNAME); + zkTrustManager.checkClientTrusted(certificateChain, null, mockSocket); + + verify(mockInetAddress, times(1)).getHostAddress(); + verify(mockInetAddress, times(1)).getHostName(); + + verify(mockX509ExtendedTrustManager, times(1)).checkClientTrusted(certificateChain, null, mockSocket); + } +} diff --git a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java index 382e1ffe786..82ad84fc930 100644 --- a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java +++ b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java @@ -384,7 +384,7 @@ private void clearSSLSystemProperties() { System.clearProperty(quorumX509Util.getSslCrlEnabledProperty()); } - @Test + @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); @@ -405,7 +405,7 @@ public void testQuorumSSL() throws Exception { Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); } - @Test + @Test(timeout = 300000) public void testRollingUpgrade() throws Exception { // Form a quorum without ssl q1 = new MainThread(1, clientPortQp1, quorumConfiguration); @@ -453,7 +453,7 @@ private void stopAppendConfigRestartAll(Map members, String } } - @Test + @Test(timeout = 300000) public void testHostnameVerificationWithInvalidHostname() throws Exception { String badhostnameKeystorePath = tmpDir + "/badhost.jks"; X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), @@ -463,7 +463,7 @@ public void testHostnameVerificationWithInvalidHostname() throws Exception { testHostnameVerification(badhostnameKeystorePath, false); } - @Test + @Test(timeout = 300000) public void testHostnameVerificationWithInvalidIPAddress() throws Exception { String badhostnameKeystorePath = tmpDir + "/badhost.jks"; X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), @@ -473,7 +473,7 @@ public void testHostnameVerificationWithInvalidIPAddress() throws Exception { testHostnameVerification(badhostnameKeystorePath, false); } - @Test + @Test(timeout = 300000) public void testHostnameVerificationWithInvalidIpAddressAndInvalidHostname() throws Exception { String badhostnameKeystorePath = tmpDir + "/badhost.jks"; X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), @@ -483,7 +483,7 @@ public void testHostnameVerificationWithInvalidIpAddressAndInvalidHostname() thr testHostnameVerification(badhostnameKeystorePath, false); } - @Test + @Test(timeout = 300000) public void testHostnameVerificationWithInvalidIpAddressAndValidHostname() throws Exception { String badhostnameKeystorePath = tmpDir + "/badhost.jks"; X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), @@ -493,7 +493,7 @@ public void testHostnameVerificationWithInvalidIpAddressAndValidHostname() throw testHostnameVerification(badhostnameKeystorePath, true); } - @Test + @Test(timeout = 300000) public void testHostnameVerificationWithValidIpAddressAndInvalidHostname() throws Exception { String badhostnameKeystorePath = tmpDir + "/badhost.jks"; X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), @@ -551,7 +551,7 @@ private void testHostnameVerification(String keystorePath, boolean expectSuccess } - @Test + @Test(timeout = 300000) public void testCertificateRevocationList() throws Exception { q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED); q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED); @@ -605,7 +605,7 @@ public void testCertificateRevocationList() throws Exception { Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); } - @Test + @Test(timeout = 300000) public void testOCSP() throws Exception { Integer ocspPort = PortAssignment.unique(); From 6f0f0696e6778fdc9bce1c2bd794108d7f4152f3 Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Thu, 13 Apr 2017 15:27:31 -0700 Subject: [PATCH 18/25] Add comments regarding SunX509 to PKIX change Add integration tests for tls cipher suites and protocol versions Claify ZKTrustManager Remove ssl socket creation code from UnifiedServerSocket HostnameVerification now on by default --- .../org/apache/zookeeper/common/X509Util.java | 35 +++++++----- .../org/apache/zookeeper/common/ZKConfig.java | 24 ++++++++- .../zookeeper/common/ZKTrustManager.java | 37 +++++++------ .../server/quorum/UnifiedServerSocket.java | 4 +- .../apache/zookeeper/common/X509UtilTest.java | 1 - .../quorum/UnifiedServerSocketTest.java | 1 + .../apache/zookeeper/test/QuorumSSLTest.java | 54 +++++++++++++++++-- 7 files changed, 122 insertions(+), 34 deletions(-) diff --git a/src/java/main/org/apache/zookeeper/common/X509Util.java b/src/java/main/org/apache/zookeeper/common/X509Util.java index 36cb536f1ac..a964448564d 100644 --- a/src/java/main/org/apache/zookeeper/common/X509Util.java +++ b/src/java/main/org/apache/zookeeper/common/X509Util.java @@ -36,6 +36,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.net.Socket; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; @@ -163,7 +164,7 @@ public SSLContext createSSLContext(ZKConfig config) throws SSLContextException { boolean sslCrlEnabled = config.getBoolean(this.sslCrlEnabledProperty); boolean sslOcspEnabled = config.getBoolean(this.sslOcspEnabledProperty); - boolean sslServerHostnameVerificationEnabled = config.getBoolean(this.getSslHostnameVerificationEnabledProperty()); + boolean sslServerHostnameVerificationEnabled = config.getBoolean(this.getSslHostnameVerificationEnabledProperty(), true); if (trustStoreLocationProp == null) { LOG.warn(getSslTruststoreLocationProperty() + " not specified"); @@ -247,6 +248,7 @@ public static X509TrustManager createTrustManager(String trustStoreLocation, Str pbParams.setRevocationEnabled(false); } + // Revocation checking is only supported with the PKIX algorithm TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); tmf.init(new CertPathTrustManagerParameters(pbParams)); @@ -271,6 +273,19 @@ public static X509TrustManager createTrustManager(String trustStoreLocation, Str public SSLSocket createSSLSocket() throws X509Exception, IOException { SSLSocket sslSocket = (SSLSocket) getDefaultSSLContext().getSocketFactory().createSocket(); + configureSSLSocket(sslSocket); + + return sslSocket; + } + + public SSLSocket createSSLSocket(Socket socket) throws X509Exception, IOException { + SSLSocket sslSocket = (SSLSocket) getDefaultSSLContext().getSocketFactory().createSocket(socket, null, socket.getPort(), false); + configureSSLSocket(sslSocket); + + return sslSocket; + } + + private void configureSSLSocket(SSLSocket sslSocket) { SSLParameters sslParameters = sslSocket.getSSLParameters(); sslParameters.setNeedClientAuth(true); if (cipherSuites != null) { @@ -278,26 +293,24 @@ public SSLSocket createSSLSocket() throws X509Exception, IOException { } sslSocket.setSSLParameters(sslParameters); - - return sslSocket; } public SSLServerSocket createSSLServerSocket() throws X509Exception, IOException { SSLServerSocket sslServerSocket = (SSLServerSocket) getDefaultSSLContext().getServerSocketFactory().createServerSocket(); - SSLParameters sslParameters = sslServerSocket.getSSLParameters(); - sslParameters.setNeedClientAuth(true); - if (cipherSuites != null) { - sslParameters.setCipherSuites(cipherSuites); - } - - sslServerSocket.setSSLParameters(sslParameters); + configureSSLServerSocket(sslServerSocket); return sslServerSocket; } public SSLServerSocket createSSLServerSocket(int port) throws X509Exception, IOException { SSLServerSocket sslServerSocket = (SSLServerSocket) getDefaultSSLContext().getServerSocketFactory().createServerSocket(port); + configureSSLServerSocket(sslServerSocket); + + return sslServerSocket; + } + + private void configureSSLServerSocket(SSLServerSocket sslServerSocket) { SSLParameters sslParameters = sslServerSocket.getSSLParameters(); sslParameters.setNeedClientAuth(true); if (cipherSuites != null) { @@ -305,7 +318,5 @@ public SSLServerSocket createSSLServerSocket(int port) throws X509Exception, IOE } sslServerSocket.setSSLParameters(sslParameters); - - return sslServerSocket; } } diff --git a/src/java/main/org/apache/zookeeper/common/ZKConfig.java b/src/java/main/org/apache/zookeeper/common/ZKConfig.java index acc28f0913b..dc24b19cf01 100644 --- a/src/java/main/org/apache/zookeeper/common/ZKConfig.java +++ b/src/java/main/org/apache/zookeeper/common/ZKConfig.java @@ -231,7 +231,29 @@ private void parseProperties(Properties cfg) { * exists and is equal to the string {@code "true"}. */ public boolean getBoolean(String key) { - return Boolean.parseBoolean(getProperty(key)); + return getBoolean(key, false); + } + + /** + * Get the value of the key property as a boolean. Returns + * {@code true} if and only if the property named by the argument exists and is equal + * to the string {@code "true"}. If the property is not set, the provided + * defaultValue is returned. + * + * @param key + * property key. + * @param defaultValue + * default value. + * @return return property value as an boolean, or + * defaultValue + */ + public boolean getBoolean(String key, boolean defaultValue) { + String propertyValue = getProperty(key); + if (propertyValue == null) { + return defaultValue; + } else { + return Boolean.parseBoolean(propertyValue); + } } /** diff --git a/src/java/main/org/apache/zookeeper/common/ZKTrustManager.java b/src/java/main/org/apache/zookeeper/common/ZKTrustManager.java index 33c8fe70088..b4b0cbd68d1 100644 --- a/src/java/main/org/apache/zookeeper/common/ZKTrustManager.java +++ b/src/java/main/org/apache/zookeeper/common/ZKTrustManager.java @@ -49,18 +49,20 @@ public class ZKTrustManager extends X509ExtendedTrustManager { /** * Instantiate a new ZKTrustManager. * - * @param x509ExtendedTrustManager The trustmanager to use for checkClientTrusted/checkServerTrusted logic - * @param hostnameVerificationEnabled If true, this TrustManager should verify hostnames. - * @param shouldVerifyClientHostname If true, and hostnameVerificationEnabled is true, the hostname of a client - * connecting to this machine will be verified in addition to the servers that this - * instance connects to. If false, and hostnameVerificationEnabled is true, only - * the hostnames of servers that this instance connects to will be verified. If - * hostnameVerificationEnabled is false, this argument is ignored. + * @param x509ExtendedTrustManager The trustmanager to use for checkClientTrusted/checkServerTrusted logic + * @param verifySSLServerHostname If true, this TrustManager should verify hostnames of servers that this + * instance connects to. + * @param verifySSLClientHostname If true, and verifySSLServerHostname is true, the hostname of a client + * connecting to this machine will be verified in addition to the servers that this + * instance connects to. If false, and verifySSLServerHostname is true, only + * the hostnames of servers that this instance connects to will be verified. If + * verifySSLServerHostname is false, this argument is ignored. */ - public ZKTrustManager(X509ExtendedTrustManager x509ExtendedTrustManager, boolean hostnameVerificationEnabled, boolean shouldVerifyClientHostname) { + public ZKTrustManager(X509ExtendedTrustManager x509ExtendedTrustManager, boolean verifySSLServerHostname, + boolean verifySSLClientHostname) { this.x509ExtendedTrustManager = x509ExtendedTrustManager; - this.hostnameVerificationEnabled = hostnameVerificationEnabled; - this.shouldVerifyClientHostname = shouldVerifyClientHostname; + this.hostnameVerificationEnabled = verifySSLServerHostname; + this.shouldVerifyClientHostname = verifySSLClientHostname; hostnameVerifier = new DefaultHostnameVerifier(); } @@ -121,15 +123,20 @@ public void checkServerTrusted(X509Certificate[] chain, String authType) throws } private void performHostVerification(InetAddress inetAddress, X509Certificate certificate) throws CertificateException { + String hostAddress = ""; + String hostName = ""; try { - hostnameVerifier.verify(inetAddress.getHostAddress(), certificate); + hostAddress = inetAddress.getHostAddress(); + hostnameVerifier.verify(hostAddress, certificate); } catch (SSLException addressVerificationException) { try { - LOG.debug("Failed to verify host address, attempting to verify host name with reverse dns lookup", addressVerificationException); - hostnameVerifier.verify(inetAddress.getHostName(), certificate); + LOG.debug("Failed to verify host address: " + hostAddress + + ", attempting to verify host name with reverse dns lookup", addressVerificationException); + hostName = inetAddress.getHostName(); + hostnameVerifier.verify(hostName, certificate); } catch (SSLException hostnameVerificationException) { - LOG.error("Failed to verify host address", addressVerificationException); - LOG.error("Failed to verify hostname", hostnameVerificationException); + LOG.error("Failed to verify host address: " + hostAddress, addressVerificationException); + LOG.error("Failed to verify hostname: " + hostName, hostnameVerificationException); throw new CertificateException("Failed to verify both host address and host name", hostnameVerificationException); } } diff --git a/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java b/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java index 9765bbd4520..c8c71740265 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java @@ -67,8 +67,8 @@ public Socket accept() throws IOException { LOG.info(getInetAddress() + " attempting to connect over ssl"); SSLSocket sslSocket; try { - sslSocket = (SSLSocket) x509Util.getDefaultSSLContext().getSocketFactory().createSocket(bufferedSocket, null, bufferedSocket.getPort(), false); - } catch (X509Exception.SSLContextException e) { + sslSocket = x509Util.createSSLSocket(bufferedSocket); + } catch (X509Exception e) { throw new IOException("failed to create SSL context", e); } sslSocket.setUseClientMode(false); diff --git a/src/java/test/org/apache/zookeeper/common/X509UtilTest.java b/src/java/test/org/apache/zookeeper/common/X509UtilTest.java index fa04020c57e..84ed51c60fe 100644 --- a/src/java/test/org/apache/zookeeper/common/X509UtilTest.java +++ b/src/java/test/org/apache/zookeeper/common/X509UtilTest.java @@ -262,5 +262,4 @@ private void setCustomCipherSuites() { System.setProperty(x509Util.getCipherSuitesProperty(), customCipherSuites[0] + "," + customCipherSuites[1]); x509Util = new ClientX509Util(); } - } diff --git a/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java b/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java index d6b69ae7028..17d881956ce 100644 --- a/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java +++ b/src/java/test/org/apache/zookeeper/server/quorum/UnifiedServerSocketTest.java @@ -61,6 +61,7 @@ public void setUp() throws Exception { System.setProperty(x509Util.getSslKeystorePasswdProperty(), "testpass"); System.setProperty(x509Util.getSslTruststoreLocationProperty(), testDataPath + "/ssl/testTrustStore.jks"); System.setProperty(x509Util.getSslTruststorePasswdProperty(), "testpass"); + System.setProperty(x509Util.getSslHostnameVerificationEnabledProperty(), "false"); } @Test diff --git a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java index 82ad84fc930..18d850cda21 100644 --- a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java +++ b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java @@ -361,7 +361,6 @@ public void setSSLSystemProperties() { System.setProperty(quorumX509Util.getSslKeystorePasswdProperty(), "testpass"); System.setProperty(quorumX509Util.getSslTruststoreLocationProperty(), truststorePath); System.setProperty(quorumX509Util.getSslTruststorePasswdProperty(), "testpass"); - System.setProperty(quorumX509Util.getSslHostnameVerificationEnabledProperty(), "false"); } @After @@ -382,6 +381,8 @@ private void clearSSLSystemProperties() { System.clearProperty(quorumX509Util.getSslHostnameVerificationEnabledProperty()); System.clearProperty(quorumX509Util.getSslOcspEnabledProperty()); System.clearProperty(quorumX509Util.getSslCrlEnabledProperty()); + System.clearProperty(quorumX509Util.getCipherSuitesProperty()); + System.clearProperty(quorumX509Util.getSslProtocolProperty()); } @Test(timeout = 300000) @@ -509,6 +510,8 @@ public void testHostnameVerificationWithValidIpAddressAndInvalidHostname() throw * @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); @@ -526,7 +529,6 @@ private void testHostnameVerification(String keystorePath, boolean expectSuccess Assert.assertTrue(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); - q1.shutdown(); q2.shutdown(); q3.shutdown(); @@ -536,7 +538,7 @@ private void testHostnameVerification(String keystorePath, boolean expectSuccess Assert.assertTrue(ClientBase.waitForServerDown("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); setSSLSystemProperties(); - System.setProperty(quorumX509Util.getSslHostnameVerificationEnabledProperty(), "true"); + System.clearProperty(quorumX509Util.getSslHostnameVerificationEnabledProperty()); q1.start(); q2.start(); @@ -665,4 +667,50 @@ public void testOCSP() throws Exception { ocspServer.stop(0); } } + + @Test(timeout = 300000) + public void testCipherSuites() throws Exception { + System.setProperty(quorumX509Util.getCipherSuitesProperty(), + "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA,SSL_RSA_WITH_RC4_128_MD5"); + + 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.getCipherSuitesProperty(), "TLS_RSA_WITH_AES_128_CBC_SHA"); + + // 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); + q3.start(); + + Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); + } + + @Test(timeout = 300000) + 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); + q3.start(); + + Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); + } } From ff5ec577eb8204fc675dc8a9f0fc65e8ead48271 Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Fri, 14 Apr 2017 10:19:22 -0700 Subject: [PATCH 19/25] Address stylistic comments from hanm's review Clean up exception handling --- .../zookeeper/ClientCnxnSocketNetty.java | 1 - .../zookeeper/client/FourLetterWordMain.java | 1 - .../org/apache/zookeeper/common/X509Util.java | 46 +++++++++++-------- .../zookeeper/server/quorum/Follower.java | 2 +- .../server/quorum/LearnerHandler.java | 5 -- 5 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/java/main/org/apache/zookeeper/ClientCnxnSocketNetty.java b/src/java/main/org/apache/zookeeper/ClientCnxnSocketNetty.java index 5031b1aa607..50d70f657cd 100644 --- a/src/java/main/org/apache/zookeeper/ClientCnxnSocketNetty.java +++ b/src/java/main/org/apache/zookeeper/ClientCnxnSocketNetty.java @@ -22,7 +22,6 @@ import org.apache.zookeeper.ClientCnxn.Packet; import org.apache.zookeeper.client.ZKClientConfig; import org.apache.zookeeper.common.ClientX509Util; -import org.apache.zookeeper.common.X509Util; import org.jboss.netty.bootstrap.ClientBootstrap; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; diff --git a/src/java/main/org/apache/zookeeper/client/FourLetterWordMain.java b/src/java/main/org/apache/zookeeper/client/FourLetterWordMain.java index 3d8d7c4f152..1470ab982cb 100644 --- a/src/java/main/org/apache/zookeeper/client/FourLetterWordMain.java +++ b/src/java/main/org/apache/zookeeper/client/FourLetterWordMain.java @@ -33,7 +33,6 @@ import org.apache.zookeeper.common.ClientX509Util; import org.apache.zookeeper.common.X509Exception.SSLContextException; -import org.apache.zookeeper.common.X509Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/java/main/org/apache/zookeeper/common/X509Util.java b/src/java/main/org/apache/zookeeper/common/X509Util.java index a964448564d..969237ad609 100644 --- a/src/java/main/org/apache/zookeeper/common/X509Util.java +++ b/src/java/main/org/apache/zookeeper/common/X509Util.java @@ -37,14 +37,20 @@ import java.io.FileInputStream; import java.io.IOException; import java.net.Socket; +import java.security.InvalidAlgorithmParameterException; import java.security.KeyManagementException; import java.security.KeyStore; +import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.Security; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import java.security.cert.PKIXBuilderParameters; import java.security.cert.X509CertSelector; -import static org.apache.zookeeper.common.X509Exception.*; +import org.apache.zookeeper.common.X509Exception.KeyManagerException; +import org.apache.zookeeper.common.X509Exception.SSLContextException; +import org.apache.zookeeper.common.X509Exception.TrustManagerException; /** * Utility code for X509 handling @@ -52,7 +58,7 @@ public abstract class X509Util { private static final Logger LOG = LoggerFactory.getLogger(X509Util.class); - public static final String DEFAULT_PROTOCOL = "TLSv1"; + static final String DEFAULT_PROTOCOL = "TLSv1"; private String sslProtocolProperty = getConfigPrefix() + "protocol"; private String cipherSuitesProperty = getConfigPrefix() + "ciphersuites"; @@ -115,15 +121,15 @@ public String getSslOcspEnabledProperty() { return sslOcspEnabledProperty; } - public synchronized SSLContext getDefaultSSLContext() throws SSLContextException { + public synchronized SSLContext getDefaultSSLContext() throws X509Exception.SSLContextException { if (defaultSSLContext == null) { defaultSSLContext = createSSLContext(); } return defaultSSLContext; } - public SSLContext createSSLContext() throws SSLContextException { - /** + private SSLContext createSSLContext() throws SSLContextException { + /* * Since Configuration initializes the key store and trust store related * configuration from system property. Reading property from * configuration will be same reading from system property @@ -154,8 +160,8 @@ public SSLContext createSSLContext(ZKConfig config) throws SSLContextException { try { keyManagers = new KeyManager[]{ createKeyManager(keyStoreLocationProp, keyStorePasswordProp)}; - } catch (KeyManagerException e) { - throw new SSLContextException("Failed to create KeyManager", e); + } catch (KeyManagerException keyManagerException) { + throw new SSLContextException("Failed to create KeyManager", keyManagerException); } } @@ -173,8 +179,8 @@ public SSLContext createSSLContext(ZKConfig config) throws SSLContextException { trustManagers = new TrustManager[]{ createTrustManager(trustStoreLocationProp, trustStorePasswordProp, sslCrlEnabled, sslOcspEnabled, sslServerHostnameVerificationEnabled, shouldVerifyClientHostname())}; - } catch (TrustManagerException e) { - throw new SSLContextException("Failed to create TrustManager", e); + } catch (TrustManagerException trustManagerException) { + throw new SSLContextException("Failed to create TrustManager", trustManagerException); } } @@ -183,8 +189,8 @@ public SSLContext createSSLContext(ZKConfig config) throws SSLContextException { SSLContext sslContext = SSLContext.getInstance(protocol); sslContext.init(keyManagers, trustManagers, null); return sslContext; - } catch (NoSuchAlgorithmException|KeyManagementException e) { - throw new SSLContextException(e); + } catch (NoSuchAlgorithmException|KeyManagementException sslContextInitException) { + throw new SSLContextException(sslContextInitException); } } @@ -207,13 +213,16 @@ public static X509KeyManager createKeyManager(String keyStoreLocation, String ke } throw new KeyManagerException("Couldn't find X509KeyManager"); - } catch (Exception e) { - throw new KeyManagerException(e); + } catch (IOException|CertificateException|UnrecoverableKeyException|NoSuchAlgorithmException|KeyStoreException + keyManagerCreationException) { + throw new KeyManagerException(keyManagerCreationException); } finally { if (inputStream != null) { try { inputStream.close(); - } catch (IOException e) {} + } catch (IOException ioException) { + LOG.info("Failed to close key store input stream", ioException); + } } } } @@ -258,14 +267,15 @@ public static X509TrustManager createTrustManager(String trustStoreLocation, Str } } throw new TrustManagerException("Couldn't find X509TrustManager"); - } catch (Exception e) { - throw new TrustManagerException(e); + } catch (IOException|CertificateException|NoSuchAlgorithmException|InvalidAlgorithmParameterException|KeyStoreException + trustManagerCreationException) { + throw new TrustManagerException(trustManagerCreationException); } finally { if (inputStream != null) { try { inputStream.close(); - } catch (IOException e) { - LOG.info("failed to close TrustStore input stream", e); + } catch (IOException ioException) { + LOG.info("failed to close TrustStore input stream", ioException); } } } diff --git a/src/java/main/org/apache/zookeeper/server/quorum/Follower.java b/src/java/main/org/apache/zookeeper/server/quorum/Follower.java index fa242f03cf3..1cdc2025c7e 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/Follower.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/Follower.java @@ -71,7 +71,7 @@ void followLeader() throws InterruptedException { self.end_fle = 0; fzk.registerJMX(new FollowerBean(this, zk), self.jmxLocalPeerBean); try { - InetSocketAddress addr = findLeader(); + InetSocketAddress addr = findLeader(); try { connectToLeader(addr); long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO); diff --git a/src/java/main/org/apache/zookeeper/server/quorum/LearnerHandler.java b/src/java/main/org/apache/zookeeper/server/quorum/LearnerHandler.java index 39874c4e4c9..dcd1b47c7af 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/LearnerHandler.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/LearnerHandler.java @@ -38,8 +38,6 @@ import org.apache.jute.Record; import org.apache.zookeeper.KeeperException.SessionExpiredException; import org.apache.zookeeper.ZooDefs.OpCode; -import org.apache.zookeeper.common.X509Exception; -import org.apache.zookeeper.common.X509Util; import org.apache.zookeeper.server.Request; import org.apache.zookeeper.server.TxnLogProposalIterator; import org.apache.zookeeper.server.ZKDatabase; @@ -53,8 +51,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.ssl.SSLSocket; - /** * There will be an instance of this class created by the Leader for each * learner. All communication with a learner is handled by this @@ -387,7 +383,6 @@ public void run() { + leader.self.getView().get(this.sid).toString()); } else { LOG.info("Follower sid: " + this.sid + " not in the current config " + Long.toHexString(leader.self.getQuorumVerifier().getVersion())); - //TODO: Hostname verification possible } if (qp.getType() == Leader.OBSERVERINFO) { From 30090f7e166a7da80841c40893bec635d4385aac Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Fri, 14 Apr 2017 15:38:34 -0700 Subject: [PATCH 20/25] Attempt to fix flaky test --- .../apache/zookeeper/common/X509UtilTest.java | 2 +- .../apache/zookeeper/test/QuorumSSLTest.java | 31 +++++++++---------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/java/test/org/apache/zookeeper/common/X509UtilTest.java b/src/java/test/org/apache/zookeeper/common/X509UtilTest.java index 84ed51c60fe..fa2db4f2926 100644 --- a/src/java/test/org/apache/zookeeper/common/X509UtilTest.java +++ b/src/java/test/org/apache/zookeeper/common/X509UtilTest.java @@ -131,7 +131,7 @@ private void writeTrustStore(char[] password) throws Exception { private X509Certificate createSelfSignedCertifcate(KeyPair keyPair) throws Exception { X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); nameBuilder.addRDN(BCStyle.CN, "localhost"); - Date notBefore = new Date(); // time from which certificate is valid + Date notBefore = new Date(); Calendar cal = Calendar.getInstance(); cal.setTime(notBefore); cal.add(Calendar.YEAR, 1); diff --git a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java index 18d850cda21..14e393a3de6 100644 --- a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java +++ b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java @@ -141,6 +141,9 @@ public class QuorumSSLTest extends QuorumPeerTestBase { private ContentSigner contentSigner; + private Date certStartTime; + private Date certEndTime; + @Before public void setup() throws Exception { ClientBase.setupTestEnv(); @@ -158,6 +161,12 @@ public void setup() throws Exception { 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); @@ -189,7 +198,6 @@ private void writeKeystore(X509Certificate certificate, KeyPair entityKeyPair, S outputStream.close(); } - private class OCSPHandler implements HttpHandler { private X509Certificate revokedCert; @@ -249,15 +257,10 @@ public void handle(com.sun.net.httpserver.HttpExchange httpExchange) throws IOEx 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()) + 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)); @@ -265,14 +268,9 @@ private X509Certificate createSelfSignedCertifcate(KeyPair keyPair) throws Excep } 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); + 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"))); @@ -302,8 +300,7 @@ public X509Certificate buildEndEntityCert(KeyPair keyPair, X509Certificate caCer 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()) + 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)) From 1a20d14929b7427e51791bba3d40ead7853d44ca Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Wed, 19 Apr 2017 13:55:34 -0700 Subject: [PATCH 21/25] look for stuck thread in tests --- .../apache/zookeeper/test/QuorumSSLTest.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java index 14e393a3de6..0a18dcd9e10 100644 --- a/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java +++ b/src/java/test/org/apache/zookeeper/test/QuorumSSLTest.java @@ -79,7 +79,9 @@ 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 java.io.FileOutputStream; import java.io.FileWriter; @@ -105,6 +107,7 @@ 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; @@ -143,6 +146,9 @@ public class QuorumSSLTest extends QuorumPeerTestBase { 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 { @@ -382,7 +388,7 @@ private void clearSSLSystemProperties() { System.clearProperty(quorumX509Util.getSslProtocolProperty()); } - @Test(timeout = 300000) + @Test public void testQuorumSSL() throws Exception { q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED); q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED); @@ -403,7 +409,7 @@ public void testQuorumSSL() throws Exception { Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); } - @Test(timeout = 300000) + @Test public void testRollingUpgrade() throws Exception { // Form a quorum without ssl q1 = new MainThread(1, clientPortQp1, quorumConfiguration); @@ -451,7 +457,7 @@ private void stopAppendConfigRestartAll(Map members, String } } - @Test(timeout = 300000) + @Test public void testHostnameVerificationWithInvalidHostname() throws Exception { String badhostnameKeystorePath = tmpDir + "/badhost.jks"; X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), @@ -461,7 +467,7 @@ public void testHostnameVerificationWithInvalidHostname() throws Exception { testHostnameVerification(badhostnameKeystorePath, false); } - @Test(timeout = 300000) + @Test public void testHostnameVerificationWithInvalidIPAddress() throws Exception { String badhostnameKeystorePath = tmpDir + "/badhost.jks"; X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), @@ -471,7 +477,7 @@ public void testHostnameVerificationWithInvalidIPAddress() throws Exception { testHostnameVerification(badhostnameKeystorePath, false); } - @Test(timeout = 300000) + @Test public void testHostnameVerificationWithInvalidIpAddressAndInvalidHostname() throws Exception { String badhostnameKeystorePath = tmpDir + "/badhost.jks"; X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), @@ -481,7 +487,7 @@ public void testHostnameVerificationWithInvalidIpAddressAndInvalidHostname() thr testHostnameVerification(badhostnameKeystorePath, false); } - @Test(timeout = 300000) + @Test public void testHostnameVerificationWithInvalidIpAddressAndValidHostname() throws Exception { String badhostnameKeystorePath = tmpDir + "/badhost.jks"; X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), @@ -491,7 +497,7 @@ public void testHostnameVerificationWithInvalidIpAddressAndValidHostname() throw testHostnameVerification(badhostnameKeystorePath, true); } - @Test(timeout = 300000) + @Test public void testHostnameVerificationWithValidIpAddressAndInvalidHostname() throws Exception { String badhostnameKeystorePath = tmpDir + "/badhost.jks"; X509Certificate badHostCert = buildEndEntityCert(defaultKeyPair, rootCertificate, rootKeyPair.getPrivate(), @@ -550,7 +556,7 @@ private void testHostnameVerification(String keystorePath, boolean expectSuccess } - @Test(timeout = 300000) + @Test public void testCertificateRevocationList() throws Exception { q1 = new MainThread(1, clientPortQp1, quorumConfiguration, SSL_QUORUM_ENABLED); q2 = new MainThread(2, clientPortQp2, quorumConfiguration, SSL_QUORUM_ENABLED); @@ -604,7 +610,7 @@ public void testCertificateRevocationList() throws Exception { Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); } - @Test(timeout = 300000) + @Test public void testOCSP() throws Exception { Integer ocspPort = PortAssignment.unique(); @@ -665,7 +671,7 @@ public void testOCSP() throws Exception { } } - @Test(timeout = 300000) + @Test public void testCipherSuites() throws Exception { System.setProperty(quorumX509Util.getCipherSuitesProperty(), "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA,SSL_RSA_WITH_RC4_128_MD5"); @@ -689,7 +695,7 @@ public void testCipherSuites() throws Exception { Assert.assertFalse(ClientBase.waitForServerUp("127.0.0.1:" + clientPortQp3, CONNECTION_TIMEOUT)); } - @Test(timeout = 300000) + @Test public void testProtocolVersion() throws Exception { System.setProperty(quorumX509Util.getSslProtocolProperty(), "TLSv1.2"); From 3c6c81b69b7105fa7c5235a0f27718a7eae195de Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Thu, 20 Apr 2017 12:19:33 -0700 Subject: [PATCH 22/25] Make sure to close underlying socket when closing ssl socket --- .../main/org/apache/zookeeper/common/X509Util.java | 2 +- .../zookeeper/server/quorum/BufferedSocket.java | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/java/main/org/apache/zookeeper/common/X509Util.java b/src/java/main/org/apache/zookeeper/common/X509Util.java index 969237ad609..0db295da344 100644 --- a/src/java/main/org/apache/zookeeper/common/X509Util.java +++ b/src/java/main/org/apache/zookeeper/common/X509Util.java @@ -289,7 +289,7 @@ public SSLSocket createSSLSocket() throws X509Exception, IOException { } public SSLSocket createSSLSocket(Socket socket) throws X509Exception, IOException { - SSLSocket sslSocket = (SSLSocket) getDefaultSSLContext().getSocketFactory().createSocket(socket, null, socket.getPort(), false); + SSLSocket sslSocket = (SSLSocket) getDefaultSSLContext().getSocketFactory().createSocket(socket, null, socket.getPort(), true); configureSSLSocket(sslSocket); return sslSocket; diff --git a/src/java/main/org/apache/zookeeper/server/quorum/BufferedSocket.java b/src/java/main/org/apache/zookeeper/server/quorum/BufferedSocket.java index 7852b06afc2..c1cf7ec17d0 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/BufferedSocket.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/BufferedSocket.java @@ -19,7 +19,6 @@ package org.apache.zookeeper.server.quorum; import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.IOException; import java.net.Socket; import java.net.SocketImpl; @@ -27,7 +26,6 @@ public class BufferedSocket extends Socket { private BufferedInputStream bufferedInputStream; - private BufferedOutputStream bufferedOutputStream; public BufferedSocket(SocketImpl base) throws IOException { super(base); @@ -42,13 +40,4 @@ public BufferedInputStream getInputStream() throws IOException { return bufferedInputStream; } - @Override - public BufferedOutputStream getOutputStream() throws IOException { - if (bufferedOutputStream == null) { - bufferedOutputStream = new BufferedOutputStream(super.getOutputStream()); - } - - return bufferedOutputStream; - } - } \ No newline at end of file From 08d29810aae233807f22a6adb5f797fb810807df Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Thu, 20 Apr 2017 16:09:30 -0700 Subject: [PATCH 23/25] reduce concurrent tests for debugging --- src/java/test/bin/test-github-pr.sh | 4 ++-- src/java/test/bin/test-patch.sh | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/java/test/bin/test-github-pr.sh b/src/java/test/bin/test-github-pr.sh index e155769f268..944ca8b8ec2 100755 --- a/src/java/test/bin/test-github-pr.sh +++ b/src/java/test/bin/test-github-pr.sh @@ -452,8 +452,8 @@ runCoreTests () { ### Kill any rogue build processes from the last attempt $PS auxwww | $GREP ZookeeperPatchProcess | /usr/bin/nawk '{print $2}' | /usr/bin/xargs -t -I {} /bin/kill -9 {} > /dev/null - echo "$ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=8 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME test-core" - $ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=8 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME test-core + echo "$ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=1 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME -Dtestcase=QuorumSSLTest -Djava.security.debug=certpath -Djavax.net.debug=ssl,handshake" + $ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=1 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME -Dtestcase=QuorumSSLTest -Djava.security.debug=certpath -Djavax.net.debug=ssl,handshake if [[ $? != 0 ]] ; then JIRA_COMMENT="$JIRA_COMMENT diff --git a/src/java/test/bin/test-patch.sh b/src/java/test/bin/test-patch.sh index fbc6037d542..aadd3b54759 100755 --- a/src/java/test/bin/test-patch.sh +++ b/src/java/test/bin/test-patch.sh @@ -476,8 +476,9 @@ runCoreTests () { ### Kill any rogue build processes from the last attempt $PS auxwww | $GREP ZookeeperPatchProcess | /usr/bin/nawk '{print $2}' | /usr/bin/xargs -t -I {} /bin/kill -9 {} > /dev/null - echo "$ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=8 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME test-core" - $ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=8 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME test-core + echo "$ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=1 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME test-core" + $ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=1 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME -Dtestcase=QuorumSSLTest -Djava.security.debug=certpath -Djavax.net.debug=ssl,handshake + test-core if [[ $? != 0 ]] ; then JIRA_COMMENT="$JIRA_COMMENT From 8686df4143988d484443344edc260d55620cd209 Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Fri, 21 Apr 2017 11:37:52 -0700 Subject: [PATCH 24/25] revert scripts, change implementation for rolling upgrade socket --- ...eredSocket.java => PrependableSocket.java} | 22 ++++++++++++------- .../server/quorum/UnifiedServerSocket.java | 14 +++++------- src/java/test/bin/test-github-pr.sh | 4 ++-- src/java/test/bin/test-patch.sh | 5 ++--- 4 files changed, 24 insertions(+), 21 deletions(-) rename src/java/main/org/apache/zookeeper/server/quorum/{BufferedSocket.java => PrependableSocket.java} (61%) diff --git a/src/java/main/org/apache/zookeeper/server/quorum/BufferedSocket.java b/src/java/main/org/apache/zookeeper/server/quorum/PrependableSocket.java similarity index 61% rename from src/java/main/org/apache/zookeeper/server/quorum/BufferedSocket.java rename to src/java/main/org/apache/zookeeper/server/quorum/PrependableSocket.java index c1cf7ec17d0..a86608ff2f2 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/BufferedSocket.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/PrependableSocket.java @@ -18,26 +18,32 @@ package org.apache.zookeeper.server.quorum; -import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; import java.net.Socket; import java.net.SocketImpl; -public class BufferedSocket extends Socket { +public class PrependableSocket extends Socket { - private BufferedInputStream bufferedInputStream; + private SequenceInputStream sequenceInputStream; - public BufferedSocket(SocketImpl base) throws IOException { + public PrependableSocket(SocketImpl base) throws IOException { super(base); } @Override - public BufferedInputStream getInputStream() throws IOException { - if (bufferedInputStream == null) { - bufferedInputStream = new BufferedInputStream(super.getInputStream()); + public InputStream getInputStream() throws IOException { + if (sequenceInputStream == null) { + return super.getInputStream(); } - return bufferedInputStream; + return sequenceInputStream; + } + + public void prependToInputStream(byte[] bytes) throws IOException { + sequenceInputStream = new SequenceInputStream(new ByteArrayInputStream(bytes), getInputStream()); } } \ No newline at end of file diff --git a/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java b/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java index c8c71740265..4802ecf308c 100644 --- a/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java +++ b/src/java/main/org/apache/zookeeper/server/quorum/UnifiedServerSocket.java @@ -54,20 +54,18 @@ public Socket accept() throws IOException { if (!isBound()) { throw new SocketException("Socket is not bound yet"); } - final Socket bufferedSocket = new BufferedSocket(null); - implAccept(bufferedSocket); - - bufferedSocket.getInputStream().mark(6); + final PrependableSocket prependableSocket = new PrependableSocket(null); + implAccept(prependableSocket); byte[] litmus = new byte[5]; - int bytesRead = bufferedSocket.getInputStream().read(litmus, 0, 5); - bufferedSocket.getInputStream().reset(); + int bytesRead = prependableSocket.getInputStream().read(litmus, 0, 5); + prependableSocket.prependToInputStream(litmus); if (bytesRead == 5 && SslHandler.isEncrypted(ChannelBuffers.wrappedBuffer(litmus))) { LOG.info(getInetAddress() + " attempting to connect over ssl"); SSLSocket sslSocket; try { - sslSocket = x509Util.createSSLSocket(bufferedSocket); + sslSocket = x509Util.createSSLSocket(prependableSocket); } catch (X509Exception e) { throw new IOException("failed to create SSL context", e); } @@ -75,7 +73,7 @@ public Socket accept() throws IOException { return sslSocket; } else { LOG.info(getInetAddress() + " attempting to connect without ssl"); - return bufferedSocket; + return prependableSocket; } } } \ No newline at end of file diff --git a/src/java/test/bin/test-github-pr.sh b/src/java/test/bin/test-github-pr.sh index 944ca8b8ec2..e155769f268 100755 --- a/src/java/test/bin/test-github-pr.sh +++ b/src/java/test/bin/test-github-pr.sh @@ -452,8 +452,8 @@ runCoreTests () { ### Kill any rogue build processes from the last attempt $PS auxwww | $GREP ZookeeperPatchProcess | /usr/bin/nawk '{print $2}' | /usr/bin/xargs -t -I {} /bin/kill -9 {} > /dev/null - echo "$ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=1 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME -Dtestcase=QuorumSSLTest -Djava.security.debug=certpath -Djavax.net.debug=ssl,handshake" - $ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=1 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME -Dtestcase=QuorumSSLTest -Djava.security.debug=certpath -Djavax.net.debug=ssl,handshake + echo "$ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=8 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME test-core" + $ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=8 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME test-core if [[ $? != 0 ]] ; then JIRA_COMMENT="$JIRA_COMMENT diff --git a/src/java/test/bin/test-patch.sh b/src/java/test/bin/test-patch.sh index aadd3b54759..fbc6037d542 100755 --- a/src/java/test/bin/test-patch.sh +++ b/src/java/test/bin/test-patch.sh @@ -476,9 +476,8 @@ runCoreTests () { ### Kill any rogue build processes from the last attempt $PS auxwww | $GREP ZookeeperPatchProcess | /usr/bin/nawk '{print $2}' | /usr/bin/xargs -t -I {} /bin/kill -9 {} > /dev/null - echo "$ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=1 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME test-core" - $ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=1 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME -Dtestcase=QuorumSSLTest -Djava.security.debug=certpath -Djavax.net.debug=ssl,handshake - test-core + echo "$ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=8 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME test-core" + $ANT_HOME/bin/ant -DZookeeperPatchProcess= -Dtest.junit.output.format=xml -Dtest.output=yes -Dtest.junit.threads=8 -Dcompile.c++=yes -Dforrest.home=$FORREST_HOME -Djava5.home=$JAVA5_HOME test-core if [[ $? != 0 ]] ; then JIRA_COMMENT="$JIRA_COMMENT From a95bb9232facc9e67646c9cad2610277fa2177ad Mon Sep 17 00:00:00 2001 From: Abraham Fine Date: Fri, 28 Apr 2017 12:51:06 -0700 Subject: [PATCH 25/25] ZOOKEEPER-2750: Document SSL Support for Atomic Broadcast protocol --- .../content/xdocs/zookeeperAdmin.xml | 164 +++++++++++++++++- 1 file changed, 162 insertions(+), 2 deletions(-) diff --git a/src/docs/src/documentation/content/xdocs/zookeeperAdmin.xml b/src/docs/src/documentation/content/xdocs/zookeeperAdmin.xml index 99407a92410..2389adbf142 100644 --- a/src/docs/src/documentation/content/xdocs/zookeeperAdmin.xml +++ b/src/docs/src/documentation/content/xdocs/zookeeperAdmin.xml @@ -1244,7 +1244,7 @@ server.3=zoo3:2888:3888 role="bold">zookeeper.ssl.keyStore.password) Specifies the file path to a JKS containing the local - credentials to be used for SSL connections, and the + credentials to be used for client to quorum SSL connections, and the password to unlock the file. @@ -1257,11 +1257,76 @@ server.3=zoo3:2888:3888 role="bold">zookeeper.ssl.trustStore.password) Specifies the file path to a JKS containing the remote - credentials to be used for SSL connections, and the + credentials to be used for client to quorum SSL connections, and the password to unlock the file. + + ssl.protocol + + (Java system properties: + zookeeper.ssl.protocol) + + Specifies the protocol to be used for client to quorum SSL connections. + Default value is TLSv1. You can find + the list of possible values here: + + Java Cryptography Architecture Standard Algorithm Name Documentation + + + + + ssl.ciphersuites + + (Java system properties: + zookeeper.ssl.ciphersuites) + + Comma separated list of cipher suites to use for client to quorum SSL connections. + You can find a list pof possible values here: + + Java Cryptography Architecture Standard Algorithm Name Documentation + + + + + ssl.hostnameVerification + + (Java system properties: + zookeeper.ssl.hostnameVerification) + + Specifies whether or not to perform hostname verification for client to quorum SSL connections. + ZooKeeper generally follows the behavior specified in + RFC 2818. + If matching cannot be performed via IP address, we do reverse DNS lookups. + Default value is true + + + + + ssl.crl + + (Java system properties: + zookeeper.ssl.crl) + + Specifies whether or not certificate revocation lists are checked when new SSL connections are + established between the ZooKeeper quorum and a client. + Default value is false + + + + + ssl.ocsp + + (Java system properties: + zookeeper.ssl.ocsp) + + Specifies whether or not the Online Certificate Status Protocol is used when new SSL connections are + established between the ZooKeeper quorum and a client. + Default value is false + + + ssl.authProvider @@ -1285,6 +1350,101 @@ server.3=zoo3:2888:3888 will be used for secure authentication. + + + ssl.quorum.keyStore.location and ssl.keyStore.password + + (Java system properties: + zookeeper.ssl.quorum.keyStore.location and zookeeper.ssl.quorum.keyStore.password) + + Specifies the file path to a JKS containing the local + credentials to be used for SSL connections within the ZooKeeper quorum, and the + password to unlock the file. + + + + + ssl.quorum.trustStore.location and ssl.trustStore.password + + (Java system properties: + zookeeper.ssl.quorum.trustStore.location and zookeeper.ssl.quorum.trustStore.password) + + Specifies the file path to a JKS containing the remote + credentials to be used for SSL connections within the ZooKeeper quorum, and the + password to unlock the file. + + + + + ssl.quorum.protocol + + (Java system properties: + zookeeper.ssl.quorum.protocol) + + Specifies the protocol to be used for SSL connections within the ZooKeeper quorum. + Default value is TLSv1. You can find + the list of possible values here: + + Java Cryptography Architecture Standard Algorithm Name Documentation + + + + + ssl.quorum.ciphersuites + + (Java system properties: + zookeeper.ssl.quorum.ciphersuites) + + Comma separated list of cipher suites to use for SSL connections within the ZooKeeper quorum. + You can find a list pof possible values here: + + Java Cryptography Architecture Standard Algorithm Name Documentation + + + + + ssl.quorum.hostnameVerification + + (Java system properties: + zookeeper.ssl.quorum.hostnameVerification) + + Specifies whether or not to perform hostname verification for SSL connections within the ZooKeeper quorum. + ZooKeeper generally follows the behavior specified in + RFC 2818. + With the exception that hostname verification is performed bidirectionally for the intra-quorum use case. + In other words, if server A is connecting to server B, server B verifies the hostname of server A and + server A verifies the hostname of server B. If matching cannot be performed via IP address, we do + reverse DNS lookups. + Default value is true + + + + + ssl.quorum.crl + + (Java system properties: + zookeeper.ssl.quorum.crl) + + Specifies whether or not certificate revocation lists are checked when new SSL connections are + established within the ZooKeeper quorum. + Default value is false + + + + + ssl.quorum.ocsp + + (Java system properties: + zookeeper.ssl.quorum.ocsp) + + Specifies whether or not the Online Certificate Status Protocol is used when new SSL connections are + established within the ZooKeeper quorum. + Default value is false + + +