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

Valery Kharseko
22 hours ago 15bff9827e9f493c38b4d8aa01280d0c7eef8326
[#1081] Wrap with a 2048-bit key in the key wrapping check, and report a provider refusing with an Error (#1104)
2 files modified
400 ■■■■■ changed files
opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java 65 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java 335 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/crypto/CryptoManagerImpl.java
@@ -351,7 +351,8 @@
                  requestedCipherTransformation,
                  requestedCipherTransformationKeyLengthBits);
        }
        catch (Exception ex) {
        catch (Exception | Error ex) {
          rethrowIfNotARefusal(ex);
          logger.traceException(ex);
          unacceptableReasons.add(
             ERR_CRYPTOMGR_CANNOT_GET_REQUESTED_ENCRYPTION_CIPHER.get(
@@ -373,7 +374,8 @@
             requestedMACAlgorithm,
             requestedMACAlgorithmKeyLengthBits);
      }
      catch (Exception ex) {
      catch (Exception | Error ex) {
        rethrowIfNotARefusal(ex);
        logger.traceException(ex);
        unacceptableReasons.add(
                ERR_CRYPTOMGR_CANNOT_GET_REQUESTED_MAC_ENGINE.get(
@@ -400,28 +402,36 @@
          /* Note that the TrustStoreBackend not available at initial,
         CryptoManager configuration, hence a "dummy" certificate must be used
         to validate the choice of secret key wrapping cipher. Otherwise, call
         getInstanceKeyCertificateFromLocalTruststore() */
         getInstanceKeyCertificateFromLocalTruststore(). Its key has 2048 bits,
         the least a FIPS approved-only provider wraps with, and the key
         identifier is not computed from it: the wrapped key is thrown away. */
          final String certificateBase64 =
                "MIIB2jCCAUMCBEb7wpYwDQYJKoZIhvcNAQEEBQAwNDEbMBkGA1UEChMST3B" +
                "lbkRTIENlcnRpZmljYXRlMRUwEwYDVQQDEwwxMC4wLjI0OC4yNTEwHhcNMD" +
                "cwOTI3MTQ0NzUwWhcNMjcwOTIyMTQ0NzUwWjA0MRswGQYDVQQKExJPcGVuR" +
                "FMgQ2VydGlmaWNhdGUxFTATBgNVBAMTDDEwLjAuMjQ4LjI1MTCBnzANBgkq" +
                "hkiG9w0BAQEFAAOBjQAwgYkCgYEAnIm6ELyuNVbpaacBQ7fzHlHMmQO/CYJ" +
                "b2gPTdb9n1HLOBqh2lmLLHvt2SgBeN5TSa1PAHW8zJy9LDhpWKZvsUOIdQD" +
                "8Ula/0d/jvMEByEj/hr00P6yqgLXk+EudPgOkFXHA+IfkkOSghMooWc/L8H" +
                "nD1REdqeZuxp+ARNU+cc/ECAwEAATANBgkqhkiG9w0BAQQFAAOBgQBemyCU" +
                "jucN34MZwvzbmFHT/leUu3/cpykbGM9HL2QUX7iKvv2LJVqexhj7CLoXxZP" +
                "oNL+HHKW0vi5/7W5KwOZsPqKI2SdYV7nDqTZklm5ZP0gmIuNO6mTqBRtC2D" +
                "lplX1Iq+BrQJAmteiPtwhdZD+EIghe51CaseImjlLlY2ZK8w==";
                "MIIDGTCCAgGgAwIBAgIICGFHa+OJNiMwDQYJKoZIhvcNAQELBQAwOjEbMBkG" +
                "A1UEChMST3BlbkRKIENlcnRpZmljYXRlMRswGQYDVQQDExJLZXkgd3JhcHBpbmcg" +
                "Y2hlY2swIBcNMjYwOTI1MDYzNTAwWhgPMjEyNjA5MDEwNjM1MDBaMDoxGzAZBgNV" +
                "BAoTEk9wZW5ESiBDZXJ0aWZpY2F0ZTEbMBkGA1UEAxMSS2V5IHdyYXBwaW5nIGNo" +
                "ZWNrMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkLf2hk4cc4FiL0lG" +
                "1efFX7hZ/RB5pvb5bKfQAlE3l/YYHmQNjdM+JgIP7t5l/vveoWkkgwSjWj2sh10H" +
                "VDpXBxDBdoLNScoKrlDryY+FKO9nDogPJDtQRaTs3ntQDtRR90qASUuw/+gmjitY" +
                "fNPHhWy1o/tiwjyz2df/y/pqGEb+VPL0zoyxat+TjCprfmYOwstlsjVhrZhbe96W" +
                "WYF8qOMiqx8lu/L9fPJcKg2zSyMnLk0KZJ9iVKWuhyojKdmHSpqSEAzjKaG15qfF" +
                "ykotuYMh+gdyMjbdvmSyRZV+XK8/2w26f3Cve3ivOPAmse6Z2aDC4AiIoFsyvAJT" +
                "dI2c4QIDAQABoyEwHzAdBgNVHQ4EFgQU0ElK+Aaz5nAV/mTc5zT2//fGtEswDQYJ" +
                "KoZIhvcNAQELBQADggEBAA0+IjrK0HWw+0nHdl4f0JI5pvyIotUbbYgZrwYWqc8V" +
                "GrHu2RzhsUDTlg/o1L/8f5rM8vKFgg73gmIGHtS16UpBp5PuKi9UXtpZ1G11yH8/" +
                "P+4PkmlWl5XNFD6sTy8sOyt0Lv3aVCXt2tkQKu5HFhoXTfLn7JsrSWp52I+QTfYT" +
                "KjB2J0IB2AsLtKeAU8r1CepS3YS+/npq4bvwjo0z7kwt6NNXbD2frC1AVVFTUNar" +
                "nop82WUyMl94WXHWCe5Q0h67a1RB8i/KTS8ro0pEhMmoHHPc8zY/hyp5Of/m9pJ5" +
                "ThBjQlyDccG+81IemDAcwmCqMnEJUcceEmy7VEZT/y4=";
          final byte[] certificate = Base64.decode(certificateBase64).toByteArray();
          final String keyID = getInstanceKeyID(certificate);
          final SecretKey macKey = macCryptoManager.generateKeyEntry(
                  requestedMACAlgorithm,
                  requestedMACAlgorithmKeyLengthBits).getSecretKey();
          encodeSymmetricKeyAttribute(requestedKeyWrappingTransformation,
                  keyID, certificate, macKey);
                  "key-wrapping-check", certificate, macKey);
        }
        catch (Exception ex) {
        catch (Exception | Error ex) {
          rethrowIfNotARefusal(ex);
          logger.traceException(ex);
          unacceptableReasons.add(
                  ERR_CRYPTOMGR_CANNOT_GET_PREFERRED_KEY_WRAPPING_CIPHER.get(
@@ -434,10 +444,25 @@
  }
  /**
   * Rethrows what no configuration can be refused for. The checks ask the JCE providers for keys
   * and ciphers, and a provider may refuse with an Error rather than an exception: the BC-FIPS
   * provider in approved-only mode throws its FipsUnapprovedOperationError. A VirtualMachineError
   * is not a refusal, and neither is a LinkageError, which a broken provider jar throws: reported
   * as a refusal, it would lose its cause, since a refusal carries only a message.
   */
  private static void rethrowIfNotARefusal(final Throwable t)
  {
    if (t instanceof VirtualMachineError || t instanceof LinkageError)
    {
      throw (Error) t;
    }
  }
  /**
   * Checks that this Java runtime provides the key wrapping transformation. Only a refusal here
   * names the key-wrapping-transformation property: the wrap which follows it also needs an MD5
   * digest and a 1024-bit RSA key, and changing the property does not help when one of those is
   * what the runtime refuses.
   * 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)
opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java
@@ -38,16 +38,30 @@
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.AlgorithmParameters;
import java.security.Key;
import java.security.MessageDigest;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import javax.crypto.CipherSpi;
import javax.crypto.KeyGeneratorSpi;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import org.bouncycastle.crypto.CryptoServicesRegistrar;
import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.ldap.Attribute;
@@ -324,12 +338,12 @@
  }
  /**
   A transformation the runtime provides, refused because of the rest of the check (here the
   MD5 digest of the instance key identifier), is not reported as a matter of the property:
   changing key-wrapping-transformation would not help.
   The check wraps with a key which a provider running in FIPS approved-only mode accepts: BC-FIPS
   in that mode refuses RSA keys under 2048 bits with an Error, and the check runs at every start,
   so a smaller key keeps the server from starting whatever the transformation.
   */
  @Test
  public void testKeyWrappingRefusalForAnotherCauseDoesNotNameTheProperty() throws Exception
  public void testKeyWrappingCheckPassesUnderApprovedOnlyBcFips() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
@@ -337,22 +351,315 @@
    assertThat(supported).isNotEqualTo(cfg.getKeyWrappingTransformation());
    final List<LocalizableMessage> why = new ArrayList<>();
    // Withdrawing MD5 withdraws the SUN provider, whose SHA-1 digest the OAEP cipher still needs.
    final Provider sha1Only = new Provider("Sha1OnlyDigest", "1.0", "SHA-1 digest only") {};
    sha1Only.put("MessageDigest.SHA-1", "sun.security.provider.SHA");
    sha1Only.put("Alg.Alias.MessageDigest.SHA1", "SHA-1");
    final boolean acceptable = inApprovedOnlyBcFipsThread(() -> cm.isConfigurationChangeAcceptable(
        withProperty(withMacKeyFromSunJce(cfg), "getKeyWrappingTransformation", supported), why));
    assertThat(why).isEmpty();
    assertThat(acceptable).isTrue();
  }
  /**
   A provider refusing the wrap with an Error, as BC-FIPS in approved-only mode refuses PKCS#1 v1.5
   encryption, is reported as a refusal of the configuration rather than escaping from the check.
   */
  @Test
  public void testKeyWrappingRefusedWithAnErrorIsReported() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final String unapproved = "RSA/ECB/PKCS1Padding";
    final List<LocalizableMessage> why = new ArrayList<>();
    final boolean acceptable = inApprovedOnlyBcFipsThread(() -> cm.isConfigurationChangeAcceptable(
        withProperty(withMacKeyFromSunJce(cfg), "getKeyWrappingTransformation", unapproved), why));
    assertThat(acceptable).isFalse();
    assertThat(why).hasSize(1);
    final String reason = why.get(0).toString();
    assertThat(why.get(0).ordinal()).as(reason).isEqualTo(ERR_CRYPTOMGR_CANNOT_GET_PREFERRED_KEY_WRAPPING_CIPHER.ordinal());
    assertThat(reason).contains("PKCS1.5");
  }
  /**
   A provider refusing to generate a MAC key with an Error, as BC-FIPS in approved-only mode
   refuses a random generator it has not approved, is reported as a refusal of the MAC algorithm.
   */
  @Test
  public void testMacKeyGenerationRefusedWithAnErrorIsReported() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final List<LocalizableMessage> why = new ArrayList<>();
    final boolean acceptable = inApprovedOnlyBcFipsThread(() -> cm.isConfigurationChangeAcceptable(
        withProperty(cfg, "getMacKeyLength", cfg.getMacKeyLength() + 64), why));
    assertThat(acceptable).isFalse();
    assertThat(why).hasSize(1);
    final String reason = why.get(0).toString();
    assertThat(why.get(0).ordinal()).as(reason).isEqualTo(ERR_CRYPTOMGR_CANNOT_GET_REQUESTED_MAC_ENGINE.ordinal());
    assertThat(reason).contains("unapproved RNG");
  }
  /**
   A provider refusing to generate a cipher key with an Error is reported as a refusal of the
   cipher transformation.
   */
  @Test
  public void testCipherKeyGenerationRefusedWithAnErrorIsReported() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final String transformation = "AES/CTR/NoPadding";
    assertThat(transformation).isNotEqualTo(cfg.getCipherTransformation());
    final List<LocalizableMessage> why = new ArrayList<>();
    final boolean acceptable = inApprovedOnlyBcFipsThread(() -> cm.isConfigurationChangeAcceptable(
        withProperty(cfg, "getCipherTransformation", transformation), why));
    assertThat(acceptable).isFalse();
    assertThat(why).hasSize(1);
    final String reason = why.get(0).toString();
    assertThat(why.get(0).ordinal()).as(reason).isEqualTo(ERR_CRYPTOMGR_CANNOT_GET_REQUESTED_ENCRYPTION_CIPHER.ordinal());
    assertThat(reason).contains("unapproved RNG");
  }
  /** The check does not need an MD5 digest, which a restricted runtime may not offer. */
  @Test
  public void testKeyWrappingCheckDoesNotNeedMd5() throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final String supported = "RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING";
    assertThat(supported).isNotEqualTo(cfg.getKeyWrappingTransformation());
    final List<LocalizableMessage> why = new ArrayList<>();
    // Withdrawing MD5 withdraws the SUN provider, and with it what the rest of the check needs of
    // that provider: the SHA-1 digest of the OAEP cipher and of the default random generator the
    // cipher is initialized with, the SHA-256 digest of the HmacSHA256 MAC of SunJCE, and the
    // X.509 certificate factory.
    final Provider sunWithoutMd5 = new Provider("SunWithoutMd5", "1.0", "SUN services the check needs") {};
    sunWithoutMd5.put("MessageDigest.SHA-1", "sun.security.provider.SHA");
    sunWithoutMd5.put("Alg.Alias.MessageDigest.SHA1", "SHA-1");
    sunWithoutMd5.put("Alg.Alias.MessageDigest.SHA", "SHA-1");
    sunWithoutMd5.put("MessageDigest.SHA-256", "sun.security.provider.SHA2$SHA256");
    sunWithoutMd5.put("CertificateFactory.X.509", "sun.security.provider.X509Factory");
    withoutJceService("MessageDigest", "MD5", () ->
    {
      assertThat(cm.isConfigurationChangeAcceptable(withProperty(cfg, "getKeyWrappingTransformation", supported), why))
          .isFalse();
          .as("%s", why).isTrue();
      return null;
    }, sha1Only);
    }, sunWithoutMd5);
  }
    assertThat(why).hasSize(1);
    final String reason = why.get(0).toString();
    assertThat(why.get(0).ordinal()).as(reason).isEqualTo(ERR_CRYPTOMGR_CANNOT_GET_PREFERRED_KEY_WRAPPING_CIPHER.ordinal());
    assertThat(reason).contains("MD5").doesNotContain("key-wrapping-transformation");
  /** The error the probe provider fails with, set by the case using it. */
  private static volatile Error probeError;
  @DataProvider
  public Object[][] errorsWhichAreNotRefusals()
  {
    final List<Object[]> cases = new ArrayList<>();
    for (final String[] check : new String[][] {
        { "getCipherTransformation", "ErrorProbe/CBC/PKCS5Padding" },
        { "getMacAlgorithm", "ErrorProbe" },
        { "getKeyWrappingTransformation", "ErrorProbeWrap/ECB/NoPadding" } })
    {
      cases.add(new Object[] { check[0], check[1], new StackOverflowError("probe") });
      cases.add(new Object[] { check[0], check[1], new ExceptionInInitializerError(new IllegalStateException("probe")) });
    }
    return cases.toArray(new Object[0][]);
  }
  /**
   An Error which is not a refusal of the configuration, of the virtual machine or of a broken
   provider jar, propagates out of the cipher, MAC and key wrapping checks with its cause.
   */
  @Test(dataProvider = "errorsWhichAreNotRefusals")
  public void testErrorWhichIsNotARefusalPropagates(final String getter, final String value, final Error error)
      throws Exception
  {
    final CryptoManagerImpl cm = DirectoryServer.getCryptoManager();
    final CryptoManagerCfg cfg = getServerContext().getRootConfig().getCryptoManager();
    final Provider probe = new Provider("ErrorProbe", "1.0", "Fails with the error of the case") {};
    probe.put("KeyGenerator.ErrorProbe", FailingKeyGenerator.class.getName());
    probe.put("Cipher.ErrorProbeWrap", FailingCipher.class.getName());
    probeError = error;
    Security.insertProviderAt(probe, 1);
    try
    {
      final List<LocalizableMessage> why = new ArrayList<>();
      assertThatThrownBy(() -> cm.isConfigurationChangeAcceptable(withProperty(cfg, getter, value), why))
          .isSameAs(error);
      assertThat(why).isEmpty();
    }
    finally
    {
      Security.removeProvider("ErrorProbe");
      probeError = null;
    }
  }
  /** A key generator failing with the error of the case. */
  public static final class FailingKeyGenerator extends KeyGeneratorSpi
  {
    @Override
    protected void engineInit(final SecureRandom random)
    {
      throw probeError;
    }
    @Override
    protected void engineInit(final AlgorithmParameterSpec params, final SecureRandom random)
    {
      throw probeError;
    }
    @Override
    protected void engineInit(final int keySize, final SecureRandom random)
    {
      throw probeError;
    }
    @Override
    protected SecretKey engineGenerateKey()
    {
      throw probeError;
    }
  }
  /** A cipher which is found, and fails with the error of the case once initialized. */
  public static final class FailingCipher extends CipherSpi
  {
    @Override
    protected void engineSetMode(final String mode)
    {
      // Any mode.
    }
    @Override
    protected void engineSetPadding(final String padding)
    {
      // Any padding.
    }
    @Override
    protected int engineGetBlockSize()
    {
      return 0;
    }
    @Override
    protected int engineGetOutputSize(final int inputLen)
    {
      return 0;
    }
    @Override
    protected byte[] engineGetIV()
    {
      return null;
    }
    @Override
    protected AlgorithmParameters engineGetParameters()
    {
      return null;
    }
    @Override
    protected void engineInit(final int opmode, final Key key, final SecureRandom random)
    {
      throw probeError;
    }
    @Override
    protected void engineInit(final int opmode, final Key key, final AlgorithmParameterSpec params,
        final SecureRandom random)
    {
      throw probeError;
    }
    @Override
    protected void engineInit(final int opmode, final Key key, final AlgorithmParameters params,
        final SecureRandom random)
    {
      throw probeError;
    }
    @Override
    protected byte[] engineUpdate(final byte[] input, final int inputOffset, final int inputLen)
    {
      throw probeError;
    }
    @Override
    protected int engineUpdate(final byte[] input, final int inputOffset, final int inputLen, final byte[] output,
        final int outputOffset)
    {
      throw probeError;
    }
    @Override
    protected byte[] engineDoFinal(final byte[] input, final int inputOffset, final int inputLen)
    {
      throw probeError;
    }
    @Override
    protected int engineDoFinal(final byte[] input, final int inputOffset, final int inputLen, final byte[] output,
        final int outputOffset)
    {
      throw probeError;
    }
  }
  /**
   Returns the crypto manager configuration with a MAC algorithm which BC-FIPS does not offer in
   approved-only mode, so that SunJCE generates the MAC key the check wraps. The crypto manager
   generates keys with a random generator of the provider that came first when it was loaded, SUN
   in the test JVM, and BC-FIPS in approved-only mode refuses to generate a key with it.
   */
  private static CryptoManagerCfg withMacKeyFromSunJce(final CryptoManagerCfg cfg)
  {
    return withProperty(cfg, "getMacAlgorithm", "HmacMD5");
  }
  /**
   Runs {@code action} in a thread of its own which BC-FIPS serves in approved-only mode (a mode
   a thread cannot leave), with that provider installed first for the duration.
   */
  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
    {
      final FutureTask<T> task = new FutureTask<>(() ->
      {
        assertThat(CryptoServicesRegistrar.setApprovedOnlyMode(true)).isTrue();
        return action.call();
      });
      new Thread(task, "approved-only BC-FIPS").start();
      try
      {
        return task.get(1, TimeUnit.MINUTES);
      }
      catch (ExecutionException e)
      {
        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);
      }
    }
  }
  /**