Mercurial > hg > release > thermostat-0.7
changeset 1051:77607020a4d3
TLS hardening and host name verification.
Reviewed-by: omajid, neugens
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-March/006214.html
line wrap: on
line diff
--- a/agent/command/src/main/java/com/redhat/thermostat/agent/command/internal/ConfigurationServerContext.java Wed Mar 06 10:48:34 2013 +0100 +++ b/agent/command/src/main/java/com/redhat/thermostat/agent/command/internal/ConfigurationServerContext.java Fri Mar 22 19:57:11 2013 +0100 @@ -40,6 +40,7 @@ import java.util.logging.Level; import java.util.logging.Logger; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import org.jboss.netty.bootstrap.Bootstrap; @@ -110,14 +111,15 @@ if (SSLKeystoreConfiguration.shouldSSLEnableCmdChannel()) { SSLEngine engine = null; try { - engine = SSLContextFactory.getServerContext() - .createSSLEngine(); + SSLContext ctxt = SSLContextFactory.getServerContext(); + engine = ctxt.createSSLEngine(); engine.setUseClientMode(false); } catch (SslInitException | InvalidConfigurationException e) { logger.log(Level.SEVERE, "Failed to initiate command channel endpoint", e); } pipeline.addLast("ssl", new SslHandler(engine)); + logger.log(Level.FINE, "Added SSL handler for command channel endpoint"); } pipeline.addLast("decoder", new RequestDecoder()); pipeline.addLast("encoder", new ResponseEncoder());
--- a/agent/command/src/main/java/com/redhat/thermostat/agent/command/internal/RequestDecoder.java Wed Mar 06 10:48:34 2013 +0100 +++ b/agent/command/src/main/java/com/redhat/thermostat/agent/command/internal/RequestDecoder.java Fri Mar 22 19:57:11 2013 +0100 @@ -36,6 +36,7 @@ package com.redhat.thermostat.agent.command.internal; +import java.net.InetSocketAddress; import java.util.logging.Level; import java.util.logging.Logger; @@ -68,7 +69,9 @@ buffer.resetReaderIndex(); throw new InvalidMessageException("Could not decode message: " + ChannelBuffers.hexDump(buffer)); } - Request request = new Request(RequestType.valueOf(typeAsString), channel.getRemoteAddress()); + // Netty javadoc tells us it's safe to downcast to more concrete type. + InetSocketAddress addr = (InetSocketAddress)channel.getRemoteAddress(); + Request request = new Request(RequestType.valueOf(typeAsString), addr); if (!DecodingHelper.decodeParameters(buffer, request)) { buffer.resetReaderIndex(); throw new InvalidMessageException("Could not decode message: " + ChannelBuffers.hexDump(buffer));
--- a/agent/command/src/test/java/com/redhat/thermostat/agent/command/internal/RequestDecoderTest.java Wed Mar 06 10:48:34 2013 +0100 +++ b/agent/command/src/test/java/com/redhat/thermostat/agent/command/internal/RequestDecoderTest.java Fri Mar 22 19:57:11 2013 +0100 @@ -36,7 +36,11 @@ package com.redhat.thermostat.agent.command.internal; -import java.net.SocketAddress; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import java.net.InetSocketAddress; import java.util.Collection; import org.jboss.netty.buffer.ChannelBuffer; @@ -45,17 +49,12 @@ import org.junit.Before; import org.junit.Test; -import com.redhat.thermostat.agent.command.internal.RequestDecoder; import com.redhat.thermostat.common.command.InvalidMessageException; import com.redhat.thermostat.common.command.Message; import com.redhat.thermostat.common.command.Messages; import com.redhat.thermostat.common.command.Request; import com.redhat.thermostat.common.command.Request.RequestType; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; - public class RequestDecoderTest { /* @@ -149,7 +148,7 @@ Message actual = new RequestDecoder().decode(channel, buffer); assertTrue(actual instanceof Request); assertTrue(Messages.equal(expected, (Request)actual)); - SocketAddress addr = mock(SocketAddress.class); + InetSocketAddress addr = new InetSocketAddress(1234); buffer = ChannelBuffers.copiedBuffer(ENCODED_REQUEST_WITH_NO_PARAMS); expected = new Request(RequestType.RESPONSE_EXPECTED, addr); actual = new RequestDecoder().decode(channel, buffer);
--- a/client/command/pom.xml Wed Mar 06 10:48:34 2013 +0100 +++ b/client/command/pom.xml Fri Mar 22 19:57:11 2013 +0100 @@ -107,6 +107,13 @@ <version>${project.version}</version> <scope>test</scope> </dependency> + <!-- Needed for Browser compatible hostname verification after SSL handshake + of cmd-channel --> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient-osgi</artifactId> + <version>${httpcomponents.version}</version> + </dependency> </dependencies>
--- a/client/command/src/main/java/com/redhat/thermostat/client/command/internal/ConfigurationRequestContext.java Wed Mar 06 10:48:34 2013 +0100 +++ b/client/command/src/main/java/com/redhat/thermostat/client/command/internal/ConfigurationRequestContext.java Fri Mar 22 19:57:11 2013 +0100 @@ -37,7 +37,10 @@ package com.redhat.thermostat.client.command.internal; import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import org.jboss.netty.bootstrap.Bootstrap; @@ -51,8 +54,11 @@ import com.redhat.thermostat.common.command.ConfigurationCommandContext; import com.redhat.thermostat.common.ssl.SSLContextFactory; import com.redhat.thermostat.common.ssl.SSLKeystoreConfiguration; +import com.redhat.thermostat.common.utils.LoggingUtils; public class ConfigurationRequestContext implements ConfigurationCommandContext { + + private static final Logger logger = LoggingUtils.getLogger(ConfigurationRequestContext.class); private final ClientBootstrap bootstrap; @@ -90,10 +96,15 @@ public ChannelPipeline getPipeline() throws Exception { ChannelPipeline pipeline = Channels.pipeline(); if (SSLKeystoreConfiguration.shouldSSLEnableCmdChannel()) { - SSLEngine engine = SSLContextFactory.getClientContext() - .createSSLEngine(); + SSLContext ctxt = SSLContextFactory.getClientContext(); + SSLEngine engine = ctxt.createSSLEngine(); engine.setUseClientMode(true); + // intentionally don't set the endpoint identification algo, + // since this doesn't seem to work for SSLEngine and nio. + // we do this manually once the hanshake finishes. + engine.setSSLParameters(SSLContextFactory.getSSLParameters(ctxt)); pipeline.addLast("ssl", new SslHandler(engine)); + logger.log(Level.FINE, "Added ssl handler for command channel client"); } pipeline.addLast("decoder", new ResponseDecoder()); pipeline.addLast("encoder", new RequestEncoder());
--- a/client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestEncoder.java Wed Mar 06 10:48:34 2013 +0100 +++ b/client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestEncoder.java Fri Mar 22 19:57:11 2013 +0100 @@ -64,7 +64,7 @@ // registered MessageEncoder is the one for Requests a cast // to Request should be safe. Request request = (Request) msg; - logger.log(Level.FINEST, "encoding Request object"); + logger.log(Level.FINEST, "encoding Request object " + request.toString()); // Request Type String requestType = EncodingHelper.trimType(request.getType()
--- a/client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestQueueImpl.java Wed Mar 06 10:48:34 2013 +0100 +++ b/client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestQueueImpl.java Fri Mar 22 19:57:11 2013 +0100 @@ -41,11 +41,11 @@ import java.util.logging.Level; import java.util.logging.Logger; + import org.apache.commons.codec.binary.Base64; import org.jboss.netty.bootstrap.ClientBootstrap; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFuture; -import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.handler.ssl.SslHandler; import org.osgi.framework.BundleContext; @@ -159,7 +159,7 @@ } - private void fireComplete(Request request, Response response) { + void fireComplete(Request request, Response response) { // TODO add more information once Response supports parameters. for (RequestResponseListener listener : request.getListeners()) { listener.fireComplete(request, response); @@ -176,27 +176,9 @@ ChannelFuture future = sslHandler.handshake(); // Register a future listener, since it gives us a way to - // report an error on client side. - future.addListener(new SSLErrorReporter(request)); - } - - private class SSLErrorReporter implements ChannelFutureListener { - - private final Request request; - - private SSLErrorReporter(Request request) { - this.request = request; - } - - @Override - public void operationComplete(ChannelFuture future) throws Exception { - if (!future.isSuccess()) { - logger.log(Level.WARNING, - "SSL handshake failed check agent logs for details!", - future.getCause()); - fireComplete(request, new Response(ResponseType.ERROR)); - } - } + // report an error on client side and to perform (optional) host name verification. + // FIXME: make hostname verification configurable + future.addListener(new SSLHandshakeFinishedListener(request, true, sslHandler, this)); } }
--- a/client/command/src/main/java/com/redhat/thermostat/client/command/internal/ResponseHandler.java Wed Mar 06 10:48:34 2013 +0100 +++ b/client/command/src/main/java/com/redhat/thermostat/client/command/internal/ResponseHandler.java Fri Mar 22 19:57:11 2013 +0100 @@ -76,6 +76,8 @@ logger.log(Level.WARNING, "exception caught: ", e.getCause()); Response response = new Response(ResponseType.ERROR); notifyListeners(response); + // Close broken channel. This is important, please keep! + e.getChannel().close(); } private void notifyListeners(Response response) {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/command/src/main/java/com/redhat/thermostat/client/command/internal/SSLHandshakeFinishedListener.java Fri Mar 22 19:57:11 2013 +0100 @@ -0,0 +1,115 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.client.command.internal; + +import java.net.InetSocketAddress; +import java.security.cert.X509Certificate; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; + +import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.handler.ssl.SslHandler; + +import com.redhat.thermostat.common.command.Request; +import com.redhat.thermostat.common.command.Response; +import com.redhat.thermostat.common.command.Response.ResponseType; +import com.redhat.thermostat.common.utils.LoggingUtils; + +/** + * Listener registered for SSL handshakes. + * + * @see RequestQueueImpl + * + */ +final class SSLHandshakeFinishedListener implements ChannelFutureListener { + + private final Request request; + private final boolean performHostNameChecking; + private final SslHandler handler; + private final RequestQueueImpl queueRunner; + private final Logger logger = LoggingUtils.getLogger(SSLHandshakeFinishedListener.class); + + SSLHandshakeFinishedListener(Request request, boolean performHostNameVerification, + SslHandler handler, RequestQueueImpl runner) { + this.request = request; + this.performHostNameChecking = performHostNameVerification; + this.handler = handler; + this.queueRunner = runner; + } + + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + logger.log(Level.WARNING, + "SSL handshake failed check agent logs for details!", + future.getCause()); + queueRunner.fireComplete(request, new Response(ResponseType.ERROR)); + } else { + if (performHostNameChecking) { + try { + doHostnameVerification(); + } catch (Exception e) { + future.setFailure(e); + future.removeListener(this); + future.getChannel().close(); + logger.log(Level.SEVERE, "Hostname verification failed!", e); + queueRunner.fireComplete(request, new Response(ResponseType.ERROR)); + } + } + } + } + + private void doHostnameVerification() throws SSLException { + SSLSession session = handler.getEngine().getSession(); + // First certificate is the peer. We only need this one for host name + // verification. + X509Certificate cert = (X509Certificate)session.getPeerCertificates()[0]; + // We use httpcomponents httpclient's hostname verifier since this one + // is well tested and supports domain wild-cards in certs and all these + // goodies. + BrowserCompatHostnameVerifier hostnameVerifier = new BrowserCompatHostnameVerifier(); + InetSocketAddress addr = request.getTarget(); + // Use getHostString in order to avoid reverse lookup. + // verify() throws SSLException if we fail to verify + hostnameVerifier.verify(addr.getHostString(), cert); + } +} \ No newline at end of file
--- a/client/command/src/test/java/com/redhat/thermostat/client/command/internal/RequestEncoderTest.java Wed Mar 06 10:48:34 2013 +0100 +++ b/client/command/src/test/java/com/redhat/thermostat/client/command/internal/RequestEncoderTest.java Fri Mar 22 19:57:11 2013 +0100 @@ -37,9 +37,8 @@ package com.redhat.thermostat.client.command.internal; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import java.net.SocketAddress; +import java.net.InetSocketAddress; import java.nio.charset.Charset; import org.jboss.netty.buffer.ChannelBuffer; @@ -64,7 +63,7 @@ buf = ChannelBuffers.buffer(4); buf.writeInt(0); ChannelBuffer expected = ChannelBuffers.wrappedBuffer(buf2, buf); - SocketAddress addr = mock(SocketAddress.class); + InetSocketAddress addr = new InetSocketAddress("testhost", 12); Request item = new Request(RequestType.RESPONSE_EXPECTED, addr); ChannelBuffer actual = encoder.encode(item); if (DEBUG) { @@ -75,7 +74,7 @@ @Test public void canEncodeRequestWithParams() throws Exception { - SocketAddress addr = mock(SocketAddress.class); + InetSocketAddress addr = new InetSocketAddress(1234); // Prepare request we'd like to encode Request item = new Request(RequestType.RESPONSE_EXPECTED, addr);
--- a/client/command/src/test/java/com/redhat/thermostat/client/command/internal/ResponseHandlerTest.java Wed Mar 06 10:48:34 2013 +0100 +++ b/client/command/src/test/java/com/redhat/thermostat/client/command/internal/ResponseHandlerTest.java Fri Mar 22 19:57:11 2013 +0100 @@ -39,6 +39,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; import java.util.ArrayList; import java.util.List; @@ -80,15 +81,19 @@ } @Test - public void exceptionCaughtCallsFireComplete() throws Exception { + public void exceptionCaughtCallsFireCompleteAndClosesChannel() throws Exception { Request req = mock(Request.class); List<RequestResponseListener> listeners = new ArrayList<>(); ResponseListenerFixture fixture = new ResponseListenerFixture(); listeners.add(fixture); when(req.getListeners()).thenReturn(listeners); ResponseHandler handler = new ResponseHandler(req); - handler.exceptionCaught(mock(ChannelHandlerContext.class), mock(ExceptionEvent.class)); + ExceptionEvent e = mock(ExceptionEvent.class); + Channel chan = mock(Channel.class); + when(e.getChannel()).thenReturn(chan); + handler.exceptionCaught(mock(ChannelHandlerContext.class), e); assertTrue(fixture.isCalled()); + verify(chan).close(); } private class ResponseListenerFixture implements RequestResponseListener {
--- a/common/command/src/main/java/com/redhat/thermostat/common/command/Request.java Wed Mar 06 10:48:34 2013 +0100 +++ b/common/command/src/main/java/com/redhat/thermostat/common/command/Request.java Fri Mar 22 19:57:11 2013 +0100 @@ -36,6 +36,7 @@ package com.redhat.thermostat.common.command; +import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Collection; import java.util.Collections; @@ -83,6 +84,8 @@ * */ public class Request implements Message { + + public static final String UNKNOWN_HOSTNAME = ""; public enum RequestType implements MessageType { NO_RESPONSE_EXPECTED, @@ -92,7 +95,7 @@ private final RequestType type; private final Map<String, String> parameters; - private final SocketAddress target; + private final InetSocketAddress target; private final Collection<RequestResponseListener> listeners; private static final String RECEIVER = "receiver"; @@ -100,7 +103,7 @@ public static final String CLIENT_TOKEN = "client-token"; public static final String AUTH_TOKEN = "auth-token"; - public Request(RequestType type, SocketAddress target) { + public Request(RequestType type, InetSocketAddress target) { this.type = type; parameters = new TreeMap<>(); this.target = target; @@ -132,7 +135,7 @@ return getParameter(RECEIVER); } - public SocketAddress getTarget() { + public InetSocketAddress getTarget() { return target; } @@ -147,5 +150,10 @@ public Collection<RequestResponseListener> getListeners() { return Collections.unmodifiableCollection(listeners); } + + @Override + public String toString() { + return "{ Request: {target = " + target.toString() + "}, {type = " + type.name() + "} }"; + } }
--- a/common/command/src/test/java/com/redhat/thermostat/common/command/RequestTest.java Wed Mar 06 10:48:34 2013 +0100 +++ b/common/command/src/test/java/com/redhat/thermostat/common/command/RequestTest.java Fri Mar 22 19:57:11 2013 +0100 @@ -45,9 +45,6 @@ import org.junit.Test; -import com.redhat.thermostat.common.command.Request; -import com.redhat.thermostat.common.command.RequestResponseListener; -import com.redhat.thermostat.common.command.Response; import com.redhat.thermostat.common.command.Request.RequestType; public class RequestTest { @@ -103,5 +100,22 @@ assertTrue(listeners.contains(listener2)); assertFalse(listeners.contains(listener1)); } + + @Test + public void canGetHostname() { + // unresolved hostname + InetSocketAddress addr = new InetSocketAddress(HOST, PORT); + assertTrue(addr.isUnresolved()); + Request request = new Request(RequestType.RESPONSE_EXPECTED, addr); + assertEquals(HOST, request.getTarget().getHostString()); + + } + + @Test + public void testToString() { + InetSocketAddress addr = new InetSocketAddress(1234); + Request request = new Request(RequestType.RESPONSE_EXPECTED, addr); + assertEquals("{ Request: {target = "+ addr.toString() + "}, {type = RESPONSE_EXPECTED} }", request.toString()); + } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/core/src/main/java/com/redhat/thermostat/common/internal/DelegateSSLSocketFactory.java Fri Mar 22 19:57:11 2013 +0100 @@ -0,0 +1,130 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.common.internal; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * This class allows us to configure the SSLSocket with the given parameters + * before the socket it returned to the consumer. + * + */ +public final class DelegateSSLSocketFactory extends SSLSocketFactory { + + private final SSLSocketFactory factoryDelegate; + private final SSLParameters params; + + public DelegateSSLSocketFactory(SSLSocketFactory delegate, SSLParameters params) { + this.factoryDelegate = delegate; + this.params = params; + } + @Override + public String[] getDefaultCipherSuites() { + return factoryDelegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return factoryDelegate.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + SSLSocket socket = (SSLSocket)factoryDelegate.createSocket(); + configureSocket(socket); + return socket; + } + + @Override + public Socket createSocket(Socket s, String host, int port, + boolean autoClose) throws IOException { + SSLSocket socket = (SSLSocket)factoryDelegate.createSocket(s, host, port, autoClose); + configureSocket(socket); + return socket; + } + + @Override + public Socket createSocket(String host, int port) throws IOException, + UnknownHostException { + SSLSocket socket = (SSLSocket)factoryDelegate.createSocket(host, port); + configureSocket(socket); + return socket; + } + + @Override + public Socket createSocket(String host, int port, + InetAddress localHost, int localPort) throws IOException, + UnknownHostException { + SSLSocket socket = (SSLSocket) factoryDelegate.createSocket(host, + port, localHost, localPort); + configureSocket(socket); + return socket; + } + + @Override + public Socket createSocket(InetAddress host, int port) + throws IOException { + SSLSocket socket = (SSLSocket) factoryDelegate.createSocket(host, + port); + configureSocket(socket); + return socket; + } + + @Override + public Socket createSocket(InetAddress address, int port, + InetAddress localAddress, int localPort) throws IOException { + SSLSocket socket = (SSLSocket) factoryDelegate.createSocket( + address, port, localAddress, localPort); + configureSocket(socket); + return socket; + } + + private void configureSocket(SSLSocket socket) throws IOException { + // Disable Nagle's algorithm + socket.setTcpNoDelay(true); + socket.setSSLParameters(params); + } + + +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/core/src/main/java/com/redhat/thermostat/common/internal/JSSEKeyManager.java Fri Mar 22 19:57:11 2013 +0100 @@ -0,0 +1,108 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.common.internal; + +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; + +import com.redhat.thermostat.common.utils.LoggingUtils; + +/** + * KeyManager for selecting the thermostat key-pair and certificate chain. + */ +public class JSSEKeyManager extends X509ExtendedKeyManager { + + private static final Logger logger = LoggingUtils.getLogger(JSSEKeyManager.class); + static final String THERMOSTAT_KEY_ALIAS = "thermostat"; + private X509KeyManager delegate; + + public JSSEKeyManager(X509KeyManager keymanager) { + this.delegate = keymanager; + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return delegate.getClientAliases(keyType, issuers); + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, + Socket socket) { + return delegate.chooseClientAlias(keyType, issuers, socket); + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return delegate.getServerAliases(keyType, issuers); + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, + Socket socket) { + logger.log(Level.FINE, "keyType: " + keyType); + return THERMOSTAT_KEY_ALIAS; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + logger.log(Level.FINE, "get private key for: " + alias); + return delegate.getCertificateChain(alias); + } + + @Override + public PrivateKey getPrivateKey(String alias) { + logger.log(Level.FINE, "get private key for: " + alias); + return delegate.getPrivateKey(alias); + } + + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, + SSLEngine engine) { + logger.log(Level.FINE, "choosing server engine alias"); + return THERMOSTAT_KEY_ALIAS; + } + + +}
--- a/common/core/src/main/java/com/redhat/thermostat/common/ssl/SSLContextFactory.java Wed Mar 06 10:48:34 2013 +0100 +++ b/common/core/src/main/java/com/redhat/thermostat/common/ssl/SSLContextFactory.java Fri Mar 22 19:57:11 2013 +0100 @@ -40,24 +40,38 @@ import java.security.GeneralSecurityException; import java.security.KeyManagementException; import java.security.KeyStore; +import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; +import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; +import javax.net.ssl.X509KeyManager; import com.redhat.thermostat.common.config.InvalidConfigurationException; +import com.redhat.thermostat.common.internal.JSSEKeyManager; import com.redhat.thermostat.common.internal.KeyStoreProvider; import com.redhat.thermostat.common.internal.TrustManagerFactory; +import com.redhat.thermostat.common.internal.DelegateSSLSocketFactory; import com.redhat.thermostat.common.utils.LoggingUtils; public class SSLContextFactory { private static final Logger logger = LoggingUtils.getLogger(SSLContextFactory.class); - private static final String PROTOCOL = "TLS"; + private static final String PROTOCOL_TLSv12 = "TLSv1.2"; + private static final String PROTOCOL_TLSv11 = "TLSv1.1"; + private static final String PROTOCOL_TLSv10 = "TLSv1"; + private static final String TLS_PROVIDER = "SunJSSE"; private static final String ALGORITHM = "SunX509"; private static SSLContext serverContext; private static SSLContext clientContext; @@ -90,16 +104,41 @@ initClientContext(); return clientContext; } + + public static SSLSocketFactory wrapSSLFactory(SSLSocketFactory socketFactory, SSLParameters params) { + return new DelegateSSLSocketFactory(socketFactory, params); + } + + public static SSLParameters getSSLParameters(SSLContext ctxt) { + SSLParameters params = ctxt.getDefaultSSLParameters(); + ArrayList<String> protocols = new ArrayList<String>( + Arrays.asList(params.getProtocols())); + // Do not send an SSL-2.0-compatible Client Hello. + protocols.remove("SSLv2Hello"); + params.setProtocols(protocols.toArray(new String[protocols.size()])); + ArrayList<String> ciphers = new ArrayList<String>(Arrays.asList(params + .getCipherSuites())); + ciphers.retainAll(Arrays + .asList("TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "SSL_RSA_WITH_3DES_EDE_CBC_SHA", + "SSL_RSA_WITH_RC4_128_SHA1", + "SSL_RSA_WITH_RC4_128_MD5", + "TLS_EMPTY_RENEGOTIATION_INFO_SCSV")); + params.setCipherSuites(ciphers.toArray(new String[ciphers.size()])); + return params; + } private static void initClientContext() throws SslInitException { SSLContext clientCtxt = null; try { - clientCtxt = SSLContext.getInstance(PROTOCOL); - TrustManager[] tms = new TrustManager[] { TrustManagerFactory - .getTrustManager() }; - clientCtxt.init(null, tms, new SecureRandom()); - } catch (NoSuchAlgorithmException | KeyManagementException e) { - e.printStackTrace(); + clientCtxt = getContextInstance(); + // Don't need key managers for client mode + clientCtxt.init(null, getTrustManagers(), new SecureRandom()); + } catch (KeyManagementException e) { + throw new SslInitException(e); } clientContext = clientCtxt; } @@ -120,19 +159,36 @@ "Failed to initialize server side SSL context"); } try { - // Set up key manager factory to use our key store - KeyManagerFactory kmf = KeyManagerFactory.getInstance(ALGORITHM); - String keystorePassword = SSLKeystoreConfiguration.getKeyStorePassword(); - kmf.init(ks, keystorePassword.toCharArray()); - - // Initialize the SSLContext to work with our key managers. - serverCtxt = SSLContext.getInstance(PROTOCOL); - serverCtxt.init(kmf.getKeyManagers(), null, new SecureRandom()); + serverCtxt = getContextInstance(); + // Initialize the SSLContext to work with our key and trust managers. + serverCtxt.init(getKeyManagers(ks, keyStorePassword), + getTrustManagers(), new SecureRandom()); } catch (GeneralSecurityException e) { throw new SslInitException(e); } serverContext = serverCtxt; } + + private static TrustManager[] getTrustManagers() { + TrustManager tm = TrustManagerFactory.getTrustManager(); + return new TrustManager[] { tm }; + } + + private static KeyManager[] getKeyManagers(KeyStore ks, String keystorePassword) + throws NoSuchAlgorithmException, UnrecoverableKeyException, + KeyStoreException, NoSuchProviderException { + // Set up key manager factory to use our key store + KeyManagerFactory kmf = KeyManagerFactory.getInstance(ALGORITHM, TLS_PROVIDER); + kmf.init(ks, keystorePassword.toCharArray()); + KeyManager[] rawKeyManagers = kmf.getKeyManagers(); + KeyManager kms[] = new KeyManager[rawKeyManagers.length]; + for (int i = 0; i < rawKeyManagers.length; i++) { + // Wrap with our keymanager, so that propperly aliased key is + // used in keystore. + kms[i] = new JSSEKeyManager((X509KeyManager)rawKeyManagers[i]); + } + return kms; + } private static void logReason(File trustStoreFile) { String detail = "Reason: no keystore file specified!"; @@ -145,5 +201,36 @@ } logger.log(Level.SEVERE, "Failed to load keystore. " + detail); } + + private static SSLContext getContextInstance() { + // Create the context. Specify the SunJSSE provider to avoid + // picking up third-party providers. Try the TLS 1.2 provider + // first, TLS 1.1 second and then fall back to TLS 1.0. + SSLContext ctx; + try { + ctx = SSLContext.getInstance(PROTOCOL_TLSv12, TLS_PROVIDER); + } catch (NoSuchAlgorithmException e) { + try { + ctx = SSLContext.getInstance(PROTOCOL_TLSv11, TLS_PROVIDER); + } catch (NoSuchAlgorithmException ex) { + try { + ctx = SSLContext.getInstance(PROTOCOL_TLSv10, TLS_PROVIDER); + } catch (NoSuchAlgorithmException exptn) { + // The TLS 1.0 provider should always be available. + throw new AssertionError(exptn); + } catch (NoSuchProviderException exptn) { + // The SunJSSE provider should always be available. + throw new AssertionError(exptn); + } + } catch (NoSuchProviderException ex) { + // The SunJSSE provider should always be available. + throw new AssertionError(e); + } + } catch (NoSuchProviderException e) { + // The SunJSSE provider should always be available. + throw new AssertionError(e); + } + return ctx; + } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/core/src/test/java/com/redhat/thermostat/common/internal/DelegateSSLSocketFactoryTest.java Fri Mar 22 19:57:11 2013 +0100 @@ -0,0 +1,141 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.common.internal; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.InetAddress; + +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class DelegateSSLSocketFactoryTest { + + private SSLSocketFactory mockDelegate; + private SSLSocket socket; + private SSLParameters params; + + @Before + public void setup() { + mockDelegate = mock(SSLSocketFactory.class); + socket = mock(SSLSocket.class); + params = mock(SSLParameters.class); + } + + @After + public void teardown() { + mockDelegate = null; + socket = null; + params = null; + } + + @Test + public void getDefaultCipherSuitesDelegates() { + DelegateSSLSocketFactory factory = new DelegateSSLSocketFactory(mockDelegate, params); + factory.getDefaultCipherSuites(); + verify(mockDelegate).getDefaultCipherSuites(); + verifyNoMoreInteractions(mockDelegate); + } + + @Test + public void getSupportedCipherSuitesDelegates() { + DelegateSSLSocketFactory factory = new DelegateSSLSocketFactory(mockDelegate, params); + factory.getSupportedCipherSuites(); + verify(mockDelegate).getSupportedCipherSuites(); + verifyNoMoreInteractions(mockDelegate); + } + + @Test + public void createSocketConfiguresSocket() throws IOException { + DelegateSSLSocketFactory factory = new DelegateSSLSocketFactory(mockDelegate, params); + when(mockDelegate.createSocket()).thenReturn(socket); + factory.createSocket(); + verify(socket).setSSLParameters(params); + } + + @Test + public void createSocketConfiguresSocket2() throws IOException { + DelegateSSLSocketFactory factory = new DelegateSSLSocketFactory(mockDelegate, params); + when(mockDelegate.createSocket(null, "blah", 2, true)).thenReturn(socket); + factory.createSocket(null, "blah", 2, true); + verify(socket).setSSLParameters(params); + } + + @Test + public void createSocketConfiguresSocket3() throws IOException { + DelegateSSLSocketFactory factory = new DelegateSSLSocketFactory(mockDelegate, params); + when(mockDelegate.createSocket("testhost.example.com", 2)).thenReturn(socket); + factory.createSocket("testhost.example.com", 2); + verify(socket).setSSLParameters(params); + } + + @Test + public void createSocketConfiguresSocket4() throws IOException { + DelegateSSLSocketFactory factory = new DelegateSSLSocketFactory(mockDelegate, params); + when(mockDelegate.createSocket("testhost.example.com", 2, null, 3)).thenReturn(socket); + factory.createSocket("testhost.example.com", 2, null, 3); + verify(socket).setSSLParameters(params); + } + + @Test + public void createSocketConfiguresSocket5() throws IOException { + DelegateSSLSocketFactory factory = new DelegateSSLSocketFactory(mockDelegate, params); + InetAddress addr = mock(InetAddress.class); + when(mockDelegate.createSocket(addr, 2)).thenReturn(socket); + factory.createSocket(addr, 2); + verify(socket).setSSLParameters(params); + } + + @Test + public void createSocketConfiguresSocket6() throws IOException { + DelegateSSLSocketFactory factory = new DelegateSSLSocketFactory(mockDelegate, params); + InetAddress addr = mock(InetAddress.class); + when(mockDelegate.createSocket(addr, 2, addr, 3)).thenReturn(socket); + factory.createSocket(addr, 2, addr, 3); + verify(socket).setSSLParameters(params); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/core/src/test/java/com/redhat/thermostat/common/internal/JSSEKeyManagerTest.java Fri Mar 22 19:57:11 2013 +0100 @@ -0,0 +1,99 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.common.internal; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import javax.net.ssl.X509KeyManager; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class JSSEKeyManagerTest { + + private X509KeyManager tm; + + @Before + public void setup() { + this.tm = mock(X509KeyManager.class); + } + + @After + public void teardown() { + this.tm = null; + } + + @Test + public void chooseServerAliasReturnsThermostat() { + JSSEKeyManager keyManager = new JSSEKeyManager(tm); + assertEquals(JSSEKeyManager.THERMOSTAT_KEY_ALIAS, + keyManager.chooseServerAlias(null, null, null)); + } + + @Test + public void testKeyAliasIsThermostat() { + // In documentation we tell our users that the keyalias for the + // agent server key has to be thermostat. + // See: http://icedtea.classpath.org/wiki/Thermostat/SecurityConsiderations + assertEquals(JSSEKeyManager.THERMOSTAT_KEY_ALIAS, "thermostat"); + } + + @Test + public void chooseEngineServerAliasReturnsThermostatAlias() { + JSSEKeyManager keyManager = new JSSEKeyManager(tm); + assertEquals(JSSEKeyManager.THERMOSTAT_KEY_ALIAS, + keyManager.chooseEngineServerAlias(null, null, null)); + } + + @Test + public void otherMethodsDelegate() { + JSSEKeyManager keyManager = new JSSEKeyManager(tm); + keyManager.chooseClientAlias(null, null, null); + verify(tm).chooseClientAlias(null, null, null); + keyManager.getCertificateChain("blah"); + verify(tm).getCertificateChain("blah"); + keyManager.getClientAliases(null, null); + verify(tm).getClientAliases(null, null); + keyManager.getPrivateKey("test"); + verify(tm).getPrivateKey("test"); + keyManager.getServerAliases("something", null); + verify(tm).getServerAliases("something", null); + } +}
--- a/common/core/src/test/java/com/redhat/thermostat/common/ssl/SSLContextFactoryTest.java Wed Mar 06 10:48:34 2013 +0100 +++ b/common/core/src/test/java/com/redhat/thermostat/common/ssl/SSLContextFactoryTest.java Fri Mar 22 19:57:11 2013 +0100 @@ -37,19 +37,22 @@ package com.redhat.thermostat.common.ssl; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.File; -import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; import org.junit.Test; import org.junit.runner.RunWith; @@ -58,10 +61,11 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import com.redhat.thermostat.common.internal.TrustManagerFactory; + @RunWith(PowerMockRunner.class) -@PrepareForTest({ SSLKeystoreConfiguration.class, SSLContext.class, - KeyManagerFactory.class }) +@PrepareForTest({ SSLKeystoreConfiguration.class, SSLContext.class, KeyManagerFactory.class }) public class SSLContextFactoryTest { /* @@ -88,26 +92,30 @@ PowerMockito.mockStatic(SSLContext.class); SSLContext context = PowerMockito.mock(SSLContext.class); - when(SSLContext.getInstance("TLS")).thenReturn(context); - KeyManagerFactory factory = PowerMockito.mock(KeyManagerFactory.class); + when(SSLContext.getInstance("TLSv1.2", "SunJSSE")).thenReturn(context); + ArgumentCaptor<KeyManager[]> keymanagersCaptor = ArgumentCaptor + .forClass(KeyManager[].class); + ArgumentCaptor<TrustManager[]> tmsCaptor = ArgumentCaptor + .forClass(TrustManager[].class); PowerMockito.mockStatic(KeyManagerFactory.class); - when(KeyManagerFactory.getInstance("SunX509")).thenReturn(factory); - - ArgumentCaptor<KeyStore> keystoreArgCaptor = ArgumentCaptor - .forClass(KeyStore.class); - ArgumentCaptor<char[]> pwdCaptor = ArgumentCaptor - .forClass(char[].class); + KeyManagerFactory mockFactory = PowerMockito.mock(KeyManagerFactory.class); + when(KeyManagerFactory.getInstance("SunX509", "SunJSSE")).thenReturn(mockFactory); + KeyManager[] mockKms = new KeyManager[] { mock(X509KeyManager.class) }; + when(mockFactory.getKeyManagers()).thenReturn(mockKms); SSLContextFactory.getServerContext(); - verify(context).init(any(KeyManager[].class), - any(TrustManager[].class), any(SecureRandom.class)); - verify(factory).init(keystoreArgCaptor.capture(), pwdCaptor.capture()); - verify(factory).getKeyManagers(); - - KeyStore keystore = keystoreArgCaptor.getValue(); - String password = new String(pwdCaptor.getValue()); - assertNotNull(keystore); - assertNotNull(password); - assertEquals("testpassword", password); + verify(context).init(keymanagersCaptor.capture(), + tmsCaptor.capture(), any(SecureRandom.class)); + KeyManager[] kms = keymanagersCaptor.getValue(); + assertEquals(1, kms.length); + // Keymanagers should be wrapped by JSSEKeyManager + assertEquals( + "com.redhat.thermostat.common.internal.JSSEKeyManager", + kms[0].getClass().getName()); + TrustManager[] tms = tmsCaptor.getValue(); + assertEquals(1, tms.length); + assertEquals( + "com.redhat.thermostat.common.internal.CustomX509TrustManager", + tms[0].getClass().getName()); } @Test @@ -124,7 +132,7 @@ PowerMockito.mockStatic(SSLContext.class); SSLContext context = PowerMockito.mock(SSLContext.class); - when(SSLContext.getInstance("TLS")).thenReturn(context); + when(SSLContext.getInstance("TLSv1.2", "SunJSSE")).thenReturn(context); ArgumentCaptor<TrustManager[]> tmsCaptor = ArgumentCaptor .forClass(TrustManager[].class); @@ -136,5 +144,62 @@ assertEquals(tms[0].getClass().getName(), "com.redhat.thermostat.common.internal.CustomX509TrustManager"); } + + @SuppressWarnings("unchecked") + @Test + @PrepareForTest({TrustManagerFactory.class, SSLContext.class}) + public void verifyTLSVersionFallsBackProperlyToTLS11() throws Exception { + PowerMockito.mockStatic(SSLContext.class); + when(SSLContext.getInstance("TLSv1.2", "SunJSSE")).thenThrow( + NoSuchAlgorithmException.class); + SSLContext context = PowerMockito.mock(SSLContext.class); + when(SSLContext.getInstance("TLSv1.1", "SunJSSE")).thenReturn(context); + PowerMockito.mockStatic(TrustManagerFactory.class); + X509TrustManager tm = PowerMockito.mock(X509TrustManager.class); + when(TrustManagerFactory.getTrustManager()).thenReturn(tm); + SSLContextFactory.getClientContext(); + verify(context).init(any(KeyManager[].class), + any(TrustManager[].class), any(SecureRandom.class)); + } + + @SuppressWarnings("unchecked") + @Test + @PrepareForTest({TrustManagerFactory.class, SSLContext.class}) + public void verifyTLSVersionFallsBackProperlyToTLS10() throws Exception { + PowerMockito.mockStatic(SSLContext.class); + when(SSLContext.getInstance("TLSv1.2", "SunJSSE")).thenThrow( + NoSuchAlgorithmException.class); + SSLContext context = PowerMockito.mock(SSLContext.class); + when(SSLContext.getInstance("TLSv1.1", "SunJSSE")).thenThrow( + NoSuchAlgorithmException.class); + when(SSLContext.getInstance("TLSv1", "SunJSSE")).thenReturn(context); + PowerMockito.mockStatic(TrustManagerFactory.class); + X509TrustManager tm = PowerMockito.mock(X509TrustManager.class); + when(TrustManagerFactory.getTrustManager()).thenReturn(tm); + SSLContextFactory.getClientContext(); + verify(context).init(any(KeyManager[].class), + any(TrustManager[].class), any(SecureRandom.class)); + } + + @SuppressWarnings("unchecked") + @Test + @PrepareForTest({TrustManagerFactory.class, SSLContext.class}) + public void throwAssertionErrorIfNoReasonableTlsAvailable() + throws Exception { + PowerMockito.mockStatic(SSLContext.class); + when(SSLContext.getInstance("TLSv1.2", "SunJSSE")).thenThrow( + NoSuchAlgorithmException.class); + when(SSLContext.getInstance("TLSv1.1", "SunJSSE")).thenThrow( + NoSuchAlgorithmException.class); + when(SSLContext.getInstance("TLSv1", "SunJSSE")).thenThrow( + NoSuchAlgorithmException.class); + try { + SSLContextFactory.getClientContext(); + fail("No suitable algos available, which should trigger AssertionError"); + } catch (AssertionError e) { + // pass + } + } + }
--- a/killvm/client-swing/src/test/java/com/redhat/thermostat/killvm/client/internal/SwingVMKilledListenerTest.java Wed Mar 06 10:48:34 2013 +0100 +++ b/killvm/client-swing/src/test/java/com/redhat/thermostat/killvm/client/internal/SwingVMKilledListenerTest.java Fri Mar 22 19:57:11 2013 +0100 @@ -41,14 +41,13 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import java.net.SocketAddress; +import java.net.InetSocketAddress; import org.junit.Test; import com.redhat.thermostat.common.command.Request; import com.redhat.thermostat.common.command.Response; import com.redhat.thermostat.common.command.Response.ResponseType; -import com.redhat.thermostat.killvm.client.internal.SwingVMKilledListener; public class SwingVMKilledListenerTest { @@ -65,7 +64,7 @@ public void okShowsNoMessage() { ResponseActionListener listener = new ResponseActionListener(); Request request = mock(Request.class); - SocketAddress addr = mock(SocketAddress.class); + InetSocketAddress addr = new InetSocketAddress(1234); when(request.getTarget()).thenReturn(addr); Response resp = new Response(ResponseType.OK); listener.fireComplete(request, resp);
--- a/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoConnection.java Wed Mar 06 10:48:34 2013 +0100 +++ b/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoConnection.java Fri Mar 22 19:57:11 2013 +0100 @@ -42,6 +42,8 @@ import java.util.logging.Logger; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocketFactory; import com.mongodb.DB; import com.mongodb.Mongo; @@ -139,7 +141,14 @@ logger.log(Level.WARNING, "Failed to get SSL context!", e); throw new MongoException(e.getMessage(), e); } - opts.socketFactory = ctxt.getSocketFactory(); + SSLParameters params = SSLContextFactory.getSSLParameters(ctxt); + // Perform HTTPS compatible host name checking. + // FIXME: make hostname verification configurable + params.setEndpointIdentificationAlgorithm("HTTPS"); + SSLSocketFactory factory = SSLContextFactory.wrapSSLFactory( + ctxt.getSocketFactory(), params); + logger.log(Level.FINE, "factory is: " + factory.getClass().getName()); + opts.socketFactory = factory; return new Mongo(getServerAddress(), opts); }
--- a/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoConnectionTest.java Wed Mar 06 10:48:34 2013 +0100 +++ b/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoConnectionTest.java Fri Mar 22 19:57:11 2013 +0100 @@ -50,6 +50,7 @@ import java.net.UnknownHostException; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSocketFactory; import org.junit.After; @@ -176,7 +177,9 @@ SSLContext context = PowerMockito.mock(SSLContext.class); when(SSLContextFactory.getClientContext()).thenReturn(context); SSLSocketFactory factory = PowerMockito.mock(SSLSocketFactory.class); - when(context.getSocketFactory()).thenReturn(factory); + when(SSLContextFactory.wrapSSLFactory(any(SSLSocketFactory.class), any(SSLParameters.class))).thenReturn(factory); + SSLParameters params = mock(SSLParameters.class); + when(SSLContextFactory.getSSLParameters(context)).thenReturn(params); Mongo mockMongo = mock(Mongo.class); ArgumentCaptor<MongoOptions> mongoOptCaptor = ArgumentCaptor.forClass(MongoOptions.class); whenNew(Mongo.class).withParameterTypes(ServerAddress.class, @@ -187,6 +190,7 @@ DBCollection mockCollection = mock(DBCollection.class); when(mockDb.getCollection(any(String.class))).thenReturn(mockCollection); conn.connect(); + verify(params).setEndpointIdentificationAlgorithm("HTTPS"); Mongo mongo = conn.getMongo(); assertEquals(mockMongo, mongo); MongoOptions opts = mongoOptCaptor.getValue();