From 3b20faae34218cdbb72008a3695f748b34cdff4a Mon Sep 17 00:00:00 2001
From: Maxim Thomas <maxim.thomas@gmail.com>
Date: Tue, 20 Oct 2026 06:45:08 +0000
Subject: [PATCH] [#1056] Add the RSASVE/KDF-SHA256/AESKWP key wrapping transformation for runtimes without RSA-OAEP (#1194)
---
opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc | 6
opendj-server-legacy/src/main/java/org/opends/server/crypto/RsaSveKeyTransport.java | 402 ++++++++++++++++
opendj-server-legacy/src/test/java/org/opends/server/TestCaseUtils.java | 58 ++
opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java | 69 ++
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CryptoManagerConfiguration.xml | 18
opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java | 256 +++++++++
opendj-server-legacy/src/test/java/org/opends/server/crypto/RsaSveKeyTransportTestCase.java | 688 +++++++++++++++++++++++++++
7 files changed, 1,463 insertions(+), 34 deletions(-)
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 565a143..2049991 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
@@ -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 \
diff --git a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CryptoManagerConfiguration.xml b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CryptoManagerConfiguration.xml
index a317878..c3653b8 100644
--- a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CryptoManagerConfiguration.xml
+++ b/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>
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 a2eb090..9c85361 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
@@ -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(
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/crypto/RsaSveKeyTransport.java b/opendj-server-legacy/src/main/java/org/opends/server/crypto/RsaSveKeyTransport.java
new file mode 100644
index 0000000..67ff01b
--- /dev/null
+++ b/opendj-server-legacy/src/main/java/org/opends/server/crypto/RsaSveKeyTransport.java
@@ -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;
+ }
+}
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 52a49da..46f6011 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
@@ -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");
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 7f5bd60..aa2254e 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
@@ -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
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/crypto/RsaSveKeyTransportTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/crypto/RsaSveKeyTransportTestCase.java
new file mode 100644
index 0000000..af31ca5
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/crypto/RsaSveKeyTransportTestCase.java
@@ -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;
+ }
+ }
+}
--
Gitblit v1.10.0