changeset 823:0fe0860a96f2

Add TLS support to WebStorage. This patch adds TLS support to WebStorage. It works as follows: 1. If the storage URL starts with https a "https" scheme is registered for the underlying HttpClient. 2. In doing so a new SSLContext gets created which registers a X509TrustManager. This trust manager is used for validating server certificates by doing the following: 2.1. Retrieves a system trust manager. If found uses that first in order to validate the cert. 2.2. Failing 2.1 it attempts to uses a "Thermostat" trust manager retrieved from a custom keystore (if any). If it finds such trust manager, it uses it for another attempt to validate the server cert. 2.3. If all of the above fail WebStorage will throw an appropriate exception on connect() and refuses to start. 3. A new optional config file is introduced in $THERMOSTAT_HOME/etc "client.properties" which contains config for this Thermostat keystore. Its location and its password (if any). Test cases have been added for all of the above. Reviewed-by: rkennke Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2012-December/004408.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Wed, 05 Dec 2012 18:13:54 +0100
parents e236c83449c6
children 5905f7f8ca6d
files distribution/config/client.properties distribution/pom.xml storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/MongoStorageProvider.java web/client/src/main/java/com/redhat/thermostat/web/client/internal/CustomX509TrustManager.java web/client/src/main/java/com/redhat/thermostat/web/client/internal/SSLKeystoreConfiguration.java web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorageProvider.java web/client/src/test/java/com/redhat/thermostat/web/client/internal/CustomX509TrustManagerTest.java web/client/src/test/java/com/redhat/thermostat/web/client/internal/SSLKeystoreConfigurationTest.java web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java web/client/src/test/resources/ca.crt web/client/src/test/resources/client.properties web/client/src/test/resources/empty.keystore web/client/src/test/resources/test_ca.keystore
diffstat 14 files changed, 759 insertions(+), 44 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/distribution/config/client.properties	Wed Dec 05 18:13:54 2012 +0100
@@ -0,0 +1,12 @@
+# This file is only used in a Web storage setup and is entirely optional.
+# It allows for a user to specify a thermostat specific keystore to be used
+# for TLS certificate validation in addition to the default as described in:
+# http://docs.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#X509TrustManager
+
+# If you'd like to use a keystore file in addition to defaults uncomment the
+# following line:
+#KEYSTORE_FILE=/path/to/thermostat.keystore
+
+# The password for the keystore file. If none is provided the empty password
+# is assumed. Only used if KEYSTORE_FILE was specified.
+#KEYSTORE_PASSWORD=nopassword
\ No newline at end of file
--- a/distribution/pom.xml	Thu Dec 06 18:53:24 2012 +0100
+++ b/distribution/pom.xml	Wed Dec 05 18:13:54 2012 +0100
@@ -128,6 +128,14 @@
                 </resource>
                 <resource>
                   <directory>config</directory>
+                  <targetPath>etc</targetPath>
+                  <filtering>true</filtering>
+                  <includes>
+                    <include>client.properties</include>
+                  </includes>
+                </resource>
+                <resource>
+                  <directory>config</directory>
                   <targetPath>storage</targetPath>
                   <filtering>true</filtering>
                   <includes>
--- a/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/MongoStorageProvider.java	Thu Dec 06 18:53:24 2012 +0100
+++ b/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/MongoStorageProvider.java	Wed Dec 05 18:13:54 2012 +0100
@@ -36,9 +36,6 @@
 
 package com.redhat.thermostat.storage.mongodb;
 
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.FrameworkUtil;
-
 import com.redhat.thermostat.storage.config.StartupConfiguration;
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.StorageProvider;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/client/src/main/java/com/redhat/thermostat/web/client/internal/CustomX509TrustManager.java	Wed Dec 05 18:13:54 2012 +0100
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2012 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.web.client.internal;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import com.redhat.thermostat.common.utils.LoggingUtils;
+
+/**
+ * Custom X509TrustManager which first attempts to verify peer certificates
+ * using Java's default trust manager. If that fails it uses thermostat's
+ * keystore file (if any) and uses it for another attempt to validate the
+ * server certificate. If that fails as well, the connection is not allowed.
+ * 
+ */
+class CustomX509TrustManager implements X509TrustManager {
+    
+    Logger logger = LoggingUtils.getLogger(CustomX509TrustManager.class);
+
+    /*
+     * The default X509TrustManager returned by SunX509. We'll delegate
+     * decisions to it, and fall back to the logic in this class if the default
+     * X509TrustManager doesn't trust it.
+     */
+    private X509TrustManager defaultX509TrustManager;
+    private X509TrustManager ourX509TrustManager;
+
+    // For testing
+    CustomX509TrustManager(X509TrustManager defaultTrustManager,
+            X509TrustManager ourTrustManager) {
+        this.defaultX509TrustManager = defaultTrustManager;
+        this.ourX509TrustManager = ourTrustManager;
+    }
+    
+    // For testing
+    CustomX509TrustManager(X509TrustManager defaultTrustManager,
+            File keyStoreFile, String keyStorePassword) {
+        this.defaultX509TrustManager = defaultTrustManager;
+        this.ourX509TrustManager = getOurTrustManager(keyStoreFile, keyStorePassword);
+    }
+
+    // For testing
+    CustomX509TrustManager(File keyStoreFile, String keyStorePassword) {
+        this.defaultX509TrustManager = getDefaultTrustManager();
+        this.ourX509TrustManager = getOurTrustManager(keyStoreFile, keyStorePassword);
+    }
+
+    /*
+     * Main constructor. Others are used for testing.
+     */
+    CustomX509TrustManager() {
+        this(SSLKeystoreConfiguration.getKeystoreFile(), SSLKeystoreConfiguration.getKeyStorePassword());
+    }
+ 
+    private X509TrustManager getDefaultTrustManager() {
+        try {
+            TrustManagerFactory tmf = TrustManagerFactory.getInstance(
+                    "SunX509", "SunJSSE");
+            // Passing a null-KeyStore in order to get default Java behaviour.
+            // See:
+            // http://docs.oracle.com/javase/1.5.0/docs/guide/security/jsse/JSSERefGuide.html#X509TrustManager
+            tmf.init((KeyStore) null);
+
+            TrustManager tms[] = tmf.getTrustManagers();
+
+            /*
+             * Iterate over the returned trustmanagers, look for an instance of
+             * X509TrustManager. If found, use that as our "default" trust
+             * manager.
+             */
+            for (int i = 0; i < tms.length; i++) {
+                if (tms[i] instanceof X509TrustManager) {
+                    logger.log(Level.FINE, "Using system trust manager.");
+                    return (X509TrustManager) tms[i];
+                }
+            }
+        } catch (NoSuchAlgorithmException | NoSuchProviderException | KeyStoreException e) {
+            logger.log(Level.WARNING, "Could not retrieve system trust manager", e);
+        }
+        return null;
+    }
+    
+    /*
+     * If thermostat trust store file exists and a X509TrustManager can be
+     * retrieved from said trust store it returns this X509TrustManager. Returns
+     * null if an exception is thrown along the way, the keystore file does not
+     * exist or no X509TrustManager was found in the backing trust store.
+     */
+    private X509TrustManager getOurTrustManager(File trustStoreFile,
+            String keyStorePassword) {
+        KeyStore trustStore = null;
+        if (trustStoreFile != null && trustStoreFile.exists()) {
+            try {
+                trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
+                try (InputStream is = new FileInputStream(trustStoreFile)) {
+                    trustStore.load(is, keyStorePassword.toCharArray());
+                } catch (IOException | CertificateException
+                        | NoSuchAlgorithmException e) {
+                    logger.log(Level.WARNING,
+                            "Could not load Thermostat trust manager");
+                    return null;
+                }
+                TrustManagerFactory tmf = null;
+                tmf = TrustManagerFactory.getInstance("SunX509", "SunJSSE");
+                tmf.init(trustStore);
+
+                for (TrustManager tm : tmf.getTrustManagers()) {
+                    if (tm instanceof X509TrustManager) {
+                        logger.log(Level.FINE,
+                                "Using Thermostat trust manager.");
+                        return (X509TrustManager) tm;
+                    }
+                }
+            } catch (NoSuchAlgorithmException | NoSuchProviderException
+                    | KeyStoreException e) {
+                logger.log(Level.WARNING,
+                        "Could not load Thermostat trust manager");
+                return null;
+            }
+        }
+        logger.log(Level.FINE, "No Thermostat trust manager found");
+        return null;
+    }
+
+    @Override
+    public void checkClientTrusted(X509Certificate[] chain, String authType)
+            throws CertificateException {
+        // no-op: we don't support client authentication
+        logger.log(Level.INFO, "Checking client authentication: Allowing all client certificates!");
+    }
+
+    @Override
+    public void checkServerTrusted(X509Certificate[] chain, String authType)
+            throws CertificateException {
+        logger.log(Level.FINE, "Checking server certificate");
+        if (defaultX509TrustManager != null) {
+            try {
+                defaultX509TrustManager.checkServerTrusted(chain, authType);
+                logger.log(Level.FINE, "Server certificate check passed using system trust manager");
+                return;
+            } catch (CertificateException e) {
+                if (ourX509TrustManager != null) {
+                    // try our trust manager instead
+                    ourX509TrustManager.checkServerTrusted(chain, authType);
+                    logger.log(Level.FINE, "Server certificate check passed using Thermostat trust manager");
+                    return;
+                } else {
+                    // just rethrow
+                    logger.log(Level.WARNING, "Server certificate check FAILED!", e);
+                    throw e;
+                }
+            }
+        } else if (ourX509TrustManager != null) {
+            ourX509TrustManager.checkServerTrusted(chain, authType);
+            logger.log(Level.FINE, "Server certificate check passed using Thermostat trust manager");
+            return;
+        }
+        logger.log(Level.SEVERE, "Server certificate could not be checked. No trust managers found. Stopping now.");
+        // Default to not trusting this cert
+        throw new CertificateException("Certificate verification failed!");
+    }
+
+    /*
+     * Union of CA's trusted by default trust manager and the thermostat trust
+     * manager (if any).
+     * 
+     * (non-Javadoc)
+     * 
+     * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers()
+     */
+    @Override
+    public X509Certificate[] getAcceptedIssuers() {
+        Set<X509Certificate> trustedSet = new HashSet<>();
+        if (defaultX509TrustManager != null) {
+            trustedSet.addAll(Arrays.asList(defaultX509TrustManager
+                    .getAcceptedIssuers()));
+        }
+        if (ourX509TrustManager != null) {
+            trustedSet.addAll(Arrays.asList(ourX509TrustManager
+                    .getAcceptedIssuers()));
+        }
+        X509Certificate[] certs = trustedSet.toArray(new X509Certificate[0]);
+        return certs;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/client/src/main/java/com/redhat/thermostat/web/client/internal/SSLKeystoreConfiguration.java	Wed Dec 05 18:13:54 2012 +0100
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2012 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.web.client.internal;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+import com.redhat.thermostat.common.config.ConfigUtils;
+import com.redhat.thermostat.common.config.InvalidConfigurationException;
+
+public class SSLKeystoreConfiguration {
+
+    private static Properties clientProps = null;
+    private static final String KEYSTORE_FILE_KEY = "KEYSTORE_FILE";
+    private static final String KEYSTORE_FILE_PWD_KEY = "KEYSTORE_PASSWORD";
+
+    /**
+     * 
+     * @return The keystore file as specified in
+     *         $THERMOSTAT_HOME/client.properties if any. null otherwise.
+     */
+    public static File getKeystoreFile() {
+        try {
+            loadClientProperties();
+        } catch (InvalidConfigurationException e) {
+            // Thermostat home not set? Should have failed earlier. Do something
+            // reasonable.
+            return null;
+        }
+        String path = clientProps.getProperty(KEYSTORE_FILE_KEY);
+        if (path != null) {
+            File file = new File(path);
+            return file;
+        }
+        return null;
+    }
+
+    /**
+     * 
+     * @return The keystore file as specified in
+     *         $THERMOSTAT_HOME/client.properties if any. The empty string
+     *         otherwise.
+     * @throws InvalidConfigurationException
+     */
+    public static String getKeyStorePassword() {
+        try {
+            loadClientProperties();
+        } catch (InvalidConfigurationException e) {
+            // Thermostat home not set? Do something reasonable
+            return "";
+        }
+        String pwd = clientProps.getProperty(KEYSTORE_FILE_PWD_KEY);
+        if (pwd == null) {
+            return "";
+        } else {
+            return pwd;
+        }
+    }
+
+    // testing hook
+    static void initClientProperties(File clientPropertiesFile) {
+        clientProps = new Properties();
+        try {
+            clientProps.load(new FileInputStream(clientPropertiesFile));
+        } catch (IOException | IllegalArgumentException e) {
+            // Could not load client properties file. This is fine as it's
+            // an optional config after all.
+        }
+    }
+
+    private static void loadClientProperties()
+            throws InvalidConfigurationException {
+        if (clientProps == null) {
+            File thermostatEtcDir = new File(ConfigUtils.getThermostatHome(),
+                    "etc");
+            File clientPropertiesFile = new File(thermostatEtcDir,
+                    "client.properties");
+            initClientProperties(clientPropertiesFile);
+        }
+    }
+}
--- a/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java	Thu Dec 06 18:53:24 2012 +0100
+++ b/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java	Wed Dec 05 18:13:54 2012 +0100
@@ -34,7 +34,6 @@
  * to do so, delete this exception statement from your version.
  */
 
-
 package com.redhat.thermostat.web.client.internal;
 
 import java.io.Closeable;
@@ -46,12 +45,20 @@
 import java.lang.reflect.Array;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
 
 import org.apache.http.Header;
 import org.apache.http.HttpEntity;
@@ -64,6 +71,8 @@
 import org.apache.http.client.entity.UrlEncodedFormEntity;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.ssl.SSLSocketFactory;
 import org.apache.http.entity.mime.MultipartEntity;
 import org.apache.http.entity.mime.content.InputStreamBody;
 import org.apache.http.impl.client.DefaultHttpClient;
@@ -73,6 +82,8 @@
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import com.redhat.thermostat.common.utils.LoggingUtils;
+import com.redhat.thermostat.storage.config.StartupConfiguration;
 import com.redhat.thermostat.storage.core.Category;
 import com.redhat.thermostat.storage.core.Connection;
 import com.redhat.thermostat.storage.core.Cursor;
@@ -91,6 +102,9 @@
 
 public class WebStorage implements Storage {
 
+    private static final String HTTPS_PREFIX = "https";
+    final Logger logger = LoggingUtils.getLogger(WebStorage.class);
+
     private static class CloseableHttpEntity implements Closeable, HttpEntity {
 
         private HttpEntity entity;
@@ -101,11 +115,12 @@
 
         @Override
         public void consumeContent() throws IOException {
-            entity.consumeContent();
+            EntityUtils.consume(entity);
         }
 
         @Override
-        public InputStream getContent() throws IOException, IllegalStateException {
+        public InputStream getContent() throws IOException,
+                IllegalStateException {
             return entity.getContent();
         }
 
@@ -159,6 +174,7 @@
         WebConnection() {
             connected = true;
         }
+
         @Override
         public void disconnect() {
             connected = false;
@@ -171,8 +187,10 @@
                 initAuthentication(httpClient);
                 ping();
                 connected = true;
+                logger.fine("Connected to storage");
                 fireChanged(ConnectionStatus.CONNECTED);
             } catch (Exception ex) {
+                logger.log(Level.WARNING, "Could not connect to storage!", ex);
                 fireChanged(ConnectionStatus.FAILED_TO_CONNECT);
             }
         }
@@ -256,24 +274,53 @@
 
     private Map<Category, Integer> categoryIds;
     private Gson gson;
-    private DefaultHttpClient httpClient;
+    // package private for testing
+    DefaultHttpClient httpClient;
     private String username;
     private String password;
 
-    public WebStorage() {
+    public WebStorage(StartupConfiguration config) throws StorageException {
         categoryIds = new HashMap<>();
-        gson = new GsonBuilder().registerTypeHierarchyAdapter(Pojo.class, new ThermostatGSONConverter()).create();
+        gson = new GsonBuilder().registerTypeHierarchyAdapter(Pojo.class,
+                new ThermostatGSONConverter()).create();
         ClientConnectionManager connManager = new ThreadSafeClientConnManager();
         DefaultHttpClient client = new DefaultHttpClient(connManager);
         httpClient = client;
+        // setup SSL if necessary
+        if (config.getDBConnectionString().startsWith(HTTPS_PREFIX)) {
+            registerSSLScheme(connManager);
+        }
     }
 
-    private void initAuthentication(DefaultHttpClient client) throws MalformedURLException {
+    private void registerSSLScheme(ClientConnectionManager conManager)
+            throws StorageException {
+        try {
+            SSLContext sc = SSLContext.getInstance("TLS");
+            X509TrustManager ourTm;
+            try {
+                ourTm = new CustomX509TrustManager();
+            } catch (Exception e) {
+                throw new StorageException(e);
+            }
+            TrustManager[] tms = new TrustManager[] { ourTm };
+            sc.init(null, tms, new SecureRandom());
+            SSLSocketFactory socketFactory = new SSLSocketFactory(sc);
+            Scheme sch = new Scheme("https", 443, socketFactory);
+            conManager.getSchemeRegistry().register(sch);
+        } catch ( GeneralSecurityException e) {
+            throw new StorageException(e);
+        }
+    }
+
+    private void initAuthentication(DefaultHttpClient client)
+            throws MalformedURLException {
         if (username != null && password != null) {
             URL endpointURL = new URL(endpoint);
             // TODO: Maybe also limit to realm like 'Thermostat Realm' or such?
-            AuthScope scope = new AuthScope(endpointURL.getHost(), endpointURL.getPort());
-            Credentials creds = new UsernamePasswordCredentials(username, password);
+            AuthScope scope = new AuthScope(endpointURL.getHost(),
+                    endpointURL.getPort());
+            Credentials creds = new UsernamePasswordCredentials(username,
+                    password);
             client.getCredentialsProvider().setCredentials(scope, creds);
         }
     }
@@ -282,7 +329,8 @@
         post(endpoint + "/ping", (HttpEntity) null).close();
     }
 
-    private CloseableHttpEntity post(String url, List<NameValuePair> formparams) throws StorageException {
+    private CloseableHttpEntity post(String url, List<NameValuePair> formparams)
+            throws StorageException {
         try {
             return postImpl(url, formparams);
         } catch (IOException ex) {
@@ -290,7 +338,8 @@
         }
     }
 
-    private CloseableHttpEntity postImpl(String url, List<NameValuePair> formparams) throws IOException {
+    private CloseableHttpEntity postImpl(String url,
+            List<NameValuePair> formparams) throws IOException {
         HttpEntity entity;
         if (formparams != null) {
             entity = new UrlEncodedFormEntity(formparams, "UTF-8");
@@ -300,8 +349,8 @@
         return postImpl(url, entity);
     }
 
-    
-    private CloseableHttpEntity post(String url, HttpEntity entity) throws StorageException {
+    private CloseableHttpEntity post(String url, HttpEntity entity)
+            throws StorageException {
         try {
             return postImpl(url, entity);
         } catch (IOException ex) {
@@ -309,7 +358,8 @@
         }
     }
 
-    private CloseableHttpEntity postImpl(String url, HttpEntity entity) throws IOException {
+    private CloseableHttpEntity postImpl(String url, HttpEntity entity)
+            throws IOException {
         HttpPost httpPost = new HttpPost(url);
         if (entity != null) {
             httpPost.setEntity(entity);
@@ -338,10 +388,14 @@
 
     @Override
     public void registerCategory(Category category) throws StorageException {
-        NameValuePair nameParam = new BasicNameValuePair("name", category.getName());
-        NameValuePair categoryParam = new BasicNameValuePair("category", gson.toJson(category));
-        List<NameValuePair> formparams = Arrays.asList(nameParam, categoryParam);
-        try (CloseableHttpEntity entity = post(endpoint + "/register-category", formparams)) {
+        NameValuePair nameParam = new BasicNameValuePair("name",
+                category.getName());
+        NameValuePair categoryParam = new BasicNameValuePair("category",
+                gson.toJson(category));
+        List<NameValuePair> formparams = Arrays
+                .asList(nameParam, categoryParam);
+        try (CloseableHttpEntity entity = post(endpoint + "/register-category",
+                formparams)) {
             Reader reader = getContentAsReader(entity);
             Integer id = gson.fromJson(reader, Integer.class);
             categoryIds.put(category, id);
@@ -363,24 +417,32 @@
         return new WebUpdate(categoryIds);
     }
 
+    @SuppressWarnings("unchecked")
     @Override
-    public <T extends Pojo> Cursor<T> findAllPojos(Query query, Class<T> resultClass) throws StorageException {
+    public <T extends Pojo> Cursor<T> findAllPojos(Query query,
+            Class<T> resultClass) throws StorageException {
         ((WebQuery) query).setResultClassName(resultClass.getName());
-        NameValuePair queryParam = new BasicNameValuePair("query", gson.toJson(query));
+        NameValuePair queryParam = new BasicNameValuePair("query",
+                gson.toJson(query));
         List<NameValuePair> formparams = Arrays.asList(queryParam);
-        try (CloseableHttpEntity entity = post(endpoint + "/find-all", formparams)) {
+        try (CloseableHttpEntity entity = post(endpoint + "/find-all",
+                formparams)) {
             Reader reader = getContentAsReader(entity);
-            T[] result = (T[]) gson.fromJson(reader, Array.newInstance(resultClass, 0).getClass());
+            T[] result = (T[]) gson.fromJson(reader,
+                    Array.newInstance(resultClass, 0).getClass());
             return new WebCursor<T>(result);
         }
     }
 
     @Override
-    public <T extends Pojo> T findPojo(Query query, Class<T> resultClass) throws StorageException {
+    public <T extends Pojo> T findPojo(Query query, Class<T> resultClass)
+            throws StorageException {
         ((WebQuery) query).setResultClassName(resultClass.getName());
-        NameValuePair queryParam = new BasicNameValuePair("query", gson.toJson(query));
+        NameValuePair queryParam = new BasicNameValuePair("query",
+                gson.toJson(query));
         List<NameValuePair> formparams = Arrays.asList(queryParam);
-        try (CloseableHttpEntity entity = post(endpoint + "/find-pojo", formparams)) {
+        try (CloseableHttpEntity entity = post(endpoint + "/find-pojo",
+                formparams)) {
             Reader reader = getContentAsReader(entity);
             T result = gson.fromJson(reader, resultClass);
             return result;
@@ -399,9 +461,11 @@
 
     @Override
     public long getCount(Category category) throws StorageException {
-        NameValuePair categoryParam = new BasicNameValuePair("category", gson.toJson(categoryIds.get(category)));
+        NameValuePair categoryParam = new BasicNameValuePair("category",
+                gson.toJson(categoryIds.get(category)));
         List<NameValuePair> formparams = Arrays.asList(categoryParam);
-        try (CloseableHttpEntity entity = post(endpoint + "/get-count", formparams)) {
+        try (CloseableHttpEntity entity = post(endpoint + "/get-count",
+                formparams)) {
             Reader reader = getContentAsReader(entity);
             long result = gson.fromJson(reader, Long.class);
             return result;
@@ -422,23 +486,29 @@
     }
 
     @Override
-    public void putPojo(Category category, boolean replace, AgentIdPojo pojo) throws StorageException {
-        // TODO: This logic should probably be moved elsewhere. I.e. out of the Storage API.
+    public void putPojo(Category category, boolean replace, AgentIdPojo pojo)
+            throws StorageException {
+        // TODO: This logic should probably be moved elsewhere. I.e. out of the
+        // Storage API.
         if (pojo.getAgentId() == null) {
             pojo.setAgentId(getAgentId());
         }
 
         int categoryId = categoryIds.get(category);
-        WebInsert insert = new WebInsert(categoryId, replace, pojo.getClass().getName());
-        NameValuePair insertParam = new BasicNameValuePair("insert", gson.toJson(insert));
-        NameValuePair pojoParam = new BasicNameValuePair("pojo", gson.toJson(pojo));
+        WebInsert insert = new WebInsert(categoryId, replace, pojo.getClass()
+                .getName());
+        NameValuePair insertParam = new BasicNameValuePair("insert",
+                gson.toJson(insert));
+        NameValuePair pojoParam = new BasicNameValuePair("pojo",
+                gson.toJson(pojo));
         List<NameValuePair> formparams = Arrays.asList(insertParam, pojoParam);
         post(endpoint + "/put-pojo", formparams).close();
     }
 
     @Override
     public void removePojo(Remove remove) throws StorageException {
-        NameValuePair removeParam = new BasicNameValuePair("remove", gson.toJson(remove));
+        NameValuePair removeParam = new BasicNameValuePair("remove",
+                gson.toJson(remove));
         List<NameValuePair> formparams = Arrays.asList(removeParam);
         post(endpoint + "/remove-pojo", formparams).close();
     }
@@ -465,9 +535,12 @@
             values.add(updateValue.getValue());
         }
 
-        NameValuePair updateParam = new BasicNameValuePair("update", gson.toJson(update));
-        NameValuePair valuesParam = new BasicNameValuePair("values", gson.toJson(values));
-        List<NameValuePair> formparams = Arrays.asList(updateParam, valuesParam);
+        NameValuePair updateParam = new BasicNameValuePair("update",
+                gson.toJson(update));
+        NameValuePair valuesParam = new BasicNameValuePair("values",
+                gson.toJson(values));
+        List<NameValuePair> formparams = Arrays
+                .asList(updateParam, valuesParam);
         post(endpoint + "/update-pojo", formparams).close();
     }
 
--- a/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorageProvider.java	Thu Dec 06 18:53:24 2012 +0100
+++ b/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorageProvider.java	Wed Dec 05 18:13:54 2012 +0100
@@ -11,7 +11,7 @@
     
     @Override
     public Storage createStorage() {
-        WebStorage storage = new WebStorage();
+        WebStorage storage = new WebStorage(config);
         storage.setEndpoint(config.getDBConnectionString());
         if (config instanceof AuthenticationConfiguration) {
             AuthenticationConfiguration authConf = (AuthenticationConfiguration) config;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/client/src/test/java/com/redhat/thermostat/web/client/internal/CustomX509TrustManagerTest.java	Wed Dec 05 18:13:54 2012 +0100
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2012 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.web.client.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+
+import java.io.File;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.X509TrustManager;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.web.client.internal.CustomX509TrustManager;
+
+/**
+ * This trust manager test uses files in src/test/resources. Files are as
+ * follows:
+ * 
+ * empty.keystore => emtpy file (not a real keystore file)
+ * 
+ * ca.crt => a openssl generated X509 certificate (representing a custom CA)
+ * 
+ * test_ca.keystore => a Java keystore file with ca.crt imported. Uses keystore
+ * password "testpassword". Used command sequence to generate this file:
+ * 
+ * $ keytool -genkey -alias com.redhat -keyalg RSA -keystore test_ca.keystore -keysize 2048
+ * $ keytool -import -trustcacerts -alias root -file ca.crt -keystore test_ca.keystore
+ * 
+ * 
+ */
+public class CustomX509TrustManagerTest {
+
+    @Test
+    public void testEmptyDefaultOur() {
+        X509TrustManager tm = new CustomX509TrustManager(
+                (X509TrustManager) null, (X509TrustManager) null);
+        assertEquals(0, tm.getAcceptedIssuers().length);
+        try {
+            tm.checkClientTrusted(null, null);
+        } catch (Exception e) {
+            fail("Should not have thrown exception");
+        }
+        try {
+            tm.checkServerTrusted(null, null);
+            fail("Expected exception since there aren't any trust managers available");
+        } catch (CertificateException e) {
+            // pass
+        }
+    }
+
+    @Test
+    public void testLoadEmptyTrustStoreForOur() {
+        File emptyKeyStore = new File(this.getClass()
+                .getResource("/empty.keystore").getFile());
+        X509TrustManager tm = new CustomX509TrustManager(null, emptyKeyStore,
+                "");
+        assertEquals(0, tm.getAcceptedIssuers().length);
+        try {
+            tm.checkClientTrusted(null, null);
+        } catch (Exception e) {
+            fail("Should not have thrown exception");
+        }
+        try {
+            X509Certificate dummyCert = mock(X509Certificate.class);
+            tm.checkServerTrusted(new X509Certificate[] { dummyCert }, "RSA");
+            fail("Expected exception since there aren't any trust managers available");
+        } catch (CertificateException e) {
+            // pass
+        }
+    }
+
+    @Test
+    public void testLoadEmptyTrustStoreForOurDefaultAsUsual() throws Exception {
+        File emptyKeyStore = new File(this.getClass()
+                .getResource("/empty.keystore").getFile());
+        X509TrustManager tm = new CustomX509TrustManager(emptyKeyStore, "");
+        // Default list should not be empty
+        assertTrue(tm.getAcceptedIssuers().length > 0);
+        try {
+            tm.checkClientTrusted(null, null);
+        } catch (Exception e) {
+            fail("Should not have thrown exception");
+        }
+    }
+    
+    @Test
+    public void canGetCustomCaCertFromOurTrustManager() {
+        File ourKeyStore = new File(this.getClass()
+                .getResource("/test_ca.keystore").getFile());
+        X509TrustManager tm = new CustomX509TrustManager((X509TrustManager)null, ourKeyStore, "testpassword");
+        // keystore contains private key of itself + imported CA cert
+        assertEquals(2, tm.getAcceptedIssuers().length);
+        String issuerNameCustomCA = "1.2.840.113549.1.9.1=#16126a6572626f6161407265646861742e636f6d,CN=test.example.com,O=Red Hat Inc.,L=Saalfelden,ST=Salzburg,C=AT";
+        String issuerNameKeystoreCA = "CN=Unknown,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=Unknown";
+        assertEquals(issuerNameCustomCA, tm.getAcceptedIssuers()[0]
+                .getIssuerX500Principal().getName());
+        assertEquals(issuerNameKeystoreCA, tm.getAcceptedIssuers()[1]
+                .getIssuerX500Principal().getName());
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/client/src/test/java/com/redhat/thermostat/web/client/internal/SSLKeystoreConfigurationTest.java	Wed Dec 05 18:13:54 2012 +0100
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2012 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.web.client.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.web.client.internal.SSLKeystoreConfiguration;
+
+public class SSLKeystoreConfigurationTest {
+
+    @Test
+    public void canGetKeystoreFileFromProps() throws Exception {
+        File clientProps = new File(this.getClass().getResource("/client.properties").getFile());
+        SSLKeystoreConfiguration.initClientProperties(clientProps);
+        String keystorePath = "/path/to/thermostat.keystore";
+        String keystorePwd = "some password";
+        assertEquals(keystorePath, SSLKeystoreConfiguration.getKeystoreFile().getAbsolutePath());
+        assertEquals(keystorePwd, SSLKeystoreConfiguration.getKeyStorePassword());
+    }
+    
+    @Test
+    public void notExistingPropertiesFileReturnsNull() throws Exception {
+        File clientProps = new File("i/am/not/there/file.txt");
+        SSLKeystoreConfiguration.initClientProperties(clientProps);
+        assertTrue(SSLKeystoreConfiguration.getKeystoreFile() == null);
+        assertEquals("", SSLKeystoreConfiguration.getKeyStorePassword());
+    }
+}
--- a/web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java	Thu Dec 06 18:53:24 2012 +0100
+++ b/web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java	Wed Dec 05 18:13:54 2012 +0100
@@ -63,6 +63,9 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.apache.http.client.HttpClient;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
 import org.eclipse.jetty.server.Request;
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.handler.AbstractHandler;
@@ -76,19 +79,19 @@
 import com.google.gson.JsonArray;
 import com.google.gson.JsonParser;
 import com.google.gson.JsonSyntaxException;
+import com.redhat.thermostat.storage.config.StartupConfiguration;
 import com.redhat.thermostat.storage.core.Categories;
 import com.redhat.thermostat.storage.core.Category;
 import com.redhat.thermostat.storage.core.Cursor;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
+import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Remove;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.test.FreePortFinder;
 import com.redhat.thermostat.test.FreePortFinder.TryPort;
-import com.redhat.thermostat.web.client.internal.WebStorage;
 import com.redhat.thermostat.web.common.Qualifier;
+import com.redhat.thermostat.web.common.WebInsert;
 import com.redhat.thermostat.web.common.WebQuery;
-import com.redhat.thermostat.web.common.WebInsert;
 import com.redhat.thermostat.web.common.WebRemove;
 import com.redhat.thermostat.web.common.WebUpdate;
 
@@ -135,7 +138,14 @@
             }
         });
 
-        storage = new WebStorage();
+        StartupConfiguration config = new StartupConfiguration() {
+            
+            @Override
+            public String getDBConnectionString() {
+                return "http://fluff.example.org";
+            }
+        };
+        storage = new WebStorage(config);
         storage.setEndpoint("http://localhost:" + port + "/");
         storage.setAgentId(new UUID(123, 456));
         headers = new HashMap<>();
@@ -470,4 +480,21 @@
         assertEquals("POST", method);
         assertTrue(requestURI.endsWith("/purge"));
     }
+    
+    @Test
+    public void canSSLEnableClient() {
+        StartupConfiguration config = new StartupConfiguration() {
+            
+            @Override
+            public String getDBConnectionString() {
+                return "https://onlyHttpsPrefixUsed.example.com";
+            }
+        };
+        storage = new WebStorage(config);
+        HttpClient client = storage.httpClient;
+        SchemeRegistry schemeReg = client.getConnectionManager().getSchemeRegistry();
+        Scheme scheme = schemeReg.getScheme("https");
+        assertNotNull(scheme);
+        assertEquals(443, scheme.getDefaultPort());
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/client/src/test/resources/ca.crt	Wed Dec 05 18:13:54 2012 +0100
@@ -0,0 +1,34 @@
+-----BEGIN CERTIFICATE-----
+MIIF6TCCA9GgAwIBAgIJAMqGn7zFkpycMA0GCSqGSIb3DQEBBQUAMIGKMQswCQYD
+VQQGEwJBVDERMA8GA1UECAwIU2FsemJ1cmcxEzARBgNVBAcMClNhYWxmZWxkZW4x
+FTATBgNVBAoMDFJlZCBIYXQgSW5jLjEZMBcGA1UEAwwQdGVzdC5leGFtcGxlLmNv
+bTEhMB8GCSqGSIb3DQEJARYSamVyYm9hYUByZWRoYXQuY29tMB4XDTEyMTIwMzEz
+MTc1M1oXDTE3MTIwMzEzMTc1M1owgYoxCzAJBgNVBAYTAkFUMREwDwYDVQQIDAhT
+YWx6YnVyZzETMBEGA1UEBwwKU2FhbGZlbGRlbjEVMBMGA1UECgwMUmVkIEhhdCBJ
+bmMuMRkwFwYDVQQDDBB0ZXN0LmV4YW1wbGUuY29tMSEwHwYJKoZIhvcNAQkBFhJq
+ZXJib2FhQHJlZGhhdC5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+AQDKdOtT5HmVdFWj05aTqWQPX9L2NA44/69hwsrIwQFHi09DD+BwobaTmRpWg3Tg
+6qr+128AFlj2N8gYHyKELSxt1rsBV1yleTcpywfsmP7XkcZfu9ZKgmB4xyXuK4IQ
+EYxGqeWIdGYFNncjFm7Na3i9aTJZqtGxGf+kkQX2OdzqMbiXg17AYxH21kqMbNPk
+R2mhWUE4sT9Nfj+qO+iUqHCUMPusJY4xi/tGjLA9phih85ShYyqRBXXHiplzDCm+
+0W/puPodPVDo1FdfAwXExYaj8QoKmqCnGT9U++gMQ0b1xkRyuyImdqF9o63HoCMm
+8/R7ye0QTU8yUXwRoujvowSiip6/FAfADe7QyhHmScP9uS2/tNR0daCKUBnkWnJf
++PvbzNCT3V4MTwvg3LvphhU1JKXLSdQiZkSsLLKjnfUFE6BuT2RNFlWsbnuF2NcK
+H8ZDySDFL2LSeROPmi1lV2k+MScG2XOwKQXVQiU5GPgqNhhYvSClIk/kOagBoI74
+Ixq9hDW1viOHwKHZ315M3rNTg3Y4dq4eohUPP9iLmCFEBOW14MGDOMRKhzdFSUnG
+UBVdUiesqi3zwOSZ0EeqU2SgvmQKwvN1imWqsZlvkKV8p9mnFNmF/idbbkq2EWN0
+q/Hg7ekq/6mdN2f3XTGf0F85pTUtWWKpoyUM2qmZ72H+hQIDAQABo1AwTjAdBgNV
+HQ4EFgQUpknaZuFsL+AYna6uu5gSlKVUdbkwHwYDVR0jBBgwFoAUpknaZuFsL+AY
+na6uu5gSlKVUdbkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAgEAyAlS
+DJ+h4SO0mfZLU88JuRF8LoADD2u2eO890aIC1PtxIm9Osrfrc104LukCGpeLJvSZ
+hee/yZklen6K7lEURCgYmHreDtWx6zkf+qGdhqq0+97cfzvnSH8pDwVJxzY8dr+Q
+K2XSFOszkmVVNOnvu/GgHjcpOvXBNItnalrAqGgFi4cOGRzFl9x4iWUEMiUWERnP
+ntsisNsG4yY8D50Vl/k9qrU02IAsNQ+pnjDI+8bPni4aDE6nGH1+aNmS0J1YBM0s
+pPpK/W9GIxltARUfHI/mnvRNMCPeaG7ohJKBaIJkvibTa5Ux6zx1lNhJmDTHH4t+
+jMdoF5+90/AxYTnOQFb9oTPTX4+HgOhib9YtaxA7mXH5lVGFB19UrT7eFFNZeUFu
+Du+HOCPSrtTgLUdrkix+znl7ai4SORYFGL/v7QDw9U3FCGgRWuKzyQasvm3xELea
+3GXOuttDMWyfsAAsBWGpcJVN+YN23B75j1xN2b8JMvNNQChifWiFcztp7DsA1qxo
+M4g88l6WlRFm7OKqi9ZMbPH9/LlmvAhak5fS4l6UI08v/bX9qUEWNo1RzBZlHA4U
+3Xs9P3VF7h2pp0O3ucWm6nWAcp0CfvmaTnjof+cW8LDRHqEK0tWLEdBLg1TnUnnk
+EUsxCO6b/aOvribtT0aDLRB6sF8MTAtOBkczmp4=
+-----END CERTIFICATE-----
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/client/src/test/resources/client.properties	Wed Dec 05 18:13:54 2012 +0100
@@ -0,0 +1,2 @@
+KEYSTORE_FILE=/path/to/thermostat.keystore
+KEYSTORE_PASSWORD=some password
\ No newline at end of file
Binary file web/client/src/test/resources/empty.keystore has changed
Binary file web/client/src/test/resources/test_ca.keystore has changed