// SPDX-FileCopyrightText: 2022 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 package pgp.cert_d; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import pgp.cert_d.backend.FileBasedCertificateDirectoryBackend; import pgp.cert_d.dummy.TestKeyMaterialMerger; import pgp.cert_d.dummy.TestKeyMaterialReaderBackend; import pgp.cert_d.subkey_lookup.InMemorySubkeyLookup; import pgp.cert_d.subkey_lookup.SubkeyLookup; import pgp.certificate_store.certificate.Certificate; import pgp.certificate_store.certificate.Key; import pgp.certificate_store.certificate.KeyMaterial; import pgp.certificate_store.certificate.KeyMaterialMerger; import pgp.certificate_store.certificate.KeyMaterialReaderBackend; import pgp.certificate_store.exception.BadDataException; import pgp.certificate_store.exception.BadNameException; import pgp.certificate_store.exception.NotAStoreException; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Set; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeFalse; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static pgp.cert_d.TestKeys.CEDRIC_FP; import static pgp.cert_d.TestKeys.HARRY_FP; import static pgp.cert_d.TestKeys.RON_FP; public class PGPCertificateDirectoryTest { private static final KeyMaterialMerger merger = new TestKeyMaterialMerger(); private static Stream provideTestSubjects() throws IOException, NotAStoreException { PGPCertificateDirectory inMemory = PGPCertificateDirectories.inMemoryCertificateDirectory( new TestKeyMaterialReaderBackend()); File tempDir = Files.createTempDirectory("pgp-cert-d-test").toFile(); tempDir.deleteOnExit(); PGPCertificateDirectory fileBased = PGPCertificateDirectories.fileBasedCertificateDirectory( new TestKeyMaterialReaderBackend(), tempDir, new InMemorySubkeyLookup()); return Stream.of( Arguments.of(Named.of("InMemoryCertificateDirectory", inMemory)), Arguments.of(Named.of("FileBasedCertificateDirectory", fileBased))); } @ParameterizedTest @MethodSource("provideTestSubjects") public void getNonExistentCertByFingerprintThrowsNoSuchElementException(PGPCertificateDirectory directory) { assertThrows(NoSuchElementException.class, () -> directory.getByFingerprint("0000000000000000000000000000000000000000")); } @ParameterizedTest @MethodSource("provideTestSubjects") public void getNonExistentCertByFingerprintIfChangedThrowsNoSuchElementException(PGPCertificateDirectory directory) { assertThrows(NoSuchElementException.class, () -> directory.getByFingerprintIfChanged("0000000000000000000000000000000000000000", 12)); } @ParameterizedTest @MethodSource("provideTestSubjects") public void getNonExistentCertBySpecialNameThrowsNoSuchElementException(PGPCertificateDirectory directory) { assertThrows(NoSuchElementException.class, () -> directory.getBySpecialName(SpecialNames.TRUST_ROOT)); } @ParameterizedTest @MethodSource("provideTestSubjects") public void getNonExistentCertBySpecialNameIfChangedThrowsNoSuchElementException(PGPCertificateDirectory directory) { assertThrows(NoSuchElementException.class, () -> directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, 12)); } @ParameterizedTest @MethodSource("provideTestSubjects") public void getNonExistentTrustRootThrowsNoSuchElementException(PGPCertificateDirectory directory) { assertThrows(NoSuchElementException.class, () -> directory.getTrustRoot()); } @ParameterizedTest @MethodSource("provideTestSubjects") public void getNonExistentTrustRootIfChangedThrowsNoSuchElementException(PGPCertificateDirectory directory) { assertThrows(NoSuchElementException.class, () -> directory.getTrustRootCertificateIfChanged(12)); } @ParameterizedTest @MethodSource("provideTestSubjects") public void getNonExistentTrustRootCertificateThrowsNoSuchElementException(PGPCertificateDirectory directory) { assertThrows(NoSuchElementException.class, () -> directory.getTrustRootCertificate()); } @ParameterizedTest @MethodSource("provideTestSubjects") public void lockDirectoryAndTryInsertWillFail(PGPCertificateDirectory directory) throws IOException, InterruptedException, BadDataException { // Manually lock the dir assertFalse(directory.backend.getLock().isLocked()); directory.backend.getLock().lockDirectory(); assertTrue(directory.backend.getLock().isLocked()); assertFalse(directory.backend.getLock().tryLockDirectory()); Certificate inserted = directory.tryInsert(TestKeys.getCedricCert(), merger); assertNull(inserted); directory.backend.getLock().releaseDirectory(); inserted = directory.tryInsert(TestKeys.getCedricCert(), merger); assertNotNull(inserted); } @ParameterizedTest @MethodSource("provideTestSubjects") public void lockDirectoryAndTryInsertTrustRootWillFail(PGPCertificateDirectory directory) throws IOException, InterruptedException, BadDataException { // Manually lock the dir assertFalse(directory.backend.getLock().isLocked()); directory.backend.getLock().lockDirectory(); assertTrue(directory.backend.getLock().isLocked()); KeyMaterial inserted = directory.tryInsertTrustRoot(TestKeys.getHarryKey(), merger); assertNull(inserted); directory.backend.getLock().releaseDirectory(); inserted = directory.tryInsertTrustRoot(TestKeys.getHarryKey(), merger); assertNotNull(inserted); } @ParameterizedTest @MethodSource("provideTestSubjects") public void lockDirectoryAndTryInsertWithSpecialNameWillFail(PGPCertificateDirectory directory) throws IOException, InterruptedException, BadDataException, BadNameException { // Manually lock the dir assertFalse(directory.backend.getLock().isLocked()); directory.backend.getLock().lockDirectory(); assertTrue(directory.backend.getLock().isLocked()); Certificate inserted = directory.tryInsertWithSpecialName(SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger); assertNull(inserted); directory.backend.getLock().releaseDirectory(); inserted = directory.tryInsertWithSpecialName(SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger); assertNotNull(inserted); } @ParameterizedTest @MethodSource("provideTestSubjects") public void getByInvalidNameFails(PGPCertificateDirectory directory) { assertThrows(BadNameException.class, () -> directory.getBySpecialName("invalid")); } @ParameterizedTest @MethodSource("provideTestSubjects") public void testInsertAndGetSingleCert(PGPCertificateDirectory directory) throws BadDataException, IOException, InterruptedException, BadNameException { assertThrows(NoSuchElementException.class, () -> directory.getByFingerprint(CEDRIC_FP), "Empty directory MUST NOT contain certificate"); Certificate certificate = directory.insert(TestKeys.getCedricCert(), merger); assertEquals(CEDRIC_FP, certificate.getFingerprint(), "Fingerprint of inserted cert MUST match"); Certificate get = directory.getByFingerprint(CEDRIC_FP); assertEquals(CEDRIC_FP, get.getFingerprint(), "Fingerprint of retrieved cert MUST match"); byte[] expected = TestKeys.CEDRIC_CERT.getBytes(Charset.forName("UTF8")); ByteArrayOutputStream actual = new ByteArrayOutputStream(); Streams.pipeAll(get.getInputStream(), actual); assertArrayEquals(expected, actual.toByteArray(), "InputStream of cert MUST match what we gave in"); } @ParameterizedTest @MethodSource("provideTestSubjects") public void testInsertAndGetTrustRootAndCert(PGPCertificateDirectory directory) throws BadDataException, IOException, InterruptedException { assertThrows(NoSuchElementException.class, () -> directory.getTrustRoot()); KeyMaterial trustRootMaterial = directory.insertTrustRoot( TestKeys.getHarryKey(), merger); assertNotNull(trustRootMaterial); assertTrue(trustRootMaterial instanceof Key); assertEquals(HARRY_FP, trustRootMaterial.getFingerprint()); Key trustRoot = (Key) directory.getTrustRoot(); assertEquals(HARRY_FP, trustRoot.getFingerprint()); Certificate trustRootCert = directory.getTrustRootCertificate(); assertEquals(HARRY_FP, trustRootCert.getFingerprint()); directory.tryInsert(TestKeys.getRonCert(), merger); directory.insert(TestKeys.getCedricCert(), merger); Set expected = new HashSet<>(Arrays.asList(RON_FP, CEDRIC_FP)); Set actual = new HashSet<>(); Iterator fingerprints = directory.fingerprints(); actual.add(fingerprints.next()); actual.add(fingerprints.next()); assertFalse(fingerprints.hasNext()); assertEquals(expected, actual); } @ParameterizedTest @MethodSource("provideTestSubjects") public void testGetTrustRootIfChanged(PGPCertificateDirectory directory) throws BadDataException, IOException, InterruptedException { KeyMaterial trustRootMaterial = directory.insertTrustRoot( TestKeys.getHarryKey(), merger); assertNotNull(trustRootMaterial.getTag()); Long tag = trustRootMaterial.getTag(); assertNull(directory.getTrustRootCertificateIfChanged(tag)); assertNotNull(directory.getTrustRootCertificateIfChanged(tag + 1)); Long oldTag = tag; Thread.sleep(10); // "update" key trustRootMaterial = directory.insertTrustRoot( TestKeys.getHarryKey(), merger); tag = trustRootMaterial.getTag(); assertNotEquals(oldTag, tag); assertNotNull(directory.getTrustRootCertificateIfChanged(oldTag)); } @ParameterizedTest @MethodSource("provideTestSubjects") public void testGetBySpecialNameIfChanged(PGPCertificateDirectory directory) throws BadDataException, IOException, InterruptedException, BadNameException { KeyMaterial specialName = directory.insertWithSpecialName(SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger); assertNotNull(specialName.getTag()); Long tag = specialName.getTag(); assertNull(directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag)); assertNotNull(directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag + 1)); } @ParameterizedTest @MethodSource("provideTestSubjects") public void testGetByFingerprintIfChanged(PGPCertificateDirectory directory) throws BadDataException, IOException, InterruptedException, BadNameException { Certificate certificate = directory.insert(TestKeys.getCedricCert(), merger); Long tag = certificate.getTag(); assertNotNull(tag); assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag)); assertNotNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag + 1)); } @Test public void testFileBasedCertificateDirectoryTagChangesWhenFileChanges() throws IOException, NotAStoreException, BadDataException, InterruptedException, BadNameException { File tempDir = Files.createTempDirectory("file-based-changes").toFile(); tempDir.deleteOnExit(); PGPCertificateDirectory directory = PGPCertificateDirectories.fileBasedCertificateDirectory( new TestKeyMaterialReaderBackend(), tempDir, new InMemorySubkeyLookup()); FileBasedCertificateDirectoryBackend.FilenameResolver resolver = new FileBasedCertificateDirectoryBackend.FilenameResolver(tempDir); // Insert certificate Certificate certificate = directory.insert(TestKeys.getCedricCert(), merger); Long tag = certificate.getTag(); assertNotNull(tag); assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag)); Long oldTag = tag; Thread.sleep(10); // Change the file on disk directly, this invalidates the tag due to changed modification date File certFile = resolver.getCertFileByFingerprint(certificate.getFingerprint()); FileOutputStream fileOut = new FileOutputStream(certFile); Streams.pipeAll(certificate.getInputStream(), fileOut); fileOut.write("\n".getBytes()); fileOut.close(); // Old invalidated tag indicates a change, so the modified certificate is returned certificate = directory.getByFingerprintIfChanged(certificate.getFingerprint(), oldTag); assertNotNull(certificate); // new tag is valid tag = certificate.getTag(); assertNotEquals(oldTag, tag); assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag)); } @Test public void fileBasedStoreInWriteProtectedAreaThrows() { File root = new File("/"); assumeTrue(root.exists(), "This test only runs on unix-like systems"); File baseDirectory = new File(root, "pgp.cert.d"); assumeFalse(baseDirectory.mkdirs(), "This test assumes that we cannot create dirs in /"); KeyMaterialReaderBackend reader = new TestKeyMaterialReaderBackend(); SubkeyLookup lookup = new InMemorySubkeyLookup(); assertThrows(NotAStoreException.class, () -> PGPCertificateDirectories.fileBasedCertificateDirectory( reader, baseDirectory, lookup)); } @Test public void fileBasedStoreOnFileThrows() throws IOException { File tempDir = Files.createTempDirectory("containsAFile").toFile(); tempDir.deleteOnExit(); File baseDir = new File(tempDir, "pgp.cert.d"); baseDir.createNewFile(); // this is a file, not a dir KeyMaterialReaderBackend reader = new TestKeyMaterialReaderBackend(); SubkeyLookup lookup = new InMemorySubkeyLookup(); assertThrows(NotAStoreException.class, () -> PGPCertificateDirectories.fileBasedCertificateDirectory( reader, baseDir, lookup)); } }