Compare commits

...

18 commits
0.2.0 ... main

Author SHA1 Message Date
Paul Schaub e4128a002a
Update changelog 2023-07-07 12:05:07 +02:00
Paul Schaub 9d20355a58
Cert-D-Java 0.2.3-SNAPSHOT 2023-07-07 12:04:56 +02:00
Paul Schaub 26666fa3e6
Cert-D-Java 0.2.2 2023-07-07 12:01:41 +02:00
Paul Schaub 8b14f76add
Fix version name 2023-07-07 12:00:38 +02:00
Paul Schaub 7d67029748
Bump sqlite-jdbc from 3.36.0.3 to 3.42.0.0 2023-07-07 12:00:25 +02:00
Paul Schaub fd43ef27ba
Bump Bouncy Castle to 1.75 2023-07-07 11:53:43 +02:00
Paul Schaub ac5097482a
Documentation is lying 2023-06-25 11:19:49 +02:00
Paul Schaub 8a3337f1f5
Update changelog 2022-11-25 16:06:58 +01:00
Paul Schaub aa521bac47 Bump Bouncycastle to 1.72 2022-11-25 16:05:41 +01:00
Paul Schaub 6ae12c1262 Fix test running inside of flatpaked intellij resolving custom XDG_DATA_HOME 2022-11-25 16:05:41 +01:00
Paul Schaub 0e8bf060f2
Update javadoc to reflect what happens when a cert is queried but not found 2022-08-27 13:33:52 +02:00
Paul Schaub 190194f932
Cert-D-Java 0.2.2-SNAPSHOT 2022-08-27 13:26:26 +02:00
Paul Schaub 08837f4999
Cert-D-Java 0.2.1 2022-08-27 13:24:27 +02:00
Paul Schaub b827739892
Fix link in readme 2022-08-27 13:20:58 +02:00
Paul Schaub ceaa1e0c80
More tests 2022-08-27 13:11:11 +02:00
Paul Schaub 24f4e2d771
Add test for creating stores in write-protected places 2022-08-27 12:36:07 +02:00
Paul Schaub a248e0d717
Throw NoSuchElementException for non-existent certificates/keys
Fixes #2
2022-08-27 12:16:53 +02:00
Paul Schaub eab31b8c12
Cert-D-Java 0.2.1-SNAPSHOT 2022-08-25 12:02:01 +02:00
12 changed files with 269 additions and 60 deletions

View file

@ -5,6 +5,13 @@ SPDX-License-Identifier: CC0-1.0
# Cert-D-Java Changelog # Cert-D-Java Changelog
## 0.2.2
- Bump Bouncy Castle to `1.75`
- Bump `sqlite-jdbc` to `3.42.0.0`
## 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

@ -32,7 +32,7 @@ dependencies {
testImplementation project(":pgp-cert-d-java-jdbc-sqlite-lookup") testImplementation project(":pgp-cert-d-java-jdbc-sqlite-lookup")
testImplementation "org.bouncycastle:bcprov-jdk15to18:$bouncycastleVersion" testImplementation "org.bouncycastle:bcprov-jdk15to18:$bouncycastleVersion"
testImplementation "org.bouncycastle:bcpg-jdk15to18:$bouncycastleVersion" testImplementation "org.bouncycastle:bcpg-jdk15to18:$bouncyPgVersion"
} }
animalsniffer { animalsniffer {

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

@ -10,6 +10,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.NoSuchElementException;
/** /**
* Interface for a read-only OpenPGP certificate directory. * Interface for a read-only OpenPGP certificate directory.
@ -19,12 +20,12 @@ public interface ReadOnlyPGPCertificateDirectory {
/** /**
* Get the trust-root certificate. This is a certificate which is stored under the special name * Get the trust-root certificate. This is a certificate which is stored under the special name
* <pre>trust-root</pre>. * <pre>trust-root</pre>.
* If no such certificate is found, <pre>null</pre> is returned.
* *
* @return trust-root certificate * @return trust-root certificate
* *
* @throws IOException in case of an IO error * @throws IOException in case of an IO error
* @throws BadDataException if the certificate contains bad data * @throws BadDataException if the certificate contains bad data
* @throws NoSuchElementException if no such certificate is found
*/ */
Certificate getTrustRootCertificate() Certificate getTrustRootCertificate()
throws IOException, BadDataException; throws IOException, BadDataException;
@ -36,24 +37,25 @@ public interface ReadOnlyPGPCertificateDirectory {
* Otherwise. the changed certificate is returned. * Otherwise. the changed certificate is returned.
* *
* @param tag tag * @param tag tag
* @return changed certificate, or null if the certificate is unchanged or not found. * @return changed certificate, or null if the certificate is unchanged.
* *
* @throws IOException in case of an IO error * @throws IOException in case of an IO error
* @throws BadDataException if the certificate contains bad data * @throws BadDataException if the certificate contains bad data
* @throws NoSuchElementException if no such certificate is found
*/ */
Certificate getTrustRootCertificateIfChanged(long tag) Certificate getTrustRootCertificateIfChanged(long tag)
throws IOException, BadDataException; throws IOException, BadDataException;
/** /**
* Get the certificate identified by the given fingerprint. * Get the certificate identified by the given fingerprint.
* If no such certificate is found, return <pre>null</pre>.
* *
* @param fingerprint lower-case fingerprint of the certificate * @param fingerprint lower-case fingerprint of the certificate
* @return certificate or null if no such certificate has been found * @return certificate
* *
* @throws IOException in case of an IO error * @throws IOException in case of an IO error
* @throws BadNameException if the fingerprint is malformed * @throws BadNameException if the fingerprint is malformed
* @throws BadDataException if the certificate contains bad data * @throws BadDataException if the certificate contains bad data
* @throws NoSuchElementException if no such certificate is found
*/ */
Certificate getByFingerprint(String fingerprint) Certificate getByFingerprint(String fingerprint)
throws IOException, BadNameException, BadDataException; throws IOException, BadNameException, BadDataException;
@ -66,25 +68,26 @@ public interface ReadOnlyPGPCertificateDirectory {
* *
* @param fingerprint lower-case fingerprint of the certificate * @param fingerprint lower-case fingerprint of the certificate
* @param tag tag * @param tag tag
* @return certificate or null if the certificate has not been changed or has not been found * @return certificate or null if the certificate has not been changed
* *
* @throws IOException in case of an IO error * @throws IOException in case of an IO error
* @throws BadNameException if the fingerprint is malformed * @throws BadNameException if the fingerprint is malformed
* @throws BadDataException if the certificate contains bad data * @throws BadDataException if the certificate contains bad data
* @throws NoSuchElementException if no such certificate is found
*/ */
Certificate getByFingerprintIfChanged(String fingerprint, long tag) Certificate getByFingerprintIfChanged(String fingerprint, long tag)
throws IOException, BadNameException, BadDataException; throws IOException, BadNameException, BadDataException;
/** /**
* Get the certificate identified by the given special name. * Get the certificate identified by the given special name.
* If no such certificate is found, <pre>null</pre> is returned.
* *
* @param specialName special name * @param specialName special name
* @return certificate or null * @return certificate
* *
* @throws IOException in case of an IO error * @throws IOException in case of an IO error
* @throws BadNameException if the special name is not known * @throws BadNameException if the special name is not known
* @throws BadDataException if the certificate contains bad data * @throws BadDataException if the certificate contains bad data
* @throws NoSuchElementException if no such certificate is found
*/ */
Certificate getBySpecialName(String specialName) Certificate getBySpecialName(String specialName)
throws IOException, BadNameException, BadDataException; throws IOException, BadNameException, BadDataException;
@ -102,6 +105,7 @@ public interface ReadOnlyPGPCertificateDirectory {
* @throws IOException in case of an IO error * @throws IOException in case of an IO error
* @throws BadNameException if the special name is not known * @throws BadNameException if the special name is not known
* @throws BadDataException if the certificate contains bad data * @throws BadDataException if the certificate contains bad data
* @throws NoSuchElementException if no such certificate is found
*/ */
Certificate getBySpecialNameIfChanged(String specialName, long tag) Certificate getBySpecialNameIfChanged(String specialName, long tag)
throws IOException, BadNameException, BadDataException; throws IOException, BadNameException, BadDataException;

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

@ -18,7 +18,7 @@ public class BaseDirectoryProviderTest {
public void testGetDefaultBaseDir_Linux() { public void testGetDefaultBaseDir_Linux() {
assumeTrue(System.getProperty("os.name").equalsIgnoreCase("linux")); assumeTrue(System.getProperty("os.name").equalsIgnoreCase("linux"));
File baseDir = BaseDirectoryProvider.getDefaultBaseDirForOS("linux"); File baseDir = BaseDirectoryProvider.getDefaultBaseDirForOS("linux");
assertTrue(baseDir.getAbsolutePath().endsWith("/.local/share/pgp.cert.d")); assertTrue(baseDir.getAbsolutePath().endsWith("pgp.cert.d"));
} }
@Test @Test

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

@ -12,6 +12,7 @@ import pgp.certificate_store.exception.BadNameException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Iterator; import java.util.Iterator;
import java.util.NoSuchElementException;
/** /**
* Interface for an OpenPGP certificate (public key) store. * Interface for an OpenPGP certificate (public key) store.
@ -20,7 +21,6 @@ public interface PGPCertificateStore {
/** /**
* Return the certificate that matches the given identifier. * Return the certificate that matches the given identifier.
* If no matching certificate can be found, return null.
* *
* @param identifier identifier for a certificate. * @param identifier identifier for a certificate.
* @return certificate or null * @return certificate or null
@ -28,6 +28,7 @@ public interface PGPCertificateStore {
* @throws IOException in case of an IO-error * @throws IOException in case of an IO-error
* @throws BadNameException if the identifier is invalid * @throws BadNameException if the identifier is invalid
* @throws BadDataException if the certificate file contains invalid data * @throws BadDataException if the certificate file contains invalid data
* @throws NoSuchElementException if no such certificate is found
*/ */
Certificate getCertificate(String identifier) Certificate getCertificate(String identifier)
throws IOException, BadNameException, BadDataException; throws IOException, BadNameException, BadDataException;
@ -45,6 +46,7 @@ public interface PGPCertificateStore {
* @throws IOException in case of an IO-error * @throws IOException in case of an IO-error
* @throws BadNameException if the identifier is invalid * @throws BadNameException if the identifier is invalid
* @throws BadDataException if the certificate file contains invalid data * @throws BadDataException if the certificate file contains invalid data
* @throws NoSuchElementException if no such certificate is found
*/ */
Certificate getCertificateIfChanged(String identifier, Long tag) Certificate getCertificateIfChanged(String identifier, Long tag)
throws IOException, BadNameException, BadDataException; throws IOException, BadNameException, BadDataException;

View file

@ -4,15 +4,16 @@
allprojects { allprojects {
ext { ext {
shortVersion = '0.2.0' shortVersion = '0.2.3'
isSnapshot = false isSnapshot = true
minAndroidSdk = 26 minAndroidSdk = 26
animalsnifferSignatureVersion = "$minAndroidSdk:8.0.0_r2" animalsnifferSignatureVersion = "$minAndroidSdk:8.0.0_r2"
javaSourceCompatibility = 1.8 javaSourceCompatibility = 1.8
bouncycastleVersion = '1.71' bouncycastleVersion = '1.75'
bouncyPgVersion = "$bouncycastleVersion"
slf4jVersion = '1.7.36' slf4jVersion = '1.7.36'
logbackVersion = '1.2.11' logbackVersion = '1.2.11'
junitVersion = '5.8.2' junitVersion = '5.8.2'
sqliteJdbcVersion = '3.36.0.3' sqliteJdbcVersion = '3.42.0.0'
} }
} }