From 3af16baa20871ed0d89bb4bec49fe8e426fce446 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 17 Mar 2022 15:27:28 +0100 Subject: [PATCH] Refactoring and dynamic test suite --- wkd-java-cli/build.gradle | 3 +- ...erImpl.java => CertificateParserImpl.java} | 4 +- .../main/java/pgp/wkd/cli/DiscoverImpl.java | 20 +++---- .../main/java/pgp/wkd/cli/command/Fetch.java | 14 ++--- ... => DirectoryBasedCertificateFetcher.java} | 10 ++-- .../cli/test_suite/TestSuiteTestRunner.java | 38 ++++++++----- wkd-java/build.gradle | 3 + .../java/pgp/wkd/CertificateAndUserIds.java | 15 ++++- wkd-java/src/main/java/pgp/wkd/Discover.java | 38 ------------- .../main/java/pgp/wkd/DiscoveryMethod.java | 11 ---- .../pgp/wkd/MalformedUserIdException.java | 4 ++ .../java/pgp/wkd/RejectedCertificate.java | 25 +++++++-- .../src/main/java/pgp/wkd/WKDAddress.java | 55 ++++++++++++++----- .../main/java/pgp/wkd/WKDAddressHelper.java | 9 ++- .../AbstractUriCertificateFetcher.java} | 15 ++--- .../wkd/discovery/CertificateDiscoverer.java | 42 ++++++++++++++ .../CertificateDiscoveryImplementation.java} | 22 +++++--- .../CertificateFetcher.java} | 8 ++- .../CertificateParser.java} | 6 +- .../pgp/wkd/discovery/DiscoveryMethod.java | 24 ++++++++ .../DiscoveryResponse.java} | 51 +++++++++++------ .../DiscoveryResult.java} | 24 ++++---- .../HttpUrlConnectionCertificateFetcher.java} | 8 +-- 23 files changed, 287 insertions(+), 162 deletions(-) rename wkd-java-cli/src/main/java/pgp/wkd/cli/{CertificateReaderImpl.java => CertificateParserImpl.java} (93%) rename wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/{DirectoryBasedWkdFetcher.java => DirectoryBasedCertificateFetcher.java} (69%) delete mode 100644 wkd-java/src/main/java/pgp/wkd/Discover.java delete mode 100644 wkd-java/src/main/java/pgp/wkd/DiscoveryMethod.java rename wkd-java/src/main/java/pgp/wkd/{AbstractUriWKDFetcher.java => discovery/AbstractUriCertificateFetcher.java} (66%) create mode 100644 wkd-java/src/main/java/pgp/wkd/discovery/CertificateDiscoverer.java rename wkd-java/src/main/java/pgp/wkd/{AbstractDiscover.java => discovery/CertificateDiscoveryImplementation.java} (67%) rename wkd-java/src/main/java/pgp/wkd/{WKDFetcher.java => discovery/CertificateFetcher.java} (76%) rename wkd-java/src/main/java/pgp/wkd/{CertificateReader.java => discovery/CertificateParser.java} (72%) create mode 100644 wkd-java/src/main/java/pgp/wkd/discovery/DiscoveryMethod.java rename wkd-java/src/main/java/pgp/wkd/{WKDDiscoveryItem.java => discovery/DiscoveryResponse.java} (50%) rename wkd-java/src/main/java/pgp/wkd/{WKDDiscoveryResult.java => discovery/DiscoveryResult.java} (61%) rename wkd-java/src/main/java/pgp/wkd/{HttpUrlConnectionWKDFetcher.java => discovery/HttpUrlConnectionCertificateFetcher.java} (74%) diff --git a/wkd-java-cli/build.gradle b/wkd-java-cli/build.gradle index fa1aaba..d4dc225 100644 --- a/wkd-java-cli/build.gradle +++ b/wkd-java-cli/build.gradle @@ -10,6 +10,7 @@ group 'org.pgpainless' repositories { mavenCentral() + mavenLocal() } dependencies { @@ -20,7 +21,7 @@ dependencies { testImplementation 'com.ginsberg:junit5-system-exit:1.1.2' testImplementation 'org.mockito:mockito-core:4.3.1' - implementation("org.pgpainless:pgpainless-cert-d:0.1.0") + implementation("org.pgpainless:pgpainless-cert-d:0.1.1") 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/CertificateParserImpl.java similarity index 93% rename from wkd-java-cli/src/main/java/pgp/wkd/cli/CertificateReaderImpl.java rename to wkd-java-cli/src/main/java/pgp/wkd/cli/CertificateParserImpl.java index a64de11..3ed47eb 100644 --- a/wkd-java-cli/src/main/java/pgp/wkd/cli/CertificateReaderImpl.java +++ b/wkd-java-cli/src/main/java/pgp/wkd/cli/CertificateParserImpl.java @@ -12,14 +12,14 @@ 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 pgp.wkd.discovery.CertificateParser; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; -public class CertificateReaderImpl implements CertificateReader { +public class CertificateParserImpl implements CertificateParser { @Override public List read(InputStream inputStream) throws IOException { List certificatesAndUserIds = new ArrayList<>(); 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 index 8e4752e..0ab39c8 100644 --- a/wkd-java-cli/src/main/java/pgp/wkd/cli/DiscoverImpl.java +++ b/wkd-java-cli/src/main/java/pgp/wkd/cli/DiscoverImpl.java @@ -1,21 +1,21 @@ package pgp.wkd.cli; -import pgp.wkd.AbstractDiscover; -import pgp.wkd.CertificateReader; -import pgp.wkd.HttpUrlConnectionWKDFetcher; -import pgp.wkd.WKDFetcher; +import pgp.wkd.discovery.CertificateDiscoveryImplementation; +import pgp.wkd.discovery.CertificateParser; +import pgp.wkd.discovery.HttpUrlConnectionCertificateFetcher; +import pgp.wkd.discovery.CertificateFetcher; -public class DiscoverImpl extends AbstractDiscover { +public class DiscoverImpl extends CertificateDiscoveryImplementation { public DiscoverImpl() { - super(new CertificateReaderImpl(), new HttpUrlConnectionWKDFetcher()); + super(new CertificateParserImpl(), new HttpUrlConnectionCertificateFetcher()); } - public DiscoverImpl(WKDFetcher fetcher) { - super(new CertificateReaderImpl(), fetcher); + public DiscoverImpl(CertificateFetcher fetcher) { + super(new CertificateParserImpl(), fetcher); } - public DiscoverImpl(CertificateReader certificateReader, WKDFetcher fetcher) { - super(certificateReader, fetcher); + public DiscoverImpl(CertificateParser certificateParser, CertificateFetcher fetcher) { + super(certificateParser, fetcher); } } 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 de558d0..a1d33fb 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 @@ -7,13 +7,13 @@ package pgp.wkd.cli.command; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.util.io.Streams; import pgp.certificate_store.Certificate; -import pgp.wkd.Discover; -import pgp.wkd.HttpUrlConnectionWKDFetcher; +import pgp.wkd.discovery.CertificateDiscoverer; +import pgp.wkd.discovery.HttpUrlConnectionCertificateFetcher; import pgp.wkd.MalformedUserIdException; import pgp.wkd.WKDAddress; import pgp.wkd.WKDAddressHelper; -import pgp.wkd.WKDDiscoveryResult; -import pgp.wkd.WKDFetcher; +import pgp.wkd.discovery.DiscoveryResult; +import pgp.wkd.discovery.CertificateFetcher; import pgp.wkd.cli.CertNotFetchableException; import pgp.wkd.cli.DiscoverImpl; import picocli.CommandLine; @@ -42,14 +42,14 @@ public class Fetch implements Runnable { boolean armor = false; // TODO: Better way to inject fetcher implementation - public static WKDFetcher fetcher = new HttpUrlConnectionWKDFetcher(); + public static CertificateFetcher fetcher = new HttpUrlConnectionCertificateFetcher(); @Override public void run() { - Discover discover = new DiscoverImpl(fetcher); + CertificateDiscoverer certificateDiscoverer = new DiscoverImpl(fetcher); WKDAddress address = addressFromUserId(userId); - WKDDiscoveryResult result = discover.discover(address); + DiscoveryResult result = certificateDiscoverer.discover(address); if (!result.isSuccessful()) { throw new CertNotFetchableException("Cannot fetch cert."); 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/DirectoryBasedCertificateFetcher.java similarity index 69% rename from wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/DirectoryBasedWkdFetcher.java rename to wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/DirectoryBasedCertificateFetcher.java index 546e325..3a4f8bc 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/DirectoryBasedCertificateFetcher.java @@ -4,9 +4,9 @@ package pgp.wkd.cli.test_suite; -import pgp.wkd.DiscoveryMethod; +import pgp.wkd.discovery.DiscoveryMethod; import pgp.wkd.WKDAddress; -import pgp.wkd.WKDFetcher; +import pgp.wkd.discovery.CertificateFetcher; import java.io.File; import java.io.FileInputStream; @@ -15,17 +15,17 @@ import java.io.InputStream; import java.net.URI; import java.nio.file.Path; -public class DirectoryBasedWkdFetcher implements WKDFetcher { +public class DirectoryBasedCertificateFetcher implements CertificateFetcher { // The directory containing the .well-known subdirectory private final Path rootPath; - public DirectoryBasedWkdFetcher(Path rootPath) { + public DirectoryBasedCertificateFetcher(Path rootPath) { this.rootPath = rootPath; } @Override - public InputStream fetch(WKDAddress address, DiscoveryMethod method) throws IOException { + public InputStream fetchCertificate(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 diff --git a/wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/TestSuiteTestRunner.java b/wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/TestSuiteTestRunner.java index 69e4d03..05a4a01 100644 --- a/wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/TestSuiteTestRunner.java +++ b/wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/TestSuiteTestRunner.java @@ -5,7 +5,9 @@ package pgp.wkd.cli.test_suite; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; import pgp.wkd.cli.WKDCLI; @@ -17,6 +19,7 @@ import pgp.wkd.test_suite.TestSuiteGenerator; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -39,24 +42,29 @@ public class TestSuiteTestRunner { suite = generator.generateTestSuiteInDirectory(tempFile, TestSuiteGenerator.Method.direct); // Fetch certificates from a local directory instead of the internetzzz. - Fetch.fetcher = new DirectoryBasedWkdFetcher(tempPath); + Fetch.fetcher = new DirectoryBasedCertificateFetcher(tempPath); } - @Test - void runTestsAgainstTestSuite() { - for (TestCase testCase : suite.getTestCases()) { - LOGGER.info(() -> "Execute Test Case '" + testCase.getTestTitle() + "'"); - String mail = testCase.getLookupMailAddress(); + @TestFactory + public Iterable testsFromTestSuite() { + return suite.getTestCases() + .stream() + .map(testCase -> DynamicTest.dynamicTest( + testCase.getTestTitle(), + () -> { + String mail = testCase.getLookupMailAddress(); - int exitCode = WKDCLI.execute(new String[] { - "fetch", "--armor", mail - }); + int exitCode = WKDCLI.execute(new String[] { + "fetch", "--armor", mail + }); - if (testCase.isExpectSuccess()) { - assertEquals(0, exitCode, testCase.getTestDescription()); - } else { - assertNotEquals(0, exitCode, testCase.getTestDescription()); - } - } + if (testCase.isExpectSuccess()) { + assertEquals(0, exitCode, testCase.getTestDescription()); + } else { + assertNotEquals(0, exitCode, testCase.getTestDescription()); + } + } + )) + .collect(Collectors.toList()); } } diff --git a/wkd-java/build.gradle b/wkd-java/build.gradle index e6fdb27..b4e2867 100644 --- a/wkd-java/build.gradle +++ b/wkd-java/build.gradle @@ -23,6 +23,9 @@ dependencies { // Z-Base32 implementation 'com.sandinh:zbase32-commons-codec:1.0.0' + + // @Nullable etc. + implementation "com.google.code.findbugs:jsr305:3.0.2" } test { diff --git a/wkd-java/src/main/java/pgp/wkd/CertificateAndUserIds.java b/wkd-java/src/main/java/pgp/wkd/CertificateAndUserIds.java index 214db5e..726d314 100644 --- a/wkd-java/src/main/java/pgp/wkd/CertificateAndUserIds.java +++ b/wkd-java/src/main/java/pgp/wkd/CertificateAndUserIds.java @@ -9,20 +9,33 @@ import pgp.certificate_store.Certificate; import java.util.ArrayList; import java.util.List; +/** + * Tuple class which bundles a {@link Certificate} and a list of its valid or expired user ids. + */ public class CertificateAndUserIds { private final Certificate certificate; private final List userIds; - + public CertificateAndUserIds(Certificate certificate, List userIds) { this.certificate = certificate; this.userIds = userIds; } + /** + * Return a list containing the valid or expired user-ids of the certificate. + * + * @return user ids + */ public List getUserIds() { return new ArrayList<>(userIds); } + /** + * Return the certificate itself. + * + * @return certificate + */ public Certificate getCertificate() { return certificate; } diff --git a/wkd-java/src/main/java/pgp/wkd/Discover.java b/wkd-java/src/main/java/pgp/wkd/Discover.java deleted file mode 100644 index ee5078e..0000000 --- a/wkd-java/src/main/java/pgp/wkd/Discover.java +++ /dev/null @@ -1,38 +0,0 @@ -// 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 deleted file mode 100644 index eb8318c..0000000 --- a/wkd-java/src/main/java/pgp/wkd/DiscoveryMethod.java +++ /dev/null @@ -1,11 +0,0 @@ -// 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/MalformedUserIdException.java b/wkd-java/src/main/java/pgp/wkd/MalformedUserIdException.java index d4d9a0a..45f6a1e 100644 --- a/wkd-java/src/main/java/pgp/wkd/MalformedUserIdException.java +++ b/wkd-java/src/main/java/pgp/wkd/MalformedUserIdException.java @@ -4,6 +4,10 @@ package pgp.wkd; +/** + * Exception that gets thrown when the application is presented with a malformed user-id. + * A malformed user-id is a user-id which does not contain an email address. + */ public class MalformedUserIdException extends RuntimeException { public MalformedUserIdException(String message) { diff --git a/wkd-java/src/main/java/pgp/wkd/RejectedCertificate.java b/wkd-java/src/main/java/pgp/wkd/RejectedCertificate.java index e17964c..7a227b4 100644 --- a/wkd-java/src/main/java/pgp/wkd/RejectedCertificate.java +++ b/wkd-java/src/main/java/pgp/wkd/RejectedCertificate.java @@ -6,21 +6,36 @@ package pgp.wkd; import pgp.certificate_store.Certificate; +/** + * A rejected OpenPGP certificate. + * The WKD specification requires that a certificate fetched via the Web Key Directory MUST contain the mail address + * that was used to look up the certificate as a user id. + * + * A rejected certificate may not have carried the lookup email address. + */ public class RejectedCertificate { private final Certificate certificate; - private final Throwable failure; + private final Throwable reasonForRejection; - public RejectedCertificate(Certificate certificate, Throwable failure) { + public RejectedCertificate(Certificate certificate, Throwable reasonForRejection) { this.certificate = certificate; - this.failure = failure; + this.reasonForRejection = reasonForRejection; } + /** + * Return the certificate. + * @return certificate + */ public Certificate getCertificate() { return certificate; } - public Throwable getFailure() { - return failure; + /** + * Return the reason for rejection. + * @return rejection reason + */ + public Throwable getReasonForRejection() { + return reasonForRejection; } } diff --git a/wkd-java/src/main/java/pgp/wkd/WKDAddress.java b/wkd-java/src/main/java/pgp/wkd/WKDAddress.java index 35183dd..83b957f 100644 --- a/wkd-java/src/main/java/pgp/wkd/WKDAddress.java +++ b/wkd-java/src/main/java/pgp/wkd/WKDAddress.java @@ -5,7 +5,9 @@ package pgp.wkd; import org.apache.commons.codec.binary.ZBase32; +import pgp.wkd.discovery.DiscoveryMethod; +import javax.annotation.Nonnull; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; @@ -26,7 +28,7 @@ public final class WKDAddress { private static final String SCHEME = "https://"; private static final String ADV_SUBDOMAIN = "openpgpkey."; private static final String DIRECT_WELL_KNOWN = "/.well-known/openpgpkey/hu/"; - private static String ADV_WELL_KNOWN(String domain) { + private static String ADV_WELL_KNOWN(@Nonnull String domain) { return "/.well-known/openpgpkey/" + domain + "/hu/"; } @@ -40,6 +42,7 @@ public final class WKDAddress { private static final Pattern PATTERN_DOMAIN_PART = Pattern.compile("[a-zA-Z0-9.-]+$"); // Android API lvl 10 does not yet know StandardCharsets.UTF_8 :/ + @SuppressWarnings("CharsetObjectCanBeUsed") private static final Charset utf8 = Charset.forName("UTF8"); // Z-Base32 encoding is described in https://www.rfc-editor.org/rfc/rfc6189.html#section-5.1.6 private static final ZBase32 zBase32 = new ZBase32(); @@ -70,14 +73,17 @@ public final class WKDAddress { * @param domainPart domain part of the email address, case-insensitive * * @return WKD address + * @throws IllegalArgumentException in case of malformed local or domain part */ - public static WKDAddress fromLocalAndDomainPart(String localPart, String domainPart) { + @Nonnull + public static WKDAddress fromLocalAndDomainPart(@Nonnull String localPart, @Nonnull String domainPart) { if (!PATTERN_LOCAL_PART.matcher(localPart).matches()) { throw new IllegalArgumentException("Invalid local part."); } if (!PATTERN_DOMAIN_PART.matcher(domainPart).matches()) { throw new IllegalArgumentException("Invalid domain part."); } + return new WKDAddress(localPart, domainPart); } @@ -87,20 +93,35 @@ public final class WKDAddress { * @param email email address, case sensitive * @return WKDAddress object */ - public static WKDAddress fromEmail(String email) throws MalformedUserIdException { + public static WKDAddress fromEmail(@Nonnull 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(); + /** + * Return the {@link URI} for the respective {@link DiscoveryMethod}. + * + * @param method discovery method + * @return uri of the certificate + */ + @Nonnull + public URI getUri(@Nonnull DiscoveryMethod method) { + switch (method) { + case advanced: + return getAdvancedMethodURI(); + case direct: + return getDirectMethodURI(); + default: + throw new IllegalArgumentException("Invalid discovery method: " + method); } - throw new IllegalArgumentException("Invalid discovery method."); } + /** + * Return the email address from which the {@link WKDAddress} was created. + * + * @return email address + */ + @Nonnull public String getEmail() { return localPart + '@' + domainPart; } @@ -117,6 +138,7 @@ public final class WKDAddress { * * @return URI using the direct lookup method */ + @Nonnull public URI getDirectMethodURI() { return URI.create(SCHEME + domainPart + DIRECT_WELL_KNOWN + zbase32LocalPart + "?l=" + percentEncodedLocalPart); } @@ -133,6 +155,7 @@ public final class WKDAddress { * * @return URI using the advanced lookup method */ + @Nonnull public URI getAdvancedMethodURI() { return URI.create(SCHEME + ADV_SUBDOMAIN + domainPart + ADV_WELL_KNOWN(domainPart) + zbase32LocalPart + "?l=" + percentEncodedLocalPart); } @@ -143,7 +166,8 @@ public final class WKDAddress { * @param string string * @return zbase32 encoded sha1 sum of the string */ - private String sha1AndZBase32Encode(String string) { + @Nonnull + private String sha1AndZBase32Encode(@Nonnull String string) { String lowerCase = string.toLowerCase(); byte[] bytes = lowerCase.getBytes(utf8); @@ -166,7 +190,8 @@ public final class WKDAddress { * @param string string * @return percent encoded string */ - private String percentEncode(String string) { + @Nonnull + private String percentEncode(@Nonnull String string) { try { return URLEncoder.encode(string, "UTF-8"); } catch (UnsupportedEncodingException e) { @@ -181,7 +206,9 @@ public final class WKDAddress { * @param email email address string * @return validated and split mail address */ - private static MailAddress parseMailAddress(String email) throws MalformedUserIdException { + @Nonnull + private static MailAddress parseMailAddress(@Nonnull String email) + throws MalformedUserIdException { Matcher matcher = PATTERN_EMAIL.matcher(email); if (!matcher.matches()) { throw new MalformedUserIdException("Invalid email address."); @@ -207,7 +234,7 @@ public final class WKDAddress { * @param localPart local part * @param domainPart domain part */ - MailAddress(String localPart, String domainPart) { + MailAddress(@Nonnull String localPart, @Nonnull String domainPart) { this.localPart = localPart; this.domainPart = domainPart; } @@ -218,6 +245,7 @@ public final class WKDAddress { * * @return local part */ + @Nonnull public String getLocalPart() { return localPart; } @@ -228,6 +256,7 @@ public final class WKDAddress { * * @return domain part */ + @Nonnull public String getDomainPart() { return domainPart; } diff --git a/wkd-java/src/main/java/pgp/wkd/WKDAddressHelper.java b/wkd-java/src/main/java/pgp/wkd/WKDAddressHelper.java index c1e4d74..21247eb 100644 --- a/wkd-java/src/main/java/pgp/wkd/WKDAddressHelper.java +++ b/wkd-java/src/main/java/pgp/wkd/WKDAddressHelper.java @@ -4,6 +4,7 @@ package pgp.wkd; +import javax.annotation.Nonnull; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -31,7 +32,9 @@ 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) throws MalformedUserIdException { + @Nonnull + public static String emailFromUserId(String userId) + throws MalformedUserIdException { Matcher matcher = PATTERN_USER_ID.matcher(userId); if (!matcher.matches()) { throw new MalformedUserIdException("User-ID does not follow excepted pattern \"Firstname Lastname [Optional Comment]\""); @@ -47,7 +50,9 @@ public class WKDAddressHelper { * @param userId user-id * @return WKD address for the user-id's email address. */ - public static WKDAddress wkdAddressFromUserId(String userId) throws MalformedUserIdException { + @Nonnull + 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/AbstractUriWKDFetcher.java b/wkd-java/src/main/java/pgp/wkd/discovery/AbstractUriCertificateFetcher.java similarity index 66% rename from wkd-java/src/main/java/pgp/wkd/AbstractUriWKDFetcher.java rename to wkd-java/src/main/java/pgp/wkd/discovery/AbstractUriCertificateFetcher.java index ebe4d35..5ea47a6 100644 --- a/wkd-java/src/main/java/pgp/wkd/AbstractUriWKDFetcher.java +++ b/wkd-java/src/main/java/pgp/wkd/discovery/AbstractUriCertificateFetcher.java @@ -2,26 +2,27 @@ // // SPDX-License-Identifier: Apache-2.0 -package pgp.wkd; +package pgp.wkd.discovery; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import pgp.wkd.WKDAddress; import java.io.IOException; import java.io.InputStream; import java.net.URI; -public abstract class AbstractUriWKDFetcher implements WKDFetcher { +public abstract class AbstractUriCertificateFetcher implements CertificateFetcher { - private static final Logger LOGGER = LoggerFactory.getLogger(WKDFetcher.class); + private static final Logger LOGGER = LoggerFactory.getLogger(CertificateFetcher.class); @Override - public InputStream fetch(WKDAddress address, DiscoveryMethod method) throws IOException { + public InputStream fetchCertificate(WKDAddress address, DiscoveryMethod method) throws IOException { URI uri = address.getUri(method); try { - return fetchUri(uri); + return fetchFromUri(uri); } catch (IOException e) { - LOGGER.debug("Could not fetch key using " + method + " method from " + uri.toString(), e); + LOGGER.debug("Could not fetch key using " + method + " method from " + uri, e); throw e; } } @@ -34,6 +35,6 @@ public abstract class AbstractUriWKDFetcher implements WKDFetcher { * @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; + protected abstract InputStream fetchFromUri(URI uri) throws IOException; } diff --git a/wkd-java/src/main/java/pgp/wkd/discovery/CertificateDiscoverer.java b/wkd-java/src/main/java/pgp/wkd/discovery/CertificateDiscoverer.java new file mode 100644 index 0000000..b598e4c --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/discovery/CertificateDiscoverer.java @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd.discovery; + +import pgp.wkd.MalformedUserIdException; +import pgp.wkd.WKDAddress; +import pgp.wkd.WKDAddressHelper; + +import java.util.ArrayList; +import java.util.List; + +public interface CertificateDiscoverer { + + DiscoveryResponse discover(DiscoveryMethod method, WKDAddress address); + + default DiscoveryResult discover(WKDAddress address) { + List results = new ArrayList<>(); + + // advanced method + DiscoveryResponse advanced = discover(DiscoveryMethod.advanced, address); + results.add(advanced); + + if (advanced.isSuccessful()) { + return new DiscoveryResult(results); + } + // direct method + results.add(discover(DiscoveryMethod.direct, address)); + + return new DiscoveryResult(results); + } + + default DiscoveryResult discoverByEmail(String email) throws MalformedUserIdException { + return discover(WKDAddress.fromEmail(email)); + } + + default DiscoveryResult discoverByUserId(String userId) throws MalformedUserIdException { + return discover(WKDAddressHelper.wkdAddressFromUserId(userId)); + } + +} diff --git a/wkd-java/src/main/java/pgp/wkd/AbstractDiscover.java b/wkd-java/src/main/java/pgp/wkd/discovery/CertificateDiscoveryImplementation.java similarity index 67% rename from wkd-java/src/main/java/pgp/wkd/AbstractDiscover.java rename to wkd-java/src/main/java/pgp/wkd/discovery/CertificateDiscoveryImplementation.java index 1ec9e9d..337676d 100644 --- a/wkd-java/src/main/java/pgp/wkd/AbstractDiscover.java +++ b/wkd-java/src/main/java/pgp/wkd/discovery/CertificateDiscoveryImplementation.java @@ -2,29 +2,33 @@ // // SPDX-License-Identifier: Apache-2.0 -package pgp.wkd; +package pgp.wkd.discovery; import pgp.certificate_store.Certificate; +import pgp.wkd.CertificateAndUserIds; +import pgp.wkd.MissingUserIdException; +import pgp.wkd.RejectedCertificate; +import pgp.wkd.WKDAddress; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; -public class AbstractDiscover implements Discover { +public class CertificateDiscoveryImplementation implements CertificateDiscoverer { - protected final CertificateReader reader; - protected final WKDFetcher fetcher; + protected final CertificateParser reader; + protected final CertificateFetcher fetcher; - public AbstractDiscover(CertificateReader reader, WKDFetcher fetcher) { + public CertificateDiscoveryImplementation(CertificateParser reader, CertificateFetcher fetcher) { this.reader = reader; this.fetcher = fetcher; } @Override - public WKDDiscoveryItem discover(DiscoveryMethod method, WKDAddress address) { + public DiscoveryResponse discover(DiscoveryMethod method, WKDAddress address) { try { - InputStream inputStream = fetcher.fetch(address, method); + InputStream inputStream = fetcher.fetchCertificate(address, method); List fetchedCertificates = reader.read(inputStream); List rejectedCertificates = new ArrayList<>(); @@ -50,10 +54,10 @@ public class AbstractDiscover implements Discover { } } - return WKDDiscoveryItem.success(method, address, acceptableCertificates, rejectedCertificates); + return DiscoveryResponse.success(method, address, acceptableCertificates, rejectedCertificates); } catch (IOException e) { - return WKDDiscoveryItem.failure(method, address, e); + return DiscoveryResponse.failure(method, address, e); } } } diff --git a/wkd-java/src/main/java/pgp/wkd/WKDFetcher.java b/wkd-java/src/main/java/pgp/wkd/discovery/CertificateFetcher.java similarity index 76% rename from wkd-java/src/main/java/pgp/wkd/WKDFetcher.java rename to wkd-java/src/main/java/pgp/wkd/discovery/CertificateFetcher.java index 6ec22dc..b2409fb 100644 --- a/wkd-java/src/main/java/pgp/wkd/WKDFetcher.java +++ b/wkd-java/src/main/java/pgp/wkd/discovery/CertificateFetcher.java @@ -2,7 +2,9 @@ // // SPDX-License-Identifier: Apache-2.0 -package pgp.wkd; +package pgp.wkd.discovery; + +import pgp.wkd.WKDAddress; import java.io.IOException; import java.io.InputStream; @@ -11,7 +13,7 @@ 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 { +public interface CertificateFetcher { /** * Attempt to fetch an OpenPGP certificate from the Web Key Directory. @@ -21,5 +23,5 @@ public interface WKDFetcher { * * @throws IOException in case of an error */ - InputStream fetch(WKDAddress address, DiscoveryMethod method) throws IOException; + InputStream fetchCertificate(WKDAddress address, DiscoveryMethod method) throws IOException; } diff --git a/wkd-java/src/main/java/pgp/wkd/CertificateReader.java b/wkd-java/src/main/java/pgp/wkd/discovery/CertificateParser.java similarity index 72% rename from wkd-java/src/main/java/pgp/wkd/CertificateReader.java rename to wkd-java/src/main/java/pgp/wkd/discovery/CertificateParser.java index e491dd3..001db2b 100644 --- a/wkd-java/src/main/java/pgp/wkd/CertificateReader.java +++ b/wkd-java/src/main/java/pgp/wkd/discovery/CertificateParser.java @@ -2,13 +2,15 @@ // // SPDX-License-Identifier: Apache-2.0 -package pgp.wkd; +package pgp.wkd.discovery; + +import pgp.wkd.CertificateAndUserIds; import java.io.IOException; import java.io.InputStream; import java.util.List; -public interface CertificateReader { +public interface CertificateParser { List read(InputStream inputStream) throws IOException; } diff --git a/wkd-java/src/main/java/pgp/wkd/discovery/DiscoveryMethod.java b/wkd-java/src/main/java/pgp/wkd/discovery/DiscoveryMethod.java new file mode 100644 index 0000000..1ac07f3 --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/discovery/DiscoveryMethod.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd.discovery; + +public enum DiscoveryMethod { + + /** + * Advanced method. + * This is the preferred method which MUST be checked first. + * + * @see OpenPGP Web Key Directory: Advanced Method + */ + advanced, + + /** + * Direct method. + * This is the fallback method. + * + * @see OpenPGP web Key Directory: Direct Method + */ + direct +} diff --git a/wkd-java/src/main/java/pgp/wkd/WKDDiscoveryItem.java b/wkd-java/src/main/java/pgp/wkd/discovery/DiscoveryResponse.java similarity index 50% rename from wkd-java/src/main/java/pgp/wkd/WKDDiscoveryItem.java rename to wkd-java/src/main/java/pgp/wkd/discovery/DiscoveryResponse.java index 32b4831..b0244d1 100644 --- a/wkd-java/src/main/java/pgp/wkd/WKDDiscoveryItem.java +++ b/wkd-java/src/main/java/pgp/wkd/discovery/DiscoveryResponse.java @@ -2,78 +2,95 @@ // // SPDX-License-Identifier: Apache-2.0 -package pgp.wkd; +package pgp.wkd.discovery; import pgp.certificate_store.Certificate; +import pgp.wkd.RejectedCertificate; +import pgp.wkd.WKDAddress; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; import java.util.List; -public final class WKDDiscoveryItem { +public final class DiscoveryResponse { private final DiscoveryMethod method; private final WKDAddress address; private final List certificates; private final List rejectedCertificates; - private final Throwable failure; + private final Throwable fetchingFailure; /** - * Constructor for a {@link WKDDiscoveryItem} object. + * Constructor for a {@link DiscoveryResponse} 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...) + * @param fetchingFailure general fetching error (e.g. connection error, 404...) */ - private WKDDiscoveryItem( + private DiscoveryResponse( DiscoveryMethod method, WKDAddress address, List certificates, List rejectedCertificates, - Throwable failure) { + Throwable fetchingFailure) { this.method = method; this.address = address; this.certificates = certificates; this.rejectedCertificates = rejectedCertificates; - this.failure = failure; + this.fetchingFailure = fetchingFailure; } - public static WKDDiscoveryItem success(DiscoveryMethod method, WKDAddress address, List certificates, List rejectedCertificates) { - return new WKDDiscoveryItem(method, address, certificates, rejectedCertificates, null); + public static DiscoveryResponse success( + @Nonnull DiscoveryMethod method, + @Nonnull WKDAddress address, + @Nonnull List certificates, + @Nonnull List rejectedCertificates) { + return new DiscoveryResponse(method, address, certificates, rejectedCertificates, null); } - public static WKDDiscoveryItem failure(DiscoveryMethod method, WKDAddress address, Throwable failure) { - return new WKDDiscoveryItem(method, address, null, null, failure); + public static DiscoveryResponse failure( + @Nonnull DiscoveryMethod method, + @Nonnull WKDAddress address, + @Nonnull Throwable fetchingFailure) { + return new DiscoveryResponse(method, address, Collections.emptyList(), Collections.emptyList(), fetchingFailure); } + @Nonnull public DiscoveryMethod getMethod() { return method; } + @Nonnull public WKDAddress getAddress() { return address; } public boolean isSuccessful() { - return !hasFailure(); + return !hasFetchingFailure(); } + @Nonnull public List getCertificates() { return certificates; } + @Nonnull public List getRejectedCertificates() { return rejectedCertificates; } - public Throwable getFailure() { - return failure; + @Nullable + public Throwable getFetchingFailure() { + return fetchingFailure; } public boolean hasCertificates() { return certificates != null && !certificates.isEmpty(); } - public boolean hasFailure() { - return failure != null; + public boolean hasFetchingFailure() { + return fetchingFailure != null; } } diff --git a/wkd-java/src/main/java/pgp/wkd/WKDDiscoveryResult.java b/wkd-java/src/main/java/pgp/wkd/discovery/DiscoveryResult.java similarity index 61% rename from wkd-java/src/main/java/pgp/wkd/WKDDiscoveryResult.java rename to wkd-java/src/main/java/pgp/wkd/discovery/DiscoveryResult.java index 91bf5ad..a619d4a 100644 --- a/wkd-java/src/main/java/pgp/wkd/WKDDiscoveryResult.java +++ b/wkd-java/src/main/java/pgp/wkd/discovery/DiscoveryResult.java @@ -2,25 +2,27 @@ // // SPDX-License-Identifier: Apache-2.0 -package pgp.wkd; +package pgp.wkd.discovery; import pgp.certificate_store.Certificate; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.List; -public class WKDDiscoveryResult { +public class DiscoveryResult { - private List items; + private List items; - public WKDDiscoveryResult(List items) { + public DiscoveryResult(@Nonnull List items) { this.items = items; } + @Nonnull public List getCertificates() { List certificates = new ArrayList<>(); - for (WKDDiscoveryItem item : items) { + for (DiscoveryResponse item : items) { if (item.isSuccessful()) { certificates.addAll(item.getCertificates()); } @@ -29,7 +31,7 @@ public class WKDDiscoveryResult { } public boolean isSuccessful() { - for (WKDDiscoveryItem item : items) { + for (DiscoveryResponse item : items) { if (item.isSuccessful() && item.hasCertificates()) { return true; } @@ -37,13 +39,15 @@ public class WKDDiscoveryResult { return false; } - public List getItems() { + @Nonnull + public List getItems() { return items; } - public List getFailedItems() { - List fails = new ArrayList<>(); - for (WKDDiscoveryItem item : items) { + @Nonnull + public List getFailedItems() { + List fails = new ArrayList<>(); + for (DiscoveryResponse item : items) { if (!item.isSuccessful()) { fails.add(item); } diff --git a/wkd-java/src/main/java/pgp/wkd/HttpUrlConnectionWKDFetcher.java b/wkd-java/src/main/java/pgp/wkd/discovery/HttpUrlConnectionCertificateFetcher.java similarity index 74% rename from wkd-java/src/main/java/pgp/wkd/HttpUrlConnectionWKDFetcher.java rename to wkd-java/src/main/java/pgp/wkd/discovery/HttpUrlConnectionCertificateFetcher.java index 52d9b81..aea62e7 100644 --- a/wkd-java/src/main/java/pgp/wkd/HttpUrlConnectionWKDFetcher.java +++ b/wkd-java/src/main/java/pgp/wkd/discovery/HttpUrlConnectionCertificateFetcher.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package pgp.wkd; +package pgp.wkd.discovery; import java.io.IOException; import java.io.InputStream; @@ -12,11 +12,11 @@ import java.net.URI; import java.net.URL; /** - * Implementation of {@link WKDFetcher} using Java's {@link HttpURLConnection}. + * Implementation of {@link CertificateFetcher} using Java's {@link HttpURLConnection}. */ -public class HttpUrlConnectionWKDFetcher extends AbstractUriWKDFetcher { +public class HttpUrlConnectionCertificateFetcher extends AbstractUriCertificateFetcher { - public InputStream fetchUri(URI uri) throws IOException { + public InputStream fetchFromUri(URI uri) throws IOException { URL url = uri.toURL(); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("GET");