Modularize WKD discovery

This commit is contained in:
Paul Schaub 2022-03-10 16:56:46 +01:00
parent 30e8a55ef6
commit d1d953e802
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
24 changed files with 511 additions and 111 deletions

View file

@ -20,7 +20,7 @@ dependencies {
testImplementation 'com.ginsberg:junit5-system-exit:1.1.2'
testImplementation 'org.mockito:mockito-core:4.3.1'
implementation("org.pgpainless:pgpainless-core:$pgpainlessVersion")
implementation("org.pgpainless:pgpainless-cert-d:0.1.0")
implementation project(':wkd-java')
implementation "info.picocli:picocli:4.6.3"

View file

@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.wkd.cli;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.pgpainless.PGPainless;
import org.pgpainless.certificate_store.CertificateFactory;
import org.pgpainless.key.info.KeyRingInfo;
import pgp.certificate_store.Certificate;
import pgp.wkd.CertificateAndUserIds;
import pgp.wkd.CertificateReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class CertificateReaderImpl implements CertificateReader {
@Override
public List<CertificateAndUserIds> read(InputStream inputStream) throws IOException {
List<CertificateAndUserIds> certificatesAndUserIds = new ArrayList<>();
try {
PGPPublicKeyRingCollection certificates = PGPainless.readKeyRing().publicKeyRingCollection(inputStream);
for (PGPPublicKeyRing certificate : certificates) {
KeyRingInfo info = PGPainless.inspectKeyRing(certificate);
Certificate parsedCert = CertificateFactory.certificateFromPublicKeyRing(certificate);
List<String> userIds = info.getValidAndExpiredUserIds();
certificatesAndUserIds.add(new CertificateAndUserIds(parsedCert, userIds));
}
return certificatesAndUserIds;
} catch (PGPException e) {
throw new IOException("Cannot parse certificates.", e);
}
}
}

View file

@ -0,0 +1,21 @@
package pgp.wkd.cli;
import pgp.wkd.AbstractDiscover;
import pgp.wkd.CertificateReader;
import pgp.wkd.HttpUrlConnectionWKDFetcher;
import pgp.wkd.WKDFetcher;
public class DiscoverImpl extends AbstractDiscover {
public DiscoverImpl() {
super(new CertificateReaderImpl(), new HttpUrlConnectionWKDFetcher());
}
public DiscoverImpl(WKDFetcher fetcher) {
super(new CertificateReaderImpl(), fetcher);
}
public DiscoverImpl(CertificateReader certificateReader, WKDFetcher fetcher) {
super(certificateReader, fetcher);
}
}

View file

@ -1,18 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.wkd.cli;
/**
* Exception that gets thrown when an OpenPGP certificate is not carrying a User-ID binding for the email address
* that was used to look the certificate up via WKD.
*/
public class MissingUserIdException extends RuntimeException {
public static final int ERROR_CODE = 7;
public MissingUserIdException(String message) {
super(message);
}
}

View file

@ -4,6 +4,7 @@
package pgp.wkd.cli;
import pgp.wkd.MissingUserIdException;
import pgp.wkd.cli.command.Fetch;
import picocli.CommandLine;

View file

@ -5,21 +5,21 @@
package pgp.wkd.cli.command;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.pgpainless.PGPainless;
import org.pgpainless.key.info.KeyRingInfo;
import pgp.wkd.AbstractWKDFetcher;
import org.bouncycastle.util.io.Streams;
import pgp.certificate_store.Certificate;
import pgp.wkd.Discover;
import pgp.wkd.HttpUrlConnectionWKDFetcher;
import pgp.wkd.MalformedUserIdException;
import pgp.wkd.WKDAddress;
import pgp.wkd.WKDAddressHelper;
import pgp.wkd.WKDDiscoveryResult;
import pgp.wkd.WKDFetcher;
import pgp.wkd.cli.CertNotFetchableException;
import pgp.wkd.cli.MissingUserIdException;
import pgp.wkd.cli.DiscoverImpl;
import picocli.CommandLine;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
@CommandLine.Command(
name = "fetch",
@ -42,47 +42,44 @@ public class Fetch implements Runnable {
boolean armor = false;
// TODO: Better way to inject fetcher implementation
public static AbstractWKDFetcher fetcher = new HttpUrlConnectionWKDFetcher();
public static WKDFetcher fetcher = new HttpUrlConnectionWKDFetcher();
@Override
public void run() {
String email;
try {
email = WKDAddressHelper.emailFromUserId(userId);
} catch (IllegalArgumentException e) {
email = userId;
Discover discover = new DiscoverImpl(fetcher);
WKDAddress address = addressFromUserId(userId);
WKDDiscoveryResult result = discover.discover(address);
if (!result.isSuccessful()) {
throw new CertNotFetchableException("Cannot fetch cert.");
}
WKDAddress address = WKDAddress.fromEmail(email);
try (InputStream inputStream = fetcher.fetch(address)) {
PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(inputStream);
if (cert == null) {
throw new CertNotFetchableException("Fetched data does not contain an OpenPGP certificate.");
}
KeyRingInfo info = PGPainless.inspectKeyRing(cert);
List<String> userIds = info.getValidAndExpiredUserIds();
boolean containsEmail = false;
for (String certUserId : userIds) {
if (certUserId.contains("<" + email + ">") || certUserId.equals(email)) {
containsEmail = true;
break;
}
}
if (!containsEmail) {
throw new MissingUserIdException("Fetched certificate does not contain email address " + email);
}
try {
if (armor) {
OutputStream out = new ArmoredOutputStream(System.out);
cert.encode(out);
for (Certificate certificate : result.getCertificates()) {
Streams.pipeAll(certificate.getInputStream(), out);
}
out.close();
} else {
cert.encode(System.out);
for (Certificate certificate : result.getCertificates()) {
Streams.pipeAll(certificate.getInputStream(), System.out);
}
}
} catch (IOException e) {
throw new CertNotFetchableException("Certificate cannot be fetched.", e);
}
}
private WKDAddress addressFromUserId(String userId) {
String email;
try {
email = WKDAddressHelper.emailFromUserId(userId);
} catch (MalformedUserIdException e) {
email = userId;
}
return WKDAddress.fromEmail(email);
}
}

View file

@ -4,7 +4,9 @@
package pgp.wkd.cli.test_suite;
import pgp.wkd.AbstractWKDFetcher;
import pgp.wkd.DiscoveryMethod;
import pgp.wkd.WKDAddress;
import pgp.wkd.WKDFetcher;
import java.io.File;
import java.io.FileInputStream;
@ -13,7 +15,7 @@ import java.io.InputStream;
import java.net.URI;
import java.nio.file.Path;
public class DirectoryBasedWkdFetcher extends AbstractWKDFetcher {
public class DirectoryBasedWkdFetcher implements WKDFetcher {
// The directory containing the .well-known subdirectory
private final Path rootPath;
@ -23,7 +25,8 @@ public class DirectoryBasedWkdFetcher extends AbstractWKDFetcher {
}
@Override
protected InputStream fetchUri(URI uri) throws IOException {
public InputStream fetch(WKDAddress address, DiscoveryMethod method) throws IOException {
URI uri = address.getUri(method);
String path = uri.getPath();
File file = rootPath.resolve(path.substring(1)).toFile(); // get rid of leading slash at start of path
FileInputStream fileIn = new FileInputStream(file);