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/FileBasedTrustManagerProvider.java | 284 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 files changed, 274 insertions(+), 10 deletions(-)
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;
--
Gitblit v1.10.0