From 17d2f45e83b74ccc330680635eba735a38814490 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Jul 2022 19:42:02 +0200 Subject: [PATCH] Implement storing of trust-root keys --- .../main/java/pgp/cert_d/BackendProvider.java | 3 + ...gSharedPGPCertificateDirectoryWrapper.java | 75 +++++++++++++-- .../java/pgp/cert_d/FilenameResolver.java | 18 ++++ .../cert_d/SharedPGPCertificateDirectory.java | 14 +++ .../SharedPGPCertificateDirectoryImpl.java | 96 +++++++++++++++++-- .../pgp/certificate_store/Certificate.java | 3 + .../certificate_store/CertificateStore.java | 2 +- 7 files changed, 192 insertions(+), 19 deletions(-) diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/BackendProvider.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/BackendProvider.java index be0d710..66df316 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/BackendProvider.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/BackendProvider.java @@ -6,11 +6,14 @@ package pgp.cert_d; import pgp.certificate_store.CertificateReaderBackend; import pgp.certificate_store.CertificateMerger; +import pgp.certificate_store.KeyReaderBackend; public abstract class BackendProvider { public abstract CertificateReaderBackend provideCertificateReaderBackend(); + public abstract KeyReaderBackend provideKeyReaderBackend(); + public abstract CertificateMerger provideDefaultMergeCallback(); } diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/CachingSharedPGPCertificateDirectoryWrapper.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/CachingSharedPGPCertificateDirectoryWrapper.java index 332ebd6..d8bb635 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/CachingSharedPGPCertificateDirectoryWrapper.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/CachingSharedPGPCertificateDirectoryWrapper.java @@ -4,6 +4,8 @@ package pgp.cert_d; +import pgp.certificate_store.Key; +import pgp.certificate_store.KeyMerger; import pgp.certificate_store.exception.BadDataException; import pgp.certificate_store.exception.BadNameException; import pgp.certificate_store.Certificate; @@ -21,8 +23,10 @@ import java.util.Map; public class CachingSharedPGPCertificateDirectoryWrapper implements SharedPGPCertificateDirectory { - private static final Map tagMap = new HashMap<>(); + private static final Map certTagMap = new HashMap<>(); + private static final Map keyTagMap = new HashMap<>(); private static final Map certificateMap = new HashMap<>(); + private static final Map keyMap = new HashMap<>(); private final SharedPGPCertificateDirectory underlyingCertificateDirectory; public CachingSharedPGPCertificateDirectoryWrapper(SharedPGPCertificateDirectory wrapped) { @@ -38,12 +42,26 @@ public class CachingSharedPGPCertificateDirectoryWrapper private void remember(String identifier, Certificate certificate) { certificateMap.put(identifier, certificate); try { - tagMap.put(identifier, certificate.getTag()); + certTagMap.put(identifier, certificate.getTag()); } catch (IOException e) { - tagMap.put(identifier, null); + certTagMap.put(identifier, null); } } + /** + * Store the given key under the given identifier into the cache. + * + * @param identifier fingerprint or special name + * @param key key + */ + private void remember(String identifier, Key key) { + keyMap.put(identifier, key); + try { + keyTagMap.put(identifier, key.getTag()); + } catch (IOException e) { + keyTagMap.put(identifier, null); + } + } /** * Returns true, if the cached tag differs from the provided tag. * @@ -51,8 +69,13 @@ public class CachingSharedPGPCertificateDirectoryWrapper * @param tag tag * @return true if cached tag differs, false otherwise */ - private boolean tagChanged(String identifier, String tag) { - String tack = tagMap.get(identifier); + private boolean certTagChanged(String identifier, String tag) { + String tack = certTagMap.get(identifier); + return !tagEquals(tag, tack); + } + + private boolean keyTagChanged(String identifier, String tag) { + String tack = keyTagMap.get(identifier); return !tagEquals(tag, tack); } @@ -72,7 +95,7 @@ public class CachingSharedPGPCertificateDirectoryWrapper */ public void invalidate() { certificateMap.clear(); - tagMap.clear(); + certTagMap.clear(); } @Override @@ -111,7 +134,7 @@ public class CachingSharedPGPCertificateDirectoryWrapper @Override public Certificate getByFingerprintIfChanged(String fingerprint, String tag) throws IOException, BadNameException, BadDataException { - if (tagChanged(fingerprint, tag)) { + if (certTagChanged(fingerprint, tag)) { return getByFingerprint(fingerprint); } return null; @@ -120,12 +143,32 @@ public class CachingSharedPGPCertificateDirectoryWrapper @Override public Certificate getBySpecialNameIfChanged(String specialName, String tag) throws IOException, BadNameException, BadDataException { - if (tagChanged(specialName, tag)) { + if (certTagChanged(specialName, tag)) { return getBySpecialName(specialName); } return null; } + @Override + public Key getTrustRoot() throws IOException, BadDataException { + Key key = keyMap.get(SpecialNames.TRUST_ROOT); + if (key == null) { + key = underlyingCertificateDirectory.getTrustRoot(); + if (key != null) { + remember(SpecialNames.TRUST_ROOT, key); + } + } + return key; + } + + @Override + public Key getTrustRootIfChanged(String tag) throws IOException, BadDataException { + if (keyTagChanged(SpecialNames.TRUST_ROOT, tag)) { + return getTrustRoot(); + } + return null; + } + @Override public Certificate insert(InputStream data, CertificateMerger merge) throws IOException, BadDataException, InterruptedException { @@ -144,6 +187,22 @@ public class CachingSharedPGPCertificateDirectoryWrapper return certificate; } + @Override + public Key insertTrustRoot(InputStream data, KeyMerger merge) throws IOException, BadDataException, InterruptedException { + Key key = underlyingCertificateDirectory.insertTrustRoot(data, merge); + remember(SpecialNames.TRUST_ROOT, key); + return key; + } + + @Override + public Key tryInsertTrustRoot(InputStream data, KeyMerger merge) throws IOException, BadDataException { + Key key = underlyingCertificateDirectory.tryInsertTrustRoot(data, merge); + if (key != null) { + remember(SpecialNames.TRUST_ROOT, key); + } + return key; + } + @Override public Certificate insertWithSpecialName(String specialName, InputStream data, CertificateMerger merge) throws IOException, BadDataException, BadNameException, InterruptedException { diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/FilenameResolver.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/FilenameResolver.java index d27fce9..0bbdbc1 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/FilenameResolver.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/FilenameResolver.java @@ -60,6 +60,24 @@ public class FilenameResolver { return new File(getBaseDirectory(), specialName); } + /** + * Calculate the file location for the key addressed using the given special name. + * For known special names, see {@link SpecialNames}. + * + * @param specialName special name (e.g. "trust-root") + * @return absolute key file location + * + * @throws BadNameException in case the given special name is not known + */ + public File getKeyFileBySpecialName(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 + ".key"); + } + private boolean isFingerprint(String fingerprint) { return openPgpV4FingerprintPattern.matcher(fingerprint).matches(); } diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectory.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectory.java index 7952529..b6b83f4 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectory.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectory.java @@ -8,6 +8,8 @@ import java.io.IOException; import java.io.InputStream; import java.util.Iterator; +import pgp.certificate_store.Key; +import pgp.certificate_store.KeyMerger; import pgp.certificate_store.exception.BadDataException; import pgp.certificate_store.exception.BadNameException; import pgp.certificate_store.Certificate; @@ -23,6 +25,18 @@ public interface SharedPGPCertificateDirectory { Certificate getBySpecialName(String specialName) throws IOException, BadNameException, BadDataException; + Key getTrustRoot() + throws IOException, BadDataException; + + Key getTrustRootIfChanged(String tag) + throws IOException, BadDataException; + + Key insertTrustRoot(InputStream data, KeyMerger merge) + throws IOException, BadDataException, InterruptedException; + + Key tryInsertTrustRoot(InputStream data, KeyMerger merge) + throws IOException, BadDataException; + Certificate getByFingerprintIfChanged(String fingerprint, String tag) throws IOException, BadNameException, BadDataException; diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectoryImpl.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectoryImpl.java index c46d1f6..3e8ebe9 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectoryImpl.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectoryImpl.java @@ -16,6 +16,9 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import pgp.certificate_store.Key; +import pgp.certificate_store.KeyMerger; +import pgp.certificate_store.KeyReaderBackend; import pgp.certificate_store.exception.BadDataException; import pgp.certificate_store.exception.BadNameException; import pgp.certificate_store.exception.NotAStoreException; @@ -28,33 +31,41 @@ public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDi private final FilenameResolver resolver; private final LockingMechanism writeLock; private final CertificateReaderBackend certificateReaderBackend; + private final KeyReaderBackend keyReaderBackend; public SharedPGPCertificateDirectoryImpl(BackendProvider backendProvider) throws NotAStoreException { - this(backendProvider.provideCertificateReaderBackend()); + this(backendProvider.provideCertificateReaderBackend(), backendProvider.provideKeyReaderBackend()); } - public SharedPGPCertificateDirectoryImpl(CertificateReaderBackend certificateReaderBackend) + public SharedPGPCertificateDirectoryImpl(CertificateReaderBackend certificateReaderBackend, + KeyReaderBackend keyReaderBackend) throws NotAStoreException { this( BaseDirectoryProvider.getDefaultBaseDir(), - certificateReaderBackend); + certificateReaderBackend, + keyReaderBackend); } - public SharedPGPCertificateDirectoryImpl(File baseDirectory, CertificateReaderBackend certificateReaderBackend) + public SharedPGPCertificateDirectoryImpl(File baseDirectory, + CertificateReaderBackend certificateReaderBackend, + KeyReaderBackend keyReaderBackend) throws NotAStoreException { this( certificateReaderBackend, + keyReaderBackend, new FilenameResolver(baseDirectory), FileLockingMechanism.defaultDirectoryFileLock(baseDirectory)); } public SharedPGPCertificateDirectoryImpl( CertificateReaderBackend certificateReaderBackend, + KeyReaderBackend keyReaderBackend, FilenameResolver filenameResolver, LockingMechanism writeLock) throws NotAStoreException { this.certificateReaderBackend = certificateReaderBackend; + this.keyReaderBackend = keyReaderBackend; this.resolver = filenameResolver; this.writeLock = writeLock; @@ -130,6 +141,36 @@ public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDi return certificate; } + @Override + public Key getTrustRoot() throws IOException, BadDataException { + File keyFile; + try { + keyFile = resolver.getKeyFileBySpecialName(SpecialNames.TRUST_ROOT); + } catch (BadNameException e) { + throw new RuntimeException("trust-root MUST be a known special name", e); + } + + if (!keyFile.exists()) { + return null; + } + FileInputStream fileIn = new FileInputStream(keyFile); + BufferedInputStream bufferedInputStream = new BufferedInputStream(fileIn); + Key key = keyReaderBackend.readKey(bufferedInputStream); + + return key; + } + + @Override + public Key getTrustRootIfChanged(String tag) throws IOException, BadDataException { + // TODO: The tag is likely intended for performance improvements, + // so really we should look it up somewhere without the need to parse the whole key. + Key key = getTrustRoot(); + if (key.getTag().equals(tag)) { + return null; + } + return key; + } + @Override public Certificate insert(InputStream data, CertificateMerger merge) throws IOException, BadDataException, InterruptedException { @@ -170,28 +211,63 @@ public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDi newCertificate = merge.merge(newCertificate, existingCertificate); } - writeCertificate(newCertificate, certFile); + writeToFile(newCertificate.getInputStream(), certFile); return newCertificate; } - private void writeCertificate(Certificate certificate, File certFile) + private Key _insertTrustRoot(InputStream data, KeyMerger merge) + throws IOException, BadDataException { + Key newKey = keyReaderBackend.readKey(data); + Key existingKey; + File keyFile; + try { + existingKey = getTrustRoot(); + keyFile = resolver.getKeyFileBySpecialName(SpecialNames.TRUST_ROOT); + } catch (BadNameException e) { + throw new RuntimeException(String.format("trust-root MUST be known special name.", e)); + } + + if (existingKey != null && !existingKey.getTag().equals(newKey.getTag())) { + newKey = merge.merge(newKey, existingKey); + } + + writeToFile(newKey.getInputStream(), keyFile); + + return newKey; + } + + @Override + public Key insertTrustRoot(InputStream data, KeyMerger merge) throws IOException, BadDataException, InterruptedException { + writeLock.lockDirectory(); + + Key key = _insertTrustRoot(data, merge); + + writeLock.releaseDirectory(); + return key; + } + + @Override + public Key tryInsertTrustRoot(InputStream data, KeyMerger merge) throws IOException, BadDataException { + return null; + } + + private void 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()); } - InputStream certIn = certificate.getInputStream(); FileOutputStream fileOut = new FileOutputStream(certFile); byte[] buffer = new byte[4096]; int read; - while ((read = certIn.read(buffer)) != -1) { + while ((read = inputStream.read(buffer)) != -1) { fileOut.write(buffer, 0, read); } - certIn.close(); + inputStream.close(); fileOut.close(); } @@ -229,7 +305,7 @@ public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDi newCertificate = merge.merge(newCertificate, existingCertificate); } - writeCertificate(newCertificate, certFile); + writeToFile(newCertificate.getInputStream(), certFile); return newCertificate; } diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/Certificate.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/Certificate.java index ea3f363..530e51c 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/Certificate.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/Certificate.java @@ -8,6 +8,9 @@ import java.io.IOException; import java.io.InputStream; import java.util.Set; +/** + * OpenPGP certificate (public key). + */ public abstract class Certificate { /** * Return the fingerprint of the certificate as 40 lowercase hex characters. diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateStore.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateStore.java index a8325ee..594e3f6 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateStore.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateStore.java @@ -4,6 +4,6 @@ package pgp.certificate_store; -public interface CertificateStore extends CertificateDirectory, SubkeyLookup { +public interface CertificateStore extends CertificateDirectory, SubkeyLookup, TrustRootStore { }