Message-ID: From: "vlsi (@vlsi)" To: "pgjdbc/pgjdbc" Date: Tue, 16 Jun 2026 19:49:13 +0000 Subject: [pgjdbc/pgjdbc] PR #4193: fix(ssl): build PKIX trust anchors without a KeyStore so FIPS-mode JVMs can load sslrootcert List-Id: X-GitHub-Additions: 118 X-GitHub-Author-Id: 213894 X-GitHub-Author-Login: vlsi X-GitHub-Base: master X-GitHub-Changed-Files: 5 X-GitHub-Commits: 1 X-GitHub-Deletions: 29 X-GitHub-Head-Branch: claude/suspicious-antonelli-4a8c63 X-GitHub-Head-SHA: 08cfd02aeb835ac1db01938d29454e47b0cd3365 X-GitHub-Issue: 4193 X-GitHub-Merge-SHA: 6abfc848bc1d700fdf82c9a8014bcad0ac36b9dc X-GitHub-Merged-By: vlsi X-GitHub-Repo: pgjdbc/pgjdbc X-GitHub-State: merged X-GitHub-Type: pull_request X-GitHub-Url: https://github.com/pgjdbc/pgjdbc/pull/4193 Content-Type: text/plain; charset=utf-8 ## 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 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 #4191: 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. + * + *

Each test makes every {@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 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 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 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;