postgresql-jdbc.git / summary / log / commit / refs

commit    d45f279ada83902ae32ef4d6dcea9bdf2caaf047
Author:   Harinath Kanchu <kanchuharinath@gmail.com>
Commit:   GitHub <noreply@github.com>
Date:     Tue Mar 10 12:51:22 2026 +0000

    ssl: align key file permission check with libpq (#3952)
    
    Root-owned key files with GROUP_READ (0640) are now accepted,
    matching the behavior of libpq and psql. Non-root-owned files
    still require 0600 or stricter.
    
    Splits validatePosixPermissions into isFileOwnedByRoot and
    hasInsecurePosixPermissions for clarity. Adds four tests covering
    the permission matrix, skipped on non-POSIX systems.
    
    Co-authored-by: harinath <harinath@harinaths-MacBook-Pro.local>


.../org/postgresql/ssl/BaseX509KeyManager.java | 67 +++++++++++++++----- .../org/postgresql/test/ssl/PEMKeyManagerTest.java | 73 ++++++++++++++++++++++ 2 files changed, 125 insertions(+), 15 deletions(-) diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/BaseX509KeyManager.java b/pgjdbc/src/main/java/org/postgresql/ssl/BaseX509KeyManager.java index 3f7de878..10e855af 100644 --- a/pgjdbc/src/main/java/org/postgresql/ssl/BaseX509KeyManager.java +++ b/pgjdbc/src/main/java/org/postgresql/ssl/BaseX509KeyManager.java @@ -31,6 +31,8 @@ import javax.security.auth.x500.X500Principal; public abstract class BaseX509KeyManager implements X509KeyManager { + private static final int ROOT_UID = 0; + protected @Nullable PSQLException error; /** @@ -107,9 +109,11 @@ public abstract class BaseX509KeyManager implements X509KeyManager { } /** - * Validates that the private key file has secure permissions (owner-only readable). - * On POSIX systems, ensures no group or other permissions are set. - * On Windows systems, checks ACLs to ensure only the owner and trusted system accounts have access. + * Validates that the private key file has secure permissions, matching libpq behavior. + * On POSIX systems, root-owned files are allowed group-read access (up to 0640), since + * it's common for root to own certs and grant read access via group membership. Files + * owned by anyone else must be 0600 or stricter. + * On Windows, ACLs are checked to ensure only the owner and trusted system accounts have access. * * @param keyPath the path to the private key file * @throws PSQLException if the file has insecure permissions @@ -131,7 +135,9 @@ public abstract class BaseX509KeyManager implements X509KeyManager { } /** - * Validates POSIX file permissions of key. + * Validates POSIX file permissions of key, matching libpq behavior. + * Root-owned files (uid 0) allow GROUP_READ (up to 0640). + * Non-root-owned files require 0600 or less (no group or other permissions). * * @param keyPath the path to the private key file * @return true if validation succeeded (permissions are secure), false if POSIX is not supported @@ -140,19 +146,13 @@ public abstract class BaseX509KeyManager implements X509KeyManager { private static boolean validatePosixPermissions(Path keyPath) throws PSQLException { try { Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(keyPath); + boolean isOwnedByRoot = isFileOwnedByRoot(keyPath); - boolean hasGroupPerms = permissions.contains(PosixFilePermission.GROUP_READ) - || permissions.contains(PosixFilePermission.GROUP_WRITE) - || permissions.contains(PosixFilePermission.GROUP_EXECUTE); - - boolean hasOtherPerms = permissions.contains(PosixFilePermission.OTHERS_READ) - || permissions.contains(PosixFilePermission.OTHERS_WRITE) - || permissions.contains(PosixFilePermission.OTHERS_EXECUTE); - - if (hasGroupPerms || hasOtherPerms) { + if (hasInsecurePosixPermissions(permissions, isOwnedByRoot)) { throw new PSQLException( - GT.tr("Private key file \"{0}\" has insecure permissions. " - + "Permissions for group and other must be revoked. " + GT.tr("private key file \"{0}\" has group or world access; " + + "file must have permissions u=rw (0600) or less if owned by the current user, " + + "or permissions u=rw,g=r (0640) or less if owned by root. " + "Current permissions: {1}", keyPath.toString(), PosixFilePermissions.toString(permissions)), @@ -168,6 +168,43 @@ public abstract class BaseX509KeyManager implements X509KeyManager { } } + /** + * Checks whether the file is owned by root (uid 0). + * Falls back to false if the unix:uid attribute is not available. + */ + private static boolean isFileOwnedByRoot(Path keyPath) throws IOException { + try { + Object uid = Files.getAttribute(keyPath, "unix:uid"); + return Integer.valueOf(ROOT_UID).equals(uid); + } catch (UnsupportedOperationException | IllegalArgumentException e) { + // unix:uid not available + return false; + } + } + + /** + * Determines whether the given POSIX permissions are insecure for a private key file. + * Matches libpq behavior: root-owned files allow GROUP_READ (0640), + * while non-root-owned files reject all group and other permissions (0600). + */ + private static boolean hasInsecurePosixPermissions( + Set<PosixFilePermission> permissions, boolean isOwnedByRoot) { + boolean hasOtherPerms = permissions.contains(PosixFilePermission.OTHERS_READ) + || permissions.contains(PosixFilePermission.OTHERS_WRITE) + || permissions.contains(PosixFilePermission.OTHERS_EXECUTE); + + if (isOwnedByRoot) { + boolean hasGroupWriteOrExecute = permissions.contains(PosixFilePermission.GROUP_WRITE) + || permissions.contains(PosixFilePermission.GROUP_EXECUTE); + return hasGroupWriteOrExecute || hasOtherPerms; + } + + boolean hasGroupPerms = permissions.contains(PosixFilePermission.GROUP_READ) + || permissions.contains(PosixFilePermission.GROUP_WRITE) + || permissions.contains(PosixFilePermission.GROUP_EXECUTE); + return hasGroupPerms || hasOtherPerms; + } + /** * Validates Windows ACL permissions of the key file. * diff --git a/pgjdbc/src/test/java/org/postgresql/test/ssl/PEMKeyManagerTest.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/PEMKeyManagerTest.java index 8a6e95fa..0f428d59 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/ssl/PEMKeyManagerTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/PEMKeyManagerTest.java @@ -5,18 +5,24 @@ package org.postgresql.test.ssl; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.postgresql.PGProperty; import org.postgresql.core.ServerVersion; +import org.postgresql.ssl.BaseX509KeyManager; import org.postgresql.ssl.PEMKeyManager; import org.postgresql.test.TestUtil; +import org.postgresql.util.PSQLException; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.io.File; import java.nio.file.Files; @@ -192,4 +198,71 @@ public class PEMKeyManagerTest { assertTrue(sslUsed, "SSL should be in use"); } } + + @Test + void testPermissionsOwnerOnly(@TempDir Path tempDir) throws Exception { + Assumptions.assumeTrue( + tempDir.getFileSystem().supportedFileAttributeViews().contains("posix"), + "POSIX file permissions not supported"); + + Path keyFile = tempDir.resolve("test.key"); + Files.createFile(keyFile); + Set<PosixFilePermission> perms = EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(keyFile, perms); + + assertDoesNotThrow(() -> BaseX509KeyManager.validateKeyFilePermissions(keyFile), + "File with 0600 permissions should pass validation"); + } + + @Test + void testPermissionsGroupReadNonRootFails(@TempDir Path tempDir) throws Exception { + Assumptions.assumeTrue( + tempDir.getFileSystem().supportedFileAttributeViews().contains("posix"), + "POSIX file permissions not supported"); + + Path keyFile = tempDir.resolve("test.key"); + Files.createFile(keyFile); + Set<PosixFilePermission> perms = EnumSet.of( + PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, + PosixFilePermission.GROUP_READ); + Files.setPosixFilePermissions(keyFile, perms); + + // Non-root owned file with GROUP_READ should fail + assertThrows(PSQLException.class, + () -> BaseX509KeyManager.validateKeyFilePermissions(keyFile), + "File with GROUP_READ and non-root owner should fail validation"); + } + + @Test + void testPermissionsOthersReadFails(@TempDir Path tempDir) throws Exception { + Assumptions.assumeTrue( + tempDir.getFileSystem().supportedFileAttributeViews().contains("posix"), + "POSIX file permissions not supported"); + + Path keyFile = tempDir.resolve("test.key"); + Files.createFile(keyFile); + Set<PosixFilePermission> perms = EnumSet.of( + PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OTHERS_READ); + Files.setPosixFilePermissions(keyFile, perms); + + assertThrows(PSQLException.class, + () -> BaseX509KeyManager.validateKeyFilePermissions(keyFile), + "File with OTHERS_READ should fail validation"); + } + + @Test + void testPermissionsOwnerReadOnly(@TempDir Path tempDir) throws Exception { + Assumptions.assumeTrue( + tempDir.getFileSystem().supportedFileAttributeViews().contains("posix"), + "POSIX file permissions not supported"); + + Path keyFile = tempDir.resolve("test.key"); + Files.createFile(keyFile); + Set<PosixFilePermission> perms = EnumSet.of(PosixFilePermission.OWNER_READ); + Files.setPosixFilePermissions(keyFile, perms); + + assertDoesNotThrow(() -> BaseX509KeyManager.validateKeyFilePermissions(keyFile), + "File with 0400 permissions should pass validation"); + } } [parent: e8e1ab306a91]