216 changes: 136 additions & 80 deletions pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -42,7 +43,6 @@
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

import javax.net.SocketFactory;

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
Expand All @@ -242,19 +284,19 @@ 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;
}
throw new PSQLException(GT.tr("The connection attempt failed."),
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;
}
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
81 changes: 81 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/jdbc/SslMode.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
20 changes: 20 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/ssl/DefaultJavaSSLFactory.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
220 changes: 220 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java
Original file line number Diff line number Diff line change
@@ -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()));
}
}
}
}
77 changes: 30 additions & 47 deletions pgjdbc/src/main/java/org/postgresql/ssl/MakeSSL.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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);
}

}
264 changes: 264 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/ssl/PGjdbcHostnameVerifier.java
Original file line number Diff line number Diff line change
@@ -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);
}

}
323 changes: 39 additions & 284 deletions pgjdbc/src/main/java/org/postgresql/ssl/jdbc4/LibPQFactory.java
Original file line number Diff line number Diff line change
@@ -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);
}

/**
Expand All @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class ObjectFactory {
* 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
Expand Down
38 changes: 33 additions & 5 deletions pgjdbc/src/test/java/org/postgresql/test/TestUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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());
}
Expand All @@ -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);
}

/*
Expand Down
22 changes: 12 additions & 10 deletions pgjdbc/src/test/java/org/postgresql/test/jdbc2/NotifyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand All @@ -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());
Expand All @@ -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());
Expand All @@ -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);
Expand All @@ -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());
Expand All @@ -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());
Expand All @@ -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());
Expand All @@ -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());
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
BinaryStreamTest.class,
CharacterStreamTest.class,
UUIDTest.class,
LibPQFactoryHostNameTest.class,
XmlTest.class
})
public class Jdbc4TestSuite {
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,11 +48,16 @@ public static Iterable<Object[]> data() {
{"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));
}
}
745 changes: 445 additions & 300 deletions pgjdbc/src/test/java/org/postgresql/test/ssl/SslTest.java

Large diffs are not rendered by default.

49 changes: 10 additions & 39 deletions pgjdbc/src/test/java/org/postgresql/test/ssl/SslTestSuite.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
}
32 changes: 1 addition & 31 deletions ssltest.properties
Original file line number Diff line number Diff line change
@@ -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=
#enable_ssl_tests=true