This commit is contained in:
Paul Schaub 2022-04-05 16:11:06 +02:00
parent d0a161e87a
commit df85b4202d
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
9 changed files with 224 additions and 41 deletions

View file

@ -10,7 +10,7 @@ import pgp.wkd.WKDAddressHelper;
import pgp.wkd.cli.PGPainlessCertificateParser;
import pgp.wkd.cli.RuntimeIOException;
import pgp.wkd.discovery.CertificateDiscoverer;
import pgp.wkd.discovery.DefaultCertificateDiscoverer;
import pgp.wkd.discovery.ValidatingCertificateDiscoverer;
import pgp.wkd.discovery.DiscoveryResult;
import pgp.wkd.discovery.HttpsUrlConnectionCertificateFetcher;
import pgp.wkd.exception.MalformedUserIdException;
@ -39,7 +39,7 @@ public class Fetch implements Runnable {
)
boolean armor = false;
public static final CertificateDiscoverer DEFAULT_DISCOVERER = new DefaultCertificateDiscoverer(
public static final CertificateDiscoverer DEFAULT_DISCOVERER = new ValidatingCertificateDiscoverer(
new PGPainlessCertificateParser(), new HttpsUrlConnectionCertificateFetcher());
private static CertificateDiscoverer discoverer = DEFAULT_DISCOVERER;

View file

@ -12,7 +12,7 @@ import pgp.wkd.cli.PGPainlessCertificateParser;
import pgp.wkd.cli.WKDCLI;
import pgp.wkd.cli.command.Fetch;
import pgp.wkd.discovery.CertificateDiscoverer;
import pgp.wkd.discovery.DefaultCertificateDiscoverer;
import pgp.wkd.discovery.ValidatingCertificateDiscoverer;
import pgp.wkd.discovery.DiscoveryMethod;
import pgp.wkd.test_suite.TestCase;
import pgp.wkd.test_suite.TestSuite;
@ -43,7 +43,7 @@ public class TestSuiteTestRunner {
suite = generator.generateTestSuiteInDirectory(tempFile, DiscoveryMethod.direct);
// Fetch certificates from a local directory instead of the internetzzz.
CertificateDiscoverer discoverer = new DefaultCertificateDiscoverer(
CertificateDiscoverer discoverer = new ValidatingCertificateDiscoverer(
new PGPainlessCertificateParser(), new DirectoryBasedCertificateFetcher(tempPath));
Fetch.setCertificateDiscoverer(discoverer);
}

View file

@ -12,10 +12,27 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
/**
* Abstract implementation of the {@link CertificateFetcher} interface.
* The purpose of this class is to map {@link #fetchCertificate(WKDAddress, DiscoveryMethod)}
* and {@link #fetchPolicy(WKDAddress, DiscoveryMethod)} calls to {@link #fetchFromUri(URI)}.
*
* A concrete implementation of this class then simply needs to implement the latter method.
*/
public abstract class AbstractUriCertificateFetcher implements CertificateFetcher {
private static final Logger LOGGER = LoggerFactory.getLogger(CertificateFetcher.class);
/**
* Fetch the contents of the file that the {@link URI} points to from the remote server.
* @param uri uri
* @return file contents
*
* @throws java.net.ConnectException in case the file or host does not exist
* @throws IOException in case of an IO-error
*/
protected abstract InputStream fetchFromUri(URI uri) throws IOException;
@Override
public InputStream fetchCertificate(WKDAddress address, DiscoveryMethod method) throws IOException {
URI uri = address.getUri(method);
@ -38,14 +55,4 @@ public abstract class AbstractUriCertificateFetcher implements CertificateFetche
}
}
/**
* Fetch the contents of the file that the {@link URI} points to from the remote server.
* @param uri uri
* @return file contents
*
* @throws java.net.ConnectException in case the file or host does not exist
* @throws IOException in case of an IO-error
*/
protected abstract InputStream fetchFromUri(URI uri) throws IOException;
}

View file

@ -11,10 +11,26 @@ import pgp.wkd.WKDAddressHelper;
import java.util.ArrayList;
import java.util.List;
/**
* Interface which describes an API to discover OpenPGP certificates via the WKD.
*/
public interface CertificateDiscoverer {
/**
* Discover OpenPGP certificates by querying the given <pre>address</pre> via the given <pre>method</pre>.
*
* @param method discovery method
* @param address wkd address
* @return response
*/
DiscoveryResponse discover(DiscoveryMethod method, WKDAddress address);
/**
* Discover OpenPGP certificates by {@link WKDAddress}.
*
* @param address address
* @return discovery result
*/
default DiscoveryResult discover(WKDAddress address) {
List<DiscoveryResponse> results = new ArrayList<>();
@ -31,10 +47,26 @@ public interface CertificateDiscoverer {
return new DiscoveryResult(results);
}
/**
* Discover OpenPGP certificates by email address.
*
* @param email email address
* @return discovery result
*
* @throws MalformedUserIdException in case of a malformed email address
*/
default DiscoveryResult discoverByEmail(String email) throws MalformedUserIdException {
return discover(WKDAddress.fromEmail(email));
}
/**
* Discover OpenPGP certificates by user-id.
*
* @param userId user-id
* @return discovery result
*
* @throws MalformedUserIdException in case of a malformed user-id
*/
default DiscoveryResult discoverByUserId(String userId) throws MalformedUserIdException {
return discover(WKDAddressHelper.wkdAddressFromUserId(userId));
}

View file

@ -10,7 +10,22 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* Interface for an OpenPGP certificate parser class.
*/
public interface CertificateParser {
/**
* Read a list of OpenPGP certificates from the given input stream.
* The input stream contains binary OpenPGP certificate data.
*
* The result is a list of {@link CertificateAndUserIds}, where {@link CertificateAndUserIds#getUserIds()} only
* contains validly bound user-ids.
*
* @param inputStream input stream of binary OpenPGP certificates
* @return list of parsed certificates and their user-ids
*
* @throws IOException in case of an IO error
*/
List<CertificateAndUserIds> read(InputStream inputStream) throws IOException;
}

View file

@ -14,6 +14,9 @@ import javax.annotation.Nullable;
import java.net.URI;
import java.util.List;
/**
* A single response to a WKD query.
*/
public final class DiscoveryResponse {
private final DiscoveryMethod method;
@ -49,47 +52,100 @@ public final class DiscoveryResponse {
this.missingPolicyFileException = missingPolicyFileException;
}
/**
* Return the method that was used to fetch this response.
*
* @return method
*/
@Nonnull
public DiscoveryMethod getMethod() {
return method;
}
/**
* Return the WKD-Address which is queried.
*
* @return address
*/
@Nonnull
public WKDAddress getAddress() {
return address;
}
/**
* Return the URI that was queried against.
*
* @return URI
*/
public URI getUri() {
return getAddress().getUri(getMethod());
}
/**
* Return true, if the query was successful.
* That is, if there were no fetching errors, and if the server presented a policy.
*
* @return success
*/
public boolean isSuccessful() {
return !hasFetchingFailure() && hasPolicy();
}
/**
* Return the list of acceptable certificates that were returned by the WKD service.
*
* @return certificates
*/
@Nonnull
public List<Certificate> getCertificates() {
return certificates;
}
/**
* Return a list containing all rejected certificates returned by the WKD service.
* Certificates can be rejected for several reasons such as a missing user-id, or if the certificate is malformed.
*
* @return list of rejected certificates
*/
@Nonnull
public List<RejectedCertificate> getRejectedCertificates() {
return rejectedCertificates;
}
/**
* Return the cause of fetching errors, if any.
* A fetching failure might be e.g. a connection exception in case the WKD service cannot be reached.
*
* @return fetching failure
*/
@Nullable
public Throwable getFetchingFailure() {
return fetchingFailure;
}
/**
* Return true, if the result contains acceptable certificates.
*
* @return true if the response has certificates
*/
public boolean hasCertificates() {
return certificates != null && !certificates.isEmpty();
}
/**
* Return true, if there was a fetching failure.
*
* @return true if failure
*/
public boolean hasFetchingFailure() {
return fetchingFailure != null;
}
/**
* Return true, if the WKD service presented a policy.
*
* @return true if policy available
*/
public boolean hasPolicy() {
return getPolicy() != null;
}
@ -99,12 +155,18 @@ public final class DiscoveryResponse {
return policy;
}
public static Builder builder(@Nonnull DiscoveryMethod discoveryMethod, @Nonnull WKDAddress address) {
Builder builder = new Builder(discoveryMethod, address);
return builder;
/**
* Builder for {@link DiscoveryResponse}.
*
* @param discoveryMethod method used for discovery
* @param address WKD address
* @return builder
*/
static Builder builder(@Nonnull DiscoveryMethod discoveryMethod, @Nonnull WKDAddress address) {
return new Builder(discoveryMethod, address);
}
public static class Builder {
static class Builder {
private DiscoveryMethod discoveryMethod;
private WKDAddress address;
@ -114,37 +176,37 @@ public final class DiscoveryResponse {
private WKDPolicy policy;
private MissingPolicyFileException missingPolicyFileException;
public Builder(DiscoveryMethod discoveryMethod, WKDAddress address) {
Builder(DiscoveryMethod discoveryMethod, WKDAddress address) {
this.discoveryMethod = discoveryMethod;
this.address = address;
}
public Builder setAcceptableCertificates(List<Certificate> acceptableCertificates) {
Builder setAcceptableCertificates(List<Certificate> acceptableCertificates) {
this.acceptableCertificates = acceptableCertificates;
return this;
}
public Builder setRejectedCertificates(List<RejectedCertificate> rejectedCertificates) {
Builder setRejectedCertificates(List<RejectedCertificate> rejectedCertificates) {
this.rejectedCertificates = rejectedCertificates;
return this;
}
public Builder setFetchingFailure(Throwable throwable) {
Builder setFetchingFailure(Throwable throwable) {
this.fetchingFailure = throwable;
return this;
}
public Builder setPolicy(WKDPolicy policy) {
Builder setPolicy(WKDPolicy policy) {
this.policy = policy;
return this;
}
public Builder setMissingPolicyFileException(MissingPolicyFileException exception) {
Builder setMissingPolicyFileException(MissingPolicyFileException exception) {
this.missingPolicyFileException = exception;
return this;
}
public DiscoveryResponse build() {
DiscoveryResponse build() {
return new DiscoveryResponse(
discoveryMethod,
address,

View file

@ -14,14 +14,28 @@ import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
/**
* Result of discovering an OpenPGP certificate via WKD.
*/
public class DiscoveryResult {
private List<DiscoveryResponse> items;
private final List<DiscoveryResponse> items;
/**
* Create a {@link DiscoveryResult} from a list of {@link DiscoveryResponse DiscoveryResponses}.
* Usually the list contains one or two responses (one for each {@link DiscoveryMethod}.
*
* @param items responses
*/
public DiscoveryResult(@Nonnull List<DiscoveryResponse> items) {
this.items = items;
}
/**
* Return the list of acceptable certificates that were discovered.
*
* @return certificates
*/
@Nonnull
public List<Certificate> getCertificates() {
List<Certificate> certificates = new ArrayList<>();
@ -34,6 +48,11 @@ public class DiscoveryResult {
return certificates;
}
/**
* Return true, if at least one {@link DiscoveryResponse} was successful and contained acceptable certificates.
*
* @return success
*/
public boolean isSuccessful() {
for (DiscoveryResponse item : items) {
if (item.isSuccessful() && item.hasCertificates()) {
@ -69,7 +88,7 @@ public class DiscoveryResult {
private void throwCertNotFetchableException() {
Throwable cause = null;
for (DiscoveryResponse response : getItems()) {
for (DiscoveryResponse response : getResponses()) {
// Find the most "useful" exception.
// Rejections are more useful than fetching failures
if (!response.getRejectedCertificates().isEmpty()) {
@ -82,19 +101,13 @@ public class DiscoveryResult {
throw new CertNotFetchableException("Could not fetch certificates.", cause);
}
/**
* Return the list of responses.
*
* @return responses
*/
@Nonnull
public List<DiscoveryResponse> getItems() {
public List<DiscoveryResponse> getResponses() {
return items;
}
@Nonnull
public List<DiscoveryResponse> getFailedItems() {
List<DiscoveryResponse> fails = new ArrayList<>();
for (DiscoveryResponse item : items) {
if (!item.isSuccessful()) {
fails.add(item);
}
}
return fails;
}
}

View file

@ -16,12 +16,16 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class DefaultCertificateDiscoverer implements CertificateDiscoverer {
/**
* Default implementation of the {@link CertificateDiscoverer}.
* This implementation validates the received certificates.
*/
public class ValidatingCertificateDiscoverer implements CertificateDiscoverer {
protected final CertificateParser reader;
protected final CertificateFetcher fetcher;
public DefaultCertificateDiscoverer(CertificateParser reader, CertificateFetcher fetcher) {
public ValidatingCertificateDiscoverer(CertificateParser reader, CertificateFetcher fetcher) {
this.reader = reader;
this.fetcher = fetcher;
}

View file

@ -10,6 +10,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
* Class describing the contents of a policy file.
* The WKD policy file is found at ".well-known/policy"
*/
public final class WKDPolicy {
public static final String KEYWORD_MAILBOX_ONLY = "mailbox-only";
@ -83,23 +87,69 @@ public final class WKDPolicy {
return new WKDPolicy(mailboxOnly, daneOnly, authSubmit, protocolVersion, submissionAddress);
}
/**
* Return <pre>true</pre> if the <pre>mailbox-only</pre> flag is set.
*
* The mail server provider does only accept keys with only a mailbox in the User ID.
* In particular User IDs with a real name in addition to the mailbox will be rejected as invalid.
*
* @return whether mailbox-only flag is set
*/
public boolean isMailboxOnly() {
return mailboxOnly;
}
/**
* Return <pre>true</pre> if the <pre>dane-only</pre> flag is set.
*
* The mail server provider does not run a Web Key Directory but only an OpenPGP DANE service.
* The Web Key Directory Update protocol is used to update the keys for the DANE service.
*
* @return whether dane-only flag is set
*/
public boolean isDaneOnly() {
return daneOnly;
}
/**
* Return <pre>true</pre> if the <pre>auth-submit</pre> flag is set.
*
* The submission of the mail to the server is done using an authenticated connection.
* Thus the submitted key will be published immediately without any confirmation request.
*
* @return whether auth-submit flag is set
*/
public boolean isAuthSubmit() {
return authSubmit;
}
/**
* Return the protocol version.
*
* This keyword can be used to explicitly claim the support of a specific version of the Web Key Directory
* update protocol.
* This is in general not needed but implementations may have workarounds for providers which only support
* an old protocol version.
* If these providers update to a newer version they should add this keyword so that the implementation
* can disable the workaround.
* The value is an integer corresponding to the respective draft revision number.
*
* @return value of the protocol-version field
*/
@Nullable
public Integer getProtocolVersion() {
return protocolVersion;
}
/**
* Return the <pre>submission-address</pre>.
*
* An alternative way to specify the submission address.
* The value is the addr-spec part of the address to send requests to this server.
* If this keyword is used in addition to the submission-address file, both MUST have the same value.
*
* @return value of the submission-address field
*/
@Nullable
public String getSubmissionAddress() {
return submissionAddress;