pgjdbc/pgjdbc GitHub issues and pull requests (mirror)
help / color / mirror / Atom feedFrom: davecramer (@davecramer) <[email protected]>
To: pgjdbc/pgjdbc <[email protected]>
Subject: [pgjdbc/pgjdbc] PR #4192: fix: use default KeyStore type so FIPS-mode JVMs can load sslrootcert
Date: Tue, 16 Jun 2026 15:28:14 +0000
Message-ID: <[email protected]> (raw)
LibPQFactory builds a transient in-memory truststore to hold the certs parsed from sslrootcert, then hands it to a PKIX TrustManagerFactory. The store is never serialised, so the type is incidental — but it was hardcoded to "jks", which FIPS-mode JCE providers (Semeru FIPS 140-3, IBM SDK in FIPS mode, BouncyCastle FIPS) reject as a non-approved algorithm. Connections under -Dsemeru.fips=true failed with NoSuchAlgorithmException("jks KeyStore not available").
Use KeyStore.getDefaultType() instead, mirroring what SingleCertValidatingFactory has been doing in the same package. On non-FIPS stock JDKs this resolves to PKCS12 (JDK 9+) or jks (JDK 8), both of which support the in-memory setCertificateEntry use case identically.
Also improve the diagnostic if the default-type lookup ever fails: the thrown NoSuchAlgorithmException now interpolates the resolved KeyStore type name and chains the underlying KeyStoreException as its cause, replacing the previous fixed "jks KeyStore not available" message and a misleading "this should never happen" comment.
Closes Issue #4191
diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java b/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java
index cf0a0eaabf..71a9d1c56d 100644
--- a/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java
+++ b/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java
@@ -153,12 +153,16 @@ public LibPQFactory(Properties info) throws PSQLException {
// Load the server certificate
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
+ // Use the runtime's default KeyStore type so FIPS-mode JVMs (which reject "jks")
+ // can still build the in-memory truststore that holds the sslrootcert. The store
+ // is never serialised, so the on-disk format is irrelevant.
KeyStore ks;
+ String ksType = KeyStore.getDefaultType();
try {
- ks = KeyStore.getInstance("jks");
+ ks = KeyStore.getInstance(ksType);
} catch (KeyStoreException e) {
- // this should never happen
- throw new NoSuchAlgorithmException("jks KeyStore not available");
+ throw new NoSuchAlgorithmException(
+ "Default KeyStore type \"" + ksType + "\" not available", e);
}
String sslrootcertfile = PGProperty.SSL_ROOT_CERT.getOrDefault(info);
if (sslrootcertfile == null) { // Fall back to default
diff --git a/pgjdbc/src/test/java/org/postgresql/test/ssl/LibPQFactoryKeyStoreTypeTest.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/LibPQFactoryKeyStoreTypeTest.java
new file mode 100644
index 0000000000..bba6d85254
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/LibPQFactoryKeyStoreTypeTest.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2026, 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.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import org.postgresql.PGProperty;
+import org.postgresql.ssl.LibPQFactory;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.KeyStoreSpi;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.Security;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.Properties;
+
+/**
+ * Regression test for https://github.com/pgjdbc/pgjdbc/issues/4191: under FIPS-mode JVMs, the
+ * legacy {@code "jks"} KeyStore type is unavailable, and the driver must use
+ * {@link KeyStore#getDefaultType()} instead.
+ *
+ * <p>We can't actually run on a FIPS JVM in CI, so we simulate the failure by installing a
+ * {@link Provider} that exposes a {@code "jks"} KeyStoreSpi which throws on every operation.
+ * With that provider at position 1, any caller that asks for {@code KeyStore.getInstance("jks")}
+ * will get a broken store. The driver instead asks for the default type, which still resolves to
+ * a working SUN/PKCS12 implementation, so {@link LibPQFactory} constructs successfully.
+ */
+class LibPQFactoryKeyStoreTypeTest {
+
+ private static final String FAKE_PROVIDER_NAME = "PgJdbcFipsSimulator";
+
+ /**
+ * KeyStoreSpi that throws on every operation. Stands in for what a FIPS provider would do if a
+ * non-approved {@code jks} type were requested.
+ */
+ public static class FailingKeyStoreSpi extends KeyStoreSpi {
+ @Override
+ public Key engineGetKey(String alias, char[] password) {
+ throw new UnsupportedOperationException("FIPS: jks not allowed");
+ }
+
+ @Override
+ public Certificate[] engineGetCertificateChain(String alias) {
+ throw new UnsupportedOperationException("FIPS: jks not allowed");
+ }
+
+ @Override
+ public Certificate engineGetCertificate(String alias) {
+ throw new UnsupportedOperationException("FIPS: jks not allowed");
+ }
+
+ @Override
+ public Date engineGetCreationDate(String alias) {
+ throw new UnsupportedOperationException("FIPS: jks not allowed");
+ }
+
+ @Override
+ public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) {
+ throw new UnsupportedOperationException("FIPS: jks not allowed");
+ }
+
+ @Override
+ public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) {
+ throw new UnsupportedOperationException("FIPS: jks not allowed");
+ }
+
+ @Override
+ public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException {
+ throw new KeyStoreException("FIPS: jks not allowed");
+ }
+
+ @Override
+ public void engineDeleteEntry(String alias) {
+ throw new UnsupportedOperationException("FIPS: jks not allowed");
+ }
+
+ @Override
+ public Enumeration<String> engineAliases() {
+ return Collections.emptyEnumeration();
+ }
+
+ @Override
+ public boolean engineContainsAlias(String alias) {
+ return false;
+ }
+
+ @Override
+ public int engineSize() {
+ return 0;
+ }
+
+ @Override
+ public boolean engineIsKeyEntry(String alias) {
+ return false;
+ }
+
+ @Override
+ public boolean engineIsCertificateEntry(String alias) {
+ return false;
+ }
+
+ @Override
+ public String engineGetCertificateAlias(Certificate cert) {
+ return null;
+ }
+
+ @Override
+ public void engineStore(OutputStream stream, char[] password) {
+ throw new UnsupportedOperationException("FIPS: jks not allowed");
+ }
+
+ @Override
+ public void engineLoad(InputStream stream, char[] password)
+ throws IOException, NoSuchAlgorithmException, CertificateException {
+ throw new IOException("FIPS: jks not allowed");
+ }
+ }
+
+ /**
+ * Test-only Provider that registers the failing {@code "jks"} KeyStoreSpi. Installed at
+ * position 1 so it shadows the real SUN provider's {@code "jks"} entry while preserving every
+ * other algorithm and keystore type.
+ */
+ public static class FipsSimulatorProvider extends Provider {
+ // Use the JDK 8-compatible deprecated constructor;
+ // Provider(String, String, String) requires JDK 9+.
+ @SuppressWarnings("deprecation")
+ public FipsSimulatorProvider() {
+ super(FAKE_PROVIDER_NAME, 1.0, "Simulates FIPS-mode rejection of jks for tests");
+ put("KeyStore.jks", FailingKeyStoreSpi.class.getName());
+ }
+ }
+
+ private String originalKeyStoreType;
+
+ @BeforeEach
+ void installFipsSimulator() {
+ // Security.* state is JVM-global. If install partially succeeds, clean up before
+ // propagating so a sibling test in the same JVM cannot be shadowed.
+ originalKeyStoreType = Security.getProperty("keystore.type");
+ boolean clean = false;
+ try {
+ // Force the JVM default to a working type even on JDK 8 where the default is "jks".
+ Security.setProperty("keystore.type", "PKCS12");
+ Security.insertProviderAt(new FipsSimulatorProvider(), 1);
+ clean = true;
+ } finally {
+ if (!clean) {
+ removeFipsSimulator();
+ }
+ }
+ }
+
+ @AfterEach
+ void removeFipsSimulator() {
+ Security.removeProvider(FAKE_PROVIDER_NAME);
+ if (originalKeyStoreType != null) {
+ Security.setProperty("keystore.type", originalKeyStoreType);
+ }
+ }
+
+ @Test
+ void simulatorMakesJksUnusable() throws Exception {
+ // Sanity check: with our provider at position 1, asking for "jks" returns the failing SPI.
+ KeyStore ks = KeyStore.getInstance("jks");
+ assertEquals(FAKE_PROVIDER_NAME, ks.getProvider().getName(),
+ "test fixture must shadow the real jks provider");
+ }
+
+ @Test
+ void defaultKeyStoreTypeBypassesFailingJks() {
+ // With keystore.type=PKCS12 the default-type lookup must NOT return our failing jks SPI.
+ KeyStore ks = assertDoesNotThrow(() -> KeyStore.getInstance(KeyStore.getDefaultType()));
+ assertNotEquals(FAKE_PROVIDER_NAME, ks.getProvider().getName(),
+ "default keystore type must resolve to a working provider, not the simulated FIPS one");
+ }
+
+ @Test
+ void libpqFactoryConstructsUnderSimulatedFips() {
+ // The verifyCertificate path inside LibPQFactory creates the in-memory truststore that the
+ // bug reports under #4191. With the fix, it asks for the default KeyStore type and skips
+ // our failing "jks" SPI; without the fix, this constructor would throw
+ // NoSuchAlgorithmException("jks KeyStore not available").
+ Properties info = new Properties();
+ // sslmode=verify-ca is what makes LibPQFactory hit the truststore branch (#4191's path).
+ PGProperty.SSL_MODE.set(info, "verify-ca");
+ PGProperty.SSL_ROOT_CERT.set(info, "../certdir/goodroot.crt");
+ // Point sslkey/sslcert at existing files so the constructor reaches the keystore branch
+ // even if LazyKeyManager/PEMKeyManager become eager about file I/O in the future
+ // (today they defer reads until the SSL handshake, so the bug site at LibPQFactory.java:159
+ // is reachable without these — but pinning real paths keeps this test from rotting silently).
+ PGProperty.SSL_KEY.set(info, "../certdir/goodclient.key");
+ PGProperty.SSL_CERT.set(info, "../certdir/goodclient.crt");
+ assertDoesNotThrow(() -> new LibPQFactory(info));
+ }
+}
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: github://pgjdbc/pgjdbc
Cc: [email protected], [email protected]
Subject: Re: [pgjdbc/pgjdbc] PR #4192: fix: use default KeyStore type so FIPS-mode JVMs can load sslrootcert
In-Reply-To: <<[email protected]>>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox