mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
yesterday f93487455cc1b5d7062ea6577d99dea25bfd81b3
[#1188] Serialize and replace atomically the writes of the ads-truststore, and report a certificate it cannot take or drop (#1189)

Fixes #1188 in part: the trust store side. The instance key stored in
`cn=admin data` without its certificate is not explained by this change;
see "Not fixed here" below.

## The defect

Every change to the ads-truststore reads the file, changes that copy and
writes the whole file back over the old one, truncating it first, and
nothing kept two of them apart. Replication replays the instance keys of
`cn=admin data` on parallel threads (16 at least), each of which copies
its key into the trust store in `CryptoManagerSync`, and an SSL context
is built from the file for every replication connection. So:

- two keys replayed together lost one of them (the second write is made
from a copy read before the first one);
- a reader could find the file half written: `Keystore was tampered
with`, `signed overrun`, `EOFException`;
- a directory server which lost the certificate of a replication server
that way failed every handshake with it (`bad_certificate`).

A failed copy into the trust store was logged at debug level only, so
the server said nothing about why it did not trust the other one.

## The change

- `TrustStoreBackend` holds one lock over every access to the file and
to its certificate manager: reads, adds, deletes, the instance
certificate it generates and the change of its configuration. The key
managers and keys it hands out are opened under that lock as well, with
the PIN of the file they were read from rather than that of a
configuration changed meanwhile.
- On a POSIX file system, `Platform` writes every key store to a file of
its own next to it and moves that over the old one at once
(`ATOMIC_MOVE`): a reader outside the lock never finds the file half
written, and a server stopped part way through a write keeps the old
file whole instead of an empty one it cannot start with. The new file
takes the owner, the group and then the mode of the old one (not its
ACLs), so that it is never open to a group the old one was not; a key
store path which is a link goes on pointing where it did. The key store
is written in place, as before, where replacing the file would change
what it is or cannot be done: 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 (a file mounted on its own into a container).
On a file system without POSIX attributes (Windows) the key store is
always written in place, as before: neither the owner, the ACLs nor the
hard links of the old file would reach a new one there.
- `CryptoManagerSync` reports a certificate it cannot copy into the
trust store, and one it cannot drop from it (a compromised key the
server would go on trusting), at error level:
`INFO_TRUSTSTORESYNC_ADD_FAILED_655` and
`INFO_TRUSTSTORESYNC_DELETE_FAILED_656` become `ERR_`, same ordinals.
Nothing is reported where nothing failed: a certificate the trust store
holds already (dsreplication seeds the trust store of the servers it
enables) and one it holds no longer (it leaves when its key is marked
compromised).
- `TrustStoreBackend` keeps the result code of those two refusals, an
alias in use (entry already exists) and an alias not in use (no such
entry), instead of turning them into a server error, which
`CryptoManagerSync` would have reported.

## Not fixed here

#1188 also found an instance key stored in `cn=admin data` on the
directory server without `ds-cfg-public-key-certificate`, which no trust
store can take. I ran the scenario of the issue (the #534 topology of
#1184, a replication server and a directory server set up again on empty
volumes while the first directory server is down) three times on an
image built from this branch, with temporary logging of every `AddMsg`
of an instance key: built, decoded on the replication and the directory
servers, replayed, and what each add left in `cn=admin data`. It did not
reproduce: every message carried the certificate (745 bytes), the
encoder threw nothing, all four servers held every key with its
certificate in `cn=admin data` and in the trust store, before and after
a restart of the directory server. The cause of the entry without a
certificate is still open.

One thing the runs did show: the directory server that was down fails
three handshakes with the new replication server, one per domain, in the
second it starts, and none after. That is before it has learned the new
certificate through the other replication server; I have not verified
that further.

## Tests

- `CryptoManagerTestCase`:
- sixteen instance keys added together all reach the trust store, while
a reader builds trust managers from the file the whole time — once with
the file replaced at once, once with it written in place, where only the
lock keeps the reader whole;
- sixteen instance keys added while sixteen others are deleted: every
added key is in the trust store, no deleted one;
- an instance key the trust store cannot take (refused by the schema
check of its copy) is in the error log, and one it holds already is not;
- a compromised key it cannot drop is in the error log, and the delete
of a key whose certificate has left already is not;
- the trust store refuses an alias in use and one not in use with their
own result codes.
- `CertificateManagerTestCase`:
- a reader of a key store file never finds it half written while it is
rewritten (POSIX only);
- the rewrite keeps the mode of the file, its group and the link to it,
and still replaces the file;
- a key store with another hard link, one whose group the new file
cannot take (ignored or refused), one in a directory this process cannot
write to, and one which cannot be replaced are rewritten in place;
- a key store file this process may not write to is refused.

RED on master for the concurrency, error-log and result-code tests, and
for the half-written file. Each of the changes reverted on its own turns
its test red (22 mutants over the four rounds). `CryptoManagerTestCase`,
`CertificateManagerTestCase`, `AdsTrustStoreProvisionerTest` and
`AdsTrustStoreInstallTestCase`: 138 tests, all green. Without a test:
the PIN taken under the lock and the configuration change under it (the
race with a change of the configuration cannot be made to happen
reliably in one), the check of the owner the new file took (a process
which is not root cannot make a file whose owner it cannot take), the
order of the group and the mode (the window between them cannot be seen
from a test), the in-place road where the links of the file cannot be
told, and the in-place road on Windows (no Windows run).
13 files modified
1 files added
1307 ■■■■■ changed files
opendj-server-legacy/src/main/java/org/opends/server/backends/TrustStoreBackend.java 156 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerSync.java 22 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/util/Platform.java 188 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/core.properties 4 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/core_de.properties 5 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/core_es.properties 5 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/core_fr.properties 5 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/core_ja.properties 5 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/core_ko.properties 5 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/core_zh_CN.properties 5 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/core_zh_TW.properties 5 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java 426 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/util/CertificateManagerTestCase.java 398 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/util/KeyStoreReplaceFailure.java 78 ●●●●● patch | view | raw | blame | history
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(
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());
    }
  }
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)
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 \
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
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
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
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
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.
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
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
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
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.
opendj-server-legacy/src/test/java/org/opends/server/util/KeyStoreReplaceFailure.java
New file
@@ -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;
  }
}