pgjdbc/pgjdbc GitHub issues and pull requests (mirror)  
help / color / mirror / Atom feed
From: vlsi (@vlsi) <[email protected]>
To: pgjdbc/pgjdbc <[email protected]>
Subject: [pgjdbc/pgjdbc] PR #4193: fix(ssl): build PKIX trust anchors without a KeyStore so FIPS-mode JVMs can load sslrootcert
Date: Tue, 16 Jun 2026 19:49:13 +0000
Message-ID: <[email protected]> (raw)

## Why

In FIPS mode (Semeru FIPS 140-3, IBM SDK in FIPS mode, BouncyCastle FIPS) the JVM exposes no general-purpose `KeyStore` type. `LibPQFactory` builds a transient, in-memory truststore for `sslrootcert` by calling `KeyStore.getInstance("jks")`, which those JVMs reject, so `verify-ca`/`verify-full` connections fail with `NoSuchAlgorithmException`.

[#4192](https://github.com/pgjdbc/pgjdbc/pull/4192) proposed switching to `KeyStore.getDefaultType()`, but the reporter confirmed on #4191 that the default (`pkcs12`) is unavailable under Semeru FIPS too — so swapping one missing type for another is not enough.

`SingleCertValidatingFactory` builds the same kind of in-memory truststore and has the identical bug.

## What

Drop the `KeyStore` from both in-memory truststores and build the PKIX trust anchors directly:

- Parse the certificates with `CertificateFactory`, wrap each in a `TrustAnchor`.
- Feed them to the PKIX `TrustManagerFactory` via `PKIXBuilderParameters` + `CertPathTrustManagerParameters`.

The PKIX `TrustManagerFactory` derives the same trust anchors it would have extracted from a keystore of trusted-cert entries, so non-FIPS behaviour is unchanged, while FIPS JVMs no longer need any `KeyStore` type. Revocation honours `com.sun.net.ssl.checkRevocation` exactly as the keystore-based factory did (default off).

The two remaining `KeyStore.getInstance` call sites (`PKCS12KeyManager`, `DbKeyStoreSocketFactory`) load real on-disk keystore files, where the type is the file format; they are intentionally left as is.

## How to verify

`LibPQFactoryFipsTest` (in `pgjdbc-mockito-test`) makes **every** `KeyStore.getInstance(...)` throw — stricter than real FIPS — and asserts both factories still build a working trust manager:

```
./gradlew :pgjdbc-mockito-test:test --tests 'org.postgresql.test.ssl.LibPQFactoryFipsTest'
```

Closes #4191

diff --git a/config/checkerframework/PKIXBuilderParameters.astub b/config/checkerframework/PKIXBuilderParameters.astub
new file mode 100644
index 0000000000..4eb5d4c084
--- /dev/null
+++ b/config/checkerframework/PKIXBuilderParameters.astub
@@ -0,0 +1,8 @@
+package java.security.cert;
+
+import org.checkerframework.checker.nullness.qual.*;
+
+class PKIXBuilderParameters {
+    // targetConstraints may be null: "null to specify that there are no constraints".
+    PKIXBuilderParameters(java.util.Set<TrustAnchor> trustAnchors, @Nullable CertSelector targetConstraints);
+}
diff --git a/config/checkerframework/TrustAnchor.astub b/config/checkerframework/TrustAnchor.astub
new file mode 100644
index 0000000000..dbeef4da03
--- /dev/null
+++ b/config/checkerframework/TrustAnchor.astub
@@ -0,0 +1,8 @@
+package java.security.cert;
+
+import org.checkerframework.checker.nullness.qual.*;
+
+class TrustAnchor {
+    // nameConstraints is documented as optional: "Specify null to omit the parameter."
+    TrustAnchor(X509Certificate trustedCert, byte @Nullable [] nameConstraints);
+}
diff --git a/pgjdbc-mockito-test/src/test/java/org/postgresql/test/ssl/LibPQFactoryFipsTest.java b/pgjdbc-mockito-test/src/test/java/org/postgresql/test/ssl/LibPQFactoryFipsTest.java
new file mode 100644
index 0000000000..a6f808fe4a
--- /dev/null
+++ b/pgjdbc-mockito-test/src/test/java/org/postgresql/test/ssl/LibPQFactoryFipsTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mockStatic;
+
+import org.postgresql.PGProperty;
+import org.postgresql.ssl.LibPQFactory;
+import org.postgresql.ssl.SingleCertValidatingFactory.SingleCertTrustManager;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.util.Properties;
+
+/**
+ * Regression test for <a href="https://github.com/pgjdbc/pgjdbc/issues/4191">#4191</a;: a FIPS-mode
+ * JVM (Semeru FIPS 140-3) exposes no general-purpose KeyStore type -- {@code "jks"} and the default
+ * {@code "pkcs12"} both throw -- so the SSL factories must build their PKIX trust anchors without
+ * any {@link KeyStore} at all.
+ *
+ * <p>Each test makes <em>every</em> {@code KeyStore.getInstance(...)} call fail through a
+ * thread-scoped static mock, which is stricter than real FIPS, and asserts the factory still works.
+ */
+class LibPQFactoryFipsTest {
+
+  @Test
+  void libpqFactoryBuildsTruststoreWithoutKeyStore() {
+    Properties info = new Properties();
+    PGProperty.SSL_MODE.set(info, "verify-ca");
+    PGProperty.SSL_ROOT_CERT.set(info, "../certdir/goodroot.crt");
+
+    try (MockedStatic<KeyStore> ks = mockStatic(KeyStore.class, CALLS_REAL_METHODS)) {
+      ks.when(() -> KeyStore.getInstance(anyString()))
+          .thenThrow(new KeyStoreException("FIPS: no general-purpose KeyStore"));
+
+      assertDoesNotThrow(() -> new LibPQFactory(info));
+    }
+  }
+
+  @Test
+  void singleCertTrustManagerBuildsWithoutKeyStore() {
+    try (MockedStatic<KeyStore> ks = mockStatic(KeyStore.class, CALLS_REAL_METHODS)) {
+      ks.when(() -> KeyStore.getInstance(anyString()))
+          .thenThrow(new KeyStoreException("FIPS: no general-purpose KeyStore"));
+
+      assertDoesNotThrow(() -> {
+        try (InputStream in = Files.newInputStream(Paths.get("../certdir/goodroot.crt"))) {
+          new SingleCertTrustManager(in);
+        }
+      });
+    }
+  }
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java b/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java
index cf0a0eaabf..49461d6bfd 100644
--- a/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java
+++ b/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java
@@ -25,14 +25,18 @@
 import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.security.KeyManagementException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateFactory;
+import java.security.cert.PKIXBuilderParameters;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509Certificate;
+import java.util.HashSet;
 import java.util.Locale;
 import java.util.Properties;
+import java.util.Set;
 
+import javax.net.ssl.CertPathTrustManagerParameters;
 import javax.net.ssl.KeyManager;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.TrustManager;
@@ -153,13 +157,6 @@ public LibPQFactory(Properties info) throws PSQLException {
         // Load the server certificate
 
         TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
-        KeyStore ks;
-        try {
-          ks = KeyStore.getInstance("jks");
-        } catch (KeyStoreException e) {
-          // this should never happen
-          throw new NoSuchAlgorithmException("jks KeyStore not available");
-        }
         String sslrootcertfile = PGProperty.SSL_ROOT_CERT.getOrDefault(info);
         if (sslrootcertfile == null) { // Fall back to default
           sslrootcertfile = defaultdir + "root.crt";
@@ -174,18 +171,19 @@ public LibPQFactory(Properties info) throws PSQLException {
         }
         try {
           CertificateFactory cf = CertificateFactory.getInstance("X.509");
-          // Certificate[] certs = cf.generateCertificates(is).toArray(new Certificate[]{}); //Does
-          // not work in java 1.4
-          Object[] certs = cf.generateCertificates(is).toArray(new Certificate[]{});
-          ks.load(null, null);
-          for (int i = 0; i < certs.length; i++) {
-            ks.setCertificateEntry("cert" + i, (Certificate) certs[i]);
+          // Build PKIX trust anchors straight from the certificates rather than via an
+          // intermediate KeyStore. FIPS-mode JVMs (Semeru FIPS 140-3, IBM SDK, BouncyCastle FIPS)
+          // expose no general-purpose KeyStore type -- both "jks" and the default "pkcs12" fail --
+          // yet this truststore is transient and never serialised, so a KeyStore buys nothing.
+          Set<TrustAnchor> anchors = new HashSet<>();
+          for (Certificate cert : cf.generateCertificates(is)) {
+            anchors.add(new TrustAnchor((X509Certificate) cert, null));
           }
-          tmf.init(ks);
-        } catch (IOException ioex) {
-          throw new PSQLException(
-              GT.tr("Could not read SSL root certificate file {0}.", sslrootcertfile),
-              PSQLState.CONNECTION_FAILURE, ioex);
+          PKIXBuilderParameters params = new PKIXBuilderParameters(anchors, null);
+          // Honour the same revocation toggle the KeyStore-based PKIX TrustManagerFactory read,
+          // so applications that enabled CRL/OCSP checking keep it (default off, as before).
+          params.setRevocationEnabled(Boolean.getBoolean("com.sun.net.ssl.checkRevocation"));
+          tmf.init(new CertPathTrustManagerParameters(params));
         } catch (GeneralSecurityException gsex) {
           throw new PSQLException(
               GT.tr("Loading the SSL root certificate {0} into a TrustManager failed.",
diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/SingleCertValidatingFactory.java b/pgjdbc/src/main/java/org/postgresql/ssl/SingleCertValidatingFactory.java
index bd47d6907f..510589bb30 100644
--- a/pgjdbc/src/main/java/org/postgresql/ssl/SingleCertValidatingFactory.java
+++ b/pgjdbc/src/main/java/org/postgresql/ssl/SingleCertValidatingFactory.java
@@ -14,12 +14,14 @@
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.security.GeneralSecurityException;
-import java.security.KeyStore;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
+import java.security.cert.PKIXBuilderParameters;
+import java.security.cert.TrustAnchor;
 import java.security.cert.X509Certificate;
-import java.util.UUID;
+import java.util.Collections;
 
+import javax.net.ssl.CertPathTrustManagerParameters;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.TrustManagerFactory;
@@ -167,15 +169,23 @@ public static class SingleCertTrustManager implements X509TrustManager {
     X509TrustManager trustManager;
 
     public SingleCertTrustManager(InputStream in) throws IOException, GeneralSecurityException {
-      KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
-      // Note: KeyStore requires it be loaded even if you don't load anything into it:
-      ks.load(null);
       CertificateFactory cf = CertificateFactory.getInstance("X509");
       cert = (X509Certificate) cf.generateCertificate(in);
-      ks.setCertificateEntry(UUID.randomUUID().toString(), cert);
-      TrustManagerFactory tmf =
-          TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
-      tmf.init(ks);
+      // Build the trust anchor directly instead of via an in-memory KeyStore. FIPS-mode JVMs
+      // (Semeru FIPS 140-3, IBM SDK, BouncyCastle FIPS) expose no general-purpose KeyStore type --
+      // both "jks" and the default "pkcs12" fail -- and this single-cert truststore is never
+      // serialised, so a KeyStore buys nothing.
+      PKIXBuilderParameters params =
+          new PKIXBuilderParameters(Collections.singleton(new TrustAnchor(cert, null)), null);
+      // Honour the same revocation toggle the KeyStore-based PKIX TrustManagerFactory read,
+      // so applications that enabled CRL/OCSP checking keep it (default off, as before).
+      params.setRevocationEnabled(Boolean.getBoolean("com.sun.net.ssl.checkRevocation"));
+      // Request "PKIX" explicitly rather than TrustManagerFactory.getDefaultAlgorithm(). The
+      // KeyStore-free init below passes ManagerFactoryParameters, which only the PKIX factory
+      // accepts (SunX509 rejects them). The default algorithm is already "PKIX" on a stock JDK, so
+      // this changes behaviour only if a custom provider made some other algorithm the default.
+      TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
+      tmf.init(new CertPathTrustManagerParameters(params));
       for (TrustManager tm : tmf.getTrustManagers()) {
         if (tm instanceof X509TrustManager) {
           trustManager = (X509TrustManager) tm;


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 #4193: fix(ssl): build PKIX trust anchors without a KeyStore 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