mirror of
https://codeberg.org/PGPainless/wkd-java.git
synced 2024-11-22 07:12:05 +01:00
Modularize WKD discovery
This commit is contained in:
parent
30e8a55ef6
commit
d1d953e802
24 changed files with 511 additions and 111 deletions
|
@ -20,7 +20,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-core:$pgpainlessVersion")
|
implementation("org.pgpainless:pgpainless-cert-d:0.1.0")
|
||||||
implementation project(':wkd-java')
|
implementation project(':wkd-java')
|
||||||
implementation "info.picocli:picocli:4.6.3"
|
implementation "info.picocli:picocli:4.6.3"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.wkd.cli;
|
||||||
|
|
||||||
|
import org.bouncycastle.openpgp.PGPException;
|
||||||
|
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||||
|
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
||||||
|
import org.pgpainless.PGPainless;
|
||||||
|
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 java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CertificateReaderImpl implements CertificateReader {
|
||||||
|
@Override
|
||||||
|
public List<CertificateAndUserIds> read(InputStream inputStream) throws IOException {
|
||||||
|
List<CertificateAndUserIds> certificatesAndUserIds = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
PGPPublicKeyRingCollection certificates = PGPainless.readKeyRing().publicKeyRingCollection(inputStream);
|
||||||
|
for (PGPPublicKeyRing certificate : certificates) {
|
||||||
|
KeyRingInfo info = PGPainless.inspectKeyRing(certificate);
|
||||||
|
Certificate parsedCert = CertificateFactory.certificateFromPublicKeyRing(certificate);
|
||||||
|
List<String> userIds = info.getValidAndExpiredUserIds();
|
||||||
|
certificatesAndUserIds.add(new CertificateAndUserIds(parsedCert, userIds));
|
||||||
|
}
|
||||||
|
return certificatesAndUserIds;
|
||||||
|
} catch (PGPException e) {
|
||||||
|
throw new IOException("Cannot parse certificates.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
wkd-java-cli/src/main/java/pgp/wkd/cli/DiscoverImpl.java
Normal file
21
wkd-java-cli/src/main/java/pgp/wkd/cli/DiscoverImpl.java
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package pgp.wkd.cli;
|
||||||
|
|
||||||
|
import pgp.wkd.AbstractDiscover;
|
||||||
|
import pgp.wkd.CertificateReader;
|
||||||
|
import pgp.wkd.HttpUrlConnectionWKDFetcher;
|
||||||
|
import pgp.wkd.WKDFetcher;
|
||||||
|
|
||||||
|
public class DiscoverImpl extends AbstractDiscover {
|
||||||
|
|
||||||
|
public DiscoverImpl() {
|
||||||
|
super(new CertificateReaderImpl(), new HttpUrlConnectionWKDFetcher());
|
||||||
|
}
|
||||||
|
|
||||||
|
public DiscoverImpl(WKDFetcher fetcher) {
|
||||||
|
super(new CertificateReaderImpl(), fetcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DiscoverImpl(CertificateReader certificateReader, WKDFetcher fetcher) {
|
||||||
|
super(certificateReader, fetcher);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
package pgp.wkd.cli;
|
package pgp.wkd.cli;
|
||||||
|
|
||||||
|
import pgp.wkd.MissingUserIdException;
|
||||||
import pgp.wkd.cli.command.Fetch;
|
import pgp.wkd.cli.command.Fetch;
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
|
|
||||||
|
|
|
@ -5,21 +5,21 @@
|
||||||
package pgp.wkd.cli.command;
|
package pgp.wkd.cli.command;
|
||||||
|
|
||||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
import org.bouncycastle.util.io.Streams;
|
||||||
import org.pgpainless.PGPainless;
|
import pgp.certificate_store.Certificate;
|
||||||
import org.pgpainless.key.info.KeyRingInfo;
|
import pgp.wkd.Discover;
|
||||||
import pgp.wkd.AbstractWKDFetcher;
|
|
||||||
import pgp.wkd.HttpUrlConnectionWKDFetcher;
|
import pgp.wkd.HttpUrlConnectionWKDFetcher;
|
||||||
|
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.WKDFetcher;
|
||||||
import pgp.wkd.cli.CertNotFetchableException;
|
import pgp.wkd.cli.CertNotFetchableException;
|
||||||
import pgp.wkd.cli.MissingUserIdException;
|
import pgp.wkd.cli.DiscoverImpl;
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@CommandLine.Command(
|
@CommandLine.Command(
|
||||||
name = "fetch",
|
name = "fetch",
|
||||||
|
@ -42,47 +42,44 @@ 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 AbstractWKDFetcher fetcher = new HttpUrlConnectionWKDFetcher();
|
public static WKDFetcher fetcher = new HttpUrlConnectionWKDFetcher();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
String email;
|
Discover discover = new DiscoverImpl(fetcher);
|
||||||
try {
|
|
||||||
email = WKDAddressHelper.emailFromUserId(userId);
|
WKDAddress address = addressFromUserId(userId);
|
||||||
} catch (IllegalArgumentException e) {
|
WKDDiscoveryResult result = discover.discover(address);
|
||||||
email = userId;
|
|
||||||
|
if (!result.isSuccessful()) {
|
||||||
|
throw new CertNotFetchableException("Cannot fetch cert.");
|
||||||
}
|
}
|
||||||
|
|
||||||
WKDAddress address = WKDAddress.fromEmail(email);
|
try {
|
||||||
try (InputStream inputStream = fetcher.fetch(address)) {
|
|
||||||
PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(inputStream);
|
|
||||||
if (cert == null) {
|
|
||||||
throw new CertNotFetchableException("Fetched data does not contain an OpenPGP certificate.");
|
|
||||||
}
|
|
||||||
KeyRingInfo info = PGPainless.inspectKeyRing(cert);
|
|
||||||
|
|
||||||
List<String> userIds = info.getValidAndExpiredUserIds();
|
|
||||||
boolean containsEmail = false;
|
|
||||||
for (String certUserId : userIds) {
|
|
||||||
if (certUserId.contains("<" + email + ">") || certUserId.equals(email)) {
|
|
||||||
containsEmail = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!containsEmail) {
|
|
||||||
throw new MissingUserIdException("Fetched certificate does not contain email address " + email);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (armor) {
|
if (armor) {
|
||||||
OutputStream out = new ArmoredOutputStream(System.out);
|
OutputStream out = new ArmoredOutputStream(System.out);
|
||||||
cert.encode(out);
|
for (Certificate certificate : result.getCertificates()) {
|
||||||
|
Streams.pipeAll(certificate.getInputStream(), out);
|
||||||
|
}
|
||||||
out.close();
|
out.close();
|
||||||
} else {
|
} else {
|
||||||
cert.encode(System.out);
|
for (Certificate certificate : result.getCertificates()) {
|
||||||
|
Streams.pipeAll(certificate.getInputStream(), System.out);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new CertNotFetchableException("Certificate cannot be fetched.", e);
|
throw new CertNotFetchableException("Certificate cannot be fetched.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WKDAddress addressFromUserId(String userId) {
|
||||||
|
String email;
|
||||||
|
try {
|
||||||
|
email = WKDAddressHelper.emailFromUserId(userId);
|
||||||
|
} catch (MalformedUserIdException e) {
|
||||||
|
email = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return WKDAddress.fromEmail(email);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
|
|
||||||
package pgp.wkd.cli.test_suite;
|
package pgp.wkd.cli.test_suite;
|
||||||
|
|
||||||
import pgp.wkd.AbstractWKDFetcher;
|
import pgp.wkd.DiscoveryMethod;
|
||||||
|
import pgp.wkd.WKDAddress;
|
||||||
|
import pgp.wkd.WKDFetcher;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
@ -13,7 +15,7 @@ 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 extends AbstractWKDFetcher {
|
public class DirectoryBasedWkdFetcher implements WKDFetcher {
|
||||||
|
|
||||||
// The directory containing the .well-known subdirectory
|
// The directory containing the .well-known subdirectory
|
||||||
private final Path rootPath;
|
private final Path rootPath;
|
||||||
|
@ -23,7 +25,8 @@ public class DirectoryBasedWkdFetcher extends AbstractWKDFetcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected InputStream fetchUri(URI uri) throws IOException {
|
public InputStream fetch(WKDAddress address, DiscoveryMethod method) throws IOException {
|
||||||
|
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
|
||||||
FileInputStream fileIn = new FileInputStream(file);
|
FileInputStream fileIn = new FileInputStream(file);
|
||||||
|
|
|
@ -19,6 +19,7 @@ dependencies {
|
||||||
// Logging
|
// Logging
|
||||||
api "org.slf4j:slf4j-api:$slf4jVersion"
|
api "org.slf4j:slf4j-api:$slf4jVersion"
|
||||||
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
|
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
|
||||||
|
api "org.pgpainless:pgp-certificate-store:0.1.0"
|
||||||
|
|
||||||
// Z-Base32
|
// Z-Base32
|
||||||
implementation 'com.sandinh:zbase32-commons-codec:1.0.0'
|
implementation 'com.sandinh:zbase32-commons-codec:1.0.0'
|
||||||
|
|
59
wkd-java/src/main/java/pgp/wkd/AbstractDiscover.java
Normal file
59
wkd-java/src/main/java/pgp/wkd/AbstractDiscover.java
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.wkd;
|
||||||
|
|
||||||
|
import pgp.certificate_store.Certificate;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AbstractDiscover implements Discover {
|
||||||
|
|
||||||
|
protected final CertificateReader reader;
|
||||||
|
protected final WKDFetcher fetcher;
|
||||||
|
|
||||||
|
public AbstractDiscover(CertificateReader reader, WKDFetcher fetcher) {
|
||||||
|
this.reader = reader;
|
||||||
|
this.fetcher = fetcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WKDDiscoveryItem discover(DiscoveryMethod method, WKDAddress address) {
|
||||||
|
try {
|
||||||
|
InputStream inputStream = fetcher.fetch(address, method);
|
||||||
|
List<CertificateAndUserIds> fetchedCertificates = reader.read(inputStream);
|
||||||
|
|
||||||
|
List<RejectedCertificate> rejectedCertificates = new ArrayList<>();
|
||||||
|
List<Certificate> acceptableCertificates = new ArrayList<>();
|
||||||
|
|
||||||
|
String email = address.getEmail();
|
||||||
|
|
||||||
|
for (CertificateAndUserIds certAndUserIds : fetchedCertificates) {
|
||||||
|
Certificate certificate = certAndUserIds.getCertificate();
|
||||||
|
boolean containsEmail = false;
|
||||||
|
for (String userId : certAndUserIds.getUserIds()) {
|
||||||
|
if (userId.contains("<" + email + ">") || userId.equals(email)) {
|
||||||
|
containsEmail = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!containsEmail) {
|
||||||
|
rejectedCertificates.add(new RejectedCertificate(certificate,
|
||||||
|
new MissingUserIdException("Certificate " + certificate.getFingerprint() +
|
||||||
|
" does not contain user-id with email '" + email + "'")));
|
||||||
|
} else {
|
||||||
|
acceptableCertificates.add(certificate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return WKDDiscoveryItem.success(method, address, acceptableCertificates, rejectedCertificates);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
return WKDDiscoveryItem.failure(method, address, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
wkd-java/src/main/java/pgp/wkd/AbstractUriWKDFetcher.java
Normal file
39
wkd-java/src/main/java/pgp/wkd/AbstractUriWKDFetcher.java
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.wkd;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
public abstract class AbstractUriWKDFetcher implements WKDFetcher {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(WKDFetcher.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream fetch(WKDAddress address, DiscoveryMethod method) throws IOException {
|
||||||
|
URI uri = address.getUri(method);
|
||||||
|
try {
|
||||||
|
return fetchUri(uri);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.debug("Could not fetch key using " + method + " method from " + uri.toString(), e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 fetchUri(URI uri) throws IOException;
|
||||||
|
|
||||||
|
}
|
|
@ -1,60 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package pgp.wkd;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.URI;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 abstract class AbstractWKDFetcher {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractWKDFetcher.class);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to fetch an OpenPGP certificate from the Web Key Directory.
|
|
||||||
*
|
|
||||||
* @param address WKDAddress object
|
|
||||||
* @return input stream containing the certificate in its binary representation
|
|
||||||
*
|
|
||||||
* @throws IOException in case of an error
|
|
||||||
*/
|
|
||||||
public InputStream fetch(WKDAddress address) throws IOException {
|
|
||||||
URI advanced = address.getAdvancedMethodURI();
|
|
||||||
IOException advancedException;
|
|
||||||
try {
|
|
||||||
return fetchUri(advanced);
|
|
||||||
} catch (IOException e) {
|
|
||||||
advancedException = e;
|
|
||||||
LOGGER.debug("Could not fetch key using advanced method from " + advanced.toString(), advancedException);
|
|
||||||
}
|
|
||||||
|
|
||||||
URI direct = address.getDirectMethodURI();
|
|
||||||
try {
|
|
||||||
return fetchUri(direct);
|
|
||||||
} catch (IOException e) {
|
|
||||||
// we would like to use addSuppressed eventually, but Android API 10 does not support it
|
|
||||||
// e.addSuppressed(advancedException);
|
|
||||||
LOGGER.debug("Could not fetch key using direct method from " + direct.toString(), e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 fetchUri(URI uri) throws IOException;
|
|
||||||
}
|
|
29
wkd-java/src/main/java/pgp/wkd/CertificateAndUserIds.java
Normal file
29
wkd-java/src/main/java/pgp/wkd/CertificateAndUserIds.java
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.wkd;
|
||||||
|
|
||||||
|
import pgp.certificate_store.Certificate;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CertificateAndUserIds {
|
||||||
|
|
||||||
|
private final Certificate certificate;
|
||||||
|
private final List<String> userIds;
|
||||||
|
|
||||||
|
public CertificateAndUserIds(Certificate certificate, List<String> userIds) {
|
||||||
|
this.certificate = certificate;
|
||||||
|
this.userIds = userIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getUserIds() {
|
||||||
|
return new ArrayList<>(userIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Certificate getCertificate() {
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
}
|
14
wkd-java/src/main/java/pgp/wkd/CertificateReader.java
Normal file
14
wkd-java/src/main/java/pgp/wkd/CertificateReader.java
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.wkd;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface CertificateReader {
|
||||||
|
|
||||||
|
List<CertificateAndUserIds> read(InputStream inputStream) throws IOException;
|
||||||
|
}
|
38
wkd-java/src/main/java/pgp/wkd/Discover.java
Normal file
38
wkd-java/src/main/java/pgp/wkd/Discover.java
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
11
wkd-java/src/main/java/pgp/wkd/DiscoveryMethod.java
Normal file
11
wkd-java/src/main/java/pgp/wkd/DiscoveryMethod.java
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.wkd;
|
||||||
|
|
||||||
|
public enum DiscoveryMethod {
|
||||||
|
advanced,
|
||||||
|
direct,
|
||||||
|
;
|
||||||
|
}
|
|
@ -12,9 +12,9 @@ import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of {@link AbstractWKDFetcher} using Java's {@link HttpURLConnection}.
|
* Implementation of {@link WKDFetcher} using Java's {@link HttpURLConnection}.
|
||||||
*/
|
*/
|
||||||
public class HttpUrlConnectionWKDFetcher extends AbstractWKDFetcher {
|
public class HttpUrlConnectionWKDFetcher extends AbstractUriWKDFetcher {
|
||||||
|
|
||||||
public InputStream fetchUri(URI uri) throws IOException {
|
public InputStream fetchUri(URI uri) throws IOException {
|
||||||
URL url = uri.toURL();
|
URL url = uri.toURL();
|
||||||
|
|
12
wkd-java/src/main/java/pgp/wkd/MalformedUserIdException.java
Normal file
12
wkd-java/src/main/java/pgp/wkd/MalformedUserIdException.java
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.wkd;
|
||||||
|
|
||||||
|
public class MalformedUserIdException extends RuntimeException {
|
||||||
|
|
||||||
|
public MalformedUserIdException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package pgp.wkd.cli;
|
package pgp.wkd;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exception that gets thrown when an OpenPGP certificate is not carrying a User-ID binding for the email address
|
* Exception that gets thrown when an OpenPGP certificate is not carrying a User-ID binding for the email address
|
26
wkd-java/src/main/java/pgp/wkd/RejectedCertificate.java
Normal file
26
wkd-java/src/main/java/pgp/wkd/RejectedCertificate.java
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.wkd;
|
||||||
|
|
||||||
|
import pgp.certificate_store.Certificate;
|
||||||
|
|
||||||
|
public class RejectedCertificate {
|
||||||
|
|
||||||
|
private final Certificate certificate;
|
||||||
|
private final Throwable failure;
|
||||||
|
|
||||||
|
public RejectedCertificate(Certificate certificate, Throwable failure) {
|
||||||
|
this.certificate = certificate;
|
||||||
|
this.failure = failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Certificate getCertificate() {
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Throwable getFailure() {
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
}
|
|
@ -87,11 +87,24 @@ 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) {
|
public static WKDAddress fromEmail(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 getAdvancedMethodURI();
|
||||||
|
} else if (method == DiscoveryMethod.direct) {
|
||||||
|
return getDirectMethodURI();
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Invalid discovery method.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return localPart + '@' + domainPart;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an {@link URI} pointing to the certificate using the direct lookup method.
|
* Get an {@link URI} pointing to the certificate using the direct lookup method.
|
||||||
* The direct method requires that a WKD is available on the same domain as the users mail server.
|
* The direct method requires that a WKD is available on the same domain as the users mail server.
|
||||||
|
@ -168,10 +181,10 @@ 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) {
|
private static MailAddress parseMailAddress(String email) throws MalformedUserIdException {
|
||||||
Matcher matcher = PATTERN_EMAIL.matcher(email);
|
Matcher matcher = PATTERN_EMAIL.matcher(email);
|
||||||
if (!matcher.matches()) {
|
if (!matcher.matches()) {
|
||||||
throw new IllegalArgumentException("Invalid email address.");
|
throw new MalformedUserIdException("Invalid email address.");
|
||||||
}
|
}
|
||||||
|
|
||||||
String localPart = matcher.group(1);
|
String localPart = matcher.group(1);
|
||||||
|
|
|
@ -31,10 +31,10 @@ 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) {
|
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 IllegalArgumentException("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]\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
String email = matcher.group(1);
|
String email = matcher.group(1);
|
||||||
|
@ -47,7 +47,7 @@ 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) {
|
public static WKDAddress wkdAddressFromUserId(String userId) throws MalformedUserIdException {
|
||||||
String email = emailFromUserId(userId);
|
String email = emailFromUserId(userId);
|
||||||
return WKDAddress.fromEmail(email);
|
return WKDAddress.fromEmail(email);
|
||||||
}
|
}
|
||||||
|
|
79
wkd-java/src/main/java/pgp/wkd/WKDDiscoveryItem.java
Normal file
79
wkd-java/src/main/java/pgp/wkd/WKDDiscoveryItem.java
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.wkd;
|
||||||
|
|
||||||
|
import pgp.certificate_store.Certificate;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class WKDDiscoveryItem {
|
||||||
|
|
||||||
|
private final DiscoveryMethod method;
|
||||||
|
private final WKDAddress address;
|
||||||
|
private final List<Certificate> certificates;
|
||||||
|
private final List<RejectedCertificate> rejectedCertificates;
|
||||||
|
private final Throwable failure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for a {@link WKDDiscoveryItem} 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...)
|
||||||
|
*/
|
||||||
|
private WKDDiscoveryItem(
|
||||||
|
DiscoveryMethod method,
|
||||||
|
WKDAddress address,
|
||||||
|
List<Certificate> certificates,
|
||||||
|
List<RejectedCertificate> rejectedCertificates,
|
||||||
|
Throwable failure) {
|
||||||
|
this.method = method;
|
||||||
|
this.address = address;
|
||||||
|
this.certificates = certificates;
|
||||||
|
this.rejectedCertificates = rejectedCertificates;
|
||||||
|
this.failure = failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WKDDiscoveryItem success(DiscoveryMethod method, WKDAddress address, List<Certificate> certificates, List<RejectedCertificate> rejectedCertificates) {
|
||||||
|
return new WKDDiscoveryItem(method, address, certificates, rejectedCertificates, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WKDDiscoveryItem failure(DiscoveryMethod method, WKDAddress address, Throwable failure) {
|
||||||
|
return new WKDDiscoveryItem(method, address, null, null, failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DiscoveryMethod getMethod() {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WKDAddress getAddress() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccessful() {
|
||||||
|
return !hasFailure();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Certificate> getCertificates() {
|
||||||
|
return certificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RejectedCertificate> getRejectedCertificates() {
|
||||||
|
return rejectedCertificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Throwable getFailure() {
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasCertificates() {
|
||||||
|
return certificates != null && !certificates.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasFailure() {
|
||||||
|
return failure != null;
|
||||||
|
}
|
||||||
|
}
|
53
wkd-java/src/main/java/pgp/wkd/WKDDiscoveryResult.java
Normal file
53
wkd-java/src/main/java/pgp/wkd/WKDDiscoveryResult.java
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.wkd;
|
||||||
|
|
||||||
|
import pgp.certificate_store.Certificate;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class WKDDiscoveryResult {
|
||||||
|
|
||||||
|
private List<WKDDiscoveryItem> items;
|
||||||
|
|
||||||
|
public WKDDiscoveryResult(List<WKDDiscoveryItem> items) {
|
||||||
|
this.items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Certificate> getCertificates() {
|
||||||
|
List<Certificate> certificates = new ArrayList<>();
|
||||||
|
|
||||||
|
for (WKDDiscoveryItem item : items) {
|
||||||
|
if (item.isSuccessful()) {
|
||||||
|
certificates.addAll(item.getCertificates());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return certificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccessful() {
|
||||||
|
for (WKDDiscoveryItem item : items) {
|
||||||
|
if (item.isSuccessful() && item.hasCertificates()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<WKDDiscoveryItem> getItems() {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<WKDDiscoveryItem> getFailedItems() {
|
||||||
|
List<WKDDiscoveryItem> fails = new ArrayList<>();
|
||||||
|
for (WKDDiscoveryItem item : items) {
|
||||||
|
if (!item.isSuccessful()) {
|
||||||
|
fails.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fails;
|
||||||
|
}
|
||||||
|
}
|
25
wkd-java/src/main/java/pgp/wkd/WKDFetcher.java
Normal file
25
wkd-java/src/main/java/pgp/wkd/WKDFetcher.java
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.wkd;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
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 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to fetch an OpenPGP certificate from the Web Key Directory.
|
||||||
|
*
|
||||||
|
* @param address WKDAddress object
|
||||||
|
* @return input stream containing the certificate in its binary representation
|
||||||
|
*
|
||||||
|
* @throws IOException in case of an error
|
||||||
|
*/
|
||||||
|
InputStream fetch(WKDAddress address, DiscoveryMethod method) throws IOException;
|
||||||
|
}
|
|
@ -66,14 +66,14 @@ public class WKDAddressTest {
|
||||||
"John Doe [The Real One]",
|
"John Doe [The Real One]",
|
||||||
"<John Doe",
|
"<John Doe",
|
||||||
"Don Joeh>")) {
|
"Don Joeh>")) {
|
||||||
assertThrows(IllegalArgumentException.class, () -> WKDAddressHelper.wkdAddressFromUserId(brokenUserId));
|
assertThrows(MalformedUserIdException.class, () -> WKDAddressHelper.wkdAddressFromUserId(brokenUserId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFromInvalidEmail() {
|
public void testFromInvalidEmail() {
|
||||||
for (String brokenEmail : Arrays.asList("john.doe", "@example.org", "john doe@example.org", "john.doe@example org")) {
|
for (String brokenEmail : Arrays.asList("john.doe", "@example.org", "john doe@example.org", "john.doe@example org")) {
|
||||||
assertThrows(IllegalArgumentException.class, () -> WKDAddress.fromEmail(brokenEmail));
|
assertThrows(MalformedUserIdException.class, () -> WKDAddress.fromEmail(brokenEmail));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue