From d1d953e802df2c29cbd065e2f5edaf77072dc8a4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Mar 2022 16:56:46 +0100 Subject: [PATCH] Modularize WKD discovery --- wkd-java-cli/build.gradle | 2 +- .../pgp/wkd/cli/CertificateReaderImpl.java | 39 +++++++++ .../main/java/pgp/wkd/cli/DiscoverImpl.java | 21 +++++ .../src/main/java/pgp/wkd/cli/WKDCLI.java | 1 + .../main/java/pgp/wkd/cli/command/Fetch.java | 69 ++++++++-------- .../test_suite/DirectoryBasedWkdFetcher.java | 9 ++- wkd-java/build.gradle | 1 + .../main/java/pgp/wkd/AbstractDiscover.java | 59 ++++++++++++++ .../java/pgp/wkd/AbstractUriWKDFetcher.java | 39 +++++++++ .../main/java/pgp/wkd/AbstractWKDFetcher.java | 60 -------------- .../java/pgp/wkd/CertificateAndUserIds.java | 29 +++++++ .../main/java/pgp/wkd/CertificateReader.java | 14 ++++ wkd-java/src/main/java/pgp/wkd/Discover.java | 38 +++++++++ .../main/java/pgp/wkd/DiscoveryMethod.java | 11 +++ .../pgp/wkd/HttpUrlConnectionWKDFetcher.java | 4 +- .../pgp/wkd/MalformedUserIdException.java | 12 +++ .../java/pgp/wkd}/MissingUserIdException.java | 2 +- .../java/pgp/wkd/RejectedCertificate.java | 26 ++++++ .../src/main/java/pgp/wkd/WKDAddress.java | 19 ++++- .../main/java/pgp/wkd/WKDAddressHelper.java | 6 +- .../main/java/pgp/wkd/WKDDiscoveryItem.java | 79 +++++++++++++++++++ .../main/java/pgp/wkd/WKDDiscoveryResult.java | 53 +++++++++++++ .../src/main/java/pgp/wkd/WKDFetcher.java | 25 ++++++ .../src/test/java/pgp/wkd/WKDAddressTest.java | 4 +- 24 files changed, 511 insertions(+), 111 deletions(-) create mode 100644 wkd-java-cli/src/main/java/pgp/wkd/cli/CertificateReaderImpl.java create mode 100644 wkd-java-cli/src/main/java/pgp/wkd/cli/DiscoverImpl.java create mode 100644 wkd-java/src/main/java/pgp/wkd/AbstractDiscover.java create mode 100644 wkd-java/src/main/java/pgp/wkd/AbstractUriWKDFetcher.java delete mode 100644 wkd-java/src/main/java/pgp/wkd/AbstractWKDFetcher.java create mode 100644 wkd-java/src/main/java/pgp/wkd/CertificateAndUserIds.java create mode 100644 wkd-java/src/main/java/pgp/wkd/CertificateReader.java create mode 100644 wkd-java/src/main/java/pgp/wkd/Discover.java create mode 100644 wkd-java/src/main/java/pgp/wkd/DiscoveryMethod.java create mode 100644 wkd-java/src/main/java/pgp/wkd/MalformedUserIdException.java rename {wkd-java-cli/src/main/java/pgp/wkd/cli => wkd-java/src/main/java/pgp/wkd}/MissingUserIdException.java (95%) create mode 100644 wkd-java/src/main/java/pgp/wkd/RejectedCertificate.java create mode 100644 wkd-java/src/main/java/pgp/wkd/WKDDiscoveryItem.java create mode 100644 wkd-java/src/main/java/pgp/wkd/WKDDiscoveryResult.java create mode 100644 wkd-java/src/main/java/pgp/wkd/WKDFetcher.java diff --git a/wkd-java-cli/build.gradle b/wkd-java-cli/build.gradle index f686ac0..219adf2 100644 --- a/wkd-java-cli/build.gradle +++ b/wkd-java-cli/build.gradle @@ -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" diff --git a/wkd-java-cli/src/main/java/pgp/wkd/cli/CertificateReaderImpl.java b/wkd-java-cli/src/main/java/pgp/wkd/cli/CertificateReaderImpl.java new file mode 100644 index 0000000..a64de11 --- /dev/null +++ b/wkd-java-cli/src/main/java/pgp/wkd/cli/CertificateReaderImpl.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// 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 read(InputStream inputStream) throws IOException { + List 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 userIds = info.getValidAndExpiredUserIds(); + certificatesAndUserIds.add(new CertificateAndUserIds(parsedCert, userIds)); + } + return certificatesAndUserIds; + } catch (PGPException e) { + throw new IOException("Cannot parse certificates.", e); + } + } +} diff --git a/wkd-java-cli/src/main/java/pgp/wkd/cli/DiscoverImpl.java b/wkd-java-cli/src/main/java/pgp/wkd/cli/DiscoverImpl.java new file mode 100644 index 0000000..8e4752e --- /dev/null +++ b/wkd-java-cli/src/main/java/pgp/wkd/cli/DiscoverImpl.java @@ -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); + } +} diff --git a/wkd-java-cli/src/main/java/pgp/wkd/cli/WKDCLI.java b/wkd-java-cli/src/main/java/pgp/wkd/cli/WKDCLI.java index 29b2cd2..5c1bc19 100644 --- a/wkd-java-cli/src/main/java/pgp/wkd/cli/WKDCLI.java +++ b/wkd-java-cli/src/main/java/pgp/wkd/cli/WKDCLI.java @@ -4,6 +4,7 @@ package pgp.wkd.cli; +import pgp.wkd.MissingUserIdException; import pgp.wkd.cli.command.Fetch; import picocli.CommandLine; diff --git a/wkd-java-cli/src/main/java/pgp/wkd/cli/command/Fetch.java b/wkd-java-cli/src/main/java/pgp/wkd/cli/command/Fetch.java index 34a46cd..de558d0 100644 --- a/wkd-java-cli/src/main/java/pgp/wkd/cli/command/Fetch.java +++ b/wkd-java-cli/src/main/java/pgp/wkd/cli/command/Fetch.java @@ -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 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); + } } diff --git a/wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/DirectoryBasedWkdFetcher.java b/wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/DirectoryBasedWkdFetcher.java index db0d7a3..546e325 100644 --- a/wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/DirectoryBasedWkdFetcher.java +++ b/wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/DirectoryBasedWkdFetcher.java @@ -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); diff --git a/wkd-java/build.gradle b/wkd-java/build.gradle index d45fbbf..e6fdb27 100644 --- a/wkd-java/build.gradle +++ b/wkd-java/build.gradle @@ -19,6 +19,7 @@ dependencies { // Logging api "org.slf4j:slf4j-api:$slf4jVersion" testImplementation "ch.qos.logback:logback-classic:$logbackVersion" + api "org.pgpainless:pgp-certificate-store:0.1.0" // Z-Base32 implementation 'com.sandinh:zbase32-commons-codec:1.0.0' diff --git a/wkd-java/src/main/java/pgp/wkd/AbstractDiscover.java b/wkd-java/src/main/java/pgp/wkd/AbstractDiscover.java new file mode 100644 index 0000000..1ec9e9d --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/AbstractDiscover.java @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd; + +import pgp.certificate_store.Certificate; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class AbstractDiscover implements Discover { + + protected final CertificateReader reader; + protected final WKDFetcher fetcher; + + public AbstractDiscover(CertificateReader reader, WKDFetcher fetcher) { + this.reader = reader; + this.fetcher = fetcher; + } + + @Override + public WKDDiscoveryItem discover(DiscoveryMethod method, WKDAddress address) { + try { + InputStream inputStream = fetcher.fetch(address, method); + List fetchedCertificates = reader.read(inputStream); + + List rejectedCertificates = new ArrayList<>(); + List acceptableCertificates = new ArrayList<>(); + + String email = address.getEmail(); + + for (CertificateAndUserIds certAndUserIds : fetchedCertificates) { + Certificate certificate = certAndUserIds.getCertificate(); + boolean containsEmail = false; + for (String userId : certAndUserIds.getUserIds()) { + if (userId.contains("<" + email + ">") || userId.equals(email)) { + containsEmail = true; + break; + } + } + if (!containsEmail) { + rejectedCertificates.add(new RejectedCertificate(certificate, + new MissingUserIdException("Certificate " + certificate.getFingerprint() + + " does not contain user-id with email '" + email + "'"))); + } else { + acceptableCertificates.add(certificate); + } + } + + return WKDDiscoveryItem.success(method, address, acceptableCertificates, rejectedCertificates); + + } catch (IOException e) { + return WKDDiscoveryItem.failure(method, address, e); + } + } +} diff --git a/wkd-java/src/main/java/pgp/wkd/AbstractUriWKDFetcher.java b/wkd-java/src/main/java/pgp/wkd/AbstractUriWKDFetcher.java new file mode 100644 index 0000000..ebe4d35 --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/AbstractUriWKDFetcher.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; + +public abstract class AbstractUriWKDFetcher implements WKDFetcher { + + private static final Logger LOGGER = LoggerFactory.getLogger(WKDFetcher.class); + + @Override + public InputStream fetch(WKDAddress address, DiscoveryMethod method) throws IOException { + URI uri = address.getUri(method); + try { + return fetchUri(uri); + } catch (IOException e) { + LOGGER.debug("Could not fetch key using " + method + " method from " + uri.toString(), e); + throw e; + } + } + + /** + * Fetch the contents of the file that the {@link URI} points to from the remote server. + * @param uri uri + * @return file contents + * + * @throws java.net.ConnectException in case the file or host does not exist + * @throws IOException in case of an IO-error + */ + protected abstract InputStream fetchUri(URI uri) throws IOException; + +} diff --git a/wkd-java/src/main/java/pgp/wkd/AbstractWKDFetcher.java b/wkd-java/src/main/java/pgp/wkd/AbstractWKDFetcher.java deleted file mode 100644 index 15b828b..0000000 --- a/wkd-java/src/main/java/pgp/wkd/AbstractWKDFetcher.java +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.wkd; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; - -/** - * Abstract class for fetching OpenPGP certificates from the WKD. - * This class can be extended to fetch files from remote servers using different HTTP clients. - */ -public abstract class AbstractWKDFetcher { - - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractWKDFetcher.class); - - /** - * Attempt to fetch an OpenPGP certificate from the Web Key Directory. - * - * @param address WKDAddress object - * @return input stream containing the certificate in its binary representation - * - * @throws IOException in case of an error - */ - public InputStream fetch(WKDAddress address) throws IOException { - URI advanced = address.getAdvancedMethodURI(); - IOException advancedException; - try { - return fetchUri(advanced); - } catch (IOException e) { - advancedException = e; - LOGGER.debug("Could not fetch key using advanced method from " + advanced.toString(), advancedException); - } - - URI direct = address.getDirectMethodURI(); - try { - return fetchUri(direct); - } catch (IOException e) { - // we would like to use addSuppressed eventually, but Android API 10 does not support it - // e.addSuppressed(advancedException); - LOGGER.debug("Could not fetch key using direct method from " + direct.toString(), e); - throw e; - } - } - - /** - * Fetch the contents of the file that the {@link URI} points to from the remote server. - * @param uri uri - * @return file contents - * - * @throws java.net.ConnectException in case the file or host does not exist - * @throws IOException in case of an IO-error - */ - protected abstract InputStream fetchUri(URI uri) throws IOException; -} diff --git a/wkd-java/src/main/java/pgp/wkd/CertificateAndUserIds.java b/wkd-java/src/main/java/pgp/wkd/CertificateAndUserIds.java new file mode 100644 index 0000000..214db5e --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/CertificateAndUserIds.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd; + +import pgp.certificate_store.Certificate; + +import java.util.ArrayList; +import java.util.List; + +public class CertificateAndUserIds { + + private final Certificate certificate; + private final List userIds; + + public CertificateAndUserIds(Certificate certificate, List userIds) { + this.certificate = certificate; + this.userIds = userIds; + } + + public List getUserIds() { + return new ArrayList<>(userIds); + } + + public Certificate getCertificate() { + return certificate; + } +} diff --git a/wkd-java/src/main/java/pgp/wkd/CertificateReader.java b/wkd-java/src/main/java/pgp/wkd/CertificateReader.java new file mode 100644 index 0000000..e491dd3 --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/CertificateReader.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +public interface CertificateReader { + + List read(InputStream inputStream) throws IOException; +} diff --git a/wkd-java/src/main/java/pgp/wkd/Discover.java b/wkd-java/src/main/java/pgp/wkd/Discover.java new file mode 100644 index 0000000..ee5078e --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/Discover.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd; + +import java.util.ArrayList; +import java.util.List; + +public interface Discover { + + WKDDiscoveryItem discover(DiscoveryMethod method, WKDAddress address); + + default WKDDiscoveryResult discover(WKDAddress address) { + List results = new ArrayList<>(); + + // advanced method + WKDDiscoveryItem advanced = discover(DiscoveryMethod.advanced, address); + results.add(advanced); + + if (advanced.isSuccessful()) { + return new WKDDiscoveryResult(results); + } + // direct method + results.add(discover(DiscoveryMethod.direct, address)); + + return new WKDDiscoveryResult(results); + } + + default WKDDiscoveryResult discoverByEmail(String email) throws MalformedUserIdException { + return discover(WKDAddress.fromEmail(email)); + } + + default WKDDiscoveryResult discoverByUserId(String userId) throws MalformedUserIdException { + return discover(WKDAddressHelper.wkdAddressFromUserId(userId)); + } + +} diff --git a/wkd-java/src/main/java/pgp/wkd/DiscoveryMethod.java b/wkd-java/src/main/java/pgp/wkd/DiscoveryMethod.java new file mode 100644 index 0000000..eb8318c --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/DiscoveryMethod.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd; + +public enum DiscoveryMethod { + advanced, + direct, + ; +} diff --git a/wkd-java/src/main/java/pgp/wkd/HttpUrlConnectionWKDFetcher.java b/wkd-java/src/main/java/pgp/wkd/HttpUrlConnectionWKDFetcher.java index 76db45f..52d9b81 100644 --- a/wkd-java/src/main/java/pgp/wkd/HttpUrlConnectionWKDFetcher.java +++ b/wkd-java/src/main/java/pgp/wkd/HttpUrlConnectionWKDFetcher.java @@ -12,9 +12,9 @@ import java.net.URI; import java.net.URL; /** - * Implementation of {@link AbstractWKDFetcher} using Java's {@link HttpURLConnection}. + * Implementation of {@link WKDFetcher} using Java's {@link HttpURLConnection}. */ -public class HttpUrlConnectionWKDFetcher extends AbstractWKDFetcher { +public class HttpUrlConnectionWKDFetcher extends AbstractUriWKDFetcher { public InputStream fetchUri(URI uri) throws IOException { URL url = uri.toURL(); diff --git a/wkd-java/src/main/java/pgp/wkd/MalformedUserIdException.java b/wkd-java/src/main/java/pgp/wkd/MalformedUserIdException.java new file mode 100644 index 0000000..d4d9a0a --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/MalformedUserIdException.java @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd; + +public class MalformedUserIdException extends RuntimeException { + + public MalformedUserIdException(String message) { + super(message); + } +} diff --git a/wkd-java-cli/src/main/java/pgp/wkd/cli/MissingUserIdException.java b/wkd-java/src/main/java/pgp/wkd/MissingUserIdException.java similarity index 95% rename from wkd-java-cli/src/main/java/pgp/wkd/cli/MissingUserIdException.java rename to wkd-java/src/main/java/pgp/wkd/MissingUserIdException.java index 89c2c46..b5e2bdd 100644 --- a/wkd-java-cli/src/main/java/pgp/wkd/cli/MissingUserIdException.java +++ b/wkd-java/src/main/java/pgp/wkd/MissingUserIdException.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package pgp.wkd.cli; +package pgp.wkd; /** * Exception that gets thrown when an OpenPGP certificate is not carrying a User-ID binding for the email address diff --git a/wkd-java/src/main/java/pgp/wkd/RejectedCertificate.java b/wkd-java/src/main/java/pgp/wkd/RejectedCertificate.java new file mode 100644 index 0000000..e17964c --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/RejectedCertificate.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd; + +import pgp.certificate_store.Certificate; + +public class RejectedCertificate { + + private final Certificate certificate; + private final Throwable failure; + + public RejectedCertificate(Certificate certificate, Throwable failure) { + this.certificate = certificate; + this.failure = failure; + } + + public Certificate getCertificate() { + return certificate; + } + + public Throwable getFailure() { + return failure; + } +} diff --git a/wkd-java/src/main/java/pgp/wkd/WKDAddress.java b/wkd-java/src/main/java/pgp/wkd/WKDAddress.java index 7b2039c..35183dd 100644 --- a/wkd-java/src/main/java/pgp/wkd/WKDAddress.java +++ b/wkd-java/src/main/java/pgp/wkd/WKDAddress.java @@ -87,11 +87,24 @@ public final class WKDAddress { * @param email email address, case sensitive * @return WKDAddress object */ - public static WKDAddress fromEmail(String email) { + public static WKDAddress fromEmail(String email) throws MalformedUserIdException { MailAddress mailAddress = parseMailAddress(email); return new WKDAddress(mailAddress.getLocalPart(), mailAddress.getDomainPart()); } + public URI getUri(DiscoveryMethod method) { + if (method == DiscoveryMethod.advanced) { + return getAdvancedMethodURI(); + } else if (method == DiscoveryMethod.direct) { + return getDirectMethodURI(); + } + throw new IllegalArgumentException("Invalid discovery method."); + } + + public String getEmail() { + return localPart + '@' + domainPart; + } + /** * Get an {@link URI} pointing to the certificate using the direct lookup method. * The direct method requires that a WKD is available on the same domain as the users mail server. @@ -168,10 +181,10 @@ public final class WKDAddress { * @param email email address string * @return validated and split mail address */ - private static MailAddress parseMailAddress(String email) { + private static MailAddress parseMailAddress(String email) throws MalformedUserIdException { Matcher matcher = PATTERN_EMAIL.matcher(email); if (!matcher.matches()) { - throw new IllegalArgumentException("Invalid email address."); + throw new MalformedUserIdException("Invalid email address."); } String localPart = matcher.group(1); diff --git a/wkd-java/src/main/java/pgp/wkd/WKDAddressHelper.java b/wkd-java/src/main/java/pgp/wkd/WKDAddressHelper.java index 2856347..c1e4d74 100644 --- a/wkd-java/src/main/java/pgp/wkd/WKDAddressHelper.java +++ b/wkd-java/src/main/java/pgp/wkd/WKDAddressHelper.java @@ -31,10 +31,10 @@ public class WKDAddressHelper { * @throws IllegalArgumentException in case the user-id does not match the expected format * and does not contain an email address. */ - public static String emailFromUserId(String userId) { + public static String emailFromUserId(String userId) throws MalformedUserIdException { Matcher matcher = PATTERN_USER_ID.matcher(userId); if (!matcher.matches()) { - throw new IllegalArgumentException("User-ID does not follow excepted pattern \"Firstname Lastname [Optional Comment]\""); + throw new MalformedUserIdException("User-ID does not follow excepted pattern \"Firstname Lastname [Optional Comment]\""); } String email = matcher.group(1); @@ -47,7 +47,7 @@ public class WKDAddressHelper { * @param userId user-id * @return WKD address for the user-id's email address. */ - public static WKDAddress wkdAddressFromUserId(String userId) { + public static WKDAddress wkdAddressFromUserId(String userId) throws MalformedUserIdException { String email = emailFromUserId(userId); return WKDAddress.fromEmail(email); } diff --git a/wkd-java/src/main/java/pgp/wkd/WKDDiscoveryItem.java b/wkd-java/src/main/java/pgp/wkd/WKDDiscoveryItem.java new file mode 100644 index 0000000..32b4831 --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/WKDDiscoveryItem.java @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd; + +import pgp.certificate_store.Certificate; + +import java.util.List; + +public final class WKDDiscoveryItem { + + private final DiscoveryMethod method; + private final WKDAddress address; + private final List certificates; + private final List rejectedCertificates; + private final Throwable failure; + + /** + * Constructor for a {@link WKDDiscoveryItem} object. + * @param method discovery method + * @param address wkd address used for discovery + * @param certificates list of successfully fetched certificates + * @param rejectedCertificates list of invalid fetched certificates (e.g. missing user-id) + * @param failure general fetching error (e.g. connection error, 404...) + */ + private WKDDiscoveryItem( + DiscoveryMethod method, + WKDAddress address, + List certificates, + List rejectedCertificates, + Throwable failure) { + this.method = method; + this.address = address; + this.certificates = certificates; + this.rejectedCertificates = rejectedCertificates; + this.failure = failure; + } + + public static WKDDiscoveryItem success(DiscoveryMethod method, WKDAddress address, List certificates, List rejectedCertificates) { + return new WKDDiscoveryItem(method, address, certificates, rejectedCertificates, null); + } + + public static WKDDiscoveryItem failure(DiscoveryMethod method, WKDAddress address, Throwable failure) { + return new WKDDiscoveryItem(method, address, null, null, failure); + } + + public DiscoveryMethod getMethod() { + return method; + } + + public WKDAddress getAddress() { + return address; + } + + public boolean isSuccessful() { + return !hasFailure(); + } + + public List getCertificates() { + return certificates; + } + + public List getRejectedCertificates() { + return rejectedCertificates; + } + + public Throwable getFailure() { + return failure; + } + + public boolean hasCertificates() { + return certificates != null && !certificates.isEmpty(); + } + + public boolean hasFailure() { + return failure != null; + } +} diff --git a/wkd-java/src/main/java/pgp/wkd/WKDDiscoveryResult.java b/wkd-java/src/main/java/pgp/wkd/WKDDiscoveryResult.java new file mode 100644 index 0000000..91bf5ad --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/WKDDiscoveryResult.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd; + +import pgp.certificate_store.Certificate; + +import java.util.ArrayList; +import java.util.List; + +public class WKDDiscoveryResult { + + private List items; + + public WKDDiscoveryResult(List items) { + this.items = items; + } + + public List getCertificates() { + List certificates = new ArrayList<>(); + + for (WKDDiscoveryItem item : items) { + if (item.isSuccessful()) { + certificates.addAll(item.getCertificates()); + } + } + return certificates; + } + + public boolean isSuccessful() { + for (WKDDiscoveryItem item : items) { + if (item.isSuccessful() && item.hasCertificates()) { + return true; + } + } + return false; + } + + public List getItems() { + return items; + } + + public List getFailedItems() { + List fails = new ArrayList<>(); + for (WKDDiscoveryItem item : items) { + if (!item.isSuccessful()) { + fails.add(item); + } + } + return fails; + } +} diff --git a/wkd-java/src/main/java/pgp/wkd/WKDFetcher.java b/wkd-java/src/main/java/pgp/wkd/WKDFetcher.java new file mode 100644 index 0000000..6ec22dc --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/WKDFetcher.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Abstract class for fetching OpenPGP certificates from the WKD. + * This class can be extended to fetch files from remote servers using different HTTP clients. + */ +public interface WKDFetcher { + + /** + * Attempt to fetch an OpenPGP certificate from the Web Key Directory. + * + * @param address WKDAddress object + * @return input stream containing the certificate in its binary representation + * + * @throws IOException in case of an error + */ + InputStream fetch(WKDAddress address, DiscoveryMethod method) throws IOException; +} diff --git a/wkd-java/src/test/java/pgp/wkd/WKDAddressTest.java b/wkd-java/src/test/java/pgp/wkd/WKDAddressTest.java index 7eec691..66dd910 100644 --- a/wkd-java/src/test/java/pgp/wkd/WKDAddressTest.java +++ b/wkd-java/src/test/java/pgp/wkd/WKDAddressTest.java @@ -66,14 +66,14 @@ public class WKDAddressTest { "John Doe [The Real One]", "")) { - assertThrows(IllegalArgumentException.class, () -> WKDAddressHelper.wkdAddressFromUserId(brokenUserId)); + assertThrows(MalformedUserIdException.class, () -> WKDAddressHelper.wkdAddressFromUserId(brokenUserId)); } } @Test public void testFromInvalidEmail() { for (String brokenEmail : Arrays.asList("john.doe", "@example.org", "john doe@example.org", "john.doe@example org")) { - assertThrows(IllegalArgumentException.class, () -> WKDAddress.fromEmail(brokenEmail)); + assertThrows(MalformedUserIdException.class, () -> WKDAddress.fromEmail(brokenEmail)); } } }