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

Valery Kharseko
14 hours ago e333af0c8fbb8d69d79f420de01ce39dcade5930
Keep PKCS5S2 usable on a FIPS-restricted JCE, and name the key wrapping property when the runtime has no RSA-OAEP (#1058)
14 files modified
1 files added
649 ■■■■■ changed files
opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-import-export.adoc 8 ●●●●● patch | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc 56 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java 8 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java 26 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/AbstractPBKDF2PasswordStorageScheme.java 3 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/ExtensionsConstants.java 6 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/PKCS5S2PasswordStorageScheme.java 10 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/tools/ConfigureDS.java 130 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/core.properties 4 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/tool.properties 5 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java 61 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java 58 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/extensions/PBKDF2PasswordStorageSchemeTestCase.java 30 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/extensions/PKCS5S2PasswordStorageSchemeTestCase.java 77 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/tools/ConfigureDSTestCase.java 167 ●●●●● patch | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-import-export.adoc
@@ -12,7 +12,7 @@
  information: "Portions copyright [year] [name of copyright owner]".
 
  Copyright 2017 ForgeRock AS.
  Portions Copyright 2025 3A Systems LLC.
  Portions Copyright 2025-2026 3A Systems LLC.
////
:figure-caption!:
@@ -228,9 +228,9 @@
initials: AAA
$ ldifmodify \
 --sourceLDIF generated.ldif \
 --changesLDIF changes.ldif \
 --targetLDIF new.ldif
 --outputLDIF new.ldif \
 generated.ldif \
 changes.ldif
----
Notice that the resulting new LDIF file is likely to be about the same size as the source LDIF file.
opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc
@@ -789,6 +789,60 @@
====
[#install-fips]
.To Install OpenDJ Directory Server on a FIPS 140 Java Runtime
====
OpenDJ ships the Bouncy Castle FIPS provider. The `setup` command and the server register it themselves when the server key store is a BCFKS key store (`--useBcfksKeystore`), and the server registers it at start whenever the `org.openidentityplatform.opendj.fips.register` Java system property is `true`, for example through the `OPENDJ_JAVA_ARGS` environment variable. With this provider in its default mode the default configuration works as it is. The approved-only mode of the provider (the `org.bouncycastle.fips.approved_only` Java system property), which OpenDJ does not turn on, is not covered here.
The crypto manager wraps the secret keys it shares with the other servers of a replication topology with each server's public key. The transformation it uses, the `key-wrapping-transformation` property of the crypto manager, is `RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING` by default: RSA-OAEP, the key transport scheme of NIST SP 800-56B, which Bouncy Castle FIPS provides. A Java runtime whose cryptography comes from a `SunPKCS11` provider alone, such as `SunPKCS11-NSS-FIPS` on a Linux system in FIPS mode, provides no RSA-OAEP at all: its only RSA cipher is `RSA/ECB/PKCS1Padding`, which NIST SP 800-131A Rev. 2 disallows for key transport. On such a runtime the server refuses to start with the default transformation, `setup` says so, and the choice of another one is yours to make: OpenDJ does not make it for you. If you make it, keep in mind that every server of a replication topology must use the same transformation, since each server unwraps what the others wrapped.
. Install the server without starting it:
+
[source, console]
----
$ ./setup --cli --doNotStart \
 --hostname opendj.example.com \
 --ldapPort 1389 \
 --adminConnectorPort 4444 \
 --rootUserDN "cn=Directory Manager" \
 --rootUserPassword password \
 --baseDN dc=example,dc=com \
 --acceptLicense \
 --no-prompt
----
. Set the transformation in the server configuration file, which the `dsconfig` command cannot change while the server is stopped:
+
[source, console]
----
$ cat changes.ldif
dn: cn=Crypto Manager,cn=config
changetype: modify
replace: ds-cfg-key-wrapping-transformation
ds-cfg-key-wrapping-transformation: RSA/ECB/PKCS1Padding
$ ldifmodify \
 --outputLDIF /path/to/opendj/config/config.ldif.new \
 /path/to/opendj/config/config.ldif \
 changes.ldif
$ mv /path/to/opendj/config/config.ldif.new /path/to/opendj/config/config.ldif
----
. Start the server:
+
[source, console]
----
$ start-ds
----
+
Once the server runs, the `dsconfig set-crypto-manager-prop` command changes the property, and refuses a transformation the runtime does not support.
====
[#pdb-to-je]
.To Move Data from a PDB Backend to a JE Backend
====
@@ -933,7 +987,7 @@
ds-cfg-java-class: org.opends.server.backends.jeb.JEBackend
EOF
./bin/ldifmodify --targetLDIF "$LOC"/config/config.ldif.$$ --sourceLDIF "$LOC"/config/config.ldif --changesLDIF /tmp/changes_$$
./bin/ldifmodify --outputLDIF "$LOC"/config/config.ldif.$$ "$LOC"/config/config.ldif /tmp/changes_$$
if test $? -ne 0
then
  echo "Modifications failed. Restoring the original configuration"
opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java
@@ -130,6 +130,7 @@
import org.opends.server.backends.task.TaskState;
import org.opends.server.tools.BackendTypeHelper;
import org.opends.server.tools.BackendTypeHelper.BackendTypeUIAdapter;
import org.opends.server.tools.ConfigureDS;
import org.opends.server.types.HostPort;
import org.opends.server.util.CertificateManager;
import org.opends.server.util.CollectionUtils;
@@ -1363,6 +1364,13 @@
    };
    invokeLongOperation(thread);
    notifyListeners(getFormattedDoneWithLineBreak());
    // Given here rather than by ConfigureDS, whose output the listeners do not see while it runs.
    final LocalizableMessage keyWrappingWarning = ConfigureDS.unsupportedKeyWrappingTransformationWarning();
    if (keyWrappingWarning != null)
    {
      notifyListeners(getFormattedWarning(keyWrappingWarning));
      notifyListeners(getLineBreak());
    }
    checkAbort();
    configureCertificate(sec);
  }
opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java
@@ -392,6 +392,9 @@
                        requestedKeyWrappingTransformation));
        isAcceptable = false;
      }
      else if (!isKeyWrappingTransformationSupported(requestedKeyWrappingTransformation, unacceptableReasons)) {
        isAcceptable = false;
      }
      else {
        try {
          /* Note that the TrustStoreBackend not available at initial,
@@ -430,6 +433,29 @@
    return isAcceptable;
  }
  /**
   * Checks that this Java runtime provides the key wrapping transformation. Only a refusal here
   * names the key-wrapping-transformation property: the wrap which follows it also needs an MD5
   * digest and a 1024-bit RSA key, and changing the property does not help when one of those is
   * what the runtime refuses.
   */
  private static boolean isKeyWrappingTransformationSupported(
      final String transformation, final List<LocalizableMessage> unacceptableReasons)
  {
    try
    {
      Cipher.getInstance(transformation);
      return true;
    }
    catch (GeneralSecurityException ex)
    {
      logger.traceException(ex);
      unacceptableReasons.add(
          ERR_CRYPTOMGR_KEY_WRAPPING_TRANSFORMATION_UNSUPPORTED.get(transformation, getExceptionMessage(ex)));
      return false;
    }
  }
  @Override
  public ConfigChangeResult applyConfigurationChange(CryptoManagerCfg cfg)
  {
opendj-server-legacy/src/main/java/org/opends/server/extensions/AbstractPBKDF2PasswordStorageScheme.java
@@ -82,7 +82,8 @@
        }
        catch (NoSuchAlgorithmException e)
        {
            throw new InitializationException(null);
            throw new InitializationException(
                ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(getMessageDigestAlgorithm(), e), e);
        }
        this.config = configuration;
opendj-server-legacy/src/main/java/org/opends/server/extensions/ExtensionsConstants.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions copyright 2013-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.extensions;
@@ -153,11 +154,6 @@
  public static final String MESSAGE_DIGEST_ALGORITHM_PBKDF2_HMAC_SHA512 =
          "PBKDF2WithHmacSHA512";
  /**
   * The name of the pseudo-random number generator using SHA-1.
   */
  public static final String SECURE_PRNG_SHA1 = "SHA1PRNG";
  /**
opendj-server-legacy/src/main/java/org/opends/server/extensions/PKCS5S2PasswordStorageScheme.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2014 Emidio Stani & Andrea Stani
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.extensions;
@@ -85,13 +86,15 @@
  {
    try
    {
      random = SecureRandom.getInstance(SECURE_PRNG_SHA1);
      // The provider's default random source: a FIPS-restricted JCE registers no SHA1PRNG.
      random = new SecureRandom();
      // Just try to verify if the algorithm is supported
      SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2);
    }
    catch (NoSuchAlgorithmException e)
    {
      throw new InitializationException(null);
      throw new InitializationException(
          ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(MESSAGE_DIGEST_ALGORITHM_PBKDF2, e), e);
    }
  }
@@ -246,8 +249,7 @@
  {
    try
    {
      final SecureRandom random = SecureRandom.getInstance(SECURE_PRNG_SHA1);
      return encodeWithRandomSalt(plaintext, saltBytes, random);
      return encodeWithRandomSalt(plaintext, saltBytes, new SecureRandom());
    }
    catch (DirectoryException e)
    {
opendj-server-legacy/src/main/java/org/opends/server/tools/ConfigureDS.java
@@ -1208,45 +1208,36 @@
   */
  private void updateCryptoCipher() throws ConfigureDSException
  {
    final CryptoManagerCfgDefn cryptoManager = CryptoManagerCfgDefn.getInstance();
    final StringPropertyDefinition prop = cryptoManager.getKeyWrappingTransformationPropertyDefinition();
    String defaultCipher = null;
    final DefaultBehaviorProvider<?> p = prop.getDefaultBehaviorProvider();
    if (p instanceof DefinedDefaultBehaviorProvider)
    {
      final Collection<?> defaultValues = ((DefinedDefaultBehaviorProvider<?>) p).getDefaultValues();
      if (!defaultValues.isEmpty())
      {
        defaultCipher = defaultValues.iterator().next().toString();
      }
    }
    final String defaultCipher = defaultKeyWrappingTransformation();
    if (defaultCipher != null)
    {
      // Check that the default cipher is supported by the JVM.
      final String cipher;
      try
      {
        Cipher.getInstance(defaultCipher);
        cipher = supportedKeyWrappingTransformation(defaultCipher);
      }
      catch (final GeneralSecurityException ex)
      {
        // The cipher is not supported: try to find an alternative one.
        final String alternativeCipher = getAlternativeCipher();
        if (alternativeCipher != null)
        // The default stays, and the server will refuse to start with it: there is no secure
        // transformation to fall back to (#776), so the administrator has to choose one. Under
        // setup this stream reaches the setup log only, and the installer gives the warning
        // itself (see unsupportedKeyWrappingTransformationWarning()).
        printWrappedText(err, unsupportedKeyWrappingTransformationWarning(defaultCipher, ex));
        return;
      }
      if (!cipher.equals(defaultCipher))
      {
        try
        {
          try
          {
            updateConfigEntryWithAttribute(
                DN_CRYPTO_MANAGER,
                ATTR_CRYPTO_CIPHER_KEY_WRAPPING_TRANSFORMATION,
                CoreSchema.getDirectoryStringSyntax(),
                alternativeCipher);
          }
          catch (final Exception e)
          {
            throw new ConfigureDSException(e, ERR_CONFIGDS_CANNOT_UPDATE_CRYPTO_MANAGER.get(e));
          }
          updateConfigEntryWithAttribute(
              DN_CRYPTO_MANAGER,
              ATTR_CRYPTO_CIPHER_KEY_WRAPPING_TRANSFORMATION,
              CoreSchema.getDirectoryStringSyntax(),
              cipher);
        }
        catch (final Exception e)
        {
          throw new ConfigureDSException(e, ERR_CONFIGDS_CANNOT_UPDATE_CRYPTO_MANAGER.get(e));
        }
      }
    }
@@ -1327,6 +1318,83 @@
  }
  /**
   * Returns the warning to give when this Java runtime supports neither the default key wrapping
   * transformation of the crypto manager nor an alternative to it: the server will then refuse to
   * start until the administrator sets one. The installer calls this itself, since what
   * {@code configMain} writes while it runs under setup reaches the setup log only.
   *
   * @return The warning, or {@code null} when the runtime supports a transformation.
   */
  public static LocalizableMessage unsupportedKeyWrappingTransformationWarning()
  {
    final String defaultCipher = defaultKeyWrappingTransformation();
    if (defaultCipher == null)
    {
      return null;
    }
    try
    {
      supportedKeyWrappingTransformation(defaultCipher);
      return null;
    }
    catch (final GeneralSecurityException ex)
    {
      return unsupportedKeyWrappingTransformationWarning(defaultCipher, ex);
    }
  }
  private static LocalizableMessage unsupportedKeyWrappingTransformationWarning(
      final String defaultCipher, final GeneralSecurityException ex)
  {
    return WARN_CONFIGDS_KEY_WRAPPING_TRANSFORMATION_UNSUPPORTED.get(defaultCipher, ex.getMessage());
  }
  /** Returns the default key wrapping transformation of the crypto manager, or {@code null}. */
  private static String defaultKeyWrappingTransformation()
  {
    final StringPropertyDefinition prop =
        CryptoManagerCfgDefn.getInstance().getKeyWrappingTransformationPropertyDefinition();
    final DefaultBehaviorProvider<?> p = prop.getDefaultBehaviorProvider();
    if (p instanceof DefinedDefaultBehaviorProvider)
    {
      final Collection<?> defaultValues = ((DefinedDefaultBehaviorProvider<?>) p).getDefaultValues();
      if (!defaultValues.isEmpty())
      {
        return defaultValues.iterator().next().toString();
      }
    }
    return null;
  }
  /**
   * Returns the key wrapping transformation this Java runtime supports: the default one when it
   * does, otherwise the OAEP alternative of {@link #getAlternativeCipher()}.
   *
   * @param defaultCipher
   *          The default key wrapping transformation of the crypto manager.
   * @return The transformation to configure.
   * @throws GeneralSecurityException
   *           If the runtime supports neither, with the reason the default one is not.
   */
  static String supportedKeyWrappingTransformation(final String defaultCipher) throws GeneralSecurityException
  {
    try
    {
      Cipher.getInstance(defaultCipher);
      return defaultCipher;
    }
    catch (final GeneralSecurityException ex)
    {
      final String alternativeCipher = getAlternativeCipher();
      if (alternativeCipher == null)
      {
        throw ex;
      }
      return alternativeCipher;
    }
  }
  /**
   * Returns a cipher that is supported by the JVM we are running at.
   * Returns <CODE>null</CODE> if no alternative cipher could be found.
   * @return a cipher that is supported by the JVM we are running at.
opendj-server-legacy/src/messages/org/opends/messages/core.properties
@@ -1376,3 +1376,7 @@
 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
ERR_CRYPTOMGR_KEY_WRAPPING_TRANSFORMATION_UNSUPPORTED_766=This Java runtime \
 does not provide the key wrapping transformation %s: %s. The \
 key-wrapping-transformation property of the crypto manager must name a \
 transformation which the runtime supports
opendj-server-legacy/src/messages/org/opends/messages/tool.properties
@@ -2634,6 +2634,11 @@
SUPPLEMENT_DESCRIPTION_BACKEND_TOOL_SUBCMD_LIST_INDEX_STATUS_20016=\
  <xinclude:include href="variablelist-backendstat-index-status.xml" />
INFO_DESCRIPTION_DEFAULT_ADD_20017=Legacy argument for ForgeRock OpenDJ compatibility.
WARN_CONFIGDS_KEY_WRAPPING_TRANSFORMATION_UNSUPPORTED_20018=This Java runtime \
 supports neither the default key wrapping transformation %s nor an alternative \
 to it: %s. The server will not start until the key-wrapping-transformation \
 property of the crypto manager names a transformation which the runtime \
 supports; set it in config/config.ldif before starting the server
INFO_LDAP_CONN_PROMPT_SECURITY_LDAP=LDAP
INFO_LDAP_CONN_PROMPT_SECURITY_USE_SSL=LDAP with SSL
INFO_LDAP_CONN_PROMPT_SECURITY_USE_START_TLS=LDAP with StartTLS
opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java
@@ -57,6 +57,8 @@
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.file.Paths;
import java.security.Provider;
import java.security.Security;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
@@ -68,6 +70,7 @@
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
@@ -321,6 +324,64 @@
    }
  }
  /**
   * Runs {@code action} while no installed JCE provider offers the given service, the way a
   * FIPS-restricted JVM lacks it, and puts the withdrawn providers back where they were
   * afterwards. The {@code standIns} are installed ahead of the remaining providers for the
   * duration, for whatever the action still needs that only the withdrawn providers offered.
   *
   * @param type
   *          The JCE service type, e.g. {@code SecureRandom}.
   * @param algorithm
   *          The algorithm to withdraw, e.g. {@code SHA1PRNG}.
   * @param action
   *          What to run without the service.
   * @param standIns
   *          Providers to install first while the service is withdrawn.
   * @throws Exception
   *           If the action fails, or if the service could not be withdrawn.
   */
  public static void withoutJceService(final String type, final String algorithm,
      final Callable<Void> action, final Provider... standIns) throws Exception
  {
    final String service = type + "." + algorithm;
    final List<Provider> installed = Arrays.asList(Security.getProviders());
    final Provider[] offering = Security.getProviders(service);
    assertNotNull(offering, "no installed provider offers " + service + ": nothing to withdraw");
    for (Provider provider : offering)
    {
      Security.removeProvider(provider.getName());
    }
    final List<Provider> addedStandIns = new ArrayList<>();
    for (int i = 0; i < standIns.length; i++)
    {
      if (Security.insertProviderAt(standIns[i], i + 1) != -1)
      {
        addedStandIns.add(standIns[i]);
      }
    }
    try
    {
      assertNull(Security.getProviders(service), service + " is still offered: the fixture does not withdraw it");
      action.call();
    }
    finally
    {
      for (Provider standIn : addedStandIns)
      {
        Security.removeProvider(standIn.getName());
      }
      // Ascending original positions, so that the list comes back in its original order.
      for (Provider provider : installed)
      {
        if (Arrays.asList(offering).contains(provider))
        {
          Security.insertProviderAt(provider, installed.indexOf(provider) + 1);
        }
      }
    }
  }
  public static void startServer() throws Exception
  {
    System.setProperty(PROPERTY_RUNNING_UNIT_TESTS, "true");
opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java
@@ -39,7 +39,10 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.Provider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;
import java.util.UUID;
@@ -298,6 +301,61 @@
  }
  /**
   A key wrapping transformation this Java runtime cannot provide is refused, at start (where
   the refusal is what keeps the server from starting) as on a change, and the refusal has to
   name the property to set, not only the cipher which failed: on a FIPS-restricted runtime
   without RSA-OAEP that is all the administrator has to go on.
   */
  @Test
  public void testUnsupportedKeyWrappingTransformationIsRefusedNamingTheProperty() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final String unsupported = "RSA/ECB/NoSuchPadding";
    final List<LocalizableMessage> why = new ArrayList<>();
    final boolean acceptable =
        cm.isConfigurationChangeAcceptable(withProperty(cfg, "getKeyWrappingTransformation", unsupported), why);
    assertThat(acceptable).isFalse();
    assertThat(why).hasSize(1);
    assertThat(why.get(0).ordinal()).isEqualTo(ERR_CRYPTOMGR_KEY_WRAPPING_TRANSFORMATION_UNSUPPORTED.ordinal());
    assertThat(why.get(0).toString()).contains(unsupported).contains("key-wrapping-transformation");
  }
  /**
   A transformation the runtime provides, refused because of the rest of the check (here the
   MD5 digest of the instance key identifier), is not reported as a matter of the property:
   changing key-wrapping-transformation would not help.
   */
  @Test
  public void testKeyWrappingRefusalForAnotherCauseDoesNotNameTheProperty() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final String supported = "RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING";
    assertThat(supported).isNotEqualTo(cfg.getKeyWrappingTransformation());
    final List<LocalizableMessage> why = new ArrayList<>();
    // Withdrawing MD5 withdraws the SUN provider, whose SHA-1 digest the OAEP cipher still needs.
    final Provider sha1Only = new Provider("Sha1OnlyDigest", "1.0", "SHA-1 digest only") {};
    sha1Only.put("MessageDigest.SHA-1", "sun.security.provider.SHA");
    sha1Only.put("Alg.Alias.MessageDigest.SHA1", "SHA-1");
    withoutJceService("MessageDigest", "MD5", () ->
    {
      assertThat(cm.isConfigurationChangeAcceptable(withProperty(cfg, "getKeyWrappingTransformation", supported), why))
          .isFalse();
      return null;
    }, sha1Only);
    assertThat(why).hasSize(1);
    final String reason = why.get(0).toString();
    assertThat(why.get(0).ordinal()).as(reason).isEqualTo(ERR_CRYPTOMGR_CANNOT_GET_PREFERRED_KEY_WRAPPING_CIPHER.ordinal());
    assertThat(reason).contains("MD5").doesNotContain("key-wrapping-transformation");
  }
  /**
   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.
opendj-server-legacy/src/test/java/org/opends/server/extensions/PBKDF2PasswordStorageSchemeTestCase.java
@@ -12,13 +12,19 @@
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.extensions;
import org.forgerock.opendj.server.config.meta.PBKDF2PasswordStorageSchemeCfgDefn;
import org.opends.server.api.PasswordStorageScheme;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.opends.server.TestCaseUtils.withoutJceService;
import static org.testng.Assert.*;
/**
 * A set of test cases for the PBKDF2 password storage scheme.
@@ -70,4 +76,28 @@
  {
    return PBKDF2PasswordStorageScheme.encodeOffline(plaintextBytes);
  }
  /**
   * When the derivation is unavailable, the failure has to name the algorithm: a message-less
   * InitializationException leaves the administrator with a server which does not start and
   * no word on why.
   */
  @Test
  public void testInitializationFailureNamesTheMissingAlgorithm() throws Exception
  {
    withoutJceService("SecretKeyFactory", "PBKDF2WithHmacSHA1", () ->
    {
      try
      {
        getScheme();
        fail("initialization succeeded without PBKDF2WithHmacSHA1");
      }
      catch (InitializationException e)
      {
        assertNotNull(e.getMessageObject(), "the failure carries no message");
        assertTrue(e.getMessage().contains("for the PBKDF2WithHmacSHA1 algorithm"), e.getMessage());
      }
      return null;
    });
  }
}
opendj-server-legacy/src/test/java/org/opends/server/extensions/PKCS5S2PasswordStorageSchemeTestCase.java
@@ -12,15 +12,25 @@
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.extensions;
import java.security.SecureRandom;
import java.util.concurrent.Callable;
import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.server.config.meta.PKCS5S2PasswordStorageSchemeCfgDefn;
import org.opends.server.api.PasswordStorageScheme;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.opends.server.TestCaseUtils.withoutJceService;
import static org.testng.Assert.*;
/**
 * A set of test cases for the PKCS5S2 password storage scheme.
 */
@@ -123,4 +133,71 @@
    return PKCS5S2PasswordStorageScheme.encodeOffline(plaintextBytes);
  }
  /**
   * A FIPS-restricted JCE (SunPKCS11-NSS-FIPS, BC-FIPS) registers no {@code SHA1PRNG}: the
   * scheme has to take the provider's default random source, as the other PBKDF2 schemes do,
   * instead of failing to initialize and taking the server start down with it.
   */
  @Test
  public void testInitializesAndEncodesWithoutSha1Prng() throws Exception
  {
    withoutSha1Prng(() ->
    {
      final PasswordStorageScheme<?> scheme = getScheme();
      final ByteString plaintext = ByteString.valueOfUtf8("correct horse battery staple");
      assertTrue(scheme.passwordMatches(plaintext, scheme.encodePassword(plaintext)));
      return null;
    });
  }
  /** Same for the offline encoder, which a tool may call before any scheme is initialized. */
  @Test
  public void testEncodesOfflineWithoutSha1Prng() throws Exception
  {
    withoutSha1Prng(() ->
    {
      final ByteString plaintext = ByteString.valueOfUtf8("correct horse battery staple");
      final String encoded = PKCS5S2PasswordStorageScheme.encodeOffline(plaintext.toByteArray());
      final String prefix = "{" + getScheme().getStorageSchemeName() + "}";
      assertTrue(encoded.startsWith(prefix), encoded);
      assertTrue(getScheme().passwordMatches(plaintext, ByteString.valueOfUtf8(encoded.substring(prefix.length()))));
      return null;
    });
  }
  /**
   * When the derivation itself is unavailable, the failure has to name the algorithm: a
   * message-less InitializationException leaves the administrator with a server which does
   * not start and no word on why.
   */
  @Test
  public void testInitializationFailureNamesTheMissingAlgorithm() throws Exception
  {
    withoutJceService("SecretKeyFactory", "PBKDF2WithHmacSHA1", () ->
    {
      try
      {
        getScheme();
        fail("initialization succeeded without PBKDF2WithHmacSHA1");
      }
      catch (InitializationException e)
      {
        assertNotNull(e.getMessageObject(), "the failure carries no message");
        assertTrue(e.getMessage().contains("for the PBKDF2WithHmacSHA1 algorithm"), e.getMessage());
      }
      return null;
    });
  }
  /**
   * Withdraws every provider registering {@code SHA1PRNG} (the SUN provider on a stock JDK),
   * with BC-FIPS standing in for the digests the derivation still needs from it.
   */
  private static void withoutSha1Prng(final Callable<Void> action) throws Exception
  {
    final BouncyCastleFipsProvider bcFips = new BouncyCastleFipsProvider();
    // Seed the provider's DRBG while the JDK's own random source is still installed.
    SecureRandom.getInstance("DEFAULT", bcFips).nextBytes(new byte[8]);
    withoutJceService("SecureRandom", "SHA1PRNG", action, bcFips);
  }
}
opendj-server-legacy/src/test/java/org/opends/server/tools/ConfigureDSTestCase.java
New file
@@ -0,0 +1,167 @@
/*
 * 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.tools;
import static org.opends.server.TestCaseUtils.withoutJceService;
import static org.testng.Assert.*;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Provider;
import java.security.Security;
import javax.crypto.Cipher;
import org.forgerock.i18n.LocalizableMessage;
import org.opends.server.TestCaseUtils;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
/** Tests the setup-time configuration done by {@link ConfigureDS}. */
@SuppressWarnings("javadoc")
public class ConfigureDSTestCase extends ToolsTestCase
{
  private static final String DEFAULT_KEY_WRAPPING_TRANSFORMATION = "RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING";
  /** The tool locates the server lock file when it is created, from the server environment. */
  @BeforeClass
  public void startServer() throws Exception
  {
    TestCaseUtils.startServer();
  }
  /** A runtime which has the default transformation keeps it. */
  @Test
  public void testKeyWrappingTransformationStaysTheDefaultWhereTheRuntimeHasIt() throws Exception
  {
    assertEquals(ConfigureDS.supportedKeyWrappingTransformation(DEFAULT_KEY_WRAPPING_TRANSFORMATION),
        DEFAULT_KEY_WRAPPING_TRANSFORMATION);
  }
  /**
   * A runtime without RSA-OAEP under either spelling gets no transformation at all, rather than
   * a weaker one: setup is to say so, and the administrator is to choose.
   */
  @Test
  public void testNoKeyWrappingTransformationIsChosenWhereTheRuntimeHasNoRsaOaep() throws Exception
  {
    withoutJceService("Cipher", "RSA", () ->
    {
      try
      {
        final String chosen = ConfigureDS.supportedKeyWrappingTransformation(DEFAULT_KEY_WRAPPING_TRANSFORMATION);
        fail("a transformation was chosen on a runtime without RSA-OAEP: " + chosen);
      }
      catch (GeneralSecurityException expected)
      {
        assertTrue(expected.getMessage().contains(DEFAULT_KEY_WRAPPING_TRANSFORMATION), expected.getMessage());
      }
      return null;
    });
  }
  /**
   * A runtime whose only RSA cipher is PKCS#1 v1.5, as a SunPKCS11 provider on its own, does not
   * get that transformation as the fallback either (#776). Withdrawing {@code Cipher.RSA} alone
   * cannot show it, since the PKCS#1 v1.5 transformation goes through that service as well.
   */
  @Test
  public void testNoKeyWrappingTransformationIsChosenWhereTheRuntimeHasOnlyPkcs1() throws Exception
  {
    withoutJceService("Cipher", "RSA", () ->
    {
      final Provider pkcs1Only = new Provider("Pkcs1OnlyRsa", "1.0", "RSA with PKCS#1 v1.5 padding only") {};
      pkcs1Only.put("Cipher.RSA/ECB/PKCS1Padding", "com.sun.crypto.provider.RSACipher");
      Security.insertProviderAt(pkcs1Only, 1);
      try
      {
        assertEquals(Cipher.getInstance("RSA/ECB/PKCS1Padding").getProvider().getName(), pkcs1Only.getName(),
            "the fixture offers no PKCS#1 v1.5 transformation");
        try
        {
          final String chosen = ConfigureDS.supportedKeyWrappingTransformation(DEFAULT_KEY_WRAPPING_TRANSFORMATION);
          fail("a transformation was chosen on a runtime whose only RSA cipher is PKCS#1 v1.5: " + chosen);
        }
        catch (GeneralSecurityException expected)
        {
          assertTrue(expected.getMessage().contains(DEFAULT_KEY_WRAPPING_TRANSFORMATION), expected.getMessage());
        }
      }
      finally
      {
        Security.removeProvider(pkcs1Only.getName());
      }
      return null;
    });
  }
  /** Where the runtime has the default transformation, setup has nothing to warn about. */
  @Test
  public void testNoWarningWhereTheRuntimeHasTheDefaultKeyWrappingTransformation() throws Exception
  {
    assertNull(ConfigureDS.unsupportedKeyWrappingTransformationWarning());
    assertEquals(updateCryptoCipher(), "");
  }
  /**
   * Where the runtime has no RSA-OAEP, the warning names the default transformation and the
   * property to set, both the one the installer gives and the one configure-ds writes; and the
   * configuration keeps the default (the configuration handler is not even there to change it).
   */
  @Test
  public void testWarningNamesTheTransformationAndThePropertyWhereTheRuntimeHasNoRsaOaep() throws Exception
  {
    withoutJceService("Cipher", "RSA", () ->
    {
      final LocalizableMessage warning = ConfigureDS.unsupportedKeyWrappingTransformationWarning();
      assertNotNull(warning, "no warning on a runtime without RSA-OAEP");
      assertNamesTransformationAndProperty(warning.toString());
      assertNamesTransformationAndProperty(updateCryptoCipher());
      return null;
    });
  }
  private static void assertNamesTransformationAndProperty(final String warning)
  {
    final String text = warning.replaceAll("\\s+", " ");
    assertTrue(text.startsWith("This Java runtime supports neither the default key wrapping transformation "
        + DEFAULT_KEY_WRAPPING_TRANSFORMATION + " "), text);
    assertTrue(text.contains("the key-wrapping-transformation property"), text);
  }
  /**
   * Runs the key wrapping step of configure-ds on its own, and returns what it wrote to its error
   * stream. The tool has no configuration handler here, so a step which tried to change the
   * configuration would fail.
   */
  private static String updateCryptoCipher() throws Exception
  {
    final ByteArrayOutputStream err = new ByteArrayOutputStream();
    final Constructor<ConfigureDS> constructor =
        ConfigureDS.class.getDeclaredConstructor(String[].class, OutputStream.class, OutputStream.class);
    constructor.setAccessible(true);
    final ConfigureDS tool = constructor.newInstance(new String[0], new ByteArrayOutputStream(), err);
    final Method updateCryptoCipher = ConfigureDS.class.getDeclaredMethod("updateCryptoCipher");
    updateCryptoCipher.setAccessible(true);
    updateCryptoCipher.invoke(tool);
    return new String(err.toByteArray(), StandardCharsets.UTF_8);
  }
}