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 9a3cb46055..b65a114adb 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 @@ replace: initials 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 7e0bee33e3..106f82998a 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 @@ At this point you can use OpenDJ directory server, or you can perform additional ==== +[#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 @@ replace: ds-cfg-java-class 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 0d3096cfc2..bcb26d92cb 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 @@ -133,6 +133,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; @@ -1346,6 +1347,13 @@ public void abort() }; 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 c15db1c66a..4ab3d15b3b 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 @@ public boolean isConfigurationChangeAcceptable( requestedKeyWrappingTransformation)); isAcceptable = false; } + else if (!isKeyWrappingTransformationSupported(requestedKeyWrappingTransformation, unacceptableReasons)) { + isAcceptable = false; + } else { try { /* Note that the TrustStoreBackend not available at initial, @@ -430,6 +433,29 @@ public boolean isConfigurationChangeAcceptable( 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 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 c92c267307..0eaa4ad4af 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 @@ public void initializePasswordStorageScheme(PBKDF2PasswordStorageSchemeCfg confi } 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 43b709238a..81ad8a030a 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 class ExtensionsConstants 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 8b881cc22d..dbcd4feab4 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 @@ public void initializePasswordStorageScheme(PKCS5S2PasswordStorageSchemeCfg conf { 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 @@ private static byte[] encodeWithRandomSalt(ByteString plaintext, byte[] saltByte { 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 02efe2c534..8df0c77aa5 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 updateCryptoManagerCertNickname() throws ConfigureDSException */ 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)); } } } @@ -1326,6 +1317,83 @@ private Entry removeAttribute(Entry entry, String attrName) return duplicateEntry; } + /** + * 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 null if no alternative cipher could be found. 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 ddbd5db640..72759e0df7 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 @@ WARN_CRYPTOMGR_SSL_CERT_NICKNAME_LOOKUP_FAILED_765=The certificate nicknames now 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 930df01dfe..34d2f5e210 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 @@ ERR_FILE_NOT_FULLY_READABLE_20015=Could not completely read file '%s' SUPPLEMENT_DESCRIPTION_BACKEND_TOOL_SUBCMD_LIST_INDEX_STATUS_20016=\ 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 0f5e74b6db..3d41854f7c 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 @@ static class TestPorts } } + /** + * 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 action, final Provider... standIns) throws Exception + { + final String service = type + "." + algorithm; + final List 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 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 37537a72b7..0642f4cb4e 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; @@ -297,6 +300,61 @@ public void testSslCertNicknameChangeReportsATrustStoreItCannotRead() throws Exc .contains(trustStore.getTrustStoreFile()); } + /** + 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 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 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 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 65641de2d1..9cc9455c66 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 @@ protected String encodeOffline(final byte[] plaintextBytes) throws DirectoryExce { 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 b5cb07d339..ec0d4b7b49 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 @@ protected String encodeOffline(final byte[] plaintextBytes) throws DirectoryExce 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 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 0000000000..a158ce47e3 --- /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 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); + } +}