Add support for fetching policy objects

This commit is contained in:
Paul Schaub 2022-03-21 16:03:11 +01:00
parent cb996733fb
commit d7cddf26bb
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
9 changed files with 318 additions and 22 deletions

View file

@ -10,6 +10,7 @@ import pgp.wkd.discovery.CertificateFetcher;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI; import java.net.URI;
@ -26,6 +27,15 @@ public class DirectoryBasedCertificateFetcher implements CertificateFetcher {
@Override @Override
public InputStream fetchCertificate(WKDAddress address, DiscoveryMethod method) throws IOException { 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); URI uri = address.getUri(method);
String path = uri.getPath().substring(1); // get rid of leading slash at start of path String path = uri.getPath().substring(1); // get rid of leading slash at start of path
File file = rootPath.resolve(path).toFile(); File file = rootPath.resolve(path).toFile();

View file

@ -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. * Fetch the contents of the file that the {@link URI} points to from the remote server.
* @param uri uri * @param uri uri

View file

@ -24,4 +24,15 @@ public interface CertificateFetcher {
* @throws IOException in case of an error * @throws IOException in case of an error
*/ */
InputStream fetchCertificate(WKDAddress address, DiscoveryMethod method) throws IOException; 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;
} }

View file

@ -6,6 +6,7 @@ package pgp.wkd.discovery;
import pgp.certificate_store.Certificate; import pgp.certificate_store.Certificate;
import pgp.wkd.CertificateAndUserIds; import pgp.wkd.CertificateAndUserIds;
import pgp.wkd.exception.MissingPolicyFileException;
import pgp.wkd.exception.RejectedCertificateException; import pgp.wkd.exception.RejectedCertificateException;
import pgp.wkd.RejectedCertificate; import pgp.wkd.RejectedCertificate;
import pgp.wkd.WKDAddress; import pgp.wkd.WKDAddress;
@ -27,9 +28,18 @@ public class DefaultCertificateDiscoverer implements CertificateDiscoverer {
@Override @Override
public DiscoveryResponse discover(DiscoveryMethod method, WKDAddress address) { 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 { try {
InputStream inputStream = fetcher.fetchCertificate(address, method); InputStream certificateIn = fetcher.fetchCertificate(address, method);
List<CertificateAndUserIds> fetchedCertificates = reader.read(inputStream); List<CertificateAndUserIds> fetchedCertificates = reader.read(certificateIn);
List<RejectedCertificate> rejectedCertificates = new ArrayList<>(); List<RejectedCertificate> rejectedCertificates = new ArrayList<>();
List<Certificate> acceptableCertificates = new ArrayList<>(); List<Certificate> 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) { } 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));
} }
} }
} }

View file

@ -7,11 +7,11 @@ package pgp.wkd.discovery;
import pgp.certificate_store.Certificate; import pgp.certificate_store.Certificate;
import pgp.wkd.RejectedCertificate; import pgp.wkd.RejectedCertificate;
import pgp.wkd.WKDAddress; import pgp.wkd.WKDAddress;
import pgp.wkd.exception.MissingPolicyFileException;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.net.URI; import java.net.URI;
import java.util.Collections;
import java.util.List; import java.util.List;
public final class DiscoveryResponse { public final class DiscoveryResponse {
@ -21,6 +21,8 @@ public final class DiscoveryResponse {
private final List<Certificate> certificates; private final List<Certificate> certificates;
private final List<RejectedCertificate> rejectedCertificates; private final List<RejectedCertificate> rejectedCertificates;
private final Throwable fetchingFailure; private final Throwable fetchingFailure;
private final WKDPolicy policy;
private final MissingPolicyFileException missingPolicyFileException;
/** /**
* Constructor for a {@link DiscoveryResponse} object. * Constructor for a {@link DiscoveryResponse} object.
@ -35,27 +37,16 @@ public final class DiscoveryResponse {
WKDAddress address, WKDAddress address,
List<Certificate> certificates, List<Certificate> certificates,
List<RejectedCertificate> rejectedCertificates, List<RejectedCertificate> rejectedCertificates,
Throwable fetchingFailure) { Throwable fetchingFailure,
WKDPolicy policy,
MissingPolicyFileException missingPolicyFileException) {
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.fetchingFailure = fetchingFailure; this.fetchingFailure = fetchingFailure;
} this.policy = policy;
this.missingPolicyFileException = missingPolicyFileException;
public static DiscoveryResponse success(
@Nonnull DiscoveryMethod method,
@Nonnull WKDAddress address,
@Nonnull List<Certificate> certificates,
@Nonnull List<RejectedCertificate> 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);
} }
@Nonnull @Nonnull
@ -73,7 +64,7 @@ public final class DiscoveryResponse {
} }
public boolean isSuccessful() { public boolean isSuccessful() {
return !hasFetchingFailure(); return !hasFetchingFailure() && hasPolicy();
} }
@Nonnull @Nonnull
@ -98,4 +89,71 @@ public final class DiscoveryResponse {
public boolean hasFetchingFailure() { public boolean hasFetchingFailure() {
return fetchingFailure != null; 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<Certificate> acceptableCertificates;
private List<RejectedCertificate> 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<Certificate> acceptableCertificates) {
this.acceptableCertificates = acceptableCertificates;
return this;
}
public Builder setRejectedCertificates(List<RejectedCertificate> 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
);
}
}
} }

View file

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

View file

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.wkd.exception;
public class MissingPolicyFileException extends RuntimeException {
public MissingPolicyFileException(Throwable cause) {
super(cause);
}
}

View file

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

View file

@ -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 URI getAddress(String mail);
public abstract File resolve(Path path); public abstract File resolve(Path path);
@ -47,10 +56,12 @@ public abstract class WkdDirectoryStructure {
public static class DirectMethod extends WkdDirectoryStructure { public static class DirectMethod extends WkdDirectoryStructure {
private final File hu; private final File hu;
private final File policy;
public DirectMethod(File rootDirectory, String domain) { public DirectMethod(File rootDirectory, String domain) {
super(rootDirectory, domain); super(rootDirectory, domain);
this.hu = new File(openpgpkey, "hu"); this.hu = new File(openpgpkey, "hu");
this.policy = new File(openpgpkey, "policy");
} }
@Override @Override
@ -72,6 +83,8 @@ public abstract class WkdDirectoryStructure {
mkdir(wellKnown); mkdir(wellKnown);
mkdir(openpgpkey); mkdir(openpgpkey);
mkdir(hu); mkdir(hu);
touch(policy);
} }
@Override @Override
@ -89,11 +102,13 @@ public abstract class WkdDirectoryStructure {
private final File domainFile; private final File domainFile;
private final File hu; private final File hu;
private final File policy;
public AdvancedMethod(File rootDir, String domain) { public AdvancedMethod(File rootDir, String domain) {
super(rootDir, domain); super(rootDir, domain);
this.domainFile = new File(openpgpkey, domain); this.domainFile = new File(openpgpkey, domain);
this.hu = new File(domainFile, "hu"); this.hu = new File(domainFile, "hu");
this.policy = new File(domainFile, "policy");
} }
@Override @Override
@ -116,6 +131,8 @@ public abstract class WkdDirectoryStructure {
mkdir(openpgpkey); mkdir(openpgpkey);
mkdir(domainFile); mkdir(domainFile);
mkdir(hu); mkdir(hu);
touch(policy);
} }
@Override @Override