/* * 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. *

* 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 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. *

* 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; } }