| | |
| | | ==== |
| | | 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: |
| | | + |
| | |
| | | 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 \ |
| | |
| | | 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> |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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) |
| | |
| | | * 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 |
| | |
| | | = 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) { |
| | |
| | | * 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, |
| | |
| | | * @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. |
| | |
| | | } |
| | | |
| | | // 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; |
| | | } |
| | |
| | | // 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( |
| New file |
| | |
| | | /* |
| | | * 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; |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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"); |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | }, 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; |
| | | |
| | |
| | | */ |
| | | 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<>(() -> |
| | | { |
| | |
| | | { |
| | | 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); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | 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 |
| New file |
| | |
| | | /* |
| | | * 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; |
| | | } |
| | | } |
| | | } |