Compare commits

...

6 commits

8 changed files with 245 additions and 47 deletions

View file

@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0
# Cert-D-Java Changelog # Cert-D-Java Changelog
## 0.2.1
- Throw `NoSuchElementException` when querying non-existent certificates
## 0.2.0 ## 0.2.0
- `pgp-certificate-store`: - `pgp-certificate-store`:
- Rework `Certificate`, `Key` to inherit from `KeyMaterial` - Rework `Certificate`, `Key` to inherit from `KeyMaterial`

View file

@ -11,7 +11,7 @@ SPDX-License-Identifier: Apache-2.0
This repository contains a number of modules defining OpenPGP certificate storage for Java and Android applications. This repository contains a number of modules defining OpenPGP certificate storage for Java and Android applications.
The module [pgp-certificate-store](pgp-certificate-store] defines generalized The module [pgp-certificate-store](pgp-certificate-store) defines generalized
interfaces for OpenPGP Certificate storage. interfaces for OpenPGP Certificate storage.
It can be used by applications and libraries such as It can be used by applications and libraries such as
[PGPainless](https://pgpainless.org/) for certificate management. [PGPainless](https://pgpainless.org/) for certificate management.

View file

@ -15,6 +15,8 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -46,13 +48,17 @@ public class PGPCertificateDirectory
if (!openPgpV4FingerprintPattern.matcher(fingerprint).matches()) { if (!openPgpV4FingerprintPattern.matcher(fingerprint).matches()) {
throw new BadNameException(); throw new BadNameException();
} }
return backend.readByFingerprint(fingerprint); Certificate certificate = backend.readByFingerprint(fingerprint);
if (certificate == null) {
throw new NoSuchElementException();
}
return certificate;
} }
@Override @Override
public Certificate getByFingerprintIfChanged(String fingerprint, long tag) public Certificate getByFingerprintIfChanged(String fingerprint, long tag)
throws IOException, BadNameException, BadDataException { throws IOException, BadNameException, BadDataException {
if (tag != backend.getTagForFingerprint(fingerprint)) { if (!Objects.equals(tag, backend.getTagForFingerprint(fingerprint))) {
return getByFingerprint(fingerprint); return getByFingerprint(fingerprint);
} }
return null; return null;
@ -66,13 +72,13 @@ public class PGPCertificateDirectory
if (keyMaterial != null) { if (keyMaterial != null) {
return keyMaterial.asCertificate(); return keyMaterial.asCertificate();
} }
return null; throw new NoSuchElementException();
} }
@Override @Override
public Certificate getBySpecialNameIfChanged(String specialName, long tag) public Certificate getBySpecialNameIfChanged(String specialName, long tag)
throws IOException, BadNameException, BadDataException { throws IOException, BadNameException, BadDataException {
if (tag != backend.getTagForSpecialName(specialName)) { if (!Objects.equals(tag, backend.getTagForSpecialName(specialName))) {
return getBySpecialName(specialName); return getBySpecialName(specialName);
} }
return null; return null;
@ -121,7 +127,11 @@ public class PGPCertificateDirectory
@Override @Override
public KeyMaterial getTrustRoot() throws IOException, BadDataException { public KeyMaterial getTrustRoot() throws IOException, BadDataException {
try { try {
return backend.readBySpecialName(SpecialNames.TRUST_ROOT); KeyMaterial keyMaterial = backend.readBySpecialName(SpecialNames.TRUST_ROOT);
if (keyMaterial == null) {
throw new NoSuchElementException();
}
return keyMaterial;
} catch (BadNameException e) { } catch (BadNameException e) {
throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is implementation MUST"); throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is implementation MUST");
} }

View file

@ -33,6 +33,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
@ -348,7 +349,7 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec
private Long getTag(File file) throws IOException { private Long getTag(File file) throws IOException {
if (!file.exists()) { if (!file.exists()) {
throw new IllegalArgumentException("File MUST exist."); throw new NoSuchElementException();
} }
Path path = file.toPath(); Path path = file.toPath();
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);

View file

@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.Test;
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.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.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
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.Assumptions.assumeFalse;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
public class FileBasedPGPCertificateDirectoryTest {
private static final KeyMaterialMerger merger = new TestKeyMaterialMerger();
@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));
}
@Test
public void testCertificateStoredUnderWrongFingerprintThrowsBadData()
throws IOException, NotAStoreException, BadDataException, InterruptedException, BadNameException {
File tempDir = Files.createTempDirectory("wrong-fingerprint").toFile();
tempDir.deleteOnExit();
PGPCertificateDirectory directory = PGPCertificateDirectories.fileBasedCertificateDirectory(
new TestKeyMaterialReaderBackend(),
tempDir,
new InMemorySubkeyLookup());
FileBasedCertificateDirectoryBackend.FilenameResolver resolver =
new FileBasedCertificateDirectoryBackend.FilenameResolver(tempDir);
// Insert Rons certificate
directory.insert(TestKeys.getRonCert(), merger);
// Copy Rons cert to Cedrics cert file
File ronCert = resolver.getCertFileByFingerprint(TestKeys.RON_FP);
FileInputStream inputStream = new FileInputStream(ronCert);
File cedricCert = resolver.getCertFileByFingerprint(TestKeys.CEDRIC_FP);
cedricCert.getParentFile().mkdirs();
cedricCert.createNewFile();
FileOutputStream outputStream = new FileOutputStream(cedricCert);
Streams.pipeAll(inputStream, outputStream);
inputStream.close();
outputStream.close();
// Reading cedrics cert will fail, as it has Rons fingerprint
assertThrows(BadDataException.class, () -> directory.getByFingerprint(TestKeys.CEDRIC_FP));
}
}

View file

@ -6,11 +6,9 @@ package pgp.cert_d;
import org.bouncycastle.util.io.Streams; import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import pgp.cert_d.backend.FileBasedCertificateDirectoryBackend;
import pgp.cert_d.dummy.TestKeyMaterialMerger; import pgp.cert_d.dummy.TestKeyMaterialMerger;
import pgp.cert_d.dummy.TestKeyMaterialReaderBackend; import pgp.cert_d.dummy.TestKeyMaterialReaderBackend;
import pgp.cert_d.subkey_lookup.InMemorySubkeyLookup; import pgp.cert_d.subkey_lookup.InMemorySubkeyLookup;
@ -24,13 +22,13 @@ import pgp.certificate_store.exception.NotAStoreException;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Set; import java.util.Set;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -68,6 +66,55 @@ public class PGPCertificateDirectoryTest {
Arguments.of(Named.of("FileBasedCertificateDirectory", fileBased))); 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 @ParameterizedTest
@MethodSource("provideTestSubjects") @MethodSource("provideTestSubjects")
public void lockDirectoryAndTryInsertWillFail(PGPCertificateDirectory directory) public void lockDirectoryAndTryInsertWillFail(PGPCertificateDirectory directory)
@ -130,7 +177,7 @@ public class PGPCertificateDirectoryTest {
@MethodSource("provideTestSubjects") @MethodSource("provideTestSubjects")
public void testInsertAndGetSingleCert(PGPCertificateDirectory directory) public void testInsertAndGetSingleCert(PGPCertificateDirectory directory)
throws BadDataException, IOException, InterruptedException, BadNameException { throws BadDataException, IOException, InterruptedException, BadNameException {
assertNull(directory.getByFingerprint(CEDRIC_FP), "Empty directory MUST NOT contain certificate"); assertThrows(NoSuchElementException.class, () -> directory.getByFingerprint(CEDRIC_FP), "Empty directory MUST NOT contain certificate");
Certificate certificate = directory.insert(TestKeys.getCedricCert(), merger); Certificate certificate = directory.insert(TestKeys.getCedricCert(), merger);
assertEquals(CEDRIC_FP, certificate.getFingerprint(), "Fingerprint of inserted cert MUST match"); assertEquals(CEDRIC_FP, certificate.getFingerprint(), "Fingerprint of inserted cert MUST match");
@ -148,7 +195,7 @@ public class PGPCertificateDirectoryTest {
@MethodSource("provideTestSubjects") @MethodSource("provideTestSubjects")
public void testInsertAndGetTrustRootAndCert(PGPCertificateDirectory directory) public void testInsertAndGetTrustRootAndCert(PGPCertificateDirectory directory)
throws BadDataException, IOException, InterruptedException { throws BadDataException, IOException, InterruptedException {
assertNull(directory.getTrustRoot()); assertThrows(NoSuchElementException.class, () -> directory.getTrustRoot());
KeyMaterial trustRootMaterial = directory.insertTrustRoot( KeyMaterial trustRootMaterial = directory.insertTrustRoot(
TestKeys.getHarryKey(), merger); TestKeys.getHarryKey(), merger);
@ -188,6 +235,7 @@ public class PGPCertificateDirectoryTest {
assertNotNull(directory.getTrustRootCertificateIfChanged(tag + 1)); assertNotNull(directory.getTrustRootCertificateIfChanged(tag + 1));
Long oldTag = tag; Long oldTag = tag;
Thread.sleep(10);
// "update" key // "update" key
trustRootMaterial = directory.insertTrustRoot( trustRootMaterial = directory.insertTrustRoot(
TestKeys.getHarryKey(), merger); TestKeys.getHarryKey(), merger);
@ -222,38 +270,42 @@ public class PGPCertificateDirectoryTest {
assertNotNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag + 1)); assertNotNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag + 1));
} }
@Test @ParameterizedTest
public void testFileBasedCertificateDirectoryTagChangesWhenFileChanges() throws IOException, NotAStoreException, BadDataException, InterruptedException, BadNameException { @MethodSource("provideTestSubjects")
File tempDir = Files.createTempDirectory("file-based-changes").toFile(); public void testOverwriteTrustRoot(PGPCertificateDirectory directory)
tempDir.deleteOnExit(); throws BadDataException, IOException, InterruptedException {
PGPCertificateDirectory directory = PGPCertificateDirectories.fileBasedCertificateDirectory( directory.insertTrustRoot(TestKeys.getHarryKey(), merger);
new TestKeyMaterialReaderBackend(), assertEquals(HARRY_FP, directory.getTrustRoot().getFingerprint());
tempDir, assertTrue(directory.getTrustRoot() instanceof Key);
new InMemorySubkeyLookup());
FileBasedCertificateDirectoryBackend.FilenameResolver resolver =
new FileBasedCertificateDirectoryBackend.FilenameResolver(tempDir);
// Insert certificate directory.insertTrustRoot(TestKeys.getCedricCert(), merger);
Certificate certificate = directory.insert(TestKeys.getCedricCert(), merger); assertEquals(CEDRIC_FP, directory.getTrustRoot().getFingerprint());
Long tag = certificate.getTag(); assertTrue(directory.getTrustRoot() instanceof Certificate);
assertNotNull(tag);
assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag));
Long oldTag = tag;
// 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.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));
} }
@ParameterizedTest
@MethodSource("provideTestSubjects")
public void testOverwriteSpecialName(PGPCertificateDirectory directory)
throws BadDataException, IOException, InterruptedException, BadNameException {
directory.insertWithSpecialName(SpecialNames.TRUST_ROOT, TestKeys.getRonCert(), merger);
KeyMaterial bySpecialName = directory.getBySpecialName(SpecialNames.TRUST_ROOT);
assertEquals(RON_FP, bySpecialName.getFingerprint());
directory.insertWithSpecialName(SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger);
assertEquals(HARRY_FP, directory.getBySpecialName(SpecialNames.TRUST_ROOT).getFingerprint());
}
@ParameterizedTest
@MethodSource("provideTestSubjects")
public void testOverwriteByFingerprint(PGPCertificateDirectory directory)
throws BadDataException, IOException, InterruptedException, BadNameException {
directory.insert(TestKeys.getRonCert(), merger);
Certificate extracted = directory.getByFingerprint(RON_FP);
assertEquals(RON_FP, extracted.getFingerprint());
directory.insert(TestKeys.getRonCert(), merger);
extracted = directory.getByFingerprint(RON_FP);
assertEquals(RON_FP, extracted.getFingerprint());
}
} }

View file

@ -17,6 +17,7 @@ import pgp.certificate_store.exception.BadNameException;
import java.io.IOException; import java.io.IOException;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
@ -52,7 +53,7 @@ public class PGPCertificateStoreAdapterTest {
@Test @Test
public void testInsertGetCertificate() public void testInsertGetCertificate()
throws BadDataException, IOException, InterruptedException, BadNameException { throws BadDataException, IOException, InterruptedException, BadNameException {
assertNull(adapter.getCertificate(TestKeys.CEDRIC_FP)); assertThrows(NoSuchElementException.class, () -> adapter.getCertificate(TestKeys.CEDRIC_FP));
assertFalse(adapter.getCertificates().hasNext()); assertFalse(adapter.getCertificates().hasNext());
Certificate certificate = adapter.insertCertificate(TestKeys.getCedricCert(), merger); Certificate certificate = adapter.insertCertificate(TestKeys.getCedricCert(), merger);
@ -70,7 +71,7 @@ public class PGPCertificateStoreAdapterTest {
@Test @Test
public void testInsertGetTrustRoot() public void testInsertGetTrustRoot()
throws BadDataException, BadNameException, IOException, InterruptedException { throws BadDataException, BadNameException, IOException, InterruptedException {
assertNull(adapter.getCertificate(SpecialNames.TRUST_ROOT)); assertThrows(NoSuchElementException.class, () -> adapter.getCertificate(SpecialNames.TRUST_ROOT));
Certificate certificate = adapter.insertCertificateBySpecialName( Certificate certificate = adapter.insertCertificateBySpecialName(
SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger); SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger);

View file

@ -4,7 +4,7 @@
allprojects { allprojects {
ext { ext {
shortVersion = '0.2.1' shortVersion = '0.2.2'
isSnapshot = true isSnapshot = true
minAndroidSdk = 26 minAndroidSdk = 26
animalsnifferSignatureVersion = "$minAndroidSdk:8.0.0_r2" animalsnifferSignatureVersion = "$minAndroidSdk:8.0.0_r2"