Implement storing of trust-root key

This commit is contained in:
Paul Schaub 2022-07-04 20:12:42 +02:00
parent fca9a8ef91
commit ee1fd669ed
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
13 changed files with 357 additions and 104 deletions

View file

@ -1,3 +1,8 @@
<!--
SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
SPDX-License-Identifier: Apache-2.0
-->
# Shared PGP Certificate Directory for Java
This repository contains implementations of the [Shared PGP Certificate Directory](https://sequoia-pgp.gitlab.io/pgp-cert-d/) specification using [PGPainless](https://pgpainless.org) as backend.

View file

@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.cli;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.pgpainless.PGPainless;
import org.pgpainless.certificate_store.CertificateFactory;
import org.pgpainless.key.OpenPgpFingerprint;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.CertificateMerger;
import pgp.certificate_store.Key;
import pgp.certificate_store.KeyMerger;
import java.io.IOException;
import java.util.Iterator;
public class MergeCallbacks {
/**
* Return a {@link CertificateMerger} that merges the two copies of the same certificate (same primary key) into one
* combined certificate.
*
* @return merging callback
*/
public static CertificateMerger mergeCertificates() {
return new CertificateMerger() {
@Override
public Certificate merge(Certificate data, Certificate existing) throws IOException {
try {
PGPPublicKeyRing existingCert = PGPainless.readKeyRing().publicKeyRing(existing.getInputStream());
PGPPublicKeyRing updatedCert = PGPainless.readKeyRing().publicKeyRing(data.getInputStream());
PGPPublicKeyRing mergedCert = PGPPublicKeyRing.join(existingCert, updatedCert);
printOutDifferences(existingCert, mergedCert);
return CertificateFactory.certificateFromPublicKeyRing(mergedCert);
} catch (PGPException e) {
throw new RuntimeException(e);
}
}
private void printOutDifferences(PGPPublicKeyRing existingCert, PGPPublicKeyRing mergedCert) {
int numSigsBefore = countSigs(existingCert);
int numSigsAfter = countSigs(mergedCert);
int newSigs = numSigsAfter - numSigsBefore;
int numUidsBefore = count(existingCert.getPublicKey().getUserIDs());
int numUidsAfter = count(mergedCert.getPublicKey().getUserIDs());
int newUids = numUidsAfter - numUidsBefore;
if (!existingCert.equals(mergedCert)) {
OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(mergedCert);
StringBuilder sb = new StringBuilder();
sb.append(String.format("Certificate %s has", fingerprint));
if (newSigs != 0) {
sb.append(String.format(" %d new signatures", newSigs));
}
if (newUids != 0) {
if (newSigs != 0) {
sb.append(" and");
}
sb.append(String.format(" %d new UIDs", newUids));
}
if (newSigs == 0 && newUids == 0) {
sb.append(" changed");
}
// In this case it is okay to print to stdout, since we are a CLI app
// CHECKSTYLE:OFF
System.out.println(sb);
// CHECKSTYLE:ON
}
}
private int countSigs(PGPPublicKeyRing keys) {
int numSigs = 0;
for (PGPPublicKey key : keys) {
numSigs += count(key.getSignatures());
}
return numSigs;
}
// TODO: Use CollectionUtils.count() once available
private int count(Iterator<?> iterator) {
int num = 0;
while (iterator.hasNext()) {
iterator.next();
num++;
}
return num;
}
};
}
/**
* Return an implementation of {@link CertificateMerger} that ignores the existing certificate and instead
* returns the first instance.
*
* @return overriding callback
*/
public static CertificateMerger overrideCertificate() {
// noinspection Convert2Lambda
return new CertificateMerger() {
@Override
public Certificate merge(Certificate data, Certificate existing) {
return data;
}
};
}
public static KeyMerger overrideKey() {
// noinspection Convert2Lambda
return new KeyMerger() {
@Override
public Key merge(Key data, Key existing) {
return data;
}
};
}
}

View file

@ -5,6 +5,7 @@
package pgp.cert_d.cli;
import org.pgpainless.certificate_store.CertificateReader;
import org.pgpainless.certificate_store.KeyReader;
import org.pgpainless.certificate_store.SharedPGPCertificateDirectoryAdapter;
import pgp.cert_d.BaseDirectoryProvider;
import pgp.cert_d.SharedPGPCertificateDirectoryImpl;
@ -12,6 +13,7 @@ import pgp.cert_d.cli.commands.Export;
import pgp.cert_d.cli.commands.Get;
import pgp.cert_d.cli.commands.Insert;
import pgp.cert_d.cli.commands.Import;
import pgp.cert_d.cli.commands.Setup;
import pgp.cert_d.jdbc.sqlite.DatabaseSubkeyLookup;
import pgp.cert_d.jdbc.sqlite.SqliteSubkeyLookupDaoImpl;
import pgp.certificate_store.SubkeyLookup;
@ -26,15 +28,19 @@ import java.sql.SQLException;
name = "certificate-store",
description = "Store and manage public OpenPGP certificates",
subcommands = {
CommandLine.HelpCommand.class,
Export.class,
Insert.class,
Import.class,
Get.class,
Setup.class
}
)
public class PGPCertDCli {
@CommandLine.Option(names = "--base-directory", paramLabel = "DIRECTORY", description = "Overwrite the default certificate directory")
@CommandLine.Option(names = {"-s", "--store"}, paramLabel = "DIRECTORY",
description = "Overwrite the default certificate directory path",
scope = CommandLine.ScopeType.INHERIT)
File baseDirectory;
private static CertificateDirectory certificateDirectory;
@ -57,7 +63,8 @@ public class PGPCertDCli {
certificateDirectory = new SharedPGPCertificateDirectoryImpl(
baseDirectory,
new CertificateReader());
new CertificateReader(),
new KeyReader());
subkeyLookup = new DatabaseSubkeyLookup(
SqliteSubkeyLookupDaoImpl.forDatabaseFile(new File(baseDirectory, "_pgpainless_subkey_map.db")));

View file

@ -1,85 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.cli.commands;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.pgpainless.PGPainless;
import org.pgpainless.certificate_store.CertificateFactory;
import org.pgpainless.key.OpenPgpFingerprint;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.MergeCallback;
import java.io.IOException;
import java.util.Iterator;
public class DefaultMergeCallback implements MergeCallback {
@Override
public Certificate merge(Certificate data, Certificate existing) throws IOException {
try {
PGPPublicKeyRing existingCert = PGPainless.readKeyRing().publicKeyRing(existing.getInputStream());
PGPPublicKeyRing updatedCert = PGPainless.readKeyRing().publicKeyRing(data.getInputStream());
PGPPublicKeyRing mergedCert = PGPPublicKeyRing.join(existingCert, updatedCert);
printOutDifferences(existingCert, mergedCert);
return CertificateFactory.certificateFromPublicKeyRing(mergedCert);
} catch (PGPException e) {
throw new RuntimeException(e);
}
}
private void printOutDifferences(PGPPublicKeyRing existingCert, PGPPublicKeyRing mergedCert) {
int numSigsBefore = countSigs(existingCert);
int numSigsAfter = countSigs(mergedCert);
int newSigs = numSigsAfter - numSigsBefore;
int numUidsBefore = count(existingCert.getPublicKey().getUserIDs());
int numUidsAfter = count(mergedCert.getPublicKey().getUserIDs());
int newUids = numUidsAfter - numUidsBefore;
if (!existingCert.equals(mergedCert)) {
OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(mergedCert);
StringBuilder sb = new StringBuilder();
sb.append(String.format("Certificate %s has", fingerprint));
if (newSigs != 0) {
sb.append(String.format(" %d new signatures", newSigs));
}
if (newUids != 0) {
if (newSigs != 0) {
sb.append(" and");
}
sb.append(String.format(" %d new UIDs", newUids));
}
if (newSigs == 0 && newUids == 0) {
sb.append(" changed");
}
// In this case it is okay to print to stdout, since we are a CLI app
// CHECKSTYLE:OFF
System.out.println(sb);
// CHECKSTYLE:ON
}
}
private static int countSigs(PGPPublicKeyRing keys) {
int numSigs = 0;
for (PGPPublicKey key : keys) {
numSigs += count(key.getSignatures());
}
return numSigs;
}
// TODO: Use CollectionUtils.count() once available
private static int count(Iterator<?> iterator) {
int num = 0;
while (iterator.hasNext()) {
iterator.next();
num++;
}
return num;
}
}

View file

@ -10,9 +10,9 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.pgpainless.PGPainless;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pgp.cert_d.cli.MergeCallbacks;
import pgp.cert_d.cli.PGPCertDCli;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.MergeCallback;
import pgp.certificate_store.exception.BadDataException;
import picocli.CommandLine;
@ -24,7 +24,6 @@ import java.io.IOException;
public class Import implements Runnable {
private static final Logger LOGGER = LoggerFactory.getLogger(Import.class);
private final MergeCallback mergeCallback = new DefaultMergeCallback();
@Override
public void run() {
@ -33,7 +32,7 @@ public class Import implements Runnable {
for (PGPPublicKeyRing cert : certificates) {
ByteArrayInputStream certIn = new ByteArrayInputStream(cert.getEncoded());
Certificate certificate = PGPCertDCli.getCertificateDirectory()
.insertCertificate(certIn, mergeCallback);
.insertCertificate(certIn, MergeCallbacks.mergeCertificates());
}
} catch (IOException e) {
LOGGER.error("IO-Error.", e);

View file

@ -6,9 +6,9 @@ package pgp.cert_d.cli.commands;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pgp.cert_d.cli.MergeCallbacks;
import pgp.cert_d.cli.PGPCertDCli;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.MergeCallback;
import pgp.certificate_store.exception.BadDataException;
import picocli.CommandLine;
@ -19,12 +19,12 @@ import java.io.IOException;
public class Insert implements Runnable {
private static final Logger LOGGER = LoggerFactory.getLogger(Insert.class);
private final MergeCallback mergeCallback = new DefaultMergeCallback();
@Override
public void run() {
try {
Certificate certificate = PGPCertDCli.getCertificateDirectory().insertCertificate(System.in, mergeCallback);
Certificate certificate = PGPCertDCli.getCertificateDirectory()
.insertCertificate(System.in, MergeCallbacks.mergeCertificates());
} catch (IOException e) {
LOGGER.error("IO-Error.", e);
System.exit(-1);

View file

@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.cli.commands;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.key.generation.KeyRingBuilder;
import org.pgpainless.key.generation.KeySpec;
import org.pgpainless.key.generation.type.KeyType;
import org.pgpainless.key.generation.type.eddsa.EdDSACurve;
import org.pgpainless.util.Passphrase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pgp.cert_d.cli.MergeCallbacks;
import pgp.cert_d.cli.PGPCertDCli;
import pgp.certificate_store.exception.BadDataException;
import picocli.CommandLine;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
@CommandLine.Command(name = "setup",
description = "Setup a new certificate directory")
public class Setup implements Runnable {
public static final Logger LOGGER = LoggerFactory.getLogger(Setup.class);
@CommandLine.ArgGroup()
Exclusive exclusive;
static class Exclusive {
@CommandLine.Option(names = "--with-password",
paramLabel = "PASSWORD",
description = "Ask for a password for the trust-root key")
String password;
@CommandLine.Option(names = "--import-from-stdin",
description = "Import trust-root from stdin")
boolean importFromStdin;
}
@Override
public void run() {
PGPSecretKeyRing trustRoot;
if (exclusive == null) {
trustRoot = generateTrustRoot(Passphrase.emptyPassphrase());
} else {
if (exclusive.importFromStdin) {
trustRoot = readTrustRoot(System.in);
} else {
trustRoot = generateTrustRoot(Passphrase.fromPassword(exclusive.password.trim()));
}
}
try {
InputStream inputStream = new ByteArrayInputStream(trustRoot.getEncoded());
PGPCertDCli.getCertificateDirectory().insertTrustRoot(inputStream, MergeCallbacks.overrideKey());
} catch (BadDataException e) {
throw new RuntimeException(e);
} catch (IOException e) {
LOGGER.error("IO error.", e);
System.exit(-1);
} catch (InterruptedException e) {
LOGGER.error("Thread interrupted.", e);
System.exit(-1);
}
}
private PGPSecretKeyRing generateTrustRoot(Passphrase passphrase) {
PGPSecretKeyRing trustRoot;
KeyRingBuilder builder = PGPainless.buildKeyRing()
.addUserId("trust-root");
if (passphrase != null) {
builder.setPassphrase(passphrase);
}
builder.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER));
try {
trustRoot = builder.build();
} catch (NoSuchAlgorithmException | PGPException | InvalidAlgorithmParameterException e) {
throw new RuntimeException("Cannot generate trust-root OpenPGP key", e);
}
return trustRoot;
}
private PGPSecretKeyRing readTrustRoot(InputStream inputStream) {
try {
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(inputStream);
if (secretKeys == null) {
throw new BadDataException();
}
return secretKeys;
} catch (IOException e) {
throw new RuntimeException("Cannot read trust-root OpenPGP key", e);
} catch (BadDataException e) {
throw new RuntimeException("trust-root does not contain OpenPGP key", e);
}
}
}

View file

@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.certificate_store;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.util.encoders.Base64;
import org.pgpainless.PGPainless;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.Key;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class KeyFactory {
public static Key keyFromSecretKeyRing(PGPSecretKeyRing secretKeyRing) {
return new Key() {
@Override
public Certificate getCertificate() {
PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeyRing);
return CertificateFactory.certificateFromPublicKeyRing(publicKeys);
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(secretKeyRing.getEncoded());
}
@Override
public String getTag() throws IOException {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("No MessageDigest for SHA-256 instantiated, although BC is on the classpath: " + e.getMessage());
}
digest.update(secretKeyRing.getEncoded());
return Base64.toBase64String(digest.digest());
}
};
}
}

View file

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.certificate_store;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.pgpainless.PGPainless;
import pgp.certificate_store.Key;
import pgp.certificate_store.KeyReaderBackend;
import pgp.certificate_store.exception.BadDataException;
import java.io.IOException;
import java.io.InputStream;
public class KeyReader implements KeyReaderBackend {
@Override
public Key readKey(InputStream data) throws IOException, BadDataException {
final PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(data);
return KeyFactory.keyFromSecretKeyRing(key);
}
}

View file

@ -15,8 +15,10 @@ import pgp.cert_d.SharedPGPCertificateDirectory;
import pgp.cert_d.SpecialNames;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.CertificateDirectory;
import pgp.certificate_store.CertificateMerger;
import pgp.certificate_store.CertificateStore;
import pgp.certificate_store.MergeCallback;
import pgp.certificate_store.Key;
import pgp.certificate_store.KeyMerger;
import pgp.certificate_store.SubkeyLookup;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
@ -66,7 +68,7 @@ public class SharedPGPCertificateDirectoryAdapter
}
@Override
public Certificate insertCertificate(InputStream data, MergeCallback merge)
public Certificate insertCertificate(InputStream data, CertificateMerger merge)
throws IOException, InterruptedException, BadDataException {
Certificate certificate = directory.insert(data, merge);
storeIdentifierForSubkeys(certificate);
@ -74,7 +76,7 @@ public class SharedPGPCertificateDirectoryAdapter
}
@Override
public Certificate tryInsertCertificate(InputStream data, MergeCallback merge)
public Certificate tryInsertCertificate(InputStream data, CertificateMerger merge)
throws IOException, BadDataException {
Certificate certificate = directory.tryInsert(data, merge);
storeIdentifierForSubkeys(certificate);
@ -82,13 +84,13 @@ public class SharedPGPCertificateDirectoryAdapter
}
@Override
public Certificate insertCertificateBySpecialName(String specialName, InputStream data, MergeCallback merge)
public Certificate insertCertificateBySpecialName(String specialName, InputStream data, CertificateMerger merge)
throws IOException, InterruptedException, BadDataException, BadNameException {
return directory.insertWithSpecialName(specialName, data, merge);
}
@Override
public Certificate tryInsertCertificateBySpecialName(String specialName, InputStream data, MergeCallback merge)
public Certificate tryInsertCertificateBySpecialName(String specialName, InputStream data, CertificateMerger merge)
throws IOException, BadDataException, BadNameException {
return directory.tryInsertWithSpecialName(specialName, data, merge);
}
@ -120,4 +122,24 @@ public class SharedPGPCertificateDirectoryAdapter
public void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) throws IOException {
subkeyLookup.storeCertificateSubkeyIds(certificate, subkeyIds);
}
@Override
public Key getTrustRoot() throws IOException, BadDataException {
return directory.getTrustRoot();
}
@Override
public Key getTrustRootIfChanged(String tag) throws IOException, BadDataException {
return directory.getTrustRootIfChanged(tag);
}
@Override
public Key insertTrustRoot(InputStream data, KeyMerger keyMerger) throws IOException, InterruptedException, BadDataException {
return directory.insertTrustRoot(data, keyMerger);
}
@Override
public Key tryInsertTrustRoot(InputStream data, KeyMerger keyMerger) throws IOException, BadDataException {
return directory.tryInsertTrustRoot(data, keyMerger);
}
}

View file

@ -27,6 +27,7 @@ import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.pgpainless.certificate_store.CertificateReader;
import org.pgpainless.certificate_store.KeyReader;
import org.pgpainless.certificate_store.SharedPGPCertificateDirectoryAdapter;
import pgp.cert_d.InMemorySubkeyLookup;
import pgp.cert_d.SharedPGPCertificateDirectoryImpl;
@ -50,7 +51,7 @@ public class SharedPGPCertificateDirectoryAdapterTest {
@BeforeEach
public void setupInstance() throws IOException, NotAStoreException {
adapter = new SharedPGPCertificateDirectoryAdapter(
new SharedPGPCertificateDirectoryImpl(tempDir(), new CertificateReader()),
new SharedPGPCertificateDirectoryImpl(tempDir(), new CertificateReader(), new KeyReader()),
new InMemorySubkeyLookup());
store = adapter;
}

View file

@ -33,6 +33,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.certificate_store.CertificateReader;
import org.pgpainless.certificate_store.KeyReader;
import org.pgpainless.key.OpenPgpFingerprint;
import org.pgpainless.key.generation.KeySpec;
import org.pgpainless.key.generation.type.KeyType;
@ -41,15 +42,15 @@ import pgp.cert_d.CachingSharedPGPCertificateDirectoryWrapper;
import pgp.cert_d.FileLockingMechanism;
import pgp.cert_d.SharedPGPCertificateDirectory;
import pgp.cert_d.SharedPGPCertificateDirectoryImpl;
import pgp.certificate_store.CertificateMerger;
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.MergeCallback;
public class SharedPGPCertificateDirectoryTest {
private static MergeCallback dummyMerge = new MergeCallback() {
private static CertificateMerger dummyMerge = new CertificateMerger() {
@Override
public Certificate merge(Certificate data, Certificate existing) {
return data;
@ -58,9 +59,9 @@ public class SharedPGPCertificateDirectoryTest {
private static Stream<SharedPGPCertificateDirectory> provideTestSubjects() throws IOException, NotAStoreException {
return Stream.of(
new SharedPGPCertificateDirectoryImpl(tempDir(), new CertificateReader()),
new SharedPGPCertificateDirectoryImpl(tempDir(), new CertificateReader(), new KeyReader()),
new CachingSharedPGPCertificateDirectoryWrapper(
new SharedPGPCertificateDirectoryImpl(tempDir(), new CertificateReader()))
new SharedPGPCertificateDirectoryImpl(tempDir(), new CertificateReader(), new KeyReader()))
);
}

View file

@ -13,7 +13,7 @@ allprojects {
junitVersion = '5.8.2'
mockitoVersion = '4.5.1'
pgpainlessVersion = '1.2.1'
pgpCertDJavaVersion = '0.1.1'
pgpCertDJavaVersion = '0.1.2-SNAPSHOT'
picocliVersion = '4.6.3'
}
}