public inbox for [email protected]  
help / color / mirror / Atom feed
From: harinath kanchu <[email protected]>
To: [email protected]
Subject: Patch for supporting PEM based certs and keys
Date: Thu, 26 Jun 2025 14:15:31 -0700
Message-ID: <CAO7WNRRhJst=iT2C6kBjg+bYsQTBvN5ksXrNS5m+vkYGa+wGGw@mail.gmail.com> (raw)

Hello Pgjdbc community,

I found that PGJDBC currently lacks support for PEM based certs and keys.

We have a use case where PEM files are auto renewed on disk and
converting them to DER format requires running something that watches
files on disk and auto-converts to DER.

Hence I would like to propose a patch for supporting PEM based certs, keys.

This is the approach for adding the support,

- Introduce a new PEMKeyManager which implements X509KeyManager.
- PEMKeyManager will have the logic for extracting the BASE64 encoded
DER bytes to convert into private key using key algorithm specified by
property PGProperty.PEM_KEY_ALGORITHM.
- PEMKeyManager will read the PEM based cert chain using
CertificateFactory to get the X509Certificate chain.
- Now LibPQFactory can initialize PEMKeyManager if the SSL Keyfile
ends with .key or .pem

I am attaching a patch file which also contains new test cases for PEM
based certs, keys. Please take a look.

Thanks.

Regards,
Harinath


Attachments:

  [application/octet-stream] 0001-Add-PEMKeyManager-to-handle-PEM-based-certs-and-keys.patch (13.4K, 2-0001-Add-PEMKeyManager-to-handle-PEM-based-certs-and-keys.patch)
  download | inline diff:
From a0f86a3e38a09a5ae616252c3b8d64cb5982643d Mon Sep 17 00:00:00 2001
From: harinath <[email protected]>
Date: Thu, 1 May 2025 14:48:02 -0700
Subject: [PATCH] Add PEMKeyManager to handle PEM based certs and keys.

---
 .../main/java/org/postgresql/PGProperty.java  |   7 +
 .../java/org/postgresql/ssl/LibPQFactory.java |  35 +++-
 .../org/postgresql/ssl/PEMKeyManager.java     | 168 ++++++++++++++++++
 .../test/ssl/PEMKeyManagerTest.java           |  58 ++++++
 4 files changed, 262 insertions(+), 6 deletions(-)
 create mode 100644 pgjdbc/src/main/java/org/postgresql/ssl/PEMKeyManager.java
 create mode 100644 pgjdbc/src/test/java/org/postgresql/test/ssl/PEMKeyManagerTest.java

diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
index 740f4e59..b31fc5a5 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
@@ -830,6 +830,13 @@ public enum PGProperty {
       "",
       "Factory class to instantiate factories for XML processing"),
 
+    /**
+     * Algorithm for the PEM key.
+     */
+    PEM_KEY_ALGORITHM(
+            "pemKeyAlgorithm",
+            "RSA",
+            "Algorithm of the PEM key"),
   ;
 
   private final String name;
diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java b/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java
index 33f16e0d..e51b981e 100644
--- a/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java
+++ b/pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java
@@ -70,16 +70,20 @@ public class LibPQFactory extends WrappedFactory {
     return cbh;
   }
 
+  private String getCertFilePath(String defaultdir, Properties info) {
+      // Load the client's certificate and key
+      String sslcertfile = PGProperty.SSL_CERT.getOrDefault(info);
+      if (sslcertfile == null) { // Fall back to default
+          defaultfile = true;
+          sslcertfile = defaultdir + "postgresql.crt";
+      }
+      return sslcertfile;
+  }
   private void initPk8(
       @UnderInitialization(WrappedFactory.class) LibPQFactory this,
       String sslkeyfile, String defaultdir, Properties info) throws  PSQLException {
 
-    // Load the client's certificate and key
-    String sslcertfile = PGProperty.SSL_CERT.getOrDefault(info);
-    if (sslcertfile == null) { // Fall back to default
-      defaultfile = true;
-      sslcertfile = defaultdir + "postgresql.crt";
-    }
+    String sslcertfile = getCertFilePath(defaultdir, info);
 
     // If the properties are empty, give null to prevent client key selection
     km = new LazyKeyManager(("".equals(sslcertfile) ? null : sslcertfile),
@@ -92,6 +96,20 @@ public class LibPQFactory extends WrappedFactory {
     km = new PKCS12KeyManager(sslkeyfile, getCallbackHandler(info));
   }
 
+  private void initPEM(@UnderInitialization(WrappedFactory.class) LibPQFactory this,
+                       String sslKeyFile,
+                       String defaultdir,
+                       Properties info) throws PSQLException {
+    try {
+        String sslCertFile = getCertFilePath(defaultdir, info);
+        String algorithm = PGProperty.PEM_KEY_ALGORITHM.getOrDefault(info);
+        km = new PEMKeyManager(sslKeyFile, sslCertFile, algorithm);
+    } catch (Exception ex) {
+        ex.printStackTrace(System.out);
+        throw new PSQLException(GT.tr("Could not initial PEM files."), PSQLState.CONNECTION_FAILURE, ex);
+    }
+  }
+
   /**
    * @param info the connection parameters The following parameters are used:
    *        sslmode,sslcert,sslkey,sslrootcert,sslhostnameverifier,sslpasswordcallback,sslpassword
@@ -119,6 +137,8 @@ public class LibPQFactory extends WrappedFactory {
 
       if (sslkeyfile.endsWith(".p12") || sslkeyfile.endsWith(".pfx")) {
         initP12(sslkeyfile, info);
+      } else if (sslkeyfile.endsWith(".key") || sslkeyfile.endsWith(".pem")) {
+        initPEM(sslkeyfile, defaultdir, info);
       } else {
         initPk8(sslkeyfile, defaultdir, info);
       }
@@ -209,6 +229,9 @@ public class LibPQFactory extends WrappedFactory {
       if (km instanceof PKCS12KeyManager) {
         ((PKCS12KeyManager) km).throwKeyManagerException();
       }
+      if (km instanceof PEMKeyManager) {
+          ((PEMKeyManager) km).throwKeyManagerException();
+      }
     }
   }
 
diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/PEMKeyManager.java b/pgjdbc/src/main/java/org/postgresql/ssl/PEMKeyManager.java
new file mode 100644
index 00000000..41eb0e93
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/ssl/PEMKeyManager.java
@@ -0,0 +1,168 @@
+package org.postgresql.ssl;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.postgresql.util.GT;
+import org.postgresql.util.PSQLException;
+import org.postgresql.util.PSQLState;
+
+import javax.net.ssl.X509KeyManager;
+import javax.security.auth.x500.X500Principal;
+import java.io.InputStream;
+import java.net.Socket;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.*;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.*;
+
+public class PEMKeyManager implements X509KeyManager {
+
+    // private final CallbackHandler cbh;
+
+    private @Nullable PSQLException error;
+
+    private final String keyFilePath;
+    private final String certFilePath;
+
+
+    private final String keyAlgorithm;
+
+    public PEMKeyManager(String pemKeyPath, String pemCertsPath, String keyAlgorithm) {
+        this.keyFilePath = pemKeyPath;
+        this.certFilePath = pemCertsPath;
+        this.keyAlgorithm = keyAlgorithm;
+    }
+
+    /**
+     * getCertificateChain and getPrivateKey cannot throw exceptions, therefore any exception is stored
+     * in {@link #error} and can be raised by this method.
+     *
+     * @throws PSQLException if any exception is stored in {@link #error} and can be raised
+     */
+    public void throwKeyManagerException() throws PSQLException {
+        if (error != null) {
+            throw error;
+        }
+    }
+
+    @Override
+    public @Nullable PrivateKey getPrivateKey(String s) {
+        try {
+            List<String> lines = Files.readAllLines(Paths.get(keyFilePath));
+            StringBuilder keyContent = new StringBuilder();
+            for (String line : lines) {
+                if (!line.contains("BEGIN PRIVATE KEY") &&
+                    !line.contains("BEGIN RSA PRIVATE KEY") &&
+                    !line.contains("END PRIVATE KEY")&&
+                    !line.contains("END RSA PRIVATE KEY")) {
+                    keyContent.append(line.trim());
+                }
+            }
+
+            byte[] privateKeyDERBytes = Base64.getDecoder().decode(keyContent.toString());
+            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyDERBytes);
+            KeyFactory kf = KeyFactory.getInstance(this.keyAlgorithm);
+
+            return kf.generatePrivate(keySpec);
+        } catch (Exception e) {
+            error = new PSQLException(GT.tr("Could not load the private key"), PSQLState.CONNECTION_FAILURE, e);
+        }
+        return null;
+    }
+
+    @Override
+    public X509Certificate @Nullable [] getCertificateChain(String alias) {
+        try (InputStream inStream = Files.newInputStream(Paths.get(this.certFilePath))) {
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+
+            Collection<? extends java.security.cert.Certificate> certs = cf.generateCertificates(inStream);
+            List<X509Certificate> certChain = new ArrayList<>();
+
+            for (Certificate cert : certs) {
+                if (cert instanceof X509Certificate) {
+                    certChain.add((X509Certificate) cert);
+                }
+            }
+
+            return certChain.toArray(new X509Certificate[0]);
+        } catch (Exception e) {
+            error = new PSQLException(GT.tr("Could not load cert chain"), PSQLState.CONNECTION_FAILURE, e);
+        }
+        return null;
+    }
+
+    @Override
+    public String @Nullable [] getClientAliases(String keyType, Principal @Nullable [] principals) {
+        String alias = chooseClientAlias(new String[]{keyType}, principals, (Socket) null);
+        return alias == null ? null : new String[]{alias};
+    }
+
+    @Override
+    public @Nullable String chooseClientAlias(String[] keyType, Principal @Nullable [] principals,
+                                              @Nullable Socket socket) {
+        if (principals == null || principals.length == 0) {
+            // Postgres 8.4 and earlier do not send the list of accepted certificate authorities
+            // to the client. See BUG #5468. We only hope, that our certificate will be accepted.
+            return "user";
+        } else {
+            // Sending a wrong certificate makes the connection rejected, even, if clientcert=0 in
+            // pg_hba.conf.
+            // therefore we only send our certificate, if the issuer is listed in issuers
+            X509Certificate[] certchain = getCertificateChain("user");
+            if (certchain == null) {
+                return null;
+            } else {
+                X509Certificate cert = certchain[certchain.length - 1];
+                X500Principal ourissuer = cert.getIssuerX500Principal();
+                String certKeyType = cert.getPublicKey().getAlgorithm();
+                boolean keyTypeFound = false;
+                boolean found = false;
+                if (keyType != null && keyType.length > 0) {
+                    for (String kt : keyType) {
+                        if (kt.equalsIgnoreCase(certKeyType)) {
+                            keyTypeFound = true;
+                        }
+                    }
+                } else {
+                    // If no key types were passed in, assume we don't care
+                    // about checking that the cert uses a particular key type.
+                    keyTypeFound = true;
+                }
+                if (keyTypeFound) {
+                    for (Principal issuer : principals) {
+                        if (ourissuer.equals(issuer)) {
+                            found = keyTypeFound;
+                        }
+                    }
+                }
+                return found ? "user" : null;
+            }
+        }
+    }
+
+    @Override
+    public String @Nullable [] getServerAliases(String s, Principal @Nullable [] principals) {
+        return new String[]{};
+    }
+
+    @Override
+    public @Nullable String chooseServerAlias(String s, Principal @Nullable [] principals,
+                                              @Nullable Socket socket) {
+        // we are not a server
+        return null;
+    }
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/ssl/PEMKeyManagerTest.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/PEMKeyManagerTest.java
new file mode 100644
index 00000000..3d66326b
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/PEMKeyManagerTest.java
@@ -0,0 +1,58 @@
+package org.postgresql.test.ssl;
+
+import org.junit.jupiter.api.Test;
+import org.postgresql.PGProperty;
+import org.postgresql.ssl.PEMKeyManager;
+import org.postgresql.test.TestUtil;
+
+import javax.security.auth.x500.X500Principal;
+import java.sql.Connection;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class PEMKeyManagerTest {
+
+    @Test
+    void TestGoodClientPEM() throws Exception {
+        TestUtil.assumeSslTestsEnabled();
+
+        Properties props = new Properties();
+        props.put(TestUtil.DATABASE_PROP, "hostssldb");
+        PGProperty.SSL_MODE.set(props, "prefer");
+        PGProperty.SSL_KEY.set(props, TestUtil.getSslTestCertPath("goodclient.key"));
+        PGProperty.SSL_CERT.set(props, TestUtil.getSslTestCertPath("goodclient.crt"));
+        PGProperty.PEM_KEY_ALGORITHM.set(props, "RSA");
+
+        try (Connection conn = TestUtil.openDB(props)) {
+            boolean sslUsed = TestUtil.queryForBoolean(conn, "SELECT ssl_is_used()");
+            assertTrue(sslUsed, "SSL should be in use");
+        }
+    }
+    @Test
+    void TestChooseClientAlias() {
+        String sslKeyFile = TestUtil.getSslTestCertPath("goodclient.key");
+        String sslCertFile = TestUtil.getSslTestCertPath("goodclient.crt");
+        PEMKeyManager pemKeyManager = new PEMKeyManager(sslKeyFile, sslCertFile, "RSA");
+
+        X500Principal testPrincipal = new X500Principal("CN=root certificate, O=PgJdbc test, ST=CA, C=US");
+        X500Principal[] issuers = new X500Principal[]{testPrincipal};
+
+        String validKeyType = pemKeyManager.chooseClientAlias(new String[]{"RSA"}, issuers, null);
+        assertNotNull(validKeyType);
+
+        String ignoresCase = pemKeyManager.chooseClientAlias(new String[]{"rsa"}, issuers, null);
+        assertNotNull(ignoresCase);
+
+        String invalidKeyType = pemKeyManager.chooseClientAlias(new String[]{"EC"}, issuers, null);
+        assertNull(invalidKeyType);
+
+        String containsValidKeyType = pemKeyManager.chooseClientAlias(new String[]{"EC", "RSA"}, issuers, null);
+        assertNotNull(containsValidKeyType);
+
+        String ignoresBlank = pemKeyManager.chooseClientAlias(new String[]{}, issuers, null);
+        assertNotNull(ignoresBlank);
+    }
+}
-- 



view thread (3+ messages)  latest in thread

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: [email protected]
  Cc: [email protected], [email protected]
  Subject: Re: Patch for supporting PEM based certs and keys
  In-Reply-To: <CAO7WNRRhJst=iT2C6kBjg+bYsQTBvN5ksXrNS5m+vkYGa+wGGw@mail.gmail.com>

* 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