diff --git a/wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/DirectoryBasedCertificateFetcher.java b/wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/DirectoryBasedCertificateFetcher.java index 998e028..b468778 100644 --- a/wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/DirectoryBasedCertificateFetcher.java +++ b/wkd-java-cli/src/test/java/pgp/wkd/cli/test_suite/DirectoryBasedCertificateFetcher.java @@ -10,6 +10,7 @@ import pgp.wkd.discovery.CertificateFetcher; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -26,6 +27,15 @@ public class DirectoryBasedCertificateFetcher implements CertificateFetcher { @Override public InputStream fetchCertificate(WKDAddress address, DiscoveryMethod method) throws IOException { + return inputStreamFromFile(address, method); + } + + @Override + public InputStream fetchPolicy(WKDAddress address, DiscoveryMethod method) throws IOException { + return inputStreamFromFile(address, method); + } + + private InputStream inputStreamFromFile(WKDAddress address, DiscoveryMethod method) throws FileNotFoundException { URI uri = address.getUri(method); String path = uri.getPath().substring(1); // get rid of leading slash at start of path File file = rootPath.resolve(path).toFile(); diff --git a/wkd-java/src/main/java/pgp/wkd/discovery/AbstractUriCertificateFetcher.java b/wkd-java/src/main/java/pgp/wkd/discovery/AbstractUriCertificateFetcher.java index 5ea47a6..7383d89 100644 --- a/wkd-java/src/main/java/pgp/wkd/discovery/AbstractUriCertificateFetcher.java +++ b/wkd-java/src/main/java/pgp/wkd/discovery/AbstractUriCertificateFetcher.java @@ -27,6 +27,17 @@ public abstract class AbstractUriCertificateFetcher implements CertificateFetche } } + @Override + public InputStream fetchPolicy(WKDAddress address, DiscoveryMethod method) throws IOException { + URI uri = address.getPolicyUri(method); + try { + return fetchFromUri(uri); + } catch (IOException e) { + LOGGER.debug("Could not fetch policy file using " + method + " method from " + uri, e); + throw e; + } + } + /** * Fetch the contents of the file that the {@link URI} points to from the remote server. * @param uri uri diff --git a/wkd-java/src/main/java/pgp/wkd/discovery/CertificateFetcher.java b/wkd-java/src/main/java/pgp/wkd/discovery/CertificateFetcher.java index b2409fb..59f0f2a 100644 --- a/wkd-java/src/main/java/pgp/wkd/discovery/CertificateFetcher.java +++ b/wkd-java/src/main/java/pgp/wkd/discovery/CertificateFetcher.java @@ -24,4 +24,15 @@ public interface CertificateFetcher { * @throws IOException in case of an error */ InputStream fetchCertificate(WKDAddress address, DiscoveryMethod method) throws IOException; + + /** + * Fetch the policy file belonging to the address and discovery method. + * + * @param address WKDAddress object + * @param method discovery method + * @return input stream containing the WKD policy file + * + * @throws IOException in case of an error + */ + InputStream fetchPolicy(WKDAddress address, DiscoveryMethod method) throws IOException; } diff --git a/wkd-java/src/main/java/pgp/wkd/discovery/DefaultCertificateDiscoverer.java b/wkd-java/src/main/java/pgp/wkd/discovery/DefaultCertificateDiscoverer.java index ca84c03..ae5ff9b 100644 --- a/wkd-java/src/main/java/pgp/wkd/discovery/DefaultCertificateDiscoverer.java +++ b/wkd-java/src/main/java/pgp/wkd/discovery/DefaultCertificateDiscoverer.java @@ -6,6 +6,7 @@ package pgp.wkd.discovery; import pgp.certificate_store.Certificate; import pgp.wkd.CertificateAndUserIds; +import pgp.wkd.exception.MissingPolicyFileException; import pgp.wkd.exception.RejectedCertificateException; import pgp.wkd.RejectedCertificate; import pgp.wkd.WKDAddress; @@ -27,9 +28,18 @@ public class DefaultCertificateDiscoverer implements CertificateDiscoverer { @Override public DiscoveryResponse discover(DiscoveryMethod method, WKDAddress address) { + DiscoveryResponse.Builder builder = DiscoveryResponse.builder(method, address); + + fetchPolicy(method, address, builder); + fetchCertificates(method, address, builder); + + return builder.build(); + } + + private void fetchCertificates(DiscoveryMethod method, WKDAddress address, DiscoveryResponse.Builder builder) { try { - InputStream inputStream = fetcher.fetchCertificate(address, method); - List fetchedCertificates = reader.read(inputStream); + InputStream certificateIn = fetcher.fetchCertificate(address, method); + List fetchedCertificates = reader.read(certificateIn); List rejectedCertificates = new ArrayList<>(); List acceptableCertificates = new ArrayList<>(); @@ -54,10 +64,21 @@ public class DefaultCertificateDiscoverer implements CertificateDiscoverer { } } - return DiscoveryResponse.success(method, address, acceptableCertificates, rejectedCertificates); + builder.setAcceptableCertificates(acceptableCertificates); + builder.setRejectedCertificates(rejectedCertificates); } catch (IOException e) { - return DiscoveryResponse.failure(method, address, e); + builder.setFetchingFailure(e); + } + } + + private void fetchPolicy(DiscoveryMethod method, WKDAddress address, DiscoveryResponse.Builder builder) { + try { + InputStream policyIn = fetcher.fetchPolicy(address, method); + WKDPolicy policy = WKDPolicy.fromInputStream(policyIn); + builder.setPolicy(policy); + } catch (IOException e) { + builder.setMissingPolicyFileException(new MissingPolicyFileException(e)); } } } diff --git a/wkd-java/src/main/java/pgp/wkd/discovery/DiscoveryResponse.java b/wkd-java/src/main/java/pgp/wkd/discovery/DiscoveryResponse.java index fcd5582..1dd0fb6 100644 --- a/wkd-java/src/main/java/pgp/wkd/discovery/DiscoveryResponse.java +++ b/wkd-java/src/main/java/pgp/wkd/discovery/DiscoveryResponse.java @@ -7,11 +7,11 @@ package pgp.wkd.discovery; import pgp.certificate_store.Certificate; import pgp.wkd.RejectedCertificate; import pgp.wkd.WKDAddress; +import pgp.wkd.exception.MissingPolicyFileException; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.net.URI; -import java.util.Collections; import java.util.List; public final class DiscoveryResponse { @@ -21,6 +21,8 @@ public final class DiscoveryResponse { private final List certificates; private final List rejectedCertificates; private final Throwable fetchingFailure; + private final WKDPolicy policy; + private final MissingPolicyFileException missingPolicyFileException; /** * Constructor for a {@link DiscoveryResponse} object. @@ -35,27 +37,16 @@ public final class DiscoveryResponse { WKDAddress address, List certificates, List rejectedCertificates, - Throwable fetchingFailure) { + Throwable fetchingFailure, + WKDPolicy policy, + MissingPolicyFileException missingPolicyFileException) { this.method = method; this.address = address; this.certificates = certificates; this.rejectedCertificates = rejectedCertificates; this.fetchingFailure = fetchingFailure; - } - - 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 DiscoveryResponse failure( - @Nonnull DiscoveryMethod method, - @Nonnull WKDAddress address, - @Nonnull Throwable fetchingFailure) { - return new DiscoveryResponse(method, address, Collections.emptyList(), Collections.emptyList(), fetchingFailure); + this.policy = policy; + this.missingPolicyFileException = missingPolicyFileException; } @Nonnull @@ -73,7 +64,7 @@ public final class DiscoveryResponse { } public boolean isSuccessful() { - return !hasFetchingFailure(); + return !hasFetchingFailure() && hasPolicy(); } @Nonnull @@ -98,4 +89,71 @@ public final class DiscoveryResponse { public boolean hasFetchingFailure() { return fetchingFailure != null; } + + public boolean hasPolicy() { + return getPolicy() != null; + } + + @Nullable + public WKDPolicy getPolicy() { + return policy; + } + + public static Builder builder(@Nonnull DiscoveryMethod discoveryMethod, @Nonnull WKDAddress address) { + Builder builder = new Builder(discoveryMethod, address); + return builder; + } + + public static class Builder { + + private DiscoveryMethod discoveryMethod; + private WKDAddress address; + private List acceptableCertificates; + private List rejectedCertificates; + private Throwable fetchingFailure; + private WKDPolicy policy; + private MissingPolicyFileException missingPolicyFileException; + + public Builder(DiscoveryMethod discoveryMethod, WKDAddress address) { + this.discoveryMethod = discoveryMethod; + this.address = address; + } + + public Builder setAcceptableCertificates(List acceptableCertificates) { + this.acceptableCertificates = acceptableCertificates; + return this; + } + + public Builder setRejectedCertificates(List rejectedCertificates) { + this.rejectedCertificates = rejectedCertificates; + return this; + } + + public Builder setFetchingFailure(Throwable throwable) { + this.fetchingFailure = throwable; + return this; + } + + public Builder setPolicy(WKDPolicy policy) { + this.policy = policy; + return this; + } + + public Builder setMissingPolicyFileException(MissingPolicyFileException exception) { + this.missingPolicyFileException = exception; + return this; + } + + public DiscoveryResponse build() { + return new DiscoveryResponse( + discoveryMethod, + address, + acceptableCertificates, + rejectedCertificates, + fetchingFailure, + policy, + missingPolicyFileException + ); + } + } } diff --git a/wkd-java/src/main/java/pgp/wkd/discovery/WKDPolicy.java b/wkd-java/src/main/java/pgp/wkd/discovery/WKDPolicy.java new file mode 100644 index 0000000..763a24e --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/discovery/WKDPolicy.java @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd.discovery; + +import javax.annotation.Nullable; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +public final class WKDPolicy { + + public static final String KEYWORD_MAILBOX_ONLY = "mailbox-only"; + public static final String KEYWORD_DANE_ONLY = "dane-only"; + public static final String KEYWORD_AUTH_SUBMIT = "auth-submit"; + public static final String KEYWORD_PROTOCOL_VERSION = "protocol-version"; + public static final String KEYWORD_SUBMISSION_ADDRESS = "submission-address"; + + private final boolean mailboxOnly; + private final boolean daneOnly; + private final boolean authSubmit; + private final Integer protocolVersion; + private final String submissionAddress; + + private WKDPolicy(boolean mailboxOnly, boolean daneOnly, boolean authSubmit, Integer protocolVersion, String submissionAddress) { + this.mailboxOnly = mailboxOnly; + this.daneOnly = daneOnly; + this.authSubmit = authSubmit; + this.protocolVersion = protocolVersion; + this.submissionAddress = submissionAddress; + } + + public static WKDPolicy fromInputStream(InputStream inputStream) throws IOException { + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + + boolean mailboxOnly = false; + boolean daneOnly = false; + boolean authSubmit = false; + Integer protocolVersion = null; + String submissionAddress = null; + + String line; + while ((line = bufferedReader.readLine()) != null) { + String prepared = line.trim(); + if (prepared.equals(KEYWORD_MAILBOX_ONLY)) { + mailboxOnly = true; + continue; + } + if (prepared.equals(KEYWORD_DANE_ONLY)) { + daneOnly = true; + continue; + } + if (prepared.equals(KEYWORD_AUTH_SUBMIT)) { + authSubmit = true; + continue; + } + if (prepared.startsWith(KEYWORD_PROTOCOL_VERSION + ": ")) { + protocolVersion = Integer.parseInt(prepared.substring(KEYWORD_PROTOCOL_VERSION.length() + 2)); + continue; + } + if (prepared.startsWith(KEYWORD_SUBMISSION_ADDRESS + ": ")) { + submissionAddress = prepared.substring(KEYWORD_SUBMISSION_ADDRESS.length() + 2).trim(); + } + } + + return new WKDPolicy(mailboxOnly, daneOnly, authSubmit, protocolVersion, submissionAddress); + } + + public boolean isMailboxOnly() { + return mailboxOnly; + } + + public boolean isDaneOnly() { + return daneOnly; + } + + public boolean isAuthSubmit() { + return authSubmit; + } + + @Nullable + public Integer getProtocolVersion() { + return protocolVersion; + } + + @Nullable + public String getSubmissionAddress() { + return submissionAddress; + } +} diff --git a/wkd-java/src/main/java/pgp/wkd/exception/MissingPolicyFileException.java b/wkd-java/src/main/java/pgp/wkd/exception/MissingPolicyFileException.java new file mode 100644 index 0000000..2b02e34 --- /dev/null +++ b/wkd-java/src/main/java/pgp/wkd/exception/MissingPolicyFileException.java @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd.exception; + +public class MissingPolicyFileException extends RuntimeException { + + public MissingPolicyFileException(Throwable cause) { + super(cause); + } +} diff --git a/wkd-java/src/test/java/pgp/wkd/WKDPolicyTest.java b/wkd-java/src/test/java/pgp/wkd/WKDPolicyTest.java new file mode 100644 index 0000000..94d63c9 --- /dev/null +++ b/wkd-java/src/test/java/pgp/wkd/WKDPolicyTest.java @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd; + +import org.junit.jupiter.api.Test; +import pgp.wkd.discovery.WKDPolicy; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class WKDPolicyTest { + + @Test + public void parseEmptyPolicy() throws IOException { + ByteArrayInputStream empty = new ByteArrayInputStream(new byte[0]); + WKDPolicy policy = WKDPolicy.fromInputStream(empty); + + assertFalse(policy.isMailboxOnly()); + assertFalse(policy.isDaneOnly()); + assertFalse(policy.isAuthSubmit()); + assertNull(policy.getProtocolVersion()); + assertNull(policy.getSubmissionAddress()); + } + + @Test + public void parseSparsePolicy() throws IOException { + ByteArrayInputStream sparse = new ByteArrayInputStream( + "protocol-version: 13\n".getBytes(StandardCharsets.UTF_8)); + WKDPolicy policy = WKDPolicy.fromInputStream(sparse); + + assertFalse(policy.isMailboxOnly()); + assertFalse(policy.isDaneOnly()); + assertFalse(policy.isAuthSubmit()); + assertEquals(13, policy.getProtocolVersion()); + assertNull(policy.getSubmissionAddress()); + } + + @Test + public void parseFullPolicy() throws IOException { + ByteArrayInputStream full = new ByteArrayInputStream( + ("mailbox-only\n" + + "dane-only\n" + + "auth-submit\n" + + "protocol-version: 12\n" + + "submission-address: key-submission-example.org@directory.example.org") + .getBytes(StandardCharsets.UTF_8)); + WKDPolicy policy = WKDPolicy.fromInputStream(full); + + assertTrue(policy.isMailboxOnly()); + assertTrue(policy.isDaneOnly()); + assertTrue(policy.isAuthSubmit()); + + assertEquals(12, policy.getProtocolVersion()); + assertEquals("key-submission-example.org@directory.example.org", policy.getSubmissionAddress()); + } +} diff --git a/wkd-test-suite/src/main/java/pgp/wkd/test_suite/WkdDirectoryStructure.java b/wkd-test-suite/src/main/java/pgp/wkd/test_suite/WkdDirectoryStructure.java index 7c00af7..5e09fa2 100644 --- a/wkd-test-suite/src/main/java/pgp/wkd/test_suite/WkdDirectoryStructure.java +++ b/wkd-test-suite/src/main/java/pgp/wkd/test_suite/WkdDirectoryStructure.java @@ -40,6 +40,15 @@ public abstract class WkdDirectoryStructure { } } + protected void touch(File file) throws IOException { + if (!file.exists() && !file.createNewFile()) { + throw new IOException("Cannot create file '" + file.getAbsolutePath() + "'."); + } + if (!file.isFile()) { + throw new IOException("Cannot create file '" + file.getAbsolutePath() + "': Is not a file."); + } + } + public abstract URI getAddress(String mail); public abstract File resolve(Path path); @@ -47,10 +56,12 @@ public abstract class WkdDirectoryStructure { public static class DirectMethod extends WkdDirectoryStructure { private final File hu; + private final File policy; public DirectMethod(File rootDirectory, String domain) { super(rootDirectory, domain); this.hu = new File(openpgpkey, "hu"); + this.policy = new File(openpgpkey, "policy"); } @Override @@ -72,6 +83,8 @@ public abstract class WkdDirectoryStructure { mkdir(wellKnown); mkdir(openpgpkey); mkdir(hu); + + touch(policy); } @Override @@ -89,11 +102,13 @@ public abstract class WkdDirectoryStructure { private final File domainFile; private final File hu; + private final File policy; public AdvancedMethod(File rootDir, String domain) { super(rootDir, domain); this.domainFile = new File(openpgpkey, domain); this.hu = new File(domainFile, "hu"); + this.policy = new File(domainFile, "policy"); } @Override @@ -116,6 +131,8 @@ public abstract class WkdDirectoryStructure { mkdir(openpgpkey); mkdir(domainFile); mkdir(hu); + + touch(policy); } @Override