Initial commit

This commit is contained in:
Paul Schaub 2022-03-01 15:19:01 +01:00
commit b142f310be
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
46 changed files with 2494 additions and 0 deletions

View file

@ -0,0 +1,10 @@
<!--
SPDX-FileCopyrightText: 2022 Paul Schaub <info@pgpainless.org>
SPDX-License-Identifier: Apache-2.0
-->
# PGP Certificate Store Definitions
This module contains API definitions for a certificate store for PGPainless.
A certificate store is used to store public key certificates only.

View file

@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
plugins {
id 'java-library'
}
group 'org.pgpainless'
repositories {
mavenCentral()
}
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
// Logging
api "org.slf4j:slf4j-api:$slf4jVersion"
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
}
test {
useJUnitPlatform()
}

View file

@ -0,0 +1,39 @@
// 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

@ -0,0 +1,36 @@
// 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

@ -0,0 +1,147 @@
// 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

@ -0,0 +1,26 @@
// 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

@ -0,0 +1,9 @@
// 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

@ -0,0 +1,25 @@
// 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,31 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
import java.io.IOException;
import java.util.List;
import java.util.Set;
public interface SubkeyLookup {
/**
* Lookup the fingerprint of the certificate that contains the given subkey.
* If no record is found, return null.
*
* @param subkeyId subkey id
* @return fingerprint of the certificate
*/
Set<String> getCertificateFingerprintsForSubkeyId(long subkeyId) throws IOException;
/**
* Record, which certificate the subkey-ids in the list belong to.
* This method does not change the affiliation of subkey-ids not contained in the provided list.
*
* @param certificate certificate fingerprint
* @param subkeyIds subkey ids
* @throws IOException in case of an IO error
*/
void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) throws IOException;
}

View file

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store.exception;
/**
* The data was not a valid OpenPGP cert or key in binary format.
*/
public class BadDataException extends Exception {
}

View file

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store.exception;
/**
* Provided name was neither a valid fingerprint, nor a known special name.
*/
public class BadNameException extends Exception {
public BadNameException() {
super();
}
public BadNameException(String message) {
super(message);
}
}

View file

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store.exception;
/**
* The base dir cannot possibly contain a store.
*/
public class NotAStoreException extends Exception {
public NotAStoreException() {
super();
}
public NotAStoreException(String message) {
super(message);
}
}

View file

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Exceptions defined by the Shared PGP Certificate Directory.
*
* @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/#name-failure-modes">Failure Modes</a>
*/
package pgp.certificate_store.exception;

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Abstract definitions of an OpenPGP certificate store.
*/
package pgp.certificate_store;

View file

@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class DummyTest {
@Test
public void test() {
assertTrue(true);
}
}