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

Add Windows and MacOS native certificate support #3124

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
18 changes: 18 additions & 0 deletions docs/content/documentation/ssl.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ Information on how to actually implement such a class is beyond the scope of thi
are the [JSSE Reference Guide](https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html)
and the source to the `NonValidatingFactory` provided by the JDBC driver.

## Available SSLSocketFactory Implementations

The following implementations of SSLSocketFactory are shipped with the driver.

|sslfactory|Description|
|---|---|
|org.postgresql.ssl.DefaultJavaSSLFactory|Use the JDK default implementation.|
|org.postgresql.ssl.KeychainSSLFactory|Use certificates and keys from the MacOS keychain. If the MacOS truststore is unsupported by the JDK, we fall back to the default cacerts JKS truststore.|
|org.postgresql.ssl.LibPQFactory|Use the same certificates and keys as the libpq library. The key must be in DER encoded PKCS8 format. This is the default when sslfactory is unspecified.|
|org.postgresql.ssl.MSCAPILocalMachineSSLFactory|Use certificates and keys from the Windows local machine certificate manager.|
|org.postgresql.ssl.MSCAPISSLFactory|Use certificates and keys from the Windows certificate manager belonging to the current user.|
|org.postgresql.ssl.NonValidatingFactory|Connect to anyone without checking. No validation is performed.|
|org.postgresql.ssl.SingleCertValidatingFactory|Accept the pinned server certificate specified in the sslfactoryarg parameter.|

## Configuring the Client

There are a number of connection parameters for configuring the client for SSL. See [SSL Connection parameters](/documentation/use/#connection-parameters/)
Expand Down Expand Up @@ -122,6 +136,10 @@ When starting your Java application you must specify this keystore and password

In the event of problems extra debugging information is available by adding `-Djavax.net.debug=ssl` to your command line.

### Choosing a Specific Certificate

When using the Keychain or MSCAPI factories, and more than one client certificate matches during SSL negotiation, the first negotiated certificate will be chosen. To control which of many negotiated certificates will be returned, the sslsubject parameter can be used to set the subject of the certificate to be chosen. Note that sslsubject chooses from the list of negotiated certificates, it does not override the negotiation mechanism. If there is no match, no certificate will be sent to the server.

### Using SSL without Certificate Validation

In some situations it may not be possible to configure your Java environment to make the server certificate available, for example in an applet. For a large scale deployment it would be best to get a certificate signed by recognized
Expand Down
8 changes: 8 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/PGProperty.java
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,14 @@ public enum PGProperty {
null,
"The location of the root certificate for authenticating the server."),

/**
* The subject of the desired client certificate in a store containing many certificates.
*/
SSL_SUBJECT(
"sslsubject",
null,
"The subject of the desired client certificate in a store with many certificates."),

/**
* Specifies the name of the SSPI service class that forms the service class part of the SPN. The
* default, {@code POSTGRES}, is almost always correct.
Expand Down
16 changes: 16 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,22 @@ public void setSslPasswordCallback(@Nullable String className) {
PGProperty.SSL_PASSWORD_CALLBACK.set(properties, className);
}

/**
* @return SSL subject
* @see PGProperty#SSL_SUBJECT
*/
public @Nullable String getSslSubject() {
return PGProperty.SSL_SUBJECT.getOrDefault(properties);
}

/**
* @param file SSL subject
* @see PGProperty#SSL_SUBJECT
*/
public void setSslSubject(@Nullable String file) {
PGProperty.SSL_SUBJECT.set(properties, file);
}

/**
* @param applicationName application name
* @see PGProperty#APPLICATION_NAME
Expand Down
33 changes: 33 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/ssl/KeychainSSLFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2024, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/

package org.postgresql.ssl;

import org.postgresql.util.PSQLException;

import java.util.Properties;

/**
* <p>Provides a SSLSocketFactory that authenticates the remote server against
* the keychain provided by MacOS.</p>
*
* <p>Older versions of the JDK support MacOS key stores but not trust stores.
* If the trust store implementation is not found, this factory falls back to
* the default JDK trust store in <code>cacerts</code>.
*
* <p>When multiple certificates match for the given connection, the optional
* <code>sslsubject</code> connection property can be used to choose the
* desired certificate from the matching set. Note that this property does not
* override the certificate selection outside of the matching set.
*/
public class KeychainSSLFactory extends SSLFactory {

public KeychainSSLFactory(Properties info) throws PSQLException {
super(info, "TLS", "PKIX", "Apple",
"KeychainStore",
"Apple", "KeychainStore-ROOT");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2024, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/

package org.postgresql.ssl;

import org.postgresql.util.PSQLException;

import java.util.Properties;

/**
* <p>Provides a SSLSocketFactory that authenticates the remote server against
* the local machine's certificate store provided by Windows.</p>
*
* <p>The remote certificate is validated against the local machine's
* certificate trust store.</p>
*
* <p>When multiple certificates match for the given connection, the optional
* <code>sslsubject</code> connection property can be used to choose the
* desired certificate from the matching set. Note that this property does not
* override the certificate selection outside of the matching set.
*/
public class MSCAPILocalMachineSSLFactory extends SSLFactory {

public MSCAPILocalMachineSSLFactory(Properties info) throws PSQLException {
super(info, "TLS", "PKIX", "SunMSCAPI",
"Windows-MY-LOCALMACHINE",
"SunMSCAPI", "Windows-ROOT");
}

}
32 changes: 32 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/ssl/MSCAPISSLFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2024, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/

package org.postgresql.ssl;

import org.postgresql.util.PSQLException;

import java.util.Properties;

/**
* <p>Provides a SSLSocketFactory that authenticates the remote server against
* the logged in user's certificate store provided by Windows.</p>
*
* <p>The remote certificate is validated against the logged in user's
* certificate trust store.</p>
*
* <p>When multiple certificates match for the given connection, the optional
* <code>sslsubject</code> connection property can be used to choose the
* desired certificate from the matching set. Note that this property does not
* override the certificate selection outside of the matching set.
*/
public class MSCAPISSLFactory extends SSLFactory {

public MSCAPISSLFactory(Properties info) throws PSQLException {
super(info, "TLS", "PKIX", "SunMSCAPI",
"Windows-MY",
"SunMSCAPI", "Windows-ROOT");
}

}
197 changes: 197 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/ssl/SSLFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* Copyright (c) 2024, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/

package org.postgresql.ssl;

import org.postgresql.PGProperty;
import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;

import org.checkerframework.checker.nullness.qual.Nullable;

import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Properties;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509KeyManager;
import javax.security.auth.x500.X500Principal;

/**
* Socket factory that uses typical "zero configuration" key and trust
* stores, like the native Windows "SunMSCAPI" or native Apple "Apple"
* providers.
*/
public class SSLFactory extends WrappedFactory {

protected SSLFactory(Properties info, final String protocol, final String algorithm, final String keyStoreProvider, final String keyStoreType, final String trustStoreProvider, final String trustStoreType) throws PSQLException {

SSLContext ctx;
final KeyStore keyStore;
KeyStore trustStore;
final char[] keyPassphrase = new char[0];
final String sslsubject = PGProperty.SSL_SUBJECT.getOrDefault(info);
final @Nullable X500Principal subject;
final KeyManagerFactory keyManagerFactory;
final TrustManagerFactory trustManagerFactory;

try {

keyStore = KeyStore.getInstance(keyStoreType, keyStoreProvider);

} catch (KeyStoreException ex) {
throw new PSQLException(GT.tr("SSL keystore {0} not available.",
keyStoreType), PSQLState.CONNECTION_FAILURE, ex);
} catch (NoSuchProviderException ex) {
throw new PSQLException(GT.tr("SSL keystore {0} not available.",
keyStoreType), PSQLState.CONNECTION_FAILURE, ex);
}

try {

keyStore.load(null, null);

} catch (CertificateException ex) {
throw new PSQLException(GT.tr("SSL keystore {0} could not be loaded.",
keyStoreType), PSQLState.CONNECTION_FAILURE, ex);
} catch (IOException ex) {
throw new PSQLException(GT.tr("SSL keystore {0} could not be loaded.",
keyStoreType), PSQLState.CONNECTION_FAILURE, ex);
} catch (NoSuchAlgorithmException ex) {
throw new PSQLException(GT.tr("Could not find a Java cryptographic algorithm: {0}.",
ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex);
}

try {

keyManagerFactory = KeyManagerFactory
.getInstance(algorithm);

} catch (NoSuchAlgorithmException ex) {
throw new PSQLException(GT.tr("Could not find a Java cryptographic algorithm: {0}.",
ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex);
}

try {

keyManagerFactory.init(keyStore, keyPassphrase);
Copy link
Author

Choose a reason for hiding this comment

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

I'm getting a test failure, more specifically a compilation failure that I don't understand.

[Task :postgresql:compileJava] [argument] incompatible argument for parameter arg1 of KeyManagerFactory.init.
      keyManagerFactory.init(keyStore, keyPassphrase);
                                       ^
  found   : @initialized @nonnull char @FBCBottom @nullable []

keyPassphrase is a char[], the extra annotations seem sane.

Can you confirm for me if possible what specifically is wrong with this line so I can fix it?

Copy link
Author

Choose a reason for hiding this comment

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

These warnings might be relevant:

> Task :postgresql:compileJava
warning: /home/runner/work/pgjdbc/pgjdbc/config/checkerframework/Assert.astub:(line 1,col 1): Package not found: org.junit
warning: /home/runner/work/pgjdbc/pgjdbc/config/checkerframework/Assert.astub:(line 6,col 1): Type not found: org.junit.Assert

Copy link
Author

Choose a reason for hiding this comment

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

Quick ping on this comment - I don't understand how it compiles everywhere else but not in this specific case. Is someone familiar with CheckerFramework able to confirm?

https://github.com/pgjdbc/pgjdbc/actions/runs/8323877965/job/23147420376?pr=3124


} catch (NoSuchAlgorithmException ex) {
throw new PSQLException(GT.tr("Could not find a Java cryptographic algorithm: {0}.",
ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex);

Choose a reason for hiding this comment

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

Isn't ex.getMessage() redudant since the target message will be recursively built?

Copy link
Author

Choose a reason for hiding this comment

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

Java is fixed.

The ex.getMessage() is there on purpose so that the whole error message is on one single line.

The target audience for this patch are data scientists who aren't Java developers, nor are they experts in SSL. They see the message "Could not find a java cryptographic algorithm", they don't understand it (fair: "which crypto algorithm?"), and I have to pick it all apart for them, first helping them give me the whole exception (I'll get a screenshot of the top few lines, then I'll explain how to cut and paste), then finding the needle in the haystack of "caused by", then googling for them.

With the whole message on one line, they have one line to google themselves, on the top line, and lots and lots of make work is avoided.

} catch (KeyStoreException ex) {
throw new PSQLException(GT.tr("Could not initialize SSL keystore."),
PSQLState.CONNECTION_FAILURE, ex);
} catch (UnrecoverableKeyException ex) {
throw new PSQLException(GT.tr("Could not read SSL key."),
PSQLState.CONNECTION_FAILURE, ex);
}

try {

trustStore = KeyStore.getInstance(trustStoreType, trustStoreProvider);

} catch (KeyStoreException ex) {
// On the Mac, the truststore is new and won't be available
// on old versions of the JDK. In these cases fall back to
// the default truststore in cacerts.
if (ex.getCause() instanceof NoSuchAlgorithmException) {
trustStore = null;
} else {
throw new PSQLException(GT.tr("SSL truststore {0} not available.",
trustStoreType), PSQLState.CONNECTION_FAILURE, ex);
}
} catch (NoSuchProviderException ex) {
throw new PSQLException(GT.tr("SSL truststore {0} not available.",
trustStoreType), PSQLState.CONNECTION_FAILURE, ex);
}

try {

if (trustStore != null) {
trustStore.load(null, null);
}

} catch (CertificateException ex) {
throw new PSQLException(GT.tr("SSL truststore {0} could not be loaded.",
trustStoreType), PSQLState.CONNECTION_FAILURE, ex);
} catch (IOException ex) {
throw new PSQLException(GT.tr("SSL truststore {0} could not be loaded.",
trustStoreType), PSQLState.CONNECTION_FAILURE, ex);
} catch (NoSuchAlgorithmException ex) {
throw new PSQLException(GT.tr("Could not find a Java cryptographic algorithm: {0}.",
ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex);

Choose a reason for hiding this comment

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

Same here

Copy link
Author

Choose a reason for hiding this comment

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

Java is fixed.

}

try {

trustManagerFactory = TrustManagerFactory
.getInstance(algorithm);

} catch (NoSuchAlgorithmException ex) {
throw new PSQLException(GT.tr("Could not find a Java cryptographic algorithm: {0}.",
ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex);

Choose a reason for hiding this comment

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

Both same here

Copy link
Author

Choose a reason for hiding this comment

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

Java is fixed.

}

try {

trustManagerFactory.init(trustStore);

} catch (KeyStoreException ex) {
throw new PSQLException(GT.tr("Could not initialize SSL truststore."),
PSQLState.CONNECTION_FAILURE, ex);
}

try {

ctx = SSLContext.getInstance(protocol);

} catch (NoSuchAlgorithmException ex) {
throw new PSQLException(GT.tr("Could not find a Java cryptographic algorithm: {0}.",
ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex);

Choose a reason for hiding this comment

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

Same both here

Copy link
Author

Choose a reason for hiding this comment

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

Java is fixed.

}

try {

if (sslsubject != null && sslsubject.length() != 0) {
subject = new X500Principal(sslsubject);
} else {
subject = null;
}

} catch (IllegalArgumentException ex) {
throw new PSQLException(GT.tr("Could not parse sslsubject {0}: {1}.",
sslsubject, ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex);

Choose a reason for hiding this comment

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

Same here

Copy link
Author

Choose a reason for hiding this comment

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

This is intended.

}

try {

KeyManager km = keyManagerFactory.getKeyManagers()[0];

if (subject != null) {
km = new SubjectKeyManager(X509KeyManager.class.cast(km), subject);
}

ctx.init(new KeyManager[] { km }, null, null);

} catch (KeyManagementException ex) {
throw new PSQLException(GT.tr("Could not initialize SSL keystore/truststore."),
PSQLState.CONNECTION_FAILURE, ex);
}

factory = ctx.getSocketFactory();

}
}