From 8821bb54271124a0ee74442f3414ecba824e0152 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Thu, 24 Sep 2026 17:14:35 +0300 Subject: [PATCH 1/4] [#1095] Load a file based key store or trust store again when the file changes A connection handler builds its SSL context once, so a renewed key store file was not used until the server restarted or the handler's configuration changed. The key and trust managers handed out by the file based providers now check the file, and its PIN file, when a handshake starts (a certificate is checked, for a trust store) and load it again when it has changed. A file that cannot be loaded leaves the last good content in use and is logged once, until it changes again. Fixes #1095 --- .../FileBasedKeyManagerProvider.java | 172 +++++++++++++++- .../FileBasedTrustManagerProvider.java | 194 +++++++++++++++++- .../opends/server/extensions/FileStamp.java | 100 +++++++++ .../org/opends/messages/extension.properties | 10 + .../FileBasedKeyManagerProviderTestCase.java | 103 ++++++++++ ...FileBasedTrustManagerProviderTestCase.java | 47 +++++ 6 files changed, 618 insertions(+), 8 deletions(-) create mode 100644 opendj-server-legacy/src/main/java/org/opends/server/extensions/FileStamp.java 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 0f62462fe8..2b225895ee 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,21 @@ 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 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; @@ -67,6 +76,78 @@ public class FileBasedKeyManagerProvider private String keyStoreFile; /** The key store type to use. */ private String keyStoreType; + /** What the key managers handed out by {@link #getKeyManagers()} delegate to. */ + private volatile LoadedKeyManager loaded; + + /** A key manager loaded from the key store file, with the stamps of the files it was loaded from. */ + private static final class LoadedKeyManager + { + private final List stamps; + private final X509ExtendedKeyManager keyManager; + + private LoadedKeyManager(List stamps, X509ExtendedKeyManager keyManager) + { + this.stamps = stamps; + this.keyManager = keyManager; + } + } + + /** + * 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. + */ + private final class ReloadingKeyManager extends X509ExtendedKeyManager + { + @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 @@ -145,8 +226,82 @@ private KeyStore getKeystore() throws DirectoryException } } + /** + * {@inheritDoc} + *

+ * 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 + { + final List stamps = stampFiles(); + final KeyManager[] keyManagers = loadKeyManagers(); + if (keyManagers.length != 1 || !(keyManagers[0] instanceof X509ExtendedKeyManager)) + { + return keyManagers; + } + loaded = new LoadedKeyManager(stamps, (X509ExtendedKeyManager) keyManagers[0]); + return new KeyManager[] { new ReloadingKeyManager() }; + } + + private List stampFiles() + { + final String pinFile = currentConfig.getKeyStorePinFile(); + return FileStamp.of(getFileForPath(keyStoreFile), pinFile != null ? getFileForPath(pinFile) : null); + } + + /** + * 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. 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. + */ + private X509ExtendedKeyManager currentKeyManager() + { + final List stamps = stampFiles(); + LoadedKeyManager current = loaded; + if (current.stamps.equals(stamps)) + { + return current.keyManager; + } + synchronized (this) + { + current = loaded; + if (current.stamps.equals(stamps)) + { + return current.keyManager; + } + try + { + 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)); + } + keyStorePIN = pin; + final KeyManager[] keyManagers = loadKeyManagers(); + 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(stamps, (X509ExtendedKeyManager) keyManagers[0]); + logger.info(NOTE_FILE_KEYMANAGER_RELOADED, keyStoreFile, currentConfig.dn()); + } + catch (DirectoryException e) + { + logger.traceException(e); + loaded = new LoadedKeyManager(stamps, current.keyManager); + logger.error(ERR_FILE_KEYMANAGER_CANNOT_RELOAD, keyStoreFile, currentConfig.dn(), e.getMessageObject()); + } + return loaded.keyManager; + } + } + + private KeyManager[] loadKeyManagers() throws DirectoryException { KeyStore keyStore = getKeystore(); @@ -231,10 +386,19 @@ public ConfigChangeResult applyConfigurationChange(FileBasedKeyManagerProviderCf if (ccr.getResultCode() == ResultCode.SUCCESS) { - currentConfig = cfg; - keyStorePIN = newPIN; - keyStoreFile = newKeyStoreFile; - keyStoreType = newKeyStoreType; + synchronized (this) + { + currentConfig = cfg; + keyStorePIN = newPIN; + 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 + if (loaded != null) + { + loaded = new LoadedKeyManager(Collections. emptyList(), loaded.keyManager); + } + } } 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 32b4ad7295..0fb7ec47df 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,18 @@ 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.Collections; 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; @@ -68,6 +76,99 @@ public class FileBasedTrustManagerProvider /** The trust store type to use. */ private String trustStoreType; + /** What the trust managers handed out by {@link #getTrustManagers()} delegate to. */ + private volatile LoadedTrustManager loaded; + + /** A trust manager loaded from the trust store file, with the stamps of the files it was loaded from. */ + private static final class LoadedTrustManager + { + private final List stamps; + private final X509TrustManager trustManager; + + private LoadedTrustManager(List stamps, X509TrustManager trustManager) + { + this.stamps = stamps; + this.trustManager = trustManager; + } + } + + /** The trust manager handed out by {@link #getTrustManagers()} over a plain trust manager. */ + private final class ReloadingTrustManager implements X509TrustManager + { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException + { + currentTrustManager().checkClientTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException + { + currentTrustManager().checkServerTrusted(chain, authType); + } + + @Override + public X509Certificate[] getAcceptedIssuers() + { + return currentTrustManager().getAcceptedIssuers(); + } + } + + /** The trust manager handed out by {@link #getTrustManagers()} over an extended trust manager. */ + private final class ReloadingExtendedTrustManager extends X509ExtendedTrustManager + { + private X509ExtendedTrustManager current() + { + return (X509ExtendedTrustManager) 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 * initializeTrustManagerProvider method must be called on the @@ -102,8 +203,84 @@ public void finalizeTrustManagerProvider() currentConfig.removeFileBasedChangeListener(this); } + /** + * {@inheritDoc} + *

+ * 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 List stamps = stampFiles(); + final TrustManager[] trustManagers = loadTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) + { + return trustManagers; + } + loaded = new LoadedTrustManager(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[] { trustManagers[0] instanceof X509ExtendedTrustManager + ? new ReloadingExtendedTrustManager() : new ReloadingTrustManager() }; + } + + private List stampFiles() + { + final String pinFile = currentConfig.getTrustStorePinFile(); + return FileStamp.of(getFileForPath(trustStoreFile), pinFile != null ? getFileForPath(pinFile) : null); + } + + /** + * 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. 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() + { + final List stamps = stampFiles(); + LoadedTrustManager current = loaded; + if (current.stamps.equals(stamps)) + { + return current.trustManager; + } + synchronized (this) + { + current = loaded; + if (current.stamps.equals(stamps)) + { + return current.trustManager; + } + try + { + 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)); + } + trustStorePIN = pin; + final TrustManager[] trustManagers = loadTrustManagers(); + if (trustManagers.length != 1 || trustManagers[0].getClass() != current.trustManager.getClass()) + { + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + ERR_FILE_TRUSTMANAGER_CANNOT_CREATE_FACTORY.get(trustStoreFile, Arrays.toString(trustManagers))); + } + loaded = new LoadedTrustManager(stamps, (X509TrustManager) trustManagers[0]); + logger.info(NOTE_FILE_TRUSTMANAGER_RELOADED, trustStoreFile, currentConfig.dn()); + } + catch (DirectoryException e) + { + logger.traceException(e); + loaded = new LoadedTrustManager(stamps, current.trustManager); + logger.error(ERR_FILE_TRUSTMANAGER_CANNOT_RELOAD, trustStoreFile, currentConfig.dn(), e.getMessageObject()); + } + return loaded.trustManager; + } + } + + private TrustManager[] loadTrustManagers() throws DirectoryException { KeyStore trustStore; try (FileInputStream inputStream = new FileInputStream(getFileForPath(trustStoreFile))) @@ -177,10 +354,19 @@ public ConfigChangeResult applyConfigurationChange(FileBasedTrustManagerProvider if (ccr.getResultCode() == ResultCode.SUCCESS) { - currentConfig = cfg; - trustStorePIN = newPIN; - trustStoreFile = newTrustStoreFile; - trustStoreType = newTrustStoreType; + synchronized (this) + { + currentConfig = cfg; + trustStorePIN = newPIN; + 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 + if (loaded != null) + { + loaded = new LoadedTrustManager(Collections. emptyList(), loaded.trustManager); + } + } } 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 0000000000..1269ceb9e1 --- /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 of(File... files) + { + final List 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 84aa00ce09..5f511fa6ac 100644 --- a/opendj-server-legacy/src/messages/org/opends/messages/extension.properties +++ b/opendj-server-legacy/src/messages/org/opends/messages/extension.properties @@ -1003,3 +1003,13 @@ ERR_LDAP_TRUSTMANAGER_PIN_FILE_EMPTY_651=File %s specified in \ 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 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 7702dce195..728269679c 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,26 @@ * * 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.util.Collections; import java.util.List; + +import javax.net.ssl.X509ExtendedKeyManager; + import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -30,6 +44,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 +304,94 @@ public void testInvalidConfigs(Entry e) 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, + * and a file that cannot be loaded 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); + + replace(keyStore, Files.readAllBytes(new File(configDir, "client.keystore").toPath())); + final String clientCertificate = serverCertificateOf(keyManager); + assertThat(clientCertificate).isNotEqualTo(serverCertificate); + + // 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); + } + finally + { + provider.finalizeKeyManagerProvider(); + Files.deleteIfExists(keyStore.toPath()); + Files.deleteIfExists(pinFile.toPath()); + } + } + + 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. */ + private static byte[] withPassword(File file, String oldPassword, String newPassword) 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()); + keyStore.setKeyEntry(alias, key, newPassword.toCharArray(), keyStore.getCertificateChain(alias)); + } + 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 4ea4ece122..529f9a5694 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,18 @@ * * 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.FileWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.List; +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 +35,8 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.opends.server.extensions.FileBasedKeyManagerProviderTestCase.replace; import static org.opends.server.util.ServerConstants.*; /** @@ -281,6 +288,46 @@ public void testInvalidConfigs(Entry e) } } + /** + * 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 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"); + replace(trustStore, Files.readAllBytes(new File(configDir, "server.truststore").toPath())); + + 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: password")); + try + { + final X509TrustManager trustManager = (X509TrustManager) provider.getTrustManagers()[0]; + assertThat(trustManager.getAcceptedIssuers()).hasSize(3); + + replace(trustStore, Files.readAllBytes(new File(configDir, "client.truststore").toPath())); + 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()); + } + } + private FileBasedTrustManagerProvider initializeTrustManagerProvider(Entry e) throws Exception { return InitializationUtils.initializeTrustManagerProvider( new FileBasedTrustManagerProvider(), e, FileBasedTrustManagerProviderCfgDefn.getInstance()); From 735b9aa0c3a67b984f90745a1e4d617cc1c9999f Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Fri, 25 Sep 2026 13:43:49 +0300 Subject: [PATCH 2/4] [#1095] Keep the last good key store when a renewal holds no key a handler can present, and read the PIN on every load Review round 2 of #1101: - a reloaded key store with no private key, or with none under the aliases the last good one held keys under, leaves the last good key manager in use and logs why (ERR_FILE_KEYMANAGER_NO_KNOWN_KEY_ALIAS_656); a configuration change clears the aliases remembered; - the PIN is no longer cached: every load reads the PIN the configuration names at that moment, so a failed reload leaves nothing behind, and getKeyManagers() / getTrustManagers() follow a renewed PIN file; - a plain trust manager handed out takes a new trust manager of either kind, so a server that has turned to FIPS mode keeps loading a renewed trust store; - the stamps are taken again under the lock, and those are recorded; - tests for getServerAliases, the reset on a configuration change, no retry of an unchanged bad file, the trust store PIN file and wrapper kind, and the file key of FileStamp. --- .../FileBasedKeyManagerProvider.java | 134 +++++++++++------- .../FileBasedTrustManagerProvider.java | 49 ++++--- .../org/opends/messages/extension.properties | 3 + .../FileBasedKeyManagerProviderTestCase.java | 133 ++++++++++++++++- ...FileBasedTrustManagerProviderTestCase.java | 60 +++++++- .../server/extensions/FileStampTestCase.java | 69 +++++++++ 6 files changed, 368 insertions(+), 80 deletions(-) create mode 100644 opendj-server-legacy/src/test/java/org/opends/server/extensions/FileStampTestCase.java 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 2b225895ee..eb601177ad 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 @@ -35,6 +35,8 @@ 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; @@ -70,8 +72,6 @@ public class FileBasedKeyManagerProvider /** 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. */ @@ -79,15 +79,20 @@ public class FileBasedKeyManagerProvider /** What the key managers handed out by {@link #getKeyManagers()} delegate to. */ private volatile LoadedKeyManager loaded; - /** A key manager loaded from the key store file, with the stamps of the files it was loaded from. */ + /** + * A key manager loaded from the key store file, with the stamps of the files it was loaded + * from and the aliases the file held private keys under. + */ private static final class LoadedKeyManager { private final List stamps; + private final Set keyAliases; private final X509ExtendedKeyManager keyManager; - private LoadedKeyManager(List stamps, X509ExtendedKeyManager keyManager) + private LoadedKeyManager(List stamps, Set keyAliases, X509ExtendedKeyManager keyManager) { this.stamps = stamps; + this.keyAliases = keyAliases; this.keyManager = keyManager; } } @@ -168,7 +173,7 @@ public void initializeKeyManagerProvider(FileBasedKeyManagerProviderCfg cfg) 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)); @@ -188,18 +193,9 @@ public boolean containsKeyWithAlias(String alias) { try { - KeyStore keyStore = getKeystore(); - Enumeration 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); @@ -207,7 +203,22 @@ public boolean containsKeyWithAlias(String alias) 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 { @@ -237,12 +248,20 @@ private KeyStore getKeystore() throws DirectoryException public KeyManager[] getKeyManagers() throws DirectoryException { final List stamps = stampFiles(); - final KeyManager[] keyManagers = loadKeyManagers(); + final char[] pin = currentPIN(); + final KeyStore keyStore = getKeystore(pin); + final Set 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; } - loaded = new LoadedKeyManager(stamps, (X509ExtendedKeyManager) keyManagers[0]); + loaded = new LoadedKeyManager(stamps, keyAliases, (X509ExtendedKeyManager) keyManagers[0]); return new KeyManager[] { new ReloadingKeyManager() }; } @@ -256,18 +275,22 @@ private List stampFiles() * 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. 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. + * tried again until it changes again. So does a file with no private key, or with none under + * the aliases the file held private keys under before: a connection handler presents the key + * named by its ssl-cert-nickname, and would find none to present. */ private X509ExtendedKeyManager currentKeyManager() { - final List stamps = stampFiles(); LoadedKeyManager current = loaded; - if (current.stamps.equals(stamps)) + if (current.stamps.equals(stampFiles())) { return current.keyManager; } synchronized (this) { + // stamped again under the lock: stamps taken before it may be those of a write another + // thread has loaded past meanwhile + final List stamps = stampFiles(); current = loaded; if (current.stamps.equals(stamps)) { @@ -275,44 +298,42 @@ private X509ExtendedKeyManager currentKeyManager() } try { - final ConfigChangeResult ccr = new ConfigChangeResult(); - final char[] pin = getKeyStorePIN(currentConfig, ccr); - if (ccr.getResultCode() != ResultCode.SUCCESS) + final char[] pin = currentPIN(); + final KeyStore keyStore = getKeystore(pin); + final Set keyAliases = keyAliases(keyStore); + if (keyAliases.isEmpty()) { - throw new DirectoryException(ccr.getResultCode(), ccr.getMessages().get(0)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + ERR_NO_KEY_ENTRY_IN_KEYSTORE.get(keyStoreFile)); } - keyStorePIN = pin; - final KeyManager[] keyManagers = loadKeyManagers(); + if (!current.keyAliases.isEmpty() && Collections.disjoint(current.keyAliases, keyAliases)) + { + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + ERR_FILE_KEYMANAGER_NO_KNOWN_KEY_ALIAS.get(keyStoreFile, current.keyAliases)); + } + 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(stamps, (X509ExtendedKeyManager) keyManagers[0]); + loaded = new LoadedKeyManager(stamps, keyAliases, (X509ExtendedKeyManager) keyManagers[0]); logger.info(NOTE_FILE_KEYMANAGER_RELOADED, keyStoreFile, currentConfig.dn()); } catch (DirectoryException e) { logger.traceException(e); - loaded = new LoadedKeyManager(stamps, current.keyManager); + loaded = new LoadedKeyManager(stamps, current.keyAliases, current.keyManager); logger.error(ERR_FILE_KEYMANAGER_CANNOT_RELOAD, keyStoreFile, currentConfig.dn(), e.getMessageObject()); } return loaded.keyManager; } } - private KeyManager[] loadKeyManagers() throws DirectoryException + private KeyManager[] loadKeyManagers(KeyStore keyStore, char[] keyStorePIN) throws DirectoryException { - KeyStore keyStore = getKeystore(); - 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); @@ -332,7 +353,7 @@ public boolean containsAtLeastOneKey() { try { - return findOneKeyEntry(getKeystore()); + return !keyAliases(getKeystore(currentPIN())).isEmpty(); } catch (Exception e) { logger.traceException(e); @@ -340,18 +361,28 @@ public boolean containsAtLeastOneKey() } } - private boolean findOneKeyEntry(KeyStore keyStore) throws KeyStoreException + /** Returns the aliases the key store holds private keys under. */ + private Set keyAliases(KeyStore keyStore) throws DirectoryException { - Enumeration aliases = keyStore.aliases(); - while (aliases.hasMoreElements()) + try { - String alias = aliases.nextElement(); - if (keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) + final Set keyAliases = new TreeSet<>(); + final Enumeration 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; + } + catch (KeyStoreException e) + { + LocalizableMessage message = ERR_FILE_KEYMANAGER_CANNOT_LOAD.get(keyStoreFile, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), message, e); } - return false; } @Override @@ -382,21 +413,22 @@ public ConfigChangeResult applyConfigurationChange(FileBasedKeyManagerProviderCf 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) { synchronized (this) { currentConfig = cfg; - keyStorePIN = newPIN; 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 + // on their next handshake, even where its files are those they were loaded from, and + // whatever aliases it holds its keys under if (loaded != null) { - loaded = new LoadedKeyManager(Collections. emptyList(), loaded.keyManager); + loaded = new LoadedKeyManager(Collections. emptyList(), Collections. emptySet(), + loaded.keyManager); } } } 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 0fb7ec47df..1924a5dbcb 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 @@ -64,9 +64,6 @@ public class FileBasedTrustManagerProvider { 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; @@ -188,7 +185,7 @@ public void initializeTrustManagerProvider(FileBasedTrustManagerProviderCfg cfg) 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)); @@ -214,7 +211,7 @@ public void finalizeTrustManagerProvider() public TrustManager[] getTrustManagers() throws DirectoryException { final List stamps = stampFiles(); - final TrustManager[] trustManagers = loadTrustManagers(); + final TrustManager[] trustManagers = loadTrustManagers(currentPIN()); if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { return trustManagers; @@ -226,6 +223,21 @@ public TrustManager[] getTrustManagers() throws DirectoryException ? new ReloadingExtendedTrustManager() : new ReloadingTrustManager() }; } + /** + * 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 stampFiles() { final String pinFile = currentConfig.getTrustStorePinFile(); @@ -239,14 +251,16 @@ private List stampFiles() */ private X509TrustManager currentTrustManager() { - final List stamps = stampFiles(); LoadedTrustManager current = loaded; - if (current.stamps.equals(stamps)) + if (current.stamps.equals(stampFiles())) { return current.trustManager; } synchronized (this) { + // stamped again under the lock: stamps taken before it may be those of a write another + // thread has loaded past meanwhile + final List stamps = stampFiles(); current = loaded; if (current.stamps.equals(stamps)) { @@ -254,15 +268,13 @@ private X509TrustManager currentTrustManager() } try { - 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)); - } - trustStorePIN = pin; - final TrustManager[] trustManagers = loadTrustManagers(); - if (trustManagers.length != 1 || trustManagers[0].getClass() != current.trustManager.getClass()) + final TrustManager[] trustManagers = loadTrustManagers(currentPIN()); + // an extended trust manager handed out needs an extended one to delegate to; a plain one + // takes either, for the server may have turned to FIPS mode since, which leaves out the + // expiration check, as getTrustManagers() does then + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager) + || current.trustManager instanceof X509ExtendedTrustManager + && !(trustManagers[0] instanceof X509ExtendedTrustManager)) { throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), ERR_FILE_TRUSTMANAGER_CANNOT_CREATE_FACTORY.get(trustStoreFile, Arrays.toString(trustManagers))); @@ -280,7 +292,7 @@ private X509TrustManager currentTrustManager() } } - private TrustManager[] loadTrustManagers() throws DirectoryException + private TrustManager[] loadTrustManagers(char[] trustStorePIN) throws DirectoryException { KeyStore trustStore; try (FileInputStream inputStream = new FileInputStream(getFileForPath(trustStoreFile))) @@ -350,14 +362,13 @@ public ConfigChangeResult applyConfigurationChange(FileBasedTrustManagerProvider 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) { synchronized (this) { currentConfig = cfg; - trustStorePIN = newPIN; trustStoreFile = newTrustStoreFile; trustStoreType = newTrustStoreType; // the trust managers already handed out load the trust store the new configuration 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 5f511fa6ac..29ea8ff69f 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. @@ -1013,3 +1014,5 @@ NOTE_FILE_TRUSTMANAGER_RELOADED_654=The trust store file %s used by trust \ 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 728269679c..0331f9609a 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 @@ -28,6 +28,7 @@ import java.nio.file.StandardCopyOption; import java.security.Key; import java.security.KeyStore; +import java.security.cert.Certificate; import java.util.Collections; import java.util.List; @@ -307,8 +308,9 @@ public void testInvalidConfigs(Entry 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, - * and a file that cannot be loaded leaves the last certificate loaded in use. + * 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 @@ -334,10 +336,17 @@ public void testKeyStoreLoadedAgainWhenChanged() throws Exception final X509ExtendedKeyManager keyManager = (X509ExtendedKeyManager) provider.getKeyManagers()[0]; final String serverCertificate = serverCertificateOf(keyManager); - replace(keyStore, Files.readAllBytes(new File(configDir, "client.keystore").toPath())); - final String clientCertificate = 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); @@ -355,6 +364,106 @@ public void testKeyStoreLoadedAgainWhenChanged() throws Exception } } + /** + * 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()); + } + } + + /** 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); @@ -363,7 +472,17 @@ private static String serverCertificateOf(X509ExtendedKeyManager keyManager) } /** Returns the key store in the provided file, with its entries protected by another password. */ - private static byte[] withPassword(File file, String oldPassword, String newPassword) throws Exception + 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)) @@ -377,7 +496,9 @@ private static byte[] withPassword(File file, String oldPassword, String newPass continue; } final Key key = keyStore.getKey(alias, oldPassword.toCharArray()); - keyStore.setKeyEntry(alias, key, newPassword.toCharArray(), keyStore.getCertificateChain(alias)); + 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()); 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 529f9a5694..489cd85d8c 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 @@ -23,6 +23,7 @@ import java.nio.file.Files; import java.util.List; +import javax.net.ssl.X509ExtendedTrustManager; import javax.net.ssl.X509TrustManager; import org.forgerock.opendj.config.server.ConfigException; @@ -35,8 +36,10 @@ 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.opends.server.extensions.FileBasedKeyManagerProviderTestCase.replace; +import static org.opends.server.extensions.FileBasedKeyManagerProviderTestCase.withPassword; import static org.opends.server.util.ServerConstants.*; /** @@ -290,15 +293,18 @@ public void testInvalidConfigs(Entry e) /** * 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 is - * loaded again, and a file that cannot be loaded leaves the last certificates loaded in use. + * 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", @@ -309,13 +315,19 @@ public void testTrustStoreLoadedAgainWhenChanged() throws Exception "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: password")); + "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); - replace(trustStore, Files.readAllBytes(new File(configDir, "client.truststore").toPath())); + // 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)); @@ -325,6 +337,46 @@ public void testTrustStoreLoadedAgainWhenChanged() throws Exception { 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()); } } 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 0000000000..adb7ddcd02 --- /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 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); + } + } +} From 67059974ce7f4a011d8f4173a401801194a8f0ed Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Fri, 25 Sep 2026 14:58:14 +0300 Subject: [PATCH 3/4] [#1105] Test that a store renewed with its PIN file loads without a handshake in between The PIN is read on every load since the previous commit, so getKeyManagers() / getTrustManagers() open a key store or trust store renewed together with its PIN file with the new PIN, where they used the PIN held since the provider was configured. Pin it on both sides: replace the store and its PIN file, then ask the provider itself, as a connection handler rebuilding its SSL context does. Fixes #1105 --- .../FileBasedKeyManagerProviderTestCase.java | 42 +++++++++++++++++++ ...FileBasedTrustManagerProviderTestCase.java | 39 +++++++++++++++++ 2 files changed, 81 insertions(+) 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 0331f9609a..101507412c 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 @@ -456,6 +456,48 @@ public void testKeyStoreNotLoadedAgainUntilChanged() throws Exception } } + /** + * 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) { 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 489cd85d8c..1ba9ec7e89 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 @@ -380,6 +380,45 @@ public void testTrustStoreNotLoadedAgainUntilChanged() throws Exception } } + /** + * 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()); + } + } + private FileBasedTrustManagerProvider initializeTrustManagerProvider(Entry e) throws Exception { return InitializationUtils.initializeTrustManagerProvider( new FileBasedTrustManagerProvider(), e, FileBasedTrustManagerProviderCfgDefn.getInstance()); From 6fd0db1ff9fe0b75ac2f6ce0d7022639690d5280 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Fri, 25 Sep 2026 17:27:34 +0300 Subject: [PATCH 4/4] [#1095] Let each key manager and trust manager handed out load the file on its own getKeyManagers() stored what it loaded in a field shared by every key manager handed out before, without the checks a reload runs. A handler's configuration check calls it, so it could install a key store that a reload had refused, for every running handler. Each key manager and trust manager handed out now keeps what it last loaded. A configuration change reaches them through a counter of changes applied. Tests pin the managers handed out before a new getKeyManagers() / getTrustManagers() call, the aliases kept by a failed load, the four other alias choices, the reset on the trust side, and both FIPS paths, through a package-private isFipsMode() that a test overrides. --- .../FileBasedKeyManagerProvider.java | 167 +++++++++------- .../FileBasedTrustManagerProvider.java | 185 ++++++++++++------ .../FileBasedKeyManagerProviderTestCase.java | 104 ++++++++++ ...FileBasedTrustManagerProviderTestCase.java | 165 ++++++++++++++++ 4 files changed, 485 insertions(+), 136 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 eb601177ad..a0f1f7a1f4 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 @@ -76,25 +76,36 @@ public class FileBasedKeyManagerProvider private String keyStoreFile; /** The key store type to use. */ private String keyStoreType; - /** What the key managers handed out by {@link #getKeyManagers()} delegate to. */ - private volatile LoadedKeyManager loaded; + /** + * 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 stamps of the files it was loaded - * from and the aliases the file held private keys under. + * 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 stamps; private final Set keyAliases; private final X509ExtendedKeyManager keyManager; - private LoadedKeyManager(List stamps, Set keyAliases, X509ExtendedKeyManager keyManager) + private LoadedKeyManager(int configurationChanges, List stamps, Set keyAliases, + X509ExtendedKeyManager keyManager) { + this.configurationChanges = configurationChanges; this.stamps = stamps; this.keyAliases = keyAliases; this.keyManager = keyManager; } + + private boolean isLoadedFrom(int configurationChanges, List stamps) + { + return this.configurationChanges == configurationChanges && this.stamps.equals(stamps); + } } /** @@ -102,9 +113,85 @@ private LoadedKeyManager(List stamps, Set keyAliases, X509Ext * 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. + *

+ * 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; + } + synchronized (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 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 knownAliases = + current.configurationChanges == changes ? current.keyAliases : Collections. emptySet(); + try + { + final char[] pin = currentPIN(); + final KeyStore keyStore = getKeystore(pin); + final Set 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) { @@ -247,6 +334,7 @@ private KeyStore getKeystore(char[] keyStorePIN) throws DirectoryException @Override public KeyManager[] getKeyManagers() throws DirectoryException { + final int changes = configurationChanges; final List stamps = stampFiles(); final char[] pin = currentPIN(); final KeyStore keyStore = getKeystore(pin); @@ -261,8 +349,8 @@ public KeyManager[] getKeyManagers() throws DirectoryException { return keyManagers; } - loaded = new LoadedKeyManager(stamps, keyAliases, (X509ExtendedKeyManager) keyManagers[0]); - return new KeyManager[] { new ReloadingKeyManager() }; + return new KeyManager[] { new ReloadingKeyManager( + new LoadedKeyManager(changes, stamps, keyAliases, (X509ExtendedKeyManager) keyManagers[0])) }; } private List stampFiles() @@ -271,65 +359,6 @@ private List stampFiles() return FileStamp.of(getFileForPath(keyStoreFile), pinFile != null ? getFileForPath(pinFile) : null); } - /** - * 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. 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 with none under - * the aliases the file held private keys under before: a connection handler presents the key - * named by its ssl-cert-nickname, and would find none to present. - */ - private X509ExtendedKeyManager currentKeyManager() - { - LoadedKeyManager current = loaded; - if (current.stamps.equals(stampFiles())) - { - return current.keyManager; - } - synchronized (this) - { - // stamped again under the lock: stamps taken before it may be those of a write another - // thread has loaded past meanwhile - final List stamps = stampFiles(); - current = loaded; - if (current.stamps.equals(stamps)) - { - return current.keyManager; - } - try - { - final char[] pin = currentPIN(); - final KeyStore keyStore = getKeystore(pin); - final Set keyAliases = keyAliases(keyStore); - if (keyAliases.isEmpty()) - { - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - ERR_NO_KEY_ENTRY_IN_KEYSTORE.get(keyStoreFile)); - } - if (!current.keyAliases.isEmpty() && Collections.disjoint(current.keyAliases, keyAliases)) - { - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - ERR_FILE_KEYMANAGER_NO_KNOWN_KEY_ALIAS.get(keyStoreFile, current.keyAliases)); - } - 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(stamps, keyAliases, (X509ExtendedKeyManager) keyManagers[0]); - logger.info(NOTE_FILE_KEYMANAGER_RELOADED, keyStoreFile, currentConfig.dn()); - } - catch (DirectoryException e) - { - logger.traceException(e); - loaded = new LoadedKeyManager(stamps, current.keyAliases, current.keyManager); - logger.error(ERR_FILE_KEYMANAGER_CANNOT_RELOAD, keyStoreFile, currentConfig.dn(), e.getMessageObject()); - } - return loaded.keyManager; - } - } - private KeyManager[] loadKeyManagers(KeyStore keyStore, char[] keyStorePIN) throws DirectoryException { try @@ -425,11 +454,7 @@ public ConfigChangeResult applyConfigurationChange(FileBasedKeyManagerProviderCf // 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 - if (loaded != null) - { - loaded = new LoadedKeyManager(Collections. emptyList(), Collections. emptySet(), - loaded.keyManager); - } + configurationChanges++; } } 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 1924a5dbcb..9e6ce9cb90 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 @@ -27,7 +27,6 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Arrays; -import java.util.Collections; import java.util.List; import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManager; @@ -73,50 +72,144 @@ public class FileBasedTrustManagerProvider /** The trust store type to use. */ private String trustStoreType; - /** What the trust managers handed out by {@link #getTrustManagers()} delegate to. */ - private volatile LoadedTrustManager loaded; + /** + * 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 stamps of the files it was loaded from. */ + /** + * 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 stamps; private final X509TrustManager trustManager; - private LoadedTrustManager(List stamps, X509TrustManager trustManager) + private LoadedTrustManager(int configurationChanges, List stamps, X509TrustManager trustManager) { + this.configurationChanges = configurationChanges; this.stamps = stamps; this.trustManager = trustManager; } + + private boolean isLoadedFrom(int configurationChanges, List 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 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(LoadedTrustManager loaded) + { + 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; + } + synchronized (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 stamps = stampFiles(); + current = loaded; + if (current.isLoadedFrom(changes, stamps)) + { + return current.trustManager; + } + try + { + final TrustManager[] trustManagers = loadTrustManagers(currentPIN()); + // a plain trust manager handed out takes either kind, for the server may have turned to + // FIPS mode since, which leaves out the expiration check, as getTrustManagers() does then + 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 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 { - currentTrustManager().checkClientTrusted(chain, authType); + follower.currentTrustManager().checkClientTrusted(chain, authType); } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - currentTrustManager().checkServerTrusted(chain, authType); + follower.currentTrustManager().checkServerTrusted(chain, authType); } @Override public X509Certificate[] getAcceptedIssuers() { - return currentTrustManager().getAcceptedIssuers(); + return follower.currentTrustManager().getAcceptedIssuers(); } } /** The trust manager handed out by {@link #getTrustManagers()} over an extended trust manager. */ private final class ReloadingExtendedTrustManager extends X509ExtendedTrustManager { + private final TrustStoreFollower follower; + + private ReloadingExtendedTrustManager(TrustStoreFollower follower) + { + this.follower = follower; + } + private X509ExtendedTrustManager current() { - return (X509ExtendedTrustManager) currentTrustManager(); + return (X509ExtendedTrustManager) follower.currentTrustManager(); } @Override @@ -210,17 +303,19 @@ public void finalizeTrustManagerProvider() @Override public TrustManager[] getTrustManagers() throws DirectoryException { + final int changes = configurationChanges; final List stamps = stampFiles(); final TrustManager[] trustManagers = loadTrustManagers(currentPIN()); if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { return trustManagers; } - loaded = new LoadedTrustManager(stamps, (X509TrustManager) trustManagers[0]); + final TrustStoreFollower follower = + new TrustStoreFollower(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[] { trustManagers[0] instanceof X509ExtendedTrustManager - ? new ReloadingExtendedTrustManager() : new ReloadingTrustManager() }; + return new TrustManager[] { follower.extended + ? new ReloadingExtendedTrustManager(follower) : new ReloadingTrustManager(follower) }; } /** @@ -244,54 +339,6 @@ private List stampFiles() return FileStamp.of(getFileForPath(trustStoreFile), pinFile != null ? getFileForPath(pinFile) : null); } - /** - * 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. 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.stamps.equals(stampFiles())) - { - return current.trustManager; - } - synchronized (this) - { - // stamped again under the lock: stamps taken before it may be those of a write another - // thread has loaded past meanwhile - final List stamps = stampFiles(); - current = loaded; - if (current.stamps.equals(stamps)) - { - return current.trustManager; - } - try - { - final TrustManager[] trustManagers = loadTrustManagers(currentPIN()); - // an extended trust manager handed out needs an extended one to delegate to; a plain one - // takes either, for the server may have turned to FIPS mode since, which leaves out the - // expiration check, as getTrustManagers() does then - if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager) - || current.trustManager instanceof X509ExtendedTrustManager - && !(trustManagers[0] instanceof X509ExtendedTrustManager)) - { - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - ERR_FILE_TRUSTMANAGER_CANNOT_CREATE_FACTORY.get(trustStoreFile, Arrays.toString(trustManagers))); - } - loaded = new LoadedTrustManager(stamps, (X509TrustManager) trustManagers[0]); - logger.info(NOTE_FILE_TRUSTMANAGER_RELOADED, trustStoreFile, currentConfig.dn()); - } - catch (DirectoryException e) - { - logger.traceException(e); - loaded = new LoadedTrustManager(stamps, current.trustManager); - logger.error(ERR_FILE_TRUSTMANAGER_CANNOT_RELOAD, trustStoreFile, currentConfig.dn(), e.getMessageObject()); - } - return loaded.trustManager; - } - } - private TrustManager[] loadTrustManagers(char[] trustStorePIN) throws DirectoryException { KeyStore trustStore; @@ -314,7 +361,7 @@ private TrustManager[] loadTrustManagers(char[] trustStorePIN) throws DirectoryE trustManagerFactory.init(trustStore); TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); TrustManager[] newTrustManagers = new TrustManager[trustManagers.length]; - if (isFips()) { + if (isFipsMode()) { newTrustManagers = trustManagers; } else { for (int i=0; i < trustManagers.length; i++) @@ -334,6 +381,17 @@ private TrustManager[] loadTrustManagers(char[] trustStorePIN) throws DirectoryE } } + /** + * 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 unacceptableReasons) { @@ -373,10 +431,7 @@ public ConfigChangeResult applyConfigurationChange(FileBasedTrustManagerProvider 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 - if (loaded != null) - { - loaded = new LoadedTrustManager(Collections. emptyList(), loaded.trustManager); - } + configurationChanges++; } } 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 101507412c..da0b42581a 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 @@ -29,8 +29,10 @@ 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; @@ -355,6 +357,10 @@ public void testKeyStoreLoadedAgainWhenChanged() throws Exception 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 { @@ -364,6 +370,104 @@ public void testKeyStoreLoadedAgainWhenChanged() throws Exception } } + /** + * 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> 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 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. 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 1ba9ec7e89..cde33f3514 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 @@ -419,6 +419,171 @@ public void testTrustStoreAndPinRenewedTogetherLoadedWithoutCheck() throws Excep } } + /** + * 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 takes the extended trust manager the + * provider loads once the server has turned to FIPS mode. + */ + @Test + public void testPlainTrustManagerTakesExtendedOneWhenFipsModeTurnsOn() 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); + } + 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. + */ + @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); + } + finally + { + provider.finalizeTrustManagerProvider(); + Files.deleteIfExists(trustStore.toPath()); + } + } + + /** + * 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());