SSL trust and client authentication support - Mailing list pgsql-jdbc

From Neil Macneale
Subject SSL trust and client authentication support
Date
Msg-id 43FD07BC.3040305@theory.org
Whole thread Raw
List pgsql-jdbc
Hello,

I've attached a few files which add more robust SSL behavior for pgjdbc.
The patch is against the current head of the CVS tree, and it contains
changes to MakeSSL, and provides a new SSLSocketFactory class called
ValidatingFactory.

Also attached are two classes which are examples of how to use the
validating factory. One example shows how you could verify the validity
of the server connection, while providing no client authentication. The
second verifies the identity of the server, and provides a client
certificate to the server for validation. Also in header comments of
those files are some examples of how to use the keytool command to
create client keystores.

Currently the ValidatingFactory throws an Exception for any error. This
in turn results in an invokation exception being thrown in MakeSSL. It's
not ideal, but the error messages are generally complete enough to
diagnose your problem.

There is one limitation worth mentioning. This code assumes that your
subkeys use the same password as your keystore. This is primarily
because the KeyManagerFactory does not provide a mechanism to supply a
password for each alias. I'm not sure that there is a work around for
this. More investigation is required.

Hopefully this will provide a decent starting point for SSL support for
pgjdbc. I'd be happy to answer any questions you may have.

Enjoy!
Neil
### Eclipse Workspace Patch 1.0
#P pgjdbc
Index: org/postgresql/ssl/MakeSSL.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/ssl/MakeSSL.java,v
retrieving revision 1.5
diff -u -r1.5 MakeSSL.java
--- org/postgresql/ssl/MakeSSL.java    24 Nov 2005 02:29:22 -0000    1.5
+++ org/postgresql/ssl/MakeSSL.java    23 Feb 2006 00:50:55 -0000
@@ -50,8 +50,14 @@
                 }
                 catch (NoSuchMethodException nsme)
                 {
-                    ctor = factoryClass.getConstructor((Class[])null);
-                    args = null;
+                                try
+                                {
+                                        ctor = factoryClass.getConstructor((Class[])null);
+                                        args = null;
+                                } catch (NoSuchMethodException nsme2){
+                                        ctor = factoryClass.getConstructor(new Class[]{Properties.class});
+                                        args[0] = info;
+                                }
                 }
                 factory = (SSLSocketFactory)ctor.newInstance(args);
             }
@@ -60,7 +66,7 @@
                 throw new PSQLException(GT.tr("The SSLSocketFactory class provided {0} could not be instantiated.",
classname),PSQLState.CONNECTION_FAILURE, e); 
             }
         }
-
+
         Socket newConnection = factory.createSocket(stream.getSocket(), stream.getHost(), stream.getPort(), true);
         stream.changeSocket(newConnection);
     }
Index: org/postgresql/ssl/ValidatingFactory.java
===================================================================
RCS file: org/postgresql/ssl/ValidatingFactory.java
diff -N org/postgresql/ssl/ValidatingFactory.java
--- /dev/null    1 Jan 1970 00:00:00 -0000
+++ org/postgresql/ssl/ValidatingFactory.java    1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,371 @@
+package org.postgresql.ssl;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.util.Arrays;
+import java.util.Properties;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+
+public class ValidatingFactory extends WrappedFactory {
+
+    /**
+     * Template string for the property name which indicates the name of a
+     * key/trust store file.
+     */
+    private static final String SSL__FILE = "ssl._.file";
+
+    /**
+     * Template string for the property name which indicates the type of
+     * key/trust store to instanciate.
+     */
+    private static final String SSL__TYPE = "ssl._.type";
+
+    /**
+     * Template String for the property name which indicates a password to a
+     * key/trust store.
+     */
+    private static final String SSL__PASSWORD = "ssl._.password";
+
+    /**
+     * Template String for the property name which indicates the algorithm to
+     * use when instanciateing a Key/TrustManagerFactory. This is somewhat
+     * redundant since the default algorithm may be set via your security
+     * provider.
+     */
+    private static final String SSL__ALGORITHM = "ssl._.algorithm";
+
+    /**
+     * String used to differenciate a keystore from a truststore in the Property
+     * templates.
+     */
+    private static final String KEYSTORE = "keystore";
+
+    /**
+     * Property used to indicate the keystore file. If this property does
+     * not exist in the Properties object passed to the consturctor, then the
+     * file System.getProperty("user.home")/postgresql/.postgresql.jks will be
+     * used instead.
+     */
+    public static final String SSL_KEYSTORE_FILE = SSL__FILE.replaceFirst("_",
+            KEYSTORE);
+
+    /**
+     * Property used to indicate the type of keystore which will be loaded. The
+     * supported values depend on your security provider. KeyStore.getDefaultType()
+     * is used, if this property is not present.
+     *
+     * @see KeyStore#getDefaultType()
+     */
+    public static final String SSL_KEYSTORE_TYPE = SSL__TYPE.replaceFirst("_",
+            KEYSTORE);
+
+    /**
+     * Property used to indicate the password which will be used to load the key
+     * store. If this option is not present, then an empty password will be
+     * used. Note that some keystore tools will not allow you to create a keystore
+     * without a password.
+     */
+    public static final String SSL_KEYSTORE_PASSWORD = SSL__PASSWORD
+            .replaceFirst("_", KEYSTORE);
+
+    /**
+     * Property used to indicate the algorithm which should be used when
+     * creating the KeyManagerFactory, which will in turn supply the KeyManagers
+     * to the SSLContext. If this property is not present, then
+     * KeyManagerFactory.getDefaultAlgorithm will be used instead.
+     *
+     * @see KeyManagerFactory#getDefaultAlgorithm()
+     */
+    public static final String SSL_KEYSTORE_ALGORITHM = SSL__ALGORITHM
+            .replaceFirst("_", KEYSTORE);
+
+    /**
+     * The property name used to indicate that the default JRE key managers
+     * should be used. The existance of this property in the Properties object
+     * passed to the constuctor indicates that the default should be used, and
+     * the value is ignored.
+     * <p>
+     * This option will have the affect of passing a null argument to the
+     * SSLContext.init method. This behavior is useful if you are connecting to
+     * a server which does not require client authentication.
+     * <p>
+     * All other SSL_KEYSTORE_* options will be ignored if this option is set.
+     *
+     * @see SSLContext#init(KeyManager[], TrustManager[],
+     *      java.security.SecureRandom)
+     */
+    public static final String SSL_USE_DEFAULT_KEY_MANAGER = "ssl.use.default.key.manager";
+
+    /**
+     * String used to differenciate a truststore from a keystore in the Property
+     * templates.
+     */
+    private static final String TRUSTSTORE = "truststore";
+
+    /**
+     * Property used to indicate the struststore file. If this property does
+     * not exist in the Properties object passed to the consturctor, then the
+     * file System.getProperty("user.home")/postgresql/.postgresql.jks will be
+     * used instead.
+     */
+    public static final String SSL_TRUSTSTORE_FILE = SSL__FILE.replaceFirst(
+            "_", TRUSTSTORE);
+
+    /**
+     * Property used to indicate the type of keystore which will be loaded. The
+     * supported values depend on your security provider. KeyStore.getDefaultTYpe
+     * is used, if this property is not present.
+     *
+     * @see KeyStore#getDefaultType()
+     */
+    public static final String SSL_TRUSTSTORE_TYPE = SSL__TYPE.replaceFirst(
+            "_", TRUSTSTORE);
+
+    /**
+     * Property used to indicate the password which will be used to load the key
+     * store. If this option is not present, then an empty password will be
+     * used. A trust store commonly does not contain secure information, so
+     * it is likely that this option is not required.
+     */
+    public static final String SSL_TRUSTSTORE_PASSWORD = SSL__PASSWORD
+            .replaceFirst("_", TRUSTSTORE);
+
+    /**
+     * Property used to indicate the algorithm which should be used when
+     * creating the TrustManagerFactory, which will in turn supply the TrustManagers
+     * to the SSLContext. If this property is not present, then
+     * TrustManagerFactory.getDefaultAlgorithm will be used instead.
+     *
+     * @see TrustManagerFactory#getDefaultAlgorithm()
+     */
+    public static final String SSL_TRUSTSTORE_ALGORITHM = SSL__ALGORITHM
+            .replaceFirst("_", TRUSTSTORE);
+
+    /**
+     * The property name used to indicate that the default JRE trust managers
+     * should be used. The existance of this property in the Properties object
+     * passed to the constuctor indicates that the default should be used, and
+     * the value is ignored.
+     * <p>
+     * This option will have the affect of passing a null argument to the
+     * SSLContext.init method. This behavior is useful if you are connecting to
+     * a server which has a certificate signed by a widely accepted Certificate
+     * Authority. See the java API for details.
+     * <p>
+     * All other SSL_TRUSTSTORE_* options will be ignored if this option is set.
+     *
+     * @see SSLContext#init(KeyManager[], TrustManager[],
+     *      java.security.SecureRandom)
+     */
+    public static final String SSL_USE_DEFAULT_TRUST_MANAGER = "ssl.use.default.trust.manager";
+
+    /**
+     * A Property used to indicate that when constructing the trust store, the
+     * information provided for the key store should be used. This existance of
+     * the property is used to indicate that this behavior is desired, and the
+     * value is ignored.
+     * <p>
+     * This is useful in cases where the the root server certificate is stored
+     * in the client's keystore, which is a common case.
+     */
+    public static final String SSL_KEYSTORE_IS_TRUSTSTORE = "ssl.keystore.is.truststore";
+
+    /**
+     * A Property name used to identify the ssl protocol to use to connect to
+     * the server. The supported values depends on your security provider. If
+     * no option is specified, then SSLv3 is used.
+     */
+    public static final String SSL_PROTOCOL = "ssl.protocol";
+
+    /**
+     * A Property used to indicate a custom security provider.
+     */
+    public static final String SSL_PROVIDER = "ssl.provider";
+
+    /**
+     * Constructor which uses the property values specified by the
+     * public static values of this class to retrieve ssl factory
+     * configuration options.
+     *
+     * @param props
+     * @throws Exception This constructor can fail for a wide
+     *                   variety of reasons, all of which result in some
+     *                   sort of Exception.
+     */
+    public ValidatingFactory(Properties props) throws Exception {
+
+        // Obtain the trust managers for the ssl context.
+        TrustManager[] trustManagers = null;
+        if (props.getProperty(SSL_USE_DEFAULT_TRUST_MANAGER) == null) {
+            trustManagers = createTrustManagers(props);
+        }
+
+        // Obtain the key managers for the ssl context.
+        KeyManager[] keyManagers = null;
+        if (props.getProperty(SSL_USE_DEFAULT_KEY_MANAGER) == null) {
+            keyManagers = createKeyManagers(props);
+        }
+
+        // Create the ssl context.
+        String protocol = getProtocol(props);
+        SSLContext sslContext = null;
+        if (props.getProperty(SSL_PROVIDER) != null) {
+            sslContext = SSLContext.getInstance(protocol, props
+                    .getProperty(SSL_PROVIDER));
+        } else {
+            sslContext = SSLContext.getInstance(protocol);
+        }
+
+        // initialize the context.
+        sslContext.init(keyManagers, trustManagers, null);
+
+        // Create the wrapped socket factory.
+        _factory = sslContext.getSocketFactory();
+    }
+
+    /**
+     * @param props The properties object passed to the constructor should
+     *              be passed to thie method.
+     * @return Array of TrustManagers which should be used to initialize
+     *                 the SSLContext.
+     * @throws Exception
+     */
+    protected static TrustManager[] createTrustManagers(Properties props)
+            throws Exception {
+        // We may want to simply used the key store for trusted certificates.
+        String mode = TRUSTSTORE;
+        if (props.getProperty(SSL_KEYSTORE_IS_TRUSTSTORE) != null) {
+            mode = KEYSTORE;
+        }
+
+        // Obtain the keystore.
+        KeyStore trustStore = getLoadedKeyStore(props, mode);
+
+
+        String provider = props.getProperty(SSL_PROVIDER);
+        String algorithm = getAlgorithm(props, mode);
+        TrustManagerFactory tmf = null;
+
+        if (provider != null) {
+            tmf = TrustManagerFactory.getInstance(algorithm, provider);
+        } else {
+            tmf = TrustManagerFactory.getInstance(algorithm);
+        }
+
+        tmf.init(trustStore);
+
+        return tmf.getTrustManagers();
+    }
+
+    /**
+     * @param props Argument should be the same as the one passed to
+     *              the constructor.
+     * @return A KeyManager array intended for use for the SSLContext.init method.
+     * @throws Exception Many reasons this could fail...
+     */
+    private static KeyManager[] createKeyManagers(Properties props)
+            throws Exception {
+        KeyStore trustStore = getLoadedKeyStore(props, KEYSTORE);
+
+        String provider = props.getProperty(SSL_PROVIDER);
+        String algorithm = getAlgorithm(props, KEYSTORE);
+        KeyManagerFactory kmf = null;
+        if (provider != null) {
+            kmf = KeyManagerFactory.getInstance(algorithm, provider);
+        } else {
+            kmf = KeyManagerFactory.getInstance(algorithm);
+        }
+
+        char[] password = getPWD(props, KEYSTORE);
+        try {
+            kmf.init(trustStore, password);
+        }finally {
+            Arrays.fill(password, '\0');
+        }
+
+        return kmf.getKeyManagers();
+    }
+
+    private static String getAlgorithm(Properties props, String mode) {
+        String ans = props.getProperty(SSL__ALGORITHM.replaceFirst("_", mode));
+        if (ans != null) {
+            return ans;
+        }
+
+        if (mode.equals(KEYSTORE)) {
+            return KeyManagerFactory.getDefaultAlgorithm();
+        } else {
+            return TrustManagerFactory.getDefaultAlgorithm();
+        }
+    }
+
+    private static KeyStore getLoadedKeyStore(Properties props, String mode)
+            throws Exception {
+        File keystoreFile = getStoreFile(props, mode);
+
+        String keyStoreType = getStoreType(props, mode, keystoreFile);
+
+        String provider = props.getProperty(SSL_PROVIDER);
+        KeyStore store = null;
+        if (provider != null) {
+            store = KeyStore.getInstance(keyStoreType, provider);
+        } else {
+            store = KeyStore.getInstance(keyStoreType);
+        }
+
+        InputStream strm = new FileInputStream(keystoreFile);
+        char[] password = getPWD(props, mode);
+        try {
+            store.load(strm, password);
+        } finally {
+            Arrays.fill(password, '\0');
+        }
+
+        strm.close();
+
+        return store;
+    }
+
+    private static File getStoreFile(Properties props, String mode) {
+        String ks = props.getProperty(SSL__FILE.replaceFirst("_", mode));
+        if (ks != null) {
+            return new File(ks);
+        }
+
+        File ans = new File(System.getProperty("user.home"));
+        ans = new File(ans, ".postgresql");
+        ans = new File(ans, "postgresql.jks");
+        return ans;
+    }
+
+    private static String getStoreType(Properties props, String mode, File keyStore) {
+        String kst = props.getProperty(SSL__TYPE.replaceFirst("_", mode));
+        if (kst != null) {
+            return kst;
+        }
+
+        return KeyStore.getDefaultType();
+    }
+
+    private static char[] getPWD(Properties props, String mode) {
+        // Unfortunately, the password is in a string. We don't really
+        // have anyway of wiping it.
+        String pwd = props.getProperty(SSL__PASSWORD.replaceFirst("_", mode),
+                "");
+        return pwd.toCharArray();
+    }
+
+    private static String getProtocol(Properties props) {
+        if (props.containsKey(SSL_PROTOCOL)) {
+            return props.getProperty(SSL_PROTOCOL);
+        }
+        return "SSLv3";
+    }
+}
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;

import org.postgresql.Driver;
import org.postgresql.ssl.ValidatingFactory;


/**
 * An example class which makes used of a custom keystore to validate the
 * authenticity of the server.
 * <p>
 * Assuming your server's certificate has been signed by the CA whose
 * key is located in rootca.crt, then you can create a trust key store
 * by running the following command:
 *
 *   keytool -import -trustcacerts -file rootca.crt -keystore /home/user/trust.jks
 *
 * This will create a new keystore with the root certificate only. You'll
 * need to supply a password.
 *
 * Then execute this class to see if it works. Note that if the root certificate
 * is in you database directory, then the database server will attempt to
 * perform client validation, and this class does not support that.
 */
public class ServerValidate {

    /**
     * @param args
     */
    public static void main(String[] args) throws Exception {
        // TODO Auto-generated method stub
        Properties props = new Properties();
        // these properties are most likely required, depending on your
        // database set up.
        props.setProperty("user","testuser");
        props.setProperty("password","dbpassword");

        // Indicate that you want SSL.
        props.setProperty("ssl", "true");
        // Specify the Validating factory provided with the patch.
        props.setProperty("sslfactory", ValidatingFactory.class.getName());

        // No key manager is required since client authentication is not desired.
        // this option is required for this scenario.
        props.setProperty(ValidatingFactory.SSL_USE_DEFAULT_KEY_MANAGER, "");

        // Specify the file which contains the Certificat Authority keys.
        // /home/user/.postgresql/postgresql.jks will be used if this option
        // is not supplied.
        props.setProperty(ValidatingFactory.SSL_TRUSTSTORE_FILE, "/home/user/trust.jks");
        // The password to decrypt the keystore in question. If this is not
        // supplied, then an empty password will be used. I haven't figured out
        // how to create a keystore with no password, so this option is effectively
        // required.
        props.setProperty(ValidatingFactory.SSL_TRUSTSTORE_PASSWORD, "PASSWORD");


        // connect, query, go wild....
        Class c = Driver.class;
        String url = "jdbc:postgresql://host/testdb";
        Connection conn = DriverManager.getConnection(url, props);

        Statement st = conn.createStatement();
        ResultSet rs = st.executeQuery("SELECT version();");
        while (rs.next()) {
            System.out.println(rs.getString(1));
        }
        rs.close();
        st.close();
        conn.close();
    }

}
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;

import org.postgresql.Driver;
import org.postgresql.ssl.ValidatingFactory;


/**
 * An example class which makes used of a custom keystore to validate the
 * authenticity of the server, and to assert the identity of the client.
 * <p>
 * Assuming your server's certificate has been signed by the CA whose
 * key is located in rootca.crt, then you can create a key store which
 * contains both the client key and the CA's certificate by running
 * the following commands:
 * <code>
   keytool -genkey -alias testuser -keystore postgresql.jks
   keytool -certreq -alias testuser -file newreq.pem -keystore postgresql.jks
   keytool -import -trustcacerts -file rootca.crt -keystore postgresql.jks
   (your certificate request will need to be completed and returned to you before
    completing the next step)
   keytool -import -alias testuser -file newcert.pem -keystore postgresql.jks
  </code>
 * Note that user's of the OpenSSL "CA" script will be familiar with the file
 * names newreq.pem and newcert.pem. If you use that script, you'll need to
 * edit the newcert.pem file so that all headers are removed, and only the
 * CERTIFICATE block remains.
 *
 * Then execute this class to see if it works. Note that if the root certificate
 * must be in your database diretory as root.crt in order to force client
 * authentication.
 */
public class ServerAndClientValidate {

    /**
     * @param args
     */
    public static void main(String[] args) throws Exception {
        Properties props = new Properties();
        // these properties are most likely required, depending on your
        // database set up.
        props.setProperty("user","testuser");
        props.setProperty("password","PASSWORD");

        // Indicate that you want SSL.
        props.setProperty("ssl", "true");
        // Specify the Validating factory provided with the patch.
        props.setProperty("sslfactory", ValidatingFactory.class.getName());

        // The password to decrypt the keystore in question. If this is not
        // supplied, then an empty password will be used. I haven't figured out
        // how to create a keystore with no password, so this option is effectively
        // required.
        props.setProperty(ValidatingFactory.SSL_KEYSTORE_PASSWORD, "PASSWORD");

        // indicate that the trust store is the same as the client key store.
        props.setProperty(ValidatingFactory.SSL_KEYSTORE_IS_TRUSTSTORE, "");

        // Use the protocol of your choice.
        props.setProperty(ValidatingFactory.SSL_PROTOCOL, "TLS");

        // connect, query, go wild....
        Class c = Driver.class;
        String url = "jdbc:postgresql://host/testdb";
        Connection conn = DriverManager.getConnection(url, props);

        Statement st = conn.createStatement();
        ResultSet rs = st.executeQuery("SELECT version();");
        while (rs.next()) {
            System.out.println(rs.getString(1));
        }
        rs.close();
        st.close();
        conn.close();
    }
}

pgsql-jdbc by date:

Previous
From: "Shah, Anuj R"
Date:
Subject: One more: Function does not exist error
Next
From: Hugo Sacramento
Date:
Subject: Retrieve Query