Compare commits

..

64 commits
0.1.0 ... main

Author SHA1 Message Date
e4128a002a
Update changelog 2023-07-07 12:05:07 +02:00
9d20355a58
Cert-D-Java 0.2.3-SNAPSHOT 2023-07-07 12:04:56 +02:00
26666fa3e6
Cert-D-Java 0.2.2 2023-07-07 12:01:41 +02:00
8b14f76add
Fix version name 2023-07-07 12:00:38 +02:00
7d67029748
Bump sqlite-jdbc from 3.36.0.3 to 3.42.0.0 2023-07-07 12:00:25 +02:00
fd43ef27ba
Bump Bouncy Castle to 1.75 2023-07-07 11:53:43 +02:00
ac5097482a
Documentation is lying 2023-06-25 11:19:49 +02:00
8a3337f1f5
Update changelog 2022-11-25 16:06:58 +01:00
aa521bac47 Bump Bouncycastle to 1.72 2022-11-25 16:05:41 +01:00
6ae12c1262 Fix test running inside of flatpaked intellij resolving custom XDG_DATA_HOME 2022-11-25 16:05:41 +01:00
0e8bf060f2
Update javadoc to reflect what happens when a cert is queried but not found 2022-08-27 13:33:52 +02:00
190194f932
Cert-D-Java 0.2.2-SNAPSHOT 2022-08-27 13:26:26 +02:00
08837f4999
Cert-D-Java 0.2.1 2022-08-27 13:24:27 +02:00
b827739892
Fix link in readme 2022-08-27 13:20:58 +02:00
ceaa1e0c80
More tests 2022-08-27 13:11:11 +02:00
24f4e2d771
Add test for creating stores in write-protected places 2022-08-27 12:36:07 +02:00
a248e0d717
Throw NoSuchElementException for non-existent certificates/keys
Fixes #2
2022-08-27 12:16:53 +02:00
eab31b8c12
Cert-D-Java 0.2.1-SNAPSHOT 2022-08-25 12:02:01 +02:00
65a283c80b
Cert-D-Java 0.2.0 2022-08-25 12:00:14 +02:00
682b3d58fa
Update changelog 2022-08-25 11:55:15 +02:00
142d1f6b16
Add support for v5 fingerprints 2022-08-25 11:41:52 +02:00
da39d41df3
Remove unused method 2022-08-25 11:41:41 +02:00
3ec7b082be
Prevent NPEs in file walker 2022-08-25 11:41:05 +02:00
2758c3efb8
Add javadoc 2022-08-25 11:40:22 +02:00
4ce9f46846
Increase coverage of PGPCertificateDirectory 2022-08-25 11:26:12 +02:00
f34c6d7735
Add tests for PGPCertificateStoreAdapter 2022-08-24 23:18:54 +02:00
dee2ea88a7
Fix animalsniffer signature 2022-08-24 15:00:47 +02:00
ecfae83f3a
Merge pull request #1 from pgpainless/rewrite
Rewrite logic to improve ability to set different backends
2022-08-24 14:56:17 +02:00
662f96ff77
Add javadoc for interfaces 2022-08-24 14:53:47 +02:00
a956aec5fc
Add documentation to BaseDirectoryProvider 2022-08-24 14:09:20 +02:00
f382189638
DatabaseSubkeyLookupFactory: Make database name configurable 2022-08-24 13:58:18 +02:00
27f4598437
Add back support for getXIfChanged(Y, tag) 2022-08-24 13:04:28 +02:00
d050cb5516
Execute tests on both inMemory and fileBased store backends 2022-08-23 15:36:26 +02:00
5e850581c0
Add some PGPCertificateDirectory tests 2022-08-23 15:19:01 +02:00
70367e98f0
Update changelog 2022-08-12 15:42:44 +02:00
991fea2503
Add SubkeyLookupFactory class 2022-08-12 15:08:33 +02:00
a3162f0cf9
Reintroduce pgp-certificate-store layer 2022-08-12 14:10:09 +02:00
f91c5065fc
Organize cert-d-java classes in packages 2022-08-11 21:50:02 +02:00
7cc0ef5037
Get rid of certificate-store abstraction 2022-08-09 18:00:27 +02:00
7c39781d15
Rewrite of PGPCertificateDirectory using more flexible backend 2022-08-09 17:50:15 +02:00
60779b921e
Add suppression for Java 8 Map API 2022-08-08 15:07:45 +02:00
942b287beb
Make Key and Certificate extend KeyMaterial,
get rid of CertificateReader
2022-08-08 13:50:59 +02:00
2b5da18fc6
Add reuse badge 2022-08-01 17:10:27 +02:00
0c416bd166
Add badges for CI and Coveralls 2022-08-01 17:07:17 +02:00
21fee6253e
Add woodpecker CI 2022-08-01 17:01:08 +02:00
2222d95702
Move TrustRootStore methods into CertificateDirectory interface 2022-07-04 20:10:55 +02:00
533d0be2d0
Fix javadoc issue 2022-07-04 19:45:01 +02:00
17d2f45e83
Implement storing of trust-root keys 2022-07-04 19:42:02 +02:00
304d6c29e4
Throw detailled error message when bad data is encountered 2022-07-04 19:41:02 +02:00
1b63a4ac9a
Throw BadDataException when reading certificate 2022-07-04 19:40:26 +02:00
0846528072
Prepare for integration of key storing into the cert store 2022-07-04 19:38:01 +02:00
bc20b95839
Make trust-root constant in SpecialNames 2022-07-04 18:48:49 +02:00
96b22ff40a
Rename MergeCallback to CertificateMerger 2022-07-04 18:48:19 +02:00
dd2877ccee
Cert-D-Java 0.1.2-SNAPSHOT 2022-04-29 16:36:01 +02:00
0b29624b43
Cert-D-Java 0.1.1 2022-04-29 16:34:15 +02:00
27d48824b3
Fix javadoc warnings 2022-04-29 16:31:49 +02:00
ce1469948f
Update changelog 2022-04-26 01:39:24 +02:00
0fee958740
Document build.gradle files 2022-04-26 01:38:13 +02:00
42ecab5aff
Move sqlite-jdbc version to version.gradle 2022-04-26 01:35:40 +02:00
ba3d8b0230
Bump logback version to 1.2.11 2022-04-26 01:34:12 +02:00
50ab9208e6
Bump slf4j to 1.7.36 2022-04-26 01:33:48 +02:00
540150cca9
Move dependency versions into version.gradle 2022-04-07 21:37:20 +02:00
9efcae77de
Add badges and info to readme files 2022-03-01 17:13:08 +01:00
5222c54536
Cert-D-Java 0.1.1-SNAPSHOT 2022-03-01 16:13:59 +01:00
63 changed files with 2800 additions and 1076 deletions

View file

@ -14,3 +14,7 @@ Files: gradle*
Copyright: 2015 the original author or authors. Copyright: 2015 the original author or authors.
License: Apache-2.0 License: Apache-2.0
# Woodpecker build files
Files: .woodpecker/*
Copyright: 2022 the original author or authors.
License: Apache-2.0

12
.woodpecker/.build.yml Normal file
View file

@ -0,0 +1,12 @@
pipeline:
run:
image: gradle:7.5-jdk8
commands:
- git checkout $CI_COMMIT_BRANCH
# Code works
- gradle test
# Code is clean
- gradle check javadocAll
# Code has coverage
- gradle jacocoRootReport coveralls
secrets: [COVERALLS_REPO_TOKEN]

7
.woodpecker/.reuse.yml Normal file
View file

@ -0,0 +1,7 @@
# Code is licensed properly
# See https://reuse.software/
pipeline:
reuse:
image: fsfe/reuse:latest
commands:
- reuse lint

View file

@ -5,5 +5,31 @@ SPDX-License-Identifier: CC0-1.0
# Cert-D-Java Changelog # Cert-D-Java Changelog
# 0.1.0 ## 0.2.2
- Initial Release - 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
- `pgp-certificate-store`:
- Rework `Certificate`, `Key` to inherit from `KeyMaterial`
- Rename `CertificateReaderBackend` to `KeyMaterialReaderBackend`
- Rename `CertificateMerger` to `KeyMaterialMerger`
- Rework `PGPCertificateStore` class
- `pgp-cert-d-java`:
- Increase minimum Android API level to 26
- Add `PGPCertificateDirectories` factory class
- Rework `PGPCertificateDirectory` class by separating out backend logic
- Split interface into `ReadOnlyPGPCertificateDirectory` and `WritingPGPCertificateDirectory`
- `FileBasedCertificateDirectoryBackend`: Calculate tag based on file attributes (inode)
- `pgp-cert-d-java-jdbc-sqlite-lookup`:
- Add `DatabaseSubkeyLookupFactory`
## 0.1.1
- Bump `slf4j` to `1.7.36`
- Bump `logback` to `1.2.11`
## 0.1.0
- Initial Release

View file

@ -5,10 +5,13 @@ SPDX-License-Identifier: Apache-2.0
--> -->
# Shared PGP Certificate Directory for Java # Shared PGP Certificate Directory for Java
[![status-badge](https://ci.codeberg.org/api/badges/PGPainless/cert-d-java/status.svg?branch=main)](https://ci.codeberg.org/PGPainless/cert-d-java)
[![Coverage Status](https://coveralls.io/repos/github/pgpainless/cert-d-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/cert-d-java?branch=main)
[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/cert-d-java)](https://api.reuse.software/info/github.com/pgpainless/cert-d-java)
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

@ -59,9 +59,6 @@ allprojects {
} }
project.ext { project.ext {
slf4jVersion = '1.7.32'
logbackVersion = '1.2.9'
junitVersion = '5.8.2'
rootConfigDir = new File(rootDir, 'config') rootConfigDir = new File(rootDir, 'config')
gitCommit = getGitCommit() gitCommit = getGitCommit()
isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI')) isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI'))

View file

@ -0,0 +1,12 @@
<!--
SPDX-FileCopyrightText: 2022 Paul Schaub <info@pgpainless.org>
SPDX-License-Identifier: Apache-2.0
-->
# SQLite backed Subkey-ID Lookup
[![javadoc](https://javadoc.io/badge2/org.pgpainless/pgp-cert-d-java-jdbc-sqlite-lookup/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgp-cert-d-java-jdbc-sqlite-lookup)
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgp-cert-d-java-jdbc-sqlite-lookup)](https://search.maven.org/artifact/org.pgpainless/pgp-cert-d-java-jdbc-sqlite-lookup)
Implementation of `pgp-certificate-store`'s `SubkeyLookup` class using an SQLite database accessed through `sqlite-jdbc`.

View file

@ -13,6 +13,7 @@ repositories {
} }
dependencies { dependencies {
// JUnit for testing
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
@ -20,8 +21,11 @@ dependencies {
// Logging // Logging
testImplementation "ch.qos.logback:logback-classic:$logbackVersion" testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
// pgp.cert.d cert store
implementation project(":pgp-cert-d-java") implementation project(":pgp-cert-d-java")
api 'org.xerial:sqlite-jdbc:3.36.0.3'
// SQLite
api "org.xerial:sqlite-jdbc:$sqliteJdbcVersion"
} }
test { test {

View file

@ -11,7 +11,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import pgp.certificate_store.SubkeyLookup; import pgp.cert_d.subkey_lookup.SubkeyLookup;
public class DatabaseSubkeyLookup implements SubkeyLookup { public class DatabaseSubkeyLookup implements SubkeyLookup {

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.jdbc.sqlite;
import pgp.cert_d.subkey_lookup.SubkeyLookup;
import pgp.cert_d.subkey_lookup.SubkeyLookupFactory;
import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
/**
* Implementation of {@link SubkeyLookupFactory} which creates a SQLite-based {@link DatabaseSubkeyLookup}.
*/
public class DatabaseSubkeyLookupFactory implements SubkeyLookupFactory {
private String databaseName;
public DatabaseSubkeyLookupFactory() {
this("_pgpainless_subkey_map.db");
}
public DatabaseSubkeyLookupFactory(String databaseName) {
this.databaseName = databaseName;
}
@Override
public SubkeyLookup createFileBasedInstance(File baseDirectory) {
File databaseFile = new File(baseDirectory, databaseName);
SubkeyLookupDao dao;
try {
if (!databaseFile.exists()) {
databaseFile.createNewFile();
}
dao = SqliteSubkeyLookupDaoImpl.forDatabaseFile(databaseFile);
} catch (SQLException | IOException e) {
throw new RuntimeException(e);
}
return new DatabaseSubkeyLookup(dao);
}
}

View file

@ -3,6 +3,6 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
/** /**
* Implementation of a {@link pgp.certificate_store.SubkeyLookup} mechanism using an SQLite Database. * Implementation of a {@link pgp.cert_d.subkey_lookup.SubkeyLookup} mechanism using an SQLite Database.
*/ */
package pgp.cert_d.jdbc.sqlite; package pgp.cert_d.jdbc.sqlite;

View file

@ -22,15 +22,15 @@ import org.junit.jupiter.api.Test;
public class SqliteSubkeyLookupTest { public class SqliteSubkeyLookupTest {
private File databaseFile; private File tempDir;
private DatabaseSubkeyLookup lookup; private DatabaseSubkeyLookup lookup;
@BeforeEach @BeforeEach
public void setupLookup() throws IOException, SQLException { public void setupLookup() throws IOException {
databaseFile = Files.createTempFile("pgp.cert.d-", "lookup.db").toFile(); tempDir = Files.createTempDirectory("pgp.cert.d").toFile();
databaseFile.createNewFile(); tempDir.deleteOnExit();
databaseFile.deleteOnExit(); lookup = (DatabaseSubkeyLookup) new DatabaseSubkeyLookupFactory()
lookup = new DatabaseSubkeyLookup(SqliteSubkeyLookupDaoImpl.forDatabaseFile(databaseFile)); .createFileBasedInstance(tempDir);
} }
@Test @Test
@ -55,7 +55,7 @@ public class SqliteSubkeyLookupTest {
assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), lookup.getCertificateFingerprintsForSubkeyId(1337)); assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), lookup.getCertificateFingerprintsForSubkeyId(1337));
// do the lookup using a second db instance on the same file // do the lookup using a second db instance on the same file
DatabaseSubkeyLookup secondInstance = new DatabaseSubkeyLookup(SqliteSubkeyLookupDaoImpl.forDatabaseFile(databaseFile)); DatabaseSubkeyLookup secondInstance = (DatabaseSubkeyLookup) new DatabaseSubkeyLookupFactory().createFileBasedInstance(tempDir);
assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), secondInstance.getCertificateFingerprintsForSubkeyId(1337)); assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), secondInstance.getCertificateFingerprintsForSubkeyId(1337));
} }

View file

@ -6,11 +6,18 @@ SPDX-License-Identifier: Apache-2.0
# Shared PGP Certificate Directory for Java # Shared PGP Certificate Directory for Java
[![javadoc](https://javadoc.io/badge2/org.pgpainless/pgp-cert-d-java/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgp-cert-d-java)
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgp-cert-d-java)](https://search.maven.org/artifact/org.pgpainless/pgp-cert-d-java)
Backend-agnostic implementation of the [Shared PGP Certificate Directory Specification](https://sequoia-pgp.gitlab.io/pgp-cert-d/). Backend-agnostic implementation of the [Shared PGP Certificate Directory Specification](https://sequoia-pgp.gitlab.io/pgp-cert-d/).
This module implements the non-OpenPGP parts of the spec, e.g. locating the directory, resolving certificate file paths, This module implements the non-OpenPGP parts of the spec, e.g. locating the directory, resolving certificate file paths,
locking the directory for writes etc. locking the directory for writes etc.
This library can be used on Android API level 26 and up.
To get a useful implementation, a backend implementation such as `pgpainless-cert-d` is required, which needs to provide To get a useful implementation, a backend implementation such as `pgpainless-cert-d` is required, which needs to provide
support for reading and merging certificates. support for reading and merging certificates.
`pgp-cert-d-java` can be used as an implementation of `pgp-certificate-store`. `pgp-cert-d-java` can be used as an implementation of `pgp-certificate-store` using the `PGPCertificateStoreAdapter` class.
Note: This is a library module. For a command line interface, see [pgpainless-cert-d-cli](https://github.com/pgpainless/cert-d-pgpainless/tree/main/pgpainless-cert-d-cli).

View file

@ -15,9 +15,10 @@ repositories {
apply plugin: 'ru.vyarus.animalsniffer' apply plugin: 'ru.vyarus.animalsniffer'
dependencies { dependencies {
// animal sniffer // animal sniffer for ensuring Android API compatibility
signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:2.3.3_r2@signature" signature "net.sf.androidscents.signature:android-api-level-${animalsnifferSignatureVersion}@signature"
// JUnit
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
@ -25,9 +26,13 @@ dependencies {
// Logging // Logging
testImplementation "ch.qos.logback:logback-classic:$logbackVersion" testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
api project(":pgp-certificate-store")
// SQL Subkey table
testImplementation project(":pgp-cert-d-java-jdbc-sqlite-lookup") testImplementation project(":pgp-cert-d-java-jdbc-sqlite-lookup")
api project(":pgp-certificate-store") testImplementation "org.bouncycastle:bcprov-jdk15to18:$bouncycastleVersion"
testImplementation "org.bouncycastle:bcpg-jdk15to18:$bouncyPgVersion"
} }
animalsniffer { animalsniffer {

View file

@ -1,16 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import pgp.certificate_store.CertificateReaderBackend;
import pgp.certificate_store.MergeCallback;
public abstract class BackendProvider {
public abstract CertificateReaderBackend provideCertificateReaderBackend();
public abstract MergeCallback provideDefaultMergeCallback();
}

View file

@ -6,6 +6,16 @@ package pgp.cert_d;
import java.io.File; import java.io.File;
/**
* Provider class that is responsible for resolving the pgp.cert.d base directory of the system.
* The result can be overwritten by setting the <pre>PGP_CERT_D</pre> environment variable.
* If this variable is not set, the system-specific default directory will be returned.
*
* On Windows systems, this is <pre>%APPDATA%\pgp.cert.d</pre>.
* On Linux systems it is either <pre>$XDG_DATA_HOME/pgp.cert.d</pre> or, if <pre>$XDG_DATA_HOME</pre> is not set,
* it is <pre>$HOME/.local/share/pgp.cert.d</pre>
* On Mac systems it is <pre>$HOME/Library/Application Support/pgp.cert.d</pre>.
*/
public class BaseDirectoryProvider { public class BaseDirectoryProvider {
public static File getDefaultBaseDir() { public static File getDefaultBaseDir() {

View file

@ -1,190 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.MergeCallback;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Caching wrapper for {@link SharedPGPCertificateDirectory} implementations.
*/
public class CachingSharedPGPCertificateDirectoryWrapper
implements SharedPGPCertificateDirectory {
private static final Map<String, String> tagMap = new HashMap<>();
private static final Map<String, Certificate> certificateMap = new HashMap<>();
private final SharedPGPCertificateDirectory underlyingCertificateDirectory;
public CachingSharedPGPCertificateDirectoryWrapper(SharedPGPCertificateDirectory wrapped) {
this.underlyingCertificateDirectory = wrapped;
}
/**
* Store the given certificate under the given identifier into the cache.
*
* @param identifier fingerprint or special name
* @param certificate certificate
*/
private void remember(String identifier, Certificate certificate) {
certificateMap.put(identifier, certificate);
try {
tagMap.put(identifier, certificate.getTag());
} catch (IOException e) {
tagMap.put(identifier, null);
}
}
/**
* Returns true, if the cached tag differs from the provided tag.
*
* @param identifier fingerprint or special name
* @param tag tag
* @return true if cached tag differs, false otherwise
*/
private boolean tagChanged(String identifier, String tag) {
String tack = tagMap.get(identifier);
return !tagEquals(tag, tack);
}
/**
* Return true, if tag and tack are equal, false otherwise.
* @param tag tag
* @param tack other tag
* @return true if equal
*/
private static boolean tagEquals(String tag, String tack) {
return (tag == null && tack == null)
|| tag != null && tag.equals(tack);
}
/**
* Clear the cache.
*/
public void invalidate() {
certificateMap.clear();
tagMap.clear();
}
@Override
public LockingMechanism getLock() {
return underlyingCertificateDirectory.getLock();
}
@Override
public Certificate getByFingerprint(String fingerprint)
throws IOException, BadNameException, BadDataException {
Certificate certificate = certificateMap.get(fingerprint);
if (certificate == null) {
certificate = underlyingCertificateDirectory.getByFingerprint(fingerprint);
if (certificate != null) {
remember(fingerprint, certificate);
}
}
return certificate;
}
@Override
public Certificate getBySpecialName(String specialName)
throws IOException, BadNameException, BadDataException {
Certificate certificate = certificateMap.get(specialName);
if (certificate == null) {
certificate = underlyingCertificateDirectory.getBySpecialName(specialName);
if (certificate != null) {
remember(specialName, certificate);
}
}
return certificate;
}
@Override
public Certificate getByFingerprintIfChanged(String fingerprint, String tag)
throws IOException, BadNameException, BadDataException {
if (tagChanged(fingerprint, tag)) {
return getByFingerprint(fingerprint);
}
return null;
}
@Override
public Certificate getBySpecialNameIfChanged(String specialName, String tag)
throws IOException, BadNameException, BadDataException {
if (tagChanged(specialName, tag)) {
return getBySpecialName(specialName);
}
return null;
}
@Override
public Certificate insert(InputStream data, MergeCallback merge)
throws IOException, BadDataException, InterruptedException {
Certificate certificate = underlyingCertificateDirectory.insert(data, merge);
remember(certificate.getFingerprint(), certificate);
return certificate;
}
@Override
public Certificate tryInsert(InputStream data, MergeCallback merge)
throws IOException, BadDataException {
Certificate certificate = underlyingCertificateDirectory.tryInsert(data, merge);
if (certificate != null) {
remember(certificate.getFingerprint(), certificate);
}
return certificate;
}
@Override
public Certificate insertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
throws IOException, BadDataException, BadNameException, InterruptedException {
Certificate certificate = underlyingCertificateDirectory.insertWithSpecialName(specialName, data, merge);
remember(specialName, certificate);
return certificate;
}
@Override
public Certificate tryInsertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
throws IOException, BadDataException, BadNameException {
Certificate certificate = underlyingCertificateDirectory.tryInsertWithSpecialName(specialName, data, merge);
if (certificate != null) {
remember(specialName, certificate);
}
return certificate;
}
@Override
public Iterator<Certificate> items() {
Iterator<Certificate> iterator = underlyingCertificateDirectory.items();
return new Iterator<Certificate>() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public Certificate next() {
Certificate certificate = iterator.next();
remember(certificate.getFingerprint(), certificate);
return certificate;
}
};
}
@Override
public Iterator<String> fingerprints() {
return underlyingCertificateDirectory.fingerprints();
}
}

View file

@ -1,96 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
public class FileLockingMechanism implements LockingMechanism {
private final File lockFile;
private RandomAccessFile randomAccessFile;
private FileLock fileLock;
public FileLockingMechanism(File lockFile) {
this.lockFile = lockFile;
}
public static FileLockingMechanism defaultDirectoryFileLock(File baseDirectory) {
return new FileLockingMechanism(new File(baseDirectory, "writelock"));
}
@Override
public synchronized void lockDirectory() throws IOException, InterruptedException {
if (randomAccessFile != null) {
// we own the lock already. Let's wait...
this.wait();
}
try {
randomAccessFile = new RandomAccessFile(lockFile, "rw");
} catch (FileNotFoundException e) {
lockFile.createNewFile();
randomAccessFile = new RandomAccessFile(lockFile, "rw");
}
fileLock = randomAccessFile.getChannel().lock();
}
@Override
public synchronized boolean tryLockDirectory() throws IOException {
if (randomAccessFile != null) {
// We already locked the directory for another write operation.
// We fail, since we have not yet released the lock from the other operation.
return false;
}
try {
randomAccessFile = new RandomAccessFile(lockFile, "rw");
} catch (FileNotFoundException e) {
lockFile.createNewFile();
randomAccessFile = new RandomAccessFile(lockFile, "rw");
}
try {
fileLock = randomAccessFile.getChannel().tryLock();
if (fileLock == null) {
// try-lock failed, file is locked by another process.
randomAccessFile.close();
randomAccessFile = null;
return false;
}
} catch (OverlappingFileLockException e) {
// Some other object is holding the lock.
randomAccessFile.close();
randomAccessFile = null;
return false;
}
return true;
}
@Override
public synchronized void releaseDirectory() throws IOException {
// unlock file
if (fileLock != null) {
fileLock.release();
fileLock = null;
}
// close file
if (randomAccessFile != null) {
randomAccessFile.close();
randomAccessFile = null;
}
// delete file
if (lockFile.exists()) {
lockFile.delete();
}
// notify waiters
this.notify();
}
}

View file

@ -1,60 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import pgp.certificate_store.exception.BadNameException;
import java.io.File;
import java.util.regex.Pattern;
public class FilenameResolver {
private final File baseDirectory;
private final Pattern openPgpV4FingerprintPattern = Pattern.compile("^[a-f0-9]{40}$");
public FilenameResolver(File baseDirectory) {
this.baseDirectory = baseDirectory;
}
public File getBaseDirectory() {
return baseDirectory;
}
/**
* Calculate the file location for the certificate addressed by the given
* lowercase hexadecimal OpenPGP fingerprint.
*
* @param fingerprint fingerprint
* @return absolute certificate file location
* @throws BadNameException
*/
public File getCertFileByFingerprint(String fingerprint) throws BadNameException {
if (!isFingerprint(fingerprint)) {
throw new BadNameException();
}
// is fingerprint
File subdirectory = new File(getBaseDirectory(), fingerprint.substring(0, 2));
File file = new File(subdirectory, fingerprint.substring(2));
return file;
}
public File getCertFileBySpecialName(String specialName) throws BadNameException {
if (!isSpecialName(specialName)) {
throw new BadNameException();
}
return new File(getBaseDirectory(), specialName);
}
private boolean isFingerprint(String fingerprint) {
return openPgpV4FingerprintPattern.matcher(fingerprint).matches();
}
private boolean isSpecialName(String specialName) {
return SpecialNames.lookupSpecialName(specialName) != null;
}
}

View file

@ -1,30 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.io.IOException;
public interface LockingMechanism {
/**
* Lock the store for writes.
* Readers can continue to use the store and will always see consistent certs.
*/
void lockDirectory() throws IOException, InterruptedException;
/**
* Try top lock the store for writes.
* Return false without locking the store in case the store was already locked.
*
* @return true if locking succeeded, false otherwise
*/
boolean tryLockDirectory() throws IOException;
/**
* Release the directory write-lock acquired via {@link #lockDirectory()}.
*/
void releaseDirectory() throws IOException;
}

View file

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import pgp.cert_d.backend.FileBasedCertificateDirectoryBackend;
import pgp.cert_d.backend.InMemoryCertificateDirectoryBackend;
import pgp.cert_d.subkey_lookup.InMemorySubkeyLookup;
import pgp.cert_d.subkey_lookup.SubkeyLookup;
import pgp.certificate_store.certificate.KeyMaterialReaderBackend;
import pgp.certificate_store.exception.NotAStoreException;
import java.io.File;
/**
* Static factory methods that return implementations of the {@link PGPCertificateDirectory} class.
*/
public final class PGPCertificateDirectories {
private PGPCertificateDirectories() {
}
public static PGPCertificateDirectory inMemoryCertificateDirectory(
KeyMaterialReaderBackend keyReader) {
return new PGPCertificateDirectory(
new InMemoryCertificateDirectoryBackend(keyReader), new InMemorySubkeyLookup());
}
public static PGPCertificateDirectory defaultFileBasedCertificateDirectory(
KeyMaterialReaderBackend keyReader,
SubkeyLookup subkeyLookup)
throws NotAStoreException {
return fileBasedCertificateDirectory(keyReader, BaseDirectoryProvider.getDefaultBaseDir(), subkeyLookup);
}
public static PGPCertificateDirectory fileBasedCertificateDirectory(
KeyMaterialReaderBackend keyReader,
File baseDirectory,
SubkeyLookup subkeyLookup)
throws NotAStoreException {
return new PGPCertificateDirectory(
new FileBasedCertificateDirectoryBackend(baseDirectory, keyReader), subkeyLookup);
}
}

View file

@ -0,0 +1,367 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import pgp.cert_d.subkey_lookup.SubkeyLookup;
import pgp.certificate_store.certificate.Certificate;
import pgp.certificate_store.certificate.KeyMaterial;
import pgp.certificate_store.certificate.KeyMaterialMerger;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Implementation of the Shared PGP Certificate Directory.
*
* @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/">Shared PGP Certificate Directory Specification</a>
*/
public class PGPCertificateDirectory
implements ReadOnlyPGPCertificateDirectory, WritingPGPCertificateDirectory, SubkeyLookup {
final Backend backend;
final SubkeyLookup subkeyLookup;
private final Pattern openPgpV4FingerprintPattern = Pattern.compile("^[a-f0-9]{40}$");
/**
* Constructor for a PGP certificate directory.
*
* @param backend storage backend
* @param subkeyLookup subkey lookup mechanism to map subkey-ids to certificates
*/
public PGPCertificateDirectory(Backend backend, SubkeyLookup subkeyLookup) {
this.backend = backend;
this.subkeyLookup = subkeyLookup;
}
@Override
public Certificate getByFingerprint(String fingerprint) throws BadDataException, BadNameException, IOException {
if (!openPgpV4FingerprintPattern.matcher(fingerprint).matches()) {
throw new BadNameException();
}
Certificate certificate = backend.readByFingerprint(fingerprint);
if (certificate == null) {
throw new NoSuchElementException();
}
return certificate;
}
@Override
public Certificate getByFingerprintIfChanged(String fingerprint, long tag)
throws IOException, BadNameException, BadDataException {
if (!Objects.equals(tag, backend.getTagForFingerprint(fingerprint))) {
return getByFingerprint(fingerprint);
}
return null;
}
@Override
public Certificate getBySpecialName(String specialName)
throws BadNameException, BadDataException, IOException {
KeyMaterial keyMaterial = backend.readBySpecialName(specialName);
if (keyMaterial != null) {
return keyMaterial.asCertificate();
}
throw new NoSuchElementException();
}
@Override
public Certificate getBySpecialNameIfChanged(String specialName, long tag)
throws IOException, BadNameException, BadDataException {
if (!Objects.equals(tag, backend.getTagForSpecialName(specialName))) {
return getBySpecialName(specialName);
}
return null;
}
@Override
public Certificate getTrustRootCertificate()
throws IOException, BadDataException {
try {
return getBySpecialName(SpecialNames.TRUST_ROOT);
} catch (BadNameException e) {
throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is an implementation MUST");
}
}
@Override
public Certificate getTrustRootCertificateIfChanged(long tag) throws IOException, BadDataException {
try {
return getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag);
} catch (BadNameException e) {
throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is an implementation MUST");
}
}
@Override
public Iterator<Certificate> items() {
return backend.readItems();
}
@Override
public Iterator<String> fingerprints() {
Iterator<Certificate> certs = items();
return new Iterator<String>() {
@Override
public boolean hasNext() {
return certs.hasNext();
}
@Override
public String next() {
return certs.next().getFingerprint();
}
};
}
@Override
public KeyMaterial getTrustRoot() throws IOException, BadDataException {
try {
KeyMaterial keyMaterial = backend.readBySpecialName(SpecialNames.TRUST_ROOT);
if (keyMaterial == null) {
throw new NoSuchElementException();
}
return keyMaterial;
} catch (BadNameException e) {
throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is implementation MUST");
}
}
@Override
public KeyMaterial insertTrustRoot(InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException, InterruptedException {
backend.getLock().lockDirectory();
KeyMaterial inserted = backend.doInsertTrustRoot(data, merge);
subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
backend.getLock().releaseDirectory();
return inserted;
}
@Override
public KeyMaterial tryInsertTrustRoot(InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException {
if (!backend.getLock().tryLockDirectory()) {
return null;
}
KeyMaterial inserted = backend.doInsertTrustRoot(data, merge);
subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
backend.getLock().releaseDirectory();
return inserted;
}
@Override
public Certificate insert(InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException, InterruptedException {
backend.getLock().lockDirectory();
Certificate inserted = backend.doInsert(data, merge);
subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
backend.getLock().releaseDirectory();
return inserted;
}
@Override
public Certificate tryInsert(InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException {
if (!backend.getLock().tryLockDirectory()) {
return null;
}
Certificate inserted = backend.doInsert(data, merge);
subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
backend.getLock().releaseDirectory();
return inserted;
}
@Override
public Certificate insertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException, BadNameException, InterruptedException {
backend.getLock().lockDirectory();
Certificate inserted = backend.doInsertWithSpecialName(specialName, data, merge);
subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
backend.getLock().releaseDirectory();
return inserted;
}
@Override
public Certificate tryInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException, BadNameException {
if (!backend.getLock().tryLockDirectory()) {
return null;
}
Certificate inserted = backend.doInsertWithSpecialName(specialName, data, merge);
subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
backend.getLock().releaseDirectory();
return inserted;
}
@Override
public Set<String> getCertificateFingerprintsForSubkeyId(long subkeyId) throws IOException {
return subkeyLookup.getCertificateFingerprintsForSubkeyId(subkeyId);
}
@Override
public void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) throws IOException {
subkeyLookup.storeCertificateSubkeyIds(certificate, subkeyIds);
}
/**
* Storage backend.
*/
public interface Backend {
/**
* Get the locking mechanism to write-lock the backend.
*
* @return lock
*/
LockingMechanism getLock();
/**
* Read a {@link Certificate} by its OpenPGP fingerprint.
*
* @param fingerprint fingerprint
* @return certificate
*
* @throws BadNameException if the fingerprint is malformed
* @throws IOException in case of an IO error
* @throws BadDataException if the certificate contains bad data
*/
Certificate readByFingerprint(String fingerprint) throws BadNameException, IOException, BadDataException;
/**
* Read a {@link Certificate} or {@link pgp.certificate_store.certificate.Key} by the given special name.
*
* @param specialName special name
* @return certificate or key
*
* @throws BadNameException if the special name is not known
* @throws IOException in case of an IO error
* @throws BadDataException if the certificate contains bad data
*/
KeyMaterial readBySpecialName(String specialName) throws BadNameException, IOException, BadDataException;
/**
* Return an {@link Iterator} of all {@link Certificate Certificates} in the store, except for certificates
* stored under a special name.
*
* @return iterator
*/
Iterator<Certificate> readItems();
/**
* Insert a {@link pgp.certificate_store.certificate.Key} or {@link Certificate} as trust-root.
*
* @param data input stream containing the key material
* @param merge callback to merge the key material with existing key material
* @return merged or inserted key material
*
* @throws BadDataException if the data stream or existing key material contains bad data
* @throws IOException in case of an IO error
*/
KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge)
throws BadDataException, IOException;
/**
* Insert a {@link Certificate} identified by its fingerprint into the directory.
*
* @param data input stream containing the certificate data
* @param merge callback to merge the certificate with existing key material
* @return merged or inserted certificate
*
* @throws IOException in case of an IO error
* @throws BadDataException if the data stream or existing certificate contains bad data
*/
Certificate doInsert(InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException;
/**
* Insert a {@link pgp.certificate_store.certificate.Key} or {@link Certificate} under the given special name.
*
* @param specialName special name to identify the key material with
* @param data data stream containing the key or certificate
* @param merge callback to merge the key/certificate with existing key material
* @return certificate component of the merged or inserted key material
*
* @throws IOException in case of an IO error
* @throws BadDataException if the data stream or existing key material contains bad data
* @throws BadNameException if the special name is not known
*/
Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException, BadNameException;
/**
* Calculate the tag of the certificate with the given fingerprint.
*
* @param fingerprint fingerprint
* @return tag
*
* @throws BadNameException if the fingerprint is malformed
* @throws IOException in case of an IO error
* @throws IllegalArgumentException if the certificate does not exist
*/
Long getTagForFingerprint(String fingerprint) throws BadNameException, IOException;
/**
* Calculate the tag of the certificate identified by the given special name.
*
* @param specialName special name
* @return tag
*
* @throws BadNameException if the special name is not known
* @throws IOException in case of an IO error
* @throws IllegalArgumentException if the certificate or key does not exist
*/
Long getTagForSpecialName(String specialName) throws BadNameException, IOException;
}
/**
* Interface for a write-locking mechanism.
*/
public interface LockingMechanism {
/**
* Lock the store for writes.
* Readers can continue to use the store and will always see consistent certs.
*
* @throws IOException in case of an IO error
* @throws InterruptedException if the thread gets interrupted
*/
void lockDirectory() throws IOException, InterruptedException;
/**
* Try top lock the store for writes.
* Return false without locking the store in case the store was already locked.
*
* @return true if locking succeeded, false otherwise
*
* @throws IOException in case of an IO error
*/
boolean tryLockDirectory() throws IOException;
/**
* Return true if the lock is in locked state.
*
* @return true if locked
*/
boolean isLocked();
/**
* Release the directory write-lock acquired via {@link #lockDirectory()}.
*
* @throws IOException in case of an IO error
*/
void releaseDirectory() throws IOException;
}
}

View file

@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import pgp.certificate_store.PGPCertificateStore;
import pgp.certificate_store.certificate.Certificate;
import pgp.certificate_store.certificate.KeyMaterialMerger;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
* Adapter class to adapt a {@link PGPCertificateDirectory} to the {@link PGPCertificateStore} interface.
*/
public class PGPCertificateStoreAdapter implements PGPCertificateStore {
private final PGPCertificateDirectory directory;
public PGPCertificateStoreAdapter(PGPCertificateDirectory directory) {
this.directory = directory;
}
@Override
public Certificate getCertificate(String identifier)
throws IOException, BadNameException, BadDataException {
if (SpecialNames.lookupSpecialName(identifier) != null) {
return directory.getBySpecialName(identifier);
} else {
return directory.getByFingerprint(identifier.toLowerCase());
}
}
@Override
public Certificate getCertificateIfChanged(String identifier, Long tag)
throws IOException, BadNameException, BadDataException {
if (SpecialNames.lookupSpecialName(identifier) != null) {
return directory.getBySpecialNameIfChanged(identifier, tag);
} else {
return directory.getByFingerprintIfChanged(identifier.toLowerCase(), tag);
}
}
@Override
public Iterator<Certificate> getCertificatesBySubkeyId(long subkeyId)
throws IOException, BadDataException {
Set<String> fingerprints = directory.getCertificateFingerprintsForSubkeyId(subkeyId);
Set<Certificate> certificates = new HashSet<>();
for (String fingerprint : fingerprints) {
try {
certificates.add(directory.getByFingerprint(fingerprint));
} catch (BadNameException e) {
throw new RuntimeException(e);
}
}
return certificates.iterator();
}
@Override
public Certificate insertCertificate(InputStream data, KeyMaterialMerger merge)
throws IOException, InterruptedException, BadDataException {
Certificate certificate = directory.insert(data, merge);
return certificate;
}
@Override
public Certificate insertCertificateBySpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
throws IOException, InterruptedException, BadDataException, BadNameException {
return directory.insertWithSpecialName(specialName, data, merge);
}
@Override
public Iterator<Certificate> getCertificates() {
return directory.items();
}
@Override
public Iterator<String> getFingerprints() {
return directory.fingerprints();
}
}

View file

@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import pgp.certificate_store.certificate.Certificate;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
import java.io.IOException;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* Interface for a read-only OpenPGP certificate directory.
*/
public interface ReadOnlyPGPCertificateDirectory {
/**
* Get the trust-root certificate. This is a certificate which is stored under the special name
* <pre>trust-root</pre>.
*
* @return trust-root certificate
*
* @throws IOException in case of an IO error
* @throws BadDataException if the certificate contains bad data
* @throws NoSuchElementException if no such certificate is found
*/
Certificate getTrustRootCertificate()
throws IOException, BadDataException;
/**
* Get the trust-root certificate if it has changed.
* This method uses the <pre>tag</pre> to calculate if the certificate might have changed.
* If the computed tag equals the given tag, the certificate has not changed, so <pre>null</pre> is returned.
* Otherwise. the changed certificate is returned.
*
* @param tag tag
* @return changed certificate, or null if the certificate is unchanged.
*
* @throws IOException in case of an IO error
* @throws BadDataException if the certificate contains bad data
* @throws NoSuchElementException if no such certificate is found
*/
Certificate getTrustRootCertificateIfChanged(long tag)
throws IOException, BadDataException;
/**
* Get the certificate identified by the given fingerprint.
*
* @param fingerprint lower-case fingerprint of the certificate
* @return certificate
*
* @throws IOException in case of an IO error
* @throws BadNameException if the fingerprint is malformed
* @throws BadDataException if the certificate contains bad data
* @throws NoSuchElementException if no such certificate is found
*/
Certificate getByFingerprint(String fingerprint)
throws IOException, BadNameException, BadDataException;
/**
* Get the certificate identified by the given fingerprint if it has changed.
* This method uses the <pre>tag</pre> to calculate, if the certificate might have changed.
* If the computed tag equals the given tag, the certificate has not changed, so <pre>null</pre> is returned.
* Otherwise, the changed certificate is returned.
*
* @param fingerprint lower-case fingerprint of the certificate
* @param tag tag
* @return certificate or null if the certificate has not been changed
*
* @throws IOException in case of an IO error
* @throws BadNameException if the fingerprint is malformed
* @throws BadDataException if the certificate contains bad data
* @throws NoSuchElementException if no such certificate is found
*/
Certificate getByFingerprintIfChanged(String fingerprint, long tag)
throws IOException, BadNameException, BadDataException;
/**
* Get the certificate identified by the given special name.
*
* @param specialName special name
* @return certificate
*
* @throws IOException in case of an IO error
* @throws BadNameException if the special name is not known
* @throws BadDataException if the certificate contains bad data
* @throws NoSuchElementException if no such certificate is found
*/
Certificate getBySpecialName(String specialName)
throws IOException, BadNameException, BadDataException;
/**
* Get the certificate identified by the given special name or null, if it has not been changed.
* This method uses the <pre>tag</pre> to calculate, if the certificate might have changed.
* If the computed tag equals the given tag, the certificate has not changed, so <pre>null</pre> is returned.
* Otherwise, the changed certificate is returned.
*
* @param specialName special name
* @param tag tag
* @return certificate or null
*
* @throws IOException in case of an IO error
* @throws BadNameException if the special name is not known
* @throws BadDataException if the certificate contains bad data
* @throws NoSuchElementException if no such certificate is found
*/
Certificate getBySpecialNameIfChanged(String specialName, long tag)
throws IOException, BadNameException, BadDataException;
/**
* Get all certificates in the directory, except for certificates which are stored by special name.
*
* @return iterator of certificates
*/
Iterator<Certificate> items();
/**
* Get the fingerprints of all certificates in the directory, except for certificates which are stored by
* special name.
*
* @return iterator of fingerprints
*/
Iterator<String> fingerprints();
}

View file

@ -1,47 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.MergeCallback;
public interface SharedPGPCertificateDirectory {
LockingMechanism getLock();
Certificate getByFingerprint(String fingerprint)
throws IOException, BadNameException, BadDataException;
Certificate getBySpecialName(String specialName)
throws IOException, BadNameException, BadDataException;
Certificate getByFingerprintIfChanged(String fingerprint, String tag)
throws IOException, BadNameException, BadDataException;
Certificate getBySpecialNameIfChanged(String specialName, String tag)
throws IOException, BadNameException, BadDataException;
Certificate insert(InputStream data, MergeCallback merge)
throws IOException, BadDataException, InterruptedException;
Certificate tryInsert(InputStream data, MergeCallback merge)
throws IOException, BadDataException;
Certificate insertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
throws IOException, BadDataException, BadNameException, InterruptedException;
Certificate tryInsertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
throws IOException, BadDataException, BadNameException;
Iterator<Certificate> items();
Iterator<String> fingerprints();
}

View file

@ -1,314 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
import pgp.certificate_store.exception.NotAStoreException;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.CertificateReaderBackend;
import pgp.certificate_store.MergeCallback;
public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDirectory {
private final FilenameResolver resolver;
private final LockingMechanism writeLock;
private final CertificateReaderBackend certificateReaderBackend;
public SharedPGPCertificateDirectoryImpl(BackendProvider backendProvider)
throws NotAStoreException {
this(backendProvider.provideCertificateReaderBackend());
}
public SharedPGPCertificateDirectoryImpl(CertificateReaderBackend certificateReaderBackend)
throws NotAStoreException {
this(
BaseDirectoryProvider.getDefaultBaseDir(),
certificateReaderBackend);
}
public SharedPGPCertificateDirectoryImpl(File baseDirectory, CertificateReaderBackend certificateReaderBackend)
throws NotAStoreException {
this(
certificateReaderBackend,
new FilenameResolver(baseDirectory),
FileLockingMechanism.defaultDirectoryFileLock(baseDirectory));
}
public SharedPGPCertificateDirectoryImpl(
CertificateReaderBackend certificateReaderBackend,
FilenameResolver filenameResolver,
LockingMechanism writeLock)
throws NotAStoreException {
this.certificateReaderBackend = certificateReaderBackend;
this.resolver = filenameResolver;
this.writeLock = writeLock;
File baseDirectory = resolver.getBaseDirectory();
if (!baseDirectory.exists()) {
if (!baseDirectory.mkdirs()) {
throw new NotAStoreException("Cannot create base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "'");
}
} else {
if (baseDirectory.isFile()) {
throw new NotAStoreException("Base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "' appears to be a file.");
}
}
}
@Override
public LockingMechanism getLock() {
return writeLock;
}
@Override
public Certificate getByFingerprint(String fingerprint)
throws IOException, BadNameException, BadDataException {
File certFile = resolver.getCertFileByFingerprint(fingerprint);
if (!certFile.exists()) {
return null;
}
FileInputStream fileIn = new FileInputStream(certFile);
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
Certificate certificate = certificateReaderBackend.readCertificate(bufferedIn);
if (!certificate.getFingerprint().equals(fingerprint)) {
// TODO: Figure out more suitable exception
throw new BadDataException();
}
return certificate;
}
@Override
public Certificate getBySpecialName(String specialName)
throws IOException, BadNameException {
File certFile = resolver.getCertFileBySpecialName(specialName);
if (!certFile.exists()) {
return null;
}
FileInputStream fileIn = new FileInputStream(certFile);
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
Certificate certificate = certificateReaderBackend.readCertificate(bufferedIn);
return certificate;
}
@Override
public Certificate getByFingerprintIfChanged(String fingerprint, String tag)
throws IOException, BadNameException, BadDataException {
Certificate certificate = getByFingerprint(fingerprint);
if (certificate.getTag().equals(tag)) {
return null;
}
return certificate;
}
@Override
public Certificate getBySpecialNameIfChanged(String specialName, String tag)
throws IOException, BadNameException {
Certificate certificate = getBySpecialName(specialName);
if (certificate.getTag().equals(tag)) {
return null;
}
return certificate;
}
@Override
public Certificate insert(InputStream data, MergeCallback merge)
throws IOException, BadDataException, InterruptedException {
writeLock.lockDirectory();
Certificate certificate = _insert(data, merge);
writeLock.releaseDirectory();
return certificate;
}
@Override
public Certificate tryInsert(InputStream data, MergeCallback merge)
throws IOException, BadDataException {
if (!writeLock.tryLockDirectory()) {
return null;
}
Certificate certificate = _insert(data, merge);
writeLock.releaseDirectory();
return certificate;
}
private Certificate _insert(InputStream data, MergeCallback merge)
throws IOException, BadDataException {
Certificate newCertificate = certificateReaderBackend.readCertificate(data);
Certificate existingCertificate;
File certFile;
try {
existingCertificate = getByFingerprint(newCertificate.getFingerprint());
certFile = resolver.getCertFileByFingerprint(newCertificate.getFingerprint());
} catch (BadNameException e) {
throw new BadDataException();
}
if (existingCertificate != null && !existingCertificate.getTag().equals(newCertificate.getTag())) {
newCertificate = merge.merge(newCertificate, existingCertificate);
}
writeCertificate(newCertificate, certFile);
return newCertificate;
}
private void writeCertificate(Certificate certificate, File certFile)
throws IOException {
certFile.getParentFile().mkdirs();
if (!certFile.exists() && !certFile.createNewFile()) {
throw new IOException("Could not create cert file " + certFile.getAbsolutePath());
}
InputStream certIn = certificate.getInputStream();
FileOutputStream fileOut = new FileOutputStream(certFile);
byte[] buffer = new byte[4096];
int read;
while ((read = certIn.read(buffer)) != -1) {
fileOut.write(buffer, 0, read);
}
certIn.close();
fileOut.close();
}
@Override
public Certificate insertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
throws IOException, BadNameException, BadDataException, InterruptedException {
writeLock.lockDirectory();
Certificate certificate = _insertSpecial(specialName, data, merge);
writeLock.releaseDirectory();
return certificate;
}
@Override
public Certificate tryInsertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
throws IOException, BadNameException, BadDataException {
if (!writeLock.tryLockDirectory()) {
return null;
}
Certificate certificate = _insertSpecial(specialName, data, merge);
writeLock.releaseDirectory();
return certificate;
}
private Certificate _insertSpecial(String specialName, InputStream data, MergeCallback merge)
throws IOException, BadNameException, BadDataException {
Certificate newCertificate = certificateReaderBackend.readCertificate(data);
Certificate existingCertificate = getBySpecialName(specialName);
File certFile = resolver.getCertFileBySpecialName(specialName);
if (existingCertificate != null && !existingCertificate.getTag().equals(newCertificate.getTag())) {
newCertificate = merge.merge(newCertificate, existingCertificate);
}
writeCertificate(newCertificate, certFile);
return newCertificate;
}
@Override
public Iterator<Certificate> items() {
return new Iterator<Certificate>() {
private final List<Lazy<Certificate>> certificateQueue = Collections.synchronizedList(new ArrayList<>());
// Constructor... wtf.
{
File[] subdirectories = resolver.getBaseDirectory().listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory() && file.getName().matches("^[a-f0-9]{2}$");
}
});
for (File subdirectory : subdirectories) {
File[] files = subdirectory.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isFile() && file.getName().matches("^[a-f0-9]{38}$");
}
});
for (File certFile : files) {
certificateQueue.add(new Lazy<Certificate>() {
@Override
Certificate get() throws BadDataException {
try {
Certificate certificate = certificateReaderBackend.readCertificate(new FileInputStream(certFile));
if (!(subdirectory.getName() + certFile.getName()).equals(certificate.getFingerprint())) {
throw new BadDataException();
}
return certificate;
} catch (IOException e) {
throw new AssertionError("File got deleted.");
}
}
});
}
}
}
@Override
public boolean hasNext() {
return !certificateQueue.isEmpty();
}
@Override
public Certificate next() {
try {
return certificateQueue.remove(0).get();
} catch (BadDataException e) {
throw new AssertionError("Could not retrieve item: " + e.getMessage());
}
}
};
}
private abstract static class Lazy<E> {
abstract E get() throws BadDataException;
}
@Override
public Iterator<String> fingerprints() {
Iterator<Certificate> certificates = items();
return new Iterator<String>() {
@Override
public boolean hasNext() {
return certificates.hasNext();
}
@Override
public String next() {
return certificates.next().getFingerprint();
}
};
}
}

View file

@ -9,11 +9,14 @@ import java.util.Map;
public class SpecialNames { public class SpecialNames {
public static final String TRUST_ROOT = "trust-root";
// Map to allow for potentially upper- and lowercase variants of the same special name
private static final Map<String, String> SPECIAL_NAMES = new HashMap<>(); private static final Map<String, String> SPECIAL_NAMES = new HashMap<>();
static { static {
SPECIAL_NAMES.put("TRUST-ROOT", "trust-root"); // TODO: Remove SPECIAL_NAMES.put("TRUST-ROOT", TRUST_ROOT); // TODO: Remove
SPECIAL_NAMES.put("trust-root", "trust-root"); SPECIAL_NAMES.put(TRUST_ROOT, TRUST_ROOT);
} }
public static String lookupSpecialName(String specialName) { public static String lookupSpecialName(String specialName) {

View file

@ -0,0 +1,126 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import pgp.certificate_store.certificate.Certificate;
import pgp.certificate_store.certificate.KeyMaterial;
import pgp.certificate_store.certificate.KeyMaterialMerger;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
import java.io.IOException;
import java.io.InputStream;
/**
* Interface for a writing OpenPGP certificate directory.
*/
public interface WritingPGPCertificateDirectory {
/**
* Return the certificate or key identified by the special name <pre>trust-root</pre>.
*
* @return trust-root key or certificate
*
* @throws IOException in case of an IO error
* @throws BadDataException if the certificate contains bad data
*/
KeyMaterial getTrustRoot()
throws IOException, BadDataException;
/**
* Insert a key or certificate under the special name <pre>trust-root</pre>.
* This method blocks until the key material has been written.
*
* @param data input stream containing the key or certificate
* @param merge key material merger to merge the key or certificate with existing key material
* @return the merged or inserted key or certificate
*
* @throws IOException in case of an IO error
* @throws BadDataException if the data stream or the existing trust-root key material contains bad data
* @throws InterruptedException if the thread is interrupted
*/
KeyMaterial insertTrustRoot(InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException, InterruptedException;
/**
* Insert a key or certificate under the special name <pre>trust-root</pre>.
* Contrary to {@link #insertTrustRoot(InputStream, KeyMaterialMerger)}, this method does not block.
* Instead, it returns null if the write-lock cannot be obtained.
*
* @param data input stream containing the key or certificate
* @param merge key material merger to merge the key or certificate with existing key material
* @return the merged or inserted key or certificate, or null if the write-lock cannot be obtained
*
* @throws IOException in case of an IO error
* @throws BadDataException if the thread is interrupted
*/
KeyMaterial tryInsertTrustRoot(InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException;
/**
* Insert a certificate identified by its fingerprint.
* This method blocks until the certificate has been written.
*
* @param data input stream containing the certificate data
* @param merge merge callback to merge the certificate with existing certificate material
* @return the merged or inserted certificate
*
* @throws IOException in case of an IO error
* @throws BadDataException if the data stream or existing certificate contains bad data
* @throws InterruptedException if the thread is interrupted
*/
Certificate insert(InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException, InterruptedException;
/**
* Insert a certificate identified by its fingerprint.
* Contrary to {@link #insert(InputStream, KeyMaterialMerger)}, this method does not block.
* Instead, it returns null if the write-lock cannot be obtained.
*
* @param data input stream containing the certificate data
* @param merge merge callback to merge the certificate with existing certificate material
* @return the merged or inserted certificate
*
* @throws IOException in case of an IO error
* @throws BadDataException if the data stream or existing certificate contains bad data
*/
Certificate tryInsert(InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException;
/**
* Insert a certificate or key under the given special name.
* This method blocks until the certificate/key has been written.
*
* @param specialName special name under which the key material shall be inserted
* @param data input stream containing the key/certificate data
* @param merge callback to merge the key/certificate with existing key material
* @return certificate component of the merged or inserted key material data
*
* @throws IOException in case of an IO error
* @throws BadDataException if the data stream or the existing certificate contains bad data
* @throws BadNameException if the special name is not known
* @throws InterruptedException if the thread is interrupted
*/
Certificate insertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException, BadNameException, InterruptedException;
/**
* Insert a certificate or key under the given special name.
* Contrary to {@link #insertWithSpecialName(String, InputStream, KeyMaterialMerger)}, this method does not block.
* Instead, it returns null if the write-lock cannot be obtained.
*
* @param specialName special name under which the key material shall be inserted
* @param data input stream containing the key material
* @param merge callback to merge the key/certificate with existing key material
* @return certificate component of the merged or inserted key material
*
* @throws IOException in case of an IO error
* @throws BadDataException if the data stream or existing key material contains bad data
* @throws BadNameException if the special name is not known
*/
Certificate tryInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException, BadNameException;
}

View file

@ -0,0 +1,448 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.backend;
import pgp.cert_d.PGPCertificateDirectory;
import pgp.cert_d.SpecialNames;
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.BufferedInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.regex.Pattern;
/**
* Implementation of {@link PGPCertificateDirectory.Backend} which stores certificates in a directory structure.
*
* @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/#name-implementation">Shared PGP Certificate Directory</a>
*/
public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirectory.Backend {
private abstract static class Lazy<E> {
abstract E get() throws BadDataException;
}
/**
* Locking mechanism which uses a lock file to synchronize write-access to the store.
*/
private static class FileLockingMechanism implements PGPCertificateDirectory.LockingMechanism {
private final File lockFile;
private RandomAccessFile randomAccessFile;
private FileLock fileLock;
FileLockingMechanism(File lockFile) {
this.lockFile = lockFile;
}
public static FileLockingMechanism defaultDirectoryFileLock(File baseDirectory) {
return new FileLockingMechanism(new File(baseDirectory, "writelock"));
}
@Override
public synchronized void lockDirectory() throws IOException, InterruptedException {
if (randomAccessFile != null) {
// we own the lock already. Let's wait...
this.wait();
}
try {
randomAccessFile = new RandomAccessFile(lockFile, "rw");
} catch (FileNotFoundException e) {
lockFile.createNewFile();
randomAccessFile = new RandomAccessFile(lockFile, "rw");
}
fileLock = randomAccessFile.getChannel().lock();
}
@Override
public synchronized boolean tryLockDirectory() throws IOException {
if (randomAccessFile != null) {
// We already locked the directory for another write operation.
// We fail, since we have not yet released the lock from the other operation.
return false;
}
try {
randomAccessFile = new RandomAccessFile(lockFile, "rw");
} catch (FileNotFoundException e) {
lockFile.createNewFile();
randomAccessFile = new RandomAccessFile(lockFile, "rw");
}
try {
fileLock = randomAccessFile.getChannel().tryLock();
if (fileLock == null) {
// try-lock failed, file is locked by another process.
randomAccessFile.close();
randomAccessFile = null;
return false;
}
} catch (OverlappingFileLockException e) {
// Some other object is holding the lock.
randomAccessFile.close();
randomAccessFile = null;
return false;
}
return true;
}
@Override
public boolean isLocked() {
return randomAccessFile != null;
}
@Override
public synchronized void releaseDirectory() throws IOException {
// unlock file
if (fileLock != null) {
fileLock.release();
fileLock = null;
}
// close file
if (randomAccessFile != null) {
randomAccessFile.close();
randomAccessFile = null;
}
// delete file
if (lockFile.exists()) {
lockFile.delete();
}
// notify waiters
this.notify();
}
}
private final File baseDirectory;
private final PGPCertificateDirectory.LockingMechanism lock;
private final FilenameResolver resolver;
private final KeyMaterialReaderBackend reader;
public FileBasedCertificateDirectoryBackend(File baseDirectory, KeyMaterialReaderBackend reader) throws NotAStoreException {
this.baseDirectory = baseDirectory;
this.resolver = new FilenameResolver(baseDirectory);
if (!baseDirectory.exists()) {
if (!baseDirectory.mkdirs()) {
throw new NotAStoreException("Cannot create base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "'");
}
} else {
if (baseDirectory.isFile()) {
throw new NotAStoreException("Base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "' appears to be a file.");
}
}
this.lock = FileLockingMechanism.defaultDirectoryFileLock(baseDirectory);
this.reader = reader;
}
@Override
public PGPCertificateDirectory.LockingMechanism getLock() {
return lock;
}
@Override
public Certificate readByFingerprint(String fingerprint) throws BadNameException, IOException, BadDataException {
File certFile = resolver.getCertFileByFingerprint(fingerprint);
if (!certFile.exists()) {
return null;
}
long tag = getTagForFingerprint(fingerprint);
FileInputStream fileIn = new FileInputStream(certFile);
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
Certificate certificate = reader.read(bufferedIn, tag).asCertificate();
if (!certificate.getFingerprint().equals(fingerprint)) {
// TODO: Figure out more suitable exception
throw new BadDataException();
}
return certificate;
}
@Override
public KeyMaterial readBySpecialName(String specialName) throws BadNameException, IOException, BadDataException {
File certFile = resolver.getCertFileBySpecialName(specialName);
if (!certFile.exists()) {
return null;
}
long tag = getTagForSpecialName(specialName);
FileInputStream fileIn = new FileInputStream(certFile);
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
KeyMaterial keyMaterial = reader.read(bufferedIn, tag);
return keyMaterial;
}
@Override
public Iterator<Certificate> readItems() {
return new Iterator<Certificate>() {
private final List<Lazy<Certificate>> certificateQueue = Collections.synchronizedList(new ArrayList<>());
// Constructor... wtf.
{
File[] subdirectories = baseDirectory.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory() && file.getName().matches("^[a-f0-9]{2}$");
}
});
if (subdirectories == null) {
subdirectories = new File[0];
}
for (File subdirectory : subdirectories) {
File[] files = subdirectory.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isFile() && file.getName().matches("^[a-f0-9]{38}$");
}
});
if (files == null) {
files = new File[0];
}
for (File certFile : files) {
certificateQueue.add(new Lazy<Certificate>() {
@Override
Certificate get() throws BadDataException {
try {
long tag = getTag(certFile);
Certificate certificate = reader.read(new FileInputStream(certFile), tag).asCertificate();
if (!(subdirectory.getName() + certFile.getName()).equals(certificate.getFingerprint())) {
throw new BadDataException();
}
return certificate;
} catch (IOException e) {
throw new AssertionError("File got deleted.");
}
}
});
}
}
}
@Override
public boolean hasNext() {
return !certificateQueue.isEmpty();
}
@Override
public Certificate next() {
try {
return certificateQueue.remove(0).get();
} catch (BadDataException e) {
throw new AssertionError("Could not retrieve item: " + e.getMessage());
}
}
};
}
@Override
public KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge) throws BadDataException, IOException {
KeyMaterial newCertificate = reader.read(data, null);
KeyMaterial existingCertificate;
File certFile;
try {
existingCertificate = readBySpecialName(SpecialNames.TRUST_ROOT);
certFile = resolver.getCertFileBySpecialName(SpecialNames.TRUST_ROOT);
} catch (BadNameException e) {
throw new BadDataException();
}
if (existingCertificate != null) {
newCertificate = merge.merge(newCertificate, existingCertificate);
}
long tag = writeToFile(newCertificate.getInputStream(), certFile);
if (newCertificate instanceof Key) {
newCertificate = new Key((Key) newCertificate, tag);
} else {
newCertificate = new Certificate((Certificate) newCertificate, tag);
}
return newCertificate;
}
@Override
public Certificate doInsert(InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException {
KeyMaterial newCertificate = reader.read(data, null);
Certificate existingCertificate;
File certFile;
try {
existingCertificate = readByFingerprint(newCertificate.getFingerprint());
certFile = resolver.getCertFileByFingerprint(newCertificate.getFingerprint());
} catch (BadNameException e) {
throw new BadDataException();
}
if (existingCertificate != null) {
newCertificate = merge.merge(newCertificate, existingCertificate);
}
long tag = writeToFile(newCertificate.getInputStream(), certFile);
return new Certificate(newCertificate.asCertificate(), tag);
}
@Override
public Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException, BadNameException {
KeyMaterial newCertificate = reader.read(data, null);
KeyMaterial existingCertificate;
File certFile;
try {
existingCertificate = readBySpecialName(specialName);
certFile = resolver.getCertFileBySpecialName(specialName);
} catch (BadNameException e) {
throw new BadDataException();
}
if (existingCertificate != null) {
newCertificate = merge.merge(newCertificate, existingCertificate);
}
long tag = writeToFile(newCertificate.getInputStream(), certFile);
return new Certificate(newCertificate.asCertificate(), tag);
}
@Override
public Long getTagForFingerprint(String fingerprint) throws BadNameException, IOException {
File file = resolver.getCertFileByFingerprint(fingerprint);
return getTag(file);
}
@Override
public Long getTagForSpecialName(String specialName) throws BadNameException, IOException {
File file = resolver.getCertFileBySpecialName(specialName);
return getTag(file);
}
private Long getTag(File file) throws IOException {
if (!file.exists()) {
throw new NoSuchElementException();
}
Path path = file.toPath();
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
// On UNIX file systems, for example, fileKey() will return the device ID and inode
int fileId = attrs.fileKey().hashCode();
long lastMod = attrs.lastModifiedTime().toMillis();
return lastMod + (11L * fileId);
}
private long writeToFile(InputStream inputStream, File certFile)
throws IOException {
certFile.getParentFile().mkdirs();
if (!certFile.exists() && !certFile.createNewFile()) {
throw new IOException("Could not create cert file " + certFile.getAbsolutePath());
}
FileOutputStream fileOut = new FileOutputStream(certFile);
byte[] buffer = new byte[4096];
int read;
while ((read = inputStream.read(buffer)) != -1) {
fileOut.write(buffer, 0, read);
}
inputStream.close();
fileOut.close();
return getTag(certFile);
}
/**
* Class to resolve file names from certificate fingerprints / special names.
*/
public static class FilenameResolver {
private final File baseDirectory;
// matches v4 and v5 fingerprints (v4 = 40 hex chars, v5 = 64 hex chars)
private final Pattern openPgpFingerprint = Pattern.compile("^[a-f0-9]{40}([a-f0-9]{24})?$");
public FilenameResolver(File baseDirectory) {
this.baseDirectory = baseDirectory;
}
public File getBaseDirectory() {
return baseDirectory;
}
/**
* Calculate the file location for the certificate addressed by the given
* lowercase hexadecimal OpenPGP fingerprint.
*
* @param fingerprint fingerprint
* @return absolute certificate file location
*
* @throws BadNameException if the given fingerprint string is not a fingerprint
*/
public File getCertFileByFingerprint(String fingerprint) throws BadNameException {
if (!isFingerprint(fingerprint)) {
throw new BadNameException();
}
// is fingerprint
File subdirectory = new File(getBaseDirectory(), fingerprint.substring(0, 2));
File file = new File(subdirectory, fingerprint.substring(2));
return file;
}
/**
* Calculate the file location for the certification addressed using the given special name.
* For known special names, see {@link SpecialNames}.
*
* @param specialName special name (e.g. "trust-root")
* @return absolute certificate file location
*
* @throws BadNameException in case the given special name is not known
*/
public File getCertFileBySpecialName(String specialName)
throws BadNameException {
if (!isSpecialName(specialName)) {
throw new BadNameException(String.format("%s is not a known special name", specialName));
}
return new File(getBaseDirectory(), specialName);
}
private boolean isFingerprint(String fingerprint) {
return openPgpFingerprint.matcher(fingerprint).matches();
}
private boolean isSpecialName(String specialName) {
return SpecialNames.lookupSpecialName(specialName) != null;
}
}
}

View file

@ -0,0 +1,170 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.backend;
import pgp.cert_d.PGPCertificateDirectory;
import pgp.cert_d.SpecialNames;
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 java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
/**
* Implementation of the {@link PGPCertificateDirectory.Backend} which stores key material in-memory.
* It uses object locking with {@link #wait()} and {@link #notify()} to synchronize write-access.
*/
public class InMemoryCertificateDirectoryBackend implements PGPCertificateDirectory.Backend {
protected static class ObjectLockingMechanism implements PGPCertificateDirectory.LockingMechanism {
private boolean locked = false;
@Override
public synchronized void lockDirectory() throws InterruptedException {
if (isLocked()) {
wait();
}
locked = true;
}
@Override
public synchronized boolean tryLockDirectory() {
if (isLocked()) {
return false;
}
locked = true;
return true;
}
@Override
public synchronized boolean isLocked() {
return locked;
}
@Override
public synchronized void releaseDirectory() {
locked = false;
notify();
}
}
private final Map<String, Certificate> certificateFingerprintMap = new HashMap<>();
private final Map<String, KeyMaterial> keyMaterialSpecialNameMap = new HashMap<>();
private final PGPCertificateDirectory.LockingMechanism lock = new ObjectLockingMechanism();
private final KeyMaterialReaderBackend reader;
private final AtomicLong nonce = new AtomicLong(1);
public InMemoryCertificateDirectoryBackend(KeyMaterialReaderBackend reader) {
this.reader = reader;
}
@Override
public PGPCertificateDirectory.LockingMechanism getLock() {
return lock;
}
@Override
public Certificate readByFingerprint(String fingerprint) {
return certificateFingerprintMap.get(fingerprint);
}
@Override
public KeyMaterial readBySpecialName(String specialName) throws BadNameException {
if (SpecialNames.lookupSpecialName(specialName) == null) {
throw new BadNameException("Invalid special name " + specialName);
}
return keyMaterialSpecialNameMap.get(specialName);
}
@Override
public Iterator<Certificate> readItems() {
return certificateFingerprintMap.values().iterator();
}
@Override
public KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge)
throws BadDataException, IOException {
KeyMaterial update = reader.read(data, null);
KeyMaterial existing = null;
try {
existing = readBySpecialName(SpecialNames.TRUST_ROOT);
} catch (BadNameException e) {
// Does not happen
throw new RuntimeException(e);
}
KeyMaterial merged = merge.merge(update, existing);
if (merged instanceof Key) {
merged = new Key((Key) merged, newTag());
} else {
merged = new Certificate((Certificate) merged, newTag());
}
keyMaterialSpecialNameMap.put(SpecialNames.TRUST_ROOT, merged);
return merged;
}
@Override
public Certificate doInsert(InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException {
KeyMaterial update = reader.read(data, null);
Certificate existing = readByFingerprint(update.getFingerprint());
Certificate merged = merge.merge(update, existing).asCertificate();
merged = new Certificate(merged, newTag());
certificateFingerprintMap.put(update.getFingerprint(), merged);
return merged;
}
@Override
public Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException, BadNameException {
KeyMaterial keyMaterial = reader.read(data, null);
KeyMaterial existing = readBySpecialName(specialName);
KeyMaterial merged = merge.merge(keyMaterial, existing);
if (merged instanceof Key) {
merged = new Key((Key) merged, newTag());
} else {
merged = new Certificate((Certificate) merged, newTag());
}
keyMaterialSpecialNameMap.put(specialName, merged);
return merged.asCertificate();
}
@Override
public Long getTagForFingerprint(String fingerprint) throws BadNameException, IOException {
Certificate certificate = certificateFingerprintMap.get(fingerprint);
if (certificate == null) {
return null;
}
return certificate.getTag();
}
@Override
public Long getTagForSpecialName(String specialName) throws BadNameException, IOException {
if (SpecialNames.lookupSpecialName(specialName) == null) {
throw new BadNameException("Invalid special name " + specialName);
}
KeyMaterial tagged = keyMaterialSpecialNameMap.get(specialName);
if (tagged == null) {
return null;
}
return tagged.getTag();
}
private Long newTag() {
return System.currentTimeMillis() + nonce.incrementAndGet();
}
}

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Storage Backends.
*/
package pgp.cert_d.backend;

View file

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package pgp.cert_d; package pgp.cert_d.subkey_lookup;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -11,8 +11,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import pgp.certificate_store.SubkeyLookup;
public class InMemorySubkeyLookup implements SubkeyLookup { public class InMemorySubkeyLookup implements SubkeyLookup {
private static final Map<Long, Set<String>> subkeyMap = new HashMap<>(); private static final Map<Long, Set<String>> subkeyMap = new HashMap<>();
@ -30,6 +28,7 @@ public class InMemorySubkeyLookup implements SubkeyLookup {
public void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) { public void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) {
for (long subkeyId : subkeyIds) { for (long subkeyId : subkeyIds) {
Set<String> certificates = subkeyMap.get(subkeyId); Set<String> certificates = subkeyMap.get(subkeyId);
// noinspection Java8MapApi
if (certificates == null) { if (certificates == null) {
certificates = new HashSet<>(); certificates = new HashSet<>();
subkeyMap.put(subkeyId, certificates); subkeyMap.put(subkeyId, certificates);

View file

@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.subkey_lookup;
import java.io.File;
/**
* Factory class to instantiate {@link InMemorySubkeyLookup} objects.
*/
public class InMemorySubkeyLookupFactory implements SubkeyLookupFactory {
@Override
public SubkeyLookup createFileBasedInstance(File baseDirectory) {
return new InMemorySubkeyLookup();
}
}

View file

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store; package pgp.cert_d.subkey_lookup;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -16,6 +16,8 @@ public interface SubkeyLookup {
* *
* @param subkeyId subkey id * @param subkeyId subkey id
* @return fingerprint of the certificate * @return fingerprint of the certificate
*
* @throws IOException in case of an IO error
*/ */
Set<String> getCertificateFingerprintsForSubkeyId(long subkeyId) throws IOException; Set<String> getCertificateFingerprintsForSubkeyId(long subkeyId) throws IOException;
@ -25,6 +27,7 @@ public interface SubkeyLookup {
* *
* @param certificate certificate fingerprint * @param certificate certificate fingerprint
* @param subkeyIds subkey ids * @param subkeyIds subkey ids
*
* @throws IOException in case of an IO error * @throws IOException in case of an IO error
*/ */
void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) throws IOException; void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) throws IOException;

View file

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.subkey_lookup;
import java.io.File;
/**
* Factory class to instantiate different {@link SubkeyLookup} implementations.
*/
public interface SubkeyLookupFactory {
/**
* Create a new {@link SubkeyLookup} instance that lives in the given baseDirectory.
*
* @param baseDirectory base directory
* @return subkey lookup
*/
SubkeyLookup createFileBasedInstance(File baseDirectory);
}

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Subkey Lookup functionality.
*/
package pgp.cert_d.subkey_lookup;

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,6 +6,7 @@ package pgp.cert_d;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import pgp.cert_d.backend.FileBasedCertificateDirectoryBackend;
import pgp.certificate_store.exception.BadNameException; import pgp.certificate_store.exception.BadNameException;
import java.io.File; import java.io.File;
@ -18,13 +19,13 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
public class FilenameResolverTest { public class FilenameResolverTest {
private File baseDir; private File baseDir;
private FilenameResolver resolver; private FileBasedCertificateDirectoryBackend.FilenameResolver resolver;
@BeforeEach @BeforeEach
public void setup() throws IOException { public void setup() throws IOException {
baseDir = Files.createTempDirectory("filenameresolver").toFile(); baseDir = Files.createTempDirectory("filenameresolver").toFile();
baseDir.deleteOnExit(); baseDir.deleteOnExit();
resolver = new FilenameResolver(baseDir); resolver = new FileBasedCertificateDirectoryBackend.FilenameResolver(baseDir);
} }
@Test @Test

View file

@ -0,0 +1,311 @@
// 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.Named;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import pgp.cert_d.dummy.TestKeyMaterialMerger;
import pgp.cert_d.dummy.TestKeyMaterialReaderBackend;
import pgp.cert_d.subkey_lookup.InMemorySubkeyLookup;
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.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.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 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<Arguments> 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<String> expected = new HashSet<>(Arrays.asList(RON_FP, CEDRIC_FP));
Set<String> actual = new HashSet<>();
Iterator<String> 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));
}
@ParameterizedTest
@MethodSource("provideTestSubjects")
public void testOverwriteTrustRoot(PGPCertificateDirectory directory)
throws BadDataException, IOException, InterruptedException {
directory.insertTrustRoot(TestKeys.getHarryKey(), merger);
assertEquals(HARRY_FP, directory.getTrustRoot().getFingerprint());
assertTrue(directory.getTrustRoot() instanceof Key);
directory.insertTrustRoot(TestKeys.getCedricCert(), merger);
assertEquals(CEDRIC_FP, directory.getTrustRoot().getFingerprint());
assertTrue(directory.getTrustRoot() instanceof Certificate);
}
@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

@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import pgp.cert_d.backend.InMemoryCertificateDirectoryBackend;
import pgp.cert_d.dummy.TestKeyMaterialMerger;
import pgp.cert_d.dummy.TestKeyMaterialReaderBackend;
import pgp.cert_d.subkey_lookup.InMemorySubkeyLookupFactory;
import pgp.certificate_store.certificate.Certificate;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class PGPCertificateStoreAdapterTest {
private PGPCertificateDirectory directory;
private PGPCertificateStoreAdapter adapter;
private static final TestKeyMaterialMerger merger = new TestKeyMaterialMerger();
@BeforeEach
public void setup() {
directory = new PGPCertificateDirectory(
new InMemoryCertificateDirectoryBackend(new TestKeyMaterialReaderBackend()),
new InMemorySubkeyLookupFactory().createFileBasedInstance(null));
adapter = new PGPCertificateStoreAdapter(directory);
}
@Test
public void testBadFPWithInvalidCharsYieldsBadNameException() {
assertThrows(BadNameException.class, () -> adapter.getCertificate("XYZ78fd17f207fdf62f7976c4e9d98917ad84522"));
}
@Test
public void testBadFPWithTooFewCharsYieldsBadNameException() {
assertThrows(BadNameException.class, () -> adapter.getCertificate("23578fd17f207fdf62f7976c4e9d98917ad"));
}
@Test
public void testInsertGetCertificate()
throws BadDataException, IOException, InterruptedException, BadNameException {
assertThrows(NoSuchElementException.class, () -> adapter.getCertificate(TestKeys.CEDRIC_FP));
assertFalse(adapter.getCertificates().hasNext());
Certificate certificate = adapter.insertCertificate(TestKeys.getCedricCert(), merger);
assertNotNull(certificate);
assertEquals(TestKeys.CEDRIC_FP, certificate.getFingerprint());
certificate = adapter.getCertificate(TestKeys.CEDRIC_FP.toUpperCase());
assertEquals(TestKeys.CEDRIC_FP, certificate.getFingerprint(), "We can also fetch with uppercase fps");
Iterator<String> fingerprints = adapter.getFingerprints();
assertEquals(TestKeys.CEDRIC_FP, fingerprints.next());
assertFalse(fingerprints.hasNext());
}
@Test
public void testInsertGetTrustRoot()
throws BadDataException, BadNameException, IOException, InterruptedException {
assertThrows(NoSuchElementException.class, () -> adapter.getCertificate(SpecialNames.TRUST_ROOT));
Certificate certificate = adapter.insertCertificateBySpecialName(
SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger);
assertNotNull(certificate);
assertEquals(TestKeys.HARRY_FP, certificate.getFingerprint());
assertFalse(adapter.getCertificates().hasNext(), "Special-named certs are not returned by getCertificates()");
assertFalse(adapter.getFingerprints().hasNext());
}
@Test
public void testGetCertificateIfChanged()
throws BadDataException, IOException, InterruptedException, BadNameException {
Certificate certificate = adapter.insertCertificate(TestKeys.getRonCert(), merger);
Long tag = certificate.getTag();
assertNull(adapter.getCertificateIfChanged(TestKeys.RON_FP, tag), "Cert has not changed, tag is still valid");
assertNotNull(adapter.getCertificateIfChanged(TestKeys.RON_FP, tag + 1));
}
@Test
public void testGetTrustRootIfChanged()
throws BadDataException, BadNameException, IOException, InterruptedException {
Certificate certificate = adapter.insertCertificateBySpecialName(SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger);
Long tag = certificate.getTag();
assertNull(adapter.getCertificateIfChanged(SpecialNames.TRUST_ROOT, tag));
assertNotNull(adapter.getCertificateIfChanged(SpecialNames.TRUST_ROOT, tag * 2));
}
@Test
public void testGetCertificateBySubkeyId()
throws BadDataException, IOException, InterruptedException {
// Insert some certs
adapter.insertCertificate(TestKeys.getCedricCert(), merger);
adapter.insertCertificate(TestKeys.getHarryKey(), merger);
// Now insert Ron
Certificate certificate = adapter.insertCertificate(TestKeys.getRonCert(), merger);
List<Long> subkeyIds = certificate.getSubkeyIds();
assertFalse(adapter.getCertificatesBySubkeyId(0).hasNext());
for (Long subkeyId : subkeyIds) {
Iterator<Certificate> certsWithSubkey = adapter.getCertificatesBySubkeyId(subkeyId);
Certificate certWithSubkey = certsWithSubkey.next();
assertFalse(certsWithSubkey.hasNext());
assertEquals(TestKeys.RON_FP, certWithSubkey.getFingerprint());
}
}
}

View file

@ -24,7 +24,8 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import pgp.cert_d.jdbc.sqlite.DatabaseSubkeyLookup; import pgp.cert_d.jdbc.sqlite.DatabaseSubkeyLookup;
import pgp.cert_d.jdbc.sqlite.SqliteSubkeyLookupDaoImpl; import pgp.cert_d.jdbc.sqlite.SqliteSubkeyLookupDaoImpl;
import pgp.certificate_store.SubkeyLookup; import pgp.cert_d.subkey_lookup.InMemorySubkeyLookup;
import pgp.cert_d.subkey_lookup.SubkeyLookup;
public class SubkeyLookupTest { public class SubkeyLookupTest {

View file

@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
public class TestKeys {
@SuppressWarnings("CharsetObjectCanBeUsed")
private static final Charset UTF8 = Charset.forName("UTF8");
public static final String HARRY_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
"Comment: 2357 8FD1 7F20 7FDF 62F7 976C 4E9D 9891 7AD8 4522\n" +
"Comment: Harry Potter <harry@potter.more>\n" +
"\n" +
"xVgEYwTP0hYJKwYBBAHaRw8BAQdAPVcWeaMiUVG+vECWpoytSoF3wNJQG/JsnCbj\n" +
"uQtv0REAAP0cS3GCmrIMO/FqNm1FG1mKw4P+mvZ1JBFILN7Laooq7A/QwsARBB8W\n" +
"CgCDBYJjBM/SBYkFn6YAAwsJBwkQTp2YkXrYRSJHFAAAAAAAHgAgc2FsdEBub3Rh\n" +
"dGlvbnMuc2VxdW9pYS1wZ3Aub3JnRSvJhQu9P/3bpFqFdB2c5Mfg9JIdyic1tsAt\n" +
"lZ7o4k4DFQoIApsBAh4BFiEEI1eP0X8gf99i95dsTp2YkXrYRSIAAK2cAP9juDnY\n" +
"qB6XuXVx76MzDlFemqJ/r2TIlN22O33ITp23cQEAiMk/rULVdfmlFi3QBvXgtPI2\n" +
"QQYFI0UnyGLmJSa1cwzNIEhhcnJ5IFBvdHRlciA8aGFycnlAcG90dGVyLm1vcmU+\n" +
"wsAUBBMWCgCGBYJjBM/SBYkFn6YAAwsJBwkQTp2YkXrYRSJHFAAAAAAAHgAgc2Fs\n" +
"dEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn0o9na1p+a9kY3y3+xUSFFnxbuxNM\n" +
"5zvth0SAfJIH2C8DFQoIApkBApsBAh4BFiEEI1eP0X8gf99i95dsTp2YkXrYRSIA\n" +
"AC1zAP0e2qRXH4zCnjvdYwGP0tIY3dwBsm1bvk+wVFHm8h68iwEAh2uyyQ+O5iQH\n" +
"7NN/lV5dUKKsKaimj/vVGpSW3NtFZQDHWARjBM/SFgkrBgEEAdpHDwEBB0BUqcZu\n" +
"VsEO6fmW8q3S5ll9WohcTOWRX7Spg5wS3DIqPgABALzJ9ZImb4U94WqRtftSSaeF\n" +
"0w6rHCn2DiTT8pxjefGQEW7CwMUEGBYKATcFgmMEz9IFiQWfpgAJEE6dmJF62EUi\n" +
"RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ+HPX0u5kyKR\n" +
"5IwErbomgGKVCGuvR6oSKc7CDQYMJS9eApsCvqAEGRYKAG8FgmMEz9IJEKk0hrvR\n" +
"6Jc7RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ8Chba26\n" +
"1nQ6ZEZ/rVH8wMhYznGNa/Ux28sodM04wU6dFiEEli7ijJ6quX9gSoSbqTSGu9Ho\n" +
"lzsAAG1wAQDVvKVWaMOBELROkF72oBH58X6lrOmr08W5FJQxehywhQEAwetpgL1V\n" +
"DNj4qcvuCJJ2agAM1tA22WMPpQQeA5CCgwcWIQQjV4/RfyB/32L3l2xOnZiRethF\n" +
"IgAAsWEA/RfOKexMYEtzlpM71MB9SL+emHXf+w1TNAvBxrifU8bMAPoDmWHkWjZQ\n" +
"N6upbHKssRywPLKCMPLnFYtBNxDrMYr0BMddBGMEz9ISCisGAQQBl1UBBQEBB0CR\n" +
"p5dCIlSpV/EvXX2+YZnZSRtc8eTFXkph8RArNi0QPAMBCAcAAP9seqRo6mbmvS4h\n" +
"fkxmV5zap3wIemzW4iabNU2VbWJbEBALwsAGBBgWCgB4BYJjBM/SBYkFn6YACRBO\n" +
"nZiRethFIkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdx\n" +
"uRLJ/h81azzvGn5zgJ+jdfkdM6iO+f1CLgfnHUH9ugKbDBYhBCNXj9F/IH/fYveX\n" +
"bE6dmJF62EUiAACObgEAk4whKEo2nzpWht65tpFjrEXdakj00mA/P612P2CUdPQB\n" +
"ANNn+VUiu9rtnLcP4NlaUVOwsgN7yyed0orbmG1VvSMF\n" +
"=cBAn\n" +
"-----END PGP PRIVATE KEY BLOCK-----\n";
public static final String HARRY_FP = "23578fd17f207fdf62f7976c4e9d98917ad84522";
public static final String RON_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
"Comment: B798 AF18 6BFE 4C19 902D 4950 5647 F001 37EF 4C41\n" +
"Comment: Ron Weasley <ron@weasley.burrow>\n" +
"\n" +
"xjMEYwTRXBYJKwYBBAHaRw8BAQdAPHyiu4nwvo3OY3wLG1tUmS6qeTeT1zd3BrL+\n" +
"6/5Ys3jCwBEEHxYKAIMFgmME0VwFiQWfpgADCwkHCRBWR/ABN+9MQUcUAAAAAAAe\n" +
"ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfEPNi/1ObPMwDwS094Lcyq\n" +
"dRNRk2FRzvhoXKrqF/GHfQMVCggCmwECHgEWIQS3mK8Ya/5MGZAtSVBWR/ABN+9M\n" +
"QQAAR/oBAJWxxUJqOAzYG4uAd6SSF55LZVl00t3bGhgEyGmrB/ppAQCZTpWu0rwU\n" +
"GVv/MoeqRwX+P8sHS4FSu/hSYJpbNwysCM0gUm9uIFdlYXNsZXkgPHJvbkB3ZWFz\n" +
"bGV5LmJ1cnJvdz7CwBQEExYKAIYFgmME0VwFiQWfpgADCwkHCRBWR/ABN+9MQUcU\n" +
"AAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmf43PjsP9w1eGYP\n" +
"CLm6O+K27EQPiCf2cW71QnQ0RunupgMVCggCmQECmwECHgEWIQS3mK8Ya/5MGZAt\n" +
"SVBWR/ABN+9MQQAA7rYA/3U2aaw5PFa9L90PbxygOwFrgIVWLiOpnKfjqDJqEgva\n" +
"AQDxTIbpUYEAYmTpmAm1tiQSlpp9P96vqCMIj2OqtYCNAs4zBGME0VwWCSsGAQQB\n" +
"2kcPAQEHQGzhRPzKRkkce0v1NjuTV2stn8CEMVgnUxsMPtd0h2M9wsDFBBgWCgE3\n" +
"BYJjBNFcBYkFn6YACRBWR/ABN+9MQUcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z\n" +
"ZXF1b2lhLXBncC5vcmd6UNkzsh0jKRPQAKX2PoUhMN4QfhTK9IC6L+QbyL1rFgKb\n" +
"Ar6gBBkWCgBvBYJjBNFcCRCuGMJD3GUsUUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" +
"cy5zZXF1b2lhLXBncC5vcmcUTns9+sw7XKKO5ZOYQninRAchypKHbqV2LinV46Hi\n" +
"bxYhBI+SjTgn0fulukOYj64YwkPcZSxRAADZtAEApse3UJi1iuSFvnyXxuYIOm4d\n" +
"0sOaOtd18venqfWGyX4BALf7T7LknMY688vaW6/xkw2fonG6Y5VxreIHlMZAcX0H\n" +
"FiEEt5ivGGv+TBmQLUlQVkfwATfvTEEAAFQ3AQCGSLEt8wgJZXlljPdk1eQ3uvW3\n" +
"VHryNAc3/vbSOvByFAD/WKXY8Pqki2r9XVUW33Q88firoiKVuGmBxklEG3ACjALO\n" +
"OARjBNFcEgorBgEEAZdVAQUBAQdARnMlx3ST0EHPiErN7lOF+lhtJ8FmW9arc46u\n" +
"sHFMgUMDAQgHwsAGBBgWCgB4BYJjBNFcBYkFn6YACRBWR/ABN+9MQUcUAAAAAAAe\n" +
"ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfv1PKQX1GMihAdj3ftW/yS\n" +
"bnPYdE+0h5rGCuhYl7sjaQKbDBYhBLeYrxhr/kwZkC1JUFZH8AE370xBAABWugEA\n" +
"rWOEHQjzoQkxxsErVEVZjqr05SLMmo6+HMJ/4Sgur10A/0+4FSbaKKNGiCnCMRsZ\n" +
"BEswoD99mUaBXl1nPH+Hg38O\n" +
"=+pb5\n" +
"-----END PGP PUBLIC KEY BLOCK-----\n";
public static final String RON_FP = "b798af186bfe4c19902d49505647f00137ef4c41";
public static final String CEDRIC_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
"Comment: 5E75 BF20 646B C1A9 8D3B 1BC2 FE9C D472 987C 4021\n" +
"Comment: Cedric Diggory <cedric@diggo.ry>\n" +
"\n" +
"xjMEYwTIyhYJKwYBBAHaRw8BAQdA80cyaoAEfh/ENuHw8XtWqrxDoPQ/x44LQzyO\n" +
"TLhMN+PCwBEEHxYKAIMFgmMEyMoFiQWfpgADCwkHCRD+nNRymHxAIUcUAAAAAAAe\n" +
"ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmf0ckQJQzwOKkXPe8rFP5B+\n" +
"CbAshRG5OKD3Dp+hScGFXgMVCggCmwECHgEWIQRedb8gZGvBqY07G8L+nNRymHxA\n" +
"IQAA9WYBAP5rQCq/W3KV90T/wpxf5pcXoCB4tCC9Gi/1AiuGhQdAAP48PIX9fH+T\n" +
"g7N+tU0xzzCc2nWxG3cIuvGFsg94pKL8As0gQ2VkcmljIERpZ2dvcnkgPGNlZHJp\n" +
"Y0BkaWdnby5yeT7CwBQEExYKAIYFgmMEyMoFiQWfpgADCwkHCRD+nNRymHxAIUcU\n" +
"AAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdkUL5mF5SwIXja\n" +
"bCxhI3lvqiUURSoLY13K6YvHYLz7bwMVCggCmQECmwECHgEWIQRedb8gZGvBqY07\n" +
"G8L+nNRymHxAIQAA6SwA/jiM8k/Z0ljnHdFxsdoLhdnTZ0yJT/7RxreSZ3aITrDs\n" +
"AP9V8bAYy4hK0C7i4FmNcos3HQs2Si6ee2/EZjo8LqxeCc4zBGMEyMoWCSsGAQQB\n" +
"2kcPAQEHQIu0hKMngTnmIPXlZ/p9WOZmLB0s9v9yZJLdZ5ICKn7jwsDFBBgWCgE3\n" +
"BYJjBMjKBYkFn6YACRD+nNRymHxAIUcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z\n" +
"ZXF1b2lhLXBncC5vcmdCT1SyOVJwTPp4OEDWFNEgxKD12H+Dya9EzOMJ3I9frwKb\n" +
"Ar6gBBkWCgBvBYJjBMjKCRDNPli8d9EIkUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" +
"cy5zZXF1b2lhLXBncC5vcmccLTSNIhZOiNFaTj76iAutuAkUCImFp5ptMICZRo7E\n" +
"TRYhBESzEAYRbxRfM3ub5c0+WLx30QiRAAAZtwD/WRJrSxzJRsnZs4w+QgZjqOZx\n" +
"bOGwGObfbEHaExG0cKEA/R+BFODg5oPOvK9W7n0Kt9O171Po+zXB0UDmBiEhh0YL\n" +
"FiEEXnW/IGRrwamNOxvC/pzUcph8QCEAAEneAQDnOv/cf1/qmjfLnorEi+Z4gRWQ\n" +
"fp3Rp/gI4SLUQxT0PQD/USZIP0bNMGGC1TRQa+8nK6opSqtIvsatt0tQuu178A7O\n" +
"OARjBMjKEgorBgEEAZdVAQUBAQdAazcEUsYtY9f9o4A+ePR7ACMIDScVEUWS83+I\n" +
"SwJQz3QDAQgHwsAGBBgWCgB4BYJjBMjKBYkFn6YACRD+nNRymHxAIUcUAAAAAAAe\n" +
"ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmc/qxMatwD+6zaKDZGlVdn/\n" +
"TWumSgLtuyYonaOupIfMEAKbDBYhBF51vyBka8GpjTsbwv6c1HKYfEAhAADPiwEA\n" +
"vQ7fTnAHcdZlMVnNPkc0pZSp1+kO5Z789I5Pp4HloNIBAMoC84ja83PjvcpIyxgR\n" +
"kspLC9BliezVbFSHIK9NQ/wC\n" +
"=VemI\n" +
"-----END PGP PUBLIC KEY BLOCK-----\n";
public static final String CEDRIC_FP = "5e75bf20646bc1a98d3b1bc2fe9cd472987c4021";
public static InputStream getHarryKey() {
return new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8));
}
public static InputStream getRonCert() {
return new ByteArrayInputStream(RON_CERT.getBytes(UTF8));
}
public static InputStream getCedricCert() {
return new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8));
}
}

View file

@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.dummy;
import pgp.certificate_store.certificate.KeyMaterial;
import pgp.certificate_store.certificate.KeyMaterialMerger;
import java.io.IOException;
public class TestKeyMaterialMerger implements KeyMaterialMerger {
@Override
public KeyMaterial merge(KeyMaterial data, KeyMaterial existing) throws IOException {
return data;
}
}

View file

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.dummy;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyRing;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator;
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
import org.bouncycastle.util.encoders.Hex;
import org.bouncycastle.util.io.Streams;
import pgp.certificate_store.certificate.Certificate;
import pgp.certificate_store.certificate.Key;
import pgp.certificate_store.certificate.KeyMaterial;
import pgp.certificate_store.certificate.KeyMaterialReaderBackend;
import pgp.certificate_store.exception.BadDataException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class TestKeyMaterialReaderBackend implements KeyMaterialReaderBackend {
KeyFingerPrintCalculator fpCalc = new BcKeyFingerprintCalculator();
@Override
public KeyMaterial read(InputStream data, Long tag) throws IOException, BadDataException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
Streams.pipeAll(data, out);
try {
return readKey(new ByteArrayInputStream(out.toByteArray()), tag);
} catch (IOException | PGPException e) {
try {
return readCertificate(new ByteArrayInputStream(out.toByteArray()), tag);
} catch (IOException e1) {
throw new BadDataException();
}
}
}
private Key readKey(InputStream inputStream, Long tag) throws IOException, PGPException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
Streams.pipeAll(inputStream, buffer);
inputStream.close();
InputStream decoderStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(buffer.toByteArray()));
PGPSecretKeyRing secretKeys = new PGPSecretKeyRing(decoderStream, fpCalc);
PGPPublicKeyRing cert = extractCert(secretKeys);
ByteArrayInputStream encoded = new ByteArrayInputStream(cert.getEncoded());
Certificate certificate = readCertificate(encoded, tag);
return new Key(buffer.toByteArray(), certificate, tag);
}
private Certificate readCertificate(InputStream inputStream, Long tag) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
Streams.pipeAll(inputStream, buffer);
ByteArrayInputStream in = new ByteArrayInputStream(buffer.toByteArray());
InputStream decoderStream = PGPUtil.getDecoderStream(in);
PGPPublicKeyRing cert = new PGPPublicKeyRing(decoderStream, fpCalc);
String fingerprint = Hex.toHexString(cert.getPublicKey().getFingerprint()).toLowerCase();
List<Long> subKeyIds = getSubkeyIds(cert);
return new Certificate(buffer.toByteArray(), fingerprint, subKeyIds, tag);
}
private PGPPublicKeyRing extractCert(PGPSecretKeyRing secretKeys) {
List<PGPPublicKey> publicKeyList = new ArrayList<>();
Iterator<PGPPublicKey> publicKeyIterator = secretKeys.getPublicKeys();
while (publicKeyIterator.hasNext()) {
publicKeyList.add(publicKeyIterator.next());
}
return new PGPPublicKeyRing(publicKeyList);
}
private static List<Long> getSubkeyIds(PGPKeyRing keyRing) {
List<Long> keyIds = new ArrayList<>();
Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
while (keys.hasNext()) {
keyIds.add(keys.next().getKeyID());
}
return keyIds;
}
}

View file

@ -6,5 +6,8 @@ SPDX-License-Identifier: Apache-2.0
# PGP Certificate Store Definitions # PGP Certificate Store Definitions
[![javadoc](https://javadoc.io/badge2/org.pgpainless/pgp-certificate-store/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgp-certificate-store)
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgp-certificate-store)](https://search.maven.org/artifact/org.pgpainless/pgp-certificate-store)
This module contains API definitions for an OpenPGP certificate store. This module contains API definitions for an OpenPGP certificate store.
A certificate store is used to store public key certificates only. A certificate store is used to store public key certificates only.

View file

@ -15,9 +15,10 @@ repositories {
apply plugin: 'ru.vyarus.animalsniffer' apply plugin: 'ru.vyarus.animalsniffer'
dependencies { dependencies {
// animal sniffer // animal sniffer for ensuring Android API compatibility
signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:2.3.3_r2@signature" signature "net.sf.androidscents.signature:android-api-level-${animalsnifferSignatureVersion}@signature"
// JUnit for testing
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"

View file

@ -1,39 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public abstract class AbstractCertificateStore implements CertificateStore {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractCertificateStore.class);
public Set<Certificate> getCertificatesBySubkeyId(long subkeyId)
throws IOException {
Set<String> identifiers = getCertificateFingerprintsForSubkeyId(subkeyId);
if (identifiers.isEmpty()) {
return Collections.emptySet();
}
Set<Certificate> certificates = new HashSet<>();
for (String identifier : identifiers) {
try {
certificates.add(getCertificate(identifier));
} catch (BadNameException | BadDataException e) {
LOGGER.warn("Could not read certificate.", e);
}
}
return certificates;
}
}

View file

@ -1,36 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
import java.io.IOException;
import java.io.InputStream;
import java.util.Set;
public abstract class Certificate {
/**
* Return the fingerprint of the certificate as 40 lowercase hex characters.
* TODO: Allow OpenPGP V5 fingerprints
*
* @return fingerprint
*/
public abstract String getFingerprint();
/**
* Return an {@link InputStream} of the binary representation of the certificate.
*
* @return input stream
*/
public abstract InputStream getInputStream() throws IOException;
/**
* Return a tag of the certificate.
* The tag is a checksum calculated over the binary representation of the certificate.
*
* @return tag
*/
public abstract String getTag() throws IOException;
public abstract Set<Long> getSubkeyIds() throws IOException;
}

View file

@ -1,147 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
/**
* Certificate storage definition.
* This interface defines methods to insert and retrieve {@link Certificate Certificates} to and from a store.
*
* {@link Certificate Certificates} are hereby identified by identifiers. An identifier can either be a fingerprint
* or a special name. Special names are implementation-defined identifiers for certificates.
*
* Fingerprints are expected to be hexadecimal lowercase character sequences.
*/
public interface CertificateDirectory {
/**
* Return the certificate that matches the given identifier.
* If no matching certificate can be found, return null.
*
* @param identifier identifier for a certificate.
* @return certificate or null
*
* @throws IOException in case of an IO-error
*/
Certificate getCertificate(String identifier)
throws IOException, BadNameException, BadDataException;
/**
* Return the certificate that matches the given identifier, but only iff it changed since the last invocation.
* To compare the certificate against its last returned result, the given tag is used.
* If the tag of the currently found certificate matches the given argument, return null.
*
* @param identifier identifier for a certificate
* @param tag tag to compare freshness
* @return changed certificate or null
*
* @throws IOException in case of an IO-error
*/
Certificate getCertificateIfChanged(String identifier, String tag)
throws IOException, BadNameException, BadDataException;
/**
* Insert a certificate into the store.
* If an instance of the certificate is already present in the store, the given {@link MergeCallback} will be
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
* will be stored in the store and returned.
*
* This method will block until a write-lock on the store can be acquired. If you cannot afford blocking,
* consider to use {@link #tryInsertCertificate(InputStream, MergeCallback)} instead.
*
* @param data input stream containing the new certificate instance
* @param merge callback for merging with an existing certificate instance
* @return merged certificate
*
* @throws IOException in case of an IO-error
* @throws InterruptedException in case the inserting thread gets interrupted
*/
Certificate insertCertificate(InputStream data, MergeCallback merge)
throws IOException, InterruptedException, BadDataException;
/**
* Insert a certificate into the store.
* If an instance of the certificate is already present in the store, the given {@link MergeCallback} will be
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
* will be stored in the store and returned.
*
* This method will not block. Instead, if the store is already write-locked, this method will simply return null
* without any writing.
* However, if the write-lock is available, this method will acquire the lock, write to the store, release the lock
* and return the written certificate.
*
* @param data input stream containing the new certificate instance
* @param merge callback for merging with an existing certificate instance
* @return merged certificate or null if the store cannot be locked
*
* @throws IOException in case of an IO-error
*/
Certificate tryInsertCertificate(InputStream data, MergeCallback merge)
throws IOException, BadDataException;
/**
* Insert a certificate into the store.
* The certificate will be stored under the given special name instead of its fingerprint.
*
* If an instance of the certificate is already present under the special name in the store, the given {@link MergeCallback} will be
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
* will be stored in the store and returned.
*
* This method will block until a write-lock on the store can be acquired. If you cannot afford blocking,
* consider to use {@link #tryInsertCertificateBySpecialName(String, InputStream, MergeCallback)} instead.
*
* @param data input stream containing the new certificate instance
* @param merge callback for merging with an existing certificate instance
* @return merged certificate or null if the store cannot be locked
*
* @throws IOException in case of an IO-error
*/
Certificate insertCertificateBySpecialName(String specialName, InputStream data, MergeCallback merge)
throws IOException, InterruptedException, BadDataException, BadNameException;
/**
* Insert a certificate into the store.
* The certificate will be stored under the given special name instead of its fingerprint.
*
* If an instance of the certificate is already present under the special name in the store, the given {@link MergeCallback} will be
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
* will be stored in the store and returned.
*
* This method will not block. Instead, if the store is already write-locked, this method will simply return null
* without any writing.
* However, if the write-lock is available, this method will acquire the lock, write to the store, release the lock
* and return the written certificate.
*
* @param data input stream containing the new certificate instance
* @param merge callback for merging with an existing certificate instance
* @return merged certificate or null if the store cannot be locked
*
* @throws IOException in case of an IO-error
*/
Certificate tryInsertCertificateBySpecialName(String specialName, InputStream data, MergeCallback merge)
throws IOException, BadDataException, BadNameException;
/**
* Return an {@link Iterator} containing all certificates in the store.
* The iterator will contain both certificates addressed by special names and by fingerprints.
*
* @return certificates
*/
Iterator<Certificate> getCertificates();
/**
* Return an {@link Iterator} containing all certificate fingerprints from the store.
* Note that this only includes the fingerprints of certificate primary keys, not those of subkeys.
*
* @return fingerprints
*/
Iterator<String> getFingerprints();
}

View file

@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
import java.io.IOException;
import java.io.InputStream;
/**
* Interface definition for a class that can read {@link Certificate Certificates} from binary
* {@link InputStream InputStreams}.
*/
public interface CertificateReaderBackend {
/**
* Read a {@link Certificate} from the given {@link InputStream}.
*
* @param inputStream input stream containing the binary representation of the certificate.
* @return certificate object
*
* @throws IOException in case of an IO error
*/
Certificate readCertificate(InputStream inputStream) throws IOException;
}

View file

@ -1,9 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
public interface CertificateStore extends CertificateDirectory, SubkeyLookup {
}

View file

@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
import java.io.IOException;
/**
* Merge a given certificate (update) with an existing certificate.
*/
public interface MergeCallback {
/**
* Merge the given certificate data with the existing certificate and return the result.
*
* If no existing certificate is found (i.e. existing is null), this method returns the unmodified data.
*
* @param data certificate
* @param existing optional already existing copy of the certificate
* @return merged certificate
*/
Certificate merge(Certificate data, Certificate existing) throws IOException;
}

View file

@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
import pgp.certificate_store.certificate.Certificate;
import pgp.certificate_store.certificate.KeyMaterialMerger;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* Interface for an OpenPGP certificate (public key) store.
*/
public interface PGPCertificateStore {
/**
* Return the certificate that matches the given identifier.
*
* @param identifier identifier for a certificate.
* @return certificate or null
*
* @throws IOException in case of an IO-error
* @throws BadNameException if the identifier is invalid
* @throws BadDataException if the certificate file contains invalid data
* @throws NoSuchElementException if no such certificate is found
*/
Certificate getCertificate(String identifier)
throws IOException, BadNameException, BadDataException;
/**
* Return the certificate that matches the given identifier, but only if it has been changed.
* Whether it has been changed is determined by calculating the tag in the directory
* (e.g. by looking at the inode and last modification date) and comparing the result with the tag provided by
* the caller.
*
* @param identifier certificate identifier
* @param tag tag by the caller
* @return certificate if it has been changed, null otherwise
*
* @throws IOException in case of an IO-error
* @throws BadNameException if the identifier is invalid
* @throws BadDataException if the certificate file contains invalid data
* @throws NoSuchElementException if no such certificate is found
*/
Certificate getCertificateIfChanged(String identifier, Long tag)
throws IOException, BadNameException, BadDataException;
/**
* Return an {@link Iterator} over all certificates in the store that contain a subkey with the given
* subkey id.
* @param subkeyId id of the subkey
* @return iterator
*
* @throws IOException in case of an IO error
* @throws BadDataException if any of the certificate files contains invalid data
*/
Iterator<Certificate> getCertificatesBySubkeyId(long subkeyId)
throws IOException, BadDataException;
/**
* Insert a certificate into the store.
* If an instance of the certificate is already present in the store, the given {@link KeyMaterialMerger} will be
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
* will be stored in the store and returned.
*
* This method will block until a write-lock on the store can be acquired.
*
* @param data input stream containing the new certificate instance
* @param merge callback for merging with an existing certificate instance
* @return merged certificate
*
* @throws IOException in case of an IO-error
* @throws InterruptedException in case the inserting thread gets interrupted
* @throws BadDataException if the data stream does not contain valid OpenPGP data
*/
Certificate insertCertificate(InputStream data, KeyMaterialMerger merge)
throws IOException, InterruptedException, BadDataException;
/**
* Insert a certificate into the store.
* The certificate will be stored under the given special name instead of its fingerprint.
*
* If an instance of the certificate is already present under the special name in the store, the given {@link KeyMaterialMerger} will be
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
* will be stored in the store and returned.
*
* This method will block until a write-lock on the store can be acquired.
*
* @param specialName special name of the certificate
* @param data input stream containing the new certificate instance
* @param merge callback for merging with an existing certificate instance
* @return merged certificate or null if the store cannot be locked
*
* @throws IOException in case of an IO-error
* @throws InterruptedException if the thread is interrupted
* @throws BadDataException if the certificate file does not contain valid OpenPGP data
* @throws BadNameException if the special name is unknown
*/
Certificate insertCertificateBySpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
throws IOException, InterruptedException, BadDataException, BadNameException;
/**
* Return an {@link Iterator} containing all certificates in the store.
* The iterator will contain both certificates addressed by special names and by fingerprints.
*
* @return certificates
*/
Iterator<Certificate> getCertificates();
/**
* Return an {@link Iterator} containing all certificate fingerprints from the store.
* Note that this only includes the fingerprints of certificate primary keys, not those of subkeys.
*
* @return fingerprints
*/
Iterator<String> getFingerprints();
}

View file

@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store.certificate;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.List;
/**
* OpenPGP certificate (public key).
*/
public class Certificate implements KeyMaterial {
private final byte[] bytes;
private final String fingerprint;
private final List<Long> subkeyIds;
private final Long tag;
/**
* Certificate constructor.
*
* @param bytes encoding of the certificate
* @param fingerprint fingerprint (lowercase hex characters)
* @param subkeyIds list of subkey ids
* @param tag tag
*/
public Certificate(byte[] bytes, String fingerprint, List<Long> subkeyIds, Long tag) {
this.bytes = bytes;
this.fingerprint = fingerprint;
this.subkeyIds = subkeyIds;
this.tag = tag;
}
/**
* Copy constructor to assign a new tag to the {@link Certificate}.
*
* @param cert certificate
* @param tag tag
*/
public Certificate(Certificate cert, Long tag) {
this(cert.bytes, cert.fingerprint, cert.subkeyIds, tag);
}
@Override
public String getFingerprint() {
return fingerprint;
}
@Override
public Certificate asCertificate() {
return this;
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(bytes);
}
@Override
public Long getTag() {
return tag;
}
@Override
public List<Long> getSubkeyIds() {
return subkeyIds;
}
}

View file

@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store.certificate;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.List;
/**
* OpenPGP key (secret key).
*/
public class Key implements KeyMaterial {
private final byte[] bytes;
private final Certificate certificate;
private final Long tag;
/**
* Key constructor.
*
* @param bytes encoding of the key
* @param certificate associated certificate
* @param tag tag
*/
public Key(byte[] bytes, Certificate certificate, Long tag) {
this.bytes = bytes;
this.certificate = certificate;
this.tag = tag;
}
/**
* Copy constructor to change the tag of both the {@link Key} and its {@link Certificate}.
*
* @param key key
* @param tag tag
*/
public Key(Key key, Long tag) {
this(key.bytes, new Certificate(key.certificate, tag), tag);
}
/**
* Return the certificate part of this OpenPGP key.
*
* @return OpenPGP certificate
*/
public Certificate getCertificate() {
return new Certificate(certificate, getTag());
}
@Override
public String getFingerprint() {
return certificate.getFingerprint();
}
@Override
public Certificate asCertificate() {
return getCertificate();
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(bytes);
}
@Override
public Long getTag() {
return tag;
}
@Override
public List<Long> getSubkeyIds() {
return certificate.getSubkeyIds();
}
}

View file

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store.certificate;
import java.io.InputStream;
import java.util.List;
import java.util.Set;
public interface KeyMaterial {
/**
* Return the fingerprint of the certificate as 40 lowercase hex characters.
* TODO: Allow OpenPGP V5 fingerprints
*
* @return fingerprint
*/
String getFingerprint();
/**
* Return the {@link Certificate} belonging to this key material.
* If this is already a {@link Certificate}, return this.
* If this is a {@link Key}, extract the {@link Certificate} and return it.
*
* @return certificate
*/
Certificate asCertificate();
/**
* Return an {@link InputStream} of the binary representation of the secret key.
*
* @return input stream
*/
InputStream getInputStream();
/**
* Return the tag belonging to this key material.
* The tag can be used to keep an application cache in sync with what is in the directory.
*
* @return tag
*/
Long getTag();
/**
* Return a {@link Set} containing key-ids of subkeys.
*
* @return subkeys
*/
List<Long> getSubkeyIds();
}

View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store.certificate;
import java.io.IOException;
/**
* Merge a given {@link Key} (update) with an existing {@link Key}.
*/
public interface KeyMaterialMerger {
/**
* Merge the given key material with an existing copy and return the result.
* If no existing {@link KeyMaterial} is found (i.e. if existing is null), this method returns the unmodified data.
*
* @param data key material
* @param existing optional already existing copy of the key material
* @return merged key material
*
* @throws IOException in case of an IO error
*/
KeyMaterial merge(KeyMaterial data, KeyMaterial existing) throws IOException;
}

View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store.certificate;
import pgp.certificate_store.exception.BadDataException;
import java.io.IOException;
import java.io.InputStream;
public interface KeyMaterialReaderBackend {
/**
* Read a {@link KeyMaterial} (either {@link Key} or {@link Certificate}) from the given {@link InputStream}.
*
* @param data input stream containing the binary representation of the key.
* @param tag tag for the key material. Might be null.
* @return key or certificate object
*
* @throws IOException in case of an IO error
* @throws BadDataException in case that the data stream does not contain a valid OpenPGP key/certificate
*/
KeyMaterial read(InputStream data, Long tag) throws IOException, BadDataException;
}

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* General OpenPGP Certificate Storage related classes.
*/
package pgp.certificate_store.certificate;

View file

@ -3,8 +3,6 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
/** /**
* Exceptions defined by the Shared PGP Certificate Directory. * Exceptions.
*
* @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/#name-failure-modes">Failure Modes</a>
*/ */
package pgp.certificate_store.exception; package pgp.certificate_store.exception;

View file

@ -4,7 +4,7 @@
rootProject.name = 'cert-d-java' rootProject.name = 'cert-d-java'
include 'pgp-cert-d-java', include 'pgp-certificate-store',
'pgp-cert-d-java-jdbc-sqlite-lookup', 'pgp-cert-d-java',
'pgp-certificate-store' 'pgp-cert-d-java-jdbc-sqlite-lookup'

View file

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