From 64a563cc8931c3eec5d4980650f9b0dfd6a4f24e Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Mon, 28 Sep 2026 12:48:49 +0000
Subject: [PATCH] [#1095] Load a file based key store or trust store again when the file changes (#1101)
---
opendj-server-legacy/src/main/java/org/opends/server/extensions/FileBasedKeyManagerProvider.java | 293 +++++++++-
opendj-server-legacy/src/main/java/org/opends/server/extensions/FileBasedTrustManagerProvider.java | 284 ++++++++++
opendj-server-legacy/src/test/java/org/opends/server/extensions/FileBasedTrustManagerProviderTestCase.java | 419 ++++++++++++++++
opendj-server-legacy/src/test/java/org/opends/server/extensions/FileBasedKeyManagerProviderTestCase.java | 370 ++++++++++++++
opendj-server-legacy/src/test/java/org/opends/server/extensions/FileStampTestCase.java | 69 ++
opendj-server-legacy/src/main/java/org/opends/server/extensions/FileStamp.java | 100 +++
opendj-server-legacy/src/messages/org/opends/messages/extension.properties | 13
7 files changed, 1,503 insertions(+), 45 deletions(-)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/FileBasedKeyManagerProvider.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/FileBasedKeyManagerProvider.java
index 0f62462..5149cab 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/FileBasedKeyManagerProvider.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/FileBasedKeyManagerProvider.java
@@ -13,6 +13,7 @@
*
* Copyright 2006-2008 Sun Microsystems, Inc.
* Portions Copyright 2011-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.extensions;
@@ -24,13 +25,23 @@
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
+import java.net.Socket;
import java.security.KeyStore;
import java.security.KeyStoreException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.X509ExtendedKeyManager;
import com.forgerock.opendj.util.FipsStaticUtils;
import org.forgerock.i18n.LocalizableMessage;
@@ -61,12 +72,176 @@
/** The configuration for this key manager provider. */
private FileBasedKeyManagerProviderCfg currentConfig;
- /** The PIN needed to access the keystore. */
- private char[] keyStorePIN;
/** The path to the key store backing file. */
private String keyStoreFile;
/** The key store type to use. */
private String keyStoreType;
+ /**
+ * The number of configuration changes applied. It is counted after the fields above are set,
+ * and read before them.
+ */
+ private volatile int configurationChanges;
+
+ /**
+ * A key manager loaded from the key store file, with the configuration change and the stamps
+ * of the files it was loaded under, and the aliases the file held private keys under.
+ */
+ private static final class LoadedKeyManager
+ {
+ private final int configurationChanges;
+ private final List<FileStamp> stamps;
+ private final Set<String> keyAliases;
+ private final X509ExtendedKeyManager keyManager;
+
+ private LoadedKeyManager(int configurationChanges, List<FileStamp> stamps, Set<String> keyAliases,
+ X509ExtendedKeyManager keyManager)
+ {
+ this.configurationChanges = configurationChanges;
+ this.stamps = stamps;
+ this.keyAliases = keyAliases;
+ this.keyManager = keyManager;
+ }
+
+ private boolean isLoadedFrom(int configurationChanges, List<FileStamp> stamps)
+ {
+ return this.configurationChanges == configurationChanges && this.stamps.equals(stamps);
+ }
+ }
+
+ /**
+ * The key manager handed out by {@link #getKeyManagers()}. The key store file is looked at
+ * again when a handshake chooses its alias, and only then: the certificate chain and the
+ * private key of the alias chosen come from the key manager that alias was chosen from,
+ * unless the file is loaded again, by another handshake, in between.
+ * <p>
+ * Each key manager handed out loads the file on its own, so asking the provider again, as a
+ * connection handler does to check a configuration change, leaves those in use alone.
+ */
+ private final class ReloadingKeyManager extends X509ExtendedKeyManager
+ {
+ /** The key manager last loaded, when this one was handed out or by a handshake since. */
+ private volatile LoadedKeyManager loaded;
+
+ private ReloadingKeyManager(LoadedKeyManager loaded)
+ {
+ this.loaded = loaded;
+ }
+
+ /**
+ * Returns the key manager to use for a new handshake, first loading the key store file again
+ * when it has changed since it was last loaded, or the configuration has. A file that cannot
+ * be loaded - caught half written, or not matching its PIN - leaves the key manager last
+ * loaded in use, and is not tried again until it changes again. So does a file with no private
+ * key, or one that shares no alias with the file last loaded. A connection handler presents
+ * the key named by its ssl-cert-nickname, which the provider does not see, and the aliases of
+ * the file last loaded stand in for it: a file that keeps one of them, but not the one a
+ * handler names, is still taken.
+ */
+ private X509ExtendedKeyManager currentKeyManager()
+ {
+ LoadedKeyManager current = loaded;
+ if (current.isLoadedFrom(configurationChanges, stampFiles()))
+ {
+ return current.keyManager;
+ }
+ // the provider's lock, which applyConfigurationChange takes too: a load reads the
+ // configuration as it was before a change or after it, never a mix of both
+ synchronized (FileBasedKeyManagerProvider.this)
+ {
+ // stamped again under the lock: stamps taken before it may be those of a write another
+ // thread has loaded past meanwhile
+ final int changes = configurationChanges;
+ final List<FileStamp> stamps = stampFiles();
+ current = loaded;
+ if (current.isLoadedFrom(changes, stamps))
+ {
+ return current.keyManager;
+ }
+ // the key store a changed configuration names may hold its keys under any aliases
+ final Set<String> knownAliases =
+ current.configurationChanges == changes ? current.keyAliases : Collections.<String> emptySet();
+ try
+ {
+ final char[] pin = currentPIN();
+ final KeyStore keyStore = getKeystore(pin);
+ final Set<String> keyAliases = keyAliases(keyStore);
+ if (keyAliases.isEmpty())
+ {
+ throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(),
+ ERR_NO_KEY_ENTRY_IN_KEYSTORE.get(keyStoreFile));
+ }
+ if (!knownAliases.isEmpty() && Collections.disjoint(knownAliases, keyAliases))
+ {
+ throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(),
+ ERR_FILE_KEYMANAGER_NO_KNOWN_KEY_ALIAS.get(keyStoreFile, knownAliases));
+ }
+ final KeyManager[] keyManagers = loadKeyManagers(keyStore, pin);
+ if (keyManagers.length != 1 || !(keyManagers[0] instanceof X509ExtendedKeyManager))
+ {
+ throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(),
+ ERR_FILE_KEYMANAGER_CANNOT_CREATE_FACTORY.get(keyStoreFile, Arrays.toString(keyManagers)));
+ }
+ loaded = new LoadedKeyManager(changes, stamps, keyAliases, (X509ExtendedKeyManager) keyManagers[0]);
+ logger.info(NOTE_FILE_KEYMANAGER_RELOADED, keyStoreFile, currentConfig.dn());
+ }
+ catch (DirectoryException e)
+ {
+ logger.traceException(e);
+ loaded = new LoadedKeyManager(changes, stamps, knownAliases, current.keyManager);
+ logger.error(ERR_FILE_KEYMANAGER_CANNOT_RELOAD, keyStoreFile, currentConfig.dn(), e.getMessageObject());
+ }
+ return loaded.keyManager;
+ }
+ }
+
+ @Override
+ public String[] getClientAliases(String keyType, Principal[] issuers)
+ {
+ return currentKeyManager().getClientAliases(keyType, issuers);
+ }
+
+ @Override
+ public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket)
+ {
+ return currentKeyManager().chooseClientAlias(keyType, issuers, socket);
+ }
+
+ @Override
+ public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine)
+ {
+ return currentKeyManager().chooseEngineClientAlias(keyType, issuers, engine);
+ }
+
+ @Override
+ public String[] getServerAliases(String keyType, Principal[] issuers)
+ {
+ return currentKeyManager().getServerAliases(keyType, issuers);
+ }
+
+ @Override
+ public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket)
+ {
+ return currentKeyManager().chooseServerAlias(keyType, issuers, socket);
+ }
+
+ @Override
+ public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine)
+ {
+ return currentKeyManager().chooseEngineServerAlias(keyType, issuers, engine);
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias)
+ {
+ return loaded.keyManager.getCertificateChain(alias);
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias)
+ {
+ return loaded.keyManager.getPrivateKey(alias);
+ }
+ }
/**
* Creates a new instance of this file-based key manager provider. The
@@ -87,7 +262,7 @@
currentConfig = cfg;
keyStoreFile = getKeyStoreFile(cfg, ccr);
keyStoreType = getKeyStoreType(cfg, ccr);
- keyStorePIN = getKeyStorePIN(cfg, ccr);
+ getKeyStorePIN(cfg, ccr);
if (!ccr.getMessages().isEmpty())
{
throw new InitializationException(ccr.getMessages().get(0));
@@ -107,18 +282,9 @@
{
try
{
- KeyStore keyStore = getKeystore();
- Enumeration<String> aliases = keyStore.aliases();
- while (aliases.hasMoreElements())
- {
- String theAlias = aliases.nextElement();
- if (alias.equals(theAlias) && keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class))
- {
- return true;
- }
- }
+ return keyAliases(getKeystore(currentPIN())).contains(alias);
}
- catch (DirectoryException | KeyStoreException e)
+ catch (DirectoryException e)
{
// Ignore.
logger.traceException(e);
@@ -126,7 +292,22 @@
return false;
}
- private KeyStore getKeystore() throws DirectoryException
+ /**
+ * Returns the PIN the configuration names now, rather than the one it named when the provider
+ * was configured: a PIN file may have been renewed since, together with the key store.
+ */
+ private char[] currentPIN() throws DirectoryException
+ {
+ final ConfigChangeResult ccr = new ConfigChangeResult();
+ final char[] pin = getKeyStorePIN(currentConfig, ccr);
+ if (ccr.getResultCode() != ResultCode.SUCCESS)
+ {
+ throw new DirectoryException(ccr.getResultCode(), ccr.getMessages().get(0));
+ }
+ return pin;
+ }
+
+ private KeyStore getKeystore(char[] keyStorePIN) throws DirectoryException
{
try
{
@@ -145,19 +326,45 @@
}
}
+ /**
+ * {@inheritDoc}
+ * <p>
+ * The key manager returned reads the key store file again when a handshake starts after the
+ * file, or the PIN file, has changed, so that a renewed certificate is presented without
+ * restarting the server or the component using it.
+ */
@Override
public KeyManager[] getKeyManagers() throws DirectoryException
{
- KeyStore keyStore = getKeystore();
+ final int changes = configurationChanges;
+ final List<FileStamp> stamps = stampFiles();
+ final char[] pin = currentPIN();
+ final KeyStore keyStore = getKeystore(pin);
+ final Set<String> keyAliases = keyAliases(keyStore);
+ if (keyAliases.isEmpty())
+ {
+ // Troubleshooting message to let now of possible config error
+ logger.error(ERR_NO_KEY_ENTRY_IN_KEYSTORE, keyStoreFile);
+ }
+ final KeyManager[] keyManagers = loadKeyManagers(keyStore, pin);
+ if (keyManagers.length != 1 || !(keyManagers[0] instanceof X509ExtendedKeyManager))
+ {
+ return keyManagers;
+ }
+ return new KeyManager[] { new ReloadingKeyManager(
+ new LoadedKeyManager(changes, stamps, keyAliases, (X509ExtendedKeyManager) keyManagers[0])) };
+ }
+ private List<FileStamp> stampFiles()
+ {
+ final String pinFile = currentConfig.getKeyStorePinFile();
+ return FileStamp.of(getFileForPath(keyStoreFile), pinFile != null ? getFileForPath(pinFile) : null);
+ }
+
+ private KeyManager[] loadKeyManagers(KeyStore keyStore, char[] keyStorePIN) throws DirectoryException
+ {
try
{
- if (! findOneKeyEntry(keyStore))
- {
- // Troubleshooting message to let now of possible config error
- logger.error(ERR_NO_KEY_ENTRY_IN_KEYSTORE, keyStoreFile);
- }
-
String keyManagerAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(keyManagerAlgorithm);
keyManagerFactory.init(keyStore, keyStorePIN);
@@ -177,7 +384,7 @@
{
try
{
- return findOneKeyEntry(getKeystore());
+ return !keyAliases(getKeystore(currentPIN())).isEmpty();
}
catch (Exception e) {
logger.traceException(e);
@@ -185,18 +392,28 @@
}
}
- private boolean findOneKeyEntry(KeyStore keyStore) throws KeyStoreException
+ /** Returns the aliases the key store holds private keys under. */
+ private Set<String> keyAliases(KeyStore keyStore) throws DirectoryException
{
- Enumeration<String> aliases = keyStore.aliases();
- while (aliases.hasMoreElements())
+ try
{
- String alias = aliases.nextElement();
- if (keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class))
+ final Set<String> keyAliases = new TreeSet<>();
+ final Enumeration<String> aliases = keyStore.aliases();
+ while (aliases.hasMoreElements())
{
- return true;
+ final String alias = aliases.nextElement();
+ if (keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class))
+ {
+ keyAliases.add(alias);
+ }
}
+ return keyAliases;
}
- return false;
+ catch (KeyStoreException e)
+ {
+ LocalizableMessage message = ERR_FILE_KEYMANAGER_CANNOT_LOAD.get(keyStoreFile, getExceptionMessage(e));
+ throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), message, e);
+ }
}
@Override
@@ -227,14 +444,20 @@
final ConfigChangeResult ccr = new ConfigChangeResult();
String newKeyStoreFile = getKeyStoreFile(cfg, ccr);
String newKeyStoreType = getKeyStoreType(cfg, ccr);
- char[] newPIN = getKeyStorePIN(cfg, ccr);
+ getKeyStorePIN(cfg, ccr);
if (ccr.getResultCode() == ResultCode.SUCCESS)
{
- currentConfig = cfg;
- keyStorePIN = newPIN;
- keyStoreFile = newKeyStoreFile;
- keyStoreType = newKeyStoreType;
+ synchronized (this)
+ {
+ currentConfig = cfg;
+ keyStoreFile = newKeyStoreFile;
+ keyStoreType = newKeyStoreType;
+ // the key managers already handed out load the key store the new configuration names
+ // on their next handshake, even where its files are those they were loaded from, and
+ // whatever aliases it holds its keys under
+ configurationChanges++;
+ }
}
return ccr;
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/FileBasedTrustManagerProvider.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/FileBasedTrustManagerProvider.java
index 32b4ad7..5a6f4f8 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/FileBasedTrustManagerProvider.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/FileBasedTrustManagerProvider.java
@@ -13,6 +13,7 @@
*
* Copyright 2006-2010 Sun Microsystems, Inc.
* Portions Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.extensions;
@@ -20,11 +21,17 @@
import org.forgerock.i18n.LocalizableMessage;
import java.io.File;
import java.io.FileInputStream;
+import java.net.Socket;
import java.security.KeyStore;
import java.security.KeyStoreException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
import java.util.List;
+import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509ExtendedTrustManager;
import javax.net.ssl.X509TrustManager;
import org.forgerock.opendj.config.server.ConfigurationChangeListener;
@@ -56,9 +63,6 @@
{
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
- /** The PIN needed to access the trust store. */
- private char[] trustStorePIN;
-
/** The handle to the configuration for this trust manager. */
private FileBasedTrustManagerProviderCfg currentConfig;
@@ -69,6 +73,200 @@
private String trustStoreType;
/**
+ * The number of configuration changes applied. It is counted after the fields above are set,
+ * and read before them.
+ */
+ private volatile int configurationChanges;
+
+ /**
+ * A trust manager loaded from the trust store file, with the configuration change and the
+ * stamps of the files it was loaded under.
+ */
+ private static final class LoadedTrustManager
+ {
+ private final int configurationChanges;
+ private final List<FileStamp> stamps;
+ private final X509TrustManager trustManager;
+
+ private LoadedTrustManager(int configurationChanges, List<FileStamp> stamps, X509TrustManager trustManager)
+ {
+ this.configurationChanges = configurationChanges;
+ this.stamps = stamps;
+ this.trustManager = trustManager;
+ }
+
+ private boolean isLoadedFrom(int configurationChanges, List<FileStamp> stamps)
+ {
+ return this.configurationChanges == configurationChanges && this.stamps.equals(stamps);
+ }
+ }
+
+ /**
+ * What a trust manager handed out by {@link #getTrustManagers()} delegates to. Each trust
+ * manager handed out loads the file on its own, so asking the provider again, as a component
+ * does to check a configuration change, leaves those in use alone.
+ */
+ private final class TrustStoreFollower
+ {
+ /**
+ * Whether the server ran in FIPS mode when the trust manager was handed out. The trust store
+ * is loaded again as it was loaded then, so that what is loaded stays of the kind handed
+ * out, even where the server has turned to FIPS mode since, or away from it.
+ */
+ private final boolean fipsMode;
+ /** Whether the trust manager handed out is an extended one, which needs an extended one to delegate to. */
+ private final boolean extended;
+ /** The trust manager last loaded, when this one was handed out or by a check since. */
+ private volatile LoadedTrustManager loaded;
+
+ private TrustStoreFollower(boolean fipsMode, LoadedTrustManager loaded)
+ {
+ this.fipsMode = fipsMode;
+ this.extended = loaded.trustManager instanceof X509ExtendedTrustManager;
+ this.loaded = loaded;
+ }
+
+ /**
+ * Returns the trust manager to check a certificate with, first loading the trust store file
+ * again when it has changed since it was last loaded, or the configuration has. A file that
+ * cannot be loaded leaves the trust manager last loaded in use, and is not tried again until
+ * it changes again.
+ */
+ private X509TrustManager currentTrustManager()
+ {
+ LoadedTrustManager current = loaded;
+ if (current.isLoadedFrom(configurationChanges, stampFiles()))
+ {
+ return current.trustManager;
+ }
+ // the provider's lock, which applyConfigurationChange takes too: a load reads the
+ // configuration as it was before a change or after it, never a mix of both
+ synchronized (FileBasedTrustManagerProvider.this)
+ {
+ // stamped again under the lock: stamps taken before it may be those of a write another
+ // thread has loaded past meanwhile
+ final int changes = configurationChanges;
+ final List<FileStamp> stamps = stampFiles();
+ current = loaded;
+ if (current.isLoadedFrom(changes, stamps))
+ {
+ return current.trustManager;
+ }
+ try
+ {
+ final TrustManager[] trustManagers = loadTrustManagers(currentPIN(), fipsMode);
+ if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)
+ || extended != trustManagers[0] instanceof X509ExtendedTrustManager)
+ {
+ throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(),
+ ERR_FILE_TRUSTMANAGER_CANNOT_CREATE_FACTORY.get(trustStoreFile, Arrays.toString(trustManagers)));
+ }
+ loaded = new LoadedTrustManager(changes, stamps, (X509TrustManager) trustManagers[0]);
+ logger.info(NOTE_FILE_TRUSTMANAGER_RELOADED, trustStoreFile, currentConfig.dn());
+ }
+ catch (DirectoryException e)
+ {
+ logger.traceException(e);
+ loaded = new LoadedTrustManager(changes, stamps, current.trustManager);
+ logger.error(ERR_FILE_TRUSTMANAGER_CANNOT_RELOAD, trustStoreFile, currentConfig.dn(), e.getMessageObject());
+ }
+ return loaded.trustManager;
+ }
+ }
+ }
+
+ /** The trust manager handed out by {@link #getTrustManagers()} over a plain trust manager. */
+ private static final class ReloadingTrustManager implements X509TrustManager
+ {
+ private final TrustStoreFollower follower;
+
+ private ReloadingTrustManager(TrustStoreFollower follower)
+ {
+ this.follower = follower;
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException
+ {
+ follower.currentTrustManager().checkClientTrusted(chain, authType);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException
+ {
+ follower.currentTrustManager().checkServerTrusted(chain, authType);
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers()
+ {
+ return follower.currentTrustManager().getAcceptedIssuers();
+ }
+ }
+
+ /** The trust manager handed out by {@link #getTrustManagers()} over an extended trust manager. */
+ private static final class ReloadingExtendedTrustManager extends X509ExtendedTrustManager
+ {
+ private final TrustStoreFollower follower;
+
+ private ReloadingExtendedTrustManager(TrustStoreFollower follower)
+ {
+ this.follower = follower;
+ }
+
+ private X509ExtendedTrustManager current()
+ {
+ return (X509ExtendedTrustManager) follower.currentTrustManager();
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException
+ {
+ current().checkClientTrusted(chain, authType);
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket)
+ throws CertificateException
+ {
+ current().checkClientTrusted(chain, authType, socket);
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
+ throws CertificateException
+ {
+ current().checkClientTrusted(chain, authType, engine);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException
+ {
+ current().checkServerTrusted(chain, authType);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket)
+ throws CertificateException
+ {
+ current().checkServerTrusted(chain, authType, socket);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
+ throws CertificateException
+ {
+ current().checkServerTrusted(chain, authType, engine);
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers()
+ {
+ return current().getAcceptedIssuers();
+ }
+ }
+
+ /**
* Creates a new instance of this file-based trust manager provider. The
* <CODE>initializeTrustManagerProvider</CODE> method must be called on the
* resulting object before it may be used.
@@ -87,7 +285,7 @@
currentConfig = cfg;
trustStoreFile = getTrustStoreFile(cfg, ccr);
trustStoreType = getTrustStoreType(cfg, ccr);
- trustStorePIN = getTrustStorePIN(cfg, ccr);
+ getTrustStorePIN(cfg, ccr);
if (!ccr.getMessages().isEmpty())
{
throw new InitializationException(ccr.getMessages().get(0));
@@ -102,9 +300,59 @@
currentConfig.removeFileBasedChangeListener(this);
}
+ /**
+ * {@inheritDoc}
+ * <p>
+ * The trust manager returned reads the trust store file again when a certificate is checked
+ * after the file, or the PIN file, has changed, so that a renewed trust store is used without
+ * restarting the server or the component using it.
+ */
@Override
public TrustManager[] getTrustManagers() throws DirectoryException
{
+ final int changes = configurationChanges;
+ final List<FileStamp> stamps = stampFiles();
+ final boolean fipsMode = isFipsMode();
+ final TrustManager[] trustManagers = loadTrustManagers(currentPIN(), fipsMode);
+ if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager))
+ {
+ return trustManagers;
+ }
+ final TrustStoreFollower follower = new TrustStoreFollower(
+ fipsMode, new LoadedTrustManager(changes, stamps, (X509TrustManager) trustManagers[0]));
+ // an extended trust manager stays one, and a plain one stays plain, for JSSE adds checks
+ // of its own around a plain one
+ return new TrustManager[] { follower.extended
+ ? new ReloadingExtendedTrustManager(follower) : new ReloadingTrustManager(follower) };
+ }
+
+ /**
+ * Returns the PIN the configuration names now, rather than the one it named when the provider
+ * was configured: a PIN file may have been renewed since, together with the trust store.
+ */
+ private char[] currentPIN() throws DirectoryException
+ {
+ final ConfigChangeResult ccr = new ConfigChangeResult();
+ final char[] pin = getTrustStorePIN(currentConfig, ccr);
+ if (ccr.getResultCode() != ResultCode.SUCCESS)
+ {
+ throw new DirectoryException(ccr.getResultCode(), ccr.getMessages().get(0));
+ }
+ return pin;
+ }
+
+ private List<FileStamp> stampFiles()
+ {
+ final String pinFile = currentConfig.getTrustStorePinFile();
+ return FileStamp.of(getFileForPath(trustStoreFile), pinFile != null ? getFileForPath(pinFile) : null);
+ }
+
+ /**
+ * Loads the trust managers of the trust store file: in FIPS mode as they are, and otherwise each
+ * within an expiration check.
+ */
+ private TrustManager[] loadTrustManagers(char[] trustStorePIN, boolean fipsMode) throws DirectoryException
+ {
KeyStore trustStore;
try (FileInputStream inputStream = new FileInputStream(getFileForPath(trustStoreFile)))
{
@@ -125,7 +373,7 @@
trustManagerFactory.init(trustStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
TrustManager[] newTrustManagers = new TrustManager[trustManagers.length];
- if (isFips()) {
+ if (fipsMode) {
newTrustManagers = trustManagers;
} else {
for (int i=0; i < trustManagers.length; i++)
@@ -145,6 +393,17 @@
}
}
+ /**
+ * Tells whether the server runs in FIPS mode, where the trust managers loaded are handed out as
+ * they are, without an expiration check around them.
+ *
+ * @return {@code true} if the server runs in FIPS mode
+ */
+ boolean isFipsMode()
+ {
+ return isFips();
+ }
+
@Override
public boolean isConfigurationAcceptable(TrustManagerProviderCfg cfg, List<LocalizableMessage> unacceptableReasons)
{
@@ -173,14 +432,19 @@
final ConfigChangeResult ccr = new ConfigChangeResult();
String newTrustStoreFile = getTrustStoreFile(cfg, ccr);
String newTrustStoreType = getTrustStoreType(cfg, ccr);
- char[] newPIN = getTrustStorePIN(cfg, ccr);
+ getTrustStorePIN(cfg, ccr);
if (ccr.getResultCode() == ResultCode.SUCCESS)
{
- currentConfig = cfg;
- trustStorePIN = newPIN;
- trustStoreFile = newTrustStoreFile;
- trustStoreType = newTrustStoreType;
+ synchronized (this)
+ {
+ currentConfig = cfg;
+ trustStoreFile = newTrustStoreFile;
+ trustStoreType = newTrustStoreType;
+ // the trust managers already handed out load the trust store the new configuration
+ // names on their next check, even where its files are those they were loaded from
+ configurationChanges++;
+ }
}
return ccr;
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/FileStamp.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/FileStamp.java
new file mode 100644
index 0000000..1269ceb
--- /dev/null
+++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/FileStamp.java
@@ -0,0 +1,100 @@
+/*
+ * 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.extensions;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * What a file looked like when it was read: enough to tell, without reading it again, that it
+ * has been rewritten or replaced since. A file replaced by a rename, the way certificate
+ * renewal agents and Kubernetes update a mounted Secret, is a different file even when its
+ * size and modification time happen to match, which is why the file key is kept as well.
+ * Symbolic links are followed, so the stamp of a link describes the file it points to.
+ */
+final class FileStamp
+{
+ private final long lastModified;
+ private final long size;
+ private final Object fileKey;
+
+ private FileStamp(long lastModified, long size, Object fileKey)
+ {
+ this.lastModified = lastModified;
+ this.size = size;
+ this.fileKey = fileKey;
+ }
+
+ /**
+ * Returns the stamps of the provided files, in the same order. Every file that is missing or
+ * cannot be looked at gets the same stamp.
+ *
+ * @param files
+ * The files to stamp; {@code null} elements are skipped.
+ * @return The stamps of the files.
+ */
+ static List<FileStamp> of(File... files)
+ {
+ final List<FileStamp> stamps = new ArrayList<>(files.length);
+ for (File file : files)
+ {
+ if (file != null)
+ {
+ stamps.add(of(file));
+ }
+ }
+ return stamps;
+ }
+
+ private static FileStamp of(File file)
+ {
+ try
+ {
+ final BasicFileAttributes attributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
+ return new FileStamp(attributes.lastModifiedTime().toMillis(), attributes.size(), attributes.fileKey());
+ }
+ catch (IOException | SecurityException e)
+ {
+ return new FileStamp(-1, -1, null);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o)
+ {
+ return true;
+ }
+ if (!(o instanceof FileStamp))
+ {
+ return false;
+ }
+ final FileStamp other = (FileStamp) o;
+ return lastModified == other.lastModified && size == other.size && Objects.equals(fileKey, other.fileKey);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(lastModified, size, fileKey);
+ }
+}
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/extension.properties b/opendj-server-legacy/src/messages/org/opends/messages/extension.properties
index 84aa00c..29ea8ff 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/extension.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/extension.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2010 Sun Microsystems, Inc.
# Portions Copyright 2011-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
@@ -1003,3 +1004,15 @@
attribute ds-cfg-trust-store-pin-file of configuration entry %s should \
contain the PIN needed to access the LDAP trust manager, but this file \
is empty
+NOTE_FILE_KEYMANAGER_RELOADED_652=The keystore file %s used by key manager \
+ provider %s has changed and was loaded again
+ERR_FILE_KEYMANAGER_CANNOT_RELOAD_653=The keystore file %s used by key \
+ manager provider %s has changed but could not be loaded again: %s. The \
+ provider keeps using what it last loaded from it
+NOTE_FILE_TRUSTMANAGER_RELOADED_654=The trust store file %s used by trust \
+ manager provider %s has changed and was loaded again
+ERR_FILE_TRUSTMANAGER_CANNOT_RELOAD_655=The trust store file %s used by \
+ trust manager provider %s has changed but could not be loaded again: %s. \
+ The provider keeps using what it last loaded from it
+ERR_FILE_KEYMANAGER_NO_KNOWN_KEY_ALIAS_656=The keystore file %s holds no \
+ private key under any of the aliases %s it held private keys under before
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/extensions/FileBasedKeyManagerProviderTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/extensions/FileBasedKeyManagerProviderTestCase.java
index 7702dce..da0b425 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/extensions/FileBasedKeyManagerProviderTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/extensions/FileBasedKeyManagerProviderTestCase.java
@@ -13,12 +13,29 @@
*
* Copyright 2006-2008 Sun Microsystems, Inc.
* Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.extensions;
+import java.io.ByteArrayOutputStream;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileWriter;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
+import java.util.function.Function;
+
+import javax.net.ssl.X509ExtendedKeyManager;
+
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -30,6 +47,7 @@
import org.opends.server.types.Entry;
import org.opends.server.types.InitializationException;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.opends.server.util.ServerConstants.*;
/**
@@ -289,6 +307,358 @@
initializeKeyManagerProvider(e);
}
+ /**
+ * A key manager handed out by the provider presents what the key store file holds when a
+ * handshake starts, not what it held when the key manager was handed out: a file replaced
+ * with another certificate, or with the same certificate under another PIN, is loaded again.
+ * A file that cannot be loaded, that holds no private key, or that holds none under the
+ * aliases the file held keys under before leaves the last certificate loaded in use.
+ */
+ @Test
+ public void testKeyStoreLoadedAgainWhenChanged() throws Exception
+ {
+ final File configDir = new File(DirectoryServer.getInstanceRoot(), "config");
+ final File keyStore = new File(configDir, "reload-test.keystore");
+ final File pinFile = new File(configDir, "reload-test.keystore.pin");
+ replace(keyStore, Files.readAllBytes(new File(configDir, "server.keystore").toPath()));
+ replace(pinFile, ("password" + EOL).getBytes(StandardCharsets.UTF_8));
+
+ FileBasedKeyManagerProvider provider = initializeKeyManagerProvider(TestCaseUtils.makeEntry(
+ "dn: cn=Reloaded Key Manager Provider,cn=SSL,cn=config",
+ "objectClass: top",
+ "objectClass: ds-cfg-key-manager-provider",
+ "objectClass: ds-cfg-file-based-key-manager-provider",
+ "cn: Reloaded Key Manager Provider",
+ "ds-cfg-java-class: org.opends.server.extensions.FileBasedKeyManagerProvider",
+ "ds-cfg-enabled: true",
+ "ds-cfg-key-store-file: config/reload-test.keystore",
+ "ds-cfg-key-store-pin-file: config/reload-test.keystore.pin"));
+ try
+ {
+ final X509ExtendedKeyManager keyManager = (X509ExtendedKeyManager) provider.getKeyManagers()[0];
+ final String serverCertificate = serverCertificateOf(keyManager);
+
+ // renewed under the alias the handler is configured with
+ replace(keyStore, rewritten(new File(configDir, "client.keystore"), "password", "password", "server-cert"));
+ final String clientCertificate = serverCertificateByAliasesOf(keyManager);
+ assertThat(clientCertificate).isNotEqualTo(serverCertificate);
+
+ // a handler presenting the key under its ssl-cert-nickname would find none in these
+ replace(keyStore, rewritten(new File(configDir, "server.keystore"), "password", "password", "other-cert"));
+ assertThat(serverCertificateOf(keyManager)).isEqualTo(clientCertificate);
+ replace(keyStore, Files.readAllBytes(new File(configDir, "server.truststore").toPath()));
+ assertThat(serverCertificateOf(keyManager)).isEqualTo(clientCertificate);
+
+ // the PIN changes with the key store, but the new PIN is not there yet
+ replace(keyStore, withPassword(new File(configDir, "server.keystore"), "password", "changed"));
+ assertThat(serverCertificateOf(keyManager)).isEqualTo(clientCertificate);
+ replace(pinFile, ("changed" + EOL).getBytes(StandardCharsets.UTF_8));
+ assertThat(serverCertificateOf(keyManager)).isEqualTo(serverCertificate);
+
+ replace(keyStore, "not a key store".getBytes(StandardCharsets.UTF_8));
+ assertThat(serverCertificateOf(keyManager)).isEqualTo(serverCertificate);
+
+ // the failed load above kept the aliases the last key store loaded holds its key under
+ replace(keyStore, rewritten(new File(configDir, "client.keystore"), "password", "changed", "other-cert"));
+ assertThat(serverCertificateOf(keyManager)).isEqualTo(serverCertificate);
+ }
+ finally
+ {
+ provider.finalizeKeyManagerProvider();
+ Files.deleteIfExists(keyStore.toPath());
+ Files.deleteIfExists(pinFile.toPath());
+ }
+ }
+
+ /**
+ * Each key manager handed out loads the key store on its own. Asking the provider again, as a
+ * connection handler does to check a configuration change, leaves the key managers handed out
+ * before alone, and the new key manager presents what the file holds: the key under the alias
+ * a handler is being configured with, which the key managers in use refused.
+ */
+ @Test
+ public void testKeyManagerHandedOutAgainLeavesEarlierOnesAlone() throws Exception
+ {
+ final File configDir = new File(DirectoryServer.getInstanceRoot(), "config");
+ final File keyStore = new File(configDir, "handed-out-test.keystore");
+ replace(keyStore, Files.readAllBytes(new File(configDir, "server.keystore").toPath()));
+ FileBasedKeyManagerProvider provider = initializeKeyManagerProvider(TestCaseUtils.makeEntry(
+ "dn: cn=Handed Out Key Manager Provider,cn=SSL,cn=config",
+ "objectClass: top",
+ "objectClass: ds-cfg-key-manager-provider",
+ "objectClass: ds-cfg-file-based-key-manager-provider",
+ "cn: Handed Out Key Manager Provider",
+ "ds-cfg-java-class: org.opends.server.extensions.FileBasedKeyManagerProvider",
+ "ds-cfg-enabled: true",
+ "ds-cfg-key-store-file: config/handed-out-test.keystore",
+ "ds-cfg-key-store-pin: password"));
+ try
+ {
+ final X509ExtendedKeyManager keyManager = (X509ExtendedKeyManager) provider.getKeyManagers()[0];
+ final String serverCertificate = serverCertificateOf(keyManager);
+
+ // renewed under an alias the key manager in use does not know: refused
+ replace(keyStore, rewritten(new File(configDir, "client.keystore"), "password", "password", "other-cert"));
+ assertThat(serverCertificateOf(keyManager)).isEqualTo(serverCertificate);
+
+ final X509ExtendedKeyManager renamed = (X509ExtendedKeyManager) provider.getKeyManagers()[0];
+ assertThat(renamed.getServerAliases("RSA", null)).containsExactly("other-cert");
+ assertThat(serverCertificateByAliasesOf(renamed)).isNotEqualTo(serverCertificate);
+ assertThat(serverCertificateOf(keyManager)).isEqualTo(serverCertificate);
+ }
+ finally
+ {
+ provider.finalizeKeyManagerProvider();
+ Files.deleteIfExists(keyStore.toPath());
+ }
+ }
+
+ /**
+ * Every way a key manager handed out chooses an alias looks at the key store file first: the
+ * client side, which the OAuth2 client uses, and the server side over a socket, which JMX uses.
+ */
+ @Test
+ public void testEveryAliasChoiceLoadsAgain() throws Exception
+ {
+ final File configDir = new File(DirectoryServer.getInstanceRoot(), "config");
+ final File keyStore = new File(configDir, "alias-choice-test.keystore");
+ replace(keyStore, Files.readAllBytes(new File(configDir, "server.keystore").toPath()));
+ FileBasedKeyManagerProvider provider = initializeKeyManagerProvider(TestCaseUtils.makeEntry(
+ "dn: cn=Alias Choice Key Manager Provider,cn=SSL,cn=config",
+ "objectClass: top",
+ "objectClass: ds-cfg-key-manager-provider",
+ "objectClass: ds-cfg-file-based-key-manager-provider",
+ "cn: Alias Choice Key Manager Provider",
+ "ds-cfg-java-class: org.opends.server.extensions.FileBasedKeyManagerProvider",
+ "ds-cfg-enabled: true",
+ "ds-cfg-key-store-file: config/alias-choice-test.keystore",
+ "ds-cfg-key-store-pin: password"));
+ try
+ {
+ final X509ExtendedKeyManager keyManager = (X509ExtendedKeyManager) provider.getKeyManagers()[0];
+ final String serverCertificate = serverCertificateOf(keyManager);
+ final byte[] serverKeyStore = Files.readAllBytes(keyStore.toPath());
+ final byte[] clientKeyStore =
+ rewritten(new File(configDir, "client.keystore"), "password", "password", "server-cert");
+ final String[] rsa = { "RSA" };
+ final List<Function<X509ExtendedKeyManager, String>> choices = Arrays.asList(
+ km -> km.getClientAliases("RSA", null)[0],
+ km -> km.chooseClientAlias(rsa, null, null),
+ km -> km.chooseEngineClientAlias(rsa, null, null),
+ km -> km.chooseServerAlias("RSA", null, null));
+
+ boolean client = false;
+ String previous = serverCertificate;
+ for (Function<X509ExtendedKeyManager, String> choice : choices)
+ {
+ client = !client;
+ replace(keyStore, client ? clientKeyStore : serverKeyStore);
+ // server.keystore holds ads-certificate too, which a client side choice may take
+ final String alias = choice.apply(keyManager);
+ assertThat(alias).isNotNull();
+ final String certificate = keyManager.getCertificateChain(alias)[0].getSubjectX500Principal().getName();
+ assertThat(certificate).isNotEqualTo(previous);
+ previous = certificate;
+ }
+ }
+ finally
+ {
+ provider.finalizeKeyManagerProvider();
+ Files.deleteIfExists(keyStore.toPath());
+ }
+ }
+
+ /**
+ * A changed configuration makes the key managers already handed out load the key store again
+ * on their next handshake, even where the file has not changed since they last looked at it.
+ */
+ @Test
+ public void testKeyStoreLoadedAgainWhenPinChangesInConfiguration() throws Exception
+ {
+ final File configDir = new File(DirectoryServer.getInstanceRoot(), "config");
+ final File keyStore = new File(configDir, "reconfigured-test.keystore");
+ replace(keyStore, Files.readAllBytes(new File(configDir, "server.keystore").toPath()));
+ FileBasedKeyManagerProvider provider = initializeKeyManagerProvider(reconfiguredProviderEntry("password"));
+ try
+ {
+ final X509ExtendedKeyManager keyManager = (X509ExtendedKeyManager) provider.getKeyManagers()[0];
+ final String serverCertificate = serverCertificateOf(keyManager);
+
+ // a key store under a PIN the configuration does not have yet: the failed load records its stamps
+ replace(keyStore, withPassword(new File(configDir, "client.keystore"), "password", "changed"));
+ assertThat(serverCertificateOf(keyManager)).isEqualTo(serverCertificate);
+
+ // and whatever aliases the key store the new configuration names holds its keys under
+ provider.applyConfigurationChange(InitializationUtils.getConfiguration(
+ FileBasedKeyManagerProviderCfgDefn.getInstance(), reconfiguredProviderEntry("changed")));
+ final String clientCertificate = serverCertificateOf(keyManager);
+ assertThat(clientCertificate).isNotEqualTo(serverCertificate);
+
+ // with no aliases to hold a store to, a store with no private key is still not taken
+ provider.applyConfigurationChange(InitializationUtils.getConfiguration(
+ FileBasedKeyManagerProviderCfgDefn.getInstance(), reconfiguredProviderEntry("changed")));
+ replace(keyStore, withPassword(new File(configDir, "server.truststore"), "password", "changed"));
+ assertThat(serverCertificateOf(keyManager)).isEqualTo(clientCertificate);
+ }
+ finally
+ {
+ provider.finalizeKeyManagerProvider();
+ Files.deleteIfExists(keyStore.toPath());
+ }
+ }
+
+ private static Entry reconfiguredProviderEntry(String pin) throws Exception
+ {
+ return TestCaseUtils.makeEntry(
+ "dn: cn=Reconfigured Key Manager Provider,cn=SSL,cn=config",
+ "objectClass: top",
+ "objectClass: ds-cfg-key-manager-provider",
+ "objectClass: ds-cfg-file-based-key-manager-provider",
+ "cn: Reconfigured Key Manager Provider",
+ "ds-cfg-java-class: org.opends.server.extensions.FileBasedKeyManagerProvider",
+ "ds-cfg-enabled: true",
+ "ds-cfg-key-store-file: config/reconfigured-test.keystore",
+ "ds-cfg-key-store-pin: " + pin);
+ }
+
+ /**
+ * A key store file that cannot be loaded is not tried again until it changes again, even where
+ * it would load by then: the PIN below comes from a system property, which is not stamped.
+ */
+ @Test
+ public void testKeyStoreNotLoadedAgainUntilChanged() throws Exception
+ {
+ final File configDir = new File(DirectoryServer.getInstanceRoot(), "config");
+ final File keyStore = new File(configDir, "retry-test.keystore");
+ replace(keyStore, Files.readAllBytes(new File(configDir, "server.keystore").toPath()));
+ System.setProperty("retry.test.key.store.pin", "password");
+ FileBasedKeyManagerProvider provider = initializeKeyManagerProvider(TestCaseUtils.makeEntry(
+ "dn: cn=Retried Key Manager Provider,cn=SSL,cn=config",
+ "objectClass: top",
+ "objectClass: ds-cfg-key-manager-provider",
+ "objectClass: ds-cfg-file-based-key-manager-provider",
+ "cn: Retried Key Manager Provider",
+ "ds-cfg-java-class: org.opends.server.extensions.FileBasedKeyManagerProvider",
+ "ds-cfg-enabled: true",
+ "ds-cfg-key-store-file: config/retry-test.keystore",
+ "ds-cfg-key-store-pin-property: retry.test.key.store.pin"));
+ try
+ {
+ final X509ExtendedKeyManager keyManager = (X509ExtendedKeyManager) provider.getKeyManagers()[0];
+ final String serverCertificate = serverCertificateOf(keyManager);
+
+ replace(keyStore, rewritten(new File(configDir, "client.keystore"), "password", "changed", "server-cert"));
+ assertThat(serverCertificateOf(keyManager)).isEqualTo(serverCertificate);
+ System.setProperty("retry.test.key.store.pin", "changed");
+ assertThat(serverCertificateOf(keyManager)).isEqualTo(serverCertificate);
+ }
+ finally
+ {
+ provider.finalizeKeyManagerProvider();
+ System.clearProperty("retry.test.key.store.pin");
+ Files.deleteIfExists(keyStore.toPath());
+ }
+ }
+
+ /**
+ * A key store renewed together with its PIN file is loaded with the new PIN by the provider
+ * itself, as a connection handler rebuilding its SSL context asks it to, with no handshake in
+ * between to read the PIN again.
+ */
+ @Test
+ public void testKeyStoreAndPinRenewedTogetherLoadedWithoutHandshake() throws Exception
+ {
+ final File configDir = new File(DirectoryServer.getInstanceRoot(), "config");
+ final File keyStore = new File(configDir, "renewed-test.keystore");
+ final File pinFile = new File(configDir, "renewed-test.keystore.pin");
+ replace(keyStore, Files.readAllBytes(new File(configDir, "server.keystore").toPath()));
+ replace(pinFile, ("password" + EOL).getBytes(StandardCharsets.UTF_8));
+ FileBasedKeyManagerProvider provider = initializeKeyManagerProvider(TestCaseUtils.makeEntry(
+ "dn: cn=Renewed Key Manager Provider,cn=SSL,cn=config",
+ "objectClass: top",
+ "objectClass: ds-cfg-key-manager-provider",
+ "objectClass: ds-cfg-file-based-key-manager-provider",
+ "cn: Renewed Key Manager Provider",
+ "ds-cfg-java-class: org.opends.server.extensions.FileBasedKeyManagerProvider",
+ "ds-cfg-enabled: true",
+ "ds-cfg-key-store-file: config/renewed-test.keystore",
+ "ds-cfg-key-store-pin-file: config/renewed-test.keystore.pin"));
+ try
+ {
+ final String serverCertificate = serverCertificateOf((X509ExtendedKeyManager) provider.getKeyManagers()[0]);
+
+ replace(keyStore, rewritten(new File(configDir, "client.keystore"), "password", "changed", "server-cert"));
+ replace(pinFile, ("changed" + EOL).getBytes(StandardCharsets.UTF_8));
+ assertThat(provider.containsAtLeastOneKey()).isTrue();
+ assertThat(provider.containsKeyWithAlias("server-cert")).isTrue();
+ final X509ExtendedKeyManager keyManager = (X509ExtendedKeyManager) provider.getKeyManagers()[0];
+ assertThat(serverCertificateOf(keyManager)).isNotEqualTo(serverCertificate);
+ }
+ finally
+ {
+ provider.finalizeKeyManagerProvider();
+ Files.deleteIfExists(keyStore.toPath());
+ Files.deleteIfExists(pinFile.toPath());
+ }
+ }
+
+ /** The road LDAPS takes with ssl-cert-nickname set: SelectableCertificateKeyManager asks getServerAliases. */
+ private static String serverCertificateByAliasesOf(X509ExtendedKeyManager keyManager)
+ {
+ final String[] aliases = keyManager.getServerAliases("RSA", null);
+ assertThat(aliases).isNotEmpty();
+ return keyManager.getCertificateChain(aliases[0])[0].getSubjectX500Principal().getName();
+ }
+
+ private static String serverCertificateOf(X509ExtendedKeyManager keyManager)
+ {
+ final String alias = keyManager.chooseEngineServerAlias("RSA", null, null);
+ assertThat(alias).isNotNull();
+ return keyManager.getCertificateChain(alias)[0].getSubjectX500Principal().getName();
+ }
+
+ /** Returns the key store in the provided file, with its entries protected by another password. */
+ static byte[] withPassword(File file, String oldPassword, String newPassword) throws Exception
+ {
+ return rewritten(file, oldPassword, newPassword, null);
+ }
+
+ /**
+ * Returns the key store in the provided file, with its entries protected by another password
+ * and, unless the alias provided is {@code null}, its private keys under that alias.
+ */
+ private static byte[] rewritten(File file, String oldPassword, String newPassword, String keyAlias)
+ throws Exception
+ {
+ final KeyStore keyStore = KeyStore.getInstance("JKS");
+ try (InputStream in = new FileInputStream(file))
+ {
+ keyStore.load(in, oldPassword.toCharArray());
+ }
+ for (String alias : Collections.list(keyStore.aliases()))
+ {
+ if (!keyStore.isKeyEntry(alias))
+ {
+ continue;
+ }
+ final Key key = keyStore.getKey(alias, oldPassword.toCharArray());
+ final Certificate[] chain = keyStore.getCertificateChain(alias);
+ keyStore.deleteEntry(alias);
+ keyStore.setKeyEntry(keyAlias != null ? keyAlias : alias, key, newPassword.toCharArray(), chain);
+ }
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ keyStore.store(out, newPassword.toCharArray());
+ return out.toByteArray();
+ }
+
+ /** Replaces the file the way a renewal agent does: the new content is renamed over it. */
+ static void replace(File file, byte[] content) throws Exception
+ {
+ final Path tmp = Files.createTempFile(file.getParentFile().toPath(), file.getName(), ".tmp");
+ Files.write(tmp, content);
+ Files.move(tmp, file.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+ }
+
private FileBasedKeyManagerProvider initializeKeyManagerProvider(Entry e) throws Exception {
return InitializationUtils.initializeKeyManagerProvider(
new FileBasedKeyManagerProvider(), e, FileBasedKeyManagerProviderCfgDefn.getInstance());
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/extensions/FileBasedTrustManagerProviderTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/extensions/FileBasedTrustManagerProviderTestCase.java
index 4ea4ece..f942052 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/extensions/FileBasedTrustManagerProviderTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/extensions/FileBasedTrustManagerProviderTestCase.java
@@ -13,13 +13,31 @@
*
* Copyright 2006-2008 Sun Microsystems, Inc.
* Portions Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.extensions;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileWriter;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.security.KeyStore;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
import java.util.List;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509ExtendedTrustManager;
+import javax.net.ssl.X509TrustManager;
+
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.server.config.meta.FileBasedTrustManagerProviderCfgDefn;
import org.opends.server.TestCaseUtils;
@@ -30,6 +48,11 @@
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
+import static com.forgerock.opendj.util.StaticUtils.isFips;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.opends.server.extensions.FileBasedKeyManagerProviderTestCase.replace;
+import static org.opends.server.extensions.FileBasedKeyManagerProviderTestCase.withPassword;
import static org.opends.server.util.ServerConstants.*;
/**
@@ -281,6 +304,402 @@
}
}
+ /**
+ * A trust manager handed out by the provider checks certificates against what the trust
+ * store file holds at the time of the check: a file replaced with other certificates, or with
+ * the same certificates under another PIN, is loaded again, and a file that cannot be loaded
+ * leaves the last certificates loaded in use.
+ */
+ @Test
+ public void testTrustStoreLoadedAgainWhenChanged() throws Exception
+ {
+ final File configDir = new File(DirectoryServer.getInstanceRoot(), "config");
+ final File trustStore = new File(configDir, "reload-test.truststore");
+ final File pinFile = new File(configDir, "reload-test.truststore.pin");
+ replace(trustStore, Files.readAllBytes(new File(configDir, "server.truststore").toPath()));
+ replace(pinFile, ("password" + EOL).getBytes(StandardCharsets.UTF_8));
+
+ FileBasedTrustManagerProvider provider = initializeTrustManagerProvider(TestCaseUtils.makeEntry(
+ "dn: cn=Reloaded Trust Manager Provider,cn=SSL,cn=config",
+ "objectClass: top",
+ "objectClass: ds-cfg-trust-manager-provider",
+ "objectClass: ds-cfg-file-based-trust-manager-provider",
+ "cn: Reloaded Trust Manager Provider",
+ "ds-cfg-java-class: org.opends.server.extensions.FileBasedTrustManagerProvider",
+ "ds-cfg-enabled: true",
+ "ds-cfg-trust-store-file: config/reload-test.truststore",
+ "ds-cfg-trust-store-pin-file: config/reload-test.truststore.pin"));
+ try
+ {
+ final X509TrustManager trustManager = (X509TrustManager) provider.getTrustManagers()[0];
+ // a plain trust manager stays plain, for JSSE adds checks of its own around it; outside
+ // FIPS mode the provider wraps what it loads in a plain ExpirationCheckTrustManager
+ assertThat(trustManager instanceof X509ExtendedTrustManager).isEqualTo(isFips());
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(3);
+
+ // the PIN changes with the trust store, but the new PIN is not there yet
+ replace(trustStore, withPassword(new File(configDir, "client.truststore"), "password", "changed"));
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(3);
+ replace(pinFile, ("changed" + EOL).getBytes(StandardCharsets.UTF_8));
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(2);
+
+ replace(trustStore, "not a trust store".getBytes(StandardCharsets.UTF_8));
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(2);
+ }
+ finally
+ {
+ provider.finalizeTrustManagerProvider();
+ Files.deleteIfExists(trustStore.toPath());
+ Files.deleteIfExists(pinFile.toPath());
+ }
+ }
+
+ /**
+ * A trust store file that cannot be loaded is not tried again until it changes again, even
+ * where it would load by then: the PIN below comes from a system property, which is not stamped.
+ */
+ @Test
+ public void testTrustStoreNotLoadedAgainUntilChanged() throws Exception
+ {
+ final File configDir = new File(DirectoryServer.getInstanceRoot(), "config");
+ final File trustStore = new File(configDir, "retry-test.truststore");
+ replace(trustStore, Files.readAllBytes(new File(configDir, "server.truststore").toPath()));
+ System.setProperty("retry.test.trust.store.pin", "password");
+ FileBasedTrustManagerProvider provider = initializeTrustManagerProvider(TestCaseUtils.makeEntry(
+ "dn: cn=Retried Trust Manager Provider,cn=SSL,cn=config",
+ "objectClass: top",
+ "objectClass: ds-cfg-trust-manager-provider",
+ "objectClass: ds-cfg-file-based-trust-manager-provider",
+ "cn: Retried Trust Manager Provider",
+ "ds-cfg-java-class: org.opends.server.extensions.FileBasedTrustManagerProvider",
+ "ds-cfg-enabled: true",
+ "ds-cfg-trust-store-file: config/retry-test.truststore",
+ "ds-cfg-trust-store-pin-property: retry.test.trust.store.pin"));
+ try
+ {
+ final X509TrustManager trustManager = (X509TrustManager) provider.getTrustManagers()[0];
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(3);
+
+ replace(trustStore, withPassword(new File(configDir, "client.truststore"), "password", "changed"));
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(3);
+ System.setProperty("retry.test.trust.store.pin", "changed");
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(3);
+ }
+ finally
+ {
+ provider.finalizeTrustManagerProvider();
+ System.clearProperty("retry.test.trust.store.pin");
+ Files.deleteIfExists(trustStore.toPath());
+ }
+ }
+
+ /**
+ * A trust store renewed together with its PIN file is loaded with the new PIN by the provider
+ * itself, as a component rebuilding its SSL context asks it to, with no certificate checked in
+ * between to read the PIN again.
+ */
+ @Test
+ public void testTrustStoreAndPinRenewedTogetherLoadedWithoutCheck() throws Exception
+ {
+ final File configDir = new File(DirectoryServer.getInstanceRoot(), "config");
+ final File trustStore = new File(configDir, "renewed-test.truststore");
+ final File pinFile = new File(configDir, "renewed-test.truststore.pin");
+ replace(trustStore, Files.readAllBytes(new File(configDir, "server.truststore").toPath()));
+ replace(pinFile, ("password" + EOL).getBytes(StandardCharsets.UTF_8));
+ FileBasedTrustManagerProvider provider = initializeTrustManagerProvider(TestCaseUtils.makeEntry(
+ "dn: cn=Renewed Trust Manager Provider,cn=SSL,cn=config",
+ "objectClass: top",
+ "objectClass: ds-cfg-trust-manager-provider",
+ "objectClass: ds-cfg-file-based-trust-manager-provider",
+ "cn: Renewed Trust Manager Provider",
+ "ds-cfg-java-class: org.opends.server.extensions.FileBasedTrustManagerProvider",
+ "ds-cfg-enabled: true",
+ "ds-cfg-trust-store-file: config/renewed-test.truststore",
+ "ds-cfg-trust-store-pin-file: config/renewed-test.truststore.pin"));
+ try
+ {
+ assertThat(((X509TrustManager) provider.getTrustManagers()[0]).getAcceptedIssuers()).hasSize(3);
+
+ replace(trustStore, withPassword(new File(configDir, "client.truststore"), "password", "changed"));
+ replace(pinFile, ("changed" + EOL).getBytes(StandardCharsets.UTF_8));
+ assertThat(((X509TrustManager) provider.getTrustManagers()[0]).getAcceptedIssuers()).hasSize(2);
+ }
+ finally
+ {
+ provider.finalizeTrustManagerProvider();
+ Files.deleteIfExists(trustStore.toPath());
+ Files.deleteIfExists(pinFile.toPath());
+ }
+ }
+
+ /**
+ * A changed configuration makes the trust managers already handed out load the trust store
+ * again on their next check, even where the file has not changed since they last looked at it.
+ */
+ @Test
+ public void testTrustStoreLoadedAgainWhenPinChangesInConfiguration() throws Exception
+ {
+ final File configDir = new File(DirectoryServer.getInstanceRoot(), "config");
+ final File trustStore = new File(configDir, "reconfigured-test.truststore");
+ replace(trustStore, Files.readAllBytes(new File(configDir, "server.truststore").toPath()));
+ FileBasedTrustManagerProvider provider = initializeTrustManagerProvider(
+ providerEntry("Reconfigured", "reconfigured-test.truststore", "password"));
+ try
+ {
+ final X509TrustManager trustManager = (X509TrustManager) provider.getTrustManagers()[0];
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(3);
+
+ // a trust store under a PIN the configuration does not have yet: the failed load records its stamps
+ replace(trustStore, withPassword(new File(configDir, "client.truststore"), "password", "changed"));
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(3);
+
+ provider.applyConfigurationChange(InitializationUtils.getConfiguration(
+ FileBasedTrustManagerProviderCfgDefn.getInstance(),
+ providerEntry("Reconfigured", "reconfigured-test.truststore", "changed")));
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(2);
+ }
+ finally
+ {
+ provider.finalizeTrustManagerProvider();
+ Files.deleteIfExists(trustStore.toPath());
+ }
+ }
+
+ /**
+ * Each trust manager handed out loads the trust store on its own. Asking the provider again, as
+ * a component does to check a configuration change, leaves the trust managers handed out before
+ * alone: the one below keeps the trust store it last loaded, for its file has not changed since
+ * it failed to load it.
+ */
+ @Test
+ public void testTrustManagerHandedOutAgainLeavesEarlierOnesAlone() throws Exception
+ {
+ final File configDir = new File(DirectoryServer.getInstanceRoot(), "config");
+ final File trustStore = new File(configDir, "handed-out-test.truststore");
+ replace(trustStore, Files.readAllBytes(new File(configDir, "server.truststore").toPath()));
+ System.setProperty("handed.out.test.trust.store.pin", "password");
+ FileBasedTrustManagerProvider provider = initializeTrustManagerProvider(TestCaseUtils.makeEntry(
+ "dn: cn=Handed Out Trust Manager Provider,cn=SSL,cn=config",
+ "objectClass: top",
+ "objectClass: ds-cfg-trust-manager-provider",
+ "objectClass: ds-cfg-file-based-trust-manager-provider",
+ "cn: Handed Out Trust Manager Provider",
+ "ds-cfg-java-class: org.opends.server.extensions.FileBasedTrustManagerProvider",
+ "ds-cfg-enabled: true",
+ "ds-cfg-trust-store-file: config/handed-out-test.truststore",
+ "ds-cfg-trust-store-pin-property: handed.out.test.trust.store.pin"));
+ try
+ {
+ final X509TrustManager trustManager = (X509TrustManager) provider.getTrustManagers()[0];
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(3);
+
+ replace(trustStore, withPassword(new File(configDir, "client.truststore"), "password", "changed"));
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(3);
+ System.setProperty("handed.out.test.trust.store.pin", "changed");
+
+ assertThat(((X509TrustManager) provider.getTrustManagers()[0]).getAcceptedIssuers()).hasSize(2);
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(3);
+ }
+ finally
+ {
+ provider.finalizeTrustManagerProvider();
+ System.clearProperty("handed.out.test.trust.store.pin");
+ Files.deleteIfExists(trustStore.toPath());
+ }
+ }
+
+ /**
+ * A plain trust manager handed out outside FIPS mode loads the trust store as it was loaded
+ * then, within an expiration check, after the server has turned to FIPS mode, and after it has
+ * turned away from it again: a renewal is taken either way.
+ */
+ @Test
+ public void testPlainTrustManagerLoadedAgainAsHandedOutWhenFipsModeChanges() throws Exception
+ {
+ final File configDir = new File(DirectoryServer.getInstanceRoot(), "config");
+ final File trustStore = new File(configDir, "fips-on-test.truststore");
+ replace(trustStore, Files.readAllBytes(new File(configDir, "server.truststore").toPath()));
+ final FipsSwitchedTrustManagerProvider provider = InitializationUtils.initializeTrustManagerProvider(
+ new FipsSwitchedTrustManagerProvider(), providerEntry("FIPS On", "fips-on-test.truststore", "password"),
+ FileBasedTrustManagerProviderCfgDefn.getInstance());
+ try
+ {
+ final X509TrustManager trustManager = (X509TrustManager) provider.getTrustManagers()[0];
+ assertThat(trustManager).isNotInstanceOf(X509ExtendedTrustManager.class);
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(3);
+
+ provider.fips = true;
+ replace(trustStore, Files.readAllBytes(new File(configDir, "client.truststore").toPath()));
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(2);
+
+ provider.fips = false;
+ replace(trustStore, Files.readAllBytes(new File(configDir, "server.truststore").toPath()));
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(3);
+ }
+ finally
+ {
+ provider.finalizeTrustManagerProvider();
+ Files.deleteIfExists(trustStore.toPath());
+ }
+ }
+
+ /**
+ * In FIPS mode the provider hands out an extended trust manager, over the extended trust
+ * manager it loads, and loads the trust store again the same way, even after the server has
+ * turned away from FIPS mode, as it does once it has generated a certificate outside FIPS mode.
+ */
+ @Test
+ public void testTrustManagerHandedOutInFipsModeIsExtended() throws Exception
+ {
+ final File configDir = new File(DirectoryServer.getInstanceRoot(), "config");
+ final File trustStore = new File(configDir, "fips-test.truststore");
+ replace(trustStore, Files.readAllBytes(new File(configDir, "server.truststore").toPath()));
+ final FipsSwitchedTrustManagerProvider provider = new FipsSwitchedTrustManagerProvider();
+ provider.fips = true;
+ InitializationUtils.initializeTrustManagerProvider(provider,
+ providerEntry("FIPS", "fips-test.truststore", "password"), FileBasedTrustManagerProviderCfgDefn.getInstance());
+ try
+ {
+ final X509TrustManager trustManager = (X509TrustManager) provider.getTrustManagers()[0];
+ assertThat(trustManager).isInstanceOf(X509ExtendedTrustManager.class);
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(3);
+
+ replace(trustStore, Files.readAllBytes(new File(configDir, "client.truststore").toPath()));
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(2);
+
+ provider.fips = false;
+ replace(trustStore, Files.readAllBytes(new File(configDir, "server.truststore").toPath()));
+ assertThat(trustManager.getAcceptedIssuers()).hasSize(3);
+ }
+ finally
+ {
+ provider.finalizeTrustManagerProvider();
+ Files.deleteIfExists(trustStore.toPath());
+ }
+ }
+
+ /**
+ * Each certificate check of a trust manager handed out, of either kind, loads a renewed trust
+ * store first: the certificate a renewal drops is refused by the first check after it, and
+ * taken again by the first check after the next renewal brings it back. A check given a socket
+ * or an engine hands it on, and the one it delegates to refuses a check outside a handshake.
+ */
+ @Test
+ public void testEveryCertificateCheckLoadsAgain() throws Exception
+ {
+ final File configDir = new File(DirectoryServer.getInstanceRoot(), "config");
+ final File trustStore = new File(configDir, "check-test.truststore");
+ final byte[] serverTrustStore = Files.readAllBytes(new File(configDir, "server.truststore").toPath());
+ final byte[] clientTrustStore = Files.readAllBytes(new File(configDir, "client.truststore").toPath());
+ // client.truststore does not hold this self-signed certificate
+ final X509Certificate[] dropped =
+ { trustedCertificate(new File(configDir, "server.truststore"), "client-emailaddress-cert") };
+ replace(trustStore, serverTrustStore);
+ final FipsSwitchedTrustManagerProvider plainProvider = InitializationUtils.initializeTrustManagerProvider(
+ new FipsSwitchedTrustManagerProvider(), providerEntry("Plain Check", "check-test.truststore", "password"),
+ FileBasedTrustManagerProviderCfgDefn.getInstance());
+ final FipsSwitchedTrustManagerProvider extendedProvider = new FipsSwitchedTrustManagerProvider();
+ extendedProvider.fips = true;
+ InitializationUtils.initializeTrustManagerProvider(extendedProvider,
+ providerEntry("Extended Check", "check-test.truststore", "password"),
+ FileBasedTrustManagerProviderCfgDefn.getInstance());
+ try (ServerSocket serverSocket = new ServerSocket(0, 1, InetAddress.getLoopbackAddress());
+ Socket socket = SSLSocketFactory.getDefault().createSocket(
+ InetAddress.getLoopbackAddress(), serverSocket.getLocalPort()))
+ {
+ final X509TrustManager plain = (X509TrustManager) plainProvider.getTrustManagers()[0];
+ assertThat(plain).isNotInstanceOf(X509ExtendedTrustManager.class);
+ final X509ExtendedTrustManager extended = (X509ExtendedTrustManager) extendedProvider.getTrustManagers()[0];
+ final List<CertificateCheck> checks = Arrays.asList(
+ chain -> plain.checkClientTrusted(chain, "RSA"),
+ chain -> plain.checkServerTrusted(chain, "RSA"),
+ chain -> extended.checkClientTrusted(chain, "RSA"),
+ chain -> extended.checkServerTrusted(chain, "RSA"),
+ chain -> extended.checkClientTrusted(chain, "RSA", (Socket) null),
+ chain -> extended.checkServerTrusted(chain, "RSA", (Socket) null),
+ chain -> extended.checkClientTrusted(chain, "RSA", (SSLEngine) null),
+ chain -> extended.checkServerTrusted(chain, "RSA", (SSLEngine) null));
+ for (int i = 0; i < checks.size(); i++)
+ {
+ replace(trustStore, clientTrustStore);
+ assertRefused(checks.get(i), dropped, "check " + i + " after the renewal that drops the certificate");
+ replace(trustStore, serverTrustStore);
+ checks.get(i).check(dropped);
+ }
+
+ // a socket connected, or an engine, with no handshake under way
+ final SSLEngine engine = SSLContext.getDefault().createSSLEngine();
+ assertRefused(chain -> extended.checkClientTrusted(chain, "RSA", socket), dropped, "client check, socket");
+ assertRefused(chain -> extended.checkServerTrusted(chain, "RSA", socket), dropped, "server check, socket");
+ assertRefused(chain -> extended.checkClientTrusted(chain, "RSA", engine), dropped, "client check, engine");
+ assertRefused(chain -> extended.checkServerTrusted(chain, "RSA", engine), dropped, "server check, engine");
+ }
+ finally
+ {
+ plainProvider.finalizeTrustManagerProvider();
+ extendedProvider.finalizeTrustManagerProvider();
+ Files.deleteIfExists(trustStore.toPath());
+ }
+ }
+
+ /** A certificate check of a trust manager. */
+ private interface CertificateCheck
+ {
+ void check(X509Certificate[] chain) throws CertificateException;
+ }
+
+ private static void assertRefused(CertificateCheck check, X509Certificate[] chain, String description)
+ {
+ try
+ {
+ check.check(chain);
+ fail(description + ": the certificate is taken");
+ }
+ catch (CertificateException expected)
+ {
+ // refused, as expected
+ }
+ }
+
+ private static X509Certificate trustedCertificate(File trustStore, String alias) throws Exception
+ {
+ final KeyStore keyStore = KeyStore.getInstance("JKS");
+ try (InputStream in = new FileInputStream(trustStore))
+ {
+ keyStore.load(in, "password".toCharArray());
+ }
+ return (X509Certificate) keyStore.getCertificate(alias);
+ }
+
+ /**
+ * A provider whose FIPS mode the test turns on, instead of inserting a FIPS security provider
+ * into the JVM, where it would stay for every test class run after this one.
+ */
+ private static final class FipsSwitchedTrustManagerProvider extends FileBasedTrustManagerProvider
+ {
+ private volatile boolean fips;
+
+ @Override
+ boolean isFipsMode()
+ {
+ return fips;
+ }
+ }
+
+ private static Entry providerEntry(String name, String trustStoreFile, String pin) throws Exception
+ {
+ return TestCaseUtils.makeEntry(
+ "dn: cn=" + name + " Trust Manager Provider,cn=SSL,cn=config",
+ "objectClass: top",
+ "objectClass: ds-cfg-trust-manager-provider",
+ "objectClass: ds-cfg-file-based-trust-manager-provider",
+ "cn: " + name + " Trust Manager Provider",
+ "ds-cfg-java-class: org.opends.server.extensions.FileBasedTrustManagerProvider",
+ "ds-cfg-enabled: true",
+ "ds-cfg-trust-store-file: config/" + trustStoreFile,
+ "ds-cfg-trust-store-pin: " + pin);
+ }
+
private FileBasedTrustManagerProvider initializeTrustManagerProvider(Entry e) throws Exception {
return InitializationUtils.initializeTrustManagerProvider(
new FileBasedTrustManagerProvider(), e, FileBasedTrustManagerProviderCfgDefn.getInstance());
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/extensions/FileStampTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/extensions/FileStampTestCase.java
new file mode 100644
index 0000000..adb7ddc
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/extensions/FileStampTestCase.java
@@ -0,0 +1,69 @@
+/*
+ * 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.extensions;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.util.List;
+
+import org.testng.SkipException;
+import org.testng.annotations.Test;
+
+/** Tests the stamps a file based key or trust manager provider tells a changed file by. */
+@Test(sequential = true)
+public class FileStampTestCase extends ExtensionsTestCase
+{
+ /**
+ * A file renamed over another one, the way a renewal agent or a Kubernetes Secret volume
+ * replaces it, changes the stamp even where its size and modification time are the same.
+ */
+ @Test
+ public void testRenameWithSameSizeAndTimeChangesStamp() throws Exception
+ {
+ final Path dir = Files.createTempDirectory("stamp");
+ try
+ {
+ final Path file = dir.resolve("stamped.bin");
+ Files.write(file, new byte[] { 1 });
+ if (Files.readAttributes(file, BasicFileAttributes.class).fileKey() == null)
+ {
+ throw new SkipException("no file key on this file system");
+ }
+ final FileTime time = Files.getLastModifiedTime(file);
+ final List<FileStamp> before = FileStamp.of(file.toFile());
+
+ final Path tmp = dir.resolve("stamped.tmp");
+ Files.write(tmp, new byte[] { 2 });
+ Files.setLastModifiedTime(tmp, time);
+ Files.move(tmp, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+
+ assertThat(Files.size(file)).isEqualTo(1);
+ assertThat(Files.getLastModifiedTime(file)).isEqualTo(time);
+ assertThat(FileStamp.of(file.toFile())).isNotEqualTo(before);
+ }
+ finally
+ {
+ Files.deleteIfExists(dir.resolve("stamped.bin"));
+ Files.deleteIfExists(dir.resolve("stamped.tmp"));
+ Files.delete(dir);
+ }
+ }
+}
--
Gitblit v1.10.0