diff --git a/certdir/README.md b/certdir/README.md index 59fd06e329..34dcbc4733 100644 --- a/certdir/README.md +++ b/certdir/README.md @@ -48,3 +48,9 @@ openssl req -x509 -newkey rsa:1024 -nodes -days 3650 -keyout server.key -out ser cp server.crt ../goodroot.crt #Common name is localhost, no password + +#PKCS12 + +Create the goodclient.p12 file with + +openssl pkcs12 -export -in goodclient.crt -inkey goodclient.key -out goodclient.p12 -name local -CAfile client_ca.crt -caname local diff --git a/certdir/goodclient.p12 b/certdir/goodclient.p12 new file mode 100644 index 0000000000..7b46833693 Binary files /dev/null and b/certdir/goodclient.p12 differ diff --git a/docs/documentation/head/connect.md b/docs/documentation/head/connect.md index 4d16303096..ec4325381b 100644 --- a/docs/documentation/head/connect.md +++ b/docs/documentation/head/connect.md @@ -88,7 +88,7 @@ Connection conn = DriverManager.getConnection(url); * **ssl** = boolean - Connect using SSL. The driver must have been compiled with SSL support. + Connect using SSL. The server must have been compiled with SSL support. This property does not need a value associated with it. The mere presence of it specifies a SSL connection. However, for compatibility with future versions, the value "true" is preferred. For more information see [Chapter @@ -97,10 +97,10 @@ Connection conn = DriverManager.getConnection(url); Setting up the certificates and keys for ssl connection can be tricky see [The test documentation](https://github.com/pgjdbc/pgjdbc/blob/master/certdir/README.md) for detailed examples. * **sslfactory** = String - + The provided value is a class name to use as the `SSLSocketFactory` when establishing a SSL connection. For more information see the section - called [“Custom SSLSocketFactory”](ssl-factory.html). + called [“Custom SSLSocketFactory”](ssl-factory.html). defaults to LibPQFactory * **sslfactoryarg** (deprecated) = String diff --git a/docs/documentation/head/ssl-client.md b/docs/documentation/head/ssl-client.md index e49b727170..d655ab2266 100644 --- a/docs/documentation/head/ssl-client.md +++ b/docs/documentation/head/ssl-client.md @@ -35,7 +35,7 @@ stored in the server certificate. The SSL connection will fail if the server certificate cannot be verified. `verify-full` is recommended in most security-sensitive environments. - +The default SSL Socket factory is the LibPQFactory In the case where the certificate validation is failing you can try `sslcert=` and LibPQFactory will not send the client certificate. If the server is not configured to authenticate using the certificate it should connect. @@ -45,6 +45,9 @@ The location of the client certificate, client key and root certificate can be o /defaultdir/postgresql.pk8, and /defaultdir/root.crt respectively where defaultdir is ${user.home}/.postgresql/ in *nix systems and %appdata%/postgresql/ on windows +as of version 42.2.9 PKCS12 is supported. In this archive format the key, cert and root cert are all +in one file which by default is /defaultdir/postgresql.p12 + Finer control of the SSL connection can be achieved using the `sslmode` connection parameter. This parameter is the same as the libpq `sslmode` parameter and the currently SSL implements the following diff --git a/docs/documentation/head/ssl.md b/docs/documentation/head/ssl.md index a32027b64e..40fcdcb6f9 100644 --- a/docs/documentation/head/ssl.md +++ b/docs/documentation/head/ssl.md @@ -20,10 +20,12 @@ next: ssl-client.html # Configuring the Server Configuring the PostgreSQL™ server for SSL is covered in the [main -documentation](http://www.postgresql.org/docs/current/static/ssl-tcp.html), -so it will not be repeated here. Before trying to access your SSL enabled -server from Java, make sure you can get to it via **psql**. You should -see output like the following if you have established a SSL connection. +documentation](https://www.postgresql.org/docs/current/ssl-tcp.html), +so it will not be repeated here. There are also instructions in the source +[certdir](https://github.com/pgjdbc/pgjdbc/tree/master/certdir) +Before trying to access your SSL enabled server from Java, make sure +you can get to it via **psql**. You should see output like the following +if you have established a SSL connection. ``` $ ./bin/psql -h localhost -U postgres diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java b/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java index 6f7a5af22c..2f259af810 100644 --- a/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java +++ b/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java @@ -39,7 +39,46 @@ */ public class LibPQFactory extends WrappedFactory { - LazyKeyManager km; + KeyManager km; + boolean defaultfile; + + private CallbackHandler getCallbackHandler(Properties info) throws PSQLException { + // 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)); + } + return cbh; + } + + private void initPk8(String sslkeyfile, String defaultdir, Properties info) throws PSQLException { + + // 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"; + } + + + // If the properties are empty, give null to prevent client key selection + km = new LazyKeyManager(("".equals(sslcertfile) ? null : sslcertfile), + ("".equals(sslkeyfile) ? null : sslkeyfile), getCallbackHandler(info), defaultfile); + } + + private void initP12(String sslkeyfile, Properties info) throws PSQLException { + km = new PKCS12KeyManager(sslkeyfile, getCallbackHandler(info)); + } /** * @param info the connection parameters The following parameters are used: @@ -53,44 +92,25 @@ public LibPQFactory(Properties info) throws PSQLException { // 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 (sslkeyfile.endsWith("pk8")) { + initPk8(sslkeyfile, defaultdir, 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); + if (sslkeyfile.endsWith("p12")) { + initP12(sslkeyfile, info); + } TrustManager[] tm; SslMode sslMode = SslMode.of(info); @@ -171,7 +191,12 @@ public LibPQFactory(Properties info) throws PSQLException { */ public void throwKeyManagerException() throws PSQLException { if (km != null) { - km.throwKeyManagerException(); + if (km instanceof LazyKeyManager) { + ((LazyKeyManager)km).throwKeyManagerException(); + } + if (km instanceof PKCS12KeyManager) { + ((PKCS12KeyManager)km).throwKeyManagerException(); + } } } diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/PKCS12KeyManager.java b/pgjdbc/src/main/java/org/postgresql/ssl/PKCS12KeyManager.java new file mode 100644 index 0000000000..00f2e0faa2 --- /dev/null +++ b/pgjdbc/src/main/java/org/postgresql/ssl/PKCS12KeyManager.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2019, 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 org.postgresql.util.PSQLException; +import org.postgresql.util.PSQLState; + +import java.io.File; +import java.io.FileInputStream; +import java.net.Socket; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +import javax.net.ssl.X509KeyManager; +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; + +public class PKCS12KeyManager implements X509KeyManager { + + private final CallbackHandler cbh; + private PSQLException error = null; + private final String keyfile; + private final KeyStore keyStore; + boolean keystoreLoaded = false; + + public PKCS12KeyManager(String pkcsFile, CallbackHandler cbh) throws PSQLException { + try { + keyStore = KeyStore.getInstance("pkcs12"); + keyfile = pkcsFile; + this.cbh = cbh; + } catch ( KeyStoreException kse ) { + throw new PSQLException(GT.tr( + "Unable to find pkcs12 keystore."), + PSQLState.CONNECTION_FAILURE, kse); + } + } + + /** + * getCertificateChain and getPrivateKey cannot throw exeptions, therefore any exception is stored + * in {@link #error} and can be raised by this method. + * + * @throws PSQLException if any exception is stored in {@link #error} and can be raised + */ + public void throwKeyManagerException() throws PSQLException { + if (error != null) { + throw error; + } + } + + @Override + public String[] getClientAliases(String keyType, Principal[] principals) { + String alias = chooseClientAlias(new String[]{keyType}, principals, (Socket) null); + return (alias == null ? new String[]{} : new String[]{alias}); + } + + @Override + public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { + if (principals == null || principals.length == 0) { + // Postgres 8.4 and earlier do not send the list of accepted certificate authorities + // to the client. See BUG #5468. We only hope, that our certificate will be accepted. + return "user"; + } else { + // Sending a wrong certificate makes the connection rejected, even, if clientcert=0 in + // pg_hba.conf. + // therefore we only send our certificate, if the issuer is listed in issuers + X509Certificate[] certchain = getCertificateChain("user"); + if (certchain == null) { + return null; + } else { + X500Principal ourissuer = certchain[certchain.length - 1].getIssuerX500Principal(); + boolean found = false; + for (Principal issuer : principals) { + if (ourissuer.equals(issuer)) { + found = true; + } + } + return (found ? "user" : null); + } + } + } + + @Override + public String[] getServerAliases(String s, Principal[] principals) { + return new String[]{}; + } + + @Override + public String chooseServerAlias(String s, Principal[] principals, Socket socket) { + // we are not a server + return null; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + try { + loadKeyStore(); + Certificate []certs = keyStore.getCertificateChain(alias); + X509Certificate [] x509Certificates = new X509Certificate[certs.length]; + int i = 0; + for (Certificate cert : certs) { + x509Certificates[i++] = (X509Certificate)cert; + } + return x509Certificates; + } catch (Exception kse ) { + error = new PSQLException(GT.tr( + "Could not find a java cryptographic algorithm: X.509 CertificateFactory not available."), + PSQLState.CONNECTION_FAILURE, kse); + } + return null; + } + + @Override + public PrivateKey getPrivateKey(String s) { + try { + loadKeyStore(); + PasswordCallback pwdcb = new PasswordCallback(GT.tr("Enter SSL password: "), false); + cbh.handle(new Callback[]{pwdcb}); + + KeyStore.ProtectionParameter protParam = new KeyStore.PasswordProtection(pwdcb.getPassword()); + KeyStore.PrivateKeyEntry pkEntry = + (KeyStore.PrivateKeyEntry) keyStore.getEntry("user", protParam); + PrivateKey myPrivateKey = pkEntry.getPrivateKey(); + return myPrivateKey; + } catch (Exception ioex ) { + error = new PSQLException(GT.tr("Could not read SSL key file {0}.", keyfile), + PSQLState.CONNECTION_FAILURE, ioex); + } + return null; + } + + private synchronized void loadKeyStore() throws Exception { + + if (keystoreLoaded) { + return; + } + // We call back for the password + PasswordCallback pwdcb = new PasswordCallback(GT.tr("Enter SSL password: "), false); + try { + cbh.handle(new Callback[]{pwdcb}); + } catch (UnsupportedCallbackException ucex) { + if ((cbh instanceof LibPQFactory.ConsoleCallbackHandler) + && ("Console is not available".equals(ucex.getMessage()))) { + error = new PSQLException(GT + .tr("Could not read password for SSL key file, console is not available."), + PSQLState.CONNECTION_FAILURE, ucex); + } else { + error = + new PSQLException( + GT.tr("Could not read password for SSL key file by callbackhandler {0}.", + cbh.getClass().getName()), + PSQLState.CONNECTION_FAILURE, ucex); + } + + } + + keyStore.load(new FileInputStream(new File(keyfile)), pwdcb.getPassword()); + keystoreLoaded = true; + } + +} diff --git a/pgjdbc/src/test/java/org/postgresql/test/ssl/LazyKeyManagerTest.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/LazyKeyManagerTest.java index 1c1f1f0f36..22a728c7fd 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/ssl/LazyKeyManagerTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/LazyKeyManagerTest.java @@ -6,6 +6,7 @@ package org.postgresql.test.ssl; import org.postgresql.ssl.LazyKeyManager; +import org.postgresql.ssl.PKCS12KeyManager; import org.junit.Assert; import org.junit.Test; @@ -13,6 +14,7 @@ import java.io.File; import java.io.IOException; import java.security.PrivateKey; +import java.security.cert.X509Certificate; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; @@ -21,6 +23,16 @@ public class LazyKeyManagerTest { + @Test + public void testLoadP12Key() throws Exception { + String certdir = "../certdir/"; + PKCS12KeyManager pkcs12KeyManager = new PKCS12KeyManager(certdir + "goodclient.p12", new TestCallbackHandler("sslpwd")); + PrivateKey pk = pkcs12KeyManager.getPrivateKey("user"); + Assert.assertNotNull(pk); + X509Certificate[] chain = pkcs12KeyManager.getCertificateChain("user"); + Assert.assertNotNull(chain); + } + @Test public void testLoadKey() throws Exception { String certdir = "../certdir/"; diff --git a/pgjdbc/src/test/java/org/postgresql/test/ssl/PKCS12KeyTest.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/PKCS12KeyTest.java new file mode 100644 index 0000000000..5dc6f7503c --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/PKCS12KeyTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019, 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.test.TestUtil; +import org.postgresql.test.jdbc2.BaseTest4; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.sql.ResultSet; +import java.util.Properties; + +public class PKCS12KeyTest extends BaseTest4 { + @Override + @Before + public void setUp() throws Exception { + + super.setUp(); + } + + @Override + protected void updateProperties(Properties props) { + Properties prop = TestUtil.loadPropertyFiles("ssltest.properties"); + props.put(TestUtil.DATABASE_PROP, "hostssldb"); + PGProperty.SSL_MODE.set(props, "prefer"); + + File certDirFile = TestUtil.getFile(prop.getProperty("certdir")); + String certdir = certDirFile.getAbsolutePath(); + + PGProperty.SSL_KEY.set(props, certdir + "/" + "goodclient" + ".p12"); + + } + + @Test + public void TestGoodClientP12() throws Exception { + + 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); + } +}