Implement storing of trust-root keys

This commit is contained in:
Paul Schaub 2022-07-04 19:42:02 +02:00
parent 304d6c29e4
commit 17d2f45e83
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
7 changed files with 192 additions and 19 deletions

View file

@ -6,11 +6,14 @@ package pgp.cert_d;
import pgp.certificate_store.CertificateReaderBackend; import pgp.certificate_store.CertificateReaderBackend;
import pgp.certificate_store.CertificateMerger; import pgp.certificate_store.CertificateMerger;
import pgp.certificate_store.KeyReaderBackend;
public abstract class BackendProvider { public abstract class BackendProvider {
public abstract CertificateReaderBackend provideCertificateReaderBackend(); public abstract CertificateReaderBackend provideCertificateReaderBackend();
public abstract KeyReaderBackend provideKeyReaderBackend();
public abstract CertificateMerger provideDefaultMergeCallback(); public abstract CertificateMerger provideDefaultMergeCallback();
} }

View file

@ -4,6 +4,8 @@
package pgp.cert_d; 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.BadDataException;
import pgp.certificate_store.exception.BadNameException; import pgp.certificate_store.exception.BadNameException;
import pgp.certificate_store.Certificate; import pgp.certificate_store.Certificate;
@ -21,8 +23,10 @@ import java.util.Map;
public class CachingSharedPGPCertificateDirectoryWrapper public class CachingSharedPGPCertificateDirectoryWrapper
implements SharedPGPCertificateDirectory { implements SharedPGPCertificateDirectory {
private static final Map<String, String> tagMap = new HashMap<>(); private static final Map<String, String> certTagMap = new HashMap<>();
private static final Map<String, String> keyTagMap = new HashMap<>();
private static final Map<String, Certificate> certificateMap = new HashMap<>(); private static final Map<String, Certificate> certificateMap = new HashMap<>();
private static final Map<String, Key> keyMap = new HashMap<>();
private final SharedPGPCertificateDirectory underlyingCertificateDirectory; private final SharedPGPCertificateDirectory underlyingCertificateDirectory;
public CachingSharedPGPCertificateDirectoryWrapper(SharedPGPCertificateDirectory wrapped) { public CachingSharedPGPCertificateDirectoryWrapper(SharedPGPCertificateDirectory wrapped) {
@ -38,12 +42,26 @@ public class CachingSharedPGPCertificateDirectoryWrapper
private void remember(String identifier, Certificate certificate) { private void remember(String identifier, Certificate certificate) {
certificateMap.put(identifier, certificate); certificateMap.put(identifier, certificate);
try { try {
tagMap.put(identifier, certificate.getTag()); certTagMap.put(identifier, certificate.getTag());
} catch (IOException e) { } 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. * Returns true, if the cached tag differs from the provided tag.
* *
@ -51,8 +69,13 @@ public class CachingSharedPGPCertificateDirectoryWrapper
* @param tag tag * @param tag tag
* @return true if cached tag differs, false otherwise * @return true if cached tag differs, false otherwise
*/ */
private boolean tagChanged(String identifier, String tag) { private boolean certTagChanged(String identifier, String tag) {
String tack = tagMap.get(identifier); 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); return !tagEquals(tag, tack);
} }
@ -72,7 +95,7 @@ public class CachingSharedPGPCertificateDirectoryWrapper
*/ */
public void invalidate() { public void invalidate() {
certificateMap.clear(); certificateMap.clear();
tagMap.clear(); certTagMap.clear();
} }
@Override @Override
@ -111,7 +134,7 @@ public class CachingSharedPGPCertificateDirectoryWrapper
@Override @Override
public Certificate getByFingerprintIfChanged(String fingerprint, String tag) public Certificate getByFingerprintIfChanged(String fingerprint, String tag)
throws IOException, BadNameException, BadDataException { throws IOException, BadNameException, BadDataException {
if (tagChanged(fingerprint, tag)) { if (certTagChanged(fingerprint, tag)) {
return getByFingerprint(fingerprint); return getByFingerprint(fingerprint);
} }
return null; return null;
@ -120,12 +143,32 @@ public class CachingSharedPGPCertificateDirectoryWrapper
@Override @Override
public Certificate getBySpecialNameIfChanged(String specialName, String tag) public Certificate getBySpecialNameIfChanged(String specialName, String tag)
throws IOException, BadNameException, BadDataException { throws IOException, BadNameException, BadDataException {
if (tagChanged(specialName, tag)) { if (certTagChanged(specialName, tag)) {
return getBySpecialName(specialName); return getBySpecialName(specialName);
} }
return null; 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 @Override
public Certificate insert(InputStream data, CertificateMerger merge) public Certificate insert(InputStream data, CertificateMerger merge)
throws IOException, BadDataException, InterruptedException { throws IOException, BadDataException, InterruptedException {
@ -144,6 +187,22 @@ public class CachingSharedPGPCertificateDirectoryWrapper
return certificate; 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 @Override
public Certificate insertWithSpecialName(String specialName, InputStream data, CertificateMerger merge) public Certificate insertWithSpecialName(String specialName, InputStream data, CertificateMerger merge)
throws IOException, BadDataException, BadNameException, InterruptedException { throws IOException, BadDataException, BadNameException, InterruptedException {

View file

@ -60,6 +60,24 @@ public class FilenameResolver {
return new File(getBaseDirectory(), specialName); 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) { private boolean isFingerprint(String fingerprint) {
return openPgpV4FingerprintPattern.matcher(fingerprint).matches(); return openPgpV4FingerprintPattern.matcher(fingerprint).matches();
} }

View file

@ -8,6 +8,8 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Iterator; 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.BadDataException;
import pgp.certificate_store.exception.BadNameException; import pgp.certificate_store.exception.BadNameException;
import pgp.certificate_store.Certificate; import pgp.certificate_store.Certificate;
@ -23,6 +25,18 @@ public interface SharedPGPCertificateDirectory {
Certificate getBySpecialName(String specialName) Certificate getBySpecialName(String specialName)
throws IOException, BadNameException, BadDataException; 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) Certificate getByFingerprintIfChanged(String fingerprint, String tag)
throws IOException, BadNameException, BadDataException; throws IOException, BadNameException, BadDataException;

View file

@ -16,6 +16,9 @@ import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; 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.BadDataException;
import pgp.certificate_store.exception.BadNameException; import pgp.certificate_store.exception.BadNameException;
import pgp.certificate_store.exception.NotAStoreException; import pgp.certificate_store.exception.NotAStoreException;
@ -28,33 +31,41 @@ public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDi
private final FilenameResolver resolver; private final FilenameResolver resolver;
private final LockingMechanism writeLock; private final LockingMechanism writeLock;
private final CertificateReaderBackend certificateReaderBackend; private final CertificateReaderBackend certificateReaderBackend;
private final KeyReaderBackend keyReaderBackend;
public SharedPGPCertificateDirectoryImpl(BackendProvider backendProvider) public SharedPGPCertificateDirectoryImpl(BackendProvider backendProvider)
throws NotAStoreException { throws NotAStoreException {
this(backendProvider.provideCertificateReaderBackend()); this(backendProvider.provideCertificateReaderBackend(), backendProvider.provideKeyReaderBackend());
} }
public SharedPGPCertificateDirectoryImpl(CertificateReaderBackend certificateReaderBackend) public SharedPGPCertificateDirectoryImpl(CertificateReaderBackend certificateReaderBackend,
KeyReaderBackend keyReaderBackend)
throws NotAStoreException { throws NotAStoreException {
this( this(
BaseDirectoryProvider.getDefaultBaseDir(), BaseDirectoryProvider.getDefaultBaseDir(),
certificateReaderBackend); certificateReaderBackend,
keyReaderBackend);
} }
public SharedPGPCertificateDirectoryImpl(File baseDirectory, CertificateReaderBackend certificateReaderBackend) public SharedPGPCertificateDirectoryImpl(File baseDirectory,
CertificateReaderBackend certificateReaderBackend,
KeyReaderBackend keyReaderBackend)
throws NotAStoreException { throws NotAStoreException {
this( this(
certificateReaderBackend, certificateReaderBackend,
keyReaderBackend,
new FilenameResolver(baseDirectory), new FilenameResolver(baseDirectory),
FileLockingMechanism.defaultDirectoryFileLock(baseDirectory)); FileLockingMechanism.defaultDirectoryFileLock(baseDirectory));
} }
public SharedPGPCertificateDirectoryImpl( public SharedPGPCertificateDirectoryImpl(
CertificateReaderBackend certificateReaderBackend, CertificateReaderBackend certificateReaderBackend,
KeyReaderBackend keyReaderBackend,
FilenameResolver filenameResolver, FilenameResolver filenameResolver,
LockingMechanism writeLock) LockingMechanism writeLock)
throws NotAStoreException { throws NotAStoreException {
this.certificateReaderBackend = certificateReaderBackend; this.certificateReaderBackend = certificateReaderBackend;
this.keyReaderBackend = keyReaderBackend;
this.resolver = filenameResolver; this.resolver = filenameResolver;
this.writeLock = writeLock; this.writeLock = writeLock;
@ -130,6 +141,36 @@ public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDi
return certificate; 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 @Override
public Certificate insert(InputStream data, CertificateMerger merge) public Certificate insert(InputStream data, CertificateMerger merge)
throws IOException, BadDataException, InterruptedException { throws IOException, BadDataException, InterruptedException {
@ -170,28 +211,63 @@ public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDi
newCertificate = merge.merge(newCertificate, existingCertificate); newCertificate = merge.merge(newCertificate, existingCertificate);
} }
writeCertificate(newCertificate, certFile); writeToFile(newCertificate.getInputStream(), certFile);
return newCertificate; 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 { throws IOException {
certFile.getParentFile().mkdirs(); certFile.getParentFile().mkdirs();
if (!certFile.exists() && !certFile.createNewFile()) { if (!certFile.exists() && !certFile.createNewFile()) {
throw new IOException("Could not create cert file " + certFile.getAbsolutePath()); throw new IOException("Could not create cert file " + certFile.getAbsolutePath());
} }
InputStream certIn = certificate.getInputStream();
FileOutputStream fileOut = new FileOutputStream(certFile); FileOutputStream fileOut = new FileOutputStream(certFile);
byte[] buffer = new byte[4096]; byte[] buffer = new byte[4096];
int read; int read;
while ((read = certIn.read(buffer)) != -1) { while ((read = inputStream.read(buffer)) != -1) {
fileOut.write(buffer, 0, read); fileOut.write(buffer, 0, read);
} }
certIn.close(); inputStream.close();
fileOut.close(); fileOut.close();
} }
@ -229,7 +305,7 @@ public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDi
newCertificate = merge.merge(newCertificate, existingCertificate); newCertificate = merge.merge(newCertificate, existingCertificate);
} }
writeCertificate(newCertificate, certFile); writeToFile(newCertificate.getInputStream(), certFile);
return newCertificate; return newCertificate;
} }

View file

@ -8,6 +8,9 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Set; import java.util.Set;
/**
* OpenPGP certificate (public key).
*/
public abstract class Certificate { public abstract class Certificate {
/** /**
* Return the fingerprint of the certificate as 40 lowercase hex characters. * Return the fingerprint of the certificate as 40 lowercase hex characters.

View file

@ -4,6 +4,6 @@
package pgp.certificate_store; package pgp.certificate_store;
public interface CertificateStore extends CertificateDirectory, SubkeyLookup { public interface CertificateStore extends CertificateDirectory, SubkeyLookup, TrustRootStore {
} }