From f93487455cc1b5d7062ea6577d99dea25bfd81b3 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 14 Oct 2026 07:27:37 +0000
Subject: [PATCH] [#1188] Serialize and replace atomically the writes of the ads-truststore, and report a certificate it cannot take or drop (#1189)

---
 opendj-server-legacy/src/main/java/org/opends/server/util/Platform.java |  188 +++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 172 insertions(+), 16 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/util/Platform.java b/opendj-server-legacy/src/main/java/org/opends/server/util/Platform.java
index d4267e5..97951c6 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/util/Platform.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/util/Platform.java
@@ -21,8 +21,17 @@
 
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.math.BigInteger;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFileAttributes;
 import java.security.Key;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
@@ -70,6 +79,45 @@
 
   private static final PlatformIMPL IMPL;
 
+  /** Puts a key store written next to its file in the place of that file. */
+  interface KeyStoreReplacer
+  {
+    /**
+     * Puts the new file in the place of the old one at once.
+     *
+     * @param temp
+     *          the new file, next to the old one
+     * @param target
+     *          the old file
+     * @throws IOException
+     *           if the old file cannot be replaced that way
+     */
+    void replace(Path temp, Path target) throws IOException;
+  }
+
+  /** Replaces a key store file; a test makes it fail to reach the write in place. */
+  static volatile KeyStoreReplacer keyStoreReplacer = (temp, target) ->
+      Files.move(temp, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
+
+  /** Gives a key store file written next to the old one the group of that one. */
+  interface KeyStoreGroupSetter
+  {
+    /**
+     * Gives the new file the group of the old one.
+     *
+     * @param view
+     *          the attributes of the new file
+     * @param group
+     *          the group of the old file
+     * @throws IOException
+     *           if the new file cannot be given that group
+     */
+    void setGroup(PosixFileAttributeView view, GroupPrincipal group) throws IOException;
+  }
+
+  /** Gives the new key store file its group; a test makes it fail to reach the write in place. */
+  static volatile KeyStoreGroupSetter keyStoreGroupSetter = PosixFileAttributeView::setGroup;
+
   /** The minimum java supported version. */
   public static final String JAVA_MINIMUM_VERSION_NUMBER = "11";
 
@@ -131,6 +179,125 @@
     {
     }
 
+    /**
+     * Writes the key store over its file. On a POSIX file system it is written to a file of its
+     * own next to it first, which then takes the place of the old one at once: a reader of the
+     * key store, which the server reads from its file for each SSL context, never finds it half
+     * written, and a process stopped part way through leaves the old file whole (#1188). The new
+     * file keeps the mode, the owner and the group of the old one; it does not keep its ACLs.
+     * <p>
+     * The key store is written in place, as it was before, where it cannot be replaced that way
+     * without changing what the file is: a new key store, one this process may not write to
+     * (refused, as before), one with other hard links, one whose owner or group the new file
+     * cannot take, one whose links, owner or group cannot be told, one in a directory this
+     * process cannot write to, and one which cannot be replaced, such as a file mounted on its own
+     * into a container. On a file system without POSIX attributes (Windows), where neither the
+     * owner, the ACLs nor the hard links of the old file would reach a new one, it is always
+     * written in place.
+     */
+    private static void store(KeyStore ks, String ksPath, char[] pwd) throws Exception
+    {
+      Path target = Paths.get(ksPath);
+      if (!Files.exists(target) || !target.getFileSystem().supportedFileAttributeViews().contains("posix"))
+      {
+        storeInPlace(ks, target, pwd);
+        return;
+      }
+      // A link goes on pointing where it did: the file it points to is the one replaced.
+      target = target.toRealPath();
+      if (!Files.isWritable(target) || hasOtherLinks(target))
+      {
+        storeInPlace(ks, target, pwd);
+        return;
+      }
+      final Path temp;
+      try
+      {
+        temp = Files.createTempFile(target.getParent(), "." + target.getFileName() + ".", ".tmp");
+      }
+      catch (IOException e)
+      {
+        storeInPlace(ks, target, pwd);
+        return;
+      }
+      try
+      {
+        try (OutputStream out = Files.newOutputStream(temp))
+        {
+          ks.store(out, pwd);
+        }
+        if (!takeOwnerAndMode(temp, target))
+        {
+          storeInPlace(ks, target, pwd);
+          return;
+        }
+        try
+        {
+          keyStoreReplacer.replace(temp, target);
+        }
+        catch (IOException e)
+        {
+          storeInPlace(ks, target, pwd);
+        }
+      }
+      finally
+      {
+        Files.deleteIfExists(temp);
+      }
+    }
+
+    /**
+     * Whether the file has other names, which a new file in its place would not have; a file
+     * whose links cannot be told is taken to have some.
+     */
+    private static boolean hasOtherLinks(Path target)
+    {
+      try
+      {
+        return ((Number) Files.getAttribute(target, "unix:nlink")).intValue() > 1;
+      }
+      catch (IOException | UnsupportedOperationException e)
+      {
+        return true;
+      }
+    }
+
+    /**
+     * Gives the new file the group and the mode of the old one, and tells whether it has its
+     * owner and its group: a process may give a file only the groups it is a member of, and only
+     * its own user as owner. The mode comes last, so that the new file is never open to a group
+     * the old one was not, and only when the file can take the place of the old one; a file whose
+     * attributes cannot be read or set cannot.
+     */
+    private static boolean takeOwnerAndMode(Path temp, Path target)
+    {
+      try
+      {
+        final PosixFileAttributes old = Files.readAttributes(target, PosixFileAttributes.class);
+        final PosixFileAttributeView view = Files.getFileAttributeView(temp, PosixFileAttributeView.class);
+        keyStoreGroupSetter.setGroup(view, old.group());
+        final PosixFileAttributes now = view.readAttributes();
+        if (!now.owner().equals(old.owner()) || !now.group().equals(old.group()))
+        {
+          return false;
+        }
+        view.setPermissions(old.permissions());
+        return true;
+      }
+      catch (IOException | UnsupportedOperationException e)
+      {
+        return false;
+      }
+    }
+
+    private static void storeInPlace(KeyStore ks, Path target, char[] pwd) throws Exception
+    {
+      try (OutputStream out = new FileOutputStream(target.toFile()))
+      {
+        ks.store(out, pwd);
+      }
+    }
+
     private final void deleteAlias(KeyStore ks, String ksPath, String alias,
                                    char[] pwd) throws KeyStoreException
     {
@@ -142,10 +309,7 @@
           throw new KeyStoreException(msg.toString());
         }
         ks.deleteEntry(alias);
-        try (final FileOutputStream fs = new FileOutputStream(ksPath))
-        {
-          ks.store(fs, pwd);
-        }
+        store(ks, ksPath, pwd);
       }
       catch (Exception e)
       {
@@ -182,9 +346,7 @@
           LocalizableMessage msg = ERR_CERTMGR_ALIAS_INVALID.get(alias);
           throw new KeyStoreException(msg.toString());
         }
-        try (FileOutputStream fileOutStream = new FileOutputStream(ksPath)) {
-          ks.store(fileOutStream, pwd);
-        }
+        store(ks, ksPath, pwd);
       }
       catch (Exception e)
       {
@@ -212,9 +374,7 @@
         // the server are initialised with the store password only, so a key which kept the
         // password of the key store it comes from could not be read back.
         ks.setKeyEntry(alias, privateKey, pwd, chain);
-        try (FileOutputStream fileOutStream = new FileOutputStream(ksPath)) {
-          ks.store(fileOutStream, pwd);
-        }
+        store(ks, ksPath, pwd);
       }
       catch (Exception e)
       {
@@ -238,9 +398,7 @@
           throw new KeyStoreException(msg.toString());
         }
         ks.setCertificateEntry(alias, certificate);
-        try (FileOutputStream fileOutStream = new FileOutputStream(ksPath)) {
-          ks.store(fileOutStream, pwd);
-        }
+        store(ks, ksPath, pwd);
       }
       catch (Exception e)
       {
@@ -277,9 +435,7 @@
                 generateSelfCertificate(keyPair, keyType, subject, validity)
         };
         ks.setKeyEntry(alias, privateKey, pwd, certificateChain);
-        try (FileOutputStream fileOutStream = new FileOutputStream(ksPath)) {
-          ks.store(fileOutStream, pwd);
-        }
+        store(ks, ksPath, pwd);
         return ks;
       }
       catch (Exception e)

--
Gitblit v1.10.0