From 00bf6f638f8413b0ce6db755288b44cd7674289f Mon Sep 17 00:00:00 2001 From: Graham Leggett Date: Fri, 16 Feb 2024 10:07:51 +0000 Subject: [PATCH 1/7] Add Windows and MacOS native certificate support Add three new SSLSocketFactory implementations to support native keystores on Windows and Mac. org.postgresql.ssl.MSCAPILocalMachineSSLFactory org.postgresql.ssl.MSCAPISSLFactory org.postgresql.ssl.KeychainSSLFactory Add the sslsubject parameter to limit the chosen certificate where more than one certificate might match for a given connection. --- docs/content/documentation/ssl.md | 17 ++ .../main/java/org/postgresql/PGProperty.java | 8 + .../postgresql/ssl/KeychainSSLFactory.java | 33 +++ .../ssl/MSCAPILocalMachineSSLFactory.java | 32 +++ .../org/postgresql/ssl/MSCAPISSLFactory.java | 32 +++ .../java/org/postgresql/ssl/SSLFactory.java | 195 ++++++++++++++++++ .../org/postgresql/ssl/SubjectKeyManager.java | 108 ++++++++++ .../postgresql/test/ssl/SSLFactoryTest.java | 109 ++++++++++ .../org/postgresql/test/ssl/SslTestSuite.java | 2 + .../test/ssl/SubjectKeyManagerTest.java | 43 ++++ 10 files changed, 579 insertions(+) create mode 100644 pgjdbc/src/main/java/org/postgresql/ssl/KeychainSSLFactory.java create mode 100644 pgjdbc/src/main/java/org/postgresql/ssl/MSCAPILocalMachineSSLFactory.java create mode 100644 pgjdbc/src/main/java/org/postgresql/ssl/MSCAPISSLFactory.java create mode 100644 pgjdbc/src/main/java/org/postgresql/ssl/SSLFactory.java create mode 100644 pgjdbc/src/main/java/org/postgresql/ssl/SubjectKeyManager.java create mode 100644 pgjdbc/src/test/java/org/postgresql/test/ssl/SSLFactoryTest.java create mode 100644 pgjdbc/src/test/java/org/postgresql/test/ssl/SubjectKeyManagerTest.java diff --git a/docs/content/documentation/ssl.md b/docs/content/documentation/ssl.md index 51319929ed..ae6429da07 100644 --- a/docs/content/documentation/ssl.md +++ b/docs/content/documentation/ssl.md @@ -38,6 +38,19 @@ 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.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.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.SingleCertValidatingFactory|Accept the pinned server certificate specified in the sslfactoryarg parameter.| +|org.postgresql.ssl.NonValidatingFactory|Connect to anyone without checking. No validation is performed.| + ## Configuring the Client There are a number of connection parameters for configuring the client for SSL. See [SSL Connection parameters](/documentation/use/#connection-parameters/) @@ -122,6 +135,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 diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java index da11f66d7d..3980a821bc 100644 --- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java +++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java @@ -701,6 +701,14 @@ public enum PGProperty { null, "The location of the root certificate for authenticating the server."), + /** + * The alias of the desired client certificate in a store containing many certificates. + */ + SSL_SUBJECT( + "sslAlias", + null, + "The alias 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. diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/KeychainSSLFactory.java b/pgjdbc/src/main/java/org/postgresql/ssl/KeychainSSLFactory.java new file mode 100644 index 0000000000..756322f9a7 --- /dev/null +++ b/pgjdbc/src/main/java/org/postgresql/ssl/KeychainSSLFactory.java @@ -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; + +/** + *

Provides a SSLSocketFactory that authenticates the remote server against + * the keychain provided by MacOS.

+ * + *

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 cacerts. + * + *

When multiple certificates match for the given connection, the optional + * sslsubject 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"); + } + +} diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/MSCAPILocalMachineSSLFactory.java b/pgjdbc/src/main/java/org/postgresql/ssl/MSCAPILocalMachineSSLFactory.java new file mode 100644 index 0000000000..8c13d6182c --- /dev/null +++ b/pgjdbc/src/main/java/org/postgresql/ssl/MSCAPILocalMachineSSLFactory.java @@ -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; + +/** + *

Provides a SSLSocketFactory that authenticates the remote server against + * the local machine's certificate store provided by Windows.

+ * + *

The remote certificate is validated against the local machine's + * certificate trust store.

+ * + *

When multiple certificates match for the given connection, the optional + * sslsubject 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"); + } + +} diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/MSCAPISSLFactory.java b/pgjdbc/src/main/java/org/postgresql/ssl/MSCAPISSLFactory.java new file mode 100644 index 0000000000..31e05df699 --- /dev/null +++ b/pgjdbc/src/main/java/org/postgresql/ssl/MSCAPISSLFactory.java @@ -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; + +/** + *

Provides a SSLSocketFactory that authenticates the remote server against + * the logged in user's certificate store provided by Windows.

+ * + *

The remote certificate is validated against the logged in user's + * certificate trust store.

+ * + *

When multiple certificates match for the given connection, the optional + * sslsubject 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"); + } + +} diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/SSLFactory.java b/pgjdbc/src/main/java/org/postgresql/ssl/SSLFactory.java new file mode 100644 index 0000000000..4108e9c865 --- /dev/null +++ b/pgjdbc/src/main/java/org/postgresql/ssl/SSLFactory.java @@ -0,0 +1,195 @@ +/* + * 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 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 = null; + final String sslsubject = PGProperty.SSL_SUBJECT.getOrDefault(info); + final 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); + + } catch (NoSuchAlgorithmException ex) { + throw new PSQLException(GT.tr("Could not find a java cryptographic algorithm: {0}.", + ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex); + } 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); + } + + 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); + } + + 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); + } + + 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); + } + + 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(); + + } +} diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/SubjectKeyManager.java b/pgjdbc/src/main/java/org/postgresql/ssl/SubjectKeyManager.java new file mode 100644 index 0000000000..aa3fdc3673 --- /dev/null +++ b/pgjdbc/src/main/java/org/postgresql/ssl/SubjectKeyManager.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2024, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.ssl; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +import javax.net.ssl.X509KeyManager; +import javax.security.auth.x500.X500Principal; + +/** + * The SubjectKeyManager limits candidate certificates to + * the certificate subject specified in "sslsubject". + */ +public class SubjectKeyManager implements X509KeyManager { + + private final X509KeyManager km; + private final X500Principal subject; + + /** + *

+ * Wrap the provided key manager with this one, limiting + * certificates to those matching the given subject. + *

+ * + *

+ * If subject is null, this key manager gives way to the + * wrapped key manager and does nothing. + *

+ * @param km Wrapped key manager + * @param subject Subject distinguished name of the chosen + * certificate + */ + public SubjectKeyManager(X509KeyManager km, @Nullable X500Principal subject) { + + this.km = km; + this.subject = subject; + + } + + @Override + public @Nullable String chooseClientAlias(String[] keyTypes, Principal @Nullable [] issuers, + @Nullable Socket socket) { + + if (subject == null) { + return this.km.chooseClientAlias(keyTypes, issuers, socket); + } + + for (String keyType: keyTypes) { + String[] aliases = getClientAliases(keyType, issuers); + + if (aliases == null) { + continue; + } + + for (String alias: aliases) { + + X509Certificate[] certchain = getCertificateChain(alias); + if (certchain == null || certchain.length == 0) { + continue; + } + + X509Certificate leaf = certchain[0]; + X500Principal oursubject = leaf.getSubjectX500Principal(); + if (!oursubject.equals(subject)) { + continue; + } + + return alias; + } + } + + return null; + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return this.km.getClientAliases(keyType, issuers); + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return this.km.getServerAliases(keyType, issuers); + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return this.km.chooseServerAlias(keyType, issuers, socket); + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return this.km.getCertificateChain(alias); + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return this.km.getPrivateKey(alias); + } + +} diff --git a/pgjdbc/src/test/java/org/postgresql/test/ssl/SSLFactoryTest.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/SSLFactoryTest.java new file mode 100644 index 0000000000..5ccbfc0cb5 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/SSLFactoryTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2024, 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.core.SocketFactoryFactory; +import org.postgresql.test.TestUtil; +import org.postgresql.util.PSQLException; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.InvocationTargetException; +import java.security.NoSuchProviderException; +import java.util.Properties; + +class SSLFactoryTest { + + @Test + void TestDefault() throws Exception { + TestUtil.assumeSslTestsEnabled(); + + Properties props = new Properties(); + PGProperty.SSL_FACTORY.set(props, "org.postgresql.ssl.DefaultJavaSSLFactory"); + + try { + SocketFactoryFactory.getSslSocketFactory(props); + } catch (Throwable t) { + /* ignore NoSuchProviderException */ + if (!(t instanceof PSQLException) + || (!(t.getCause() instanceof InvocationTargetException)) + || (!(t.getCause().getCause() instanceof PSQLException)) + || (!(t.getCause().getCause().getCause() instanceof NoSuchProviderException)) + ) { + throw t; + } + } + + } + + @Test + void TestKeychain() throws Exception { + TestUtil.assumeSslTestsEnabled(); + + Properties props = new Properties(); + PGProperty.SSL_FACTORY.set(props, "org.postgresql.ssl.KeychainSSLFactory"); + + try { + SocketFactoryFactory.getSslSocketFactory(props); + } catch (Throwable t) { + /* ignore NoSuchProviderException */ + if (!(t instanceof PSQLException) + || (!(t.getCause() instanceof InvocationTargetException)) + || (!(t.getCause().getCause() instanceof PSQLException)) + || (!(t.getCause().getCause().getCause() instanceof NoSuchProviderException)) + ) { + throw t; + } + } + + } + + @Test + void TestMSCurrentUserSSLFactory() throws Exception { + TestUtil.assumeSslTestsEnabled(); + + Properties props = new Properties(); + PGProperty.SSL_FACTORY.set(props, "org.postgresql.ssl.MSCAPISSLFactory"); + + try { + SocketFactoryFactory.getSslSocketFactory(props); + } catch (Throwable t) { + /* ignore NoSuchProviderException */ + if (!(t instanceof PSQLException) + || (!(t.getCause() instanceof InvocationTargetException)) + || (!(t.getCause().getCause() instanceof PSQLException)) + || (!(t.getCause().getCause().getCause() instanceof NoSuchProviderException)) + ) { + throw t; + } + } + + } + + @Test + void TestMSLocalMachineSSLFactory() throws Exception { + TestUtil.assumeSslTestsEnabled(); + + Properties props = new Properties(); + PGProperty.SSL_FACTORY.set(props, "org.postgresql.ssl.MSCAPILocalMachineSSLFactory"); + + try { + SocketFactoryFactory.getSslSocketFactory(props); + } catch (Throwable t) { + /* ignore NoSuchProviderException */ + if (!(t instanceof PSQLException) + || (!(t.getCause() instanceof InvocationTargetException)) + || (!(t.getCause().getCause() instanceof PSQLException)) + || (!(t.getCause().getCause().getCause() instanceof NoSuchProviderException)) + ) { + throw t; + } + } + + } + +} diff --git a/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTestSuite.java index 95e439c45e..c462081a1c 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTestSuite.java +++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTestSuite.java @@ -13,7 +13,9 @@ CommonNameVerifierTest.class, LazyKeyManagerTest.class, LibPQFactoryHostNameTest.class, + SSLFactoryTest.class, SslTest.class, + SubjectKeyManagerTest.class, }) public class SslTestSuite { } diff --git a/pgjdbc/src/test/java/org/postgresql/test/ssl/SubjectKeyManagerTest.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/SubjectKeyManagerTest.java new file mode 100644 index 0000000000..d5e8b87539 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/SubjectKeyManagerTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.test.ssl; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.postgresql.ssl.PKCS12KeyManager; +import org.postgresql.ssl.SubjectKeyManager; +import org.postgresql.test.TestUtil; +import org.postgresql.test.ssl.PKCS12KeyTest.TestCallbackHandler; + +import org.junit.jupiter.api.Test; + +import javax.security.auth.x500.X500Principal; + +class SubjectKeyManagerTest { + + @Test + void TestChooseClientAlias() throws Exception { + PKCS12KeyManager pkcs12KeyManager = new PKCS12KeyManager(TestUtil.getSslTestCertPath("goodclient.p12"), new TestCallbackHandler("sslpwd")); + X500Principal testPrincipal = new X500Principal("CN=root certificate, O=PgJdbc test, ST=CA, C=US"); + X500Principal[] issuers = new X500Principal[]{testPrincipal}; + + String validKeyType = pkcs12KeyManager.chooseClientAlias(new String[]{"RSA"}, issuers, null); + assertNotNull(validKeyType); + + X500Principal testSubject = new X500Principal("CN=test, O=PgJdbc tests, ST=CA, C=US"); + SubjectKeyManager subjectKeyManager = new SubjectKeyManager(pkcs12KeyManager, testSubject); + + String filteredKeyType = subjectKeyManager.chooseClientAlias(new String[]{"rsa"}, issuers, null); + assertNotNull(filteredKeyType); + + X500Principal badSubject = new X500Principal("CN=bad, O=PgJdbc tests, ST=CA, C=US"); + SubjectKeyManager badSubjectKeyManager = new SubjectKeyManager(pkcs12KeyManager, badSubject); + + String badKeyType = badSubjectKeyManager.chooseClientAlias(new String[]{"rsa"}, issuers, null); + assertNull(badKeyType); + } +} From 97a9c4d2161656f7310450a588f18ed0c217ce9e Mon Sep 17 00:00:00 2001 From: Graham Leggett Date: Fri, 16 Feb 2024 11:05:08 +0000 Subject: [PATCH 2/7] Align the SSL_SUBJECT property with BaseDataSource. --- .../src/main/java/org/postgresql/PGProperty.java | 6 +++--- .../org/postgresql/ds/common/BaseDataSource.java | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java index 3980a821bc..6108169675 100644 --- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java +++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java @@ -702,12 +702,12 @@ public enum PGProperty { "The location of the root certificate for authenticating the server."), /** - * The alias of the desired client certificate in a store containing many certificates. + * The subject of the desired client certificate in a store containing many certificates. */ SSL_SUBJECT( - "sslAlias", + "sslsubject", null, - "The alias of the desired client certificate in a store with many certificates."), + "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 diff --git a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java index c3b606fa88..297bbb8091 100644 --- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java +++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java @@ -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 From 4b43fb2ccf91b842e90ffae4a3781f8cb4ef2fcb Mon Sep 17 00:00:00 2001 From: Graham Leggett Date: Fri, 16 Feb 2024 11:53:53 +0000 Subject: [PATCH 3/7] Exclude SSL_SUBJECT to align with other ssl parameters. --- .../src/test/java/org/postgresql/test/jdbc2/PGPropertyTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGPropertyTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGPropertyTest.java index 1d727fe46b..5cea50ebd6 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGPropertyTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PGPropertyTest.java @@ -248,6 +248,7 @@ void lowerCamelCase() { excluded.add("SSL_ROOT_CERT"); // ssl[r]oot[c]ert excluded.add("SSL_PASSWORD"); // ssl[p]assword excluded.add("SSL_PASSWORD_CALLBACK"); // ssl[p]assword[c]allback + excluded.add("SSL_SUBJECT"); // ssl[s]ubject excluded.add("APPLICATION_NAME"); // [A]pplicationName excluded.add("GSS_LIB"); // gss[l]ib excluded.add("REWRITE_BATCHED_INSERTS"); // re[W]riteBatchedInserts From 3ac9f7cb3470fbe774c32bc0c0e61d2b306871f6 Mon Sep 17 00:00:00 2001 From: Graham Leggett Date: Fri, 16 Feb 2024 12:05:24 +0000 Subject: [PATCH 4/7] Highlight the heading in the table. --- docs/content/documentation/ssl.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/content/documentation/ssl.md b/docs/content/documentation/ssl.md index ae6429da07..8a52d7d5d0 100644 --- a/docs/content/documentation/ssl.md +++ b/docs/content/documentation/ssl.md @@ -42,7 +42,8 @@ and the source to the `NonValidatingFactory` provided by the JDBC driver. The following implementations of SSLSocketFactory are shipped with the driver. -|sslfactory|description| +|sslfactory|Description| +|---|---| |org.postgresql.ssl.DefaultJavaSSLFactory|Use the JDK default implementation.| |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.| From 7d4adac2603cad3492bc4f95b6e151ba3102d0ea Mon Sep 17 00:00:00 2001 From: Graham Leggett Date: Fri, 16 Feb 2024 12:08:29 +0000 Subject: [PATCH 5/7] Alphabetise the list. --- docs/content/documentation/ssl.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/documentation/ssl.md b/docs/content/documentation/ssl.md index 8a52d7d5d0..37724c5b4c 100644 --- a/docs/content/documentation/ssl.md +++ b/docs/content/documentation/ssl.md @@ -45,12 +45,12 @@ 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.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.SingleCertValidatingFactory|Accept the pinned server certificate specified in the sslfactoryarg parameter.| |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 From 7262faa827600402a66fe2124f69f1716d65cf04 Mon Sep 17 00:00:00 2001 From: Graham Leggett Date: Mon, 18 Mar 2024 09:03:38 +0000 Subject: [PATCH 6/7] Capitalise java to Java. --- .../src/main/java/org/postgresql/ssl/SSLFactory.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/SSLFactory.java b/pgjdbc/src/main/java/org/postgresql/ssl/SSLFactory.java index 4108e9c865..87af9acdb6 100644 --- a/pgjdbc/src/main/java/org/postgresql/ssl/SSLFactory.java +++ b/pgjdbc/src/main/java/org/postgresql/ssl/SSLFactory.java @@ -68,7 +68,7 @@ protected SSLFactory(Properties info, final String protocol, final String algori 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}.", + throw new PSQLException(GT.tr("Could not find a Java cryptographic algorithm: {0}.", ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex); } @@ -78,7 +78,7 @@ protected SSLFactory(Properties info, final String protocol, final String algori .getInstance(algorithm); } catch (NoSuchAlgorithmException ex) { - throw new PSQLException(GT.tr("Could not find a java cryptographic algorithm: {0}.", + throw new PSQLException(GT.tr("Could not find a Java cryptographic algorithm: {0}.", ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex); } @@ -87,7 +87,7 @@ protected SSLFactory(Properties info, final String protocol, final String algori keyManagerFactory.init(keyStore, keyPassphrase); } catch (NoSuchAlgorithmException ex) { - throw new PSQLException(GT.tr("Could not find a java cryptographic algorithm: {0}.", + throw new PSQLException(GT.tr("Could not find a Java cryptographic algorithm: {0}.", ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex); } catch (KeyStoreException ex) { throw new PSQLException(GT.tr("Could not initialize SSL keystore."), @@ -129,7 +129,7 @@ protected SSLFactory(Properties info, final String protocol, final String algori 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}.", + throw new PSQLException(GT.tr("Could not find a Java cryptographic algorithm: {0}.", ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex); } @@ -139,7 +139,7 @@ protected SSLFactory(Properties info, final String protocol, final String algori .getInstance(algorithm); } catch (NoSuchAlgorithmException ex) { - throw new PSQLException(GT.tr("Could not find a java cryptographic algorithm: {0}.", + throw new PSQLException(GT.tr("Could not find a Java cryptographic algorithm: {0}.", ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex); } @@ -157,7 +157,7 @@ protected SSLFactory(Properties info, final String protocol, final String algori ctx = SSLContext.getInstance(protocol); } catch (NoSuchAlgorithmException ex) { - throw new PSQLException(GT.tr("Could not find a java cryptographic algorithm: {0}.", + throw new PSQLException(GT.tr("Could not find a Java cryptographic algorithm: {0}.", ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex); } From 30087c24aeb8ac552664025c6f6d1c2ef3b86a5d Mon Sep 17 00:00:00 2001 From: Dave Cramer Date: Wed, 24 Apr 2024 13:23:38 -0400 Subject: [PATCH 7/7] fix checkstyle --- .../src/main/java/org/postgresql/ssl/SSLFactory.java | 6 ++++-- .../java/org/postgresql/ssl/SubjectKeyManager.java | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/SSLFactory.java b/pgjdbc/src/main/java/org/postgresql/ssl/SSLFactory.java index 87af9acdb6..6b16402a40 100644 --- a/pgjdbc/src/main/java/org/postgresql/ssl/SSLFactory.java +++ b/pgjdbc/src/main/java/org/postgresql/ssl/SSLFactory.java @@ -10,6 +10,8 @@ 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; @@ -39,9 +41,9 @@ protected SSLFactory(Properties info, final String protocol, final String algori SSLContext ctx; final KeyStore keyStore; KeyStore trustStore; - final char[] keyPassphrase = null; + final char[] keyPassphrase = new char[0]; final String sslsubject = PGProperty.SSL_SUBJECT.getOrDefault(info); - final X500Principal subject; + final @Nullable X500Principal subject; final KeyManagerFactory keyManagerFactory; final TrustManagerFactory trustManagerFactory; diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/SubjectKeyManager.java b/pgjdbc/src/main/java/org/postgresql/ssl/SubjectKeyManager.java index aa3fdc3673..bb79f753f9 100644 --- a/pgjdbc/src/main/java/org/postgresql/ssl/SubjectKeyManager.java +++ b/pgjdbc/src/main/java/org/postgresql/ssl/SubjectKeyManager.java @@ -22,7 +22,7 @@ public class SubjectKeyManager implements X509KeyManager { private final X509KeyManager km; - private final X500Principal subject; + private final @Nullable X500Principal subject; /** *

@@ -81,27 +81,27 @@ public SubjectKeyManager(X509KeyManager km, @Nullable X500Principal subject) { } @Override - public String[] getClientAliases(String keyType, Principal[] issuers) { + public String @Nullable [] getClientAliases(String keyType, Principal @Nullable [] issuers) { return this.km.getClientAliases(keyType, issuers); } @Override - public String[] getServerAliases(String keyType, Principal[] issuers) { + public String @Nullable [] getServerAliases(String keyType, Principal@Nullable [] issuers) { return this.km.getServerAliases(keyType, issuers); } @Override - public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + public @Nullable String chooseServerAlias(String keyType, Principal @Nullable [] issuers, @Nullable Socket socket) { return this.km.chooseServerAlias(keyType, issuers, socket); } @Override - public X509Certificate[] getCertificateChain(String alias) { + public X509Certificate @Nullable[] getCertificateChain(String alias) { return this.km.getCertificateChain(alias); } @Override - public PrivateKey getPrivateKey(String alias) { + public @Nullable PrivateKey getPrivateKey(String alias) { return this.km.getPrivateKey(alias); }