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 +++++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 258 insertions(+), 35 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;

--
Gitblit v1.10.0