Skip to content
Permalink
Browse files

fix: Add pkcs12 key functionality (#1599)

* 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

* fix docs to add PKCS12 archive format
  • Loading branch information
davecramer committed Nov 29, 2019
1 parent 4258e0d commit 82c2008f83dd687e80b1e3acdeeb618dccc2fb5c
@@ -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
BIN +1.76 KB certdir/goodclient.p12
Binary file not shown.
@@ -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

@@ -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
@@ -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
@@ -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();
}
}
}

@@ -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;
}

}
@@ -6,13 +6,15 @@
package org.postgresql.test.ssl;

import org.postgresql.ssl.LazyKeyManager;
import org.postgresql.ssl.PKCS12KeyManager;

import org.junit.Assert;
import org.junit.Test;

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/";

0 comments on commit 82c2008

Please sign in to comment.
You can’t perform that action at this time.