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

Valery Kharseko
yesterday f93487455cc1b5d7062ea6577d99dea25bfd81b3
opendj-server-legacy/src/test/java/org/opends/server/crypto/CryptoManagerTestCase.java
@@ -28,6 +28,7 @@
import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.opends.server.protocols.internal.Requests.*;
import static org.opends.server.types.Attributes.*;
import static org.opends.server.util.CollectionUtils.newArrayList;
import static org.testng.Assert.*;
import java.io.File;
@@ -45,20 +46,32 @@
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.AlgorithmParameterSpec;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.crypto.CipherSpi;
import javax.crypto.KeyGeneratorSpi;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.bouncycastle.crypto.CryptoServicesRegistrar;
import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
@@ -72,8 +85,10 @@
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SSLContextBuilder;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.controls.RelaxRulesControl;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.opendj.ldif.ConnectionEntryReader;
import org.forgerock.opendj.ldap.schema.CoreSchema;
import org.forgerock.opendj.server.config.server.CryptoManagerCfg;
import org.forgerock.util.Options;
import org.opends.admin.ads.ADSContext;
@@ -81,14 +96,23 @@
import org.opends.server.TestCaseUtils;
import org.opends.server.backends.TrustStoreBackend;
import org.opends.server.core.DirectoryServer;
import org.opends.server.plugins.ShortCircuitPlugin;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.SearchRequest;
import org.opends.server.protocols.ldap.LDAPControl;
import org.opends.server.types.AttributeBuilder;
import org.opends.server.types.Control;
import org.opends.server.types.CryptoManager;
import org.opends.server.types.CryptoManagerException;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.Modification;
import org.opends.server.types.OperationType;
import org.opends.server.types.operation.PreOperationDeleteOperation;
import org.opends.server.util.CertificateManager;
import org.opends.server.util.KeyStoreReplaceFailure;
import org.opends.server.util.Platform.KeyType;
import org.opends.server.util.StaticUtils;
import org.opends.server.util.TimeThread;
import org.testng.annotations.AfterClass;
@@ -315,6 +339,408 @@
  }
  /**
   Replication replays the instance keys which reach cn=admin data together on parallel threads,
   and the trust store file is rewritten for each of them: none of the certificates may be lost,
   and the SSL contexts, which are built from that file, must never find it half written. A
   directory server which loses the certificate of a replication server that way fails every
   handshake with it (#1188).
   */
  @Test
  public void testInstanceKeysAddedTogetherAllReachTheTrustStore() throws Exception
  {
    addInstanceKeysTogetherWhileReadingTheTrustStore();
  }
  /**
   The same, with a trust store file which cannot be replaced at once and is written in place -
   one mounted on its own into a container, in a directory this process cannot write to, or one
   on Windows: there only the lock of the trust store keeps the SSL contexts
   from finding it half written.
   */
  @Test
  public void testInstanceKeysAddedTogetherInPlaceAllReachTheTrustStore() throws Exception
  {
    try (AutoCloseable inPlace = KeyStoreReplaceFailure.failKeyStoreReplace())
    {
      addInstanceKeysTogetherWhileReadingTheTrustStore();
    }
  }
  private void addInstanceKeysTogetherWhileReadingTheTrustStore() throws Exception
  {
    final int keys = 16;
    final TrustStoreBackend trustStore = (TrustStoreBackend) getServerContext()
        .getBackendConfigManager().getLocalBackendById(ID_ADS_TRUST_STORE_BACKEND);
    final Map<DN, ByteString> certificates = generateInstanceKeyCertificates(keys);
    final ExecutorService executor = Executors.newFixedThreadPool(keys + 1);
    final CyclicBarrier start = new CyclicBarrier(keys + 1);
    final AtomicBoolean adding = new AtomicBoolean(true);
    try
    {
      final List<Future<ResultCode>> adds = new ArrayList<>();
      for (Map.Entry<DN, ByteString> certificate : certificates.entrySet())
      {
        final Entry instanceKey = instanceKeyEntry(certificate.getKey(), certificate.getValue());
        adds.add(executor.submit(() -> {
          start.await();
          return getRootConnection().processAdd(instanceKey).getResultCode();
        }));
      }
      final Future<String> reads = executor.submit(() -> {
        start.await();
        do
        {
          try
          {
            trustStore.getTrustManagers();
          }
          catch (DirectoryException e)
          {
            return e.getMessage();
          }
        }
        while (adding.get());
        return null;
      });
      for (Future<ResultCode> add : adds)
      {
        assertThat(add.get(60, TimeUnit.SECONDS)).isEqualTo(ResultCode.SUCCESS);
      }
      adding.set(false);
      assertThat(reads.get(60, TimeUnit.SECONDS)).as("a trust store read while the keys were added").isNull();
      assertThat(trustedInstanceKeys(trustStore)).as("the instance keys the trust store holds")
          .containsAll(instanceKeyIDs(certificates));
    }
    finally
    {
      adding.set(false);
      executor.shutdownNow();
      // an add which outlived its wait above would put its entries back after the deletes
      executor.awaitTermination(60, TimeUnit.SECONDS);
      for (DN instanceKey : certificates.keySet())
      {
        getRootConnection().processDelete(instanceKey);
      }
    }
  }
  /**
   Replication replays the deletes of instance keys together with the adds of others: a delete
   lost to an add would leave a compromised key trusted, an add lost to a delete would leave a
   server untrusted (#1188).
   */
  @Test
  public void testInstanceKeysAddedAndDeletedTogetherAllReachTheTrustStore() throws Exception
  {
    final int keys = 16;
    final TrustStoreBackend trustStore = (TrustStoreBackend) getServerContext()
        .getBackendConfigManager().getLocalBackendById(ID_ADS_TRUST_STORE_BACKEND);
    final Map<DN, ByteString> deleted = generateInstanceKeyCertificates(keys);
    final Map<DN, ByteString> added = generateInstanceKeyCertificates(keys);
    final ExecutorService executor = Executors.newFixedThreadPool(2 * keys);
    final CyclicBarrier start = new CyclicBarrier(2 * keys);
    try
    {
      for (Map.Entry<DN, ByteString> certificate : deleted.entrySet())
      {
        assertThat(getRootConnection().processAdd(instanceKeyEntry(certificate.getKey(), certificate.getValue()))
            .getResultCode()).isEqualTo(ResultCode.SUCCESS);
      }
      assertThat(trustedInstanceKeys(trustStore)).as("the instance keys the trust store holds before")
          .containsAll(instanceKeyIDs(deleted));
      final List<Future<ResultCode>> changes = new ArrayList<>();
      for (Map.Entry<DN, ByteString> certificate : added.entrySet())
      {
        final Entry instanceKey = instanceKeyEntry(certificate.getKey(), certificate.getValue());
        changes.add(executor.submit(() -> {
          start.await();
          return getRootConnection().processAdd(instanceKey).getResultCode();
        }));
      }
      for (DN instanceKey : deleted.keySet())
      {
        changes.add(executor.submit(() -> {
          start.await();
          return getRootConnection().processDelete(instanceKey).getResultCode();
        }));
      }
      for (Future<ResultCode> change : changes)
      {
        assertThat(change.get(60, TimeUnit.SECONDS)).isEqualTo(ResultCode.SUCCESS);
      }
      final Set<String> trusted = trustedInstanceKeys(trustStore);
      assertThat(trusted).as("the instance keys the trust store holds").containsAll(instanceKeyIDs(added));
      assertThat(trusted).as("the instance keys the trust store holds")
          .doesNotContainAnyElementsOf(instanceKeyIDs(deleted));
    }
    finally
    {
      executor.shutdownNow();
      // a change which outlived its wait above would put its entries back after the deletes
      executor.awaitTermination(60, TimeUnit.SECONDS);
      for (DN instanceKey : added.keySet())
      {
        getRootConnection().processDelete(instanceKey);
      }
      for (DN instanceKey : deleted.keySet())
      {
        getRootConnection().processDelete(instanceKey);
      }
    }
  }
  /** The IDs of the instance keys whose certificates the trust store holds. */
  private static Set<String> trustedInstanceKeys(TrustStoreBackend trustStore) throws Exception
  {
    final Set<String> trusted = new HashSet<>();
    for (TrustManager trustManager : trustStore.getTrustManagers())
    {
      for (X509Certificate issuer : ((X509TrustManager) trustManager).getAcceptedIssuers())
      {
        trusted.add(CryptoManagerImpl.getInstanceKeyID(issuer.getEncoded()));
      }
    }
    return trusted;
  }
  private static List<String> instanceKeyIDs(Map<DN, ByteString> certificates) throws Exception
  {
    final List<String> ids = new ArrayList<>();
    for (ByteString certificate : certificates.values())
    {
      ids.add(CryptoManagerImpl.getInstanceKeyID(certificate.toByteArray()));
    }
    return ids;
  }
  /**
   An instance key the trust store cannot take, such as one which reached cn=admin data without
   its certificate, leaves this server without trust in the server it belongs to, so every
   replication connection to that server fails its handshake: the refusal must be in the error
   log, which is where the administrator of such a server looks (#1188). A key without its
   certificate is refused by the schema check of its copy, before the trust store backend sees
   it; a refusal of the backend itself takes the same branch of CryptoManagerSync, and keeps its
   result code (testTrustStoreRefusesAnAliasByWhatIsWrongWithIt).
   */
  @Test
  public void testInstanceKeyTheTrustStoreRefusesIsReported() throws Exception
  {
    final DN instanceKey = generateInstanceKeyCertificates(1).keySet().iterator().next();
    final Entry withoutCertificate = new Entry(instanceKey, null, null, null);
    withoutCertificate.addObjectClass(CoreSchema.getTopObjectClass());
    withoutCertificate.addObjectClass(getServerContext().getSchema().getObjectClass(OC_CRYPTO_INSTANCE_KEY));
    withoutCertificate.addAttribute(
        create(ATTR_CRYPTO_KEY_ID, instanceKey.rdn().getFirstAVA().getAttributeValue().toString()));
    final DN trustStoreEntry = DN.valueOf(DN_TRUST_STORE_ROOT).child(instanceKey.rdn());
    TestCaseUtils.ERROR_TEXT_WRITER.clear();
    try
    {
      // Relaxed, as only a replayed add can store an instance key without its certificate.
      assertThat(getRootConnection().processAdd(withoutCertificate,
              newArrayList((Control) new LDAPControl(RelaxRulesControl.OID, false))).getResultCode())
          .isEqualTo(ResultCode.SUCCESS);
      assertThat(DirectoryServer.getEntry(trustStoreEntry)).as("the instance key in the trust store").isNull();
      assertThat(TestCaseUtils.ERROR_TEXT_WRITER.getMessages())
          .as("the error log")
          .anyMatch(record -> record.contains("severity=ERROR") && record.contains(trustStoreEntry.toString()));
    }
    finally
    {
      getRootConnection().processDelete(instanceKey);
    }
  }
  /**
   dsreplication seeds the trust store of a server it enables with the instance keys of the
   topology, so the add of an instance key may find its certificate there already: that is no
   failure, and the error log must stay quiet about it.
   */
  @Test
  public void testInstanceKeyTheTrustStoreHoldsAlreadyIsNotReported() throws Exception
  {
    final Map.Entry<DN, ByteString> certificate =
        generateInstanceKeyCertificates(1).entrySet().iterator().next();
    final DN instanceKey = certificate.getKey();
    final DN trustStoreEntry = DN.valueOf(DN_TRUST_STORE_ROOT).child(instanceKey.rdn());
    try
    {
      assertThat(getRootConnection().processAdd(instanceKeyEntry(trustStoreEntry, certificate.getValue()))
          .getResultCode()).as("the seeding of the trust store").isEqualTo(ResultCode.SUCCESS);
      TestCaseUtils.ERROR_TEXT_WRITER.clear();
      assertThat(getRootConnection().processAdd(instanceKeyEntry(instanceKey, certificate.getValue()))
          .getResultCode()).isEqualTo(ResultCode.SUCCESS);
      assertThat(DirectoryServer.getEntry(trustStoreEntry)).as("the instance key in the trust store").isNotNull();
      assertThat(TestCaseUtils.ERROR_TEXT_WRITER.getMessages())
          .as("the error log")
          .noneMatch(record -> record.contains("severity=ERROR") && record.contains(trustStoreEntry.toString()));
    }
    finally
    {
      getRootConnection().processDelete(instanceKey);
      getRootConnection().processDelete(trustStoreEntry);
    }
  }
  /**
   An instance key marked compromised has to leave the trust store: while its certificate is
   there, this server goes on trusting whoever holds the key. A trust store which cannot drop it
   must say so in the error log, as it does for a certificate it cannot take.
   */
  @Test
  public void testCompromisedInstanceKeyTheTrustStoreCannotDropIsReported() throws Exception
  {
    final Map.Entry<DN, ByteString> certificate =
        generateInstanceKeyCertificates(1).entrySet().iterator().next();
    final DN instanceKey = certificate.getKey();
    final DN trustStoreEntry = DN.valueOf(DN_TRUST_STORE_ROOT).child(instanceKey.rdn());
    try
    {
      assertThat(getRootConnection().processAdd(instanceKeyEntry(instanceKey, certificate.getValue()))
          .getResultCode()).isEqualTo(ResultCode.SUCCESS);
      assertThat(DirectoryServer.getEntry(trustStoreEntry)).as("the instance key in the trust store").isNotNull();
      ShortCircuitPlugin.registerShortCircuit(OperationType.DELETE, "PreOperation",
          ResultCode.UNWILLING_TO_PERFORM.intValue(), 0, 1,
          op -> trustStoreEntry.equals(((PreOperationDeleteOperation) op).getEntryDN()));
      TestCaseUtils.ERROR_TEXT_WRITER.clear();
      assertThat(getRootConnection().processModify(instanceKey, newArrayList(new Modification(
              ADD, create(ATTR_CRYPTO_KEY_COMPROMISED_TIME, TimeThread.getGeneralizedTime()))))
          .getResultCode()).isEqualTo(ResultCode.SUCCESS);
      assertThat(DirectoryServer.getEntry(trustStoreEntry)).as("the compromised key in the trust store").isNotNull();
      assertThat(TestCaseUtils.ERROR_TEXT_WRITER.getMessages())
          .as("the error log")
          .anyMatch(record -> record.contains("severity=ERROR") && record.contains(trustStoreEntry.toString()));
    }
    finally
    {
      ShortCircuitPlugin.deregisterShortCircuit(OperationType.DELETE, "PreOperation");
      getRootConnection().processDelete(instanceKey);
      getRootConnection().processDelete(trustStoreEntry);
    }
  }
  /**
   The delete of an instance key whose certificate has left the trust store already - when the
   key was marked compromised, say - finds nothing to drop there: that is no failure, and the
   error log must stay quiet about it.
   */
  @Test
  public void testInstanceKeyTheTrustStoreLacksAlreadyIsNotReported() throws Exception
  {
    final Map.Entry<DN, ByteString> certificate =
        generateInstanceKeyCertificates(1).entrySet().iterator().next();
    final DN instanceKey = certificate.getKey();
    final DN trustStoreEntry = DN.valueOf(DN_TRUST_STORE_ROOT).child(instanceKey.rdn());
    try
    {
      assertThat(getRootConnection().processAdd(instanceKeyEntry(instanceKey, certificate.getValue()))
          .getResultCode()).isEqualTo(ResultCode.SUCCESS);
      assertThat(getRootConnection().processDelete(trustStoreEntry).getResultCode())
          .as("the certificate leaving the trust store first").isEqualTo(ResultCode.SUCCESS);
      TestCaseUtils.ERROR_TEXT_WRITER.clear();
      assertThat(getRootConnection().processDelete(instanceKey).getResultCode()).isEqualTo(ResultCode.SUCCESS);
      assertThat(TestCaseUtils.ERROR_TEXT_WRITER.getMessages())
          .as("the error log")
          .noneMatch(record -> record.contains("severity=ERROR") && record.contains(trustStoreEntry.toString()));
    }
    finally
    {
      getRootConnection().processDelete(instanceKey);
      getRootConnection().processDelete(trustStoreEntry);
    }
  }
  /**
   The trust store refuses an alias it holds already as an entry which exists, and the delete of
   one it does not hold as an entry which does not, whichever check catches it: the sync of the
   instance keys tells those apart from a failure by their result codes, and reports a failure in
   the error log. Here the backend is called directly, past the existence check of the core, as an
   add which lost the race for its alias to another one would reach it.
   */
  @Test
  public void testTrustStoreRefusesAnAliasByWhatIsWrongWithIt() throws Exception
  {
    final TrustStoreBackend trustStore = (TrustStoreBackend) getServerContext()
        .getBackendConfigManager().getLocalBackendById(ID_ADS_TRUST_STORE_BACKEND);
    final Map.Entry<DN, ByteString> certificate =
        generateInstanceKeyCertificates(1).entrySet().iterator().next();
    final DN trustStoreEntry = DN.valueOf(DN_TRUST_STORE_ROOT).child(certificate.getKey().rdn());
    try
    {
      assertThat(getRootConnection().processAdd(instanceKeyEntry(trustStoreEntry, certificate.getValue()))
          .getResultCode()).isEqualTo(ResultCode.SUCCESS);
      assertThatThrownBy(() -> trustStore.addEntry(instanceKeyEntry(trustStoreEntry, certificate.getValue()), null))
          .as("an alias in use")
          .isInstanceOfSatisfying(DirectoryException.class,
              e -> assertThat(e.getResultCode()).isEqualTo(ResultCode.ENTRY_ALREADY_EXISTS));
      assertThat(getRootConnection().processDelete(trustStoreEntry).getResultCode()).isEqualTo(ResultCode.SUCCESS);
      assertThatThrownBy(() -> trustStore.deleteEntry(trustStoreEntry, null))
          .as("an alias not in use")
          .isInstanceOfSatisfying(DirectoryException.class,
              e -> assertThat(e.getResultCode()).isEqualTo(ResultCode.NO_SUCH_OBJECT));
    }
    finally
    {
      getRootConnection().processDelete(trustStoreEntry);
    }
  }
  /** Generates self-signed certificates, by the DN of the instance key entry each would have. */
  private static Map<DN, ByteString> generateInstanceKeyCertificates(int count) throws Exception
  {
    final Path keyStore = Files.createTempFile("instance-keys", ".jks");
    Files.delete(keyStore);
    try
    {
      final CertificateManager certificateManager =
          new CertificateManager(keyStore.toString(), CertificateManager.KEY_STORE_TYPE_JKS, "password");
      final Map<DN, ByteString> certificates = new LinkedHashMap<>();
      for (int i = 0; i < count; i++)
      {
        final String alias = "instance-key-" + i;
        certificateManager.generateSelfSignedCertificate(KeyType.EC, alias, "CN=" + alias, 1);
        final byte[] certificate = certificateManager.getCertificate(alias).getEncoded();
        certificates.put(
            DN.valueOf(ATTR_CRYPTO_KEY_ID + "=" + CryptoManagerImpl.getInstanceKeyID(certificate)
                + ",cn=instance keys,cn=admin data"),
            ByteString.wrap(certificate));
      }
      return certificates;
    }
    finally
    {
      Files.deleteIfExists(keyStore);
    }
  }
  /** Builds an instance key entry the way the crypto manager publishes its own in cn=admin data. */
  private static Entry instanceKeyEntry(DN dn, ByteString certificate) throws DirectoryException
  {
    final Entry entry = new Entry(dn, null, null, null);
    entry.addObjectClass(CoreSchema.getTopObjectClass());
    entry.addObjectClass(getServerContext().getSchema().getObjectClass(OC_CRYPTO_INSTANCE_KEY));
    entry.addAttribute(create(ATTR_CRYPTO_KEY_ID, dn.rdn().getFirstAVA().getAttributeValue().toString()));
    final AttributeBuilder builder =
        new AttributeBuilder(getServerContext().getSchema().getAttributeType(ATTR_CRYPTO_PUBLIC_KEY_CERTIFICATE));
    builder.setOption("binary");
    builder.add(certificate);
    entry.addAttribute(builder.toAttribute());
    return entry;
  }
  /**
   A key wrapping transformation this Java runtime cannot provide is refused, at start (where
   the refusal is what keeps the server from starting) as on a change, and the refusal has to
   name the property to set, not only the cipher which failed: on a FIPS-restricted runtime