diff --git a/.travis/travis_configure_ssl.sh b/.travis/travis_configure_ssl.sh
index c01affe09c..e07aa70da5 100755
--- a/.travis/travis_configure_ssl.sh
+++ b/.travis/travis_configure_ssl.sh
@@ -22,16 +22,7 @@ set_conf_property "ssl_cert_file" "server.crt"
 set_conf_property "ssl_key_file" "server.key"
 set_conf_property "ssl_ca_file" "root.crt"
 
-enable_ssl_property "testsinglecertfactory"
-enable_ssl_property "sslhostnossl9"
-enable_ssl_property "sslhostgh9"
-enable_ssl_property "sslhostbh9"
-enable_ssl_property "sslhostsslgh9"
-enable_ssl_property "sslhostsslbh9"
-enable_ssl_property "sslhostsslcertgh9"
-enable_ssl_property "sslhostsslcertbh9"
-enable_ssl_property "sslcertgh9"
-enable_ssl_property "sslcertbh9"
+enable_ssl_property "enable_ssl_tests"
 
 PG_DATA_DIR="/etc/postgresql/${PG_VERSION}/main/"
 sudo cp certdir/server/pg_hba.conf "/etc/postgresql/${PG_VERSION}/main/pg_hba.conf"
diff --git a/README.md b/README.md
index e018dcfffb..ce63ac21bc 100644
--- a/README.md
+++ b/README.md
@@ -111,7 +111,7 @@ In addition to the standard connection parameters the driver supports a number o
 | password                      | String  | null    | The database user's password. |
 | ssl                           | Boolean | false   | Control use of SSL (true value causes SSL to be required) |
 | sslfactory                    | String  | null    | Provide a SSLSocketFactory class when using SSL. |
-| sslfactoryarg                 | String  | null    | Argument forwarded to constructor of SSLSocketFactory class. |
+| sslfactoryarg (deprecated)    | String  | null    | Argument forwarded to constructor of SSLSocketFactory class. |
 | sslmode                       | String  | null    | Parameter governing the use of SSL. |
 | sslcert                       | String  | null    | The location of the client's SSL certificate |
 | sslkey                        | String  | null    | The location of the client's PKCS#8 SSL key |
@@ -144,7 +144,7 @@ In addition to the standard connection parameters the driver supports a number o
 | hostRecheckSeconds            | Integer | 10      | Specifies period (seconds) after which the host status is checked again in case it has changed |
 | loadBalanceHosts              | Boolean | false   | If disabled hosts are connected in the given order. If enabled hosts are chosen randomly from the set of suitable candidates |
 | socketFactory                 | String  | null    | Specify a socket factory for socket creation |
-| socketFactoryArg              | String  | null    | Argument forwarded to constructor of SocketFactory class. |
+| socketFactoryArg (deprecated) | String  | null    | Argument forwarded to constructor of SocketFactory class. |
 | autosave                      | String  | never   | Specifies what the driver should do if a query fails, possible values: always, never, conservative |
 | preferQueryMode               | String  | extended | Specifies which mode is used to execute queries to database, possible values: extended, extendedForPrepared, extendedCacheEverything, simple |
 | reWriteBatchedInserts         | Boolean | false  | Enable optimization to rewrite and collapse compatible INSERT statements that are batched. |
diff --git a/build.properties b/build.properties
index 91e4cd10a7..548d2aaf94 100644
--- a/build.properties
+++ b/build.properties
@@ -19,3 +19,4 @@ preparethreshold=5
 loggerLevel=OFF
 loggerFile=target/pgjdbc-tests.log
 protocolVersion=0
+sslpassword=sslpwd
diff --git a/certdir/README b/certdir/README
deleted file mode 100644
index 8bbbf5b1c3..0000000000
--- a/certdir/README
+++ /dev/null
@@ -1,57 +0,0 @@
-
-To run the SSL tests, the following properties are used:
-
-certdir: directory where the certificates and keys are store
-
-ssl<TYPE><gh|bh><8|9>: a connection string to the appropriate database
-TYPE is the TYPE or METHOD field from pg_hba.conf that is: host, hostnossl,
-hostssl and the special types hostsslcert, that corresponds
-to a hostssl type with clientcert=1 and cert that corresponds
-to a hostssl type with cert authentication. 'gh' means, the server certificate
-matches the hostname (good hostname), 'bh' means it is not (bad
-hostname). It can be simulated with a single database, if two names
-can be used i.e. localhost and 127.0.0.1. ssloff points to a database,
-where ssl is off. The last number is the server major version
-
-For each connection, the following files should be placed into certdir:
-goodclient.crt, badclient.crt, goodclient.pk8, badclient.pk8, goodroot.crt, badroot.crt
-optionally prefixed by the value of ssl<TYPE><gh|bh>prefix property, if
-different files are necessary for different connect strings.
-
-This directory contains example certificates generated by the following
-commands:
-
-openssl req -x509 -newkey rsa:1024 -days 3650 -keyout goodclient.key -out goodclient.crt
-#Common name is test, password is sslpwd
-
-openssl req -x509 -newkey rsa:1024 -days 3650 -keyout badclient.key -out badclient.crt
-#Common name is test, password is sslpwd
-
-openssl req -x509 -newkey rsa:1024 -days 3650 -nodes -keyout badroot.key -out badroot.crt
-#Common name is localhost
-rm badroot.key
-
-openssl pkcs8 -topk8 -in goodclient.key -out goodclient.pk8 -outform DER -v1 PBE-MD5-DES
-openssl pkcs8 -topk8 -in badclient.key -out badclient.pk8 -outform DER -v1 PBE-MD5-DES
-cp goodclient.crt server/root.crt
-cd server
-openssl req -x509 -newkey rsa:1024 -nodes -days 3650 -keyout server.key -out server.crt
-cp server.crt ../goodroot.crt
-#Common name is localhost, no password
-
-The subdirectory server contains what should be copied to the PGDATA directory.
-If you do not overwrite the pg_hba.conf then remember to comment out all lines
-starting with "host all".
-
-For the tests the sslinfo module must be installed into every database.
-The ssl=on must be set in postgresql.conf
-
-The following command creates the databases and installs the sslinfo module.
-
-for db in hostssldb hostnossldb certdb hostsslcertdb; do
-  createdb $db
-  psql $db -c "create extension sslinfo"
-done
-
-The username for connecting to postgres as specified in build.local.properties tests has to be "test".
-
diff --git a/certdir/README.md b/certdir/README.md
new file mode 100644
index 0000000000..ff041e3c49
--- /dev/null
+++ b/certdir/README.md
@@ -0,0 +1,44 @@
+To run the SSL tests, the following properties are used:
+
+* certdir: directory where the certificates and keys are store
+* enable_ssl_tests: enables SSL tests
+
+In order to configure PostgreSQL for SSL tests, the following changes should be applied:
+
+* Copy server/server.crt, server/server.key, and server/root.crt to $PGDATA directory
+* In $PGDATA directory: chmod 0600 server.crt server.key root.crt
+* Set ssl=on in postgresql.conf
+* Set ssl_cert_file=server.crt in postgresql.conf
+* Set ssl_key_file=server.key in postgresql.conf
+* Set ssl_ca_file=root.crt in postgresql.conf
+* Add databases for SSL tests. Note: sslinfo extension is used in tests to tell if connection is using SSL or not
+
+      for db in hostssldb hostnossldb certdb hostsslcertdb; do
+        createdb $db
+        psql $db -c "create extension sslinfo"
+      done
+* Add test databases to pg_hba.conf. If you do not overwrite the pg_hba.conf then remember to comment out all lines
+  starting with "host all".
+* Uncomment enable_ssl_tests=true in ssltests.properties
+* The username for connecting to postgres as specified in build.local.properties tests has to be "test".
+
+This directory contains example certificates generated by the following
+commands:
+
+openssl req -x509 -newkey rsa:1024 -days 3650 -keyout goodclient.key -out goodclient.crt
+#Common name is test, password is sslpwd
+
+openssl req -x509 -newkey rsa:1024 -days 3650 -keyout badclient.key -out badclient.crt
+#Common name is test, password is sslpwd
+
+openssl req -x509 -newkey rsa:1024 -days 3650 -nodes -keyout badroot.key -out badroot.crt
+#Common name is localhost
+rm badroot.key
+
+openssl pkcs8 -topk8 -in goodclient.key -out goodclient.pk8 -outform DER -v1 PBE-MD5-DES
+openssl pkcs8 -topk8 -in badclient.key -out badclient.pk8 -outform DER -v1 PBE-MD5-DES
+cp goodclient.crt server/root.crt
+cd server
+openssl req -x509 -newkey rsa:1024 -nodes -days 3650 -keyout server.key -out server.crt
+cp server.crt ../goodroot.crt
+#Common name is localhost, no password
diff --git a/docs/documentation/head/connect.md b/docs/documentation/head/connect.md
index 9763b76d8d..9881060512 100644
--- a/docs/documentation/head/connect.md
+++ b/docs/documentation/head/connect.md
@@ -88,7 +88,7 @@ Connection conn = DriverManager.getConnection(url);
 	establishing a SSL connection. For more information see the section
 	called [“Custom SSLSocketFactory”](ssl-factory.html). 
 
-* **sslfactoryarg** = String
+* **sslfactoryarg** (deprecated) = String
 
 	This value is an optional argument to the constructor of the sslfactory
 	class provided above. For more information see the section called [“Custom SSLSocketFactory”](ssl-factory.html). 
@@ -408,7 +408,7 @@ Connection conn = DriverManager.getConnection(url);
 	This class must have a zero argument constructor or a single argument constructor taking a String argument. 
 	This argument may optionally be supplied by `socketFactoryArg`.
 
-* **socketFactoryArg** = String
+* **socketFactoryArg** (deprecated) = String
 
 	This value is an optional argument to the constructor of the socket factory
 	class provided above. 
diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
index e56e05eb73..4afefe2356 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
@@ -173,17 +173,18 @@
       "Enable optimization that disables column name sanitiser"),
 
   /**
-   * Control use of SSL (any non-null value causes SSL to be required).
+   * Control use of SSL: empty or {@code true} values imply {@code sslmode==verify-full}
    */
   SSL("ssl", null, "Control use of SSL (any non-null value causes SSL to be required)"),
 
   /**
-   * Parameter governing the use of SSL. The allowed values are {@code require}, {@code verify-ca},
-   * {@code verify-full}, or {@code disable} ({@code allow} and {@code prefer} are not implemented)
-   * If not set, the {@code ssl} property may be checked to enable SSL mode.
+   * Parameter governing the use of SSL. The allowed values are {@code disable}, {@code allow},
+   * {@code prefer}, {@code require}, {@code verify-ca}, {@code verify-full}.
+   * If {@code ssl} property is empty or set to {@code true} it implies {@code verify-full}.
+   * Default mode is "require"
    */
-  SSL_MODE("sslmode", null, "Parameter governing the use of SSL",false,
-      "disable", "require", "verify-ca", "verify-full"),
+  SSL_MODE("sslmode", null, "Parameter governing the use of SSL", false,
+      "disable", "allow", "prefer", "require", "verify-ca", "verify-full"),
 
   /**
    * Classname of the SSL Factory to use (instance of {@code javax.net.ssl.SSLSocketFactory}).
@@ -192,7 +193,9 @@
 
   /**
    * The String argument to give to the constructor of the SSL Factory.
+   * @deprecated use {@code ..Factory(Properties)} constructor.
    */
+  @Deprecated
   SSL_FACTORY_ARG("sslfactoryarg", null,
       "Argument forwarded to constructor of SSLSocketFactory class."),
 
@@ -279,7 +282,9 @@
 
   /**
    * The String argument to give to the constructor of the Socket Factory.
+   * @deprecated use {@code ..Factory(Properties)} constructor.
    */
+  @Deprecated
   SOCKET_FACTORY_ARG("socketFactoryArg", null,
       "Argument forwarded to constructor of SocketFactory class."),
 
diff --git a/pgjdbc/src/main/java/org/postgresql/core/PGStream.java b/pgjdbc/src/main/java/org/postgresql/core/PGStream.java
index 63065730a0..6f0f3a2bea 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/PGStream.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/PGStream.java
@@ -21,6 +21,7 @@
 import java.io.Writer;
 import java.net.InetSocketAddress;
 import java.net.Socket;
+import java.net.SocketTimeoutException;
 import java.sql.SQLException;
 import javax.net.SocketFactory;
 
@@ -109,7 +110,19 @@ public SocketFactory getSocketFactory() {
    * @throws IOException if something wrong happens
    */
   public boolean hasMessagePending() throws IOException {
-    return pg_input.available() > 0 || connection.getInputStream().available() > 0;
+    if (pg_input.available() > 0) {
+      return true;
+    }
+    // In certain cases, available returns 0, yet there are bytes
+    int soTimeout = getNetworkTimeout();
+    setNetworkTimeout(1);
+    try {
+      return pg_input.peek() != -1;
+    } catch (SocketTimeoutException e) {
+      return false;
+    } finally {
+      setNetworkTimeout(soTimeout);
+    }
   }
 
   /**
diff --git a/pgjdbc/src/main/java/org/postgresql/core/SocketFactoryFactory.java b/pgjdbc/src/main/java/org/postgresql/core/SocketFactoryFactory.java
index e7cfd3262d..09efa75f07 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/SocketFactoryFactory.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/SocketFactoryFactory.java
@@ -6,6 +6,7 @@
 package org.postgresql.core;
 
 import org.postgresql.PGProperty;
+import org.postgresql.ssl.LibPQFactory;
 import org.postgresql.util.GT;
 import org.postgresql.util.ObjectFactory;
 import org.postgresql.util.PSQLException;
@@ -14,6 +15,7 @@
 import java.util.Properties;
 
 import javax.net.SocketFactory;
+import javax.net.ssl.SSLSocketFactory;
 
 /**
  * Instantiates {@link SocketFactory} based on the {@link PGProperty#SOCKET_FACTORY}.
@@ -44,4 +46,28 @@ public static SocketFactory getSocketFactory(Properties info) throws PSQLExcepti
     }
   }
 
+  /**
+   * Instantiates {@link SSLSocketFactory} based on the {@link PGProperty#SSL_FACTORY}.
+   *
+   * @param info connection properties
+   * @return SSL socket factory
+   * @throws PSQLException if something goes wrong
+   */
+  public static SSLSocketFactory getSslSocketFactory(Properties info) throws PSQLException {
+    String classname = PGProperty.SSL_FACTORY.get(info);
+    if (classname == null
+        || "org.postgresql.ssl.jdbc4.LibPQFactory".equals(classname)
+        || "org.postgresql.ssl.LibPQFactory".equals(classname)) {
+      return new LibPQFactory(info);
+    }
+    try {
+      return (SSLSocketFactory) ObjectFactory.instantiate(classname, info, true,
+          PGProperty.SSL_FACTORY_ARG.get(info));
+    } catch (Exception e) {
+      throw new PSQLException(
+          GT.tr("The SSLSocketFactory class provided {0} could not be instantiated.", classname),
+          PSQLState.CONNECTION_FAILURE, e);
+    }
+  }
+
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
index 749af95889..c83ce34c59 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
@@ -21,6 +21,7 @@
 import org.postgresql.hostchooser.HostChooserFactory;
 import org.postgresql.hostchooser.HostRequirement;
 import org.postgresql.hostchooser.HostStatus;
+import org.postgresql.jdbc.SslMode;
 import org.postgresql.sspi.ISSPIClient;
 import org.postgresql.util.GT;
 import org.postgresql.util.HostSpec;
@@ -42,7 +43,6 @@
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.util.logging.Logger;
-
 import javax.net.SocketFactory;
 
 /**
@@ -82,32 +82,71 @@ private ISSPIClient createSSPI(PGStream pgStream,
     }
   }
 
-  @Override
-  public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, String database,
-      Properties info) throws SQLException {
-    // Extract interesting values from the info properties:
-    // - the SSL setting
-    boolean requireSSL;
-    boolean trySSL;
-    String sslmode = PGProperty.SSL_MODE.get(info);
-    if (sslmode == null) { // Fall back to the ssl property
-      // assume "true" if the property is set but empty
-      requireSSL = trySSL = PGProperty.SSL.getBoolean(info) || "".equals(PGProperty.SSL.get(info));
-    } else {
-      if ("disable".equals(sslmode)) {
-        requireSSL = trySSL = false;
-      } else if ("require".equals(sslmode) || "verify-ca".equals(sslmode)
-          || "verify-full".equals(sslmode)) {
-        requireSSL = trySSL = true;
+  private PGStream tryConnect(String user, String database,
+      Properties info, SocketFactory socketFactory, HostSpec hostSpec,
+      SslMode sslMode)
+      throws SQLException, IOException {
+    int connectTimeout = PGProperty.CONNECT_TIMEOUT.getInt(info) * 1000;
+
+    PGStream newStream = new PGStream(socketFactory, hostSpec, connectTimeout);
+
+    // Construct and send an ssl startup packet if requested.
+    newStream = enableSSL(newStream, sslMode, info, connectTimeout);
+
+    // Set the socket timeout if the "socketTimeout" property has been set.
+    int socketTimeout = PGProperty.SOCKET_TIMEOUT.getInt(info);
+    if (socketTimeout > 0) {
+      newStream.getSocket().setSoTimeout(socketTimeout * 1000);
+    }
+
+    // Enable TCP keep-alive probe if required.
+    boolean requireTCPKeepAlive = PGProperty.TCP_KEEP_ALIVE.getBoolean(info);
+    newStream.getSocket().setKeepAlive(requireTCPKeepAlive);
+
+    // Try to set SO_SNDBUF and SO_RECVBUF socket options, if requested.
+    // If receiveBufferSize and send_buffer_size are set to a value greater
+    // than 0, adjust. -1 means use the system default, 0 is ignored since not
+    // supported.
+
+    // Set SO_RECVBUF read buffer size
+    int receiveBufferSize = PGProperty.RECEIVE_BUFFER_SIZE.getInt(info);
+    if (receiveBufferSize > -1) {
+      // value of 0 not a valid buffer size value
+      if (receiveBufferSize > 0) {
+        newStream.getSocket().setReceiveBufferSize(receiveBufferSize);
       } else {
-        throw new PSQLException(GT.tr("Invalid sslmode value: {0}", sslmode),
-            PSQLState.CONNECTION_UNABLE_TO_CONNECT);
+        LOGGER.log(Level.WARNING, "Ignore invalid value for receiveBufferSize: {0}", receiveBufferSize);
       }
     }
 
-    boolean requireTCPKeepAlive = PGProperty.TCP_KEEP_ALIVE.getBoolean(info);
+    // Set SO_SNDBUF write buffer size
+    int sendBufferSize = PGProperty.SEND_BUFFER_SIZE.getInt(info);
+    if (sendBufferSize > -1) {
+      if (sendBufferSize > 0) {
+        newStream.getSocket().setSendBufferSize(sendBufferSize);
+      } else {
+        LOGGER.log(Level.WARNING, "Ignore invalid value for sendBufferSize: {0}", sendBufferSize);
+      }
+    }
 
-    int connectTimeout = PGProperty.CONNECT_TIMEOUT.getInt(info) * 1000;
+    if (LOGGER.isLoggable(Level.FINE)) {
+      LOGGER.log(Level.FINE, "Receive Buffer Size is {0}", newStream.getSocket().getReceiveBufferSize());
+      LOGGER.log(Level.FINE, "Send Buffer Size is {0}", newStream.getSocket().getSendBufferSize());
+    }
+
+    List<String[]> paramList = getParametersForStartup(user, database, info);
+    sendStartupPacket(newStream, paramList);
+
+    // Do authentication (until AuthenticationOk).
+    doAuthentication(newStream, hostSpec.getHost(), user, info);
+
+    return newStream;
+  }
+
+  @Override
+  public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, String database,
+      Properties info) throws SQLException {
+    SslMode sslMode = SslMode.of(info);
 
     HostRequirement targetServerType;
     String targetServerTypeStr = PGProperty.TARGET_SERVER_TYPE.get(info);
@@ -149,59 +188,62 @@ public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, Strin
 
       PGStream newStream = null;
       try {
-        newStream = new PGStream(socketFactory, hostSpec, connectTimeout);
-
-        // Construct and send an ssl startup packet if requested.
-        if (trySSL) {
-          newStream = enableSSL(newStream, requireSSL, info, connectTimeout);
-        }
-
-        // Set the socket timeout if the "socketTimeout" property has been set.
-        int socketTimeout = PGProperty.SOCKET_TIMEOUT.getInt(info);
-        if (socketTimeout > 0) {
-          newStream.getSocket().setSoTimeout(socketTimeout * 1000);
-        }
-
-        // Enable TCP keep-alive probe if required.
-        newStream.getSocket().setKeepAlive(requireTCPKeepAlive);
-
-        // Try to set SO_SNDBUF and SO_RECVBUF socket options, if requested.
-        // If receiveBufferSize and send_buffer_size are set to a value greater
-        // than 0, adjust. -1 means use the system default, 0 is ignored since not
-        // supported.
-
-        // Set SO_RECVBUF read buffer size
-        int receiveBufferSize = PGProperty.RECEIVE_BUFFER_SIZE.getInt(info);
-        if (receiveBufferSize > -1) {
-          // value of 0 not a valid buffer size value
-          if (receiveBufferSize > 0) {
-            newStream.getSocket().setReceiveBufferSize(receiveBufferSize);
-          } else {
-            LOGGER.log(Level.WARNING, "Ignore invalid value for receiveBufferSize: {0}", receiveBufferSize);
-          }
-        }
+        try {
+          newStream = tryConnect(user, database, info, socketFactory, hostSpec, sslMode);
+        } catch (SQLException e) {
+          if (sslMode == SslMode.PREFER
+              && PSQLState.INVALID_AUTHORIZATION_SPECIFICATION.getState().equals(e.getSQLState())) {
+            // Try non-SSL connection to cover case like "non-ssl only db"
+            // Note: PREFER allows loss of encryption, so no significant harm is made
+            Throwable ex = null;
+            try {
+              newStream =
+                  tryConnect(user, database, info, socketFactory, hostSpec, SslMode.DISABLE);
+              LOGGER.log(Level.FINE, "Downgraded to non-encrypted connection for host {0}",
+                  hostSpec);
+            } catch (SQLException ee) {
+              ex = ee;
+            } catch (IOException ee) {
+              ex = ee; // Can't use multi-catch in Java 6 :(
+            }
+            if (ex != null) {
+              log(Level.FINE, "sslMode==PREFER, however non-SSL connection failed as well", ex);
+              // non-SSL failed as well, so re-throw original exception
+              //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.1"
+              // Add non-SSL exception as suppressed
+              e.addSuppressed(ex);
+              //#endif
+              throw e;
+            }
+          } else if (sslMode == SslMode.ALLOW
+              && PSQLState.INVALID_AUTHORIZATION_SPECIFICATION.getState().equals(e.getSQLState())) {
+            // Try using SSL
+            Throwable ex = null;
+            try {
+              newStream =
+                  tryConnect(user, database, info, socketFactory, hostSpec, SslMode.REQUIRE);
+              LOGGER.log(Level.FINE, "Upgraded to encrypted connection for host {0}",
+                  hostSpec);
+            } catch (SQLException ee) {
+              ex = ee;
+            } catch (IOException ee) {
+              ex = ee; // Can't use multi-catch in Java 6 :(
+            }
+            if (ex != null) {
+              log(Level.FINE, "sslMode==ALLOW, however SSL connection failed as well", ex);
+              // non-SSL failed as well, so re-throw original exception
+              //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.1"
+              // Add SSL exception as suppressed
+              e.addSuppressed(ex);
+              //#endif
+              throw e;
+            }
 
-        // Set SO_SNDBUF write buffer size
-        int sendBufferSize = PGProperty.SEND_BUFFER_SIZE.getInt(info);
-        if (sendBufferSize > -1) {
-          if (sendBufferSize > 0) {
-            newStream.getSocket().setSendBufferSize(sendBufferSize);
           } else {
-            LOGGER.log(Level.WARNING, "Ignore invalid value for sendBufferSize: {0}", sendBufferSize);
+            throw e;
           }
         }
 
-        if (LOGGER.isLoggable(Level.FINE)) {
-          LOGGER.log(Level.FINE, "Receive Buffer Size is {0}", newStream.getSocket().getReceiveBufferSize());
-          LOGGER.log(Level.FINE, "Send Buffer Size is {0}", newStream.getSocket().getSendBufferSize());
-        }
-
-        List<String[]> paramList = getParametersForStartup(user, database, info);
-        sendStartupPacket(newStream, paramList);
-
-        // Do authentication (until AuthenticationOk).
-        doAuthentication(newStream, hostSpec.getHost(), user, info);
-
         int cancelSignalTimeout = PGProperty.CANCEL_SIGNAL_TIMEOUT.getInt(info) * 1000;
 
         // Do final startup.
@@ -230,8 +272,8 @@ public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, Strin
         // we trap this an return a more meaningful message for the end user
         GlobalHostStatusTracker.reportHostStatus(hostSpec, HostStatus.ConnectFail);
         knownStates.put(hostSpec, HostStatus.ConnectFail);
-        log(Level.FINE, "ConnectException occurred while connecting to {0}", cex, hostSpec);
         if (hostIter.hasNext()) {
+          log(Level.FINE, "ConnectException occurred while connecting to {0}", cex, hostSpec);
           // still more addresses to try
           continue;
         }
@@ -242,8 +284,8 @@ public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, Strin
         closeStream(newStream);
         GlobalHostStatusTracker.reportHostStatus(hostSpec, HostStatus.ConnectFail);
         knownStates.put(hostSpec, HostStatus.ConnectFail);
-        log(Level.FINE, "IOException occurred while connecting to {0}", ioe, hostSpec);
         if (hostIter.hasNext()) {
+          log(Level.FINE, "IOException occurred while connecting to {0}", ioe, hostSpec);
           // still more addresses to try
           continue;
         }
@@ -251,10 +293,10 @@ public QueryExecutor openConnectionImpl(HostSpec[] hostSpecs, String user, Strin
             PSQLState.CONNECTION_UNABLE_TO_CONNECT, ioe);
       } catch (SQLException se) {
         closeStream(newStream);
-        log(Level.FINE, "SQLException occurred while connecting to {0}", se, hostSpec);
         GlobalHostStatusTracker.reportHostStatus(hostSpec, HostStatus.ConnectFail);
         knownStates.put(hostSpec, HostStatus.ConnectFail);
         if (hostIter.hasNext()) {
+          log(Level.FINE, "SQLException occurred while connecting to {0}", se, hostSpec);
           // still more addresses to try
           continue;
         }
@@ -340,8 +382,17 @@ private static String createPostgresTimeZone() {
     return start + tz.substring(4);
   }
 
-  private PGStream enableSSL(PGStream pgStream, boolean requireSSL, Properties info, int connectTimeout)
-      throws IOException, SQLException {
+  private PGStream enableSSL(PGStream pgStream, SslMode sslMode, Properties info,
+      int connectTimeout)
+      throws IOException, PSQLException {
+    if (sslMode == SslMode.DISABLE) {
+      return pgStream;
+    }
+    if (sslMode == SslMode.ALLOW) {
+      // Allow ==> start with plaintext, use encryption if required by server
+      return pgStream;
+    }
+
     LOGGER.log(Level.FINEST, " FE=> SSLRequest");
 
     // Send SSL request packet
@@ -357,7 +408,7 @@ private PGStream enableSSL(PGStream pgStream, boolean requireSSL, Properties inf
         LOGGER.log(Level.FINEST, " <=BE SSLError");
 
         // Server doesn't even know about the SSL handshake protocol
-        if (requireSSL) {
+        if (sslMode.requireEncryption()) {
           throw new PSQLException(GT.tr("The server does not support SSL."),
               PSQLState.CONNECTION_REJECTED);
         }
@@ -370,7 +421,7 @@ private PGStream enableSSL(PGStream pgStream, boolean requireSSL, Properties inf
         LOGGER.log(Level.FINEST, " <=BE SSLRefused");
 
         // Server does not support ssl
-        if (requireSSL) {
+        if (sslMode.requireEncryption()) {
           throw new PSQLException(GT.tr("The server does not support SSL."),
               PSQLState.CONNECTION_REJECTED);
         }
@@ -608,14 +659,19 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
                 scramAuthenticator = new org.postgresql.jre8.sasl.ScramAuthenticator(user, password, pgStream);
                 scramAuthenticator.processServerMechanismsAndInit();
                 scramAuthenticator.sendScramClientFirstMessage();
-                //#else
-                if (true) {
+                // This works as follows:
+                // 1. When tests is run from IDE, it is assumed SCRAM library is on the classpath
+                // 2. In regular build for Java < 8 this `if` is deactivated and the code always throws
+                if (false) {
+                  //#else
                   throw new PSQLException(GT.tr(
                           "SCRAM authentication is not supported by this driver. You need JDK >= 8 and pgjdbc >= 42.2.0 (not \".jre\" versions)",
                           areq), PSQLState.CONNECTION_REJECTED);
+                  //#endif
+                  //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
                 }
-                //#endif
                 break;
+                //#endif
 
               //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
               case AUTH_REQ_SASL_CONTINUE:
diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
index 387a9ac1cd..4dc7f94000 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
@@ -692,7 +692,7 @@ public synchronized void processNotifies(int timeoutMillis) throws SQLException
     }
 
     try {
-      while (pgStream.hasMessagePending() || timeoutMillis >= 0 ) {
+      while (timeoutMillis >= 0 || pgStream.hasMessagePending()) {
         if (useTimeout && timeoutMillis >= 0) {
           setSocketTimeout(timeoutMillis);
         }
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/SslMode.java b/pgjdbc/src/main/java/org/postgresql/jdbc/SslMode.java
new file mode 100644
index 0000000000..bd2a3d0bcb
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/SslMode.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2018, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.jdbc;
+
+import org.postgresql.PGProperty;
+import org.postgresql.util.GT;
+import org.postgresql.util.PSQLException;
+import org.postgresql.util.PSQLState;
+
+import java.util.Properties;
+
+public enum SslMode {
+  /**
+   * Do not use encrypted connections.
+   */
+  DISABLE("disable"),
+  /**
+   * Start with non-encrypted connection, then try encrypted one.
+   */
+  ALLOW("allow"),
+  /**
+   * Start with encrypted connection, fallback to non-encrypted (default).
+   */
+  PREFER("prefer"),
+  /**
+   * Ensure connection is encrypted.
+   */
+  REQUIRE("require"),
+  /**
+   * Ensure connection is encrypted, and client trusts server certificate.
+   */
+  VERIFY_CA("verify-ca"),
+  /**
+   * Ensure connection is encrypted, client trusts server certificate, and server hostname matches
+   * the one listed in the server certificate.
+   */
+  VERIFY_FULL("verify-full"),
+  ;
+
+  public static final SslMode[] VALUES = values();
+
+  public final String value;
+
+  SslMode(String value) {
+    this.value = value;
+  }
+
+  public boolean requireEncryption() {
+    return this.compareTo(REQUIRE) >= 0;
+  }
+
+  public boolean verifyCertificate() {
+    return this == VERIFY_CA || this == VERIFY_FULL;
+  }
+
+  public boolean verifyPeerName() {
+    return this == VERIFY_FULL;
+  }
+
+  public static SslMode of(Properties info) throws PSQLException {
+    String sslmode = PGProperty.SSL_MODE.get(info);
+    // If sslmode is not set, fallback to ssl parameter
+    if (sslmode == null) {
+      if (PGProperty.SSL.getBoolean(info) || "".equals(PGProperty.SSL.get(info))) {
+        return VERIFY_FULL;
+      }
+      return PREFER;
+    }
+
+    for (SslMode sslMode : VALUES) {
+      if (sslMode.value.equalsIgnoreCase(sslmode)) {
+        return sslMode;
+      }
+    }
+    throw new PSQLException(GT.tr("Invalid sslmode value: {0}", sslmode),
+        PSQLState.CONNECTION_UNABLE_TO_CONNECT);
+  }
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/DefaultJavaSSLFactory.java b/pgjdbc/src/main/java/org/postgresql/ssl/DefaultJavaSSLFactory.java
new file mode 100644
index 0000000000..9757827e0f
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/ssl/DefaultJavaSSLFactory.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2017, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.ssl;
+
+import java.util.Properties;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * Socket factory that uses Java's default truststore to validate server certificate.
+ * Note: it always validates server certificate, so it might result to downgrade to non-encrypted
+ * connection when default truststore lacks certificates to validate server.
+ */
+public class DefaultJavaSSLFactory extends WrappedFactory {
+  public DefaultJavaSSLFactory(Properties info) {
+    _factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
+  }
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/jdbc4/LazyKeyManager.java b/pgjdbc/src/main/java/org/postgresql/ssl/LazyKeyManager.java
similarity index 99%
rename from pgjdbc/src/main/java/org/postgresql/ssl/jdbc4/LazyKeyManager.java
rename to pgjdbc/src/main/java/org/postgresql/ssl/LazyKeyManager.java
index 4585f1a968..be4db4153b 100644
--- a/pgjdbc/src/main/java/org/postgresql/ssl/jdbc4/LazyKeyManager.java
+++ b/pgjdbc/src/main/java/org/postgresql/ssl/LazyKeyManager.java
@@ -3,7 +3,7 @@
  * See the LICENSE file in the project root for more information.
  */
 
-package org.postgresql.ssl.jdbc4;
+package org.postgresql.ssl;
 
 import org.postgresql.util.GT;
 import org.postgresql.util.PSQLException;
@@ -222,6 +222,7 @@ public PrivateKey getPrivateKey(String alias) {
           }
           try {
             PBEKeySpec pbeKeySpec = new PBEKeySpec(pwdcb.getPassword());
+            pwdcb.clearPassword();
             // Now create the Key from the PBEKeySpec
             SecretKeyFactory skFac = SecretKeyFactory.getInstance(ePKInfo.getAlgName());
             Key pbeKey = skFac.generateSecret(pbeKeySpec);
diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java b/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java
new file mode 100644
index 0000000000..c0c34bd274
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2004, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.ssl;
+
+import org.postgresql.PGProperty;
+import org.postgresql.jdbc.SslMode;
+import org.postgresql.ssl.NonValidatingFactory.NonValidatingTM;
+import org.postgresql.util.GT;
+import org.postgresql.util.ObjectFactory;
+import org.postgresql.util.PSQLException;
+import org.postgresql.util.PSQLState;
+
+import java.io.Console;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.util.Properties;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+/**
+ * Provide an SSLSocketFactory that is compatible with the libpq behaviour.
+ */
+public class LibPQFactory extends WrappedFactory {
+
+  LazyKeyManager km;
+
+  /**
+   * @param info the connection parameters The following parameters are used:
+   *        sslmode,sslcert,sslkey,sslrootcert,sslhostnameverifier,sslpasswordcallback,sslpassword
+   * @throws PSQLException if security error appears when initializing factory
+   */
+  public LibPQFactory(Properties info) throws PSQLException {
+    try {
+      SSLContext ctx = SSLContext.getInstance("TLS"); // or "SSL" ?
+
+      // Determining the default file location
+      String pathsep = System.getProperty("file.separator");
+      String defaultdir;
+      boolean defaultfile = false;
+      if (System.getProperty("os.name").toLowerCase().contains("windows")) { // It is Windows
+        defaultdir = System.getenv("APPDATA") + pathsep + "postgresql" + pathsep;
+      } else {
+        defaultdir = System.getProperty("user.home") + pathsep + ".postgresql" + pathsep;
+      }
+
+      // Load the client's certificate and key
+      String sslcertfile = PGProperty.SSL_CERT.get(info);
+      if (sslcertfile == null) { // Fall back to default
+        defaultfile = true;
+        sslcertfile = defaultdir + "postgresql.crt";
+      }
+      String sslkeyfile = PGProperty.SSL_KEY.get(info);
+      if (sslkeyfile == null) { // Fall back to default
+        defaultfile = true;
+        sslkeyfile = defaultdir + "postgresql.pk8";
+      }
+
+      // Determine the callback handler
+      CallbackHandler cbh;
+      String sslpasswordcallback = PGProperty.SSL_PASSWORD_CALLBACK.get(info);
+      if (sslpasswordcallback != null) {
+        try {
+          cbh = (CallbackHandler) ObjectFactory.instantiate(sslpasswordcallback, info, false, null);
+        } catch (Exception e) {
+          throw new PSQLException(
+              GT.tr("The password callback class provided {0} could not be instantiated.",
+                  sslpasswordcallback),
+              PSQLState.CONNECTION_FAILURE, e);
+        }
+      } else {
+        cbh = new ConsoleCallbackHandler(PGProperty.SSL_PASSWORD.get(info));
+      }
+
+      // If the properties are empty, give null to prevent client key selection
+      km = new LazyKeyManager(("".equals(sslcertfile) ? null : sslcertfile),
+          ("".equals(sslkeyfile) ? null : sslkeyfile), cbh, defaultfile);
+
+      TrustManager[] tm;
+      SslMode sslMode = SslMode.of(info);
+      if (!sslMode.verifyCertificate()) {
+        // server validation is not required
+        tm = new TrustManager[]{new NonValidatingTM()};
+      } else {
+        // Load the server certificate
+
+        TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
+        KeyStore ks;
+        try {
+          ks = KeyStore.getInstance("jks");
+        } catch (KeyStoreException e) {
+          // this should never happen
+          throw new NoSuchAlgorithmException("jks KeyStore not available");
+        }
+        String sslrootcertfile = PGProperty.SSL_ROOT_CERT.get(info);
+        if (sslrootcertfile == null) { // Fall back to default
+          sslrootcertfile = defaultdir + "root.crt";
+        }
+        FileInputStream fis;
+        try {
+          fis = new FileInputStream(sslrootcertfile); // NOSONAR
+        } catch (FileNotFoundException ex) {
+          throw new PSQLException(
+              GT.tr("Could not open SSL root certificate file {0}.", sslrootcertfile),
+              PSQLState.CONNECTION_FAILURE, ex);
+        }
+        try {
+          CertificateFactory cf = CertificateFactory.getInstance("X.509");
+          // Certificate[] certs = cf.generateCertificates(fis).toArray(new Certificate[]{}); //Does
+          // not work in java 1.4
+          Object[] certs = cf.generateCertificates(fis).toArray(new Certificate[]{});
+          ks.load(null, null);
+          for (int i = 0; i < certs.length; i++) {
+            ks.setCertificateEntry("cert" + i, (Certificate) certs[i]);
+          }
+          tmf.init(ks);
+        } catch (IOException ioex) {
+          throw new PSQLException(
+              GT.tr("Could not read SSL root certificate file {0}.", sslrootcertfile),
+              PSQLState.CONNECTION_FAILURE, ioex);
+        } catch (GeneralSecurityException gsex) {
+          throw new PSQLException(
+              GT.tr("Loading the SSL root certificate {0} into a TrustManager failed.",
+                      sslrootcertfile),
+              PSQLState.CONNECTION_FAILURE, gsex);
+        } finally {
+          try {
+            fis.close();
+          } catch (IOException e) {
+            /* ignore */
+          }
+        }
+        tm = tmf.getTrustManagers();
+      }
+
+      // finally we can initialize the context
+      try {
+        ctx.init(new KeyManager[]{km}, tm, null);
+      } catch (KeyManagementException ex) {
+        throw new PSQLException(GT.tr("Could not initialize SSL context."),
+            PSQLState.CONNECTION_FAILURE, ex);
+      }
+
+      _factory = ctx.getSocketFactory();
+    } catch (NoSuchAlgorithmException ex) {
+      throw new PSQLException(GT.tr("Could not find a java cryptographic algorithm: {0}.",
+              ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex);
+    }
+  }
+
+  /**
+   * Propagates any exception from {@link LazyKeyManager}.
+   *
+   * @throws PSQLException if there is an exception to propagate
+   */
+  public void throwKeyManagerException() throws PSQLException {
+    if (km != null) {
+      km.throwKeyManagerException();
+    }
+  }
+
+  /**
+   * A CallbackHandler that reads the password from the console or returns the password given to its
+   * constructor.
+   */
+  static class ConsoleCallbackHandler implements CallbackHandler {
+
+    private char[] password = null;
+
+    ConsoleCallbackHandler(String password) {
+      if (password != null) {
+        this.password = password.toCharArray();
+      }
+    }
+
+    /**
+     * Handles the callbacks.
+     *
+     * @param callbacks The callbacks to handle
+     * @throws UnsupportedCallbackException If the console is not available or other than
+     *         PasswordCallback is supplied
+     */
+    @Override
+    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+      Console cons = System.console();
+      if (cons == null && password == null) {
+        throw new UnsupportedCallbackException(callbacks[0], "Console is not available");
+      }
+      for (Callback callback : callbacks) {
+        if (!(callback instanceof PasswordCallback)) {
+          throw new UnsupportedCallbackException(callback);
+        }
+        PasswordCallback pwdCallback = (PasswordCallback) callback;
+        if (password != null) {
+          pwdCallback.setPassword(password);
+          continue;
+        }
+        // It is used instead of cons.readPassword(prompt), because the prompt may contain '%'
+        // characters
+        pwdCallback.setPassword(cons.readPassword("%s", pwdCallback.getPrompt()));
+      }
+    }
+  }
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/MakeSSL.java b/pgjdbc/src/main/java/org/postgresql/ssl/MakeSSL.java
index d2e453552c..e09d88edbe 100644
--- a/pgjdbc/src/main/java/org/postgresql/ssl/MakeSSL.java
+++ b/pgjdbc/src/main/java/org/postgresql/ssl/MakeSSL.java
@@ -7,7 +7,8 @@
 
 import org.postgresql.PGProperty;
 import org.postgresql.core.PGStream;
-import org.postgresql.ssl.jdbc4.LibPQFactory;
+import org.postgresql.core.SocketFactoryFactory;
+import org.postgresql.jdbc.SslMode;
 import org.postgresql.util.GT;
 import org.postgresql.util.ObjectFactory;
 import org.postgresql.util.PSQLException;
@@ -17,7 +18,6 @@
 import java.util.Properties;
 import java.util.logging.Level;
 import java.util.logging.Logger;
-
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
@@ -30,47 +30,38 @@ public static void convert(PGStream stream, Properties info)
       throws PSQLException, IOException {
     LOGGER.log(Level.FINE, "converting regular socket connection to ssl");
 
-    SSLSocketFactory factory;
-
-    String sslmode = PGProperty.SSL_MODE.get(info);
-    // Use the default factory if no specific factory is requested
-    // unless sslmode is set
-    String classname = PGProperty.SSL_FACTORY.get(info);
-    if (classname == null) {
-      // If sslmode is set, use the libpq compatible factory
-      if (sslmode != null) {
-        factory = new LibPQFactory(info);
-      } else {
-        factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
-      }
-    } else {
-      try {
-        factory = (SSLSocketFactory) instantiate(classname, info, true,
-            PGProperty.SSL_FACTORY_ARG.get(info));
-      } catch (Exception e) {
-        throw new PSQLException(
-            GT.tr("The SSLSocketFactory class provided {0} could not be instantiated.", classname),
-            PSQLState.CONNECTION_FAILURE, e);
-      }
-    }
-
+    SSLSocketFactory factory = SocketFactoryFactory.getSslSocketFactory(info);
     SSLSocket newConnection;
     try {
       newConnection = (SSLSocket) factory.createSocket(stream.getSocket(),
           stream.getHostSpec().getHost(), stream.getHostSpec().getPort(), true);
       // We must invoke manually, otherwise the exceptions are hidden
+      newConnection.setUseClientMode(true);
       newConnection.startHandshake();
     } catch (IOException ex) {
-      if (factory instanceof LibPQFactory) { // throw any KeyManager exception
-        ((LibPQFactory) factory).throwKeyManagerException();
-      }
       throw new PSQLException(GT.tr("SSL error: {0}", ex.getMessage()),
           PSQLState.CONNECTION_FAILURE, ex);
     }
+    if (factory instanceof LibPQFactory) { // throw any KeyManager exception
+      ((LibPQFactory) factory).throwKeyManagerException();
+    }
 
+    SslMode sslMode = SslMode.of(info);
+    if (sslMode.verifyPeerName()) {
+      verifyPeerName(stream, info, newConnection);
+    }
+
+    stream.changeSocket(newConnection);
+  }
+
+  private static void verifyPeerName(PGStream stream, Properties info, SSLSocket newConnection)
+      throws PSQLException {
+    HostnameVerifier hvn;
     String sslhostnameverifier = PGProperty.SSL_HOSTNAME_VERIFIER.get(info);
-    if (sslhostnameverifier != null) {
-      HostnameVerifier hvn;
+    if (sslhostnameverifier == null) {
+      hvn = PGjdbcHostnameVerifier.INSTANCE;
+      sslhostnameverifier = "PgjdbcHostnameVerifier";
+    } else {
       try {
         hvn = (HostnameVerifier) instantiate(sslhostnameverifier, info, false, null);
       } catch (Exception e) {
@@ -79,24 +70,16 @@ public static void convert(PGStream stream, Properties info)
                 sslhostnameverifier),
             PSQLState.CONNECTION_FAILURE, e);
       }
-      if (!hvn.verify(stream.getHostSpec().getHost(), newConnection.getSession())) {
-        throw new PSQLException(
-            GT.tr("The hostname {0} could not be verified by hostnameverifier {1}.",
-                stream.getHostSpec().getHost(), sslhostnameverifier),
-            PSQLState.CONNECTION_FAILURE);
-      }
-    } else {
-      if ("verify-full".equals(sslmode) && factory instanceof LibPQFactory) {
-        if (!(((LibPQFactory) factory).verify(stream.getHostSpec().getHost(),
-            newConnection.getSession()))) {
-          throw new PSQLException(
-              GT.tr("The hostname {0} could not be verified.", stream.getHostSpec().getHost()),
-              PSQLState.CONNECTION_FAILURE);
-        }
-      }
+    }
 
+    if (hvn.verify(stream.getHostSpec().getHost(), newConnection.getSession())) {
+      return;
     }
-    stream.changeSocket(newConnection);
+
+    throw new PSQLException(
+        GT.tr("The hostname {0} could not be verified by hostnameverifier {1}.",
+            stream.getHostSpec().getHost(), sslhostnameverifier),
+        PSQLState.CONNECTION_FAILURE);
   }
 
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/PGjdbcHostnameVerifier.java b/pgjdbc/src/main/java/org/postgresql/ssl/PGjdbcHostnameVerifier.java
new file mode 100644
index 0000000000..851eb3aeb6
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/ssl/PGjdbcHostnameVerifier.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (c) 2018, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.ssl;
+
+import org.postgresql.util.GT;
+
+import java.net.IDN;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.security.auth.x500.X500Principal;
+
+public class PGjdbcHostnameVerifier implements HostnameVerifier {
+  private static final Logger LOGGER = Logger.getLogger(PGjdbcHostnameVerifier.class.getName());
+
+  public static final PGjdbcHostnameVerifier INSTANCE = new PGjdbcHostnameVerifier();
+
+  private static final int TYPE_DNS_NAME = 2;
+  private static final int TYPE_IP_ADDRESS = 7;
+
+  public static Comparator<String> HOSTNAME_PATTERN_COMPARATOR = new Comparator<String>() {
+    private int countChars(String value, char ch) {
+      int count = 0;
+      int pos = -1;
+      while (true) {
+        pos = value.indexOf(ch, pos + 1);
+        if (pos == -1) {
+          break;
+        }
+        count++;
+      }
+      return count;
+    }
+
+    @Override
+    public int compare(String o1, String o2) {
+      // The more the dots the better: a.b.c.postgresql.org is more specific than postgresql.org
+      int d1 = countChars(o1, '.');
+      int d2 = countChars(o2, '.');
+      if (d1 != d2) {
+        return d1 > d2 ? 1 : -1;
+      }
+
+      // The less the stars the better: postgresql.org is more specific than *.*.postgresql.org
+      int s1 = countChars(o1, '*');
+      int s2 = countChars(o2, '*');
+      if (s1 != s2) {
+        return s1 < s2 ? 1 : -1;
+      }
+
+      // The longer the better: postgresql.org is more specific than sql.org
+      int l1 = o1.length();
+      int l2 = o2.length();
+      if (l1 != l2) {
+        return l1 > l2 ? 1 : -1;
+      }
+
+      return 0;
+    }
+  };
+
+  @Override
+  public boolean verify(String hostname, SSLSession session) {
+    X509Certificate[] peerCerts;
+    try {
+      peerCerts = (X509Certificate[]) session.getPeerCertificates();
+    } catch (SSLPeerUnverifiedException e) {
+      LOGGER.log(Level.SEVERE,
+          GT.tr("Unable to parse X509Certificate for hostname {0}", hostname), e);
+      return false;
+    }
+    if (peerCerts == null || peerCerts.length == 0) {
+      LOGGER.log(Level.SEVERE,
+          GT.tr("No certificates found for hostname {0}", hostname));
+      return false;
+    }
+
+    String canonicalHostname;
+    if (hostname.startsWith("[") && hostname.endsWith("]")) {
+      // IPv6 address like [2001:db8:0:1:1:1:1:1]
+      canonicalHostname = hostname.substring(1, hostname.length() - 1);
+    } else {
+      // This converts unicode domain name to ASCII
+      try {
+        canonicalHostname = IDN.toASCII(hostname);
+        if (LOGGER.isLoggable(Level.FINEST)) {
+          LOGGER.log(Level.FINEST, "Canonical host name for {0} is {1}",
+              new Object[]{hostname, canonicalHostname});
+        }
+      } catch (IllegalArgumentException e) {
+        // e.g. hostname is invalid
+        LOGGER.log(Level.SEVERE,
+            GT.tr("Hostname {0} is invalid", hostname), e);
+        return false;
+      }
+    }
+
+    X509Certificate serverCert = peerCerts[0];
+
+    // Check for Subject Alternative Names (see RFC 6125)
+
+    Collection<List<?>> subjectAltNames;
+    try {
+      subjectAltNames = serverCert.getSubjectAlternativeNames();
+      if (subjectAltNames == null) {
+        subjectAltNames = Collections.emptyList();
+      }
+    } catch (CertificateParsingException e) {
+      LOGGER.log(Level.SEVERE,
+          GT.tr("Unable to parse certificates for hostname {0}", hostname), e);
+      return false;
+    }
+
+    boolean anyDnsSan = false;
+    /*
+     * Each item in the SAN collection is a 2-element list.
+     * See {@link X509Certificate#getSubjectAlternativeNames}
+     * The first element in each list is a number indicating the type of entry.
+     */
+    for (List<?> sanItem : subjectAltNames) {
+      if (sanItem.size() != 2) {
+        continue;
+      }
+      Integer sanType = (Integer) sanItem.get(0);
+      if (sanType == null) {
+        // just in case
+        continue;
+      }
+      if (sanType != TYPE_IP_ADDRESS && sanType != TYPE_DNS_NAME) {
+        continue;
+      }
+      String san = (String) sanItem.get(1);
+      if (sanType == TYPE_IP_ADDRESS && san.startsWith("*")) {
+        // Wildcards should not be present in the IP Address field
+        continue;
+      }
+      anyDnsSan |= sanType == TYPE_DNS_NAME;
+      if (verifyHostName(canonicalHostname, san)) {
+        if (LOGGER.isLoggable(Level.FINEST)) {
+          LOGGER.log(Level.SEVERE,
+              GT.tr("Server name validation pass for {0}, subjectAltName {1}", hostname, san));
+        }
+        return true;
+      }
+    }
+
+    if (anyDnsSan) {
+      /*
+       * RFC2818, section 3.1 (I bet you won't recheck :)
+       * If a subjectAltName extension of type dNSName is present, that MUST
+       * be used as the identity. Otherwise, the (most specific) Common Name
+       * field in the Subject field of the certificate MUST be used. Although
+       * the use of the Common Name is existing practice, it is deprecated and
+       * Certification Authorities are encouraged to use the dNSName instead.
+       */
+      LOGGER.log(Level.SEVERE,
+          GT.tr("Server name validation failed: certificate for host {0} dNSName entries subjectAltName,"
+              + " but none of them match. Assuming server name validation failed", hostname));
+      return false;
+    }
+
+    // Last attempt: no DNS Subject Alternative Name entries detected, try common name
+    LdapName DN;
+    try {
+      DN = new LdapName(serverCert.getSubjectX500Principal().getName(X500Principal.RFC2253));
+    } catch (InvalidNameException e) {
+      LOGGER.log(Level.SEVERE,
+          GT.tr("Server name validation failed: unable to extract common name"
+              + " from X509Certificate for hostname {0}", hostname), e);
+      return false;
+    }
+
+    List<String> commonNames = new ArrayList<String>(1);
+    for (Rdn rdn : DN.getRdns()) {
+      if ("CN".equals(rdn.getType())) {
+        commonNames.add((String) rdn.getValue());
+      }
+    }
+    if (commonNames.isEmpty()) {
+      LOGGER.log(Level.SEVERE,
+          GT.tr("Server name validation failed: certificate for hostname {0} has no DNS subjectAltNames,"
+                  + " and it CommonName is missing as well",
+              hostname));
+      return false;
+    }
+    if (commonNames.size() > 1) {
+      /*
+       * RFC2818, section 3.1
+       * If a subjectAltName extension of type dNSName is present, that MUST
+       * be used as the identity. Otherwise, the (most specific) Common Name
+       * field in the Subject field of the certificate MUST be used
+       *
+       * The sort is from less specific to most specific.
+       */
+      Collections.sort(commonNames, HOSTNAME_PATTERN_COMPARATOR);
+    }
+    String commonName = commonNames.get(commonNames.size() - 1);
+    boolean result = verifyHostName(canonicalHostname, commonName);
+    if (!result) {
+      LOGGER.log(Level.SEVERE,
+          GT.tr("Server name validation failed: hostname {0} does not match common name {1}",
+              hostname, commonName));
+    }
+    return result;
+  }
+
+  public boolean verifyHostName(String hostname, String pattern) {
+    if (hostname == null || pattern == null) {
+      return false;
+    }
+    int lastStar = pattern.lastIndexOf('*');
+    if (lastStar == -1) {
+      // No wildcard => just compare hostnames
+      return hostname.equalsIgnoreCase(pattern);
+    }
+    if (lastStar > 0) {
+      // Wildcards like foo*.com are not supported yet
+      return false;
+    }
+    if (pattern.indexOf('.') == -1) {
+      // Wildcard certificates should contain at least one dot
+      return false;
+    }
+    // pattern starts with *, so hostname should be at least (pattern.length-1) long
+    if (hostname.length() < pattern.length() - 1) {
+      return false;
+    }
+    // Use case insensitive comparison
+    final boolean ignoreCase = true;
+    // Below code is "hostname.endsWithIgnoreCase(pattern.withoutFirstStar())"
+
+    // E.g. hostname==sub.host.com; pattern==*.host.com
+    // We need to start the offset of ".host.com" in hostname
+    // For this we take hostname.length() - pattern.length()
+    // and +1 is required since pattern is known to start with *
+    int toffset = hostname.length() - pattern.length() + 1;
+
+    // Wildcard covers just one domain level
+    // a.b.c.com should not be covered by *.c.com
+    if (hostname.lastIndexOf('.', toffset - 1) >= 0) {
+      // If there's a dot in between 0..toffset
+      return false;
+    }
+
+    return hostname.regionMatches(ignoreCase, toffset,
+        pattern, 1, pattern.length() - 1);
+  }
+
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/jdbc4/LibPQFactory.java b/pgjdbc/src/main/java/org/postgresql/ssl/jdbc4/LibPQFactory.java
index 4aadabd4bb..7dbe9df63c 100644
--- a/pgjdbc/src/main/java/org/postgresql/ssl/jdbc4/LibPQFactory.java
+++ b/pgjdbc/src/main/java/org/postgresql/ssl/jdbc4/LibPQFactory.java
@@ -1,270 +1,63 @@
 /*
- * Copyright (c) 2004, PostgreSQL Global Development Group
+ * Copyright (c) 2017, PostgreSQL Global Development Group
  * See the LICENSE file in the project root for more information.
  */
 
 package org.postgresql.ssl.jdbc4;
 
-import org.postgresql.PGProperty;
-import org.postgresql.ssl.MakeSSL;
-import org.postgresql.ssl.NonValidatingFactory.NonValidatingTM;
-import org.postgresql.ssl.WrappedFactory;
-import org.postgresql.util.GT;
+import org.postgresql.jdbc.SslMode;
+import org.postgresql.ssl.PGjdbcHostnameVerifier;
 import org.postgresql.util.PSQLException;
-import org.postgresql.util.PSQLState;
 
-import java.io.Console;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.security.KeyManagementException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateFactory;
-import java.security.cert.CertificateParsingException;
-import java.security.cert.X509Certificate;
-import java.util.Collection;
-import java.util.List;
+import java.net.IDN;
 import java.util.Properties;
-
-import javax.naming.InvalidNameException;
-import javax.naming.ldap.LdapName;
-import javax.naming.ldap.Rdn;
 import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSession;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
-import javax.security.auth.callback.Callback;
-import javax.security.auth.callback.CallbackHandler;
-import javax.security.auth.callback.PasswordCallback;
-import javax.security.auth.callback.UnsupportedCallbackException;
-import javax.security.auth.x500.X500Principal;
 
 /**
- * Provide an SSLSocketFactory that is compatible with the libpq behaviour.
+ * @deprecated prefer {@link org.postgresql.ssl.LibPQFactory}
  */
-public class LibPQFactory extends WrappedFactory implements HostnameVerifier {
-
-  private static final int ALT_DNS_NAME = 2;
-
-  LazyKeyManager km = null;
-  String sslmode;
+@Deprecated
+public class LibPQFactory extends org.postgresql.ssl.LibPQFactory implements HostnameVerifier {
+  private final SslMode sslMode;
 
   /**
    * @param info the connection parameters The following parameters are used:
-   *        sslmode,sslcert,sslkey,sslrootcert,sslhostnameverifier,sslpasswordcallback,sslpassword
+   *             sslmode,sslcert,sslkey,sslrootcert,sslhostnameverifier,sslpasswordcallback,sslpassword
    * @throws PSQLException if security error appears when initializing factory
+   * @deprecated prefer {@link org.postgresql.ssl.LibPQFactory}
    */
+  @Deprecated
   public LibPQFactory(Properties info) throws PSQLException {
-    try {
-      sslmode = PGProperty.SSL_MODE.get(info);
-      SSLContext ctx = SSLContext.getInstance("TLS"); // or "SSL" ?
-
-      // Determining the default file location
-      String pathsep = System.getProperty("file.separator");
-      String defaultdir;
-      boolean defaultfile = false;
-      if (System.getProperty("os.name").toLowerCase().contains("windows")) { // It is Windows
-        defaultdir = System.getenv("APPDATA") + pathsep + "postgresql" + pathsep;
-      } else {
-        defaultdir = System.getProperty("user.home") + pathsep + ".postgresql" + pathsep;
-      }
-
-      // Load the client's certificate and key
-      String sslcertfile = PGProperty.SSL_CERT.get(info);
-      if (sslcertfile == null) { // Fall back to default
-        defaultfile = true;
-        sslcertfile = defaultdir + "postgresql.crt";
-      }
-      String sslkeyfile = PGProperty.SSL_KEY.get(info);
-      if (sslkeyfile == null) { // Fall back to default
-        defaultfile = true;
-        sslkeyfile = defaultdir + "postgresql.pk8";
-      }
-
-      // Determine the callback handler
-      CallbackHandler cbh;
-      String sslpasswordcallback = PGProperty.SSL_PASSWORD_CALLBACK.get(info);
-      if (sslpasswordcallback != null) {
-        try {
-          cbh = (CallbackHandler) MakeSSL.instantiate(sslpasswordcallback, info, false, null);
-        } catch (Exception e) {
-          throw new PSQLException(
-              GT.tr("The password callback class provided {0} could not be instantiated.",
-                  sslpasswordcallback),
-              PSQLState.CONNECTION_FAILURE, e);
-        }
-      } else {
-        cbh = new ConsoleCallbackHandler(PGProperty.SSL_PASSWORD.get(info));
-      }
-
-      // If the properties are empty, give null to prevent client key selection
-      km = new LazyKeyManager(("".equals(sslcertfile) ? null : sslcertfile),
-          ("".equals(sslkeyfile) ? null : sslkeyfile), cbh, defaultfile);
-
-      TrustManager[] tm;
-      if ("verify-ca".equals(sslmode) || "verify-full".equals(sslmode)) {
-        // Load the server certificate
-
-        TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
-        KeyStore ks;
-        try {
-          ks = KeyStore.getInstance("jks");
-        } catch (KeyStoreException e) {
-          // this should never happen
-          throw new NoSuchAlgorithmException("jks KeyStore not available");
-        }
-        String sslrootcertfile = PGProperty.SSL_ROOT_CERT.get(info);
-        if (sslrootcertfile == null) { // Fall back to default
-          sslrootcertfile = defaultdir + "root.crt";
-        }
-        FileInputStream fis;
-        try {
-          fis = new FileInputStream(sslrootcertfile); // NOSONAR
-        } catch (FileNotFoundException ex) {
-          throw new PSQLException(
-              GT.tr("Could not open SSL root certificate file {0}.", sslrootcertfile),
-              PSQLState.CONNECTION_FAILURE, ex);
-        }
-        try {
-          CertificateFactory cf = CertificateFactory.getInstance("X.509");
-          // Certificate[] certs = cf.generateCertificates(fis).toArray(new Certificate[]{}); //Does
-          // not work in java 1.4
-          Object[] certs = cf.generateCertificates(fis).toArray(new Certificate[]{});
-          ks.load(null, null);
-          for (int i = 0; i < certs.length; i++) {
-            ks.setCertificateEntry("cert" + i, (Certificate) certs[i]);
-          }
-          tmf.init(ks);
-        } catch (IOException ioex) {
-          throw new PSQLException(
-              GT.tr("Could not read SSL root certificate file {0}.", sslrootcertfile),
-              PSQLState.CONNECTION_FAILURE, ioex);
-        } catch (GeneralSecurityException gsex) {
-          throw new PSQLException(
-              GT.tr("Loading the SSL root certificate {0} into a TrustManager failed.",
-                      sslrootcertfile),
-              PSQLState.CONNECTION_FAILURE, gsex);
-        } finally {
-          try {
-            fis.close();
-          } catch (IOException e) {
-            /* ignore */
-          }
-        }
-        tm = tmf.getTrustManagers();
-      } else { // server validation is not required
-        tm = new TrustManager[]{new NonValidatingTM()};
-      }
+    super(info);
 
-      // finally we can initialize the context
-      try {
-        ctx.init(new KeyManager[]{km}, tm, null);
-      } catch (KeyManagementException ex) {
-        throw new PSQLException(GT.tr("Could not initialize SSL context."),
-            PSQLState.CONNECTION_FAILURE, ex);
-      }
-
-      _factory = ctx.getSocketFactory();
-    } catch (NoSuchAlgorithmException ex) {
-      throw new PSQLException(GT.tr("Could not find a java cryptographic algorithm: {0}.",
-              ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex);
-    }
+    sslMode = SslMode.of(info);
   }
 
   /**
-   * Propagates any exception from {@link LazyKeyManager}.
+   * Verifies if given hostname matches pattern.
    *
-   * @throws PSQLException if there is an exception to propagate
+   * @deprecated use {@link PGjdbcHostnameVerifier}
+   * @param hostname input hostname
+   * @param pattern domain name pattern
+   * @return true when domain matches pattern
    */
-  public void throwKeyManagerException() throws PSQLException {
-    if (km != null) {
-      km.throwKeyManagerException();
-    }
-  }
-
-  /**
-   * A CallbackHandler that reads the password from the console or returns the password given to its
-   * constructor.
-   */
-  static class ConsoleCallbackHandler implements CallbackHandler {
-
-    private char[] password = null;
-
-    ConsoleCallbackHandler(String password) {
-      if (password != null) {
-        this.password = password.toCharArray();
-      }
-    }
-
-    /**
-     * Handles the callbacks.
-     *
-     * @param callbacks The callbacks to handle
-     * @throws UnsupportedCallbackException If the console is not available or other than
-     *         PasswordCallback is supplied
-     */
-    @Override
-    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
-      Console cons = System.console();
-      if (cons == null && password == null) {
-        throw new UnsupportedCallbackException(callbacks[0], "Console is not available");
-      }
-      for (Callback callback : callbacks) {
-        if (callback instanceof PasswordCallback) {
-          if (password == null) {
-            // It is used instead of cons.readPassword(prompt), because the prompt may contain '%'
-            // characters
-            ((PasswordCallback) callback).setPassword(
-                cons.readPassword("%s", ((PasswordCallback) callback).getPrompt()));
-          } else {
-            ((PasswordCallback) callback).setPassword(password);
-          }
-        } else {
-          throw new UnsupportedCallbackException(callback);
-        }
-      }
-
-    }
-  }
-
+  @Deprecated
   public static boolean verifyHostName(String hostname, String pattern) {
-    if (hostname == null || pattern == null) {
-      return false;
-    }
-    if (!pattern.startsWith("*")) {
-      // No wildcard => just compare hostnames
-      return hostname.equalsIgnoreCase(pattern);
-    }
-    // pattern starts with *, so hostname should be at least (pattern.length-1) long
-    if (hostname.length() < pattern.length() - 1) {
-      return false;
-    }
-    // Compare ignore case
-    final boolean ignoreCase = true;
-    // Below code is "hostname.endsWithIgnoreCase(pattern.withoutFirstStar())"
-
-    // E.g. hostname==sub.host.com; pattern==*.host.com
-    // We need to start the offset of ".host.com" in hostname
-    // For this we take hostname.length() - pattern.length()
-    // and +1 is required since pattern is known to start with *
-    int toffset = hostname.length() - pattern.length() + 1;
-
-    // Wildcard covers just one domain level
-    // a.b.c.com should not be covered by *.c.com
-    if (hostname.lastIndexOf('.', toffset - 1) >= 0) {
-      // If there's a dot in between 0..toffset
-      return false;
+    String canonicalHostname;
+    if (hostname.startsWith("[") && hostname.endsWith("]")) {
+      // IPv6 address like [2001:db8:0:1:1:1:1:1]
+      canonicalHostname = hostname.substring(1, hostname.length() - 1);
+    } else {
+      // This converts unicode domain name to ASCII
+      try {
+        canonicalHostname = IDN.toASCII(hostname);
+      } catch (IllegalArgumentException e) {
+        // e.g. hostname is invalid
+        return false;
+      }
     }
-
-    return hostname.regionMatches(ignoreCase, toffset,
-        pattern, 1, pattern.length() - 1);
+    return PGjdbcHostnameVerifier.INSTANCE.verifyHostName(canonicalHostname, pattern);
   }
 
   /**
@@ -274,56 +67,18 @@ public static boolean verifyHostName(String hostname, String pattern) {
    * the certificate will not match subdomains. If the connection is made using an IP address
    * instead of a hostname, the IP address will be matched (without doing any DNS lookups).
    *
+   * @deprecated use PgjdbcHostnameVerifier
    * @param hostname Hostname or IP address of the server.
    * @param session The SSL session.
    * @return true if the certificate belongs to the server, false otherwise.
+   * @see PGjdbcHostnameVerifier
    */
+  @Deprecated
   public boolean verify(String hostname, SSLSession session) {
-    X509Certificate[] peerCerts;
-    try {
-      peerCerts = (X509Certificate[]) session.getPeerCertificates();
-    } catch (SSLPeerUnverifiedException e) {
-      return false;
+    if (!sslMode.verifyPeerName()) {
+      return true;
     }
-    if (peerCerts == null || peerCerts.length == 0) {
-      return false;
-    }
-    // Extract the common name
-    X509Certificate serverCert = peerCerts[0];
-
-    try {
-      // Check for Subject Alternative Names (see RFC 6125)
-      Collection<List<?>> subjectAltNames = serverCert.getSubjectAlternativeNames();
-
-      if (subjectAltNames != null) {
-        for (List<?> sanit : subjectAltNames) {
-          Integer type = (Integer) sanit.get(0);
-          String san = (String) sanit.get(1);
-
-          // this mimics libpq check for ALT_DNS_NAME
-          if (type != null && type == ALT_DNS_NAME && verifyHostName(hostname, san)) {
-            return true;
-          }
-        }
-      }
-    } catch (CertificateParsingException e) {
-      return false;
-    }
-
-    LdapName DN;
-    try {
-      DN = new LdapName(serverCert.getSubjectX500Principal().getName(X500Principal.RFC2253));
-    } catch (InvalidNameException e) {
-      return false;
-    }
-    String CN = null;
-    for (Rdn rdn : DN.getRdns()) {
-      if ("CN".equals(rdn.getType())) {
-        // Multiple AVAs are not treated
-        CN = (String) rdn.getValue();
-        break;
-      }
-    }
-    return verifyHostName(hostname, CN);
+    return PGjdbcHostnameVerifier.INSTANCE.verify(hostname, session);
   }
+
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/util/ObjectFactory.java b/pgjdbc/src/main/java/org/postgresql/util/ObjectFactory.java
index 4145bc54c0..273ac6d611 100644
--- a/pgjdbc/src/main/java/org/postgresql/util/ObjectFactory.java
+++ b/pgjdbc/src/main/java/org/postgresql/util/ObjectFactory.java
@@ -21,7 +21,7 @@
    * single String argument is searched if it fails, or tryString is true a no argument constructor
    * is tried.
    *
-   * @param classname Nam of the class to instantiate
+   * @param classname name of the class to instantiate
    * @param info parameter to pass as Properties
    * @param tryString weather to look for a single String argument constructor
    * @param stringarg parameter to pass as String
diff --git a/pgjdbc/src/test/java/org/postgresql/test/TestUtil.java b/pgjdbc/src/test/java/org/postgresql/test/TestUtil.java
index 8a3fb7283a..789736a6ed 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/TestUtil.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/TestUtil.java
@@ -32,14 +32,28 @@
  * Utility class for JDBC tests.
  */
 public class TestUtil {
+  /*
+   * The case is as follows:
+   * 1. Typically the database and hostname are taken from System.properties or build.properties or build.local.properties
+   *    That enables to override test DB via system property
+   * 2. There are tests where different DBs should be used (e.g. SSL tests), so we can't just use DB name from system property
+   *    That is why _test_ properties exist: they overpower System.properties and build.properties
+   */
+  public static final String SERVER_HOST_PORT_PROP = "_test_hostport";
+  public static final String DATABASE_PROP = "_test_database";
+
   /*
    * Returns the Test database JDBC URL
    */
   public static String getURL() {
-    return getURL(getServer(), getPort());
+    return getURL(getServer(), + getPort());
   }
 
   public static String getURL(String server, int port) {
+    return getURL(server + ":" + port, getDatabase());
+  }
+
+  public static String getURL(String hostport, String database) {
     String logLevel = "";
     if (getLogLevel() != null && !getLogLevel().equals("")) {
       logLevel = "&loggerLevel=" + getLogLevel();
@@ -76,9 +90,8 @@ public static String getURL(String server, int port) {
     }
 
     return "jdbc:postgresql://"
-        + server + ":"
-        + port + "/"
-        + getDatabase()
+        + hostport + "/"
+        + database
         + "?ApplicationName=Driver Tests"
         + logLevel
         + logFile
@@ -135,6 +148,13 @@ public static String getPassword() {
     return System.getProperty("password");
   }
 
+  /*
+   * Returns password for default callbackhandler
+   */
+  public static String getSslPassword() {
+    return System.getProperty(PGProperty.SSL_PASSWORD.getName());
+  }
+
   /*
    * Returns the user for SSPI authentication tests
    */
@@ -301,6 +321,11 @@ public static Connection openDB(Properties props) throws Exception {
       password = "";
     }
     props.setProperty("password", password);
+    String sslPassword = getSslPassword();
+    if (sslPassword != null) {
+      PGProperty.SSL_PASSWORD.set(props, sslPassword);
+    }
+
     if (!props.containsKey(PGProperty.PREPARE_THRESHOLD.getName())) {
       PGProperty.PREPARE_THRESHOLD.set(props, getPrepareThreshold());
     }
@@ -310,8 +335,11 @@ public static Connection openDB(Properties props) throws Exception {
         props.put(PGProperty.PREFER_QUERY_MODE.getName(), value);
       }
     }
+    // Enable Base4 tests to override host,port,database
+    String hostport = props.getProperty(SERVER_HOST_PORT_PROP, getServer() + ":" + getPort());
+    String database = props.getProperty(DATABASE_PROP, getDatabase());
 
-    return DriverManager.getConnection(getURL(), props);
+    return DriverManager.getConnection(getURL(hostport, database), props);
   }
 
   /*
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/NotifyTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/NotifyTest.java
index b9615b0967..faaa82bd45 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/NotifyTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/NotifyTest.java
@@ -9,6 +9,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
+import org.postgresql.PGConnection;
 import org.postgresql.PGNotification;
 import org.postgresql.core.ServerVersion;
 import org.postgresql.test.TestUtil;
@@ -41,7 +42,7 @@ public void testNotify() throws SQLException {
     stmt.executeUpdate("LISTEN mynotification");
     stmt.executeUpdate("NOTIFY mynotification");
 
-    PGNotification[] notifications = ((org.postgresql.PGConnection) conn).getNotifications();
+    PGNotification[] notifications = conn.unwrap(PGConnection.class).getNotifications();
     assertNotNull(notifications);
     assertEquals(1, notifications.length);
     assertEquals("mynotification", notifications[0].getName());
@@ -60,7 +61,7 @@ public void testNotifyArgument() throws Exception {
     stmt.executeUpdate("LISTEN mynotification");
     stmt.executeUpdate("NOTIFY mynotification, 'message'");
 
-    PGNotification[] notifications = ((org.postgresql.PGConnection) conn).getNotifications();
+    PGNotification[] notifications = conn.unwrap(PGConnection.class).getNotifications();
     assertNotNull(notifications);
     assertEquals(1, notifications.length);
     assertEquals("mynotification", notifications[0].getName());
@@ -83,13 +84,14 @@ public void testAsyncNotify() throws Exception {
     try {
       int retries = 20;
       while (retries-- > 0
-        && (notifications = ((org.postgresql.PGConnection) conn).getNotifications()) == null ) {
+        && (notifications = conn.unwrap(PGConnection.class).getNotifications()) == null ) {
         Thread.sleep(100);
       }
     } catch (InterruptedException ie) {
     }
 
-    assertNotNull(notifications);
+    assertNotNull("Notification is expected to be delivered when subscription was created"
+            + " before sending notification", notifications);
     assertEquals(1, notifications.length);
     assertEquals("mynotification", notifications[0].getName());
     assertEquals("", notifications[0].getParameter());
@@ -108,7 +110,7 @@ public void testAsyncNotifyWithTimeout() throws Exception {
 
     // Here we let the getNotifications() timeout.
     long startMillis = System.currentTimeMillis();
-    PGNotification[] notifications = ((org.postgresql.PGConnection) conn).getNotifications(500);
+    PGNotification[] notifications = conn.unwrap(PGConnection.class).getNotifications(500);
     long endMillis = System.currentTimeMillis();
     long runtime = endMillis - startMillis;
     assertNull("There have been notifications, although none have been expected.",notifications);
@@ -126,7 +128,7 @@ public void testAsyncNotifyWithTimeoutAndMessagesAvailableWhenStartingListening(
     // listen for notifications
     connectAndNotify("mynotification");
 
-    PGNotification[] notifications = ((org.postgresql.PGConnection) conn).getNotifications(10000);
+    PGNotification[] notifications = conn.unwrap(PGConnection.class).getNotifications(10000);
     assertNotNull(notifications);
     assertEquals(1, notifications.length);
     assertEquals("mynotification", notifications[0].getName());
@@ -143,7 +145,7 @@ public void testAsyncNotifyWithEndlessTimeoutAndMessagesAvailableWhenStartingLis
     // Now we check the case where notifications are already available while we are waiting forever
     connectAndNotify("mynotification");
 
-    PGNotification[] notifications = ((org.postgresql.PGConnection) conn).getNotifications(0);
+    PGNotification[] notifications = conn.unwrap(PGConnection.class).getNotifications(0);
     assertNotNull(notifications);
     assertEquals(1, notifications.length);
     assertEquals("mynotification", notifications[0].getName());
@@ -169,7 +171,7 @@ public void run() {
       }
     }).start();
 
-    PGNotification[] notifications = ((org.postgresql.PGConnection) conn).getNotifications(10000);
+    PGNotification[] notifications = conn.unwrap(PGConnection.class).getNotifications(10000);
     assertNotNull(notifications);
     assertEquals(1, notifications.length);
     assertEquals("mynotification", notifications[0].getName());
@@ -195,7 +197,7 @@ public void run() {
       }
     }).start();
 
-    PGNotification[] notifications = ((org.postgresql.PGConnection) conn).getNotifications(0);
+    PGNotification[] notifications = conn.unwrap(PGConnection.class).getNotifications(0);
     assertNotNull(notifications);
     assertEquals(1, notifications.length);
     assertEquals("mynotification", notifications[0].getName());
@@ -226,7 +228,7 @@ public void run() {
     }).start();
 
     try {
-      ((org.postgresql.PGConnection) conn).getNotifications(40000);
+      conn.unwrap(PGConnection.class).getNotifications(40000);
       Assert.fail("The getNotifications(...) call didn't return when the socket closed.");
     } catch (SQLException e) {
       // We expected that
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/Jdbc4TestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/Jdbc4TestSuite.java
index 46fcc4bb47..405d977fb8 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/Jdbc4TestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/Jdbc4TestSuite.java
@@ -24,7 +24,6 @@
         BinaryStreamTest.class,
         CharacterStreamTest.class,
         UUIDTest.class,
-        LibPQFactoryHostNameTest.class,
         XmlTest.class
 })
 public class Jdbc4TestSuite {
diff --git a/pgjdbc/src/test/java/org/postgresql/test/ssl/CommonNameVerifierTest.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/CommonNameVerifierTest.java
new file mode 100644
index 0000000000..a162c90e05
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/CommonNameVerifierTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2018, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.ssl;
+
+import org.postgresql.ssl.PGjdbcHostnameVerifier;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+
+@RunWith(Parameterized.class)
+public class CommonNameVerifierTest {
+
+  private final String a;
+  private final String b;
+  private final int expected;
+
+  public CommonNameVerifierTest(String a, String b, int expected) {
+    this.a = a;
+    this.b = b;
+    this.expected = expected;
+  }
+
+  @Parameterized.Parameters(name = "a={0}, b={1}")
+  public static Iterable<Object[]> data() {
+    return Arrays.asList(new Object[][]{
+        {"com", "host.com", -1},
+        {"*.com", "host.com", -1},
+        {"*.com", "*.*.com", -1},
+        {"**.com", "*.com", -1},
+        {"a.com", "*.host.com", -1},
+        {"host.com", "subhost.host.com", -1},
+        {"host.com", "host.com", 0}
+    });
+  }
+
+  @Test
+  public void comparePatterns() throws Exception {
+    Assert.assertEquals(a + " vs " + b,
+        expected, PGjdbcHostnameVerifier.HOSTNAME_PATTERN_COMPARATOR.compare(a, b));
+
+    Assert.assertEquals(b + " vs " + a,
+        -expected, PGjdbcHostnameVerifier.HOSTNAME_PATTERN_COMPARATOR.compare(b, a));
+  }
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/LibPQFactoryHostNameTest.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/LibPQFactoryHostNameTest.java
similarity index 79%
rename from pgjdbc/src/test/java/org/postgresql/test/jdbc4/LibPQFactoryHostNameTest.java
rename to pgjdbc/src/test/java/org/postgresql/test/ssl/LibPQFactoryHostNameTest.java
index 1ee140c4ee..0d998b777d 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/LibPQFactoryHostNameTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/LibPQFactoryHostNameTest.java
@@ -3,8 +3,9 @@
  * See the LICENSE file in the project root for more information.
  */
 
-package org.postgresql.test.jdbc4;
+package org.postgresql.test.ssl;
 
+import org.postgresql.ssl.PGjdbcHostnameVerifier;
 import org.postgresql.ssl.jdbc4.LibPQFactory;
 
 import org.junit.Assert;
@@ -47,11 +48,16 @@ public LibPQFactoryHostNameTest(String hostname, String pattern, boolean expecte
         {"sub.host.com", "*.hoSt.com", true},
         {"*.host.com", "host.com", false},
         {"sub.sub.host.com", "*.host.com", false}, // Wildcard should cover just one level
+        {"com", "*", false}, // Wildcard should have al least one dot
     });
   }
 
   @Test
   public void checkPattern() throws Exception {
-    Assert.assertEquals(expected, LibPQFactory.verifyHostName(hostname, pattern));
+    Assert.assertEquals(hostname + ", pattern: " + pattern,
+        expected, LibPQFactory.verifyHostName(hostname, pattern));
+
+    Assert.assertEquals(hostname + ", pattern: " + pattern,
+        expected, PGjdbcHostnameVerifier.INSTANCE.verifyHostName(hostname, pattern));
   }
 }
diff --git a/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTest.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTest.java
index fa209a33fc..adfc1dda90 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTest.java
@@ -1,336 +1,481 @@
 /*
- * Copyright (c) 2004, PostgreSQL Global Development Group
+ * Copyright (c) 2018, PostgreSQL Global Development Group
  * See the LICENSE file in the project root for more information.
  */
 
 package org.postgresql.test.ssl;
 
+import org.postgresql.PGProperty;
+import org.postgresql.jdbc.SslMode;
 import org.postgresql.test.TestUtil;
+import org.postgresql.test.jdbc2.BaseTest4;
+import org.postgresql.util.PSQLState;
 
-import junit.framework.TestCase;
-import junit.framework.TestSuite;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
 import java.io.File;
-import java.sql.Connection;
-import java.sql.DriverManager;
+import java.io.FileNotFoundException;
+import java.net.SocketException;
+import java.security.cert.CertPathValidatorException;
 import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.util.Map;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 import java.util.Properties;
-import java.util.TreeMap;
+import javax.net.ssl.SSLHandshakeException;
 
-public class SslTest extends TestCase {
+@RunWith(Parameterized.class)
+public class SslTest extends BaseTest4 {
+  enum Hostname {
+    GOOD("localhost"),
+    BAD("127.0.0.1"),
+    ;
 
-  /**
-   * Tries to connect to the database.
-   *
-   * @param connstr Connection string for the database
-   * @param expected Expected values. the first element is a String holding the expected message of
-   *        PSQLException or null, if no exception is expected, the second indicates weather ssl is
-   *        to be used (Boolean)
-   */
-  protected void driver(String connstr, Object[] expected) throws SQLException {
-    Connection conn = null;
-    String exmsg = (String) expected[0];
-    try {
-      conn = DriverManager.getConnection(connstr, TestUtil.getUser(), TestUtil.getPassword());
-      if (exmsg != null) {
-        fail("Exception did not occur: " + exmsg);
+    final String value;
+
+    Hostname(String value) {
+      this.value = value;
+    }
+  }
+
+  enum TestDatabase {
+    hostdb,
+    hostnossldb,
+    hostssldb,
+    hostsslcertdb,
+    certdb,
+    ;
+
+    public static final TestDatabase[] VALUES = values();
+
+    public boolean requiresClientCert() {
+      return this == certdb || this == hostsslcertdb;
+    }
+
+    public boolean requiresSsl() {
+      return this == certdb || this == hostssldb || this == hostsslcertdb;
+    }
+
+    public boolean rejectsSsl() {
+      return this == hostnossldb;
+    }
+  }
+
+  enum ClientCertificate {
+    EMPTY(""),
+    GOOD("goodclient"),
+    BAD("badclient"),
+    ;
+
+    public static final ClientCertificate[] VALUES = values();
+    public final String fileName;
+
+    ClientCertificate(String fileName) {
+      this.fileName = fileName;
+    }
+  }
+
+  enum ClientRootCertificate {
+    EMPTY(""),
+    GOOD("goodroot"),
+    BAD("badroot"),
+    ;
+
+    public static final ClientRootCertificate[] VALUES = values();
+    public final String fileName;
+
+    ClientRootCertificate(String fileName) {
+      this.fileName = fileName;
+    }
+  }
+
+  @Parameterized.Parameter(0)
+  public Hostname host;
+
+  @Parameterized.Parameter(1)
+  public TestDatabase db;
+
+  @Parameterized.Parameter(2)
+  public SslMode sslmode;
+
+  @Parameterized.Parameter(3)
+  public ClientCertificate clientCertificate;
+
+  @Parameterized.Parameter(4)
+  public ClientRootCertificate clientRootCertificate;
+
+  @Parameterized.Parameter(5)
+  public String certdir;
+
+  @Parameterized.Parameters(name = "host={0}, db={1} sslMode={2}, cCert={3}, cRootCert={4}")
+  public static Iterable<Object[]> data() {
+    Properties prop = TestUtil.loadPropertyFiles("ssltest.properties");
+    String enableSslTests = prop.getProperty("enable_ssl_tests");
+    if (!Boolean.valueOf(enableSslTests)) {
+      System.out.println("enableSslTests is " + enableSslTests + ", skipping SSL tests");
+      return Collections.emptyList();
+    }
+
+    Collection<Object[]> tests = new ArrayList<Object[]>();
+
+
+    File certDirFile = TestUtil.getFile(prop.getProperty("certdir"));
+    String certdir = certDirFile.getAbsolutePath();
+
+    for (SslMode sslMode : SslMode.VALUES) {
+      for (Hostname hostname : Hostname.values()) {
+        for (TestDatabase database : TestDatabase.VALUES) {
+          for (ClientCertificate clientCertificate : ClientCertificate.VALUES) {
+            for (ClientRootCertificate rootCertificate : ClientRootCertificate.VALUES) {
+              if ((sslMode == SslMode.DISABLE
+                  || database.rejectsSsl())
+                  && (clientCertificate != ClientCertificate.GOOD
+                  || rootCertificate != ClientRootCertificate.GOOD)) {
+                // When SSL is disabled, it does not make sense to verify "bad certificates"
+                // since certificates are NOT used in plaintext connections
+                continue;
+              }
+              if (database.rejectsSsl()
+                  && (sslMode.verifyCertificate()
+                      || hostname == Hostname.BAD)
+              ) {
+                // DB would reject SSL connection, so it makes no sense to test cases like verify-full
+                continue;
+              }
+              tests.add(
+                  new Object[]{hostname, database, sslMode, clientCertificate, rootCertificate,
+                      certdir});
+            }
+          }
+        }
       }
-      //
-      ResultSet rs = conn.createStatement().executeQuery("select ssl_is_used()");
-      assertTrue(rs.next());
-      assertEquals("ssl_is_used: ", ((Boolean) expected[1]).booleanValue(), rs.getBoolean(1));
-      conn.close();
+    }
+
+    return tests;
+  }
+
+  @Override
+  protected void updateProperties(Properties props) {
+    super.updateProperties(props);
+    props.put(TestUtil.SERVER_HOST_PORT_PROP, host.value + ":" + TestUtil.getPort());
+    props.put(TestUtil.DATABASE_PROP, db.toString());
+    PGProperty.SSL_MODE.set(props, sslmode.value);
+    if (clientCertificate == ClientCertificate.EMPTY) {
+      PGProperty.SSL_CERT.set(props, "");
+      PGProperty.SSL_KEY.set(props, "");
+    } else {
+      PGProperty.SSL_CERT.set(props,
+          certdir + "/" + clientCertificate.fileName + ".crt");
+      PGProperty.SSL_KEY.set(props,
+          certdir + "/" + clientCertificate.fileName + ".pk8");
+    }
+    if (clientRootCertificate == ClientRootCertificate.EMPTY) {
+      PGProperty.SSL_ROOT_CERT.set(props, "");
+    } else {
+      PGProperty.SSL_ROOT_CERT.set(props,
+          certdir + "/" + clientRootCertificate.fileName + ".crt");
+    }
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    SQLException e = null;
+    try {
+      super.setUp();
     } catch (SQLException ex) {
-      if (conn != null) {
-        conn.close();
+      e = ex;
+    }
+
+    try {
+      // Note that checkErrorCodes throws AssertionError for unexpected cases
+      checkErrorCodes(e);
+    } catch (AssertionError ae) {
+      // Make sure original SQLException is printed as well even in case of AssertionError
+      if (e != null) {
+        ae.initCause(e);
+      }
+      throw ae;
+    }
+  }
+
+  private void assertClientCertRequired(SQLException e, String caseName) {
+    if (e == null) {
+      Assert.fail(caseName + " should result in failure of client validation");
+    }
+    Assert.assertEquals(caseName + " ==> CONNECTION_FAILURE is expected",
+        PSQLState.INVALID_AUTHORIZATION_SPECIFICATION.getState(), e.getSQLState());
+  }
+
+  private void checkErrorCodes(SQLException e) {
+    if (e == null && sslmode == SslMode.ALLOW && !db.requiresSsl()) {
+      // allowed to connect with plain connection
+      return;
+    }
+
+    if (clientRootCertificate == ClientRootCertificate.EMPTY
+        && (sslmode == SslMode.VERIFY_CA || sslmode == SslMode.VERIFY_FULL)) {
+      String caseName = "rootCertificate is missing and sslmode=" + sslmode;
+      if (e == null) {
+        Assert.fail(caseName + " should result in FileNotFound exception for root certificate");
+      }
+      Assert.assertEquals(caseName + " ==> CONNECTION_FAILURE is expected",
+          PSQLState.CONNECTION_FAILURE.getState(), e.getSQLState());
+      FileNotFoundException fnf = findCause(e, FileNotFoundException.class);
+      if (fnf == null) {
+        Assert.fail(caseName + " ==> FileNotFoundException should be present in getCause chain");
+      }
+      return;
+    }
+
+    if (db.requiresSsl() && sslmode == SslMode.DISABLE) {
+      String caseName = "sslmode=DISABLE and database " + db + " requires SSL";
+      if (e == null) {
+        Assert.fail(caseName + " should result in connection failure");
+      }
+      Assert.assertEquals(caseName + " ==> INVALID_AUTHORIZATION_SPECIFICATION is expected",
+          PSQLState.INVALID_AUTHORIZATION_SPECIFICATION.getState(), e.getSQLState());
+      return;
+    }
+
+    if (db.rejectsSsl() && sslmode.requireEncryption()) {
+      String caseName =
+          "database " + db + " rejects SSL, and sslmode " + sslmode + " requires encryption";
+      if (e == null) {
+        Assert.fail(caseName + " should result in connection failure");
       }
-      if (exmsg == null) { // no exception is excepted
-        fail("Exception thrown: " + ex.getMessage());
+      Assert.assertEquals(caseName + " ==> INVALID_AUTHORIZATION_SPECIFICATION is expected",
+          PSQLState.INVALID_AUTHORIZATION_SPECIFICATION.getState(), e.getSQLState());
+      return;
+    }
+
+    // Server certificate, server hostname, and client certificate can be validated in any order
+    // So we have three validators and expect at least one of them to match
+    List<AssertionError> errors = null;
+    try {
+      if (assertServerCertificate(e)) {
+        return;
+      }
+    } catch (AssertionError ae) {
+      errors = addError(errors, ae);
+    }
+
+    try {
+      if (assertServerHostname(e)) {
+        return;
+      }
+    } catch (AssertionError ae) {
+      errors = addError(errors, ae);
+    }
+
+
+    try {
+      if (assertClientCertificate(e)) {
+        return;
+      }
+    } catch (AssertionError ae) {
+      errors = addError(errors, ae);
+    }
+
+    if (sslmode == SslMode.ALLOW && db.requiresSsl()) {
+      // Allow tries to connect with non-ssl first, and it always throws the first error even after try SSL.
+      // "If SSL was expected to fail" (e.g. invalid certificate), and db requiresSsl, then ALLOW
+      // should fail as well
+      String caseName =
+          "sslmode=ALLOW and db " + db + " requires SSL, and there are expected SSL failures";
+      if (errors == null) {
+        if (e != null) {
+          Assert.fail(caseName + " ==> connection should be upgraded to SSL with no failures");
+        }
       } else {
-        assertTrue("expected: " + exmsg + " actual: " + ex.getMessage(),
-            ex.getMessage().matches(exmsg));
+        if (e == null) {
+          Assert.fail(caseName + " ==> connection should fail");
+        }
+        Assert.assertEquals(caseName + " ==> INVALID_AUTHORIZATION_SPECIFICATION is expected",
+            PSQLState.INVALID_AUTHORIZATION_SPECIFICATION.getState(), e.getSQLState());
+      }
+      // ALLOW is ok
+      return;
+    }
+
+    if (errors == null) {
+      if (e == null) {
+        // Assume "no exception" was expected.
+        // The cases like "successfully connected in sslmode=DISABLE to SSLONLY db"
+        // should be handled with assertions above
         return;
       }
+      Assert.fail("SQLException present when it was not expected");
+    }
+
+    AssertionError firstError = errors.get(0);
+    if (errors.size() == 1) {
+      throw firstError;
+    }
+
+    for (int i = 1; i < errors.size(); i++) {
+      AssertionError error = errors.get(i);
+      // addSuppressed is Java 1.7+
+      //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.1"
+      firstError.addSuppressed(error);
+      //#endif
+      error.printStackTrace();
     }
-  }
 
-  protected String certdir;
-  protected String connstr;
-  protected String sslmode;
-  protected boolean goodclient;
-  protected boolean goodserver;
-  protected String prefix;
-  protected Object[] expected;
-
-  private String makeConnStr(String sslmode, boolean goodclient, boolean goodserver) {
-    return connstr
-        + "&sslmode=" + sslmode
-        + "&sslcert=" + certdir + "/" + prefix + (goodclient ? "goodclient.crt" : "badclient.crt")
-        + "&sslkey=" + certdir + "/" + prefix + (goodclient ? "goodclient.pk8" : "badclient.pk8")
-        + "&sslrootcert=" + certdir + "/" + prefix + (goodserver ? "goodroot.crt" : "badroot.crt")
-        // + "&sslfactory=org.postgresql.ssl.NonValidatingFactory"
-        + "&loglevel=" + TestUtil.getLogLevel();
+    throw firstError;
   }
 
-  public SslTest(String name, String certdir, String connstr, String sslmode,
-      boolean goodclient, boolean goodserver, String prefix, Object[] expected) {
-    super(name);
-    this.certdir = certdir;
-    this.connstr = connstr;
-    this.sslmode = sslmode;
-    this.goodclient = goodclient;
-    this.goodserver = goodserver;
-    this.prefix = prefix;
-    this.expected = expected;
+  private List<AssertionError> addError(List<AssertionError> errors, AssertionError ae) {
+    if (errors == null) {
+      errors = new ArrayList<AssertionError>();
+    }
+    errors.add(ae);
+    return errors;
   }
 
-  static TestSuite getSuite(Properties prop, String param) {
-    File certDirFile = TestUtil.getFile(prop.getProperty("certdir"));
-    String certdir = certDirFile.getAbsolutePath();
-    String sconnstr = prop.getProperty(param);
-    String sprefix = prop.getProperty(param + "prefix");
-    String[] sslModes = {"disable", "allow", "prefer", "require", "verify-ca", "verify-full"};
-
-    TestSuite suite = new TestSuite();
-    Map<String, Object[]> expected = expectedmap.get(param);
-    if (expected == null) {
-      expected = defaultexpected;
-    }
-    for (String sslMode : sslModes) {
-      suite.addTest(new SslTest(param + "-" + sslMode + "GG3", certdir, sconnstr, sslMode,
-           true, true, sprefix, expected.get(sslMode + "GG")));
-      suite.addTest(new SslTest(param + "-" + sslMode + "GB3", certdir, sconnstr, sslMode,
-           true, false, sprefix, expected.get(sslMode + "GB")));
-      suite.addTest(new SslTest(param + "-" + sslMode + "BG3", certdir, sconnstr, sslMode,
-           false, true, sprefix, expected.get(sslMode + "BG")));
-    }
-    return suite;
+  /**
+   * Checks server certificate validation error.
+   *
+   * @param e connection exception or null if no exception
+   * @return true when validation pass, false when the case is not applicable
+   * @throws AssertionError when exception does not match expectations
+   */
+  private boolean assertServerCertificate(SQLException e) {
+    if (clientRootCertificate == ClientRootCertificate.GOOD
+        || (sslmode != SslMode.VERIFY_CA && sslmode != SslMode.VERIFY_FULL)) {
+      return false;
+    }
+
+    String caseName = "Server certificate is " + clientRootCertificate + " + sslmode=" + sslmode;
+    if (e == null) {
+      Assert.fail(caseName + " should result in failure of server validation");
+    }
+
+    Assert.assertEquals(caseName + " ==> CONNECTION_FAILURE is expected",
+        PSQLState.CONNECTION_FAILURE.getState(), e.getSQLState());
+    CertPathValidatorException validatorEx = findCause(e, CertPathValidatorException.class);
+    if (validatorEx == null) {
+      Assert.fail(caseName + " ==> exception should be caused by CertPathValidatorException,"
+          + " but no CertPathValidatorException is present in the getCause chain");
+    }
+    // getReason is Java 1.7+
+    //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.1"
+    Assert.assertEquals(caseName + " ==> CertPathValidatorException.getReason",
+        "NO_TRUST_ANCHOR", validatorEx.getReason().toString());
+    //#endif
+    return true;
   }
 
-  protected void runTest() throws Throwable {
-    driver(makeConnStr(sslmode, goodclient, goodserver), expected);
+  /**
+   * Checks hostname validation error.
+   *
+   * @param e connection exception or null if no exception
+   * @return true when validation pass, false when the case is not applicable
+   * @throws AssertionError when exception does not match expectations
+   */
+  private boolean assertServerHostname(SQLException e) {
+    if (sslmode != SslMode.VERIFY_FULL || host != Hostname.BAD) {
+      return false;
+    }
+
+    String caseName = "VERIFY_FULL + hostname that does not match server certificate";
+    if (e == null) {
+      Assert.fail(caseName + " ==> CONNECTION_FAILURE expected");
+    }
+    Assert.assertEquals(caseName + " ==> CONNECTION_FAILURE is expected",
+        PSQLState.CONNECTION_FAILURE.getState(), e.getSQLState());
+    if (!e.getMessage().contains("PgjdbcHostnameVerifier")) {
+      Assert.fail(caseName + " ==> message should contain"
+          + " 'PgjdbcHostnameVerifier'. Actual message is " + e.getMessage());
+    }
+    return true;
   }
 
-  static Map<String, Map<String, Object[]>> expectedmap;
-  static TreeMap<String, Object[]> defaultexpected;
-
-  // For some strange reason, the v2 driver begins these error messages by "Connection rejected: "
-  // but the v3 does not.
-  // Also, for v2 there are two spaces after FATAL:, and the message ends with "\n.".
-  static String PG_HBA_ON =
-      "(Connection rejected: )?FATAL:  ?no pg_hba.conf entry for host .*, user .*, database .*, SSL on(?s-d:.*)";
-  static String PG_HBA_OFF =
-      "(Connection rejected: )?FATAL:  ?no pg_hba.conf entry for host .*, user .*, database .*, SSL off(?s-d:.*)";
-  static String FAILED = "The connection attempt failed.";
-  static String BROKEN =
-      "SSL error: (Broken pipe( \\(Write failed\\))?|Received fatal alert: unknown_ca|Connection reset|Protocol wrong type for socket)";
-  static String SSLMODEALLOW  = "Invalid sslmode value: allow";
-  static String SSLMODEPREFER  = "Invalid sslmode value: prefer";
-  // static String UNKNOWN = "SSL error: Broken pipe";
-  //static String UNKNOWN = "SSL error: Received fatal alert: unknown_ca";
-  static String ANY = ".*";
-  static String VALIDATOR =
-      "SSL error: sun.security.validator.ValidatorException: PKIX path (building|validation) failed:.*";
-  static String HOSTNAME = "The hostname .* could not be verified.";
-
-  static {
-    defaultexpected = new TreeMap<String, Object[]>();
-    defaultexpected.put("disableGG", new Object[]{null, Boolean.FALSE});
-    defaultexpected.put("disableGB", new Object[]{null, Boolean.FALSE});
-    defaultexpected.put("disableBG", new Object[]{null, Boolean.FALSE});
-    defaultexpected.put("allowGG", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    defaultexpected.put("allowGB", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    defaultexpected.put("allowBG", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    defaultexpected.put("preferGG", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    defaultexpected.put("preferGB", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    defaultexpected.put("preferBG", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    defaultexpected.put("requireGG", new Object[]{null, Boolean.TRUE});
-    defaultexpected.put("requireGB", new Object[]{null, Boolean.TRUE});
-    defaultexpected.put("requireBG", new Object[]{null, Boolean.TRUE});
-    defaultexpected.put("verify-caGG", new Object[]{null, Boolean.TRUE});
-    defaultexpected.put("verify-caGB", new Object[]{ANY, Boolean.TRUE});
-    defaultexpected.put("verify-caBG", new Object[]{null, Boolean.TRUE});
-    defaultexpected.put("verify-fullGG", new Object[]{null, Boolean.TRUE});
-    defaultexpected.put("verify-fullGB", new Object[]{ANY, Boolean.TRUE});
-    defaultexpected.put("verify-fullBG", new Object[]{null, Boolean.TRUE});
-
-    expectedmap = new TreeMap<String, Map<String, Object[]>>();
-    TreeMap<String, Object[]> work;
-
-    work = (TreeMap) defaultexpected.clone();
-    work.put("disableGG", new Object[]{null, Boolean.FALSE});
-    work.put("disableGB", new Object[]{null, Boolean.FALSE});
-    work.put("disableBG", new Object[]{null, Boolean.FALSE});
-    work.put("allowGG", new Object[]{SSLMODEALLOW, Boolean.FALSE});
-    work.put("allowGB", new Object[]{SSLMODEALLOW, Boolean.FALSE});
-    work.put("allowBG", new Object[]{SSLMODEALLOW, Boolean.FALSE});
-    work.put("preferGG", new Object[]{SSLMODEPREFER, Boolean.FALSE});
-    work.put("preferGB", new Object[]{SSLMODEPREFER, Boolean.FALSE});
-    work.put("preferBG", new Object[]{SSLMODEPREFER, Boolean.FALSE});
-    work.put("requireGG", new Object[]{ANY, Boolean.TRUE});
-    work.put("requireGB", new Object[]{ANY, Boolean.TRUE});
-    work.put("requireBG", new Object[]{ANY, Boolean.TRUE});
-    work.put("verify-caGG", new Object[]{ANY, Boolean.TRUE});
-    work.put("verify-caGB", new Object[]{ANY, Boolean.TRUE});
-    work.put("verify-caBG", new Object[]{ANY, Boolean.TRUE});
-    work.put("verify-fullGG", new Object[]{ANY, Boolean.TRUE});
-    work.put("verify-fullGB", new Object[]{ANY, Boolean.TRUE});
-    work.put("verify-fullBG", new Object[]{ANY, Boolean.TRUE});
-    expectedmap.put("ssloff9", work);
-
-    work = (TreeMap) defaultexpected.clone();
-    work.put("disableGG", new Object[]{null, Boolean.FALSE});
-    work.put("disableGB", new Object[]{null, Boolean.FALSE});
-    work.put("disableBG", new Object[]{null, Boolean.FALSE});
-    work.put("allowGG", new Object[]{SSLMODEALLOW, Boolean.FALSE});
-    work.put("allowGB", new Object[]{SSLMODEALLOW, Boolean.FALSE});
-    work.put("allowBG", new Object[]{SSLMODEALLOW, Boolean.FALSE});
-    work.put("preferGG", new Object[]{SSLMODEPREFER, Boolean.FALSE});
-    work.put("preferGB", new Object[]{SSLMODEPREFER, Boolean.FALSE});
-    work.put("preferBG", new Object[]{SSLMODEPREFER, Boolean.FALSE});
-    work.put("requireGG", new Object[]{PG_HBA_ON, Boolean.TRUE});
-    work.put("requireGB", new Object[]{PG_HBA_ON, Boolean.TRUE});
-    work.put("requireBG", new Object[]{BROKEN, Boolean.TRUE});
-    work.put("verify-caGG", new Object[]{PG_HBA_ON, Boolean.TRUE});
-    work.put("verify-caGB", new Object[]{VALIDATOR, Boolean.TRUE});
-    work.put("verify-caBG", new Object[]{BROKEN, Boolean.TRUE});
-    work.put("verify-fullGG", new Object[]{PG_HBA_ON, Boolean.TRUE});
-    work.put("verify-fullGB", new Object[]{VALIDATOR, Boolean.TRUE});
-    work.put("verify-fullBG", new Object[]{BROKEN, Boolean.TRUE});
-    expectedmap.put("sslhostnossl9", work);
-
-    work = (TreeMap) defaultexpected.clone();
-    work.put("disableGG", new Object[]{null, Boolean.FALSE});
-    work.put("disableGB", new Object[]{null, Boolean.FALSE});
-    work.put("disableBG", new Object[]{null, Boolean.FALSE});
-    work.put("allowGG", new Object[]{SSLMODEALLOW, Boolean.FALSE});
-    work.put("allowGB", new Object[]{SSLMODEALLOW, Boolean.FALSE});
-    work.put("allowBG", new Object[]{SSLMODEALLOW, Boolean.FALSE});
-    work.put("preferGG", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    work.put("preferGB", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    work.put("preferBG", new Object[]{SSLMODEPREFER, Boolean.FALSE});
-    work.put("requireGG", new Object[]{null, Boolean.TRUE});
-    work.put("requireGB", new Object[]{null, Boolean.TRUE});
-    work.put("requireBG", new Object[]{BROKEN, Boolean.TRUE});
-    work.put("verify-caGG", new Object[]{null, Boolean.TRUE});
-    work.put("verify-caGB", new Object[]{VALIDATOR, Boolean.TRUE});
-    work.put("verify-caBG", new Object[]{BROKEN, Boolean.TRUE});
-    work.put("verify-fullGG", new Object[]{null, Boolean.TRUE});
-    work.put("verify-fullGB", new Object[]{VALIDATOR, Boolean.TRUE});
-    work.put("verify-fullBG", new Object[]{BROKEN, Boolean.TRUE});
-    expectedmap.put("sslhostgh9", work);
-
-    work = (TreeMap) work.clone();
-    work.put("disableGG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("disableGB", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("disableBG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("allowGG", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("allowGB", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("allowBG", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("preferBG", new Object[]{SSLMODEPREFER, Boolean.FALSE});
-    expectedmap.put("sslhostsslgh9", work);
-
-    work = (TreeMap) defaultexpected.clone();
-    work.put("disableGG", new Object[]{null, Boolean.FALSE});
-    work.put("disableGB", new Object[]{null, Boolean.FALSE});
-    work.put("disableBG", new Object[]{null, Boolean.FALSE});
-    work.put("allowGG", new Object[]{SSLMODEALLOW, Boolean.FALSE});
-    work.put("allowGB", new Object[]{SSLMODEALLOW, Boolean.FALSE});
-    work.put("allowBG", new Object[]{SSLMODEALLOW, Boolean.FALSE});
-    work.put("preferGG", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    work.put("preferGB", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    work.put("preferBG", new Object[]{SSLMODEPREFER, Boolean.FALSE});
-    work.put("requireGG", new Object[]{null, Boolean.TRUE});
-    work.put("requireGB", new Object[]{null, Boolean.TRUE});
-    work.put("requireBG", new Object[]{BROKEN, Boolean.TRUE});
-    work.put("verify-caGG", new Object[]{null, Boolean.TRUE});
-    work.put("verify-caGB", new Object[]{VALIDATOR, Boolean.TRUE});
-    work.put("verify-caBG", new Object[]{BROKEN, Boolean.TRUE});
-    work.put("verify-fullGG", new Object[]{HOSTNAME, Boolean.TRUE});
-    work.put("verify-fullGB", new Object[]{VALIDATOR, Boolean.TRUE});
-    work.put("verify-fullBG", new Object[]{BROKEN, Boolean.TRUE});
-    expectedmap.put("sslhostbh9", work);
-
-    work = (TreeMap) work.clone();
-    work.put("disableGG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("disableGB", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("disableBG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("allowGG", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("allowGB", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("allowBG", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("preferBG", new Object[]{SSLMODEPREFER, Boolean.FALSE});
-    expectedmap.put("sslhostsslbh9", work);
-
-    work = (TreeMap) defaultexpected.clone();
-    work.put("disableGG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("disableGB", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("disableBG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("allowGG", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("allowGB", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("allowBG", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("preferGG", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    work.put("preferGB", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    work.put("preferBG", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    work.put("requireGG", new Object[]{null, Boolean.TRUE});
-    work.put("requireGB", new Object[]{null, Boolean.TRUE});
-    work.put("requireBG", new Object[]{BROKEN, Boolean.TRUE});
-    work.put("verify-caGG", new Object[]{null, Boolean.TRUE});
-    work.put("verify-caGB", new Object[]{VALIDATOR, Boolean.TRUE});
-    work.put("verify-caBG", new Object[]{BROKEN, Boolean.TRUE});
-    work.put("verify-fullGG", new Object[]{null, Boolean.TRUE});
-    work.put("verify-fullGB", new Object[]{VALIDATOR, Boolean.TRUE});
-    work.put("verify-fullBG", new Object[]{BROKEN, Boolean.TRUE});
-    expectedmap.put("sslhostsslcertgh9", work);
-
-    work = (TreeMap) defaultexpected.clone();
-    work.put("disableGG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("disableGB", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("disableBG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("allowGG", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("allowGB", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("allowBG", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("preferGG", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    work.put("preferGB", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    work.put("preferBG", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    work.put("requireGG", new Object[]{null, Boolean.TRUE});
-    work.put("requireGB", new Object[]{null, Boolean.TRUE});
-    work.put("requireBG", new Object[]{BROKEN, Boolean.TRUE});
-    work.put("verify-caGG", new Object[]{null, Boolean.TRUE});
-    work.put("verify-caGB", new Object[]{VALIDATOR, Boolean.TRUE});
-    work.put("verify-caBG", new Object[]{BROKEN, Boolean.TRUE});
-    work.put("verify-fullGG", new Object[]{HOSTNAME, Boolean.TRUE});
-    work.put("verify-fullGB", new Object[]{VALIDATOR, Boolean.TRUE});
-    work.put("verify-fullBG", new Object[]{BROKEN, Boolean.TRUE});
-    expectedmap.put("sslhostsslcertbh9", work);
-
-    work = (TreeMap) defaultexpected.clone();
-    work.put("disableGG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("disableGB", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("disableBG", new Object[]{PG_HBA_OFF, Boolean.FALSE});
-    work.put("allowGG", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("allowGB", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("allowBG", new Object[]{SSLMODEALLOW, Boolean.TRUE});
-    work.put("preferGG", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    work.put("preferGB", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    work.put("preferBG", new Object[]{SSLMODEPREFER, Boolean.TRUE});
-    work.put("requireGG", new Object[]{null, Boolean.TRUE});
-    work.put("requireGB", new Object[]{null, Boolean.TRUE});
-    work.put("requireBG", new Object[]{BROKEN, Boolean.TRUE});
-    work.put("verify-caGG", new Object[]{null, Boolean.TRUE});
-    work.put("verify-caGB", new Object[]{VALIDATOR, Boolean.TRUE});
-    work.put("verify-caBG", new Object[]{BROKEN, Boolean.TRUE});
-    work.put("verify-fullGG", new Object[]{null, Boolean.TRUE});
-    work.put("verify-fullGB", new Object[]{VALIDATOR, Boolean.TRUE});
-    work.put("verify-fullBG", new Object[]{BROKEN, Boolean.TRUE});
-    expectedmap.put("sslcertgh9", work);
-
-    work = (TreeMap) work.clone();
-    work.put("verify-fullGG", new Object[]{HOSTNAME, Boolean.TRUE});
-    expectedmap.put("sslcertbh9", work);
+  /**
+   * Checks client certificate validation error.
+   *
+   * @param e connection exception or null if no exception
+   * @return true when validation pass, false when the case is not applicable
+   * @throws AssertionError when exception does not match expectations
+   */
+  private boolean assertClientCertificate(SQLException e) {
+    if (db.requiresClientCert() && clientCertificate == ClientCertificate.EMPTY) {
+      String caseName =
+          "client certificate was not sent and database " + db + " requires client certificate";
+      assertClientCertRequired(e, caseName);
+      return true;
+    }
 
+    if (clientCertificate != ClientCertificate.BAD) {
+      return false;
+    }
+    // Server verifies certificate no matter how it is configured, so sending BAD one
+    // is doomed to fail
+    String caseName = "BAD client certificate, and database " + db + " requires one";
+    if (e == null) {
+      Assert.fail(caseName + " should result in failure of client validation");
+    }
+    Assert.assertEquals(caseName + " ==> CONNECTION_FAILURE is expected",
+        PSQLState.CONNECTION_FAILURE.getState(), e.getSQLState());
+
+    // Two exceptions are possible
+    // SSLHandshakeException: Received fatal alert: unknown_ca
+    // SocketException: broken pipe (write failed)
+
+    SocketException brokenPipe = findCause(e, SocketException.class);
+    SSLHandshakeException handshakeException = findCause(e, SSLHandshakeException.class);
+
+    if (brokenPipe == null && handshakeException == null) {
+      Assert.fail(caseName + " ==> exception should be caused by SocketException(broken pipe)"
+          + " or SSLHandshakeException. No exceptions of such kind are present in the getCause chain");
+    }
+    if (brokenPipe != null && !brokenPipe.getMessage().contains("Broken pipe")) {
+      Assert.fail(
+          caseName + " ==> server should have terminated the connection (broken pipe expected)"
+              + ", actual exception was " + brokenPipe.getMessage());
+    }
+    if (handshakeException != null && !handshakeException.getMessage().contains("unknown_ca")) {
+      Assert.fail(
+          caseName + " ==> server should have terminated the connection (expected 'unknown_ca')"
+              + ", actual exception was " + handshakeException.getMessage());
+    }
+    return true;
   }
 
+  private static <T extends Throwable> T findCause(Throwable t, Class<T> cause) {
+    while (t != null) {
+      if (cause.isInstance(t)) {
+        return (T) t;
+      }
+      t = t.getCause();
+    }
+    return null;
+  }
+
+
+  @Test
+  public void run() throws SQLException {
+    if (con == null) {
+      // e.g. expected failure to connect
+      return;
+    }
+    ResultSet rs = con.createStatement().executeQuery("select ssl_is_used()");
+    Assert.assertTrue("select ssl_is_used() should return a row", rs.next());
+    boolean sslUsed = rs.getBoolean(1);
+    if (sslmode == SslMode.ALLOW) {
+      Assert.assertEquals("ssl_is_used: ",
+          db.requiresSsl(),
+          sslUsed);
+    } else {
+      Assert.assertEquals("ssl_is_used: ",
+          sslmode != SslMode.DISABLE && !db.rejectsSsl(),
+          sslUsed);
+    }
+    TestUtil.closeQuietly(rs);
+  }
 
 }
diff --git a/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTestSuite.java
index 19c3a7c532..cf48916f42 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTestSuite.java
@@ -5,43 +5,14 @@
 
 package org.postgresql.test.ssl;
 
-import org.postgresql.test.TestUtil;
-
-import junit.framework.TestSuite;
-
-import java.util.Properties;
-
-public class SslTestSuite extends TestSuite {
-  private static Properties prop;
-
-  private static void add(TestSuite suite, String param) {
-    if (prop.getProperty(param, "").equals("")) {
-      System.out.println("Skipping " + param + ".");
-    } else {
-      suite.addTest(SslTest.getSuite(prop, param));
-    }
-  }
-
-  /*
-   * The main entry point for JUnit
-   */
-  public static TestSuite suite() throws Exception {
-    TestSuite suite = new TestSuite();
-    prop = TestUtil.loadPropertyFiles("ssltest.properties");
-    add(suite, "ssloff9");
-    add(suite, "sslhostnossl9");
-
-    String[] hostModes = {"sslhost", "sslhostssl", "sslhostsslcert", "sslcert"};
-    String[] certModes = {"gh", "bh"};
-
-    for (String hostMode : hostModes) {
-      for (String certMode : certModes) {
-        add(suite, hostMode + certMode + "9");
-      }
-    }
-
-    TestUtil.initDriver();
-
-    return suite;
-  }
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    LibPQFactoryHostNameTest.class,
+    CommonNameVerifierTest.class,
+    SslTest.class
+})
+public class SslTestSuite {
 }
diff --git a/ssltest.properties b/ssltest.properties
index 21d1d45a40..492399a428 100644
--- a/ssltest.properties
+++ b/ssltest.properties
@@ -1,32 +1,2 @@
-
-  
 certdir=certdir
-
-# Uncomment to enable testing of SingleCertValidatingFactory
-#testsinglecertfactory=true
-
-ssloff9=
-ssloff9prefix=
-
-#sslhostnossl9=jdbc:postgresql://localhost:5432/hostnossldb?sslpassword=sslpwd
-sslhostnossl9prefix=
-
-#sslhostgh9=jdbc:postgresql://localhost:5432/hostdb?sslpassword=sslpwd
-sslhostgh9prefix=
-#sslhostbh9=jdbc:postgresql://127.0.0.1:5432/hostdb?sslpassword=sslpwd
-sslhostbh9prefix=
-
-#sslhostsslgh9=jdbc:postgresql://localhost:5432/hostssldb?sslpassword=sslpwd
-sslhostsslgh9prefix=
-#sslhostsslbh9=jdbc:postgresql://127.0.0.1:5432/hostssldb?sslpassword=sslpwd
-sslhostsslbh9prefix=
-
-#sslhostsslcertgh9=jdbc:postgresql://localhost:5432/hostsslcertdb?sslpassword=sslpwd
-sslhostsslcertgh9prefix=
-#sslhostsslcertbh9=jdbc:postgresql://127.0.0.1:5432/hostsslcertdb?sslpassword=sslpwd
-sslhostsslcertbh9prefix=
-
-#sslcertgh9=jdbc:postgresql://localhost:5432/certdb?sslpassword=sslpwd
-sslcertgh9prefix=
-#sslcertbh9=jdbc:postgresql://127.0.0.1:5432/certdb?sslpassword=sslpwd
-sslcertbh9prefix=
\ No newline at end of file
+#enable_ssl_tests=true