From 3482a91d72871fc963c9fc3285c891469675177e Mon Sep 17 00:00:00 2001 From: Dave Cramer Date: Fri, 1 Nov 2019 08:21:05 -0400 Subject: [PATCH 1/2] fix: Add pkcs12 key functionality WIP PBE-MD5-DES might be inadequate in environments where high level of security is needed and the key is not protected pkcs12 uses 3DES which also isn't completely adequate but is certainly more secure than MD5-DES --- certdir/goodclient.p12 | Bin 0 -> 1806 bytes .../java/org/postgresql/ssl/LibPQFactory.java | 79 +++++--- .../org/postgresql/ssl/PKCS12KeyManager.java | 171 ++++++++++++++++++ .../test/ssl/LazyKeyManagerTest.java | 12 ++ .../postgresql/test/ssl/PKCS12KeyTest.java | 48 +++++ 5 files changed, 283 insertions(+), 27 deletions(-) create mode 100644 certdir/goodclient.p12 create mode 100644 pgjdbc/src/main/java/org/postgresql/ssl/PKCS12KeyManager.java create mode 100644 pgjdbc/src/test/java/org/postgresql/test/ssl/PKCS12KeyTest.java diff --git a/certdir/goodclient.p12 b/certdir/goodclient.p12 new file mode 100644 index 0000000000000000000000000000000000000000..7b468336938f77b657e9e7c4d15d723f0629218f GIT binary patch literal 1806 zcmY+DS5Om(7KW1$LPE__R|FA4B7H;e!qRK#F|d>k2$9Y}g475>B7q>FvPkc}h#+ea zfq)e0APNKv4tT{yvNS15yEwD=dhf%TGxL9Q&eQ*)DO}tjAP1TPnTEg=s8-Z{J|Gvc zi~{KcQy|@EaVMI>QThWx$|xM!XAvC)1e~qZ9|VY&fN=lwffoot^Mj!|wZ?puCqV!Z z2ntA|aNwlt&GIm5>EsEM^^1OgdC16X^Oo}%(HM>*ew9r_jdvEiQ{p)VMX^HA&HXLn|rozb-fN^E}X!D`Rl> zOVqFW`5D5P=Co=is1IMXX$2!$9Ei-o=X!QyN}<@7VK;XQG;Tck7c=&H%iJ{C7Ecls z@G!bITr%#Yyl!@W;tRQ!t=YBpMBwi!Zpfh09{Oj;kR?fFRj%Fad}PgfR#M-hmgsXl zCWyBsO=F}|O#>oK4gwfFQgW_$EB$KD*4};FW`6iK#j~jrIb%v{tIRIxVHu~sbf3-W zf_t1>?RRa#A(CANy!LkmNyUe52j8l&AksYT~`Av)N{suS!b9&2ug_tjqWSn~dNoE{z6$~bWYCUVd)hX7owb+X>z(4&= zX7DfF?s^HzJq;{jn!&topuo$zUr?-{v@N$5*Jyww4yvNV>?d2~SU-KNX z#!;u~+LPft|`Z zxOYLK%woANjc2Lq?$Nau=?1Qhk(sUO$G3c~bFyrNOcta)^Fu#x7h7I7G>?n5RQ78a z0D5G?jPWLyj(NRMokhd%vqZ|yqY@6S9Z&My71Qk*E+O<%{BXS5w`XA)C4N4O*kexj zs78mH%A}_;Q*Jd|9}qU7)JV27e2kgZLh*rjJ~-A56`|6)1P~M$Y>Av z`fAxK;&oFkal~+9=qL3FtO3+hfpgLj?mBfQqTv6Cc%~=^m;yRQQ$UAjvk&3*?E#l3vIz}%~btQF}p&iXMF>(SD`-6y7mul zNJbm#cX{jdGWCEdVP#j@SOqf-aL0RJa4eNMI!N1}Q!b&7B%8!z2e?*x52t>~%Wocncj$U1k|b75KbX7~ zk1~2FdtdN<`$YoF6jzH-__iVAtw7GyR*$LHEf^NM&V58M9wYkI)8j_cQzj=r zcYS$B-~KffAz2GG#m95S_;R|q_3f$rD&9SL(y?KJAd9r-RF~@|<*&VmR^ZM59h-~= zbR0#UN1ot)#&E>jtKx-;#RDpyHmw8_KSbV-r9O`u6!o=b!k}?Pg5$}sHx1|ADy&86 zG6s)2OF!BY7d&nEX_q#dotjDJ(+k6x1+-iC6r-IE(D!JYL04sJz^-$N4r?(HAuo&^ zkmvnc-2%xC?|5n;tB$D}`K;W1My*=(5`j2Iu2zZs944xDHP(lzDeb~Q(B~&u_hWxC zzIV*!a;}AtXht6gYQ&}A>3O%LM0YBOJFd;FUz#g)whIx}sT3p0nFa?2m`<;B@s{ZS>BSOt-BW#kcvRipJ zrs)2dV(85pH$>_9h3&SI^4qzt^CHGMh9cDJ%zoXZmX8+Cqamu7(PIB?HB4BQ`vMd| z0z?7)0g-6L4&NEQAY$xX;=(4)bFUm47z5C%XlXPI!l}s50TSl~fQ51D jQESl`#YU)0%ltZ%R+=1=Thd_Y-UB2eafoI9z4reG5t1_t literal 0 HcmV?d00001 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); + } +} From 8900e358f1eb4d3d47d3366250a5039f0905777d Mon Sep 17 00:00:00 2001 From: Dave Cramer Date: Fri, 1 Nov 2019 09:35:11 -0400 Subject: [PATCH 2/2] fix docs to add PKCS12 archive format --- certdir/README.md | 6 ++++++ docs/documentation/head/connect.md | 6 +++--- docs/documentation/head/ssl-client.md | 5 ++++- docs/documentation/head/ssl.md | 10 ++++++---- 4 files changed, 19 insertions(+), 8 deletions(-) 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/docs/documentation/head/connect.md b/docs/documentation/head/connect.md index c8f167e64a..238262107c 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