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