mirror of
https://codeberg.org/PGPainless/cert-d-java.git
synced 2025-09-09 03:09:39 +02:00
Initial commit
This commit is contained in:
commit
b142f310be
46 changed files with 2494 additions and 0 deletions
16
pgp-cert-d-java/README.md
Normal file
16
pgp-cert-d-java/README.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2022 Paul Schaub <info@pgpainless.org>
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
# Shared PGP Certificate Directory for Java
|
||||
|
||||
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,
|
||||
locking the directory for writes etc.
|
||||
|
||||
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.
|
||||
|
||||
`pgp-cert-d-java` can be used as an implementation of `pgp-certificate-store`.
|
30
pgp-cert-d-java/build.gradle
Normal file
30
pgp-cert-d-java/build.gradle
Normal file
|
@ -0,0 +1,30 @@
|
|||
// 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"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||
|
||||
// Logging
|
||||
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
|
||||
|
||||
testImplementation project(":pgp-cert-d-java-jdbc-sqlite-lookup")
|
||||
|
||||
api project(":pgp-certificate-store")
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// 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();
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.cert_d;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class BaseDirectoryProvider {
|
||||
|
||||
public static File getDefaultBaseDir() {
|
||||
// Check for environment variable
|
||||
String baseDirFromEnv = System.getenv("PGP_CERT_D");
|
||||
if (baseDirFromEnv != null) {
|
||||
return new File(baseDirFromEnv);
|
||||
}
|
||||
|
||||
// return OS-specific default dir
|
||||
String osName = System.getProperty("os.name", "generic")
|
||||
.toLowerCase();
|
||||
return getDefaultBaseDirForOS(osName);
|
||||
}
|
||||
|
||||
public static File getDefaultBaseDirForOS(String osName) {
|
||||
String STORE_NAME = "pgp.cert.d";
|
||||
if (osName.contains("win")) {
|
||||
// %APPDATA%\Roaming\pgp.cert.d
|
||||
return Paths.get(System.getenv("APPDATA"), "Roaming", STORE_NAME).toFile();
|
||||
}
|
||||
|
||||
if (osName.contains("nux")) {
|
||||
// $XDG_DATA_HOME/pgp.cert.d
|
||||
String xdg_data_home = System.getenv("XDG_DATA_HOME");
|
||||
if (xdg_data_home != null) {
|
||||
return Paths.get(xdg_data_home, STORE_NAME).toFile();
|
||||
}
|
||||
// $HOME/.local/share/pgp.cert.d
|
||||
return Paths.get(System.getProperty("user.home"), ".local", "share", STORE_NAME).toFile();
|
||||
}
|
||||
|
||||
if (osName.contains("mac")) {
|
||||
return Paths.get(System.getenv("HOME"), "Library", "Application Support", STORE_NAME).toFile();
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unknown OS " + osName);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
// 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
// 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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
// 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.cert_d;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import pgp.certificate_store.SubkeyLookup;
|
||||
|
||||
public class InMemorySubkeyLookup implements SubkeyLookup {
|
||||
|
||||
private static final Map<Long, Set<String>> subkeyMap = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Set<String> getCertificateFingerprintsForSubkeyId(long subkeyId) {
|
||||
Set<String> identifiers = subkeyMap.get(subkeyId);
|
||||
if (identifiers == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return Collections.unmodifiableSet(identifiers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) {
|
||||
for (long subkeyId : subkeyIds) {
|
||||
Set<String> certificates = subkeyMap.get(subkeyId);
|
||||
if (certificates == null) {
|
||||
certificates = new HashSet<>();
|
||||
subkeyMap.put(subkeyId, certificates);
|
||||
}
|
||||
certificates.add(certificate);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
subkeyMap.clear();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// 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;
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// 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();
|
||||
}
|
|
@ -0,0 +1,314 @@
|
|||
// 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
22
pgp-cert-d-java/src/main/java/pgp/cert_d/SpecialNames.java
Normal file
22
pgp-cert-d-java/src/main/java/pgp/cert_d/SpecialNames.java
Normal file
|
@ -0,0 +1,22 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.cert_d;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class SpecialNames {
|
||||
|
||||
private static final Map<String, String> SPECIAL_NAMES = new HashMap<>();
|
||||
|
||||
static {
|
||||
SPECIAL_NAMES.put("TRUST-ROOT", "trust-root"); // TODO: Remove
|
||||
SPECIAL_NAMES.put("trust-root", "trust-root");
|
||||
}
|
||||
|
||||
public static String lookupSpecialName(String specialName) {
|
||||
return SPECIAL_NAMES.get(specialName);
|
||||
}
|
||||
}
|
10
pgp-cert-d-java/src/main/java/pgp/cert_d/package-info.java
Normal file
10
pgp-cert-d-java/src/main/java/pgp/cert_d/package-info.java
Normal file
|
@ -0,0 +1,10 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* An implementation of the Shared PGP Certificate Directory for java.
|
||||
*
|
||||
* @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/">Shared PGP Certificate Directory</a>
|
||||
*/
|
||||
package pgp.cert_d;
|
|
@ -0,0 +1,43 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.cert_d;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||
|
||||
public class BaseDirectoryProviderTest {
|
||||
|
||||
@Test
|
||||
public void testGetDefaultBaseDir_Linux() {
|
||||
assumeTrue(System.getProperty("os.name").equalsIgnoreCase("linux"));
|
||||
File baseDir = BaseDirectoryProvider.getDefaultBaseDirForOS("linux");
|
||||
assertTrue(baseDir.getAbsolutePath().endsWith("/.local/share/pgp.cert.d"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDefaultBaseDir_Windows() {
|
||||
assumeTrue(System.getProperty("os.name").toLowerCase().contains("win"));
|
||||
File baseDir = BaseDirectoryProvider.getDefaultBaseDirForOS("Windows");
|
||||
assertTrue(baseDir.getAbsolutePath().endsWith("\\Roaming\\pgp.cert.d"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDefaultBaseDir_Mac() {
|
||||
assumeTrue(System.getProperty("os.name").toLowerCase().contains("mac"));
|
||||
File baseDir = BaseDirectoryProvider.getDefaultBaseDirForOS("Mac");
|
||||
assertTrue(baseDir.getAbsolutePath().endsWith("/Library/Application Support/pgp.cert.d"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDefaultBaseDirNotNull() {
|
||||
File baseDir = BaseDirectoryProvider.getDefaultBaseDir();
|
||||
assertNotNull(baseDir);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
// 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.certificate_store.exception.BadNameException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
public class FilenameResolverTest {
|
||||
|
||||
private File baseDir;
|
||||
private FilenameResolver resolver;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws IOException {
|
||||
baseDir = Files.createTempDirectory("filenameresolver").toFile();
|
||||
baseDir.deleteOnExit();
|
||||
resolver = new FilenameResolver(baseDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFileForFingerprint1() throws BadNameException {
|
||||
String fingerprint = "d1a66e1a23b182c9980f788cfbfcc82a015e7330";
|
||||
|
||||
File subDir = new File(baseDir, "d1");
|
||||
File expected = new File(subDir, "a66e1a23b182c9980f788cfbfcc82a015e7330");
|
||||
|
||||
assertEquals(expected.getAbsolutePath(), resolver.getCertFileByFingerprint(fingerprint).getAbsolutePath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFileForFingerprint2() throws BadNameException {
|
||||
String fingerprint = "eb85bb5fa33a75e15e944e63f231550c4f47e38e";
|
||||
|
||||
File subDir = new File(baseDir, "eb");
|
||||
File expected = new File(subDir, "85bb5fa33a75e15e944e63f231550c4f47e38e");
|
||||
|
||||
assertEquals(expected.getAbsolutePath(), resolver.getCertFileByFingerprint(fingerprint).getAbsolutePath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFileForInvalidNonHexFingerprint() {
|
||||
String invalidFingerprint = "thisisnothexadecimalthisisnothexadecimal";
|
||||
assertThrows(BadNameException.class, () -> resolver.getCertFileByFingerprint(invalidFingerprint));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFileForInvalidWrongLengthFingerprint() {
|
||||
String invalidFingerprint = "d1a66e1a23b182c9980f788cfbfcc82a015e73301234";
|
||||
assertThrows(BadNameException.class, () -> resolver.getCertFileByFingerprint(invalidFingerprint));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFileForNullFingerprint() {
|
||||
assertThrows(NullPointerException.class, () -> resolver.getCertFileByFingerprint(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFileForSpecialName() throws BadNameException {
|
||||
String specialName = "trust-root";
|
||||
File expected = new File(baseDir, "trust-root");
|
||||
|
||||
assertEquals(expected, resolver.getCertFileBySpecialName(specialName));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFileForInvalidSpecialName() {
|
||||
String invalidSpecialName = "invalid";
|
||||
assertThrows(BadNameException.class, () -> resolver.getCertFileBySpecialName(invalidSpecialName));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.cert_d;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
public class SpecialNamesTest {
|
||||
|
||||
@Test
|
||||
public void bothTrustRootNotationsAreRecognized() {
|
||||
assertEquals("trust-root", SpecialNames.lookupSpecialName("trust-root"));
|
||||
assertEquals("trust-root", SpecialNames.lookupSpecialName("TRUST-ROOT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidSpecialNameReturnsNull() {
|
||||
assertNull(SpecialNames.lookupSpecialName("invalid"));
|
||||
assertNull(SpecialNames.lookupSpecialName("trust root"));
|
||||
assertNull(SpecialNames.lookupSpecialName("writelock"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.cert_d;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import pgp.cert_d.jdbc.sqlite.DatabaseSubkeyLookup;
|
||||
import pgp.cert_d.jdbc.sqlite.SqliteSubkeyLookupDaoImpl;
|
||||
import pgp.certificate_store.SubkeyLookup;
|
||||
|
||||
public class SubkeyLookupTest {
|
||||
|
||||
private static final List<SubkeyLookup> testSubjects = new ArrayList<>();
|
||||
|
||||
@BeforeAll
|
||||
public static void setupLookupTestSubjects() throws IOException, SQLException {
|
||||
InMemorySubkeyLookup inMemorySubkeyLookup = new InMemorySubkeyLookup();
|
||||
testSubjects.add(inMemorySubkeyLookup);
|
||||
|
||||
File sqliteDatabase = Files.createTempFile("subkeyLookupTest", ".db").toFile();
|
||||
sqliteDatabase.createNewFile();
|
||||
sqliteDatabase.deleteOnExit();
|
||||
DatabaseSubkeyLookup sqliteSubkeyLookup = new DatabaseSubkeyLookup(SqliteSubkeyLookupDaoImpl.forDatabaseFile(sqliteDatabase));
|
||||
testSubjects.add(sqliteSubkeyLookup);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void tearDownLookupTestSubjects() {
|
||||
((InMemorySubkeyLookup) testSubjects.get(0)).clear();
|
||||
}
|
||||
|
||||
private static Stream<SubkeyLookup> provideSubkeyLookupsForTest() {
|
||||
return testSubjects.stream();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideSubkeyLookupsForTest")
|
||||
public void testInsertGet(SubkeyLookup subject) throws IOException {
|
||||
// Initially all null
|
||||
|
||||
assertTrue(subject.getCertificateFingerprintsForSubkeyId(123).isEmpty());
|
||||
assertTrue(subject.getCertificateFingerprintsForSubkeyId(1337).isEmpty());
|
||||
assertTrue(subject.getCertificateFingerprintsForSubkeyId(420).isEmpty());
|
||||
|
||||
// Store one val, others still null
|
||||
|
||||
subject.storeCertificateSubkeyIds("d1a66e1a23b182c9980f788cfbfcc82a015e7330", Collections.singletonList(123L));
|
||||
|
||||
assertEquals(Collections.singleton("d1a66e1a23b182c9980f788cfbfcc82a015e7330"), subject.getCertificateFingerprintsForSubkeyId(123));
|
||||
assertTrue(subject.getCertificateFingerprintsForSubkeyId(1337).isEmpty());
|
||||
assertTrue(subject.getCertificateFingerprintsForSubkeyId(420).isEmpty());
|
||||
|
||||
// Store other val, first stays intact
|
||||
|
||||
subject.storeCertificateSubkeyIds("d1a66e1a23b182c9980f788cfbfcc82a015e7330", Collections.singletonList(1337L));
|
||||
subject.storeCertificateSubkeyIds("d1a66e1a23b182c9980f788cfbfcc82a015e7330", Collections.singletonList(420L));
|
||||
|
||||
assertEquals(Collections.singleton("d1a66e1a23b182c9980f788cfbfcc82a015e7330"), subject.getCertificateFingerprintsForSubkeyId(123));
|
||||
assertEquals(Collections.singleton("d1a66e1a23b182c9980f788cfbfcc82a015e7330"), subject.getCertificateFingerprintsForSubkeyId(1337));
|
||||
assertEquals(Collections.singleton("d1a66e1a23b182c9980f788cfbfcc82a015e7330"), subject.getCertificateFingerprintsForSubkeyId(420));
|
||||
|
||||
// add additional entry for subkey
|
||||
|
||||
subject.storeCertificateSubkeyIds("eb85bb5fa33a75e15e944e63f231550c4f47e38e", Collections.singletonList(123L));
|
||||
|
||||
assertEquals(
|
||||
new HashSet<>(Arrays.asList("eb85bb5fa33a75e15e944e63f231550c4f47e38e", "d1a66e1a23b182c9980f788cfbfcc82a015e7330")),
|
||||
subject.getCertificateFingerprintsForSubkeyId(123));
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue