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