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 a1d58fd133..07b410d604 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 @@ public class TrustStoreBackend extends LocalBackend /** 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 @@ public void openBackend() throws ConfigException, InitializationException 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 objectClasses = new LinkedHashMap<>(2); @@ -327,7 +339,11 @@ public long getEntryCount() try { - String[] aliases = certificateManager.getCertificateAliases(); + String[] aliases; + synchronized (trustStoreLock) + { + aliases = certificateManager.getCertificateAliases(); + } if (aliases != null) { numEntries += aliases.length; @@ -409,7 +425,11 @@ private Entry getCertEntry(DN entryDN) 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 @@ public void addEntry(Entry entry, AddOperation addOperation) if (parentDN.equals(getBaseDN())) { - addCertificate(entry); + synchronized (trustStoreLock) + { + addCertificate(entry); + } } else { @@ -494,7 +517,10 @@ public void deleteEntry(DN entryDN, DeleteOperation deleteOperation) throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message); } - deleteCertificate(entryDN); + synchronized (trustStoreLock) + { + deleteCertificate(entryDN); + } } @Override @@ -538,7 +564,10 @@ public void search(SearchOperation searchOperation) String[] aliases = null; try { - aliases = certificateManager.getCertificateAliases(); + synchronized (trustStoreLock) + { + aliases = certificateManager.getCertificateAliases(); + } } catch (KeyStoreException e) { @@ -772,12 +801,15 @@ public ConfigChangeResult applyConfigurationChange(TrustStoreBackendCfg cfg) 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 @@ private static DN makeChildDN(DN parentDN, AttributeType rdnAttrType, String rdn public KeyManager[] getKeyManagers() throws DirectoryException { - final KeyStore keyStore = loadKeyStore(); - - try - { - String keyManagerAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); - KeyManagerFactory keyManagerFactory = - KeyManagerFactory.getInstance(keyManagerAlgorithm); - keyManagerFactory.init(keyStore, trustStorePIN); - return keyManagerFactory.getKeyManagers(); - } - catch (Exception e) + // the keys are opened with the PIN of the file they were read from, not that of a new one + synchronized (trustStoreLock) { - 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 @@ public boolean containsKeyWithAlias(String alias) throws DirectoryException 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 TrustManager[] getTrustManagers() 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 @@ private void addCertificate(Entry entry) } } } + 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 @@ private void deleteCertificate(DN entryDN) 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 9cff8a7f52..fe8d4210a1 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 void modifyEntry(Entry srcEntry, Entry dstEntry) 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 @@ private void addEntry(Entry srcEntry, DN dstDN) 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) + { + // 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) { - logger.debug(INFO_TRUSTSTORESYNC_ADD_FAILED, dstDN, addOperation.getErrorMessage()); + // 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 d4267e534e..97951c6f91 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 @@ public final class Platform 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 @@ protected PlatformIMPL() { } + /** + * 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. + *

+ * 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 @@ private final void deleteAlias(KeyStore ks, String ksPath, String alias, 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 @@ else if (!ks.containsAlias(alias) 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 @@ else if (ks.containsAlias(alias)) // 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 @@ else if (ks.containsAlias(alias)) 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 @@ else if (ks.containsAlias(alias)) 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 72759e0df7..545450ea64 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 @@ INFO_TRUSTSTORESYNC_ADMIN_SUFFIX_SEARCH_FAILED_653=Error while searching base \ %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 d579f6e3b9..31594eb05f 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 @@ ERR_CRYPTOMGR_ADS_TRUST_STORE_BACKEND_WRONG_CLASS_651=Das Backend %s ist kein Ve 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 9b20288b85..87a899c556 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 @@ ERR_CRYPTOMGR_ADS_TRUST_STORE_BACKEND_WRONG_CLASS_651=El backend %s no es un bac 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 b835bd8b2b..81aff4d954 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 @@ ERR_CRYPTOMGR_ADS_TRUST_STORE_BACKEND_WRONG_CLASS_651=Le backend %s n\u2019est p 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 3f4140d255..972b444463 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 @@ ERR_CRYPTOMGR_ADS_TRUST_STORE_BACKEND_WRONG_CLASS_651=\u30d0\u30c3\u30af\u30a8\u 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 478bcf3dae..2898b63186 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 @@ ERR_CRYPTOMGR_ADS_TRUST_STORE_BACKEND_WRONG_CLASS_651=\ubc31\uc5d4\ub4dc %s\uc77 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 c0b14be206..780e6729da 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 @@ ERR_CRYPTOMGR_ADS_TRUST_STORE_BACKEND_WRONG_CLASS_651=\u540e\u7aef %s \u4e0d\u66 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 2dad7fdbbf..f94e5fdb74 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 @@ ERR_CRYPTOMGR_ADS_TRUST_STORE_BACKEND_WRONG_CLASS_651=\u5f8c\u7aef %s \u4e0d\u66 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 699993498e..7f5bd606a4 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; @@ -314,6 +338,408 @@ public void testSslCertNicknameChangeReportsATrustStoreItCannotRead() throws Exc .contains(trustStore.getTrustStoreFile()); } + /** + 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 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> adds = new ArrayList<>(); + for (Map.Entry certificate : certificates.entrySet()) + { + final Entry instanceKey = instanceKeyEntry(certificate.getKey(), certificate.getValue()); + adds.add(executor.submit(() -> { + start.await(); + return getRootConnection().processAdd(instanceKey).getResultCode(); + })); + } + final Future reads = executor.submit(() -> { + start.await(); + do + { + try + { + trustStore.getTrustManagers(); + } + catch (DirectoryException e) + { + return e.getMessage(); + } + } + while (adding.get()); + return null; + }); + + for (Future 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 deleted = generateInstanceKeyCertificates(keys); + final Map added = generateInstanceKeyCertificates(keys); + final ExecutorService executor = Executors.newFixedThreadPool(2 * keys); + final CyclicBarrier start = new CyclicBarrier(2 * keys); + try + { + for (Map.Entry 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> changes = new ArrayList<>(); + for (Map.Entry 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 change : changes) + { + assertThat(change.get(60, TimeUnit.SECONDS)).isEqualTo(ResultCode.SUCCESS); + } + + final Set 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 trustedInstanceKeys(TrustStoreBackend trustStore) throws Exception + { + final Set 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 instanceKeyIDs(Map certificates) throws Exception + { + final List 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 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 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 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 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 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 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 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 774d735611..c982f90f00 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; @@ -1415,6 +1430,389 @@ public void testImportKeyEntryAndAddTrustedCertificateRefuseAnAliasInUse() + /** + * 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 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. * 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 0000000000..1c60074e08 --- /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; + } +}