Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Add pkcs12 key functionality #1599

Merged
merged 2 commits into from Nov 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions certdir/README.md
Expand Up @@ -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
Binary file added certdir/goodclient.p12
Binary file not shown.
6 changes: 3 additions & 3 deletions docs/documentation/head/connect.md
Expand Up @@ -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
Expand All @@ -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

Expand Down
5 changes: 4 additions & 1 deletion docs/documentation/head/ssl-client.md
Expand Up @@ -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.
Expand All @@ -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
Expand Down
10 changes: 6 additions & 4 deletions docs/documentation/head/ssl.md
Expand Up @@ -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
Expand Down
79 changes: 52 additions & 27 deletions pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java
Expand Up @@ -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:
Expand All @@ -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);
Expand Down Expand Up @@ -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();
}
}
}

Expand Down
171 changes: 171 additions & 0 deletions 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");
Copy link
Member

@vlsi vlsi Feb 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a good reason to hard-code this literal?
I think it must be moved to a static final constant with a comment then.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no good reason

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

}
Expand Up @@ -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;
Expand All @@ -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"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it is better to take sslpwd from the properties file.
Otherwise, the failures would be hard to analyze.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

PrivateKey pk = pkcs12KeyManager.getPrivateKey("user");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is unfortunate that getPrivateKey does not throw exceptions.
In other words, this method silently returns null, and the exception is not visible to the caller :(

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya, very hard to debug

Assert.assertNotNull(pk);
X509Certificate[] chain = pkcs12KeyManager.getCertificateChain("user");
Assert.assertNotNull(chain);
}

@Test
public void testLoadKey() throws Exception {
String certdir = "../certdir/";
Expand Down