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
author Severin Gehwolf <sgehwolf@redhat.com>
date Fri, 22 Mar 2013 19:57:11 +0100
parents 2dc4ebc08a77
children dc66dff085a1
files agent/command/src/main/java/com/redhat/thermostat/agent/command/internal/ConfigurationServerContext.java agent/command/src/main/java/com/redhat/thermostat/agent/command/internal/RequestDecoder.java agent/command/src/test/java/com/redhat/thermostat/agent/command/internal/RequestDecoderTest.java client/command/pom.xml client/command/src/main/java/com/redhat/thermostat/client/command/internal/ConfigurationRequestContext.java client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestEncoder.java client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestQueueImpl.java client/command/src/main/java/com/redhat/thermostat/client/command/internal/ResponseHandler.java client/command/src/main/java/com/redhat/thermostat/client/command/internal/SSLHandshakeFinishedListener.java client/command/src/test/java/com/redhat/thermostat/client/command/internal/RequestEncoderTest.java client/command/src/test/java/com/redhat/thermostat/client/command/internal/ResponseHandlerTest.java common/command/src/main/java/com/redhat/thermostat/common/command/Request.java common/command/src/test/java/com/redhat/thermostat/common/command/RequestTest.java common/core/src/main/java/com/redhat/thermostat/common/internal/DelegateSSLSocketFactory.java common/core/src/main/java/com/redhat/thermostat/common/internal/JSSEKeyManager.java common/core/src/main/java/com/redhat/thermostat/common/ssl/SSLContextFactory.java common/core/src/test/java/com/redhat/thermostat/common/internal/DelegateSSLSocketFactoryTest.java common/core/src/test/java/com/redhat/thermostat/common/internal/JSSEKeyManagerTest.java common/core/src/test/java/com/redhat/thermostat/common/ssl/SSLContextFactoryTest.java common/core/src/test/resources/cmdChanServer.keystore killvm/client-swing/src/test/java/com/redhat/thermostat/killvm/client/internal/SwingVMKilledListenerTest.java storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoConnection.java storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoConnectionTest.java
diffstat 23 files changed, 880 insertions(+), 91 deletions(-) [+]
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
+        }
+    }
+    
 }
 
Binary file common/core/src/test/resources/cmdChanServer.keystore has changed
--- 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();