mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
18 hours ago 89454950e49290222563e937cfaf7aa9dfa1ff33
opendj-config/src/test/java/org/forgerock/opendj/config/CryptoManagerCfgDefnTest.java
New file
@@ -0,0 +1,50 @@
/*
 * 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.forgerock.opendj.config;
import static org.forgerock.opendj.config.AdministratorAction.Type.SERVER_RESTART;
import static org.testng.Assert.assertEquals;
import org.forgerock.opendj.server.config.meta.CryptoManagerCfgDefn;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@SuppressWarnings("javadoc")
public class CryptoManagerCfgDefnTest extends ConfigTestCase {
    @DataProvider
    public Object[][] sslProperties() {
        CryptoManagerCfgDefn defn = CryptoManagerCfgDefn.getInstance();
        return new Object[][] {
            { "ssl-cert-nickname", defn.getSSLCertNicknamePropertyDefinition() },
            { "ssl-protocol", defn.getSSLProtocolPropertyDefinition() },
            { "ssl-cipher-suite", defn.getSSLCipherSuitePropertyDefinition() },
            { "ssl-encryption", defn.getSSLEncryptionPropertyDefinition() },
        };
    }
    /**
     * The crypto manager reads its SSL properties once, when it is created, and replication
     * keeps what it read for the life of the session, so a change to any of them only takes
     * effect once the server is restarted. A component restart is not an option either: the
     * crypto manager has no enabled property, so it cannot be disabled and re-enabled.
     */
    @Test(dataProvider = "sslProperties")
    public void sslPropertiesRequireAServerRestart(String name, PropertyDefinition<?> property) {
        assertEquals(property.getAdministratorAction().getType(), SERVER_RESTART,
            "unexpected administrator action for " + name);
    }
}
opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-change-certs.adoc
@@ -541,6 +541,8 @@
 --set ssl-cert-nickname:repl-cert \
 --no-prompt
----
+
The property is declared as requiring a server restart, which `dsconfig list-properties` shows, and the server writes the change to `logs/errors` saying that the new value is stored but not in force. That warning also names a nickname the `ads-truststore` does not hold, so a mistyped alias is caught while the server is still presenting the certificate it started with; if the `ads-truststore` cannot be read, the warning says so in place of the nicknames it would have named. The warning is repeated on every later change to the crypto manager until the server is restarted, as the value it compares against is the one the server started with.
.. Restart the server for the change to take effect.
+
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CryptoManagerConfiguration.xml
@@ -14,6 +14,7 @@
  Copyright 2007-2008 Sun Microsystems, Inc.
  Portions Copyright 2011 ForgeRock AS.
  Portions Copyright 2026 3A Systems, LLC.
  ! -->
<adm:managed-object name="crypto-manager" plural-name="crypto-managers"
  package="org.forgerock.opendj.server.config"
@@ -213,13 +214,7 @@
      use in SSL or TLS communication.
    </adm:synopsis>
    <adm:requires-admin-action>
      <adm:none>
        <adm:synopsis>
          Changes to this property take effect immediately but
          only impact new SSL/TLS-based sessions created after the
          change.
        </adm:synopsis>
      </adm:none>
      <adm:server-restart />
    </adm:requires-admin-action>
    <adm:default-behavior>
      <adm:alias>
@@ -244,13 +239,7 @@
      for use in SSL or TLS communication.
    </adm:synopsis>
    <adm:requires-admin-action>
      <adm:none>
        <adm:synopsis>
          Changes to this property take effect immediately but
          only impact new SSL/TLS-based sessions created after the
          change.
        </adm:synopsis>
      </adm:none>
      <adm:server-restart />
    </adm:requires-admin-action>
    <adm:default-behavior>
      <adm:alias>
@@ -275,13 +264,7 @@
      communication between two <adm:product-name /> server components.
    </adm:synopsis>
    <adm:requires-admin-action>
      <adm:none>
        <adm:synopsis>
          Changes to this property take effect immediately but
          only impact new SSL/TLS-based sessions created after the
          change.
        </adm:synopsis>
      </adm:none>
      <adm:server-restart />
    </adm:requires-admin-action>
    <adm:default-behavior>
      <adm:defined>
@@ -297,5 +280,14 @@
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property-reference name="ssl-cert-nickname" />
  <adm:property-reference name="ssl-cert-nickname">
    <!--
      The component restart declared by the common property definition does not apply
      here: the crypto manager has no enabled property, so it cannot be disabled and
      re-enabled, and it reads this property once, when it is created.
    -->
    <adm:requires-admin-action>
      <adm:server-restart />
    </adm:requires-admin-action>
  </adm:property-reference>
</adm:managed-object>
opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java
@@ -39,6 +39,7 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.UUID;
@@ -67,6 +68,7 @@
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.PropertyDefinition;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.config.server.ConfigurationChangeListener;
@@ -81,6 +83,7 @@
import org.forgerock.opendj.ldap.schema.CoreSchema;
import org.forgerock.opendj.ldap.schema.ObjectClass;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.server.config.meta.CryptoManagerCfgDefn;
import org.forgerock.opendj.server.config.server.CryptoManagerCfg;
import org.forgerock.util.Reject;
import org.opends.admin.ads.ADSContext;
@@ -293,7 +296,7 @@
    if (! isConfigurationChangeAcceptable(config, why)) {
      throw new InitializationException(why.get(0));
    }
    applyConfigurationChange(config);
    applyCryptoConfiguration(config);
    // Secure replication related...
    sslCertNicknames = config.getSSLCertNickname();
@@ -430,13 +433,110 @@
  @Override
  public ConfigChangeResult applyConfigurationChange(CryptoManagerCfg cfg)
  {
    final ConfigChangeResult ccr = new ConfigChangeResult();
    applyCryptoConfiguration(cfg);
    reportSslPropertiesWhichNeedARestart(cfg, ccr);
    return ccr;
  }
  /**
   * Applies the cryptographic properties, the ones this crypto manager re-reads whenever
   * they change. The SSL properties are not among them: they are read once, in the
   * constructor, which is why this method is called from there rather than
   * {@link #applyConfigurationChange(CryptoManagerCfg)}.
   *
   * @param cfg
   *          The configuration to apply.
   */
  private void applyCryptoConfiguration(CryptoManagerCfg cfg)
  {
    preferredDigestAlgorithm = cfg.getDigestAlgorithm();
    preferredMACAlgorithm = cfg.getMacAlgorithm();
    preferredMACAlgorithmKeyLengthBits = cfg.getMacKeyLength();
    preferredCipherTransformation = cfg.getCipherTransformation();
    preferredCipherTransformationKeyLengthBits = cfg.getCipherKeyLength();
    preferredKeyWrappingTransformation = cfg.getKeyWrappingTransformation();
    return new ConfigChangeResult();
  }
  /**
   * Reports every SSL property whose new value the running server does not use, so that a
   * change which is accepted into the configuration without being in force says so instead
   * of passing for an applied one. The properties are declared as requiring a server
   * restart, and this is what the administrator is told at the moment of the change.
   * <p>
   * The values compared against are the ones the server started with, so a value which
   * differs is reported on the change which made it differ and again on every later change
   * to the crypto manager until the server is restarted: a reminder that the value stored
   * is not the one in force, rather than a report of the one change which stored it.
   *
   * @param cfg
   *          The configuration which has just been stored.
   * @param ccr
   *          The result to report the required administrative action in.
   */
  private void reportSslPropertiesWhichNeedARestart(CryptoManagerCfg cfg, ConfigChangeResult ccr)
  {
    final CryptoManagerCfgDefn defn = CryptoManagerCfgDefn.getInstance();
    final SortedSet<String> newCertNicknames = cfg.getSSLCertNickname();
    if (reportPropertyWhichNeedsARestart(defn.getSSLCertNicknamePropertyDefinition(),
        sslCertNicknames, newCertNicknames, ccr))
    {
      reportCertNicknamesTheTrustStoreDoesNotHold(newCertNicknames, ccr);
    }
    reportPropertyWhichNeedsARestart(defn.getSSLProtocolPropertyDefinition(),
        sslProtocols, cfg.getSSLProtocol(), ccr);
    reportPropertyWhichNeedsARestart(defn.getSSLCipherSuitePropertyDefinition(),
        sslCipherSuites, cfg.getSSLCipherSuite(), ccr);
    reportPropertyWhichNeedsARestart(defn.getSSLEncryptionPropertyDefinition(),
        sslEncryption, cfg.isSSLEncryption(), ccr);
  }
  private boolean reportPropertyWhichNeedsARestart(
      PropertyDefinition<?> property, Object inForce, Object configured, ConfigChangeResult ccr)
  {
    if (Objects.equals(inForce, configured))
    {
      return false;
    }
    ccr.setAdminActionRequired(true);
    ccr.addMessage(WARN_CRYPTOMGR_SSL_PROPERTY_REQUIRES_RESTART.get(property.getName()));
    return true;
  }
  /**
   * Reports each newly configured certificate nickname which the trust store does not hold.
   * The same nicknames are looked up again whenever an SSL context is built, but only once
   * the server has been restarted with them: reporting them here names a nickname which
   * would present no certificate while the administrator is still making the change.
   *
   * @param certNicknames
   *          The certificate nicknames which have just been configured.
   * @param ccr
   *          The result to report the missing nicknames in.
   */
  private void reportCertNicknamesTheTrustStoreDoesNotHold(SortedSet<String> certNicknames, ConfigChangeResult ccr)
  {
    try
    {
      final TrustStoreBackend trustStoreBackend = getTrustStoreBackend();
      for (String nickname : certNicknames)
      {
        if (!trustStoreBackend.containsKeyWithAlias(nickname))
        {
          ccr.addMessage(WARN_CRYPTOMGR_SSL_CERT_NICKNAME_NOT_IN_TRUST_STORE.get(
              nickname, trustStoreBackend.getTrustStoreFile()));
        }
      }
    }
    catch (ConfigException | DirectoryException e)
    {
      // A trust store which cannot be read now costs the administrator this report and
      // nothing else: the change is stored either way, and a nickname it does not hold is
      // reported again, as an error, when the SSL context is built after the restart. The
      // cost is named next to the restart the change asks for, rather than paid silently.
      logger.traceException(e);
      ccr.addMessage(WARN_CRYPTOMGR_SSL_CERT_NICKNAME_LOOKUP_FAILED.get(e.getMessageObject()));
    }
  }
opendj-server-legacy/src/messages/org/opends/messages/core.properties
@@ -1361,3 +1361,18 @@
 configured, no certificate is presented at all and peers requiring client \
 authentication, replication servers included, reject the connection. Import the \
 key pair under that nickname into that trust store, or configure a nickname it holds
WARN_CRYPTOMGR_SSL_PROPERTY_REQUIRES_RESTART_763=The new value of the %s property \
 of the crypto manager has been stored in the configuration but is not in force. The \
 crypto manager reads that property when it is created and replication keeps the value \
 it read, so server to server connections go on using the value the server started with \
 until the server is restarted
WARN_CRYPTOMGR_SSL_CERT_NICKNAME_NOT_IN_TRUST_STORE_764=The certificate nickname \
 "%s" now configured in the ssl-cert-nickname property of the crypto manager is not held \
 by the trust store %s used for server to server communication. Import the key pair under \
 that nickname into that trust store before restarting the server, or the server will \
 present no certificate under that nickname to its replication peers
WARN_CRYPTOMGR_SSL_CERT_NICKNAME_LOOKUP_FAILED_765=The certificate nicknames now \
 configured in the ssl-cert-nickname property of the crypto manager could not be looked \
 up in the trust store used for server to server communication: %s. A nickname that \
 trust store does not hold is only reported once the server has been restarted with it, \
 so check the nicknames against the trust store before restarting the server
opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java
@@ -21,6 +21,7 @@
import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
import static org.forgerock.opendj.ldap.ModificationType.*;
import static org.forgerock.opendj.ldap.SearchScope.*;
import static org.opends.messages.CoreMessages.*;
import static org.opends.server.TestCaseUtils.*;
import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.crypto.CryptoManagerImpl.CERT_NICKNAME_CHECK_INTERVAL_NANOS;
@@ -34,22 +35,29 @@
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.TreeSet;
import java.util.UUID;
import javax.crypto.Mac;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.LDAPConnectionFactory;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SSLContextBuilder;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.opendj.ldif.ConnectionEntryReader;
import org.forgerock.opendj.server.config.server.CryptoManagerCfg;
import org.forgerock.util.Options;
import org.opends.admin.ads.ADSContext;
import org.opends.admin.ads.util.BlindTrustManager;
@@ -131,6 +139,186 @@
        .as("and the next interval starts from that look up").isFalse();
  }
  /**
   The crypto manager reads its SSL properties when it is created and replication keeps
   what it read, so a change to ssl-cert-nickname is accepted into the configuration
   without being in force. The change listener says so rather than staying silent.
   */
  @Test
  public void testSslCertNicknameChangeReportsThatARestartIsRequired() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final ConfigChangeResult ccr = cm.applyConfigurationChange(withSslCertNicknames(cfg, "no-such-nickname"));
    assertThat(ccr.getResultCode()).as("the change is accepted").isEqualTo(ResultCode.SUCCESS);
    assertThat(ccr.adminActionRequired()).as("but it is not in force").isTrue();
    assertThat(ccr.getMessages())
        .as("and the configuration change is not applied silently")
        .contains(WARN_CRYPTOMGR_SSL_PROPERTY_REQUIRES_RESTART.get("ssl-cert-nickname"));
  }
  @DataProvider
  public Object[][] sslPropertiesWhichNeedARestart()
  {
    // Any value the one in force does not equal: the server starts with no protocol and no
    // cipher suite configured, and with ds-cfg-ssl-encryption: false.
    return new Object[][] {
      { "getSSLProtocol", new TreeSet<>(Arrays.asList("TLSv1.3")), "ssl-protocol" },
      { "getSSLCipherSuite", new TreeSet<>(Arrays.asList("TLS_AES_256_GCM_SHA384")), "ssl-cipher-suite" },
      { "isSSLEncryption", true, "ssl-encryption" },
    };
  }
  /**
   The other SSL properties are read once as well, and used to say that no action was
   required. A change to any of them is reported the same way and, unlike a nickname, is
   looked up nowhere: the restart is the whole report.
   */
  @Test(dataProvider = "sslPropertiesWhichNeedARestart")
  public void testSslPropertyChangeReportsThatARestartIsRequired(
      final String getter, final Object value, final String property) throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final ConfigChangeResult ccr = cm.applyConfigurationChange(withProperty(cfg, getter, value));
    assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
    assertThat(ccr.adminActionRequired()).isTrue();
    assertThat(ccr.getMessages()).containsExactly(WARN_CRYPTOMGR_SSL_PROPERTY_REQUIRES_RESTART.get(property));
  }
  /**
   The report is about what changed: a change to another property of the crypto manager,
   or a re-read of the configuration as it stands, asks for no restart.
   */
  @Test
  public void testUnchangedSslPropertiesRequireNoRestart() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final ConfigChangeResult ccr = cm.applyConfigurationChange(cfg);
    assertThat(ccr.adminActionRequired()).isFalse();
    assertThat(ccr.getMessages()).isEmpty();
  }
  /**
   The cryptographic properties are the ones the change listener does apply: a new digest
   algorithm is in use as soon as the change is made, and asks for nothing.
   */
  @Test
  public void testCryptoPropertyChangeIsApplied() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final String inForce = cm.getPreferredMessageDigestAlgorithm();
    final String changed = "SHA-512";
    assertThat(changed).as("the change is a change").isNotEqualTo(inForce);
    try
    {
      final ConfigChangeResult ccr = cm.applyConfigurationChange(withProperty(cfg, "getDigestAlgorithm", changed));
      assertThat(cm.getPreferredMessageDigestAlgorithm()).as("the new algorithm is in use").isEqualTo(changed);
      assertThat(ccr.adminActionRequired()).as("and no action is asked for").isFalse();
      assertThat(ccr.getMessages()).isEmpty();
    }
    finally
    {
      // The crypto manager is the one every test of the running server shares.
      cm.applyConfigurationChange(cfg);
    }
  }
  /**
   A nickname which the trust store does not hold presents no certificate to the
   replication peers, and until the server is restarted the mistake shows up nowhere. The
   nicknames are looked up when the change is made, and only the missing one is reported.
   */
  @Test
  public void testSslCertNicknameChangeReportsTheNicknamesTheTrustStoreDoesNotHold() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final TrustStoreBackend trustStore = (TrustStoreBackend) getServerContext()
        .getBackendConfigManager().getLocalBackendById(ID_ADS_TRUST_STORE_BACKEND);
    final ConfigChangeResult ccr =
        cm.applyConfigurationChange(withSslCertNicknames(cfg, ADS_CERTIFICATE_ALIAS, "no-such-nickname"));
    assertThat(ccr.getMessages())
        .as("the nickname which is not in the trust store is named")
        .contains(WARN_CRYPTOMGR_SSL_CERT_NICKNAME_NOT_IN_TRUST_STORE.get(
            "no-such-nickname", trustStore.getTrustStoreFile()))
        .as("while the one it holds is not")
        .doesNotContain(WARN_CRYPTOMGR_SSL_CERT_NICKNAME_NOT_IN_TRUST_STORE.get(
            ADS_CERTIFICATE_ALIAS, trustStore.getTrustStoreFile()));
  }
  /**
   The lookup is best effort: a trust store which cannot be read costs the administrator
   the report of the nicknames it does not hold, and the change is stored either way. That
   cost is named next to the restart the change asks for, rather than paid silently.
   */
  @Test
  public void testSslCertNicknameChangeReportsATrustStoreItCannotRead() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final TrustStoreBackend trustStore = (TrustStoreBackend) getServerContext()
        .getBackendConfigManager().getLocalBackendById(ID_ADS_TRUST_STORE_BACKEND);
    final Path trustStoreFile = StaticUtils.getFileForPath(trustStore.getTrustStoreFile()).toPath();
    final Path movedAway = trustStoreFile.resolveSibling(trustStoreFile.getFileName() + ".moved-away");
    // The trust store is opened on every lookup, so it cannot be read for exactly as long
    // as the file is out of the way.
    Files.move(trustStoreFile, movedAway);
    final ConfigChangeResult ccr;
    try
    {
      ccr = cm.applyConfigurationChange(withSslCertNicknames(cfg, "no-such-nickname"));
    }
    finally
    {
      Files.move(movedAway, trustStoreFile);
    }
    assertThat(ccr.getResultCode()).as("the change is stored either way").isEqualTo(ResultCode.SUCCESS);
    assertThat(ccr.adminActionRequired()).isTrue();
    assertThat(ccr.getMessages()).extracting(LocalizableMessage::ordinal)
        .as("the restart is asked for, and the lookup which could not be done is named in place of its result")
        .containsExactly(WARN_CRYPTOMGR_SSL_PROPERTY_REQUIRES_RESTART.ordinal(),
            WARN_CRYPTOMGR_SSL_CERT_NICKNAME_LOOKUP_FAILED.ordinal());
    assertThat(ccr.getMessages().get(1).toString())
        .as("with the trust store which could not be read")
        .contains(trustStore.getTrustStoreFile());
  }
  /**
   Returns the crypto manager configuration as it stands, with the ssl-cert-nickname
   property answering the provided nicknames, so that a change to that property is applied
   without the configuration of the running server being modified.
   */
  private static CryptoManagerCfg withSslCertNicknames(final CryptoManagerCfg cfg, final String... nicknames)
  {
    return withProperty(cfg, "getSSLCertNickname", new TreeSet<>(Arrays.asList(nicknames)));
  }
  /**
   Returns the crypto manager configuration as it stands, with the named getter answering
   the provided value instead of the configured one.
   */
  private static CryptoManagerCfg withProperty(final CryptoManagerCfg cfg, final String getter, final Object value)
  {
    return (CryptoManagerCfg) Proxy.newProxyInstance(
        CryptoManagerCfg.class.getClassLoader(),
        new Class<?>[] { CryptoManagerCfg.class },
        (proxy, method, args) -> getter.equals(method.getName()) ? value : method.invoke(cfg, args));
  }
  @Test
  public void testImportKeysUsesLatestKey()
      throws Exception {