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