[#1095] Load a file based key store or trust store again when the file changes (#1101)
5 files modified
2 files added
| | |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.extensions; |
| | | |
| | |
| | | import java.io.FileInputStream; |
| | | import java.io.FileReader; |
| | | import java.io.IOException; |
| | | import java.net.Socket; |
| | | import java.security.KeyStore; |
| | | import java.security.KeyStoreException; |
| | | import java.security.Principal; |
| | | import java.security.PrivateKey; |
| | | import java.security.cert.X509Certificate; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.Enumeration; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import java.util.TreeSet; |
| | | |
| | | import javax.net.ssl.KeyManager; |
| | | import javax.net.ssl.KeyManagerFactory; |
| | | import javax.net.ssl.SSLEngine; |
| | | import javax.net.ssl.X509ExtendedKeyManager; |
| | | |
| | | import com.forgerock.opendj.util.FipsStaticUtils; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | |
| | | /** The configuration for this key manager provider. */ |
| | | private FileBasedKeyManagerProviderCfg currentConfig; |
| | | |
| | | /** The PIN needed to access the keystore. */ |
| | | private char[] keyStorePIN; |
| | | /** The path to the key store backing file. */ |
| | | private String keyStoreFile; |
| | | /** The key store type to use. */ |
| | | private String keyStoreType; |
| | | /** |
| | | * The number of configuration changes applied. It is counted after the fields above are set, |
| | | * and read before them. |
| | | */ |
| | | private volatile int configurationChanges; |
| | | |
| | | /** |
| | | * A key manager loaded from the key store file, with the configuration change and the stamps |
| | | * of the files it was loaded under, and the aliases the file held private keys under. |
| | | */ |
| | | private static final class LoadedKeyManager |
| | | { |
| | | private final int configurationChanges; |
| | | private final List<FileStamp> stamps; |
| | | private final Set<String> keyAliases; |
| | | private final X509ExtendedKeyManager keyManager; |
| | | |
| | | private LoadedKeyManager(int configurationChanges, List<FileStamp> stamps, Set<String> keyAliases, |
| | | X509ExtendedKeyManager keyManager) |
| | | { |
| | | this.configurationChanges = configurationChanges; |
| | | this.stamps = stamps; |
| | | this.keyAliases = keyAliases; |
| | | this.keyManager = keyManager; |
| | | } |
| | | |
| | | private boolean isLoadedFrom(int configurationChanges, List<FileStamp> stamps) |
| | | { |
| | | return this.configurationChanges == configurationChanges && this.stamps.equals(stamps); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The key manager handed out by {@link #getKeyManagers()}. The key store file is looked at |
| | | * again when a handshake chooses its alias, and only then: the certificate chain and the |
| | | * private key of the alias chosen come from the key manager that alias was chosen from, |
| | | * unless the file is loaded again, by another handshake, in between. |
| | | * <p> |
| | | * Each key manager handed out loads the file on its own, so asking the provider again, as a |
| | | * connection handler does to check a configuration change, leaves those in use alone. |
| | | */ |
| | | private final class ReloadingKeyManager extends X509ExtendedKeyManager |
| | | { |
| | | /** The key manager last loaded, when this one was handed out or by a handshake since. */ |
| | | private volatile LoadedKeyManager loaded; |
| | | |
| | | private ReloadingKeyManager(LoadedKeyManager loaded) |
| | | { |
| | | this.loaded = loaded; |
| | | } |
| | | |
| | | /** |
| | | * Returns the key manager to use for a new handshake, first loading the key store file again |
| | | * when it has changed since it was last loaded, or the configuration has. A file that cannot |
| | | * be loaded - caught half written, or not matching its PIN - leaves the key manager last |
| | | * loaded in use, and is not tried again until it changes again. So does a file with no private |
| | | * key, or one that shares no alias with the file last loaded. A connection handler presents |
| | | * the key named by its ssl-cert-nickname, which the provider does not see, and the aliases of |
| | | * the file last loaded stand in for it: a file that keeps one of them, but not the one a |
| | | * handler names, is still taken. |
| | | */ |
| | | private X509ExtendedKeyManager currentKeyManager() |
| | | { |
| | | LoadedKeyManager current = loaded; |
| | | if (current.isLoadedFrom(configurationChanges, stampFiles())) |
| | | { |
| | | return current.keyManager; |
| | | } |
| | | // the provider's lock, which applyConfigurationChange takes too: a load reads the |
| | | // configuration as it was before a change or after it, never a mix of both |
| | | synchronized (FileBasedKeyManagerProvider.this) |
| | | { |
| | | // stamped again under the lock: stamps taken before it may be those of a write another |
| | | // thread has loaded past meanwhile |
| | | final int changes = configurationChanges; |
| | | final List<FileStamp> stamps = stampFiles(); |
| | | current = loaded; |
| | | if (current.isLoadedFrom(changes, stamps)) |
| | | { |
| | | return current.keyManager; |
| | | } |
| | | // the key store a changed configuration names may hold its keys under any aliases |
| | | final Set<String> knownAliases = |
| | | current.configurationChanges == changes ? current.keyAliases : Collections.<String> emptySet(); |
| | | try |
| | | { |
| | | final char[] pin = currentPIN(); |
| | | final KeyStore keyStore = getKeystore(pin); |
| | | final Set<String> keyAliases = keyAliases(keyStore); |
| | | if (keyAliases.isEmpty()) |
| | | { |
| | | throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), |
| | | ERR_NO_KEY_ENTRY_IN_KEYSTORE.get(keyStoreFile)); |
| | | } |
| | | if (!knownAliases.isEmpty() && Collections.disjoint(knownAliases, keyAliases)) |
| | | { |
| | | throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), |
| | | ERR_FILE_KEYMANAGER_NO_KNOWN_KEY_ALIAS.get(keyStoreFile, knownAliases)); |
| | | } |
| | | final KeyManager[] keyManagers = loadKeyManagers(keyStore, pin); |
| | | if (keyManagers.length != 1 || !(keyManagers[0] instanceof X509ExtendedKeyManager)) |
| | | { |
| | | throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), |
| | | ERR_FILE_KEYMANAGER_CANNOT_CREATE_FACTORY.get(keyStoreFile, Arrays.toString(keyManagers))); |
| | | } |
| | | loaded = new LoadedKeyManager(changes, stamps, keyAliases, (X509ExtendedKeyManager) keyManagers[0]); |
| | | logger.info(NOTE_FILE_KEYMANAGER_RELOADED, keyStoreFile, currentConfig.dn()); |
| | | } |
| | | catch (DirectoryException e) |
| | | { |
| | | logger.traceException(e); |
| | | loaded = new LoadedKeyManager(changes, stamps, knownAliases, current.keyManager); |
| | | logger.error(ERR_FILE_KEYMANAGER_CANNOT_RELOAD, keyStoreFile, currentConfig.dn(), e.getMessageObject()); |
| | | } |
| | | return loaded.keyManager; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public String[] getClientAliases(String keyType, Principal[] issuers) |
| | | { |
| | | return currentKeyManager().getClientAliases(keyType, issuers); |
| | | } |
| | | |
| | | @Override |
| | | public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) |
| | | { |
| | | return currentKeyManager().chooseClientAlias(keyType, issuers, socket); |
| | | } |
| | | |
| | | @Override |
| | | public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) |
| | | { |
| | | return currentKeyManager().chooseEngineClientAlias(keyType, issuers, engine); |
| | | } |
| | | |
| | | @Override |
| | | public String[] getServerAliases(String keyType, Principal[] issuers) |
| | | { |
| | | return currentKeyManager().getServerAliases(keyType, issuers); |
| | | } |
| | | |
| | | @Override |
| | | public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) |
| | | { |
| | | return currentKeyManager().chooseServerAlias(keyType, issuers, socket); |
| | | } |
| | | |
| | | @Override |
| | | public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) |
| | | { |
| | | return currentKeyManager().chooseEngineServerAlias(keyType, issuers, engine); |
| | | } |
| | | |
| | | @Override |
| | | public X509Certificate[] getCertificateChain(String alias) |
| | | { |
| | | return loaded.keyManager.getCertificateChain(alias); |
| | | } |
| | | |
| | | @Override |
| | | public PrivateKey getPrivateKey(String alias) |
| | | { |
| | | return loaded.keyManager.getPrivateKey(alias); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Creates a new instance of this file-based key manager provider. The |
| | |
| | | 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)); |
| | |
| | | { |
| | | try |
| | | { |
| | | KeyStore keyStore = getKeystore(); |
| | | Enumeration<String> aliases = keyStore.aliases(); |
| | | while (aliases.hasMoreElements()) |
| | | { |
| | | String theAlias = aliases.nextElement(); |
| | | if (alias.equals(theAlias) && keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) |
| | | { |
| | | return true; |
| | | } |
| | | } |
| | | return keyAliases(getKeystore(currentPIN())).contains(alias); |
| | | } |
| | | catch (DirectoryException | KeyStoreException e) |
| | | catch (DirectoryException e) |
| | | { |
| | | // Ignore. |
| | | logger.traceException(e); |
| | |
| | | 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 |
| | | { |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | * <p> |
| | | * The key manager returned reads the key store file again when a handshake starts after the |
| | | * file, or the PIN file, has changed, so that a renewed certificate is presented without |
| | | * restarting the server or the component using it. |
| | | */ |
| | | @Override |
| | | public KeyManager[] getKeyManagers() throws DirectoryException |
| | | { |
| | | KeyStore keyStore = getKeystore(); |
| | | final int changes = configurationChanges; |
| | | final List<FileStamp> stamps = stampFiles(); |
| | | final char[] pin = currentPIN(); |
| | | final KeyStore keyStore = getKeystore(pin); |
| | | final Set<String> keyAliases = keyAliases(keyStore); |
| | | if (keyAliases.isEmpty()) |
| | | { |
| | | // Troubleshooting message to let now of possible config error |
| | | logger.error(ERR_NO_KEY_ENTRY_IN_KEYSTORE, keyStoreFile); |
| | | } |
| | | final KeyManager[] keyManagers = loadKeyManagers(keyStore, pin); |
| | | if (keyManagers.length != 1 || !(keyManagers[0] instanceof X509ExtendedKeyManager)) |
| | | { |
| | | return keyManagers; |
| | | } |
| | | return new KeyManager[] { new ReloadingKeyManager( |
| | | new LoadedKeyManager(changes, stamps, keyAliases, (X509ExtendedKeyManager) keyManagers[0])) }; |
| | | } |
| | | |
| | | private List<FileStamp> stampFiles() |
| | | { |
| | | final String pinFile = currentConfig.getKeyStorePinFile(); |
| | | return FileStamp.of(getFileForPath(keyStoreFile), pinFile != null ? getFileForPath(pinFile) : null); |
| | | } |
| | | |
| | | private KeyManager[] loadKeyManagers(KeyStore keyStore, char[] keyStorePIN) throws DirectoryException |
| | | { |
| | | try |
| | | { |
| | | if (! findOneKeyEntry(keyStore)) |
| | | { |
| | | // Troubleshooting message to let now of possible config error |
| | | logger.error(ERR_NO_KEY_ENTRY_IN_KEYSTORE, keyStoreFile); |
| | | } |
| | | |
| | | String keyManagerAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); |
| | | KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(keyManagerAlgorithm); |
| | | keyManagerFactory.init(keyStore, keyStorePIN); |
| | |
| | | { |
| | | try |
| | | { |
| | | return findOneKeyEntry(getKeystore()); |
| | | return !keyAliases(getKeystore(currentPIN())).isEmpty(); |
| | | } |
| | | catch (Exception e) { |
| | | logger.traceException(e); |
| | |
| | | } |
| | | } |
| | | |
| | | private boolean findOneKeyEntry(KeyStore keyStore) throws KeyStoreException |
| | | /** Returns the aliases the key store holds private keys under. */ |
| | | private Set<String> keyAliases(KeyStore keyStore) throws DirectoryException |
| | | { |
| | | Enumeration<String> aliases = keyStore.aliases(); |
| | | while (aliases.hasMoreElements()) |
| | | try |
| | | { |
| | | String alias = aliases.nextElement(); |
| | | if (keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) |
| | | final Set<String> keyAliases = new TreeSet<>(); |
| | | final Enumeration<String> aliases = keyStore.aliases(); |
| | | while (aliases.hasMoreElements()) |
| | | { |
| | | return true; |
| | | final String alias = aliases.nextElement(); |
| | | if (keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) |
| | | { |
| | | keyAliases.add(alias); |
| | | } |
| | | } |
| | | return keyAliases; |
| | | } |
| | | return false; |
| | | catch (KeyStoreException e) |
| | | { |
| | | LocalizableMessage message = ERR_FILE_KEYMANAGER_CANNOT_LOAD.get(keyStoreFile, getExceptionMessage(e)); |
| | | throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), message, e); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | |
| | | final ConfigChangeResult ccr = new ConfigChangeResult(); |
| | | String newKeyStoreFile = getKeyStoreFile(cfg, ccr); |
| | | String newKeyStoreType = getKeyStoreType(cfg, ccr); |
| | | char[] newPIN = getKeyStorePIN(cfg, ccr); |
| | | getKeyStorePIN(cfg, ccr); |
| | | |
| | | if (ccr.getResultCode() == ResultCode.SUCCESS) |
| | | { |
| | | currentConfig = cfg; |
| | | keyStorePIN = newPIN; |
| | | keyStoreFile = newKeyStoreFile; |
| | | keyStoreType = newKeyStoreType; |
| | | synchronized (this) |
| | | { |
| | | currentConfig = cfg; |
| | | keyStoreFile = newKeyStoreFile; |
| | | keyStoreType = newKeyStoreType; |
| | | // the key managers already handed out load the key store the new configuration names |
| | | // on their next handshake, even where its files are those they were loaded from, and |
| | | // whatever aliases it holds its keys under |
| | | configurationChanges++; |
| | | } |
| | | } |
| | | |
| | | return ccr; |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.extensions; |
| | | |
| | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.net.Socket; |
| | | import java.security.KeyStore; |
| | | import java.security.KeyStoreException; |
| | | import java.security.cert.CertificateException; |
| | | import java.security.cert.X509Certificate; |
| | | import java.util.Arrays; |
| | | import java.util.List; |
| | | import javax.net.ssl.SSLEngine; |
| | | import javax.net.ssl.TrustManager; |
| | | import javax.net.ssl.TrustManagerFactory; |
| | | import javax.net.ssl.X509ExtendedTrustManager; |
| | | import javax.net.ssl.X509TrustManager; |
| | | |
| | | import org.forgerock.opendj.config.server.ConfigurationChangeListener; |
| | |
| | | { |
| | | 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; |
| | | |
| | |
| | | private String trustStoreType; |
| | | |
| | | /** |
| | | * The number of configuration changes applied. It is counted after the fields above are set, |
| | | * and read before them. |
| | | */ |
| | | private volatile int configurationChanges; |
| | | |
| | | /** |
| | | * A trust manager loaded from the trust store file, with the configuration change and the |
| | | * stamps of the files it was loaded under. |
| | | */ |
| | | private static final class LoadedTrustManager |
| | | { |
| | | private final int configurationChanges; |
| | | private final List<FileStamp> stamps; |
| | | private final X509TrustManager trustManager; |
| | | |
| | | private LoadedTrustManager(int configurationChanges, List<FileStamp> stamps, X509TrustManager trustManager) |
| | | { |
| | | this.configurationChanges = configurationChanges; |
| | | this.stamps = stamps; |
| | | this.trustManager = trustManager; |
| | | } |
| | | |
| | | private boolean isLoadedFrom(int configurationChanges, List<FileStamp> stamps) |
| | | { |
| | | return this.configurationChanges == configurationChanges && this.stamps.equals(stamps); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * What a trust manager handed out by {@link #getTrustManagers()} delegates to. Each trust |
| | | * manager handed out loads the file on its own, so asking the provider again, as a component |
| | | * does to check a configuration change, leaves those in use alone. |
| | | */ |
| | | private final class TrustStoreFollower |
| | | { |
| | | /** |
| | | * Whether the server ran in FIPS mode when the trust manager was handed out. The trust store |
| | | * is loaded again as it was loaded then, so that what is loaded stays of the kind handed |
| | | * out, even where the server has turned to FIPS mode since, or away from it. |
| | | */ |
| | | private final boolean fipsMode; |
| | | /** Whether the trust manager handed out is an extended one, which needs an extended one to delegate to. */ |
| | | private final boolean extended; |
| | | /** The trust manager last loaded, when this one was handed out or by a check since. */ |
| | | private volatile LoadedTrustManager loaded; |
| | | |
| | | private TrustStoreFollower(boolean fipsMode, LoadedTrustManager loaded) |
| | | { |
| | | this.fipsMode = fipsMode; |
| | | this.extended = loaded.trustManager instanceof X509ExtendedTrustManager; |
| | | this.loaded = loaded; |
| | | } |
| | | |
| | | /** |
| | | * Returns the trust manager to check a certificate with, first loading the trust store file |
| | | * again when it has changed since it was last loaded, or the configuration has. A file that |
| | | * cannot be loaded leaves the trust manager last loaded in use, and is not tried again until |
| | | * it changes again. |
| | | */ |
| | | private X509TrustManager currentTrustManager() |
| | | { |
| | | LoadedTrustManager current = loaded; |
| | | if (current.isLoadedFrom(configurationChanges, stampFiles())) |
| | | { |
| | | return current.trustManager; |
| | | } |
| | | // the provider's lock, which applyConfigurationChange takes too: a load reads the |
| | | // configuration as it was before a change or after it, never a mix of both |
| | | synchronized (FileBasedTrustManagerProvider.this) |
| | | { |
| | | // stamped again under the lock: stamps taken before it may be those of a write another |
| | | // thread has loaded past meanwhile |
| | | final int changes = configurationChanges; |
| | | final List<FileStamp> stamps = stampFiles(); |
| | | current = loaded; |
| | | if (current.isLoadedFrom(changes, stamps)) |
| | | { |
| | | return current.trustManager; |
| | | } |
| | | try |
| | | { |
| | | final TrustManager[] trustManagers = loadTrustManagers(currentPIN(), fipsMode); |
| | | if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager) |
| | | || extended != trustManagers[0] instanceof X509ExtendedTrustManager) |
| | | { |
| | | throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), |
| | | ERR_FILE_TRUSTMANAGER_CANNOT_CREATE_FACTORY.get(trustStoreFile, Arrays.toString(trustManagers))); |
| | | } |
| | | loaded = new LoadedTrustManager(changes, stamps, (X509TrustManager) trustManagers[0]); |
| | | logger.info(NOTE_FILE_TRUSTMANAGER_RELOADED, trustStoreFile, currentConfig.dn()); |
| | | } |
| | | catch (DirectoryException e) |
| | | { |
| | | logger.traceException(e); |
| | | loaded = new LoadedTrustManager(changes, stamps, current.trustManager); |
| | | logger.error(ERR_FILE_TRUSTMANAGER_CANNOT_RELOAD, trustStoreFile, currentConfig.dn(), e.getMessageObject()); |
| | | } |
| | | return loaded.trustManager; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** The trust manager handed out by {@link #getTrustManagers()} over a plain trust manager. */ |
| | | private static final class ReloadingTrustManager implements X509TrustManager |
| | | { |
| | | private final TrustStoreFollower follower; |
| | | |
| | | private ReloadingTrustManager(TrustStoreFollower follower) |
| | | { |
| | | this.follower = follower; |
| | | } |
| | | |
| | | @Override |
| | | public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException |
| | | { |
| | | follower.currentTrustManager().checkClientTrusted(chain, authType); |
| | | } |
| | | |
| | | @Override |
| | | public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException |
| | | { |
| | | follower.currentTrustManager().checkServerTrusted(chain, authType); |
| | | } |
| | | |
| | | @Override |
| | | public X509Certificate[] getAcceptedIssuers() |
| | | { |
| | | return follower.currentTrustManager().getAcceptedIssuers(); |
| | | } |
| | | } |
| | | |
| | | /** The trust manager handed out by {@link #getTrustManagers()} over an extended trust manager. */ |
| | | private static final class ReloadingExtendedTrustManager extends X509ExtendedTrustManager |
| | | { |
| | | private final TrustStoreFollower follower; |
| | | |
| | | private ReloadingExtendedTrustManager(TrustStoreFollower follower) |
| | | { |
| | | this.follower = follower; |
| | | } |
| | | |
| | | private X509ExtendedTrustManager current() |
| | | { |
| | | return (X509ExtendedTrustManager) follower.currentTrustManager(); |
| | | } |
| | | |
| | | @Override |
| | | public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException |
| | | { |
| | | current().checkClientTrusted(chain, authType); |
| | | } |
| | | |
| | | @Override |
| | | public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) |
| | | throws CertificateException |
| | | { |
| | | current().checkClientTrusted(chain, authType, socket); |
| | | } |
| | | |
| | | @Override |
| | | public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) |
| | | throws CertificateException |
| | | { |
| | | current().checkClientTrusted(chain, authType, engine); |
| | | } |
| | | |
| | | @Override |
| | | public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException |
| | | { |
| | | current().checkServerTrusted(chain, authType); |
| | | } |
| | | |
| | | @Override |
| | | public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) |
| | | throws CertificateException |
| | | { |
| | | current().checkServerTrusted(chain, authType, socket); |
| | | } |
| | | |
| | | @Override |
| | | public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) |
| | | throws CertificateException |
| | | { |
| | | current().checkServerTrusted(chain, authType, engine); |
| | | } |
| | | |
| | | @Override |
| | | public X509Certificate[] getAcceptedIssuers() |
| | | { |
| | | return current().getAcceptedIssuers(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Creates a new instance of this file-based trust manager provider. The |
| | | * <CODE>initializeTrustManagerProvider</CODE> method must be called on the |
| | | * resulting object before it may be used. |
| | |
| | | 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)); |
| | |
| | | currentConfig.removeFileBasedChangeListener(this); |
| | | } |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | * <p> |
| | | * The trust manager returned reads the trust store file again when a certificate is checked |
| | | * after the file, or the PIN file, has changed, so that a renewed trust store is used without |
| | | * restarting the server or the component using it. |
| | | */ |
| | | @Override |
| | | public TrustManager[] getTrustManagers() throws DirectoryException |
| | | { |
| | | final int changes = configurationChanges; |
| | | final List<FileStamp> stamps = stampFiles(); |
| | | final boolean fipsMode = isFipsMode(); |
| | | final TrustManager[] trustManagers = loadTrustManagers(currentPIN(), fipsMode); |
| | | if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) |
| | | { |
| | | return trustManagers; |
| | | } |
| | | final TrustStoreFollower follower = new TrustStoreFollower( |
| | | fipsMode, new LoadedTrustManager(changes, stamps, (X509TrustManager) trustManagers[0])); |
| | | // an extended trust manager stays one, and a plain one stays plain, for JSSE adds checks |
| | | // of its own around a plain one |
| | | return new TrustManager[] { follower.extended |
| | | ? new ReloadingExtendedTrustManager(follower) : new ReloadingTrustManager(follower) }; |
| | | } |
| | | |
| | | /** |
| | | * Returns the PIN the configuration names now, rather than the one it named when the provider |
| | | * was configured: a PIN file may have been renewed since, together with the trust store. |
| | | */ |
| | | private char[] currentPIN() throws DirectoryException |
| | | { |
| | | final ConfigChangeResult ccr = new ConfigChangeResult(); |
| | | final char[] pin = getTrustStorePIN(currentConfig, ccr); |
| | | if (ccr.getResultCode() != ResultCode.SUCCESS) |
| | | { |
| | | throw new DirectoryException(ccr.getResultCode(), ccr.getMessages().get(0)); |
| | | } |
| | | return pin; |
| | | } |
| | | |
| | | private List<FileStamp> stampFiles() |
| | | { |
| | | final String pinFile = currentConfig.getTrustStorePinFile(); |
| | | return FileStamp.of(getFileForPath(trustStoreFile), pinFile != null ? getFileForPath(pinFile) : null); |
| | | } |
| | | |
| | | /** |
| | | * Loads the trust managers of the trust store file: in FIPS mode as they are, and otherwise each |
| | | * within an expiration check. |
| | | */ |
| | | private TrustManager[] loadTrustManagers(char[] trustStorePIN, boolean fipsMode) throws DirectoryException |
| | | { |
| | | KeyStore trustStore; |
| | | try (FileInputStream inputStream = new FileInputStream(getFileForPath(trustStoreFile))) |
| | | { |
| | |
| | | trustManagerFactory.init(trustStore); |
| | | TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); |
| | | TrustManager[] newTrustManagers = new TrustManager[trustManagers.length]; |
| | | if (isFips()) { |
| | | if (fipsMode) { |
| | | newTrustManagers = trustManagers; |
| | | } else { |
| | | for (int i=0; i < trustManagers.length; i++) |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Tells whether the server runs in FIPS mode, where the trust managers loaded are handed out as |
| | | * they are, without an expiration check around them. |
| | | * |
| | | * @return {@code true} if the server runs in FIPS mode |
| | | */ |
| | | boolean isFipsMode() |
| | | { |
| | | return isFips(); |
| | | } |
| | | |
| | | @Override |
| | | public boolean isConfigurationAcceptable(TrustManagerProviderCfg cfg, List<LocalizableMessage> unacceptableReasons) |
| | | { |
| | |
| | | final ConfigChangeResult ccr = new ConfigChangeResult(); |
| | | String newTrustStoreFile = getTrustStoreFile(cfg, ccr); |
| | | String newTrustStoreType = getTrustStoreType(cfg, ccr); |
| | | char[] newPIN = getTrustStorePIN(cfg, ccr); |
| | | getTrustStorePIN(cfg, ccr); |
| | | |
| | | if (ccr.getResultCode() == ResultCode.SUCCESS) |
| | | { |
| | | currentConfig = cfg; |
| | | trustStorePIN = newPIN; |
| | | trustStoreFile = newTrustStoreFile; |
| | | trustStoreType = newTrustStoreType; |
| | | synchronized (this) |
| | | { |
| | | currentConfig = cfg; |
| | | trustStoreFile = newTrustStoreFile; |
| | | trustStoreType = newTrustStoreType; |
| | | // the trust managers already handed out load the trust store the new configuration |
| | | // names on their next check, even where its files are those they were loaded from |
| | | configurationChanges++; |
| | | } |
| | | } |
| | | |
| | | return ccr; |
| New file |
| | |
| | | /* |
| | | * 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<FileStamp> of(File... files) |
| | | { |
| | | final List<FileStamp> 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); |
| | | } |
| | | } |
| | |
| | | # |
| | | # Copyright 2006-2010 Sun Microsystems, Inc. |
| | | # Portions Copyright 2011-2016 ForgeRock AS. |
| | | # Portions Copyright 2026 3A Systems, LLC. |
| | | |
| | | |
| | | |
| | |
| | | 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 |
| | | 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 |
| | |
| | | * |
| | | * 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.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; |
| | | |
| | | import org.testng.annotations.BeforeClass; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | |
| | | 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.*; |
| | | |
| | | /** |
| | |
| | | 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. |
| | | * 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 |
| | | { |
| | | 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); |
| | | |
| | | // 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); |
| | | 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); |
| | | |
| | | // 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 |
| | | { |
| | | provider.finalizeKeyManagerProvider(); |
| | | Files.deleteIfExists(keyStore.toPath()); |
| | | Files.deleteIfExists(pinFile.toPath()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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<Function<X509ExtendedKeyManager, String>> 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<X509ExtendedKeyManager, String> 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. |
| | | */ |
| | | @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()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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) |
| | | { |
| | | 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); |
| | | 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. */ |
| | | 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)) |
| | | { |
| | | 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()); |
| | | 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()); |
| | | 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()); |
| | |
| | | * |
| | | * 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.FileInputStream; |
| | | import java.io.FileWriter; |
| | | import java.io.InputStream; |
| | | import java.net.InetAddress; |
| | | import java.net.ServerSocket; |
| | | import java.net.Socket; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.nio.file.Files; |
| | | import java.security.KeyStore; |
| | | import java.security.cert.CertificateException; |
| | | import java.security.cert.X509Certificate; |
| | | import java.util.Arrays; |
| | | import java.util.List; |
| | | |
| | | import javax.net.ssl.SSLContext; |
| | | import javax.net.ssl.SSLEngine; |
| | | import javax.net.ssl.SSLSocketFactory; |
| | | import javax.net.ssl.X509ExtendedTrustManager; |
| | | 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; |
| | |
| | | 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.assertj.core.api.Assertions.fail; |
| | | import static org.opends.server.extensions.FileBasedKeyManagerProviderTestCase.replace; |
| | | import static org.opends.server.extensions.FileBasedKeyManagerProviderTestCase.withPassword; |
| | | import static org.opends.server.util.ServerConstants.*; |
| | | |
| | | /** |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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, 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", |
| | | "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-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); |
| | | |
| | | // 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)); |
| | | assertThat(trustManager.getAcceptedIssuers()).hasSize(2); |
| | | } |
| | | finally |
| | | { |
| | | 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()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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 loads the trust store as it was loaded |
| | | * then, within an expiration check, after the server has turned to FIPS mode, and after it has |
| | | * turned away from it again: a renewal is taken either way. |
| | | */ |
| | | @Test |
| | | public void testPlainTrustManagerLoadedAgainAsHandedOutWhenFipsModeChanges() 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); |
| | | |
| | | provider.fips = false; |
| | | replace(trustStore, Files.readAllBytes(new File(configDir, "server.truststore").toPath())); |
| | | assertThat(trustManager.getAcceptedIssuers()).hasSize(3); |
| | | } |
| | | 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, even after the server has |
| | | * turned away from FIPS mode, as it does once it has generated a certificate outside FIPS mode. |
| | | */ |
| | | @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); |
| | | |
| | | provider.fips = false; |
| | | replace(trustStore, Files.readAllBytes(new File(configDir, "server.truststore").toPath())); |
| | | assertThat(trustManager.getAcceptedIssuers()).hasSize(3); |
| | | } |
| | | finally |
| | | { |
| | | provider.finalizeTrustManagerProvider(); |
| | | Files.deleteIfExists(trustStore.toPath()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Each certificate check of a trust manager handed out, of either kind, loads a renewed trust |
| | | * store first: the certificate a renewal drops is refused by the first check after it, and |
| | | * taken again by the first check after the next renewal brings it back. A check given a socket |
| | | * or an engine hands it on, and the one it delegates to refuses a check outside a handshake. |
| | | */ |
| | | @Test |
| | | public void testEveryCertificateCheckLoadsAgain() throws Exception |
| | | { |
| | | final File configDir = new File(DirectoryServer.getInstanceRoot(), "config"); |
| | | final File trustStore = new File(configDir, "check-test.truststore"); |
| | | final byte[] serverTrustStore = Files.readAllBytes(new File(configDir, "server.truststore").toPath()); |
| | | final byte[] clientTrustStore = Files.readAllBytes(new File(configDir, "client.truststore").toPath()); |
| | | // client.truststore does not hold this self-signed certificate |
| | | final X509Certificate[] dropped = |
| | | { trustedCertificate(new File(configDir, "server.truststore"), "client-emailaddress-cert") }; |
| | | replace(trustStore, serverTrustStore); |
| | | final FipsSwitchedTrustManagerProvider plainProvider = InitializationUtils.initializeTrustManagerProvider( |
| | | new FipsSwitchedTrustManagerProvider(), providerEntry("Plain Check", "check-test.truststore", "password"), |
| | | FileBasedTrustManagerProviderCfgDefn.getInstance()); |
| | | final FipsSwitchedTrustManagerProvider extendedProvider = new FipsSwitchedTrustManagerProvider(); |
| | | extendedProvider.fips = true; |
| | | InitializationUtils.initializeTrustManagerProvider(extendedProvider, |
| | | providerEntry("Extended Check", "check-test.truststore", "password"), |
| | | FileBasedTrustManagerProviderCfgDefn.getInstance()); |
| | | try (ServerSocket serverSocket = new ServerSocket(0, 1, InetAddress.getLoopbackAddress()); |
| | | Socket socket = SSLSocketFactory.getDefault().createSocket( |
| | | InetAddress.getLoopbackAddress(), serverSocket.getLocalPort())) |
| | | { |
| | | final X509TrustManager plain = (X509TrustManager) plainProvider.getTrustManagers()[0]; |
| | | assertThat(plain).isNotInstanceOf(X509ExtendedTrustManager.class); |
| | | final X509ExtendedTrustManager extended = (X509ExtendedTrustManager) extendedProvider.getTrustManagers()[0]; |
| | | final List<CertificateCheck> checks = Arrays.asList( |
| | | chain -> plain.checkClientTrusted(chain, "RSA"), |
| | | chain -> plain.checkServerTrusted(chain, "RSA"), |
| | | chain -> extended.checkClientTrusted(chain, "RSA"), |
| | | chain -> extended.checkServerTrusted(chain, "RSA"), |
| | | chain -> extended.checkClientTrusted(chain, "RSA", (Socket) null), |
| | | chain -> extended.checkServerTrusted(chain, "RSA", (Socket) null), |
| | | chain -> extended.checkClientTrusted(chain, "RSA", (SSLEngine) null), |
| | | chain -> extended.checkServerTrusted(chain, "RSA", (SSLEngine) null)); |
| | | for (int i = 0; i < checks.size(); i++) |
| | | { |
| | | replace(trustStore, clientTrustStore); |
| | | assertRefused(checks.get(i), dropped, "check " + i + " after the renewal that drops the certificate"); |
| | | replace(trustStore, serverTrustStore); |
| | | checks.get(i).check(dropped); |
| | | } |
| | | |
| | | // a socket connected, or an engine, with no handshake under way |
| | | final SSLEngine engine = SSLContext.getDefault().createSSLEngine(); |
| | | assertRefused(chain -> extended.checkClientTrusted(chain, "RSA", socket), dropped, "client check, socket"); |
| | | assertRefused(chain -> extended.checkServerTrusted(chain, "RSA", socket), dropped, "server check, socket"); |
| | | assertRefused(chain -> extended.checkClientTrusted(chain, "RSA", engine), dropped, "client check, engine"); |
| | | assertRefused(chain -> extended.checkServerTrusted(chain, "RSA", engine), dropped, "server check, engine"); |
| | | } |
| | | finally |
| | | { |
| | | plainProvider.finalizeTrustManagerProvider(); |
| | | extendedProvider.finalizeTrustManagerProvider(); |
| | | Files.deleteIfExists(trustStore.toPath()); |
| | | } |
| | | } |
| | | |
| | | /** A certificate check of a trust manager. */ |
| | | private interface CertificateCheck |
| | | { |
| | | void check(X509Certificate[] chain) throws CertificateException; |
| | | } |
| | | |
| | | private static void assertRefused(CertificateCheck check, X509Certificate[] chain, String description) |
| | | { |
| | | try |
| | | { |
| | | check.check(chain); |
| | | fail(description + ": the certificate is taken"); |
| | | } |
| | | catch (CertificateException expected) |
| | | { |
| | | // refused, as expected |
| | | } |
| | | } |
| | | |
| | | private static X509Certificate trustedCertificate(File trustStore, String alias) throws Exception |
| | | { |
| | | final KeyStore keyStore = KeyStore.getInstance("JKS"); |
| | | try (InputStream in = new FileInputStream(trustStore)) |
| | | { |
| | | keyStore.load(in, "password".toCharArray()); |
| | | } |
| | | return (X509Certificate) keyStore.getCertificate(alias); |
| | | } |
| | | |
| | | /** |
| | | * 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()); |
| New file |
| | |
| | | /* |
| | | * 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<FileStamp> 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); |
| | | } |
| | | } |
| | | } |