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/test/java/org/opends/server/util/CertificateManagerTestCase.java |  398 ++++++++++++++++++
 opendj-server-legacy/src/messages/org/opends/messages/core_es.properties                  |    5 
 opendj-server-legacy/src/messages/org/opends/messages/core.properties                     |    4 
 opendj-server-legacy/src/messages/org/opends/messages/core_zh_TW.properties               |    5 
 opendj-server-legacy/src/main/java/org/opends/server/util/Platform.java                   |  188 +++++++
 opendj-server-legacy/src/messages/org/opends/messages/core_ja.properties                  |    5 
 opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java    |  426 +++++++++++++++++++
 opendj-server-legacy/src/messages/org/opends/messages/core_fr.properties                  |    5 
 opendj-server-legacy/src/messages/org/opends/messages/core_zh_CN.properties               |    5 
 opendj-server-legacy/src/messages/org/opends/messages/core_ko.properties                  |    5 
 opendj-server-legacy/src/main/java/org/opends/server/backends/TrustStoreBackend.java      |  156 ++++--
 opendj-server-legacy/src/messages/org/opends/messages/core_de.properties                  |    5 
 opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerSync.java        |   22 
 opendj-server-legacy/src/test/java/org/opends/server/util/KeyStoreReplaceFailure.java     |   78 +++
 14 files changed, 1,220 insertions(+), 87 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/TrustStoreBackend.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/TrustStoreBackend.java
index a1d58fd..07b410d 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/TrustStoreBackend.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/TrustStoreBackend.java
@@ -118,6 +118,15 @@
 
   /** The certificate manager for the trust store. */
   private CertificateManager certificateManager;
+  /**
+   * Held over every access to the trust store file and to the certificate manager. A change
+   * reads the file, changes that copy and writes the whole file back over the old one, and the
+   * certificate manager keeps the copy it read last. Replication replays the instance keys of
+   * cn=admin data on parallel threads, each of which changes the file here, and an SSL context
+   * is built from the file for every replication connection: unserialized, one of two changes
+   * is lost, and a reader finds the file half written (#1188).
+   */
+  private final Object trustStoreLock = new Object();
   /** The server context. */
   private ServerContext serverContext;
 
@@ -183,10 +192,13 @@
 
     trustStorePIN = getTrustStorePIN(configuration, true);
     final String keyStorePath = getFileForPath(trustStoreFile).getPath();
-    certificateManager = new CertificateManager(keyStorePath, trustStoreType, trustStorePIN);
+    synchronized (trustStoreLock)
+    {
+      certificateManager = new CertificateManager(keyStorePath, trustStoreType, trustStorePIN);
 
-    // Generate a self-signed certificate, if there is none.
-    generateInstanceCertificateIfAbsent();
+      // Generate a self-signed certificate, if there is none.
+      generateInstanceCertificateIfAbsent();
+    }
 
     // Construct the trust store base entry.
     LinkedHashMap<ObjectClass,String> objectClasses = new LinkedHashMap<>(2);
@@ -327,7 +339,11 @@
 
     try
     {
-      String[] aliases = certificateManager.getCertificateAliases();
+      String[] aliases;
+      synchronized (trustStoreLock)
+      {
+        aliases = certificateManager.getCertificateAliases();
+      }
       if (aliases != null)
       {
         numEntries += aliases.length;
@@ -409,7 +425,11 @@
     ByteString certValue;
     try
     {
-      Certificate cert = certificateManager.getCertificate(certAlias);
+      Certificate cert;
+      synchronized (trustStoreLock)
+      {
+        cert = certificateManager.getCertificate(certAlias);
+      }
       if (cert == null)
       {
         LocalizableMessage message = ERR_TRUSTSTORE_CERTIFICATE_NOT_FOUND.get(entryDN, certAlias);
@@ -468,7 +488,10 @@
 
     if (parentDN.equals(getBaseDN()))
     {
-      addCertificate(entry);
+      synchronized (trustStoreLock)
+      {
+        addCertificate(entry);
+      }
     }
     else
     {
@@ -494,7 +517,10 @@
       throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message);
     }
 
-    deleteCertificate(entryDN);
+    synchronized (trustStoreLock)
+    {
+      deleteCertificate(entryDN);
+    }
   }
 
   @Override
@@ -538,7 +564,10 @@
       String[] aliases = null;
       try
       {
-        aliases = certificateManager.getCertificateAliases();
+        synchronized (trustStoreLock)
+        {
+          aliases = certificateManager.getCertificateAliases();
+        }
       }
       catch (KeyStoreException e)
       {
@@ -772,12 +801,15 @@
 
     if (ccr.getResultCode() == ResultCode.SUCCESS)
     {
-      trustStoreFile = newTrustStoreFile;
-      trustStoreType = newTrustStoreType;
-      trustStorePIN  = newPIN;
-      configuration  = cfg;
-      final String keyStorePath = getFileForPath(trustStoreFile).getPath();
-      certificateManager = new CertificateManager(keyStorePath, trustStoreType, trustStorePIN);
+      synchronized (trustStoreLock)
+      {
+        trustStoreFile = newTrustStoreFile;
+        trustStoreType = newTrustStoreType;
+        trustStorePIN  = newPIN;
+        configuration  = cfg;
+        final String keyStorePath = getFileForPath(trustStoreFile).getPath();
+        certificateManager = new CertificateManager(keyStorePath, trustStoreType, trustStorePIN);
+      }
     }
 
     return ccr;
@@ -802,24 +834,28 @@
   public KeyManager[] getKeyManagers()
          throws DirectoryException
   {
-    final KeyStore keyStore = loadKeyStore();
-
-    try
+    // the keys are opened with the PIN of the file they were read from, not that of a new one
+    synchronized (trustStoreLock)
     {
-      String keyManagerAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
-      KeyManagerFactory keyManagerFactory =
-           KeyManagerFactory.getInstance(keyManagerAlgorithm);
-      keyManagerFactory.init(keyStore, trustStorePIN);
-      return keyManagerFactory.getKeyManagers();
-    }
-    catch (Exception e)
-    {
-      logger.traceException(e);
+      final KeyStore keyStore = loadKeyStore();
 
-      LocalizableMessage message = ERR_TRUSTSTORE_CANNOT_CREATE_FACTORY.get(
-          trustStoreFile, getExceptionMessage(e));
-      throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(),
-                                   message, e);
+      try
+      {
+        String keyManagerAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
+        KeyManagerFactory keyManagerFactory =
+             KeyManagerFactory.getInstance(keyManagerAlgorithm);
+        keyManagerFactory.init(keyStore, trustStorePIN);
+        return keyManagerFactory.getKeyManagers();
+      }
+      catch (Exception e)
+      {
+        logger.traceException(e);
+
+        LocalizableMessage message = ERR_TRUSTSTORE_CANNOT_CREATE_FACTORY.get(
+            trustStoreFile, getExceptionMessage(e));
+        throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(),
+                                     message, e);
+      }
     }
   }
 
@@ -864,16 +900,19 @@
 
   private KeyStore loadKeyStore() throws DirectoryException
   {
-    try (FileInputStream inputStream = new FileInputStream(getFileForPath(trustStoreFile)))
+    synchronized (trustStoreLock)
     {
-      final KeyStore keyStore = KeyStore.getInstance(trustStoreType);
-      keyStore.load(inputStream, trustStorePIN);
-      return keyStore;
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message = ERR_TRUSTSTORE_CANNOT_LOAD.get(trustStoreFile, getExceptionMessage(e));
-      throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), message, e);
+      try (FileInputStream inputStream = new FileInputStream(getFileForPath(trustStoreFile)))
+      {
+        final KeyStore keyStore = KeyStore.getInstance(trustStoreType);
+        keyStore.load(inputStream, trustStorePIN);
+        return keyStore;
+      }
+      catch (Exception e)
+      {
+        LocalizableMessage message = ERR_TRUSTSTORE_CANNOT_LOAD.get(trustStoreFile, getExceptionMessage(e));
+        throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), message, e);
+      }
     }
   }
 
@@ -925,20 +964,24 @@
   public Key getKey(String alias)
          throws DirectoryException
   {
-    KeyStore trustStore = loadKeyStore();
-
-    try
+    // the key is opened with the PIN of the file it was read from, not that of a new one
+    synchronized (trustStoreLock)
     {
-      return trustStore.getKey(alias, trustStorePIN);
-    }
-    catch (Exception e)
-    {
-      logger.traceException(e);
+      KeyStore trustStore = loadKeyStore();
 
-      LocalizableMessage message = ERR_TRUSTSTORE_ERROR_READING_KEY.get(
-           alias, trustStoreFile, getExceptionMessage(e));
-      throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(),
-                                   message, e);
+      try
+      {
+        return trustStore.getKey(alias, trustStorePIN);
+      }
+      catch (Exception e)
+      {
+        logger.traceException(e);
+
+        LocalizableMessage message = ERR_TRUSTSTORE_ERROR_READING_KEY.get(
+             alias, trustStoreFile, getExceptionMessage(e));
+        throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(),
+                                     message, e);
+      }
     }
   }
 
@@ -1047,6 +1090,12 @@
         }
       }
     }
+    catch (DirectoryException e)
+    {
+      // a refusal made above keeps its result code: an alias in use is an entry which exists,
+      // not a failure of the server
+      throw e;
+    }
     catch (Exception e)
     {
       LocalizableMessage message = ERR_TRUSTSTORE_CANNOT_ADD_CERT.get(
@@ -1079,6 +1128,11 @@
 
       certificateManager.removeCertificate(certAlias);
     }
+    catch (DirectoryException e)
+    {
+      // an alias not in use is an entry which does not exist, not a failure of the server
+      throw e;
+    }
     catch (Exception e)
     {
       LocalizableMessage message = ERR_TRUSTSTORE_CANNOT_DELETE_CERT.get(
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerSync.java b/opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerSync.java
index 9cff8a7..fe8d421 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerSync.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerSync.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2008-2010 Sun Microsystems, Inc.
  * Portions Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.crypto;
 
@@ -359,9 +360,15 @@
   private static void deleteEntry(DN dstDN)
   {
     DeleteOperation delOperation = getRootConnection().processDelete(dstDN);
-    if (delOperation.getResultCode() != ResultCode.SUCCESS)
+    if (delOperation.getResultCode() == ResultCode.NO_SUCH_OBJECT)
     {
-      logger.debug(INFO_TRUSTSTORESYNC_DELETE_FAILED, dstDN, delOperation.getErrorMessage());
+      // The certificate has left the trust store already, as it does when its key is marked compromised.
+      logger.debug(ERR_TRUSTSTORESYNC_DELETE_FAILED, dstDN, delOperation.getErrorMessage());
+    }
+    else if (delOperation.getResultCode() != ResultCode.SUCCESS)
+    {
+      // While the certificate is there, this server goes on trusting whoever holds the key.
+      logger.error(ERR_TRUSTSTORESYNC_DELETE_FAILED, dstDN, delOperation.getErrorMessage());
     }
   }
 
@@ -383,9 +390,16 @@
 
     Entry addEntry = new Entry(dstDN, ocMap, userAttrs, null);
     AddOperation addOperation = getRootConnection().processAdd(addEntry);
-    if (addOperation.getResultCode() != ResultCode.SUCCESS)
+    if (addOperation.getResultCode() == ResultCode.ENTRY_ALREADY_EXISTS)
     {
-      logger.debug(INFO_TRUSTSTORESYNC_ADD_FAILED, dstDN, addOperation.getErrorMessage());
+      // dsreplication seeds the trust store of the servers it enables: the certificate is there already.
+      logger.debug(ERR_TRUSTSTORESYNC_ADD_FAILED, dstDN, addOperation.getErrorMessage());
+    }
+    else if (addOperation.getResultCode() != ResultCode.SUCCESS)
+    {
+      // Without the certificate this server does not trust the server the key belongs to, and
+      // every replication connection between the two fails its handshake (#1188).
+      logger.error(ERR_TRUSTSTORESYNC_ADD_FAILED, dstDN, addOperation.getErrorMessage());
     }
   }
 
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)
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/core.properties b/opendj-server-legacy/src/messages/org/opends/messages/core.properties
index 72759e0..545450e 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/core.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/core.properties
@@ -1175,9 +1175,9 @@
  %s to synchronize the trust store: %s
 ERR_TRUSTSTORESYNC_EXCEPTION_654=An error occurred in the trust store \
  synchronization thread: %s
-INFO_TRUSTSTORESYNC_ADD_FAILED_655=Error while trying to add entry %s \
+ERR_TRUSTSTORESYNC_ADD_FAILED_655=Error while trying to add entry %s \
  to the trust store: %s
-INFO_TRUSTSTORESYNC_DELETE_FAILED_656=Error while trying to delete entry %s \
+ERR_TRUSTSTORESYNC_DELETE_FAILED_656=Error while trying to delete entry %s \
  from the trust store: %s
 ERR_PWPOLICY_SCHEME_DOESNT_SUPPORT_AUTH_657=The password storage \
  scheme defined in configuration entry %s does not support the auth password \
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/core_de.properties b/opendj-server-legacy/src/messages/org/opends/messages/core_de.properties
index d579f6e..31594eb 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/core_de.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/core_de.properties
@@ -12,6 +12,7 @@
 #
 # Copyright 2006-2010 Sun Microsystems, Inc.
 # Portions Copyright 2011-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
 
 
 
@@ -497,8 +498,8 @@
 INFO_DSCORE_DESCRIPTION_LASTKNOWNGOODCFG_652=Es wird versucht, mit der Konfiguration zu starten, die beim letzten erfolgreichen Start (sofern zutreffend) vorhanden war, anstatt die aktuelle aktive Konfiguration zu verwenden
 INFO_TRUSTSTORESYNC_ADMIN_SUFFIX_SEARCH_FAILED_653=Fehler beim Durchsuchen der Basis %s, um den Trust-Speicher zu synchronisieren: %s
 ERR_TRUSTSTORESYNC_EXCEPTION_654=Fehler im Vertrauensspeicher-Synchronisierungs-Thread: %s
-INFO_TRUSTSTORESYNC_ADD_FAILED_655=Fehler beim Versuch, Eintrag %s zum Trust-Speicher hinzuzuf\u00fcgen: %s
-INFO_TRUSTSTORESYNC_DELETE_FAILED_656=Fehler beim Versuch, Eintrag %s aus dem Trust-Speicher zu l\u00f6schen: %s
+ERR_TRUSTSTORESYNC_ADD_FAILED_655=Fehler beim Versuch, Eintrag %s zum Trust-Speicher hinzuzuf\u00fcgen: %s
+ERR_TRUSTSTORESYNC_DELETE_FAILED_656=Fehler beim Versuch, Eintrag %s aus dem Trust-Speicher zu l\u00f6schen: %s
 ERR_PWPOLICY_SCHEME_DOESNT_SUPPORT_AUTH_657=Das in Konfigurationseintrag %s definierte Passwortspeicherschema unterst\u00fctzt die Authentifizierungspasswortsyntax nicht, die von Passwortattribut %s verwendet wird
 ERR_PWPOLICY_DEPRECATED_SCHEME_NOT_AUTH_659=Der Passwortrichtlinien-Konfigurationseintrag %s referenziert den veralteten Passwortspeicherschema-DN %s, der die Authentifizierungspasswortsyntax nicht unterst\u00fctzt
 WARN_GROUP_FILTER_NOT_INDEXED_660=Der Suchfilter "%s", der von der Gruppenimplementierung %s verwendet wird, ist nicht in Backend %s indiziert.  Die Ausf\u00fchrung der Backend-Initialisierung f\u00fcr diese Gruppenimplementierung kann l\u00e4ngere Zeit dauern
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/core_es.properties b/opendj-server-legacy/src/messages/org/opends/messages/core_es.properties
index 9b20288..87a899c 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/core_es.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/core_es.properties
@@ -12,6 +12,7 @@
 #
 # Copyright 2006-2010 Sun Microsystems, Inc.
 # Portions Copyright 2011-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
 
 
 
@@ -497,8 +498,8 @@
 INFO_DSCORE_DESCRIPTION_LASTKNOWNGOODCFG_652=Intento de efectuar el inicio con la configuraci\u00f3n aplicada durante el \u00faltimo inicio satisfactorio (si est\u00e1 disponible) en lugar de utilizar la configuraci\u00f3n activa actual
 INFO_TRUSTSTORESYNC_ADMIN_SUFFIX_SEARCH_FAILED_653=Error al intentar buscar la base %s para sincronizar el almac\u00e9n de confianza: %s
 ERR_TRUSTSTORESYNC_EXCEPTION_654=Se ha producido un error en el subproceso de sincronizaci\u00f3n del almac\u00e9n de confianza: %s
-INFO_TRUSTSTORESYNC_ADD_FAILED_655=Error al intentar agregar la entrada %s al almac\u00e9n de confianza: %s
-INFO_TRUSTSTORESYNC_DELETE_FAILED_656=Error al intentar eliminar la entrada %s del almac\u00e9n de confianza: %s
+ERR_TRUSTSTORESYNC_ADD_FAILED_655=Error al intentar agregar la entrada %s al almac\u00e9n de confianza: %s
+ERR_TRUSTSTORESYNC_DELETE_FAILED_656=Error al intentar eliminar la entrada %s del almac\u00e9n de confianza: %s
 ERR_PWPOLICY_SCHEME_DOESNT_SUPPORT_AUTH_657=El esquema de almacenamiento de contrase\u00f1as definido en la entrada de configuraci\u00f3n %s no admite la sintaxis auth password (contrase\u00f1a de autenticaci\u00f3n) empleada por el atributo de contrase\u00f1a %s
 ERR_PWPOLICY_DEPRECATED_SCHEME_NOT_AUTH_659=La entrada de configuraci\u00f3n de la directiva de contrase\u00f1as %s hace referencia al ND del esquema de almacenamiento de contrase\u00f1as obsoleto %s que no admite la sintaxis auth password (contrase\u00f1a de autenticaci\u00f3n)
 WARN_GROUP_FILTER_NOT_INDEXED_660=El filtro de b\u00fasqueda "%s" empleado por la implementaci\u00f3n de grupo %s no est\u00e1 indexado en el backend %s.  Es posible que la inicializaci\u00f3n del backend para esta implementaci\u00f3n de grupo tarde mucho tiempo en completarse
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/core_fr.properties b/opendj-server-legacy/src/messages/org/opends/messages/core_fr.properties
index b835bd8..81aff4d 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/core_fr.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/core_fr.properties
@@ -12,6 +12,7 @@
 #
 # Copyright 2006-2010 Sun Microsystems, Inc.
 # Portions Copyright 2011-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
 
 
 
@@ -519,8 +520,8 @@
 INFO_DSCORE_DESCRIPTION_LASTKNOWNGOODCFG_652=Essayer de d\u00e9marrage en utilisant la configuration appliqu\u00e9e lors du dernier d\u00e9marrage r\u00e9ussi (si disponible), au lieu de la configuration active actuelle
 INFO_TRUSTSTORESYNC_ADMIN_SUFFIX_SEARCH_FAILED_653=Erreur lors de la recherche de l'\u00e9l\u00e9ment de base %s pour synchroniser le magasin d'approbations\u00a0: %s
 ERR_TRUSTSTORESYNC_EXCEPTION_654=Une erreur s\u2019est produite dans le thread de synchronisation de magasin d\u2019approbations\u00a0: %s
-INFO_TRUSTSTORESYNC_ADD_FAILED_655=Une erreur s'est produite lors de l'ajout de l'entr\u00e9e %s dans le magasin d'approbations\u00a0: %s
-INFO_TRUSTSTORESYNC_DELETE_FAILED_656=Une erreur s'est produite lors de la suppression de l'entr\u00e9e %s du magasin d'approbations\u00a0: %s
+ERR_TRUSTSTORESYNC_ADD_FAILED_655=Une erreur s'est produite lors de l'ajout de l'entr\u00e9e %s dans le magasin d'approbations\u00a0: %s
+ERR_TRUSTSTORESYNC_DELETE_FAILED_656=Une erreur s'est produite lors de la suppression de l'entr\u00e9e %s du magasin d'approbations\u00a0: %s
 ERR_PWPOLICY_SCHEME_DOESNT_SUPPORT_AUTH_657=Le plan de stockage des mots de passe d\u00e9fini dans l\u2019entr\u00e9e de configuration %s ne prend pas en charge la syntaxe de mot de passe d\u2019authentification (auth password), utilis\u00e9e par l\u2019attribut de mot de passe %s
 ERR_PWPOLICY_DEPRECATED_SCHEME_NOT_AUTH_659=L\u2019entr\u00e9e de configuration de strat\u00e9gie de mot de passe %s fait r\u00e9f\u00e9rence \u00e0 un DN de plan de stockage des mots de passe d\u00e9sapprouv\u00e9 (%s), qui ne prend pas en charge la syntaxe de mot de passe d\u2019authentification (auth password)
 WARN_GROUP_FILTER_NOT_INDEXED_660=Le filtre de recherche "%s" utilis\u00e9 par l\u2019impl\u00e9mentation de groupe %s n\u2019est pas index\u00e9 dans le backend %s. L\u2019initialisation du backend pour cette impl\u00e9mentation de groupe peut \u00eatre particuli\u00e8rement longue
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/core_ja.properties b/opendj-server-legacy/src/messages/org/opends/messages/core_ja.properties
index 3f4140d..972b444 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/core_ja.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/core_ja.properties
@@ -12,6 +12,7 @@
 #
 # Copyright 2006-2010 Sun Microsystems, Inc.
 # Portions Copyright 2011-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
 
 
 
@@ -487,8 +488,8 @@
 INFO_DSCORE_DESCRIPTION_LASTKNOWNGOODCFG_652=\u73fe\u5728\u30a2\u30af\u30c6\u30a3\u30d6\u306a\u69cb\u6210\u3067\u306f\u306a\u304f\u3001\u524d\u56de\u8d77\u52d5\u306b\u6210\u529f\u3057\u305f\u3068\u304d\u306e\u69cb\u6210\u3092\u4f7f\u7528\u3057\u3066\u8d77\u52d5\u3092\u8a66\u307f\u307e\u3059 (\u4f7f\u7528\u53ef\u80fd\u306a\u5834\u5408)
 INFO_TRUSTSTORESYNC_ADMIN_SUFFIX_SEARCH_FAILED_653=\u30c8\u30e9\u30b9\u30c8\u30b9\u30c8\u30a2\u3092\u540c\u671f\u3059\u308b\u305f\u3081\u306b\u30d9\u30fc\u30b9 %s \u3092\u691c\u7d22\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: %s
 ERR_TRUSTSTORESYNC_EXCEPTION_654=\u30c8\u30e9\u30b9\u30c8\u30b9\u30c8\u30a2\u540c\u671f\u30b9\u30ec\u30c3\u30c9\u3067\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: %s
-INFO_TRUSTSTORESYNC_ADD_FAILED_655=\u30a8\u30f3\u30c8\u30ea %s \u3092\u30c8\u30e9\u30b9\u30c8\u30b9\u30c8\u30a2\u306b\u8ffd\u52a0\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: %s
-INFO_TRUSTSTORESYNC_DELETE_FAILED_656=\u30a8\u30f3\u30c8\u30ea %s \u3092\u30c8\u30e9\u30b9\u30c8\u30b9\u30c8\u30a2\u304b\u3089\u524a\u9664\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: %s
+ERR_TRUSTSTORESYNC_ADD_FAILED_655=\u30a8\u30f3\u30c8\u30ea %s \u3092\u30c8\u30e9\u30b9\u30c8\u30b9\u30c8\u30a2\u306b\u8ffd\u52a0\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: %s
+ERR_TRUSTSTORESYNC_DELETE_FAILED_656=\u30a8\u30f3\u30c8\u30ea %s \u3092\u30c8\u30e9\u30b9\u30c8\u30b9\u30c8\u30a2\u304b\u3089\u524a\u9664\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: %s
 ERR_PWPOLICY_SCHEME_DOESNT_SUPPORT_AUTH_657=\u69cb\u6210\u30a8\u30f3\u30c8\u30ea %s \u306b\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u308b\u30d1\u30b9\u30ef\u30fc\u30c9\u8a18\u61b6\u30b9\u30ad\u30fc\u30de\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u5c5e\u6027 %s \u306b\u3088\u308a\u4f7f\u7528\u3055\u308c\u308b\u8a8d\u8a3c\u30d1\u30b9\u30ef\u30fc\u30c9\u69cb\u6587\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u305b\u3093
 ERR_PWPOLICY_DEPRECATED_SCHEME_NOT_AUTH_659=\u30d1\u30b9\u30ef\u30fc\u30c9\u30dd\u30ea\u30b7\u30fc\u69cb\u6210\u30a8\u30f3\u30c8\u30ea %s \u306f\u3001\u8a8d\u8a3c\u30d1\u30b9\u30ef\u30fc\u30c9\u69cb\u6587\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u306a\u3044\u975e\u63a8\u5968\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u8a18\u61b6\u30b9\u30ad\u30fc\u30de DN %s \u3092\u53c2\u7167\u3057\u3066\u3044\u307e\u3059
 WARN_GROUP_FILTER_NOT_INDEXED_660=\u30b0\u30eb\u30fc\u30d7\u5b9f\u88c5 %2$s \u3067\u4f7f\u7528\u3055\u308c\u308b\u691c\u7d22\u30d5\u30a3\u30eb\u30bf "%1$s" \u306e\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u304c\u3001\u30d0\u30c3\u30af\u30a8\u30f3\u30c9 %3$s \u5185\u306b\u4f5c\u6210\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u3053\u306e\u30b0\u30eb\u30fc\u30d7\u5b9f\u88c5\u306e\u30d0\u30c3\u30af\u30a8\u30f3\u30c9\u306e\u521d\u671f\u5316\u3092\u5b8c\u4e86\u3059\u308b\u306b\u306f\u3001\u975e\u5e38\u306b\u9577\u3044\u6642\u9593\u304c\u304b\u304b\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/core_ko.properties b/opendj-server-legacy/src/messages/org/opends/messages/core_ko.properties
index 478bcf3..2898b63 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/core_ko.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/core_ko.properties
@@ -12,6 +12,7 @@
 #
 # Copyright 2006-2009 Sun Microsystems, Inc.
 # Portions Copyright 2013-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
 
 
 
@@ -487,8 +488,8 @@
 INFO_DSCORE_DESCRIPTION_LASTKNOWNGOODCFG_652=\ud604\uc7ac \ud65c\uc131 \uad6c\uc131\uc744 \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uace0 \ub9c8\uc9c0\ub9c9 \uc131\uacf5\ud55c \uc2dc\uc791 \uc704\uce58\uc5d0 \uc788\ub294 \uad6c\uc131(\uc0ac\uc6a9 \uac00\ub2a5\ud55c \uacbd\uc6b0)\uc744 \uc0ac\uc6a9\ud558\uc5ec \uc2dc\uc791\ud558\ub824\uace0 \ud569\ub2c8\ub2e4.
 INFO_TRUSTSTORESYNC_ADMIN_SUFFIX_SEARCH_FAILED_653=\uae30\ubcf8 %s\uc744(\ub97c) \uac80\uc0c9\ud558\uc5ec \ud2b8\ub7ec\uc2a4\ud2b8 \uc800\uc7a5\uc18c\ub97c \ub3d9\uae30\ud654\ud558\ub294 \ub3d9\uc548 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4: %s
 ERR_TRUSTSTORESYNC_EXCEPTION_654=\ud2b8\ub7ec\uc2a4\ud2b8 \uc800\uc7a5\uc18c \ub3d9\uae30\ud654 \uc2a4\ub808\ub4dc\uc5d0\uc11c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4: %s
-INFO_TRUSTSTORESYNC_ADD_FAILED_655=\ud56d\ubaa9 %s\uc744(\ub97c) \ud2b8\ub7ec\uc2a4\ud2b8 \uc800\uc7a5\uc18c\uc5d0 \ucd94\uac00\ud558\ub294 \ub3d9\uc548 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4: %s
-INFO_TRUSTSTORESYNC_DELETE_FAILED_656=\ud2b8\ub7ec\uc2a4\ud2b8 \uc800\uc7a5\uc18c\uc5d0\uc11c \ud56d\ubaa9 %s\uc744(\ub97c) \uc0ad\uc81c\ud558\ub294 \ub3d9\uc548 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4: %s
+ERR_TRUSTSTORESYNC_ADD_FAILED_655=\ud56d\ubaa9 %s\uc744(\ub97c) \ud2b8\ub7ec\uc2a4\ud2b8 \uc800\uc7a5\uc18c\uc5d0 \ucd94\uac00\ud558\ub294 \ub3d9\uc548 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4: %s
+ERR_TRUSTSTORESYNC_DELETE_FAILED_656=\ud2b8\ub7ec\uc2a4\ud2b8 \uc800\uc7a5\uc18c\uc5d0\uc11c \ud56d\ubaa9 %s\uc744(\ub97c) \uc0ad\uc81c\ud558\ub294 \ub3d9\uc548 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4: %s
 ERR_PWPOLICY_SCHEME_DOESNT_SUPPORT_AUTH_657=\uad6c\uc131 \ud56d\ubaa9 %s\uc5d0 \uc815\uc758\ub41c \ube44\ubc00\ubc88\ud638 \uc800\uc7a5\uc18c \uc2a4\ud0a4\ub9c8\uac00 \ube44\ubc00\ubc88\ud638 \uc18d\uc131 %s\uc5d0 \uc0ac\uc6a9\ub418\ub294 \uc778\uc99d \ube44\ubc00\ubc88\ud638 \uad6c\ubb38\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.
 ERR_PWPOLICY_DEPRECATED_SCHEME_NOT_AUTH_659=\ube44\ubc00\ubc88\ud638 \uc815\ucc45 \uad6c\uc131 \ud56d\ubaa9 %s\uc774(\uac00) \uc778\uc99d \ube44\ubc00\ubc88\ud638 \uad6c\ubb38\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 \uad8c\uc7a5\ub418\uc9c0 \uc54a\ub294 \ube44\ubc00\ubc88\ud638 \uc800\uc7a5\uc18c \uc2a4\ud0a4\ub9c8 DN %s\uc744(\ub97c) \ucc38\uc870\ud569\ub2c8\ub2e4.
 WARN_GROUP_FILTER_NOT_INDEXED_660=\uadf8\ub8f9 \uad6c\ud604 %2$s\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \uac80\uc0c9 \ud544\ud130 \"%1$s\"\uc774(\uac00) \ubc31\uc5d4\ub4dc %3$s\uc5d0\uc11c \uc0c9\uc778\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc774 \uadf8\ub8f9 \uad6c\ud604\uc5d0 \ub300\ud55c \ubc31\uc5d4\ub4dc \ucd08\uae30\ud654\ub97c \uc644\ub8cc\ud558\ub294 \ub370 \ub9ce\uc740 \uc2dc\uac04\uc774 \uac78\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/core_zh_CN.properties b/opendj-server-legacy/src/messages/org/opends/messages/core_zh_CN.properties
index c0b14be..780e672 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/core_zh_CN.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/core_zh_CN.properties
@@ -12,6 +12,7 @@
 #
 # Copyright 2006-2010 Sun Microsystems, Inc.
 # Portions Copyright 2011-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
 
 
 
@@ -487,8 +488,8 @@
 INFO_DSCORE_DESCRIPTION_LASTKNOWNGOODCFG_652=\u5c1d\u8bd5\u5f00\u59cb\u4f7f\u7528\u6700\u540e\u4e00\u6b21\u6210\u529f\u542f\u52a8\uff08\u5982\u679c\u6709\uff09\u65f6\u7684\u914d\u7f6e\uff0c\u800c\u4e0d\u4f7f\u7528\u5f53\u524d\u7684\u6d3b\u52a8\u914d\u7f6e
 INFO_TRUSTSTORESYNC_ADMIN_SUFFIX_SEARCH_FAILED_653=\u5728\u641c\u7d22\u57fa %s \u4ee5\u4f7f\u4fe1\u4efb\u5e93\u4fdd\u6301\u540c\u6b65\u65f6\u51fa\u9519: %s
 ERR_TRUSTSTORESYNC_EXCEPTION_654=\u4fe1\u4efb\u5e93\u540c\u6b65\u7ebf\u7a0b\u51fa\u73b0\u9519\u8bef: %s
-INFO_TRUSTSTORESYNC_ADD_FAILED_655=\u5728\u5c1d\u8bd5\u5c06\u6761\u76ee %s \u6dfb\u52a0\u5230\u4fe1\u4efb\u5e93\u65f6\u51fa\u9519: %s
-INFO_TRUSTSTORESYNC_DELETE_FAILED_656=\u5728\u5c1d\u8bd5\u4ece\u4fe1\u4efb\u5e93\u4e2d\u5220\u9664\u6761\u76ee %s \u65f6\u51fa\u9519: %s
+ERR_TRUSTSTORESYNC_ADD_FAILED_655=\u5728\u5c1d\u8bd5\u5c06\u6761\u76ee %s \u6dfb\u52a0\u5230\u4fe1\u4efb\u5e93\u65f6\u51fa\u9519: %s
+ERR_TRUSTSTORESYNC_DELETE_FAILED_656=\u5728\u5c1d\u8bd5\u4ece\u4fe1\u4efb\u5e93\u4e2d\u5220\u9664\u6761\u76ee %s \u65f6\u51fa\u9519: %s
 ERR_PWPOLICY_SCHEME_DOESNT_SUPPORT_AUTH_657=\u914d\u7f6e\u6761\u76ee %s \u4e2d\u5b9a\u4e49\u7684\u5bc6\u7801\u5b58\u50a8\u65b9\u6848\u4e0d\u652f\u6301\u5bc6\u7801\u5c5e\u6027 %s \u4f7f\u7528\u7684\u9a8c\u8bc1\u5bc6\u7801\u8bed\u6cd5
 ERR_PWPOLICY_DEPRECATED_SCHEME_NOT_AUTH_659=\u5bc6\u7801\u7b56\u7565\u914d\u7f6e\u6761\u76ee %s \u5f15\u7528\u4e0d\u652f\u6301\u9a8c\u8bc1\u5bc6\u7801\u8bed\u6cd5\u7684\u8fc7\u65f6\u5bc6\u7801\u5b58\u50a8\u65b9\u6848 DN %s
 WARN_GROUP_FILTER_NOT_INDEXED_660=\u7ec4\u5b9e\u73b0 %2$s \u6240\u4f7f\u7528\u7684\u641c\u7d22\u8fc7\u6ee4\u5668 "%1$s" \u672a\u5728\u540e\u7aef %3$s \u4e2d\u5efa\u7acb\u7d22\u5f15\u3002\u8be5\u7ec4\u5b9e\u73b0\u7684\u540e\u7aef\u521d\u59cb\u5316\u53ef\u80fd\u9700\u8981\u8f83\u957f\u65f6\u95f4\u624d\u80fd\u5b8c\u6210
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/core_zh_TW.properties b/opendj-server-legacy/src/messages/org/opends/messages/core_zh_TW.properties
index 2dad7fd..f94e5fd 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/core_zh_TW.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/core_zh_TW.properties
@@ -12,6 +12,7 @@
 #
 # Copyright 2006-2009 Sun Microsystems, Inc.
 # Portions Copyright 2011-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
 
 
 
@@ -487,8 +488,8 @@
 INFO_DSCORE_DESCRIPTION_LASTKNOWNGOODCFG_652=\u8acb\u4f7f\u7528\u4e0a\u6b21\u6210\u529f\u555f\u52d5\u6642\u6240\u4f7f\u7528\u7684\u914d\u7f6e (\u5982\u679c\u6709\u7684\u8a71)\uff0c\u800c\u4e0d\u8981\u4f7f\u7528\u76ee\u524d\u4f7f\u7528\u4e2d\u7684\u914d\u7f6e\u4f86\u5617\u8a66\u555f\u52d5
 INFO_TRUSTSTORESYNC_ADMIN_SUFFIX_SEARCH_FAILED_653=\u641c\u5c0b\u57fa\u5e95 %s \u4ee5\u540c\u6b65\u5316\u4fe1\u4efb\u6e05\u55ae\u5b58\u653e\u5340\u6642\uff0c\u767c\u751f\u932f\u8aa4: %s
 ERR_TRUSTSTORESYNC_EXCEPTION_654=\u5728\u4fe1\u4efb\u6e05\u55ae\u5b58\u653e\u5340\u57f7\u884c\u7dd2\u4e2d\u767c\u751f\u932f\u8aa4: %s
-INFO_TRUSTSTORESYNC_ADD_FAILED_655=\u5617\u8a66\u5c07\u9805\u76ee %s \u589e\u52a0\u81f3\u4fe1\u4efb\u6e05\u55ae\u5b58\u653e\u5340\u6642\u767c\u751f\u932f\u8aa4: %s
-INFO_TRUSTSTORESYNC_DELETE_FAILED_656=\u5617\u8a66\u5c07\u9805\u76ee %s \u5f9e\u4fe1\u4efb\u6e05\u55ae\u5b58\u653e\u5340\u522a\u9664\u6642\u767c\u751f\u932f\u8aa4: %s
+ERR_TRUSTSTORESYNC_ADD_FAILED_655=\u5617\u8a66\u5c07\u9805\u76ee %s \u589e\u52a0\u81f3\u4fe1\u4efb\u6e05\u55ae\u5b58\u653e\u5340\u6642\u767c\u751f\u932f\u8aa4: %s
+ERR_TRUSTSTORESYNC_DELETE_FAILED_656=\u5617\u8a66\u5c07\u9805\u76ee %s \u5f9e\u4fe1\u4efb\u6e05\u55ae\u5b58\u653e\u5340\u522a\u9664\u6642\u767c\u751f\u932f\u8aa4: %s
 ERR_PWPOLICY_SCHEME_DOESNT_SUPPORT_AUTH_657=\u5b9a\u7fa9\u5728\u914d\u7f6e\u9805\u76ee %s \u7684\u5bc6\u78bc\u5132\u5b58\u6a5f\u5236\u4e0d\u652f\u63f4\u8a8d\u8b49\u5bc6\u78bc\u8a9e\u6cd5\uff0c\u8a72\u8a9e\u6cd5\u70ba\u5bc6\u78bc\u5c6c\u6027 %s \u6240\u4f7f\u7528
 ERR_PWPOLICY_DEPRECATED_SCHEME_NOT_AUTH_659=\u5bc6\u78bc\u7b56\u7565\u914d\u7f6e\u9805\u76ee %s \u53c3\u7167\u505c\u7528\u7684\u5bc6\u78bc\u5132\u5b58\u6a5f\u5236 DN %s\uff0c\u8a72 DN \u4e0d\u652f\u63f4\u8a8d\u8b49\u5bc6\u78bc\u8a9e\u6cd5
 WARN_GROUP_FILTER_NOT_INDEXED_660=\u7fa4\u7d44\u5be6\u4f5c %2$s \u6240\u4f7f\u7528\u7684\u641c\u5c0b\u7be9\u9078\u5668\u300c%1$s\u300d\u6c92\u6709\u5728\u5f8c\u7aef %3$s \u7de8\u88fd\u7d22\u5f15\u3002\u6b64\u7fa4\u7d44\u5be6\u4f5c\u7684\u5f8c\u7aef\u521d\u59cb\u5316\u53ef\u80fd\u8981\u82b1\u5f88\u4e45\u7684\u6642\u9593\u624d\u6703\u5b8c\u6210
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java
index 6999934..7f5bd60 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java
@@ -28,6 +28,7 @@
 import static org.opends.server.protocols.internal.InternalClientConnection.*;
 import static org.opends.server.protocols.internal.Requests.*;
 import static org.opends.server.types.Attributes.*;
+import static org.opends.server.util.CollectionUtils.newArrayList;
 import static org.testng.Assert.*;
 
 import java.io.File;
@@ -45,20 +46,32 @@
 import java.security.SecureRandom;
 import java.security.Security;
 import java.security.spec.AlgorithmParameterSpec;
+import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.TreeSet;
 import java.util.UUID;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 import java.util.concurrent.FutureTask;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import javax.crypto.CipherSpi;
 import javax.crypto.KeyGeneratorSpi;
 import javax.crypto.Mac;
 import javax.crypto.SecretKey;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
 
 import org.bouncycastle.crypto.CryptoServicesRegistrar;
 import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
@@ -72,8 +85,10 @@
 import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.SSLContextBuilder;
 import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.controls.RelaxRulesControl;
 import org.forgerock.opendj.ldap.responses.SearchResultEntry;
 import org.forgerock.opendj.ldif.ConnectionEntryReader;
+import org.forgerock.opendj.ldap.schema.CoreSchema;
 import org.forgerock.opendj.server.config.server.CryptoManagerCfg;
 import org.forgerock.util.Options;
 import org.opends.admin.ads.ADSContext;
@@ -81,14 +96,23 @@
 import org.opends.server.TestCaseUtils;
 import org.opends.server.backends.TrustStoreBackend;
 import org.opends.server.core.DirectoryServer;
+import org.opends.server.plugins.ShortCircuitPlugin;
 import org.opends.server.protocols.internal.InternalClientConnection;
 import org.opends.server.protocols.internal.InternalSearchOperation;
 import org.opends.server.protocols.internal.SearchRequest;
+import org.opends.server.protocols.ldap.LDAPControl;
+import org.opends.server.types.AttributeBuilder;
+import org.opends.server.types.Control;
 import org.opends.server.types.CryptoManager;
 import org.opends.server.types.CryptoManagerException;
 import org.opends.server.types.DirectoryException;
 import org.opends.server.types.Entry;
 import org.opends.server.types.Modification;
+import org.opends.server.types.OperationType;
+import org.opends.server.types.operation.PreOperationDeleteOperation;
+import org.opends.server.util.CertificateManager;
+import org.opends.server.util.KeyStoreReplaceFailure;
+import org.opends.server.util.Platform.KeyType;
 import org.opends.server.util.StaticUtils;
 import org.opends.server.util.TimeThread;
 import org.testng.annotations.AfterClass;
@@ -315,6 +339,408 @@
   }
 
   /**
+   Replication replays the instance keys which reach cn=admin data together on parallel threads,
+   and the trust store file is rewritten for each of them: none of the certificates may be lost,
+   and the SSL contexts, which are built from that file, must never find it half written. A
+   directory server which loses the certificate of a replication server that way fails every
+   handshake with it (#1188).
+   */
+  @Test
+  public void testInstanceKeysAddedTogetherAllReachTheTrustStore() throws Exception
+  {
+    addInstanceKeysTogetherWhileReadingTheTrustStore();
+  }
+
+  /**
+   The same, with a trust store file which cannot be replaced at once and is written in place -
+   one mounted on its own into a container, in a directory this process cannot write to, or one
+   on Windows: there only the lock of the trust store keeps the SSL contexts
+   from finding it half written.
+   */
+  @Test
+  public void testInstanceKeysAddedTogetherInPlaceAllReachTheTrustStore() throws Exception
+  {
+    try (AutoCloseable inPlace = KeyStoreReplaceFailure.failKeyStoreReplace())
+    {
+      addInstanceKeysTogetherWhileReadingTheTrustStore();
+    }
+  }
+
+  private void addInstanceKeysTogetherWhileReadingTheTrustStore() throws Exception
+  {
+    final int keys = 16;
+    final TrustStoreBackend trustStore = (TrustStoreBackend) getServerContext()
+        .getBackendConfigManager().getLocalBackendById(ID_ADS_TRUST_STORE_BACKEND);
+    final Map<DN, ByteString> certificates = generateInstanceKeyCertificates(keys);
+    final ExecutorService executor = Executors.newFixedThreadPool(keys + 1);
+    final CyclicBarrier start = new CyclicBarrier(keys + 1);
+    final AtomicBoolean adding = new AtomicBoolean(true);
+    try
+    {
+      final List<Future<ResultCode>> adds = new ArrayList<>();
+      for (Map.Entry<DN, ByteString> certificate : certificates.entrySet())
+      {
+        final Entry instanceKey = instanceKeyEntry(certificate.getKey(), certificate.getValue());
+        adds.add(executor.submit(() -> {
+          start.await();
+          return getRootConnection().processAdd(instanceKey).getResultCode();
+        }));
+      }
+      final Future<String> reads = executor.submit(() -> {
+        start.await();
+        do
+        {
+          try
+          {
+            trustStore.getTrustManagers();
+          }
+          catch (DirectoryException e)
+          {
+            return e.getMessage();
+          }
+        }
+        while (adding.get());
+        return null;
+      });
+
+      for (Future<ResultCode> add : adds)
+      {
+        assertThat(add.get(60, TimeUnit.SECONDS)).isEqualTo(ResultCode.SUCCESS);
+      }
+      adding.set(false);
+      assertThat(reads.get(60, TimeUnit.SECONDS)).as("a trust store read while the keys were added").isNull();
+
+      assertThat(trustedInstanceKeys(trustStore)).as("the instance keys the trust store holds")
+          .containsAll(instanceKeyIDs(certificates));
+    }
+    finally
+    {
+      adding.set(false);
+      executor.shutdownNow();
+      // an add which outlived its wait above would put its entries back after the deletes
+      executor.awaitTermination(60, TimeUnit.SECONDS);
+      for (DN instanceKey : certificates.keySet())
+      {
+        getRootConnection().processDelete(instanceKey);
+      }
+    }
+  }
+
+  /**
+   Replication replays the deletes of instance keys together with the adds of others: a delete
+   lost to an add would leave a compromised key trusted, an add lost to a delete would leave a
+   server untrusted (#1188).
+   */
+  @Test
+  public void testInstanceKeysAddedAndDeletedTogetherAllReachTheTrustStore() throws Exception
+  {
+    final int keys = 16;
+    final TrustStoreBackend trustStore = (TrustStoreBackend) getServerContext()
+        .getBackendConfigManager().getLocalBackendById(ID_ADS_TRUST_STORE_BACKEND);
+    final Map<DN, ByteString> deleted = generateInstanceKeyCertificates(keys);
+    final Map<DN, ByteString> added = generateInstanceKeyCertificates(keys);
+    final ExecutorService executor = Executors.newFixedThreadPool(2 * keys);
+    final CyclicBarrier start = new CyclicBarrier(2 * keys);
+    try
+    {
+      for (Map.Entry<DN, ByteString> certificate : deleted.entrySet())
+      {
+        assertThat(getRootConnection().processAdd(instanceKeyEntry(certificate.getKey(), certificate.getValue()))
+            .getResultCode()).isEqualTo(ResultCode.SUCCESS);
+      }
+      assertThat(trustedInstanceKeys(trustStore)).as("the instance keys the trust store holds before")
+          .containsAll(instanceKeyIDs(deleted));
+
+      final List<Future<ResultCode>> changes = new ArrayList<>();
+      for (Map.Entry<DN, ByteString> certificate : added.entrySet())
+      {
+        final Entry instanceKey = instanceKeyEntry(certificate.getKey(), certificate.getValue());
+        changes.add(executor.submit(() -> {
+          start.await();
+          return getRootConnection().processAdd(instanceKey).getResultCode();
+        }));
+      }
+      for (DN instanceKey : deleted.keySet())
+      {
+        changes.add(executor.submit(() -> {
+          start.await();
+          return getRootConnection().processDelete(instanceKey).getResultCode();
+        }));
+      }
+      for (Future<ResultCode> change : changes)
+      {
+        assertThat(change.get(60, TimeUnit.SECONDS)).isEqualTo(ResultCode.SUCCESS);
+      }
+
+      final Set<String> trusted = trustedInstanceKeys(trustStore);
+      assertThat(trusted).as("the instance keys the trust store holds").containsAll(instanceKeyIDs(added));
+      assertThat(trusted).as("the instance keys the trust store holds")
+          .doesNotContainAnyElementsOf(instanceKeyIDs(deleted));
+    }
+    finally
+    {
+      executor.shutdownNow();
+      // a change which outlived its wait above would put its entries back after the deletes
+      executor.awaitTermination(60, TimeUnit.SECONDS);
+      for (DN instanceKey : added.keySet())
+      {
+        getRootConnection().processDelete(instanceKey);
+      }
+      for (DN instanceKey : deleted.keySet())
+      {
+        getRootConnection().processDelete(instanceKey);
+      }
+    }
+  }
+
+  /** The IDs of the instance keys whose certificates the trust store holds. */
+  private static Set<String> trustedInstanceKeys(TrustStoreBackend trustStore) throws Exception
+  {
+    final Set<String> trusted = new HashSet<>();
+    for (TrustManager trustManager : trustStore.getTrustManagers())
+    {
+      for (X509Certificate issuer : ((X509TrustManager) trustManager).getAcceptedIssuers())
+      {
+        trusted.add(CryptoManagerImpl.getInstanceKeyID(issuer.getEncoded()));
+      }
+    }
+    return trusted;
+  }
+
+  private static List<String> instanceKeyIDs(Map<DN, ByteString> certificates) throws Exception
+  {
+    final List<String> ids = new ArrayList<>();
+    for (ByteString certificate : certificates.values())
+    {
+      ids.add(CryptoManagerImpl.getInstanceKeyID(certificate.toByteArray()));
+    }
+    return ids;
+  }
+
+  /**
+   An instance key the trust store cannot take, such as one which reached cn=admin data without
+   its certificate, leaves this server without trust in the server it belongs to, so every
+   replication connection to that server fails its handshake: the refusal must be in the error
+   log, which is where the administrator of such a server looks (#1188). A key without its
+   certificate is refused by the schema check of its copy, before the trust store backend sees
+   it; a refusal of the backend itself takes the same branch of CryptoManagerSync, and keeps its
+   result code (testTrustStoreRefusesAnAliasByWhatIsWrongWithIt).
+   */
+  @Test
+  public void testInstanceKeyTheTrustStoreRefusesIsReported() throws Exception
+  {
+    final DN instanceKey = generateInstanceKeyCertificates(1).keySet().iterator().next();
+    final Entry withoutCertificate = new Entry(instanceKey, null, null, null);
+    withoutCertificate.addObjectClass(CoreSchema.getTopObjectClass());
+    withoutCertificate.addObjectClass(getServerContext().getSchema().getObjectClass(OC_CRYPTO_INSTANCE_KEY));
+    withoutCertificate.addAttribute(
+        create(ATTR_CRYPTO_KEY_ID, instanceKey.rdn().getFirstAVA().getAttributeValue().toString()));
+    final DN trustStoreEntry = DN.valueOf(DN_TRUST_STORE_ROOT).child(instanceKey.rdn());
+
+    TestCaseUtils.ERROR_TEXT_WRITER.clear();
+    try
+    {
+      // Relaxed, as only a replayed add can store an instance key without its certificate.
+      assertThat(getRootConnection().processAdd(withoutCertificate,
+              newArrayList((Control) new LDAPControl(RelaxRulesControl.OID, false))).getResultCode())
+          .isEqualTo(ResultCode.SUCCESS);
+
+      assertThat(DirectoryServer.getEntry(trustStoreEntry)).as("the instance key in the trust store").isNull();
+      assertThat(TestCaseUtils.ERROR_TEXT_WRITER.getMessages())
+          .as("the error log")
+          .anyMatch(record -> record.contains("severity=ERROR") && record.contains(trustStoreEntry.toString()));
+    }
+    finally
+    {
+      getRootConnection().processDelete(instanceKey);
+    }
+  }
+
+  /**
+   dsreplication seeds the trust store of a server it enables with the instance keys of the
+   topology, so the add of an instance key may find its certificate there already: that is no
+   failure, and the error log must stay quiet about it.
+   */
+  @Test
+  public void testInstanceKeyTheTrustStoreHoldsAlreadyIsNotReported() throws Exception
+  {
+    final Map.Entry<DN, ByteString> certificate =
+        generateInstanceKeyCertificates(1).entrySet().iterator().next();
+    final DN instanceKey = certificate.getKey();
+    final DN trustStoreEntry = DN.valueOf(DN_TRUST_STORE_ROOT).child(instanceKey.rdn());
+    try
+    {
+      assertThat(getRootConnection().processAdd(instanceKeyEntry(trustStoreEntry, certificate.getValue()))
+          .getResultCode()).as("the seeding of the trust store").isEqualTo(ResultCode.SUCCESS);
+
+      TestCaseUtils.ERROR_TEXT_WRITER.clear();
+      assertThat(getRootConnection().processAdd(instanceKeyEntry(instanceKey, certificate.getValue()))
+          .getResultCode()).isEqualTo(ResultCode.SUCCESS);
+
+      assertThat(DirectoryServer.getEntry(trustStoreEntry)).as("the instance key in the trust store").isNotNull();
+      assertThat(TestCaseUtils.ERROR_TEXT_WRITER.getMessages())
+          .as("the error log")
+          .noneMatch(record -> record.contains("severity=ERROR") && record.contains(trustStoreEntry.toString()));
+    }
+    finally
+    {
+      getRootConnection().processDelete(instanceKey);
+      getRootConnection().processDelete(trustStoreEntry);
+    }
+  }
+
+  /**
+   An instance key marked compromised has to leave the trust store: while its certificate is
+   there, this server goes on trusting whoever holds the key. A trust store which cannot drop it
+   must say so in the error log, as it does for a certificate it cannot take.
+   */
+  @Test
+  public void testCompromisedInstanceKeyTheTrustStoreCannotDropIsReported() throws Exception
+  {
+    final Map.Entry<DN, ByteString> certificate =
+        generateInstanceKeyCertificates(1).entrySet().iterator().next();
+    final DN instanceKey = certificate.getKey();
+    final DN trustStoreEntry = DN.valueOf(DN_TRUST_STORE_ROOT).child(instanceKey.rdn());
+    try
+    {
+      assertThat(getRootConnection().processAdd(instanceKeyEntry(instanceKey, certificate.getValue()))
+          .getResultCode()).isEqualTo(ResultCode.SUCCESS);
+      assertThat(DirectoryServer.getEntry(trustStoreEntry)).as("the instance key in the trust store").isNotNull();
+
+      ShortCircuitPlugin.registerShortCircuit(OperationType.DELETE, "PreOperation",
+          ResultCode.UNWILLING_TO_PERFORM.intValue(), 0, 1,
+          op -> trustStoreEntry.equals(((PreOperationDeleteOperation) op).getEntryDN()));
+      TestCaseUtils.ERROR_TEXT_WRITER.clear();
+      assertThat(getRootConnection().processModify(instanceKey, newArrayList(new Modification(
+              ADD, create(ATTR_CRYPTO_KEY_COMPROMISED_TIME, TimeThread.getGeneralizedTime()))))
+          .getResultCode()).isEqualTo(ResultCode.SUCCESS);
+
+      assertThat(DirectoryServer.getEntry(trustStoreEntry)).as("the compromised key in the trust store").isNotNull();
+      assertThat(TestCaseUtils.ERROR_TEXT_WRITER.getMessages())
+          .as("the error log")
+          .anyMatch(record -> record.contains("severity=ERROR") && record.contains(trustStoreEntry.toString()));
+    }
+    finally
+    {
+      ShortCircuitPlugin.deregisterShortCircuit(OperationType.DELETE, "PreOperation");
+      getRootConnection().processDelete(instanceKey);
+      getRootConnection().processDelete(trustStoreEntry);
+    }
+  }
+
+  /**
+   The delete of an instance key whose certificate has left the trust store already - when the
+   key was marked compromised, say - finds nothing to drop there: that is no failure, and the
+   error log must stay quiet about it.
+   */
+  @Test
+  public void testInstanceKeyTheTrustStoreLacksAlreadyIsNotReported() throws Exception
+  {
+    final Map.Entry<DN, ByteString> certificate =
+        generateInstanceKeyCertificates(1).entrySet().iterator().next();
+    final DN instanceKey = certificate.getKey();
+    final DN trustStoreEntry = DN.valueOf(DN_TRUST_STORE_ROOT).child(instanceKey.rdn());
+    try
+    {
+      assertThat(getRootConnection().processAdd(instanceKeyEntry(instanceKey, certificate.getValue()))
+          .getResultCode()).isEqualTo(ResultCode.SUCCESS);
+      assertThat(getRootConnection().processDelete(trustStoreEntry).getResultCode())
+          .as("the certificate leaving the trust store first").isEqualTo(ResultCode.SUCCESS);
+
+      TestCaseUtils.ERROR_TEXT_WRITER.clear();
+      assertThat(getRootConnection().processDelete(instanceKey).getResultCode()).isEqualTo(ResultCode.SUCCESS);
+
+      assertThat(TestCaseUtils.ERROR_TEXT_WRITER.getMessages())
+          .as("the error log")
+          .noneMatch(record -> record.contains("severity=ERROR") && record.contains(trustStoreEntry.toString()));
+    }
+    finally
+    {
+      getRootConnection().processDelete(instanceKey);
+      getRootConnection().processDelete(trustStoreEntry);
+    }
+  }
+
+  /**
+   The trust store refuses an alias it holds already as an entry which exists, and the delete of
+   one it does not hold as an entry which does not, whichever check catches it: the sync of the
+   instance keys tells those apart from a failure by their result codes, and reports a failure in
+   the error log. Here the backend is called directly, past the existence check of the core, as an
+   add which lost the race for its alias to another one would reach it.
+   */
+  @Test
+  public void testTrustStoreRefusesAnAliasByWhatIsWrongWithIt() throws Exception
+  {
+    final TrustStoreBackend trustStore = (TrustStoreBackend) getServerContext()
+        .getBackendConfigManager().getLocalBackendById(ID_ADS_TRUST_STORE_BACKEND);
+    final Map.Entry<DN, ByteString> certificate =
+        generateInstanceKeyCertificates(1).entrySet().iterator().next();
+    final DN trustStoreEntry = DN.valueOf(DN_TRUST_STORE_ROOT).child(certificate.getKey().rdn());
+    try
+    {
+      assertThat(getRootConnection().processAdd(instanceKeyEntry(trustStoreEntry, certificate.getValue()))
+          .getResultCode()).isEqualTo(ResultCode.SUCCESS);
+      assertThatThrownBy(() -> trustStore.addEntry(instanceKeyEntry(trustStoreEntry, certificate.getValue()), null))
+          .as("an alias in use")
+          .isInstanceOfSatisfying(DirectoryException.class,
+              e -> assertThat(e.getResultCode()).isEqualTo(ResultCode.ENTRY_ALREADY_EXISTS));
+
+      assertThat(getRootConnection().processDelete(trustStoreEntry).getResultCode()).isEqualTo(ResultCode.SUCCESS);
+      assertThatThrownBy(() -> trustStore.deleteEntry(trustStoreEntry, null))
+          .as("an alias not in use")
+          .isInstanceOfSatisfying(DirectoryException.class,
+              e -> assertThat(e.getResultCode()).isEqualTo(ResultCode.NO_SUCH_OBJECT));
+    }
+    finally
+    {
+      getRootConnection().processDelete(trustStoreEntry);
+    }
+  }
+
+  /** Generates self-signed certificates, by the DN of the instance key entry each would have. */
+  private static Map<DN, ByteString> generateInstanceKeyCertificates(int count) throws Exception
+  {
+    final Path keyStore = Files.createTempFile("instance-keys", ".jks");
+    Files.delete(keyStore);
+    try
+    {
+      final CertificateManager certificateManager =
+          new CertificateManager(keyStore.toString(), CertificateManager.KEY_STORE_TYPE_JKS, "password");
+      final Map<DN, ByteString> certificates = new LinkedHashMap<>();
+      for (int i = 0; i < count; i++)
+      {
+        final String alias = "instance-key-" + i;
+        certificateManager.generateSelfSignedCertificate(KeyType.EC, alias, "CN=" + alias, 1);
+        final byte[] certificate = certificateManager.getCertificate(alias).getEncoded();
+        certificates.put(
+            DN.valueOf(ATTR_CRYPTO_KEY_ID + "=" + CryptoManagerImpl.getInstanceKeyID(certificate)
+                + ",cn=instance keys,cn=admin data"),
+            ByteString.wrap(certificate));
+      }
+      return certificates;
+    }
+    finally
+    {
+      Files.deleteIfExists(keyStore);
+    }
+  }
+
+  /** Builds an instance key entry the way the crypto manager publishes its own in cn=admin data. */
+  private static Entry instanceKeyEntry(DN dn, ByteString certificate) throws DirectoryException
+  {
+    final Entry entry = new Entry(dn, null, null, null);
+    entry.addObjectClass(CoreSchema.getTopObjectClass());
+    entry.addObjectClass(getServerContext().getSchema().getObjectClass(OC_CRYPTO_INSTANCE_KEY));
+    entry.addAttribute(create(ATTR_CRYPTO_KEY_ID, dn.rdn().getFirstAVA().getAttributeValue().toString()));
+    final AttributeBuilder builder =
+        new AttributeBuilder(getServerContext().getSchema().getAttributeType(ATTR_CRYPTO_PUBLIC_KEY_CERTIFICATE));
+    builder.setOption("binary");
+    builder.add(certificate);
+    entry.addAttribute(builder.toAttribute());
+    return entry;
+  }
+
+  /**
    A key wrapping transformation this Java runtime cannot provide is refused, at start (where
    the refusal is what keeps the server from starting) as on a change, and the refusal has to
    name the property to set, not only the cipher which failed: on a FIPS-restricted runtime
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/util/CertificateManagerTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/util/CertificateManagerTestCase.java
index 774d735..c982f90 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/util/CertificateManagerTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/util/CertificateManagerTestCase.java
@@ -22,11 +22,26 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFilePermissions;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.cert.Certificate;
 import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
+import com.sun.security.auth.module.UnixSystem;
+
+import org.testng.SkipException;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
@@ -1416,6 +1431,389 @@
 
 
   /**
+   * Tests that a reader of a key store file never finds it half written while it is rewritten:
+   * the trust store of the server is read from its file for each SSL context, and at the next
+   * start, by readers which do not share any lock with the writer, and a server stopped part way
+   * through a write must not be left with a truncated file it cannot start with (#1188). On a
+   * POSIX file system only: elsewhere (Windows) the key store is written in place.
+   *
+   * @throws  Exception  If a problem occurs.
+   */
+  @Test
+  public void testKeyStoreFileIsNeverSeenHalfWritten()
+         throws Exception
+  {
+    skipUnlessPosix();
+    final File tmpDir = TestCaseUtils.createTemporaryDirectory("halfWritten");
+    final ExecutorService executor = Executors.newSingleThreadExecutor();
+    final AtomicBoolean writing = new AtomicBoolean(true);
+    try
+    {
+      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
+      final File destination = new File(tmpDir, "ads-truststore");
+      final CertificateManager manager =
+           new CertificateManager(destination.getAbsolutePath(), "JKS", "password");
+      manager.addTrustedCertificate("cert-0", certificate);
+
+      final CountDownLatch started = new CountDownLatch(1);
+      final AtomicInteger loads = new AtomicInteger();
+      final Future<String> reads = executor.submit(() -> {
+        final KeyStore keyStore = KeyStore.getInstance("JKS");
+        started.countDown();
+        while (writing.get())
+        {
+          try (FileInputStream in = new FileInputStream(destination))
+          {
+            keyStore.load(in, "password".toCharArray());
+            loads.incrementAndGet();
+          }
+          catch (Exception e)
+          {
+            return e.toString();
+          }
+        }
+        return null;
+      });
+      assertTrue(started.await(60, TimeUnit.SECONDS), "the reader started");
+      for (int i = 1; i <= 200; i++)
+      {
+        manager.addTrustedCertificate("cert-" + i, certificate);
+      }
+      writing.set(false);
+      assertNull(reads.get(60, TimeUnit.SECONDS), "a read of the key store while it was rewritten");
+      assertTrue(loads.get() > 0, "the reader loaded the key store while it was rewritten");
+    }
+    finally
+    {
+      writing.set(false);
+      executor.shutdownNow();
+      executor.awaitTermination(60, TimeUnit.SECONDS);
+      TestCaseUtils.deleteDirectory(tmpDir);
+    }
+  }
+
+
+
+  /**
+   * Tests that the rewrite of a key store keeps the file it replaces as it was: its permissions,
+   * which the file it is written to first does not have, and the link the key store path may be,
+   * which must go on pointing where the administrator put the key store.
+   *
+   * @throws  Exception  If a problem occurs.
+   */
+  @Test
+  public void testKeyStoreRewriteKeepsPermissionsAndLink()
+         throws Exception
+  {
+    skipUnlessPosix();
+    final File tmpDir = TestCaseUtils.createTemporaryDirectory("rewriteKeeps");
+    try
+    {
+      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
+      final Path store = Files.createDirectory(tmpDir.toPath().resolve("store"));
+      final Path target = store.resolve("ads-truststore");
+      new CertificateManager(target.toString(), "JKS", "password").addTrustedCertificate("cert-0", certificate);
+      Files.setPosixFilePermissions(target, PosixFilePermissions.fromString("rw-r-----"));
+      final Path link = Files.createSymbolicLink(tmpDir.toPath().resolve("ads-truststore"), target);
+
+      new CertificateManager(link.toString(), "JKS", "password").addTrustedCertificate("cert-1", certificate);
+
+      assertTrue(Files.isSymbolicLink(link), "the key store path is a link still");
+      assertEquals(PosixFilePermissions.toString(Files.getPosixFilePermissions(target)), "rw-r-----");
+      assertTrue(loadJks(target.toFile(), "password").containsAlias("cert-1"), "the key store the link points to");
+    }
+    finally
+    {
+      // The link first: once the file it points to is gone, it no longer exists for deleteDirectory.
+      Files.deleteIfExists(tmpDir.toPath().resolve("ads-truststore"));
+      TestCaseUtils.deleteDirectory(tmpDir);
+    }
+  }
+
+
+
+  /**
+   * Tests that a key store in a directory this process cannot write to, as one mounted on its own
+   * into a container, is still rewritten: in place, there being nowhere else to write it.
+   *
+   * @throws  Exception  If a problem occurs.
+   */
+  @Test
+  public void testKeyStoreInADirectoryNotWritableIsRewrittenInPlace()
+         throws Exception
+  {
+    skipUnlessPosix();
+    final File tmpDir = TestCaseUtils.createTemporaryDirectory("rewriteInPlace");
+    final Path store = tmpDir.toPath().resolve("store");
+    try
+    {
+      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
+      Files.createDirectory(store);
+      final Path target = store.resolve("ads-truststore");
+      final CertificateManager manager = new CertificateManager(target.toString(), "JKS", "password");
+      manager.addTrustedCertificate("cert-0", certificate);
+      Files.setPosixFilePermissions(store, PosixFilePermissions.fromString("r-xr-xr-x"));
+      if (Files.isWritable(store))
+      {
+        throw new SkipException("this process writes to a directory whatever its permissions");
+      }
+
+      manager.addTrustedCertificate("cert-1", certificate);
+
+      assertTrue(loadJks(target.toFile(), "password").containsAlias("cert-1"), "the key store");
+    }
+    finally
+    {
+      if (Files.exists(store))
+      {
+        Files.setPosixFilePermissions(store, PosixFilePermissions.fromString("rwxr-xr-x"));
+      }
+      TestCaseUtils.deleteDirectory(tmpDir);
+    }
+  }
+
+
+
+  /**
+   * Tests that the rewrite of a key store keeps its group, a group of this process other than the
+   * one a new file gets, and still replaces the file at once rather than writing it in place.
+   *
+   * @throws  Exception  If a problem occurs.
+   */
+  @Test
+  public void testKeyStoreRewriteKeepsTheGroup()
+         throws Exception
+  {
+    skipUnlessPosix();
+    final File tmpDir = TestCaseUtils.createTemporaryDirectory("rewriteKeepsGroup");
+    try
+    {
+      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
+      final Path target = tmpDir.toPath().resolve("ads-truststore");
+      new CertificateManager(target.toString(), "JKS", "password").addTrustedCertificate("cert-0", certificate);
+      final long group = moveToAnotherGroup(target);
+      final Object file = Files.getAttribute(target, "unix:ino");
+
+      new CertificateManager(target.toString(), "JKS", "password").addTrustedCertificate("cert-1", certificate);
+
+      assertEquals(((Integer) Files.getAttribute(target, "unix:gid")).longValue(), group,
+          "the group of the key store");
+      assertNotEquals(Files.getAttribute(target, "unix:ino"), file, "the key store replaced, not written in place");
+      assertTrue(loadJks(target.toFile(), "password").containsAlias("cert-1"), "the key store");
+    }
+    finally
+    {
+      TestCaseUtils.deleteDirectory(tmpDir);
+    }
+  }
+
+
+
+  @DataProvider
+  public Object[][] groupNotTaken()
+  {
+    return new Object[][] { { "ignored" }, { "refused" } };
+  }
+
+  /**
+   * Tests that a key store whose group the new file cannot take - on a file system which ignores
+   * a change of group, or for a group this process is not a member of - is rewritten in place,
+   * where it keeps its group, rather than replaced by a file of another group.
+   *
+   * @param  how  How the group is not taken.
+   *
+   * @throws  Exception  If a problem occurs.
+   */
+  @Test(dataProvider = "groupNotTaken")
+  public void testKeyStoreWhoseGroupCannotBeTakenIsRewrittenInPlace(String how)
+         throws Exception
+  {
+    skipUnlessPosix();
+    final File tmpDir = TestCaseUtils.createTemporaryDirectory("rewriteGroupNotTaken");
+    try
+    {
+      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
+      final Path target = tmpDir.toPath().resolve("ads-truststore");
+      new CertificateManager(target.toString(), "JKS", "password").addTrustedCertificate("cert-0", certificate);
+      final long group = moveToAnotherGroup(target);
+      final Object file = Files.getAttribute(target, "unix:ino");
+
+      try (AutoCloseable notTaken = "refused".equals(how)
+          ? KeyStoreReplaceFailure.refuseKeyStoreGroup()
+          : KeyStoreReplaceFailure.ignoreKeyStoreGroup())
+      {
+        new CertificateManager(target.toString(), "JKS", "password").addTrustedCertificate("cert-1", certificate);
+      }
+
+      assertEquals(((Integer) Files.getAttribute(target, "unix:gid")).longValue(), group,
+          "the group of the key store");
+      assertEquals(Files.getAttribute(target, "unix:ino"), file, "the key store written in place, not replaced");
+      assertTrue(loadJks(target.toFile(), "password").containsAlias("cert-1"), "the key store");
+      assertEquals(tmpDir.list(), new String[] { "ads-truststore" }, "the files next to the key store");
+    }
+    finally
+    {
+      TestCaseUtils.deleteDirectory(tmpDir);
+    }
+  }
+
+
+
+  /**
+   * Gives a file a group of this process other than the one a new file gets.
+   *
+   * @param  target  The file.
+   *
+   * @return  the group given
+   */
+  private static long moveToAnotherGroup(Path target) throws Exception
+  {
+    final long newFileGroup = ((Integer) Files.getAttribute(target, "unix:gid")).longValue();
+    final UnixSystem process = new UnixSystem();
+    long group = process.getGid();
+    for (long g : process.getGroups())
+    {
+      if (group == newFileGroup)
+      {
+        group = g;
+      }
+    }
+    if (group == newFileGroup)
+    {
+      throw new SkipException("this process is a member of no group but the one a new file gets");
+    }
+    Files.getFileAttributeView(target, PosixFileAttributeView.class).setGroup(FileSystems.getDefault()
+        .getUserPrincipalLookupService().lookupPrincipalByGroupName(String.valueOf(group)));
+    return group;
+  }
+
+
+
+  /**
+   * Tests that a key store file with another hard link is rewritten in place: a new file in its
+   * place would leave the other name with the old key store.
+   *
+   * @throws  Exception  If a problem occurs.
+   */
+  @Test
+  public void testKeyStoreWithAnotherHardLinkIsRewrittenInPlace()
+         throws Exception
+  {
+    skipUnlessPosix();
+    final File tmpDir = TestCaseUtils.createTemporaryDirectory("rewriteHardLink");
+    try
+    {
+      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
+      final Path target = tmpDir.toPath().resolve("ads-truststore");
+      new CertificateManager(target.toString(), "JKS", "password").addTrustedCertificate("cert-0", certificate);
+      final Path other = Files.createLink(tmpDir.toPath().resolve("ads-truststore.link"), target);
+
+      new CertificateManager(target.toString(), "JKS", "password").addTrustedCertificate("cert-1", certificate);
+
+      assertTrue(loadJks(other.toFile(), "password").containsAlias("cert-1"), "the key store by its other name");
+    }
+    finally
+    {
+      TestCaseUtils.deleteDirectory(tmpDir);
+    }
+  }
+
+
+
+  /**
+   * Tests that a key store file this process may not write to is not rewritten, even in a
+   * directory it may write to, where the file could be replaced.
+   *
+   * @throws  Exception  If a problem occurs.
+   */
+  @Test
+  public void testKeyStoreNotWritableIsRefused()
+         throws Exception
+  {
+    skipUnlessPosix();
+    final File tmpDir = TestCaseUtils.createTemporaryDirectory("rewriteRefused");
+    try
+    {
+      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
+      final Path target = tmpDir.toPath().resolve("ads-truststore");
+      final CertificateManager manager = new CertificateManager(target.toString(), "JKS", "password");
+      manager.addTrustedCertificate("cert-0", certificate);
+      Files.setPosixFilePermissions(target, PosixFilePermissions.fromString("r--r--r--"));
+      if (Files.isWritable(target))
+      {
+        throw new SkipException("this process writes to a file whatever its permissions");
+      }
+
+      assertThrows(KeyStoreException.class, () -> manager.addTrustedCertificate("cert-1", certificate));
+
+      assertFalse(loadJks(target.toFile(), "password").containsAlias("cert-1"), "the key store");
+      assertEquals(PosixFilePermissions.toString(Files.getPosixFilePermissions(target)), "r--r--r--");
+    }
+    finally
+    {
+      TestCaseUtils.deleteDirectory(tmpDir);
+    }
+  }
+
+
+
+  /**
+   * Tests that a key store which cannot be replaced at once, as a file mounted on its own into a
+   * container (EBUSY, EXDEV), is still rewritten: in place.
+   *
+   * @throws  Exception  If a problem occurs.
+   */
+  @Test
+  public void testKeyStoreWhichCannotBeReplacedIsRewrittenInPlace()
+         throws Exception
+  {
+    final File tmpDir = TestCaseUtils.createTemporaryDirectory("rewriteNotReplaced");
+    try
+    {
+      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
+      final File target = new File(tmpDir, "ads-truststore");
+      final CertificateManager manager = new CertificateManager(target.getAbsolutePath(), "JKS", "password");
+      manager.addTrustedCertificate("cert-0", certificate);
+
+      try (AutoCloseable inPlace = KeyStoreReplaceFailure.failKeyStoreReplace())
+      {
+        manager.addTrustedCertificate("cert-1", certificate);
+      }
+
+      assertTrue(loadJks(target, "password").containsAlias("cert-1"), "the key store");
+      assertEquals(tmpDir.list(), new String[] { "ads-truststore" }, "the files next to the key store");
+    }
+    finally
+    {
+      TestCaseUtils.deleteDirectory(tmpDir);
+    }
+  }
+
+
+
+  private static void skipUnlessPosix()
+  {
+    if (!FileSystems.getDefault().supportedFileAttributeViews().contains("posix"))
+    {
+      throw new SkipException("POSIX file permissions and links are not available here");
+    }
+  }
+
+
+
+  private static KeyStore loadJks(File file, String password) throws Exception
+  {
+    final KeyStore keyStore = KeyStore.getInstance("JKS");
+    try (FileInputStream in = new FileInputStream(file))
+    {
+      keyStore.load(in, password.toCharArray());
+    }
+    return keyStore;
+  }
+
+
+
+  /**
    * Exports a certificate to a temporary file.
    *
    * @throws  Exception  If a problem occurs.
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/util/KeyStoreReplaceFailure.java b/opendj-server-legacy/src/test/java/org/opends/server/util/KeyStoreReplaceFailure.java
new file mode 100644
index 0000000..1c60074
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/util/KeyStoreReplaceFailure.java
@@ -0,0 +1,78 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.server.util;
+
+import java.io.IOException;
+import java.nio.file.AtomicMoveNotSupportedException;
+
+/**
+ * Makes every key store written by {@link Platform} fail to replace its file at once, as a file
+ * mounted on its own into a container does, or fail to give the new file the group of the old
+ * one, so that it is written in place.
+ */
+public final class KeyStoreReplaceFailure
+{
+  private KeyStoreReplaceFailure()
+  {
+  }
+
+  /**
+   * Makes the replace of a key store file fail until the result is closed.
+   *
+   * @return what puts the replace back
+   */
+  public static AutoCloseable failKeyStoreReplace()
+  {
+    final Platform.KeyStoreReplacer replacer = Platform.keyStoreReplacer;
+    Platform.keyStoreReplacer = (temp, target) -> {
+      throw new AtomicMoveNotSupportedException(temp.toString(), target.toString(), "a test");
+    };
+    return () -> Platform.keyStoreReplacer = replacer;
+  }
+
+  /**
+   * Makes the new file of every key store written keep the group it is made with, as on a file
+   * system which ignores a change of group, until the result is closed.
+   *
+   * @return what puts the group back
+   */
+  public static AutoCloseable ignoreKeyStoreGroup()
+  {
+    return setKeyStoreGroup((view, group) -> {
+      // the group of the new file stays as it is
+    });
+  }
+
+  /**
+   * Makes the new file of every key store written refuse the group of the old one, as for a
+   * group this process is not a member of, until the result is closed.
+   *
+   * @return what puts the group back
+   */
+  public static AutoCloseable refuseKeyStoreGroup()
+  {
+    return setKeyStoreGroup((view, group) -> {
+      throw new IOException("a test: not a member of " + group.getName());
+    });
+  }
+
+  private static AutoCloseable setKeyStoreGroup(Platform.KeyStoreGroupSetter setter)
+  {
+    final Platform.KeyStoreGroupSetter old = Platform.keyStoreGroupSetter;
+    Platform.keyStoreGroupSetter = setter;
+    return () -> Platform.keyStoreGroupSetter = old;
+  }
+}

--
Gitblit v1.10.0