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

Maxim Thomas
21 hours ago 3b20faae34218cdbb72008a3695f748b34cdff4a
[#1056] Add the RSASVE/KDF-SHA256/AESKWP key wrapping transformation for runtimes without RSA-OAEP (#1194)

Closes #1056.

## Summary

A Java runtime whose cryptography comes from a `SunPKCS11` provider
alone, such as `SunPKCS11-NSS-FIPS` on a Linux system in FIPS mode,
offers no RSA-OAEP. Its only RSA padding for key transport is PKCS#1
v1.5, which NIST SP 800-131A Rev. 2 disallows. On such a runtime the
crypto manager has no acceptable value for
`key-wrapping-transformation`.

This PR adds one: `RSASVE/KDF-SHA256/AESKWP`. It is a key wrapping
scheme of OpenDJ's own, built on the hybrid key transport of NIST SP
800-56B Rev. 2, section 9.3. It needs only `RSA/ECB/NoPadding`, SHA-256
and AES-KWP from the runtime. The scheme is opt-in only: the default
stays `RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING`, and neither `setup` nor the
server selects the new value by itself.

## The scheme

A new package-private class,
`org.opends.server.crypto.RsaSveKeyTransport`, wraps a secret key in
three steps:

| Step | Standard | What it does |
| --- | --- | --- |
| RSASVE.GENERATE | SP 800-56B Rev. 2, 7.2.1 | Draws z uniformly in [2,
n-2] by rejection sampling over nLen random bytes. Raw RSA of I2OS(z,
nLen) gives C, normalized to exactly nLen bytes. |
| One-step KDF | SP 800-56C Rev. 2, 4.1 | KWK = SHA-256(00000001 ‖ Z ‖
FixedInfo), used as an AES-256 key. |
| AES-KWP | SP 800-38F | W = KWP(KWK, secret key). Asks for `AESWrapPad`
first, the only name Bouncy Castle FIPS knows, so that Bouncy Castle
FIPS does the key wrap when it is installed first; then
`AES/KWP/NoPadding`. SunJCE and SunPKCS11 register `AESWrapPad` as an
alias of their `AES/KWP/NoPadding`. |

Before the wrap, the certificate of the receiving server is checked the
way `Cipher.init(WRAP_MODE, certificate)` checks it for RSA-OAEP: a
critical key usage without keyEncipherment is refused.

The wrapped key is `len16(C) ‖ C ‖ W`. It goes, hex-encoded, into the
fourth field of the existing `ds-cfg-symmetric-key` value
`wrappingKeyID:transformation:algorithm:hexWrappedKey`. The value format
does not change.

FixedInfo is length-prefixed (a 4-byte big-endian length before each
variable field) and binds, in order:
- the canonical scheme name as AlgorithmID;
- an empty PartyUInfo;
- the wrapping key ID (field 1 of the stored value) as PartyVInfo;
- the wrapped key's algorithm (field 3);
- L = 256.

So none of the fields stored next to the wrapped key can be changed
without the unwrap failing.

**No nonce.** Keys are wrapped for a server from its published
certificate alone, without that server taking part, so no nonce of the
receiving server enters the derivation. The scheme is therefore "RSASVE
+ one-step KDF + AES-KWP", built on section 9.3 but not the KAS1-basic
scheme to the letter. The documentation says so, and leaves to each
deployment's FIPS 140-3 assessment whether it counts as an approved key
transport method there.

**Unwrap.** It splits `len16(C) ‖ C ‖ W`. Before the private key
decrypts anything, it refuses an instance key pair without its private
key or without an RSA public key, and a C which is not on exactly nLen
bytes (RSASVE.RECOVER) or is not an integer with 1 < c < n − 1 (RSADP).
The modulus comes from the instance key certificate, because a private
key held by a PKCS#11 token does not always implement `RSAKey`. It then
raw-RSA-decrypts C and left-pads the result to nLen bytes, because
Bouncy Castle FIPS drops leading zero bytes. Last, it derives the KWK
and KWP-unwraps W. Both ciphers are asked for, and the raw RSA cipher is
initialized with the private key, before the decryption, since none of
this depends on the value: a runtime which lacks a cipher is reported
naming it, and a provider which cannot use the private key with its own
reason, not as a value which cannot be unwrapped. Every failure from the
decryption on is reported as one `InvalidKeyException`,
`RSASVE/KDF-SHA256/AESKWP cannot unwrap the key`, with the original
exception as its cause. So how a value is refused does not depend on
what the private key recovered from it. Bad keys and malformed values
are reported only as `GeneralSecurityException` subtypes, the only type
`CryptoManagerImpl` catches around the wrap and the unwrap.

## Crypto manager

`CryptoManagerImpl` routes to the new class by the transformation name,
matched case-insensitively like a JCE transformation, in three places:
- `isKeyWrappingTransformationSupported` checks that the runtime offers
the three algorithms. A missing one is refused through the existing
`ERR_CRYPTOMGR_KEY_WRAPPING_TRANSFORMATION_UNSUPPORTED`, which names the
property. The existing trial wrap that follows it then runs the whole
RSASVE wrap at configuration change and start time.
- `encodeSymmetricKeyAttribute` wraps with the scheme, passing the
certificate so that its key usage is checked.
- `decodeSymmetricKeyAttribute` unwraps by the name each stored value
records. Existing RSA-OAEP values keep decoding as before. It reads the
instance key certificate once: the instance key ID and, on the scheme's
branch, the public key both come from it.

No message and no message ordinal is added. `ConfigureDS`, the installer
and the setup warning are unchanged.

## Topology

A server without this change that meets a value wrapped with the scheme
fails in `Cipher.getInstance`. So does a server with this change on a
runtime without one of the three algorithms, such as Java 11 without
Bouncy Castle FIPS, which has no AES-KWP: since it does not use the
scheme itself, it refuses nothing. Both report
`ERR_CRYPTOMGR_DECODE_SYMMETRIC_KEY_ATTRIBUTE_DECIPHER`, which
`CryptoManagerSync` only logs; the second names the missing algorithm.
The property description and the install guide therefore say that every
server of the replication topology must run a version supporting the
scheme, on a runtime which offers the three algorithms, before any
server is switched to it.

## Documentation

- The description of `key-wrapping-transformation` in
`CryptoManagerConfiguration.xml`, shown by `dsconfig` help and the
generated configuration reference.
- The FIPS install procedure, `[#install-fips]` in `chap-install.adoc`.
It corrects what a SunPKCS11 runtime offers (`RSA/ECB/PKCS1Padding` and
`RSA/ECB/NoPadding`), describes the scheme, and uses it in the
`changes.ldif` example.

Both say that the `SunPKCS11` provider of OpenJDK 17 offers no AES-KWP,
so that the server refuses the value on an OpenJDK 17 runtime with that
provider alone, and that the provider offers it from OpenJDK 19 on
(`P11KeyWrapCipher` is absent from Temurin 17.0.7 and 17.0.11, present
in 19.0.1, 21.0.2, 24.0.2, 25.0.2 and Zulu 26).

## Tests

- `RsaSveKeyTransportTestCase` (new, 45 cases):
- round trips of five keys from 8 to 32 bytes (DES, AES-128, HmacSHA1,
DESede, AES-256), including the 20-byte HmacSHA1 key that needs KWP's
padding;
- a known-answer test pinning the stored layout, the KDF and the KWP
output, whose vector was computed independently of the implementation,
for the key wrap of both the JDK and Bouncy Castle FIPS;
- rejection of secret values outside [2, n-2];
- a secret value with a leading zero byte under Bouncy Castle FIPS;
- a 1024-bit modulus;
- another private key and changed stored fields failing the unwrap;
- malformed values refused with `InvalidKeyException`;
- encapsulated values of another length than the modulus, or outside 1 <
c < n − 1, refused before the decryption under both the JDK providers
and Bouncy Castle FIPS;
- one message for every unwrap failure after the decryption;
- a provider which drops the leading zero bytes of the encapsulated
value: the value is still stored on the modulus length;
- the support check and the unwrap naming the missing key wrap;
- the unwrap reporting the RSA cipher's own refusal of the private key,
not "cannot unwrap the key";
- an instance key pair without its private key or with a non-RSA public
key refused before any cipher;
- the key wrap served by Bouncy Castle FIPS when it is installed first,
even where the JDK offers `AES/KWP/NoPadding`.

The round trips of the five keys run under both the JDK providers and
Bouncy Castle FIPS (`AESWrapPad` only, and a raw RSA that drops leading
zeros).
- `CryptoManagerTestCase` (+8 cases):
- the scheme is accepted under both spellings;
- the scheme is refused, naming `key-wrapping-transformation` and
`AES/KWP/NoPadding`, on a runtime without AES-KWP;
- a key generated while it is configured is published under its name,
unwraps, and decrypts the data it encrypted;
- each value is unwrapped by the transformation it records: an RSA-OAEP
value while the scheme is configured, and the scheme's value, also with
its name in lower case, once RSA-OAEP is configured again;
- a secret key is re-wrapped for another instance only where its
certificate's key usage allows it: a critical key usage without
keyEncipherment is refused under both RSA-OAEP and the scheme, while one
with keyEncipherment, or one that is not critical, is accepted.

The cases which need AES-KWP run with Bouncy Castle FIPS first, so that
JDK 11 has it.
- `TestCaseUtils` gets two fixtures: `withBcFipsFirst` installs Bouncy
Castle FIPS first and puts the providers back afterwards (the existing
approved-only fixture of `CryptoManagerTestCase` now uses it), and
`withoutKeyWrap` runs an action on a runtime without AES-KWP, as Java 11
without Bouncy Castle FIPS is.
- The `CryptoManagerTestCase` case which generates a key wrapped with
the scheme deletes it afterwards, so that no later restart of the test
instance meets a value a Java 11 runtime cannot unwrap.

Local runs:

| Class | JDK 26.0.1 | JDK 11.0.25 |
| --- | --- | --- |
| `RsaSveKeyTransportTestCase` | 45/0/0/0 | 45/0/0/10 (the JDK-provider
rows and the 1024-bit case need `AES/KWP/NoPadding`, which JDK 11 lacks)
|
| `CryptoManagerTestCase` | 58/0/0/0 (50 before) | 58/0/0/0 |
| `GetSymmetricKeyExtendedOperationTestCase` | 2/0/0/0 | 2/0/0/0 |

Results are tests/failures/errors/skipped.

## Outstanding: one manual run on a SunPKCS11-only runtime

No CI cell runs a runtime whose only provider is SunPKCS11. One manual
run on a RHEL FIPS-mode OpenJDK 17+ with `SunPKCS11-NSS-FIPS` only is
still outstanding:

The runtime's `SunPKCS11` has to offer `AES/KWP/NoPadding`. In the
upstream OpenJDK sources, 17.0.7 and 17.0.11 register no AES-KWP in
`SunPKCS11`, while 19.0.1 and later do; a RHEL build may differ. On a
runtime without it, the server refuses the value at step 1 and names the
property.

1. Set `key-wrapping-transformation` to `RSASVE/KDF-SHA256/AESKWP` and
start the server. This also exercises the wrap side through the trial
wrap.
2. Make the server generate a secret key: an encrypted backup, or a
backend with confidentiality enabled.
3. Restart. Check that no `ERR_TRUSTSTORESYNC_EXCEPTION` appears and
that the data still decrypts. This is the only check of the unwrap side,
which no configuration-time check covers: a raw RSA decrypt with the
token-held private key, the KWP unwrap, and the import of the unwrapped
key.
4. Add a second server, to exercise the re-wrap for another instance.

If step 3 fails, data under that key cannot be read until the server
runs on a runtime that can unwrap it. The wrapped value stays in
`cn=secret keys`, so this is recoverable.

Also unverified without a token: wrapping a sensitive PKCS#11 secret
key, whose `getEncoded()` returns null, relies on the provider's
`C_WrapKey`.
5 files modified
2 files added
1497 ■■■■■ changed files
opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc 6 ●●●●● patch | view | raw | blame | history
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CryptoManagerConfiguration.xml 18 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java 69 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/crypto/RsaSveKeyTransport.java 402 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java 58 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java 256 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/crypto/RsaSveKeyTransportTestCase.java 688 ●●●●● patch | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc
@@ -844,7 +844,9 @@
====
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.
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 RSA cipher offers PKCS#1 v1.5 padding, `RSA/ECB/PKCS1Padding`, which NIST SP 800-131A Rev. 2 disallows for key transport, and no padding, `RSA/ECB/NoPadding`. 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.
For such a runtime OpenDJ provides a key wrapping scheme of its own, `RSASVE/KDF-SHA256/AESKWP`, built on the hybrid key transport of NIST SP 800-56B Rev. 2, section 9.3. It encapsulates a random secret value with `RSA/ECB/NoPadding`, derives a key wrapping key from it with the one-step key derivation of NIST SP 800-56C Rev. 2 over SHA-256, and wraps the secret key with the AES key wrap with padding of NIST SP 800-38F (`AES/KWP/NoPadding`). No nonce of the receiving server enters the key derivation, because a server wraps keys for another server from its published certificate alone: the scheme is therefore not the KAS1-basic scheme of NIST SP 800-56B to the letter, and whether it is an approved key transport method for a FIPS 140-3 deployment is for the assessment of that deployment to decide. The runtime has to offer these three algorithms: when it lacks one, the server refuses the value and names the `key-wrapping-transformation` property. The `SunPKCS11` provider of OpenJDK 17, for example, offers no AES-KWP, so on an OpenJDK 17 runtime whose cryptography comes from that provider alone the server refuses the value at start; the provider offers AES-KWP from OpenJDK 19 on. A server which does not support the scheme cannot unwrap the secret keys wrapped with it, and neither can a server whose Java runtime lacks one of these algorithms: Java 11, for example, offers no AES-KWP unless the Bouncy Castle FIPS provider is registered. Such a server refuses nothing, since it does not use the scheme itself: it only logs an error for each key it cannot unwrap. So every server of the replication topology must run an OpenDJ version which supports the scheme, on a Java runtime which offers these three algorithms, before any server uses it. Setting the property back does not re-wrap the secret keys already wrapped with the scheme: every server of the replication topology must keep supporting it for as long as those keys are in use.
. Install the server without starting it:
+
@@ -871,7 +873,7 @@
dn: cn=Crypto Manager,cn=config
changetype: modify
replace: ds-cfg-key-wrapping-transformation
ds-cfg-key-wrapping-transformation: RSA/ECB/PKCS1Padding
ds-cfg-key-wrapping-transformation: RSASVE/KDF-SHA256/AESKWP
$ ldifmodify \
 --outputLDIF /path/to/opendj/config/config.ldif.new \
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CryptoManagerConfiguration.xml
@@ -185,6 +185,24 @@
      The preferred key wrapping transformation for the directory server. This value must
      be the same for all server instances in a replication topology.
    </adm:synopsis>
    <adm:description>
      Besides the transformations of the Java runtime, the value
      RSASVE/KDF-SHA256/AESKWP selects a scheme built on the hybrid key
      transport of NIST SP 800-56B Rev. 2, section 9.3, without a nonce
      of the receiving server, for a Java runtime which offers no
      RSA-OAEP: it needs only RSA/ECB/NoPadding, SHA-256 and the AES key
      wrap with padding (AES/KWP/NoPadding) from the runtime. The
      SunPKCS11 provider of OpenJDK 17 offers no AES/KWP/NoPadding, so a
      server whose runtime has that provider alone refuses this value;
      the provider offers it from OpenJDK 19 on. A server which does not
      support this value cannot unwrap the keys wrapped with it, and
      neither can a server whose Java runtime lacks one of these
      algorithms, such as Java 11 without Bouncy Castle FIPS: it only
      logs an error for each key it cannot unwrap. So every server of
      the replication topology must run a version which supports this
      value, on a Java runtime which offers these algorithms, before any
      server uses it.
    </adm:description>
    <adm:requires-admin-action>
      <adm:none>
        <adm:synopsis>
opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java
@@ -26,6 +26,7 @@
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
@@ -459,17 +460,25 @@
  }
  /**
   * 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 a MAC
   * key of the mac-algorithm property, and changing the key wrapping property does not help when
   * that is what the runtime refuses.
   * Checks that this Java runtime provides the key wrapping transformation, or every algorithm the
   * RSASVE scheme needs when the transformation names that scheme. Only a refusal here names the
   * key-wrapping-transformation property: the wrap which follows it also needs a MAC key of the
   * mac-algorithm property, and changing the key wrapping property does not help when that is what
   * the runtime refuses.
   */
  private static boolean isKeyWrappingTransformationSupported(
      final String transformation, final List<LocalizableMessage> unacceptableReasons)
  {
    try
    {
      Cipher.getInstance(transformation);
      if (RsaSveKeyTransport.isScheme(transformation))
      {
        RsaSveKeyTransport.checkSupported();
      }
      else
      {
        Cipher.getInstance(transformation);
      }
      return true;
    }
    catch (GeneralSecurityException ex)
@@ -847,6 +856,9 @@
   * wrappedKeyAlgorithm ::= e.g., DESede
   * hexifiedwrappedKey ::= 0123456789abcdef01...
   * </pre>
   * The wrapping transformation is a transformation of the Java runtime, or
   * RSASVE/KDF-SHA256/AESKWP, the scheme of {@link RsaSveKeyTransport}, whose
   * wrapped key is the one {@link RsaSveKeyTransport#wrap} returns.
   *
   * @param wrappingKeyID The key identifier of the wrapping key. This
   * parameter is the first field in the encoded value and identifies
@@ -915,10 +927,17 @@
              = CertificateFactory.getInstance("X.509");
      final Certificate certificate = cf.generateCertificate(
              new ByteArrayInputStream(wrappingKeyCertificateData));
      final Cipher wrapper
              = Cipher.getInstance(wrappingTransformationName);
      wrapper.init(Cipher.WRAP_MODE, certificate);
      byte[] wrappedKey = wrapper.wrap(secretKey);
      final byte[] wrappedKey;
      if (RsaSveKeyTransport.isScheme(wrappingTransformationName)) {
        wrappedKey = RsaSveKeyTransport.wrap(certificate,
                secretKey, wrappingKeyID, secureRandom);
      }
      else {
        final Cipher wrapper
                = Cipher.getInstance(wrappingTransformationName);
        wrapper.init(Cipher.WRAP_MODE, certificate);
        wrappedKey = wrapper.wrap(secretKey);
      }
      wrappedKeyElement = StaticUtils.bytesToHexNoSpace(wrappedKey);
    }
    catch (GeneralSecurityException ex) {
@@ -938,6 +957,8 @@
   * Takes an encoded ds-cfg-symmetric-key attribute value and the
   * associated key algorithm name, and returns an initialized
   * {@code java.security.Key} object.
   * <p>
   * Package private for testing.
   * @param symmetricKeyAttribute The encoded
   * ds-cfg-symmetric-key-attribute value.
   * @return A SecretKey object instantiated with the key data,
@@ -946,7 +967,7 @@
   * @throws CryptoManagerException If there is a problem decomposing
   * the supplied attribute value or unwrapping the encoded key.
   */
  private SecretKey decodeSymmetricKeyAttribute(
  SecretKey decodeSymmetricKeyAttribute(
          final String symmetricKeyAttribute)
          throws CryptoManagerException {
    // Initial decomposition.
@@ -983,7 +1004,9 @@
    }
    // Confirm key can be unwrapped at this instance.
    final String instanceKeyID = getInstanceKeyID();
    final byte[] instanceKeyCertificate =
            getInstanceKeyCertificateFromLocalTruststore();
    final String instanceKeyID = getInstanceKeyID(instanceKeyCertificate);
    if (! wrappingKeyIDElement.equals(instanceKeyID)) {
      return null;
    }
@@ -1008,11 +1031,25 @@
    // Unwrap secret key.
    SecretKey secretKey;
    try {
      final Cipher unwrapper
              = Cipher.getInstance(wrappingTransformationElement);
      unwrapper.init(Cipher.UNWRAP_MODE, privateKey);
      secretKey = (SecretKey)unwrapper.unwrap(wrappedKeyCipherTextElement,
              wrappedKeyAlgorithmElement, Cipher.SECRET_KEY);
      if (RsaSveKeyTransport.isScheme(wrappingTransformationElement)) {
        // The public key comes from the instance key certificate whose
        // identifier was just matched: a private key held by a token does
        // not always expose its modulus.
        final Certificate certificate = CertificateFactory.getInstance("X.509")
                .generateCertificate(
                        new ByteArrayInputStream(instanceKeyCertificate));
        secretKey = RsaSveKeyTransport.unwrap(
                new KeyPair(certificate.getPublicKey(), privateKey),
                wrappedKeyCipherTextElement, wrappedKeyAlgorithmElement,
                wrappingKeyIDElement);
      }
      else {
        final Cipher unwrapper
                = Cipher.getInstance(wrappingTransformationElement);
        unwrapper.init(Cipher.UNWRAP_MODE, privateKey);
        secretKey = (SecretKey)unwrapper.unwrap(wrappedKeyCipherTextElement,
                wrappedKeyAlgorithmElement, Cipher.SECRET_KEY);
      }
    } catch(GeneralSecurityException ex) {
      logger.traceException(ex);
      throw new CryptoManagerException(
opendj-server-legacy/src/main/java/org/opends/server/crypto/RsaSveKeyTransport.java
New file
@@ -0,0 +1,402 @@
/*
 * 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.crypto;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.Set;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
 * The key wrapping scheme for a Java runtime which offers no RSA-OAEP, such as one whose only
 * provider is SunPKCS11: a scheme of OpenDJ's own, built on the hybrid key transport of NIST
 * SP 800-56B Rev. 2, section 9.3, but not the KAS1-basic scheme to the letter, since no nonce of
 * the receiving instance enters the derivation. The RSA secret value encapsulation (RSASVE,
 * section 7.2.1) establishes a secret value with the public key of the receiving instance, the
 * one-step key derivation of NIST SP 800-56C Rev. 2 over SHA-256 turns it into a key wrapping key,
 * and the AES key wrap with padding of NIST SP 800-38F (KWP) wraps the secret key with that key.
 * The runtime has to offer a raw RSA cipher, a SHA-256 digest and AES-KWP, nothing more.
 * <p>
 * A wrapped key is the encapsulated secret value, on as many bytes as the RSA modulus and preceded
 * by that length on two bytes, followed by the KWP-wrapped secret key. The derivation binds the
 * scheme name, the identifier of the instance key the secret value is encapsulated with and the
 * algorithm of the wrapped key, the fields stored next to the wrapped key in a ds-cfg-symmetric-key
 * value, so that none of them can be changed without the unwrapping failing. No nonce of the
 * receiving instance enters the derivation: keys are wrapped for an instance from its published
 * certificate alone, without the instance taking part.
 */
final class RsaSveKeyTransport
{
  /**
   * The name of the scheme: the value of the key-wrapping-transformation property which selects
   * it, and the transformation recorded in the ds-cfg-symmetric-key values it produces.
   */
  static final String NAME = "RSASVE/KDF-SHA256/AESKWP";
  private static final String RSA_TRANSFORMATION = "RSA/ECB/NoPadding";
  private static final String DIGEST_ALGORITHM = "SHA-256";
  /**
   * The names of AES-KWP: first the only one Bouncy Castle FIPS knows, so that it does the key wrap
   * when it is installed first, as in the FIPS mode of OpenDJ, even where the Java runtime offers
   * AES-KWP too; then the standard one.
   */
  private static final String[] KEY_WRAP_TRANSFORMATIONS = { "AESWrapPad", "AES/KWP/NoPadding" };
  /** The length in bits of the key wrapping key: one SHA-256 hash. */
  private static final int KEY_WRAPPING_KEY_LENGTH_BITS = 256;
  /** The counter of the only hash the key derivation computes. */
  private static final byte[] KDF_COUNTER = { 0, 0, 0, 1 };
  /** The object identifier of the key usage extension of an X.509 certificate. */
  private static final String KEY_USAGE_OID = "2.5.29.15";
  /** The position of keyEncipherment in the key usage of an X.509 certificate. */
  private static final int KEY_ENCIPHERMENT = 2;
  private RsaSveKeyTransport()
  {
    // Not instantiable.
  }
  /**
   * Returns whether a key wrapping transformation names this scheme. Like the names of the Java
   * transformations, the name is not case sensitive.
   *
   * @param transformation
   *          The key wrapping transformation.
   * @return Whether it names this scheme.
   */
  static boolean isScheme(final String transformation)
  {
    return NAME.equalsIgnoreCase(transformation);
  }
  /**
   * Checks that this Java runtime offers every algorithm the scheme needs.
   *
   * @throws GeneralSecurityException
   *           If it does not, naming the first algorithm missing.
   */
  static void checkSupported() throws GeneralSecurityException
  {
    Cipher.getInstance(RSA_TRANSFORMATION);
    MessageDigest.getInstance(DIGEST_ALGORITHM);
    getKeyWrapCipher();
  }
  /**
   * Wraps a secret key for the instance which holds the private key of a certificate. Like
   * {@code Cipher.init(Cipher.WRAP_MODE, certificate)}, it refuses an X.509 certificate whose key
   * usage extension is critical and does not allow keyEncipherment.
   *
   * @param certificate
   *          The certificate of the instance, with its RSA public key.
   * @param secretKey
   *          The secret key to wrap.
   * @param wrappingKeyID
   *          The identifier of the instance key, as stored next to the wrapped key.
   * @param random
   *          The random generator of the secret value.
   * @return The wrapped key.
   * @throws GeneralSecurityException
   *           If the key usage of the certificate does not allow wrapping keys, if its public key
   *           is not an RSA key, or if the runtime cannot wrap the key.
   */
  static byte[] wrap(final Certificate certificate, final SecretKey secretKey, final String wrappingKeyID,
      final SecureRandom random) throws GeneralSecurityException
  {
    if (certificate instanceof X509Certificate)
    {
      final X509Certificate x509Certificate = (X509Certificate) certificate;
      final Set<String> criticalExtensions = x509Certificate.getCriticalExtensionOIDs();
      final boolean[] keyUsage = x509Certificate.getKeyUsage();
      if (criticalExtensions != null && criticalExtensions.contains(KEY_USAGE_OID)
          && keyUsage != null && keyUsage.length > KEY_ENCIPHERMENT && !keyUsage[KEY_ENCIPHERMENT])
      {
        throw new InvalidKeyException(NAME + " needs a certificate whose key usage allows keyEncipherment");
      }
    }
    return wrap(certificate.getPublicKey(), secretKey, wrappingKeyID, random);
  }
  /**
   * Wraps a secret key for the instance which holds the private key of an RSA public key.
   *
   * @param publicKey
   *          The RSA public key of the instance.
   * @param secretKey
   *          The secret key to wrap.
   * @param wrappingKeyID
   *          The identifier of the instance key, as stored next to the wrapped key.
   * @param random
   *          The random generator of the secret value.
   * @return The wrapped key.
   * @throws GeneralSecurityException
   *           If the public key is not an RSA key, or if the runtime cannot wrap the key.
   */
  static byte[] wrap(final PublicKey publicKey, final SecretKey secretKey, final String wrappingKeyID,
      final SecureRandom random) throws GeneralSecurityException
  {
    final BigInteger modulus = getModulus(publicKey);
    final int length = (modulus.bitLength() + 7) / 8;
    final byte[] secretValue = generateSecretValue(modulus, length, random);
    try
    {
      final Cipher rsa = Cipher.getInstance(RSA_TRANSFORMATION);
      rsa.init(Cipher.ENCRYPT_MODE, publicKey);
      // On as many bytes as the modulus whatever the provider: the unwrapping takes the length of
      // the encapsulated value for the length of the secret value.
      final byte[] encapsulated = toLength(rsa.doFinal(secretValue), length);
      final Cipher keyWrap = getKeyWrapCipher();
      keyWrap.init(Cipher.WRAP_MODE, deriveKeyWrappingKey(secretValue, wrappingKeyID, secretKey.getAlgorithm()));
      final byte[] wrapped = keyWrap.wrap(secretKey);
      return ByteBuffer.allocate(2 + encapsulated.length + wrapped.length)
          .putShort((short) encapsulated.length)
          .put(encapsulated)
          .put(wrapped)
          .array();
    }
    finally
    {
      Arrays.fill(secretValue, (byte) 0);
    }
  }
  /**
   * Unwraps a secret key which {@link #wrap} wrapped with the public key of this instance.
   * <p>
   * The encapsulated value is checked against the modulus before the private key decrypts it, and
   * every failure from the decryption on is reported with one and the same message, the cause
   * aside: what the private key recovers from a value does not show in how the value is refused.
   * A runtime which lacks one of the ciphers, or whose RSA cipher cannot use the private key, is
   * reported before that, with its own reason.
   *
   * @param instanceKeyPair
   *          The RSA key pair of this instance. The modulus is taken from its public key, since a
   *          private key held by a token does not always expose it.
   * @param wrappedKey
   *          The wrapped key.
   * @param keyAlgorithm
   *          The algorithm of the wrapped key, as stored next to it.
   * @param wrappingKeyID
   *          The identifier of the instance key, as stored next to the wrapped key.
   * @return The secret key.
   * @throws GeneralSecurityException
   *           If the wrapped key is malformed, if it was not wrapped for this key pair with these
   *           fields, or if the runtime cannot unwrap it.
   */
  static SecretKey unwrap(final KeyPair instanceKeyPair, final byte[] wrappedKey, final String keyAlgorithm,
      final String wrappingKeyID) throws GeneralSecurityException
  {
    final PrivateKey privateKey = instanceKeyPair.getPrivate();
    if (privateKey == null)
    {
      throw new InvalidKeyException(NAME + " needs the private key of this instance");
    }
    final BigInteger modulus = getModulus(instanceKeyPair.getPublic());
    if (wrappedKey.length < 2)
    {
      throw new InvalidKeyException(NAME + " wrapped key is truncated");
    }
    final ByteBuffer buffer = ByteBuffer.wrap(wrappedKey);
    final int length = buffer.getShort() & 0xFFFF;
    if (length == 0 || buffer.remaining() <= length)
    {
      throw new InvalidKeyException(NAME + " wrapped key is truncated");
    }
    // RSASVE.RECOVER takes a value on as many bytes as the modulus, and RSADP an integer c with
    // 1 < c < n - 1: a value which is neither does not reach the private key.
    if (length != (modulus.bitLength() + 7) / 8)
    {
      throw new InvalidKeyException(NAME + " encapsulated value is not as long as the RSA modulus");
    }
    final byte[] encapsulated = new byte[length];
    buffer.get(encapsulated);
    final BigInteger c = new BigInteger(1, encapsulated);
    if (c.compareTo(BigInteger.ONE) <= 0 || c.compareTo(modulus.subtract(BigInteger.ONE)) >= 0)
    {
      throw new InvalidKeyException(NAME + " encapsulated value is out of range");
    }
    final byte[] wrapped = new byte[buffer.remaining()];
    buffer.get(wrapped);
    // Neither cipher depends on the value, nor does the private key: a runtime which lacks one of
    // the ciphers, or whose RSA cipher cannot use the private key, is told apart from a value which
    // cannot be unwrapped.
    final Cipher rsa = Cipher.getInstance(RSA_TRANSFORMATION);
    rsa.init(Cipher.DECRYPT_MODE, privateKey);
    final Cipher keyWrap = getKeyWrapCipher();
    byte[] decrypted = null;
    byte[] secretValue = null;
    try
    {
      decrypted = rsa.doFinal(encapsulated);
      // Bouncy Castle FIPS drops the leading zero bytes of the secret value.
      secretValue = toLength(decrypted, length);
      keyWrap.init(Cipher.UNWRAP_MODE, deriveKeyWrappingKey(secretValue, wrappingKeyID, keyAlgorithm));
      return (SecretKey) keyWrap.unwrap(wrapped, keyAlgorithm, Cipher.SECRET_KEY);
    }
    catch (GeneralSecurityException | RuntimeException e)
    {
      throw new InvalidKeyException(NAME + " cannot unwrap the key", e);
    }
    finally
    {
      if (decrypted != null)
      {
        Arrays.fill(decrypted, (byte) 0);
      }
      if (secretValue != null)
      {
        Arrays.fill(secretValue, (byte) 0);
      }
    }
  }
  /** Returns the modulus of an RSA public key, refusing any other key. */
  private static BigInteger getModulus(final PublicKey publicKey) throws InvalidKeyException
  {
    if (!(publicKey instanceof RSAPublicKey))
    {
      throw new InvalidKeyException(
          NAME + " needs an RSA public key, not " + (publicKey != null ? publicKey.getAlgorithm() : null));
    }
    return ((RSAPublicKey) publicKey).getModulus();
  }
  /**
   * Draws the secret value of RSASVE.GENERATE: a random integer z with 1 < z < n - 1, on as many
   * bytes as the modulus n. A draw outside the interval is discarded and drawn again.
   */
  private static byte[] generateSecretValue(final BigInteger modulus, final int length, final SecureRandom random)
  {
    final BigInteger upperBound = modulus.subtract(BigInteger.ONE);
    final byte[] secretValue = new byte[length];
    while (true)
    {
      random.nextBytes(secretValue);
      final BigInteger z = new BigInteger(1, secretValue);
      if (z.compareTo(BigInteger.ONE) > 0 && z.compareTo(upperBound) < 0)
      {
        return secretValue;
      }
    }
  }
  /**
   * Returns the unsigned big-endian integer of the bytes on exactly {@code length} bytes, the way
   * NIST SP 800-56B converts an integer to a byte string: leading zero bytes are added or dropped.
   */
  private static byte[] toLength(final byte[] bytes, final int length) throws InvalidKeyException
  {
    int start = 0;
    while (bytes.length - start > length && bytes[start] == 0)
    {
      start++;
    }
    final int significant = bytes.length - start;
    if (significant > length)
    {
      throw new InvalidKeyException(NAME + " value is longer than the RSA modulus");
    }
    if (significant == length)
    {
      return start == 0 ? bytes : Arrays.copyOfRange(bytes, start, bytes.length);
    }
    final byte[] result = new byte[length];
    System.arraycopy(bytes, start, result, length - significant, significant);
    return result;
  }
  /**
   * Derives the key wrapping key with the one-step key derivation of NIST SP 800-56C Rev. 2, with
   * SHA-256 as the auxiliary function: one hash of the counter 1, the secret value and the fixed
   * information yields its 256 bits.
   */
  private static SecretKey deriveKeyWrappingKey(final byte[] secretValue, final String wrappingKeyID,
      final String keyAlgorithm) throws NoSuchAlgorithmException
  {
    final MessageDigest digest = MessageDigest.getInstance(DIGEST_ALGORITHM);
    digest.update(KDF_COUNTER);
    digest.update(secretValue);
    digest.update(fixedInfo(wrappingKeyID, keyAlgorithm));
    final byte[] key = digest.digest();
    try
    {
      return new SecretKeySpec(key, "AES");
    }
    finally
    {
      Arrays.fill(key, (byte) 0);
    }
  }
  /**
   * Returns the fixed information of the key derivation, each variable-length field preceded by
   * its length on four bytes: the scheme name as AlgorithmID; an empty PartyUInfo, since the
   * wrapping instance is not identified; the identifier of the instance key as PartyVInfo; then,
   * as SuppPubInfo, the algorithm of the wrapped key and the length in bits of the key wrapping key.
   */
  private static byte[] fixedInfo(final String wrappingKeyID, final String keyAlgorithm)
  {
    final byte[] algorithmID = NAME.getBytes(UTF_8);
    final byte[] partyVInfo = wrappingKeyID.getBytes(UTF_8);
    final byte[] wrappedKeyAlgorithm = keyAlgorithm.getBytes(UTF_8);
    return ByteBuffer.allocate(5 * 4 + algorithmID.length + partyVInfo.length + wrappedKeyAlgorithm.length)
        .putInt(algorithmID.length).put(algorithmID)
        .putInt(0)
        .putInt(partyVInfo.length).put(partyVInfo)
        .putInt(wrappedKeyAlgorithm.length).put(wrappedKeyAlgorithm)
        .putInt(KEY_WRAPPING_KEY_LENGTH_BITS)
        .array();
  }
  /**
   * Returns an AES-KWP cipher under the first of its names the runtime knows. When it knows none,
   * the failure names the standard one.
   */
  static Cipher getKeyWrapCipher() throws GeneralSecurityException
  {
    GeneralSecurityException lastFailure = null;
    for (final String transformation : KEY_WRAP_TRANSFORMATIONS)
    {
      try
      {
        return Cipher.getInstance(transformation);
      }
      catch (NoSuchAlgorithmException | NoSuchPaddingException e)
      {
        lastFailure = e;
      }
    }
    throw lastFailure;
  }
}
opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java
@@ -83,6 +83,7 @@
import java.util.logging.Logger;
import com.forgerock.opendj.ldap.tools.LDAPSearch;
import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.dsconfig.DSConfig;
import org.forgerock.opendj.config.server.ConfigException;
@@ -386,6 +387,63 @@
    }
  }
  /**
   * Runs {@code action} on a runtime which offers raw RSA and SHA-256 but no AES-KWP, as Java 11
   * does without Bouncy Castle FIPS. Where a provider offers {@code AES/KWP/NoPadding}, it is
   * withdrawn with {@link #withoutJceService}, and its {@code AESWrapPad} alias with it; since that
   * provider is SunJCE, a stand-in gives back the raw RSA cipher of SunJCE for the duration.
   *
   * @param action
   *          What to run without AES-KWP.
   * @throws Exception
   *           If the action fails.
   */
  public static void withoutKeyWrap(final Callable<Void> action) throws Exception
  {
    if (Security.getProviders("Cipher.AES/KWP/NoPadding") == null)
    {
      action.call();
    }
    else
    {
      final Provider rawRsa = new Provider("RawRsaOfSunJce", "1.0", "RSA cipher of SunJCE") {};
      rawRsa.put("Cipher.RSA", "com.sun.crypto.provider.RSACipher");
      withoutJceService("Cipher", "AES/KWP/NoPadding", action, rawRsa);
    }
  }
  /**
   * Runs {@code action} with the Bouncy Castle FIPS provider installed first, in its default mode,
   * and puts the providers back as they were afterwards.
   *
   * @param <T>
   *          The type of what the action returns.
   * @param action
   *          What to run.
   * @return What the action returns.
   * @throws Exception
   *           If the action fails.
   */
  public static <T> T withBcFipsFirst(final Callable<T> action) throws Exception
  {
    final List<Provider> installed = Arrays.asList(Security.getProviders());
    final Provider previous = Security.getProvider(BouncyCastleFipsProvider.PROVIDER_NAME);
    Security.removeProvider(BouncyCastleFipsProvider.PROVIDER_NAME);
    Security.insertProviderAt(previous != null ? previous : new BouncyCastleFipsProvider(), 1);
    try
    {
      return action.call();
    }
    finally
    {
      Security.removeProvider(BouncyCastleFipsProvider.PROVIDER_NAME);
      if (previous != null)
      {
        Security.insertProviderAt(previous, installed.indexOf(previous) + 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
@@ -37,18 +37,26 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Proxy;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.AlgorithmParameters;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.AlgorithmParameterSpec;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -66,15 +74,21 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.crypto.Cipher;
import javax.crypto.CipherSpi;
import javax.crypto.KeyGeneratorSpi;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.crypto.CryptoServicesRegistrar;
import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.ldap.Attribute;
@@ -879,6 +893,223 @@
    }, sunWithoutMd5);
  }
  @DataProvider
  public Object[][] rsaSveSpellings()
  {
    return new Object[][] { { RsaSveKeyTransport.NAME }, { RsaSveKeyTransport.NAME.toLowerCase() } };
  }
  /**
   The RSASVE scheme is accepted where the runtime offers its algorithms, whatever the case of its
   name, as a Java transformation would be: neither the support check nor the trial wrap of the
   check asks the runtime for a cipher of that name, which no provider offers.
   */
  @Test(dataProvider = "rsaSveSpellings")
  public void testRsaSveKeyWrappingIsAccepted(final String spelling) throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final List<LocalizableMessage> why = new ArrayList<>();
    final boolean acceptable = withBcFipsFirst(() -> cm.isConfigurationChangeAcceptable(
        withProperty(cfg, "getKeyWrappingTransformation", spelling), why));
    assertThat(why).isEmpty();
    assertThat(acceptable).isTrue();
  }
  /**
   The RSASVE scheme is refused where the runtime lacks one of its algorithms, here AES-KWP as on
   Java 11 without Bouncy Castle FIPS, and the refusal names the property to set and the missing
   algorithm: the trial wrap which follows the support check would refuse it too, but without
   naming the property.
   */
  @Test
  public void testRsaSveKeyWrappingWithoutAesKwpIsRefusedNamingTheProperty() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final List<LocalizableMessage> why = new ArrayList<>();
    withoutKeyWrap(() ->
    {
      // SunJCE offers AES-KWP under that name too, and goes with AES/KWP/NoPadding.
      assertThat(Security.getProviders("Cipher.AESWrapPad")).as("providers offering AESWrapPad").isNull();
      assertThat(cm.isConfigurationChangeAcceptable(
          withProperty(cfg, "getKeyWrappingTransformation", RsaSveKeyTransport.NAME), why)).isFalse();
      return null;
    });
    assertThat(why).hasSize(1);
    assertThat(why.get(0).ordinal()).isEqualTo(ERR_CRYPTOMGR_KEY_WRAPPING_TRANSFORMATION_UNSUPPORTED.ordinal());
    assertThat(why.get(0).toString())
        .contains(RsaSveKeyTransport.NAME).contains("key-wrapping-transformation").contains("AES/KWP/NoPadding");
  }
  /**
   A key generated while the RSASVE scheme is configured is published wrapped with it, under its
   name, and unwraps to the key the data was encrypted with.
   */
  @Test
  public void testRsaSveWrappedKeyIsPublishedAndUnwraps() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final CryptoManagerCfg rsaSve = withProperty(cfg, "getKeyWrappingTransformation", RsaSveKeyTransport.NAME);
    // No other test generates a key for this cipher and key length: the key is generated here.
    final String transformation = "AES/CBC/PKCS5Padding";
    final int keyLengthBits = 192;
    final byte[] data = "RSASVE key transport".getBytes(StandardCharsets.UTF_8);
    final String instanceKeyID = cm.getInstanceKeyID();
    // A key of the default cipher, wrapped with the default transformation, RSA-OAEP.
    cm.encrypt(data);
    final String oaepValue =
        getValueWrappedFor(instanceKeyID, cfg.getCipherTransformation(), cfg.getCipherKeyLength());
    assertThat(oaepValue.split(":")[1]).isEqualTo(cfg.getKeyWrappingTransformation());
    final byte[] oaepKey = cm.decodeSymmetricKeyAttribute(oaepValue).getEncoded();
    withBcFipsFirst(() ->
    {
      final List<LocalizableMessage> why = new ArrayList<>();
      assertThat(cm.isConfigurationChangeAcceptable(rsaSve, why)).as("%s", why).isTrue();
      cm.applyConfigurationChange(rsaSve);
      final String value;
      final SecretKey key;
      try
      {
        final byte[] cipherText = cm.encrypt(transformation, keyLengthBits, data);
        value = getValueWrappedFor(instanceKeyID, transformation, keyLengthBits);
        assertThat(value.split(":")[1]).isEqualTo(RsaSveKeyTransport.NAME);
        key = cm.decodeSymmetricKeyAttribute(value);
        // The cipher text is the prologue version, the 16-byte key identifier, the IV, then the data.
        final int ivOffset = 1 + 16;
        final int ivLength = 16;
        final Cipher cipher = Cipher.getInstance(transformation);
        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(cipherText, ivOffset, ivLength));
        assertThat(cipher.doFinal(cipherText, ivOffset + ivLength, cipherText.length - ivOffset - ivLength))
            .isEqualTo(data);
        assertThat(cm.decodeSymmetricKeyAttribute(oaepValue).getEncoded())
            .as("RSA-OAEP value while the RSASVE scheme is configured").isEqualTo(oaepKey);
      }
      finally
      {
        cm.applyConfigurationChange(cfg);
        // The admin data backend keeps the key across restarts, where a runtime without AES-KWP,
        // such as Java 11 without Bouncy Castle FIPS, cannot unwrap it and logs an error for it.
        for (final Entry entry : getKeysForCipher(transformation, keyLengthBits))
        {
          deleteEntry(entry);
        }
      }
      // A value is unwrapped by the transformation it records, whatever the configured one, and
      // the scheme name is not case sensitive there either.
      assertThat(cm.decodeSymmetricKeyAttribute(value).getEncoded())
          .as("RSASVE value while RSA-OAEP is configured").isEqualTo(key.getEncoded());
      final String lowerCaseValue =
          value.replace(":" + RsaSveKeyTransport.NAME + ":", ":" + RsaSveKeyTransport.NAME.toLowerCase() + ":");
      assertThat(cm.decodeSymmetricKeyAttribute(lowerCaseValue).getEncoded())
          .as("RSASVE value recording the scheme name in lower case").isEqualTo(key.getEncoded());
      return null;
    });
  }
  @DataProvider
  public Object[][] keyUsages()
  {
    final String oaep = "RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING";
    final int signing = KeyUsage.digitalSignature;
    final int wrapping = KeyUsage.digitalSignature | KeyUsage.keyEncipherment;
    // Transformation, key usage, whether it is critical, whether a key is wrapped.
    return new Object[][] {
      { oaep, signing, true, false },
      { RsaSveKeyTransport.NAME, signing, true, false },
      { RsaSveKeyTransport.NAME, wrapping, true, true },
      { RsaSveKeyTransport.NAME, signing, false, true },
    };
  }
  /**
   No secret key is wrapped for an instance whose certificate has a critical key usage without
   keyEncipherment, whatever the transformation: Cipher.init refuses such a certificate for
   RSA-OAEP, and the RSASVE scheme, which takes the public key alone, checks the key usage the same
   way. A key usage which is not critical restricts nothing.
   */
  @Test(dataProvider = "keyUsages")
  public void testSecretKeyIsWrappedOnlyWhereTheKeyUsageAllowsIt(final String transformation, final int keyUsage,
      final boolean critical, final boolean wrapped) throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final CryptoManagerCfg withTransformation = withProperty(cfg, "getKeyWrappingTransformation", transformation);
    cm.encrypt("key usage".getBytes(StandardCharsets.UTF_8));
    final String value =
        getValueWrappedFor(cm.getInstanceKeyID(), cfg.getCipherTransformation(), cfg.getCipherKeyLength());
    final byte[] certificate = rsaCertificate(keyUsage, critical);
    final String keyID = CryptoManagerImpl.getInstanceKeyID(certificate);
    final DN instanceKey = DN.valueOf(ATTR_CRYPTO_KEY_ID + "=" + keyID + ",cn=instance keys,cn=admin data");
    final DN trustStoreEntry = DN.valueOf(DN_TRUST_STORE_ROOT).child(instanceKey.rdn());
    withBcFipsFirst(() ->
    {
      final List<LocalizableMessage> why = new ArrayList<>();
      assertThat(cm.isConfigurationChangeAcceptable(withTransformation, why)).as("%s", why).isTrue();
      cm.applyConfigurationChange(withTransformation);
      try
      {
        assertThat(getRootConnection().processAdd(instanceKeyEntry(instanceKey, ByteString.wrap(certificate)))
            .getResultCode()).isEqualTo(ResultCode.SUCCESS);
        if (wrapped)
        {
          assertThat(cm.reencodeSymmetricKeyAttribute(value, keyID)).startsWith(keyID + ":" + transformation + ":");
        }
        else
        {
          assertThatThrownBy(() -> cm.reencodeSymmetricKeyAttribute(value, keyID))
              .isInstanceOf(CryptoManagerException.class)
              .cause().isInstanceOf(InvalidKeyException.class).hasMessageContaining("key usage");
        }
      }
      finally
      {
        cm.applyConfigurationChange(cfg);
        getRootConnection().processDelete(instanceKey);
        getRootConnection().processDelete(trustStoreEntry);
      }
      return null;
    });
  }
  /** Returns a self-signed RSA certificate, DER encoded, with the given key usage. */
  private static byte[] rsaCertificate(final int keyUsage, final boolean critical) throws Exception
  {
    final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "SunRsaSign");
    generator.initialize(2048);
    final KeyPair keyPair = generator.generateKeyPair();
    final Instant now = Instant.now();
    final X500Name subject = new X500Name("CN=key usage " + keyUsage);
    final JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(subject,
        BigInteger.valueOf(now.toEpochMilli()), Date.from(now.minus(1, ChronoUnit.DAYS)),
        Date.from(now.plus(1, ChronoUnit.DAYS)), subject, keyPair.getPublic());
    builder.addExtension(Extension.keyUsage, critical, new KeyUsage(keyUsage));
    return builder.build(new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate())).getEncoded();
  }
  /** Returns the ds-cfg-symmetric-key value of the key of a cipher which is wrapped for an instance. */
  private String getValueWrappedFor(final String instanceKeyID, final String transformation,
      final int keyLengthBits) throws DirectoryException
  {
    final Set<String> values = getKeyForCipher(transformation, keyLengthBits)
        .parseAttribute(ATTR_CRYPTO_SYMMETRIC_KEY).asSetOfString();
    return values.stream()
        .filter(v -> v.startsWith(instanceKeyID + ":"))
        .findFirst()
        .orElseThrow(() -> new AssertionError("no value wrapped for this instance in " + values));
  }
  /** The error the probe provider fails with, set by the case using it. */
  private static volatile Error probeError;
@@ -1057,11 +1288,7 @@
   */
  private static <T> T inApprovedOnlyBcFipsThread(final Callable<T> action) throws Exception
  {
    final List<Provider> installed = Arrays.asList(Security.getProviders());
    final Provider previous = Security.getProvider("BCFIPS");
    Security.removeProvider("BCFIPS");
    Security.insertProviderAt(previous != null ? previous : new BouncyCastleFipsProvider(), 1);
    try
    return withBcFipsFirst(() ->
    {
      final FutureTask<T> task = new FutureTask<>(() ->
      {
@@ -1077,15 +1304,7 @@
      {
        throw new AssertionError("the action failed in approved-only mode", e.getCause());
      }
    }
    finally
    {
      Security.removeProvider("BCFIPS");
      if (previous != null)
      {
        Security.insertProviderAt(previous, installed.indexOf(previous) + 1);
      }
    }
    });
  }
  /**
@@ -1128,11 +1347,16 @@
  private Entry getKeyForCipher(String cipher, int keyLength) throws DirectoryException
  {
    return getKeysForCipher(cipher, keyLength).get(0);
  }
  private List<? extends Entry> getKeysForCipher(String cipher, int keyLength) throws DirectoryException
  {
    SearchRequest request = newSearchRequest("cn=secret keys, cn=admin data", SearchScope.WHOLE_SUBTREE,
        "&(ds-cfg-cipher-transformation-name=" + cipher + ")(ds-cfg-key-length-bits=" + keyLength + ")");
    InternalClientConnection conn = getRootConnection();
    InternalSearchOperation search = conn.processSearch(request);
    return search.getSearchEntries().get(0);
    return search.getSearchEntries();
  }
  @Test
opendj-server-legacy/src/test/java/org/opends/server/crypto/RsaSveKeyTransportTestCase.java
New file
@@ -0,0 +1,688 @@
/*
 * 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.crypto;
import static org.assertj.core.api.Assertions.*;
import static org.opends.server.TestCaseUtils.*;
import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.AlgorithmParameters;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.interfaces.RSAKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import javax.crypto.Cipher;
import javax.crypto.CipherSpi;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
import org.opends.server.util.StaticUtils;
import org.testng.SkipException;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/** Tests the RSASVE key transport scheme of the crypto manager. */
@SuppressWarnings("javadoc")
public class RsaSveKeyTransportTestCase extends CryptoTestCase
{
  private static final String KEY_ID = "0123456789ABCDEF0123456789ABCDEF";
  /** The providers of the Java runtime, which offer AES/KWP/NoPadding from Java 17 on. */
  private static final String JDK = "jdk";
  /**
   * Bouncy Castle FIPS first and no provider offering AES/KWP/NoPadding: AES-KWP only under the
   * name AESWrapPad, and a raw RSA which drops the leading zero bytes of what it decrypts.
   */
  private static final String BC_FIPS = "bcfips";
  /** Created before any test installs Bouncy Castle FIPS, so that it comes from the SUN provider. */
  private final SecureRandom random = new SecureRandom();
  private KeyPair keyPair;
  @BeforeClass
  public void generateKeyPair() throws Exception
  {
    keyPair = rsaKeyPair(2048);
  }
  @DataProvider
  public Object[][] providers()
  {
    return new Object[][] { { JDK }, { BC_FIPS } };
  }
  @DataProvider
  public Object[][] keys()
  {
    final Object[][] keys = { { "DES", 8 }, { "AES", 16 }, { "HmacSHA1", 20 }, { "DESede", 24 }, { "AES", 32 } };
    final Object[][] rows = new Object[keys.length * 2][];
    for (int i = 0; i < keys.length; i++)
    {
      rows[2 * i] = new Object[] { JDK, keys[i][0], keys[i][1] };
      rows[2 * i + 1] = new Object[] { BC_FIPS, keys[i][0], keys[i][1] };
    }
    return rows;
  }
  /** Keys of every length unwrap to themselves: the 20 bytes of an HmacSHA1 key need the padding of KWP. */
  @Test(dataProvider = "keys")
  public void testWrappedKeyUnwrapsToTheSameKey(final String providers, final String algorithm, final int length)
      throws Exception
  {
    final SecretKey key = new SecretKeySpec(bytes(length, 0x3C), algorithm);
    inProviders(providers, () ->
    {
      final byte[] wrapped = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, random);
      final SecretKey unwrapped = RsaSveKeyTransport.unwrap(keyPair, wrapped, algorithm, KEY_ID);
      assertThat(unwrapped.getAlgorithm()).isEqualTo(algorithm);
      assertThat(unwrapped.getEncoded()).isEqualTo(key.getEncoded());
      return null;
    });
  }
  /**
   * The key wrapping key is derived from the encapsulated secret value: another private key
   * recovers another value and cannot unwrap. The other modulus is the larger one, so that the
   * encapsulated value is in its range and its private key decrypts it instead of refusing it.
   */
  @Test
  public void testAnotherPrivateKeyCannotUnwrap() throws Exception
  {
    KeyPair other;
    do
    {
      other = rsaKeyPair(2048);
    }
    while (modulus(other).compareTo(modulus(keyPair)) <= 0);
    final KeyPair otherKeyPair = other;
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    inProviders(BC_FIPS, () ->
    {
      final byte[] wrapped = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, random);
      assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(otherKeyPair, wrapped, "AES", KEY_ID))
          .isInstanceOf(GeneralSecurityException.class);
      return null;
    });
  }
  /** The derivation binds the fields stored next to the wrapped key: changing either one fails the unwrapping. */
  @Test
  public void testFieldsStoredNextToTheWrappedKeyAreBound() throws Exception
  {
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    inProviders(BC_FIPS, () ->
    {
      final byte[] wrapped = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, random);
      assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID.toLowerCase()))
          .as("another instance key identifier").isInstanceOf(GeneralSecurityException.class);
      assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(keyPair, wrapped, "HmacSHA256", KEY_ID))
          .as("another key algorithm").isInstanceOf(GeneralSecurityException.class);
      return null;
    });
  }
  /**
   * The wrapped key of a known secret value. What this version stores in cn=secret keys is what
   * every later version has to unwrap, so the derivation and the key wrap are pinned to a value
   * computed independently of this implementation: SHA-256 of the counter 1, the secret value and
   * the fixed information, then AES-KWP of the key with it. The key wrap of both the Java runtime and
   * Bouncy Castle FIPS has to give it.
   */
  @Test(dataProvider = "providers")
  public void testKnownAnswer(final String providers) throws Exception
  {
    final byte[] secretValue = new byte[256];
    for (int i = 0; i < secretValue.length; i++)
    {
      secretValue[i] = (byte) (i + 1);
    }
    final Draws draws = new Draws(secretValue);
    final SecretKey key =
        new SecretKeySpec(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, "AES");
    inProviders(providers, () ->
    {
      final byte[] wrapped = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, draws);
      assertThat(ByteBuffer.wrap(wrapped).getShort()).isEqualTo((short) 256);
      final byte[] wrappedKey = Arrays.copyOfRange(wrapped, 2 + 256, wrapped.length);
      assertThat(StaticUtils.bytesToHexNoSpace(wrappedKey))
          .isEqualToIgnoringCase("A29EDD3A5DDBD441B69FB866DDE179CF6CC59C86B54A1AF5");
      return null;
    });
  }
  @DataProvider
  public Object[][] secretValuesOutsideTheInterval()
  {
    return new Object[][] {
      { "0", new byte[256] },
      { "1", toBytes(BigInteger.ONE) },
      { "n - 1", toBytes(modulus(keyPair).subtract(BigInteger.ONE)) },
      { "2^2048 - 1", bytes(256, 0xFF) } };
  }
  /** RSASVE.GENERATE draws z with 1 < z < n - 1: a value outside is discarded and drawn again. */
  @Test(dataProvider = "secretValuesOutsideTheInterval")
  public void testSecretValueOutsideTheIntervalIsDrawnAgain(final String value, final byte[] outside)
      throws Exception
  {
    final Draws draws = new Draws(outside, bytes(256, 0x5A));
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    inProviders(BC_FIPS, () ->
    {
      final byte[] wrapped = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, draws);
      assertThat(draws.count()).as(value).isEqualTo(2);
      assertThat(RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID).getEncoded())
          .isEqualTo(key.getEncoded());
      return null;
    });
  }
  /** Bouncy Castle FIPS drops the leading zero bytes of a decrypted secret value, which the derivation needs. */
  @Test
  public void testSecretValueWithALeadingZeroByteUnwraps() throws Exception
  {
    final byte[] secretValue = bytes(256, 0x5A);
    secretValue[0] = 0;
    final Draws draws = new Draws(secretValue);
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    inProviders(BC_FIPS, () ->
    {
      final byte[] wrapped = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, draws);
      assertThat(RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID).getEncoded())
          .isEqualTo(key.getEncoded());
      return null;
    });
  }
  /**
   * A provider may drop the leading zero bytes of the encapsulated value as well, which the
   * wrapping puts back: the value is stored on as many bytes as the modulus, the only length the
   * unwrapping accepts.
   */
  @Test
  public void testEncapsulatedValueWithALeadingZeroByteKeepsTheModulusLength() throws Exception
  {
    final BigInteger n = modulus(keyPair);
    final BigInteger e = ((RSAPublicKey) keyPair.getPublic()).getPublicExponent();
    final BigInteger upperBound = n.subtract(BigInteger.ONE);
    // About one secret value in 256 has an encapsulated value z^e mod n below 2^2040.
    BigInteger z;
    do
    {
      z = new BigInteger(n.bitLength(), random);
    }
    while (z.compareTo(BigInteger.ONE) <= 0 || z.compareTo(upperBound) >= 0 || z.modPow(e, n).bitLength() > 2040);
    final Draws draws = new Draws(toBytes(z));
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    inProviders(BC_FIPS, () ->
    {
      final Provider droppingRsa = new Provider("LeadingZeroDroppingRsa", "1.0", "Raw RSA without leading zeros") {};
      droppingRsa.put("Cipher.RSA", LeadingZeroDroppingRawRsa.class.getName());
      Security.insertProviderAt(droppingRsa, 1);
      try
      {
        final byte[] wrapped = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, draws);
        assertThat(ByteBuffer.wrap(wrapped).getShort()).isEqualTo((short) 256);
        assertThat(wrapped[2]).isEqualTo((byte) 0);
        assertThat(RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID).getEncoded())
            .isEqualTo(key.getEncoded());
      }
      finally
      {
        Security.removeProvider(droppingRsa.getName());
      }
      return null;
    });
  }
  /** Nothing assumes a 2048-bit modulus: instance keys generated before 2015 have 1024 bits. */
  @Test
  public void testSmallerModulusRoundTrips() throws Exception
  {
    final KeyPair small = rsaKeyPair(1024);
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    inProviders(JDK, () ->
    {
      final byte[] wrapped = RsaSveKeyTransport.wrap(small.getPublic(), key, KEY_ID, random);
      assertThat(ByteBuffer.wrap(wrapped).getShort()).isEqualTo((short) 128);
      assertThat(RsaSveKeyTransport.unwrap(small, wrapped, "AES", KEY_ID).getEncoded())
          .isEqualTo(key.getEncoded());
      return null;
    });
  }
  /** An instance key which is not an RSA key is refused with an exception the crypto manager reports. */
  @Test
  public void testNonRsaPublicKeyIsRefusedWithASecurityException() throws Exception
  {
    final KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
    generator.initialize(256);
    final PublicKey ecKey = generator.generateKeyPair().getPublic();
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    assertThatThrownBy(() -> RsaSveKeyTransport.wrap(ecKey, key, KEY_ID, random))
        .isInstanceOf(InvalidKeyException.class);
  }
  /**
   * The unwrapping refuses an instance key pair without its private key, as a trust store entry
   * holding only a certificate gives, or whose public key is not an RSA key, with an exception the
   * crypto manager reports and before any cipher.
   */
  @Test
  public void testUnusableInstanceKeyPairIsRefusedWithASecurityException() throws Exception
  {
    final byte[] wrapped = { 0, 3, 1, 2, 3, 4 };
    assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(new KeyPair(keyPair.getPublic(), null), wrapped, "AES", KEY_ID))
        .isInstanceOf(InvalidKeyException.class)
        .hasMessageContaining("needs the private key");
    final KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
    generator.initialize(256);
    final KeyPair ecKeyPair = new KeyPair(generator.generateKeyPair().getPublic(), keyPair.getPrivate());
    assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(ecKeyPair, wrapped, "AES", KEY_ID))
        .isInstanceOf(InvalidKeyException.class)
        .hasMessageContaining("needs an RSA public key");
  }
  @DataProvider
  public Object[][] malformedWrappedKeys()
  {
    return new Object[][] {
      { "empty", new byte[0] },
      { "one byte", new byte[] { 1 } },
      { "zero length", new byte[] { 0, 0, 1, 2, 3 } },
      { "length beyond the end", new byte[] { 0, 9, 1, 2, 3 } },
      { "no wrapped key after the encapsulated value", new byte[] { 0, 3, 1, 2, 3 } } };
  }
  /** A malformed value in cn=secret keys is refused with an exception the crypto manager reports. */
  @Test(dataProvider = "malformedWrappedKeys")
  public void testMalformedWrappedKeyIsRefusedWithASecurityException(final String what, final byte[] wrapped)
  {
    assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID))
        .as(what).isInstanceOf(InvalidKeyException.class).hasMessageContaining("truncated");
  }
  @DataProvider
  public Object[][] encapsulatedValuesRefusedBeforeDecrypting()
  {
    final BigInteger n = modulus(keyPair);
    return new Object[][] {
      { "one byte shorter than the modulus", bytes(255, 0x5A), "not as long as the RSA modulus" },
      { "longer than the modulus", bytes(300, 0x5A), "not as long as the RSA modulus" },
      { "0", new byte[256], "out of range" },
      { "1", toBytes(BigInteger.ONE), "out of range" },
      { "n - 1", toBytes(n.subtract(BigInteger.ONE)), "out of range" },
      { "n", toBytes(n), "out of range" },
      { "2^2048 - 1", bytes(256, 0xFF), "out of range" } };
  }
  /**
   * An encapsulated value which is not on as many bytes as the modulus, or not an integer c with
   * 1 < c < n - 1, is refused before the private key decrypts it, whatever the providers: Bouncy
   * Castle FIPS throws an ArrayIndexOutOfBoundsException for a value longer than the modulus.
   */
  @Test(dataProvider = "encapsulatedValuesRefusedBeforeDecrypting")
  public void testEncapsulatedValueIsCheckedBeforeDecrypting(final String what, final byte[] encapsulated,
      final String message) throws Exception
  {
    final byte[] wrapped = wrappedKey(encapsulated, bytes(24, 0x3C));
    final Callable<Void> unwrap = () ->
    {
      assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID))
          .as(what).isInstanceOf(InvalidKeyException.class).hasMessageContaining(message);
      return null;
    };
    unwrap.call();
    inProviders(BC_FIPS, unwrap);
  }
  @DataProvider
  public Object[][] encapsulatedValuesInRange()
  {
    final BigInteger n = modulus(keyPair);
    final Object[][] values = {
      { "2", toBytes(BigInteger.valueOf(2)) },
      { "n - 2", toBytes(n.subtract(BigInteger.valueOf(2))) },
      { "0x5A on every byte", bytes(256, 0x5A) } };
    final Object[][] rows = new Object[values.length * 2][];
    for (int i = 0; i < values.length; i++)
    {
      rows[2 * i] = new Object[] { JDK, values[i][0], values[i][1] };
      rows[2 * i + 1] = new Object[] { BC_FIPS, values[i][0], values[i][1] };
    }
    return rows;
  }
  /**
   * Whatever secret value the private key recovers from an encapsulated value in range, a wrapped
   * key which does not unwrap is refused with one and the same message.
   */
  @Test(dataProvider = "encapsulatedValuesInRange")
  public void testUnwrapFailureHasOneMessage(final String providers, final String what, final byte[] encapsulated)
      throws Exception
  {
    final SecretKey key = new SecretKeySpec(bytes(16, 0x3C), "AES");
    final Callable<Void> unwrap = () ->
    {
      final byte[] genuine = RsaSveKeyTransport.wrap(keyPair.getPublic(), key, KEY_ID, random);
      final byte[] wrapped = wrappedKey(encapsulated, Arrays.copyOfRange(genuine, 2 + 256, genuine.length));
      assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID))
          .as(what).isInstanceOf(InvalidKeyException.class)
          .hasMessage(RsaSveKeyTransport.NAME + " cannot unwrap the key");
      return null;
    };
    inProviders(providers, unwrap);
  }
  /**
   * The support check names AES/KWP/NoPadding when the runtime offers raw RSA and SHA-256 but no
   * AES-KWP, as a Java 11 runtime does without Bouncy Castle FIPS.
   */
  @Test
  public void testCheckSupportedNamesTheMissingKeyWrap() throws Exception
  {
    withoutKeyWrap(() ->
    {
      assertThatThrownBy(RsaSveKeyTransport::checkSupported)
          .isInstanceOf(GeneralSecurityException.class)
          .hasMessageContaining("AES/KWP/NoPadding");
      return null;
    });
  }
  /**
   * The unwrapping names AES/KWP/NoPadding on a runtime which lacks it, as a server which does not
   * use the scheme but receives keys wrapped with it does: no cipher depends on the value, so such a
   * runtime is not reported as a value which cannot be unwrapped.
   */
  @Test
  public void testUnwrapNamesTheMissingKeyWrap() throws Exception
  {
    final byte[] wrapped = wrappedKey(toBytes(BigInteger.valueOf(2)), bytes(24, 0x3C));
    withoutKeyWrap(() ->
    {
      assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(keyPair, wrapped, "AES", KEY_ID))
          .isInstanceOf(GeneralSecurityException.class)
          .hasMessageContaining("AES/KWP/NoPadding");
      return null;
    });
  }
  /**
   * The unwrapping reports why the RSA cipher refuses the private key, as a provider which cannot
   * use the key of the trust store does: the private key does not depend on the value either, so
   * such a runtime is not reported as a value which cannot be unwrapped.
   */
  @Test
  public void testUnwrapReportsWhyTheRsaCipherRefusesThePrivateKey() throws Exception
  {
    final KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
    generator.initialize(256);
    final PrivateKey ecKey = generator.generateKeyPair().getPrivate();
    final Throwable refusal =
        catchThrowable(() -> Cipher.getInstance("RSA/ECB/NoPadding").init(Cipher.DECRYPT_MODE, ecKey));
    assertThat(refusal).isInstanceOf(InvalidKeyException.class);
    final byte[] wrapped = wrappedKey(toBytes(BigInteger.valueOf(2)), bytes(24, 0x3C));
    assertThatThrownBy(() -> RsaSveKeyTransport.unwrap(new KeyPair(keyPair.getPublic(), ecKey), wrapped, "AES", KEY_ID))
        .isInstanceOf(InvalidKeyException.class)
        .hasMessage(refusal.getMessage());
  }
  /**
   * With Bouncy Castle FIPS installed first, as in the FIPS mode of OpenDJ, Bouncy Castle FIPS does
   * the key wrap even where the Java runtime offers AES/KWP/NoPadding too, a name it does not know.
   */
  @Test
  public void testKeyWrapComesFromBcFipsWhenItIsFirst() throws Exception
  {
    assertThat(withBcFipsFirst(() -> RsaSveKeyTransport.getKeyWrapCipher().getProvider().getName()))
        .isEqualTo(BouncyCastleFipsProvider.PROVIDER_NAME);
  }
  /** Runs the action with the providers of the Java runtime, or with AES-KWP from Bouncy Castle FIPS only. */
  private static void inProviders(final String providers, final Callable<Void> action) throws Exception
  {
    final boolean jdkOffersKwp = Security.getProviders("Cipher.AES/KWP/NoPadding") != null;
    if (JDK.equals(providers))
    {
      if (!jdkOffersKwp)
      {
        throw new SkipException("This Java runtime offers no AES/KWP/NoPadding");
      }
      action.call();
    }
    else if (jdkOffersKwp)
    {
      withoutJceService("Cipher", "AES/KWP/NoPadding", action, new BouncyCastleFipsProvider());
    }
    else
    {
      withBcFipsFirst(action);
    }
  }
  private static KeyPair rsaKeyPair(final int bits) throws Exception
  {
    final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "SunRsaSign");
    generator.initialize(bits);
    return generator.generateKeyPair();
  }
  private static BigInteger modulus(final KeyPair keyPair)
  {
    return ((RSAPublicKey) keyPair.getPublic()).getModulus();
  }
  private static byte[] bytes(final int length, final int value)
  {
    final byte[] bytes = new byte[length];
    Arrays.fill(bytes, (byte) value);
    return bytes;
  }
  /** Returns a wrapped key: the length of the encapsulated value on two bytes, that value, then the KWP-wrapped key. */
  private static byte[] wrappedKey(final byte[] encapsulated, final byte[] wrapped)
  {
    return ByteBuffer.allocate(2 + encapsulated.length + wrapped.length)
        .putShort((short) encapsulated.length)
        .put(encapsulated)
        .put(wrapped)
        .array();
  }
  /** Returns the value on 256 bytes, big-endian, without the sign byte BigInteger may add. */
  private static byte[] toBytes(final BigInteger value)
  {
    final byte[] bytes = value.toByteArray();
    final byte[] result = new byte[256];
    final int length = Math.min(bytes.length, result.length);
    System.arraycopy(bytes, bytes.length - length, result, result.length - length, length);
    return result;
  }
  /**
   * A random generator which hands out the given values, one per draw, the last one again once
   * they are all drawn, and counts the draws. Created outside any provider fixture, so that its
   * own default generator comes from the SUN provider.
   */
  private static final class Draws extends SecureRandom
  {
    private static final long serialVersionUID = 1L;
    private final byte[][] values;
    private final AtomicInteger draws = new AtomicInteger();
    private Draws(final byte[]... values)
    {
      this.values = values;
    }
    @Override
    public void nextBytes(final byte[] bytes)
    {
      final byte[] value = values[Math.min(draws.getAndIncrement(), values.length - 1)];
      assertThat(value).hasSize(bytes.length);
      System.arraycopy(value, 0, bytes, 0, bytes.length);
    }
    int count()
    {
      return draws.get();
    }
  }
  /**
   * A raw RSA cipher which returns what it encrypts or decrypts on as few bytes as the value needs,
   * without its leading zero bytes.
   */
  public static final class LeadingZeroDroppingRawRsa extends CipherSpi
  {
    private final ByteArrayOutputStream input = new ByteArrayOutputStream();
    private RSAKey key;
    private BigInteger exponent;
    @Override
    protected void engineSetMode(final String mode) throws NoSuchAlgorithmException
    {
      if (!"ECB".equalsIgnoreCase(mode))
      {
        throw new NoSuchAlgorithmException(mode);
      }
    }
    @Override
    protected void engineSetPadding(final String padding) throws NoSuchPaddingException
    {
      if (!"NoPadding".equalsIgnoreCase(padding))
      {
        throw new NoSuchPaddingException(padding);
      }
    }
    @Override
    protected int engineGetBlockSize()
    {
      return 0;
    }
    @Override
    protected int engineGetOutputSize(final int inputLength)
    {
      return (key.getModulus().bitLength() + 7) / 8;
    }
    @Override
    protected byte[] engineGetIV()
    {
      return null;
    }
    @Override
    protected AlgorithmParameters engineGetParameters()
    {
      return null;
    }
    @Override
    protected void engineInit(final int mode, final Key key, final SecureRandom random) throws InvalidKeyException
    {
      if (mode == Cipher.ENCRYPT_MODE && key instanceof RSAPublicKey)
      {
        exponent = ((RSAPublicKey) key).getPublicExponent();
      }
      else if (mode == Cipher.DECRYPT_MODE && key instanceof RSAPrivateKey)
      {
        exponent = ((RSAPrivateKey) key).getPrivateExponent();
      }
      else
      {
        throw new InvalidKeyException("Mode " + mode + " with " + key.getClass().getName());
      }
      this.key = (RSAKey) key;
      input.reset();
    }
    @Override
    protected void engineInit(final int mode, final Key key, final AlgorithmParameterSpec params,
        final SecureRandom random) throws InvalidKeyException
    {
      engineInit(mode, key, random);
    }
    @Override
    protected void engineInit(final int mode, final Key key, final AlgorithmParameters params,
        final SecureRandom random) throws InvalidKeyException
    {
      engineInit(mode, key, random);
    }
    @Override
    protected byte[] engineUpdate(final byte[] in, final int offset, final int length)
    {
      input.write(in, offset, length);
      return new byte[0];
    }
    @Override
    protected int engineUpdate(final byte[] in, final int offset, final int length, final byte[] out,
        final int outOffset)
    {
      input.write(in, offset, length);
      return 0;
    }
    @Override
    protected byte[] engineDoFinal(final byte[] in, final int offset, final int length)
    {
      input.write(in, offset, length);
      final byte[] result = new BigInteger(1, input.toByteArray()).modPow(exponent, key.getModulus()).toByteArray();
      input.reset();
      int start = 0;
      while (start < result.length - 1 && result[start] == 0)
      {
        start++;
      }
      return Arrays.copyOfRange(result, start, result.length);
    }
    @Override
    protected int engineDoFinal(final byte[] in, final int offset, final int length, final byte[] out,
        final int outOffset)
    {
      final byte[] result = engineDoFinal(in, offset, length);
      System.arraycopy(result, 0, out, outOffset, result.length);
      return result.length;
    }
  }
}