/* * Copyright (c) 2004, PostgreSQL Global Development Group * See the LICENSE file in the project root for more information. */ package org.postgresql.test.ssl; import org.postgresql.test.TestUtil; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Properties; @RunWith(Parameterized.class) public class SingleCertValidatingFactoryTestSuite { private static String IS_ENABLED_PROP_NAME = "testsinglecertfactory"; /** * This method returns the paramaters that JUnit will use when constructing this class for * testing. It returns a collection of arrays, each containing a single value for the JDBC URL to * test against. * * To point the test at a different set of test databases edit the JDBC URL list accordingly. By * default it points to the test databases setup by the pgjdbc-test-vm virtual machine. * * Note: The test assumes that the username as password for all the test databases are the same * (pulled from system properties). */ @Parameters public static Collection<Object[]> data() throws IOException { Properties props = new Properties(); File sslTestFile = TestUtil.getFile(System.getProperty("ssltest.properties", "ssltest.properties")); props.load(new FileInputStream(sslTestFile)); String testSingleCertFactory = props.getProperty(IS_ENABLED_PROP_NAME); boolean skipTest = testSingleCertFactory == null || "".equals(testSingleCertFactory); if (skipTest) { System.out.println("Skipping SingleCertSocketFactoryTests. To enable set the property " + IS_ENABLED_PROP_NAME + "=true in the ssltest.properties file."); return Collections.emptyList(); } return Arrays.asList(new Object[][]{ {"jdbc:postgresql://localhost:10084/test"}, {"jdbc:postgresql://localhost:10090/test"}, {"jdbc:postgresql://localhost:10091/test"}, {"jdbc:postgresql://localhost:10092/test"}, {"jdbc:postgresql://localhost:10093/test"}, }); } // The valid and invalid server SSL certfiicates: private static final String goodServerCertPath = "certdir/goodroot.crt"; private static final String badServerCertPath = "certdir/badroot.crt"; private String getGoodServerCert() { return loadFile(goodServerCertPath); } private String getBadServerCert() { return loadFile(badServerCertPath); } protected String getUsername() { return System.getProperty("username"); } protected String getPassword() { return System.getProperty("password"); } private String serverJdbcUrl; public SingleCertValidatingFactoryTestSuite(String serverJdbcUrl) { this.serverJdbcUrl = serverJdbcUrl; } protected String getServerJdbcUrl() { return serverJdbcUrl; } /** * Helper method to create a connection using the additional properites specified in the "info" * paramater. * * @param info The additional properties to use when creating a connection */ protected Connection getConnection(Properties info) throws SQLException { String url = getServerJdbcUrl(); info.setProperty("user", getUsername()); info.setProperty("password", getPassword()); return DriverManager.getConnection(url, info); } /** * Tests whether a given throwable or one of it's root causes matches of a given class. */ private boolean matchesExpected(Throwable t, Class<? extends Throwable> expectedThrowable) throws SQLException { if (t == null || expectedThrowable == null) { return false; } if (expectedThrowable.isAssignableFrom(t.getClass())) { return true; } return matchesExpected(t.getCause(), expectedThrowable); } protected void testConnect(Properties info, boolean sslExpected) throws SQLException { testConnect(info, sslExpected, null); } /** * Connects to the database with the given connection properties and then verifies that connection * is using SSL. */ protected void testConnect(Properties info, boolean sslExpected, Class<? extends Throwable> expectedThrowable) throws SQLException { Connection conn = null; try { conn = getConnection(info); Statement stmt = conn.createStatement(); // Basic SELECT test: ResultSet rs = stmt.executeQuery("SELECT 1"); rs.next(); Assert.assertEquals(1, rs.getInt(1)); rs.close(); // Verify SSL usage is as expected: rs = stmt.executeQuery("SELECT ssl_is_used()"); rs.next(); boolean sslActual = rs.getBoolean(1); Assert.assertEquals(sslExpected, sslActual); stmt.close(); } catch (Exception e) { if (matchesExpected(e, expectedThrowable)) { // do nothing and just suppress the exception return; } else { if (e instanceof RuntimeException) { throw (RuntimeException) e; } else if (e instanceof SQLException) { throw (SQLException) e; } else { throw new RuntimeException(e); } } } finally { if (conn != null) { try { conn.close(); } catch (Exception e) { } } } if (expectedThrowable != null) { Assert.fail("Expected exception " + expectedThrowable.getName() + " but it did not occur."); } } /** * Connect using SSL and attempt to validate the server's certificate but don't actually provide * it. This connection attempt should *fail* as the client should reject the server. */ @Test public void connectSSLWithValidationNoCert() throws SQLException { Properties info = new Properties(); info.setProperty("ssl", "true"); testConnect(info, true, javax.net.ssl.SSLHandshakeException.class); } /** * Connect using SSL and attempt to validate the server's certificate against the wrong pre shared * certificate. This test uses a pre generated certificate that will *not* match the test * PostgreSQL server (the certificate is for properssl.example.com). * * This connection uses a custom SSLSocketFactory using a custom trust manager that validates the * remote server's certificate against the pre shared certificate. * * This test should throw an exception as the client should reject the server since the * certificate does not match. */ @Test public void connectSSLWithValidationWrongCert() throws SQLException, IOException { Properties info = new Properties(); info.setProperty("ssl", "true"); info.setProperty("sslfactory", "org.postgresql.ssl.SingleCertValidatingFactory"); info.setProperty("sslfactoryarg", "file:" + badServerCertPath); testConnect(info, true, javax.net.ssl.SSLHandshakeException.class); } @Test public void fileCertInvalid() throws SQLException, IOException { Properties info = new Properties(); info.setProperty("ssl", "true"); info.setProperty("sslfactory", "org.postgresql.ssl.SingleCertValidatingFactory"); info.setProperty("sslfactoryarg", "file:foo/bar/baz"); testConnect(info, true, java.io.FileNotFoundException.class); } @Test public void stringCertInvalid() throws SQLException, IOException { Properties info = new Properties(); info.setProperty("ssl", "true"); info.setProperty("sslfactory", "org.postgresql.ssl.SingleCertValidatingFactory"); info.setProperty("sslfactoryarg", "foobar!"); testConnect(info, true, java.security.GeneralSecurityException.class); } /** * Connect using SSL and attempt to validate the server's certificate against the proper pre * shared certificate. The certificate is specified as a String. Note that the test read's the * certificate from a local file. */ @Test public void connectSSLWithValidationProperCertFile() throws SQLException, IOException { Properties info = new Properties(); info.setProperty("ssl", "true"); info.setProperty("sslfactory", "org.postgresql.ssl.SingleCertValidatingFactory"); info.setProperty("sslfactoryarg", "file:" + goodServerCertPath); testConnect(info, true); } /** * Connect using SSL and attempt to validate the server's certificate against the proper pre * shared certificate. The certificate is specified as a String (eg. the "----- BEGIN CERTIFICATE * ----- ... etc"). */ @Test public void connectSSLWithValidationProperCertString() throws SQLException, IOException { Properties info = new Properties(); info.setProperty("ssl", "true"); info.setProperty("sslfactory", "org.postgresql.ssl.SingleCertValidatingFactory"); info.setProperty("sslfactoryarg", getGoodServerCert()); testConnect(info, true); } /** * Connect using SSL and attempt to validate the server's certificate against the proper pre * shared certificate. The certificate is specified as a system property. */ @Test public void connectSSLWithValidationProperCertSysProp() throws SQLException, IOException { // System property name we're using for the SSL cert. This can be anything. String sysPropName = "org.postgresql.jdbc.test.sslcert"; try { System.setProperty(sysPropName, getGoodServerCert()); Properties info = new Properties(); info.setProperty("ssl", "true"); info.setProperty("sslfactory", "org.postgresql.ssl.SingleCertValidatingFactory"); info.setProperty("sslfactoryarg", "sys:" + sysPropName); testConnect(info, true); } finally { // Clear it out when we're done: System.setProperty(sysPropName, ""); } } /** * Connect using SSL and attempt to validate the server's certificate against the proper pre * shared certificate. The certificate is specified as an environment variable. * * Note: To execute this test succesfully you need to set the value of the environment variable * DATASOURCE_SSL_CERT prior to running the test. * * Here's one way to do it: $ DATASOURCE_SSL_CERT=$(cat certdir/goodroot.crt) ant clean test */ @Test public void connectSSLWithValidationProperCertEnvVar() throws SQLException, IOException { String envVarName = "DATASOURCE_SSL_CERT"; if (System.getenv(envVarName) == null) { System.out.println( "Skipping test connectSSLWithValidationProperCertEnvVar (env variable is not defined)"); return; } Properties info = new Properties(); info.setProperty("ssl", "true"); info.setProperty("sslfactory", "org.postgresql.ssl.SingleCertValidatingFactory"); info.setProperty("sslfactoryarg", "env:" + envVarName); testConnect(info, true); } /** * Connect using SSL using a system property to specify the SSL certificate but not actually * having it set. This tests whether the proper exception is thrown. */ @Test public void connectSSLWithValidationMissingSysProp() throws SQLException, IOException { // System property name we're using for the SSL cert. This can be anything. String sysPropName = "org.postgresql.jdbc.test.sslcert"; try { System.setProperty(sysPropName, ""); Properties info = new Properties(); info.setProperty("ssl", "true"); info.setProperty("sslfactory", "org.postgresql.ssl.SingleCertValidatingFactory"); info.setProperty("sslfactoryarg", "sys:" + sysPropName); testConnect(info, true, java.security.GeneralSecurityException.class); } finally { // Clear it out when we're done: System.setProperty(sysPropName, ""); } } /** * Connect using SSL using an environment var to specify the SSL certificate but not actually * having it set. This tests whether the proper exception is thrown. */ @Test public void connectSSLWithValidationMissingEnvVar() throws SQLException, IOException { // Use an environment variable that does *not* exist: String envVarName = "MISSING_DATASOURCE_SSL_CERT"; if (System.getenv(envVarName) != null) { System.out .println("Skipping test connectSSLWithValidationMissingEnvVar (env variable is defined)"); return; } Properties info = new Properties(); info.setProperty("ssl", "true"); info.setProperty("sslfactory", "org.postgresql.ssl.SingleCertValidatingFactory"); info.setProperty("sslfactoryarg", "env:" + envVarName); testConnect(info, true, java.security.GeneralSecurityException.class); } /////////////////////////////////////////////////////////////////// /** * Utility function to load a file as a string */ public static String loadFile(String path) { BufferedReader br = null; try { br = new BufferedReader(new InputStreamReader(new FileInputStream(path))); StringBuilder sb = new StringBuilder(); String line; while ((line = br.readLine()) != null) { sb.append(line); sb.append("\n"); } return sb.toString(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (br != null) { try { br.close(); } catch (Exception e) { } } } } }