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