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