Refactoring and dynamic test suite

This commit is contained in:
Paul Schaub 2022-03-17 15:27:28 +01:00
parent 38ef283313
commit 3af16baa20
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
23 changed files with 287 additions and 162 deletions

View file

@ -10,6 +10,7 @@ group 'org.pgpainless'
repositories { repositories {
mavenCentral() mavenCentral()
mavenLocal()
} }
dependencies { dependencies {
@ -20,7 +21,7 @@ dependencies {
testImplementation 'com.ginsberg:junit5-system-exit:1.1.2' testImplementation 'com.ginsberg:junit5-system-exit:1.1.2'
testImplementation 'org.mockito:mockito-core:4.3.1' 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 project(':wkd-java')
implementation "info.picocli:picocli:4.6.3" implementation "info.picocli:picocli:4.6.3"

View file

@ -12,14 +12,14 @@ import org.pgpainless.certificate_store.CertificateFactory;
import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.info.KeyRingInfo;
import pgp.certificate_store.Certificate; import pgp.certificate_store.Certificate;
import pgp.wkd.CertificateAndUserIds; import pgp.wkd.CertificateAndUserIds;
import pgp.wkd.CertificateReader; import pgp.wkd.discovery.CertificateParser;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class CertificateReaderImpl implements CertificateReader { public class CertificateParserImpl implements CertificateParser {
@Override @Override
public List<CertificateAndUserIds> read(InputStream inputStream) throws IOException { public List<CertificateAndUserIds> read(InputStream inputStream) throws IOException {
List<CertificateAndUserIds> certificatesAndUserIds = new ArrayList<>(); List<CertificateAndUserIds> certificatesAndUserIds = new ArrayList<>();

View file

@ -1,21 +1,21 @@
package pgp.wkd.cli; package pgp.wkd.cli;
import pgp.wkd.AbstractDiscover; import pgp.wkd.discovery.CertificateDiscoveryImplementation;
import pgp.wkd.CertificateReader; import pgp.wkd.discovery.CertificateParser;
import pgp.wkd.HttpUrlConnectionWKDFetcher; import pgp.wkd.discovery.HttpUrlConnectionCertificateFetcher;
import pgp.wkd.WKDFetcher; import pgp.wkd.discovery.CertificateFetcher;
public class DiscoverImpl extends AbstractDiscover { public class DiscoverImpl extends CertificateDiscoveryImplementation {
public DiscoverImpl() { public DiscoverImpl() {
super(new CertificateReaderImpl(), new HttpUrlConnectionWKDFetcher()); super(new CertificateParserImpl(), new HttpUrlConnectionCertificateFetcher());
} }
public DiscoverImpl(WKDFetcher fetcher) { public DiscoverImpl(CertificateFetcher fetcher) {
super(new CertificateReaderImpl(), fetcher); super(new CertificateParserImpl(), fetcher);
} }
public DiscoverImpl(CertificateReader certificateReader, WKDFetcher fetcher) { public DiscoverImpl(CertificateParser certificateParser, CertificateFetcher fetcher) {
super(certificateReader, fetcher); super(certificateParser, fetcher);
} }
} }

View file

@ -7,13 +7,13 @@ package pgp.wkd.cli.command;
import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.util.io.Streams; import org.bouncycastle.util.io.Streams;
import pgp.certificate_store.Certificate; import pgp.certificate_store.Certificate;
import pgp.wkd.Discover; import pgp.wkd.discovery.CertificateDiscoverer;
import pgp.wkd.HttpUrlConnectionWKDFetcher; import pgp.wkd.discovery.HttpUrlConnectionCertificateFetcher;
import pgp.wkd.MalformedUserIdException; import pgp.wkd.MalformedUserIdException;
import pgp.wkd.WKDAddress; import pgp.wkd.WKDAddress;
import pgp.wkd.WKDAddressHelper; import pgp.wkd.WKDAddressHelper;
import pgp.wkd.WKDDiscoveryResult; import pgp.wkd.discovery.DiscoveryResult;
import pgp.wkd.WKDFetcher; import pgp.wkd.discovery.CertificateFetcher;
import pgp.wkd.cli.CertNotFetchableException; import pgp.wkd.cli.CertNotFetchableException;
import pgp.wkd.cli.DiscoverImpl; import pgp.wkd.cli.DiscoverImpl;
import picocli.CommandLine; import picocli.CommandLine;
@ -42,14 +42,14 @@ public class Fetch implements Runnable {
boolean armor = false; boolean armor = false;
// TODO: Better way to inject fetcher implementation // TODO: Better way to inject fetcher implementation
public static WKDFetcher fetcher = new HttpUrlConnectionWKDFetcher(); public static CertificateFetcher fetcher = new HttpUrlConnectionCertificateFetcher();
@Override @Override
public void run() { public void run() {
Discover discover = new DiscoverImpl(fetcher); CertificateDiscoverer certificateDiscoverer = new DiscoverImpl(fetcher);
WKDAddress address = addressFromUserId(userId); WKDAddress address = addressFromUserId(userId);
WKDDiscoveryResult result = discover.discover(address); DiscoveryResult result = certificateDiscoverer.discover(address);
if (!result.isSuccessful()) { if (!result.isSuccessful()) {
throw new CertNotFetchableException("Cannot fetch cert."); throw new CertNotFetchableException("Cannot fetch cert.");

View file

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

View file

@ -5,7 +5,9 @@
package pgp.wkd.cli.test_suite; package pgp.wkd.cli.test_suite;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test; 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.Logger;
import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.logging.LoggerFactory;
import pgp.wkd.cli.WKDCLI; import pgp.wkd.cli.WKDCLI;
@ -17,6 +19,7 @@ import pgp.wkd.test_suite.TestSuiteGenerator;
import java.io.File; import java.io.File;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals;
@ -39,13 +42,16 @@ public class TestSuiteTestRunner {
suite = generator.generateTestSuiteInDirectory(tempFile, TestSuiteGenerator.Method.direct); suite = generator.generateTestSuiteInDirectory(tempFile, TestSuiteGenerator.Method.direct);
// Fetch certificates from a local directory instead of the internetzzz. // Fetch certificates from a local directory instead of the internetzzz.
Fetch.fetcher = new DirectoryBasedWkdFetcher(tempPath); Fetch.fetcher = new DirectoryBasedCertificateFetcher(tempPath);
} }
@Test @TestFactory
void runTestsAgainstTestSuite() { public Iterable<DynamicTest> testsFromTestSuite() {
for (TestCase testCase : suite.getTestCases()) { return suite.getTestCases()
LOGGER.info(() -> "Execute Test Case '" + testCase.getTestTitle() + "'"); .stream()
.map(testCase -> DynamicTest.dynamicTest(
testCase.getTestTitle(),
() -> {
String mail = testCase.getLookupMailAddress(); String mail = testCase.getLookupMailAddress();
int exitCode = WKDCLI.execute(new String[] { int exitCode = WKDCLI.execute(new String[] {
@ -58,5 +64,7 @@ public class TestSuiteTestRunner {
assertNotEquals(0, exitCode, testCase.getTestDescription()); assertNotEquals(0, exitCode, testCase.getTestDescription());
} }
} }
))
.collect(Collectors.toList());
} }
} }

View file

@ -23,6 +23,9 @@ dependencies {
// Z-Base32 // Z-Base32
implementation 'com.sandinh:zbase32-commons-codec:1.0.0' implementation 'com.sandinh:zbase32-commons-codec:1.0.0'
// @Nullable etc.
implementation "com.google.code.findbugs:jsr305:3.0.2"
} }
test { test {

View file

@ -9,6 +9,9 @@ import pgp.certificate_store.Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/**
* Tuple class which bundles a {@link Certificate} and a list of its valid or expired user ids.
*/
public class CertificateAndUserIds { public class CertificateAndUserIds {
private final Certificate certificate; private final Certificate certificate;
@ -19,10 +22,20 @@ public class CertificateAndUserIds {
this.userIds = userIds; this.userIds = userIds;
} }
/**
* Return a list containing the valid or expired user-ids of the certificate.
*
* @return user ids
*/
public List<String> getUserIds() { public List<String> getUserIds() {
return new ArrayList<>(userIds); return new ArrayList<>(userIds);
} }
/**
* Return the certificate itself.
*
* @return certificate
*/
public Certificate getCertificate() { public Certificate getCertificate() {
return certificate; return certificate;
} }

View file

@ -1,38 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<WKDDiscoveryItem> 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));
}
}

View file

@ -1,11 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.wkd;
public enum DiscoveryMethod {
advanced,
direct,
;
}

View file

@ -4,6 +4,10 @@
package pgp.wkd; 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 class MalformedUserIdException extends RuntimeException {
public MalformedUserIdException(String message) { public MalformedUserIdException(String message) {

View file

@ -6,21 +6,36 @@ package pgp.wkd;
import pgp.certificate_store.Certificate; 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 { public class RejectedCertificate {
private final Certificate certificate; 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.certificate = certificate;
this.failure = failure; this.reasonForRejection = reasonForRejection;
} }
/**
* Return the certificate.
* @return certificate
*/
public Certificate getCertificate() { public Certificate getCertificate() {
return certificate; return certificate;
} }
public Throwable getFailure() { /**
return failure; * Return the reason for rejection.
* @return rejection reason
*/
public Throwable getReasonForRejection() {
return reasonForRejection;
} }
} }

View file

@ -5,7 +5,9 @@
package pgp.wkd; package pgp.wkd;
import org.apache.commons.codec.binary.ZBase32; import org.apache.commons.codec.binary.ZBase32;
import pgp.wkd.discovery.DiscoveryMethod;
import javax.annotation.Nonnull;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URI; import java.net.URI;
import java.net.URLEncoder; import java.net.URLEncoder;
@ -26,7 +28,7 @@ public final class WKDAddress {
private static final String SCHEME = "https://"; private static final String SCHEME = "https://";
private static final String ADV_SUBDOMAIN = "openpgpkey."; private static final String ADV_SUBDOMAIN = "openpgpkey.";
private static final String DIRECT_WELL_KNOWN = "/.well-known/openpgpkey/hu/"; 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/"; 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.-]+$"); private static final Pattern PATTERN_DOMAIN_PART = Pattern.compile("[a-zA-Z0-9.-]+$");
// Android API lvl 10 does not yet know StandardCharsets.UTF_8 :/ // Android API lvl 10 does not yet know StandardCharsets.UTF_8 :/
@SuppressWarnings("CharsetObjectCanBeUsed")
private static final Charset utf8 = Charset.forName("UTF8"); 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 // 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(); 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 * @param domainPart domain part of the email address, case-insensitive
* *
* @return WKD address * @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()) { if (!PATTERN_LOCAL_PART.matcher(localPart).matches()) {
throw new IllegalArgumentException("Invalid local part."); throw new IllegalArgumentException("Invalid local part.");
} }
if (!PATTERN_DOMAIN_PART.matcher(domainPart).matches()) { if (!PATTERN_DOMAIN_PART.matcher(domainPart).matches()) {
throw new IllegalArgumentException("Invalid domain part."); throw new IllegalArgumentException("Invalid domain part.");
} }
return new WKDAddress(localPart, domainPart); return new WKDAddress(localPart, domainPart);
} }
@ -87,20 +93,35 @@ public final class WKDAddress {
* @param email email address, case sensitive * @param email email address, case sensitive
* @return WKDAddress object * @return WKDAddress object
*/ */
public static WKDAddress fromEmail(String email) throws MalformedUserIdException { public static WKDAddress fromEmail(@Nonnull String email) throws MalformedUserIdException {
MailAddress mailAddress = parseMailAddress(email); MailAddress mailAddress = parseMailAddress(email);
return new WKDAddress(mailAddress.getLocalPart(), mailAddress.getDomainPart()); return new WKDAddress(mailAddress.getLocalPart(), mailAddress.getDomainPart());
} }
public URI getUri(DiscoveryMethod method) { /**
if (method == DiscoveryMethod.advanced) { * 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(); return getAdvancedMethodURI();
} else if (method == DiscoveryMethod.direct) { case direct:
return getDirectMethodURI(); 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() { public String getEmail() {
return localPart + '@' + domainPart; return localPart + '@' + domainPart;
} }
@ -117,6 +138,7 @@ public final class WKDAddress {
* *
* @return URI using the direct lookup method * @return URI using the direct lookup method
*/ */
@Nonnull
public URI getDirectMethodURI() { public URI getDirectMethodURI() {
return URI.create(SCHEME + domainPart + DIRECT_WELL_KNOWN + zbase32LocalPart + "?l=" + percentEncodedLocalPart); 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 * @return URI using the advanced lookup method
*/ */
@Nonnull
public URI getAdvancedMethodURI() { public URI getAdvancedMethodURI() {
return URI.create(SCHEME + ADV_SUBDOMAIN + domainPart + ADV_WELL_KNOWN(domainPart) + zbase32LocalPart + "?l=" + percentEncodedLocalPart); 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 * @param string string
* @return zbase32 encoded sha1 sum of the 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(); String lowerCase = string.toLowerCase();
byte[] bytes = lowerCase.getBytes(utf8); byte[] bytes = lowerCase.getBytes(utf8);
@ -166,7 +190,8 @@ public final class WKDAddress {
* @param string string * @param string string
* @return percent encoded string * @return percent encoded string
*/ */
private String percentEncode(String string) { @Nonnull
private String percentEncode(@Nonnull String string) {
try { try {
return URLEncoder.encode(string, "UTF-8"); return URLEncoder.encode(string, "UTF-8");
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
@ -181,7 +206,9 @@ public final class WKDAddress {
* @param email email address string * @param email email address string
* @return validated and split mail address * @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); Matcher matcher = PATTERN_EMAIL.matcher(email);
if (!matcher.matches()) { if (!matcher.matches()) {
throw new MalformedUserIdException("Invalid email address."); throw new MalformedUserIdException("Invalid email address.");
@ -207,7 +234,7 @@ public final class WKDAddress {
* @param localPart local part * @param localPart local part
* @param domainPart domain part * @param domainPart domain part
*/ */
MailAddress(String localPart, String domainPart) { MailAddress(@Nonnull String localPart, @Nonnull String domainPart) {
this.localPart = localPart; this.localPart = localPart;
this.domainPart = domainPart; this.domainPart = domainPart;
} }
@ -218,6 +245,7 @@ public final class WKDAddress {
* *
* @return local part * @return local part
*/ */
@Nonnull
public String getLocalPart() { public String getLocalPart() {
return localPart; return localPart;
} }
@ -228,6 +256,7 @@ public final class WKDAddress {
* *
* @return domain part * @return domain part
*/ */
@Nonnull
public String getDomainPart() { public String getDomainPart() {
return domainPart; return domainPart;
} }

View file

@ -4,6 +4,7 @@
package pgp.wkd; package pgp.wkd;
import javax.annotation.Nonnull;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; 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 * @throws IllegalArgumentException in case the user-id does not match the expected format
* and does not contain an email address. * 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); Matcher matcher = PATTERN_USER_ID.matcher(userId);
if (!matcher.matches()) { if (!matcher.matches()) {
throw new MalformedUserIdException("User-ID does not follow excepted pattern \"Firstname Lastname <email.address> [Optional Comment]\""); throw new MalformedUserIdException("User-ID does not follow excepted pattern \"Firstname Lastname <email.address> [Optional Comment]\"");
@ -47,7 +50,9 @@ public class WKDAddressHelper {
* @param userId user-id * @param userId user-id
* @return WKD address for the user-id's email address. * @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); String email = emailFromUserId(userId);
return WKDAddress.fromEmail(email); return WKDAddress.fromEmail(email);
} }

View file

@ -2,26 +2,27 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package pgp.wkd; package pgp.wkd.discovery;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import pgp.wkd.WKDAddress;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI; 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 @Override
public InputStream fetch(WKDAddress address, DiscoveryMethod method) throws IOException { public InputStream fetchCertificate(WKDAddress address, DiscoveryMethod method) throws IOException {
URI uri = address.getUri(method); URI uri = address.getUri(method);
try { try {
return fetchUri(uri); return fetchFromUri(uri);
} catch (IOException e) { } 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; 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 java.net.ConnectException in case the file or host does not exist
* @throws IOException in case of an IO-error * @throws IOException in case of an IO-error
*/ */
protected abstract InputStream fetchUri(URI uri) throws IOException; protected abstract InputStream fetchFromUri(URI uri) throws IOException;
} }

View file

@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<DiscoveryResponse> 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));
}
}

View file

@ -2,29 +2,33 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package pgp.wkd; package pgp.wkd.discovery;
import pgp.certificate_store.Certificate; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class AbstractDiscover implements Discover { public class CertificateDiscoveryImplementation implements CertificateDiscoverer {
protected final CertificateReader reader; protected final CertificateParser reader;
protected final WKDFetcher fetcher; protected final CertificateFetcher fetcher;
public AbstractDiscover(CertificateReader reader, WKDFetcher fetcher) { public CertificateDiscoveryImplementation(CertificateParser reader, CertificateFetcher fetcher) {
this.reader = reader; this.reader = reader;
this.fetcher = fetcher; this.fetcher = fetcher;
} }
@Override @Override
public WKDDiscoveryItem discover(DiscoveryMethod method, WKDAddress address) { public DiscoveryResponse discover(DiscoveryMethod method, WKDAddress address) {
try { try {
InputStream inputStream = fetcher.fetch(address, method); InputStream inputStream = fetcher.fetchCertificate(address, method);
List<CertificateAndUserIds> fetchedCertificates = reader.read(inputStream); List<CertificateAndUserIds> fetchedCertificates = reader.read(inputStream);
List<RejectedCertificate> rejectedCertificates = new ArrayList<>(); List<RejectedCertificate> 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) { } catch (IOException e) {
return WKDDiscoveryItem.failure(method, address, e); return DiscoveryResponse.failure(method, address, e);
} }
} }
} }

View file

@ -2,7 +2,9 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package pgp.wkd; package pgp.wkd.discovery;
import pgp.wkd.WKDAddress;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -11,7 +13,7 @@ import java.io.InputStream;
* Abstract class for fetching OpenPGP certificates from the WKD. * Abstract class for fetching OpenPGP certificates from the WKD.
* This class can be extended to fetch files from remote servers using different HTTP clients. * 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. * 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 * @throws IOException in case of an error
*/ */
InputStream fetch(WKDAddress address, DiscoveryMethod method) throws IOException; InputStream fetchCertificate(WKDAddress address, DiscoveryMethod method) throws IOException;
} }

View file

@ -2,13 +2,15 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package pgp.wkd; package pgp.wkd.discovery;
import pgp.wkd.CertificateAndUserIds;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.List;
public interface CertificateReader { public interface CertificateParser {
List<CertificateAndUserIds> read(InputStream inputStream) throws IOException; List<CertificateAndUserIds> read(InputStream inputStream) throws IOException;
} }

View file

@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <a href="https://www.ietf.org/archive/id/draft-koch-openpgp-webkey-service-13.html#section-3.1-5">OpenPGP Web Key Directory: Advanced Method</a>
*/
advanced,
/**
* Direct method.
* This is the fallback method.
*
* @see <a href="https://www.ietf.org/archive/id/draft-koch-openpgp-webkey-service-13.html#section-3.1-10">OpenPGP web Key Directory: Direct Method</a>
*/
direct
}

View file

@ -2,78 +2,95 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package pgp.wkd; package pgp.wkd.discovery;
import pgp.certificate_store.Certificate; 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; import java.util.List;
public final class WKDDiscoveryItem { public final class DiscoveryResponse {
private final DiscoveryMethod method; private final DiscoveryMethod method;
private final WKDAddress address; private final WKDAddress address;
private final List<Certificate> certificates; private final List<Certificate> certificates;
private final List<RejectedCertificate> rejectedCertificates; private final List<RejectedCertificate> 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 method discovery method
* @param address wkd address used for discovery * @param address wkd address used for discovery
* @param certificates list of successfully fetched certificates * @param certificates list of successfully fetched certificates
* @param rejectedCertificates list of invalid fetched certificates (e.g. missing user-id) * @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, DiscoveryMethod method,
WKDAddress address, WKDAddress address,
List<Certificate> certificates, List<Certificate> certificates,
List<RejectedCertificate> rejectedCertificates, List<RejectedCertificate> rejectedCertificates,
Throwable failure) { Throwable fetchingFailure) {
this.method = method; this.method = method;
this.address = address; this.address = address;
this.certificates = certificates; this.certificates = certificates;
this.rejectedCertificates = rejectedCertificates; this.rejectedCertificates = rejectedCertificates;
this.failure = failure; this.fetchingFailure = fetchingFailure;
} }
public static WKDDiscoveryItem success(DiscoveryMethod method, WKDAddress address, List<Certificate> certificates, List<RejectedCertificate> rejectedCertificates) { public static DiscoveryResponse success(
return new WKDDiscoveryItem(method, address, certificates, rejectedCertificates, null); @Nonnull DiscoveryMethod method,
@Nonnull WKDAddress address,
@Nonnull List<Certificate> certificates,
@Nonnull List<RejectedCertificate> rejectedCertificates) {
return new DiscoveryResponse(method, address, certificates, rejectedCertificates, null);
} }
public static WKDDiscoveryItem failure(DiscoveryMethod method, WKDAddress address, Throwable failure) { public static DiscoveryResponse failure(
return new WKDDiscoveryItem(method, address, null, null, failure); @Nonnull DiscoveryMethod method,
@Nonnull WKDAddress address,
@Nonnull Throwable fetchingFailure) {
return new DiscoveryResponse(method, address, Collections.emptyList(), Collections.emptyList(), fetchingFailure);
} }
@Nonnull
public DiscoveryMethod getMethod() { public DiscoveryMethod getMethod() {
return method; return method;
} }
@Nonnull
public WKDAddress getAddress() { public WKDAddress getAddress() {
return address; return address;
} }
public boolean isSuccessful() { public boolean isSuccessful() {
return !hasFailure(); return !hasFetchingFailure();
} }
@Nonnull
public List<Certificate> getCertificates() { public List<Certificate> getCertificates() {
return certificates; return certificates;
} }
@Nonnull
public List<RejectedCertificate> getRejectedCertificates() { public List<RejectedCertificate> getRejectedCertificates() {
return rejectedCertificates; return rejectedCertificates;
} }
public Throwable getFailure() { @Nullable
return failure; public Throwable getFetchingFailure() {
return fetchingFailure;
} }
public boolean hasCertificates() { public boolean hasCertificates() {
return certificates != null && !certificates.isEmpty(); return certificates != null && !certificates.isEmpty();
} }
public boolean hasFailure() { public boolean hasFetchingFailure() {
return failure != null; return fetchingFailure != null;
} }
} }

View file

@ -2,25 +2,27 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package pgp.wkd; package pgp.wkd.discovery;
import pgp.certificate_store.Certificate; import pgp.certificate_store.Certificate;
import javax.annotation.Nonnull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class WKDDiscoveryResult { public class DiscoveryResult {
private List<WKDDiscoveryItem> items; private List<DiscoveryResponse> items;
public WKDDiscoveryResult(List<WKDDiscoveryItem> items) { public DiscoveryResult(@Nonnull List<DiscoveryResponse> items) {
this.items = items; this.items = items;
} }
@Nonnull
public List<Certificate> getCertificates() { public List<Certificate> getCertificates() {
List<Certificate> certificates = new ArrayList<>(); List<Certificate> certificates = new ArrayList<>();
for (WKDDiscoveryItem item : items) { for (DiscoveryResponse item : items) {
if (item.isSuccessful()) { if (item.isSuccessful()) {
certificates.addAll(item.getCertificates()); certificates.addAll(item.getCertificates());
} }
@ -29,7 +31,7 @@ public class WKDDiscoveryResult {
} }
public boolean isSuccessful() { public boolean isSuccessful() {
for (WKDDiscoveryItem item : items) { for (DiscoveryResponse item : items) {
if (item.isSuccessful() && item.hasCertificates()) { if (item.isSuccessful() && item.hasCertificates()) {
return true; return true;
} }
@ -37,13 +39,15 @@ public class WKDDiscoveryResult {
return false; return false;
} }
public List<WKDDiscoveryItem> getItems() { @Nonnull
public List<DiscoveryResponse> getItems() {
return items; return items;
} }
public List<WKDDiscoveryItem> getFailedItems() { @Nonnull
List<WKDDiscoveryItem> fails = new ArrayList<>(); public List<DiscoveryResponse> getFailedItems() {
for (WKDDiscoveryItem item : items) { List<DiscoveryResponse> fails = new ArrayList<>();
for (DiscoveryResponse item : items) {
if (!item.isSuccessful()) { if (!item.isSuccessful()) {
fails.add(item); fails.add(item);
} }

View file

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package pgp.wkd; package pgp.wkd.discovery;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -12,11 +12,11 @@ import java.net.URI;
import java.net.URL; 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(); URL url = uri.toURL();
HttpURLConnection con = (HttpURLConnection) url.openConnection(); HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET"); con.setRequestMethod("GET");