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

Valery Kharseko
yesterday f93487455cc1b5d7062ea6577d99dea25bfd81b3
opendj-server-legacy/src/test/java/org/opends/server/util/CertificateManagerTestCase.java
@@ -22,11 +22,26 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import com.sun.security.auth.module.UnixSystem;
import org.testng.SkipException;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -1416,6 +1431,389 @@
  /**
   * Tests that a reader of a key store file never finds it half written while it is rewritten:
   * the trust store of the server is read from its file for each SSL context, and at the next
   * start, by readers which do not share any lock with the writer, and a server stopped part way
   * through a write must not be left with a truncated file it cannot start with (#1188). On a
   * POSIX file system only: elsewhere (Windows) the key store is written in place.
   *
   * @throws  Exception  If a problem occurs.
   */
  @Test
  public void testKeyStoreFileIsNeverSeenHalfWritten()
         throws Exception
  {
    skipUnlessPosix();
    final File tmpDir = TestCaseUtils.createTemporaryDirectory("halfWritten");
    final ExecutorService executor = Executors.newSingleThreadExecutor();
    final AtomicBoolean writing = new AtomicBoolean(true);
    try
    {
      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
      final File destination = new File(tmpDir, "ads-truststore");
      final CertificateManager manager =
           new CertificateManager(destination.getAbsolutePath(), "JKS", "password");
      manager.addTrustedCertificate("cert-0", certificate);
      final CountDownLatch started = new CountDownLatch(1);
      final AtomicInteger loads = new AtomicInteger();
      final Future<String> reads = executor.submit(() -> {
        final KeyStore keyStore = KeyStore.getInstance("JKS");
        started.countDown();
        while (writing.get())
        {
          try (FileInputStream in = new FileInputStream(destination))
          {
            keyStore.load(in, "password".toCharArray());
            loads.incrementAndGet();
          }
          catch (Exception e)
          {
            return e.toString();
          }
        }
        return null;
      });
      assertTrue(started.await(60, TimeUnit.SECONDS), "the reader started");
      for (int i = 1; i <= 200; i++)
      {
        manager.addTrustedCertificate("cert-" + i, certificate);
      }
      writing.set(false);
      assertNull(reads.get(60, TimeUnit.SECONDS), "a read of the key store while it was rewritten");
      assertTrue(loads.get() > 0, "the reader loaded the key store while it was rewritten");
    }
    finally
    {
      writing.set(false);
      executor.shutdownNow();
      executor.awaitTermination(60, TimeUnit.SECONDS);
      TestCaseUtils.deleteDirectory(tmpDir);
    }
  }
  /**
   * Tests that the rewrite of a key store keeps the file it replaces as it was: its permissions,
   * which the file it is written to first does not have, and the link the key store path may be,
   * which must go on pointing where the administrator put the key store.
   *
   * @throws  Exception  If a problem occurs.
   */
  @Test
  public void testKeyStoreRewriteKeepsPermissionsAndLink()
         throws Exception
  {
    skipUnlessPosix();
    final File tmpDir = TestCaseUtils.createTemporaryDirectory("rewriteKeeps");
    try
    {
      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
      final Path store = Files.createDirectory(tmpDir.toPath().resolve("store"));
      final Path target = store.resolve("ads-truststore");
      new CertificateManager(target.toString(), "JKS", "password").addTrustedCertificate("cert-0", certificate);
      Files.setPosixFilePermissions(target, PosixFilePermissions.fromString("rw-r-----"));
      final Path link = Files.createSymbolicLink(tmpDir.toPath().resolve("ads-truststore"), target);
      new CertificateManager(link.toString(), "JKS", "password").addTrustedCertificate("cert-1", certificate);
      assertTrue(Files.isSymbolicLink(link), "the key store path is a link still");
      assertEquals(PosixFilePermissions.toString(Files.getPosixFilePermissions(target)), "rw-r-----");
      assertTrue(loadJks(target.toFile(), "password").containsAlias("cert-1"), "the key store the link points to");
    }
    finally
    {
      // The link first: once the file it points to is gone, it no longer exists for deleteDirectory.
      Files.deleteIfExists(tmpDir.toPath().resolve("ads-truststore"));
      TestCaseUtils.deleteDirectory(tmpDir);
    }
  }
  /**
   * Tests that a key store in a directory this process cannot write to, as one mounted on its own
   * into a container, is still rewritten: in place, there being nowhere else to write it.
   *
   * @throws  Exception  If a problem occurs.
   */
  @Test
  public void testKeyStoreInADirectoryNotWritableIsRewrittenInPlace()
         throws Exception
  {
    skipUnlessPosix();
    final File tmpDir = TestCaseUtils.createTemporaryDirectory("rewriteInPlace");
    final Path store = tmpDir.toPath().resolve("store");
    try
    {
      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
      Files.createDirectory(store);
      final Path target = store.resolve("ads-truststore");
      final CertificateManager manager = new CertificateManager(target.toString(), "JKS", "password");
      manager.addTrustedCertificate("cert-0", certificate);
      Files.setPosixFilePermissions(store, PosixFilePermissions.fromString("r-xr-xr-x"));
      if (Files.isWritable(store))
      {
        throw new SkipException("this process writes to a directory whatever its permissions");
      }
      manager.addTrustedCertificate("cert-1", certificate);
      assertTrue(loadJks(target.toFile(), "password").containsAlias("cert-1"), "the key store");
    }
    finally
    {
      if (Files.exists(store))
      {
        Files.setPosixFilePermissions(store, PosixFilePermissions.fromString("rwxr-xr-x"));
      }
      TestCaseUtils.deleteDirectory(tmpDir);
    }
  }
  /**
   * Tests that the rewrite of a key store keeps its group, a group of this process other than the
   * one a new file gets, and still replaces the file at once rather than writing it in place.
   *
   * @throws  Exception  If a problem occurs.
   */
  @Test
  public void testKeyStoreRewriteKeepsTheGroup()
         throws Exception
  {
    skipUnlessPosix();
    final File tmpDir = TestCaseUtils.createTemporaryDirectory("rewriteKeepsGroup");
    try
    {
      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
      final Path target = tmpDir.toPath().resolve("ads-truststore");
      new CertificateManager(target.toString(), "JKS", "password").addTrustedCertificate("cert-0", certificate);
      final long group = moveToAnotherGroup(target);
      final Object file = Files.getAttribute(target, "unix:ino");
      new CertificateManager(target.toString(), "JKS", "password").addTrustedCertificate("cert-1", certificate);
      assertEquals(((Integer) Files.getAttribute(target, "unix:gid")).longValue(), group,
          "the group of the key store");
      assertNotEquals(Files.getAttribute(target, "unix:ino"), file, "the key store replaced, not written in place");
      assertTrue(loadJks(target.toFile(), "password").containsAlias("cert-1"), "the key store");
    }
    finally
    {
      TestCaseUtils.deleteDirectory(tmpDir);
    }
  }
  @DataProvider
  public Object[][] groupNotTaken()
  {
    return new Object[][] { { "ignored" }, { "refused" } };
  }
  /**
   * Tests that a key store whose group the new file cannot take - on a file system which ignores
   * a change of group, or for a group this process is not a member of - is rewritten in place,
   * where it keeps its group, rather than replaced by a file of another group.
   *
   * @param  how  How the group is not taken.
   *
   * @throws  Exception  If a problem occurs.
   */
  @Test(dataProvider = "groupNotTaken")
  public void testKeyStoreWhoseGroupCannotBeTakenIsRewrittenInPlace(String how)
         throws Exception
  {
    skipUnlessPosix();
    final File tmpDir = TestCaseUtils.createTemporaryDirectory("rewriteGroupNotTaken");
    try
    {
      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
      final Path target = tmpDir.toPath().resolve("ads-truststore");
      new CertificateManager(target.toString(), "JKS", "password").addTrustedCertificate("cert-0", certificate);
      final long group = moveToAnotherGroup(target);
      final Object file = Files.getAttribute(target, "unix:ino");
      try (AutoCloseable notTaken = "refused".equals(how)
          ? KeyStoreReplaceFailure.refuseKeyStoreGroup()
          : KeyStoreReplaceFailure.ignoreKeyStoreGroup())
      {
        new CertificateManager(target.toString(), "JKS", "password").addTrustedCertificate("cert-1", certificate);
      }
      assertEquals(((Integer) Files.getAttribute(target, "unix:gid")).longValue(), group,
          "the group of the key store");
      assertEquals(Files.getAttribute(target, "unix:ino"), file, "the key store written in place, not replaced");
      assertTrue(loadJks(target.toFile(), "password").containsAlias("cert-1"), "the key store");
      assertEquals(tmpDir.list(), new String[] { "ads-truststore" }, "the files next to the key store");
    }
    finally
    {
      TestCaseUtils.deleteDirectory(tmpDir);
    }
  }
  /**
   * Gives a file a group of this process other than the one a new file gets.
   *
   * @param  target  The file.
   *
   * @return  the group given
   */
  private static long moveToAnotherGroup(Path target) throws Exception
  {
    final long newFileGroup = ((Integer) Files.getAttribute(target, "unix:gid")).longValue();
    final UnixSystem process = new UnixSystem();
    long group = process.getGid();
    for (long g : process.getGroups())
    {
      if (group == newFileGroup)
      {
        group = g;
      }
    }
    if (group == newFileGroup)
    {
      throw new SkipException("this process is a member of no group but the one a new file gets");
    }
    Files.getFileAttributeView(target, PosixFileAttributeView.class).setGroup(FileSystems.getDefault()
        .getUserPrincipalLookupService().lookupPrincipalByGroupName(String.valueOf(group)));
    return group;
  }
  /**
   * Tests that a key store file with another hard link is rewritten in place: a new file in its
   * place would leave the other name with the old key store.
   *
   * @throws  Exception  If a problem occurs.
   */
  @Test
  public void testKeyStoreWithAnotherHardLinkIsRewrittenInPlace()
         throws Exception
  {
    skipUnlessPosix();
    final File tmpDir = TestCaseUtils.createTemporaryDirectory("rewriteHardLink");
    try
    {
      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
      final Path target = tmpDir.toPath().resolve("ads-truststore");
      new CertificateManager(target.toString(), "JKS", "password").addTrustedCertificate("cert-0", certificate);
      final Path other = Files.createLink(tmpDir.toPath().resolve("ads-truststore.link"), target);
      new CertificateManager(target.toString(), "JKS", "password").addTrustedCertificate("cert-1", certificate);
      assertTrue(loadJks(other.toFile(), "password").containsAlias("cert-1"), "the key store by its other name");
    }
    finally
    {
      TestCaseUtils.deleteDirectory(tmpDir);
    }
  }
  /**
   * Tests that a key store file this process may not write to is not rewritten, even in a
   * directory it may write to, where the file could be replaced.
   *
   * @throws  Exception  If a problem occurs.
   */
  @Test
  public void testKeyStoreNotWritableIsRefused()
         throws Exception
  {
    skipUnlessPosix();
    final File tmpDir = TestCaseUtils.createTemporaryDirectory("rewriteRefused");
    try
    {
      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
      final Path target = tmpDir.toPath().resolve("ads-truststore");
      final CertificateManager manager = new CertificateManager(target.toString(), "JKS", "password");
      manager.addTrustedCertificate("cert-0", certificate);
      Files.setPosixFilePermissions(target, PosixFilePermissions.fromString("r--r--r--"));
      if (Files.isWritable(target))
      {
        throw new SkipException("this process writes to a file whatever its permissions");
      }
      assertThrows(KeyStoreException.class, () -> manager.addTrustedCertificate("cert-1", certificate));
      assertFalse(loadJks(target.toFile(), "password").containsAlias("cert-1"), "the key store");
      assertEquals(PosixFilePermissions.toString(Files.getPosixFilePermissions(target)), "r--r--r--");
    }
    finally
    {
      TestCaseUtils.deleteDirectory(tmpDir);
    }
  }
  /**
   * Tests that a key store which cannot be replaced at once, as a file mounted on its own into a
   * container (EBUSY, EXDEV), is still rewritten: in place.
   *
   * @throws  Exception  If a problem occurs.
   */
  @Test
  public void testKeyStoreWhichCannotBeReplacedIsRewrittenInPlace()
         throws Exception
  {
    final File tmpDir = TestCaseUtils.createTemporaryDirectory("rewriteNotReplaced");
    try
    {
      final Certificate certificate = new CertificateFixture("CN=Example CA,O=Example").getCaCertificate();
      final File target = new File(tmpDir, "ads-truststore");
      final CertificateManager manager = new CertificateManager(target.getAbsolutePath(), "JKS", "password");
      manager.addTrustedCertificate("cert-0", certificate);
      try (AutoCloseable inPlace = KeyStoreReplaceFailure.failKeyStoreReplace())
      {
        manager.addTrustedCertificate("cert-1", certificate);
      }
      assertTrue(loadJks(target, "password").containsAlias("cert-1"), "the key store");
      assertEquals(tmpDir.list(), new String[] { "ads-truststore" }, "the files next to the key store");
    }
    finally
    {
      TestCaseUtils.deleteDirectory(tmpDir);
    }
  }
  private static void skipUnlessPosix()
  {
    if (!FileSystems.getDefault().supportedFileAttributeViews().contains("posix"))
    {
      throw new SkipException("POSIX file permissions and links are not available here");
    }
  }
  private static KeyStore loadJks(File file, String password) throws Exception
  {
    final KeyStore keyStore = KeyStore.getInstance("JKS");
    try (FileInputStream in = new FileInputStream(file))
    {
      keyStore.load(in, password.toCharArray());
    }
    return keyStore;
  }
  /**
   * Exports a certificate to a temporary file.
   *
   * @throws  Exception  If a problem occurs.