From e333af0c8fbb8d69d79f420de01ce39dcade5930 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Thu, 24 Sep 2026 08:06:21 +0000
Subject: [PATCH] Keep PKCS5S2 usable on a FIPS-restricted JCE, and name the key wrapping property when the runtime has no RSA-OAEP (#1058)

---
 opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc                                |   56 +++++
 opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java                                   |   61 +++++
 opendj-server-legacy/src/main/java/org/opends/server/extensions/AbstractPBKDF2PasswordStorageScheme.java  |    3 
 opendj-server-legacy/src/main/java/org/opends/server/extensions/PKCS5S2PasswordStorageScheme.java         |   10 
 opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java                        |   26 ++
 opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-import-export.adoc                            |    8 
 opendj-server-legacy/src/messages/org/opends/messages/core.properties                                     |    4 
 opendj-server-legacy/src/test/java/org/opends/server/extensions/PBKDF2PasswordStorageSchemeTestCase.java  |   30 ++
 opendj-server-legacy/src/main/java/org/opends/server/tools/ConfigureDS.java                               |  130 +++++++++--
 opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java                    |   58 +++++
 opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java                         |    8 
 opendj-server-legacy/src/test/java/org/opends/server/tools/ConfigureDSTestCase.java                       |  167 +++++++++++++++
 opendj-server-legacy/src/test/java/org/opends/server/extensions/PKCS5S2PasswordStorageSchemeTestCase.java |   77 +++++++
 opendj-server-legacy/src/messages/org/opends/messages/tool.properties                                     |    5 
 opendj-server-legacy/src/main/java/org/opends/server/extensions/ExtensionsConstants.java                  |    6 
 15 files changed, 603 insertions(+), 46 deletions(-)

diff --git a/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-import-export.adoc b/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-import-export.adoc
index 9a3cb46..b65a114 100644
--- a/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-import-export.adoc
+++ b/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.
 
diff --git a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc
index 7e0bee3..106f829 100644
--- a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc
+++ b/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"
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java
index a512941..09ab444 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java
+++ b/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);
   }
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java b/opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java
index c15db1c..4ab3d15 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java
+++ b/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)
   {
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/AbstractPBKDF2PasswordStorageScheme.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/AbstractPBKDF2PasswordStorageScheme.java
index c92c267..0eaa4ad 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/AbstractPBKDF2PasswordStorageScheme.java
+++ b/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;
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/ExtensionsConstants.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/ExtensionsConstants.java
index 43b7092..81ad8a0 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/ExtensionsConstants.java
+++ b/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";
-
 
 
   /**
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/PKCS5S2PasswordStorageScheme.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/PKCS5S2PasswordStorageScheme.java
index 8b881cc..dbcd4fe 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/PKCS5S2PasswordStorageScheme.java
+++ b/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)
     {
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tools/ConfigureDS.java b/opendj-server-legacy/src/main/java/org/opends/server/tools/ConfigureDS.java
index 02efe2c..8df0c77 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/tools/ConfigureDS.java
+++ b/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.
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/core.properties b/opendj-server-legacy/src/messages/org/opends/messages/core.properties
index ddbd5db..72759e0 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/core.properties
+++ b/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
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/tool.properties b/opendj-server-legacy/src/messages/org/opends/messages/tool.properties
index 930df01..34d2f5e 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/tool.properties
+++ b/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
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java b/opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java
index 0f5e74b..3d41854 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java
+++ b/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");
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java
index 37537a7..0642f4c 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java
+++ b/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.
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/extensions/PBKDF2PasswordStorageSchemeTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/extensions/PBKDF2PasswordStorageSchemeTestCase.java
index 65641de..9cc9455 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/extensions/PBKDF2PasswordStorageSchemeTestCase.java
+++ b/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;
+    });
+  }
 }
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/extensions/PKCS5S2PasswordStorageSchemeTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/extensions/PKCS5S2PasswordStorageSchemeTestCase.java
index b5cb07d..ec0d4b7 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/extensions/PKCS5S2PasswordStorageSchemeTestCase.java
+++ b/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);
+  }
 }
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/tools/ConfigureDSTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/tools/ConfigureDSTestCase.java
new file mode 100644
index 0000000..a158ce4
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/tools/ConfigureDSTestCase.java
@@ -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);
+  }
+}

--
Gitblit v1.10.0