mirror of
https://codeberg.org/PGPainless/wkd-java.git
synced 2024-12-22 13:47:58 +01:00
Refactoring and dynamic test suite
This commit is contained in:
parent
38ef283313
commit
3af16baa20
23 changed files with 287 additions and 162 deletions
|
@ -10,6 +10,7 @@ group 'org.pgpainless'
|
|||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -20,7 +21,7 @@ dependencies {
|
|||
testImplementation 'com.ginsberg:junit5-system-exit:1.1.2'
|
||||
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 "info.picocli:picocli:4.6.3"
|
||||
|
||||
|
|
|
@ -12,14 +12,14 @@ 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 pgp.wkd.discovery.CertificateParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CertificateReaderImpl implements CertificateReader {
|
||||
public class CertificateParserImpl implements CertificateParser {
|
||||
@Override
|
||||
public List<CertificateAndUserIds> read(InputStream inputStream) throws IOException {
|
||||
List<CertificateAndUserIds> certificatesAndUserIds = new ArrayList<>();
|
|
@ -1,21 +1,21 @@
|
|||
package pgp.wkd.cli;
|
||||
|
||||
import pgp.wkd.AbstractDiscover;
|
||||
import pgp.wkd.CertificateReader;
|
||||
import pgp.wkd.HttpUrlConnectionWKDFetcher;
|
||||
import pgp.wkd.WKDFetcher;
|
||||
import pgp.wkd.discovery.CertificateDiscoveryImplementation;
|
||||
import pgp.wkd.discovery.CertificateParser;
|
||||
import pgp.wkd.discovery.HttpUrlConnectionCertificateFetcher;
|
||||
import pgp.wkd.discovery.CertificateFetcher;
|
||||
|
||||
public class DiscoverImpl extends AbstractDiscover {
|
||||
public class DiscoverImpl extends CertificateDiscoveryImplementation {
|
||||
|
||||
public DiscoverImpl() {
|
||||
super(new CertificateReaderImpl(), new HttpUrlConnectionWKDFetcher());
|
||||
super(new CertificateParserImpl(), new HttpUrlConnectionCertificateFetcher());
|
||||
}
|
||||
|
||||
public DiscoverImpl(WKDFetcher fetcher) {
|
||||
super(new CertificateReaderImpl(), fetcher);
|
||||
public DiscoverImpl(CertificateFetcher fetcher) {
|
||||
super(new CertificateParserImpl(), fetcher);
|
||||
}
|
||||
|
||||
public DiscoverImpl(CertificateReader certificateReader, WKDFetcher fetcher) {
|
||||
super(certificateReader, fetcher);
|
||||
public DiscoverImpl(CertificateParser certificateParser, CertificateFetcher fetcher) {
|
||||
super(certificateParser, fetcher);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,13 +7,13 @@ package pgp.wkd.cli.command;
|
|||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||
import org.bouncycastle.util.io.Streams;
|
||||
import pgp.certificate_store.Certificate;
|
||||
import pgp.wkd.Discover;
|
||||
import pgp.wkd.HttpUrlConnectionWKDFetcher;
|
||||
import pgp.wkd.discovery.CertificateDiscoverer;
|
||||
import pgp.wkd.discovery.HttpUrlConnectionCertificateFetcher;
|
||||
import pgp.wkd.MalformedUserIdException;
|
||||
import pgp.wkd.WKDAddress;
|
||||
import pgp.wkd.WKDAddressHelper;
|
||||
import pgp.wkd.WKDDiscoveryResult;
|
||||
import pgp.wkd.WKDFetcher;
|
||||
import pgp.wkd.discovery.DiscoveryResult;
|
||||
import pgp.wkd.discovery.CertificateFetcher;
|
||||
import pgp.wkd.cli.CertNotFetchableException;
|
||||
import pgp.wkd.cli.DiscoverImpl;
|
||||
import picocli.CommandLine;
|
||||
|
@ -42,14 +42,14 @@ public class Fetch implements Runnable {
|
|||
boolean armor = false;
|
||||
|
||||
// TODO: Better way to inject fetcher implementation
|
||||
public static WKDFetcher fetcher = new HttpUrlConnectionWKDFetcher();
|
||||
public static CertificateFetcher fetcher = new HttpUrlConnectionCertificateFetcher();
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Discover discover = new DiscoverImpl(fetcher);
|
||||
CertificateDiscoverer certificateDiscoverer = new DiscoverImpl(fetcher);
|
||||
|
||||
WKDAddress address = addressFromUserId(userId);
|
||||
WKDDiscoveryResult result = discover.discover(address);
|
||||
DiscoveryResult result = certificateDiscoverer.discover(address);
|
||||
|
||||
if (!result.isSuccessful()) {
|
||||
throw new CertNotFetchableException("Cannot fetch cert.");
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
|
||||
package pgp.wkd.cli.test_suite;
|
||||
|
||||
import pgp.wkd.DiscoveryMethod;
|
||||
import pgp.wkd.discovery.DiscoveryMethod;
|
||||
import pgp.wkd.WKDAddress;
|
||||
import pgp.wkd.WKDFetcher;
|
||||
import pgp.wkd.discovery.CertificateFetcher;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
|
@ -15,17 +15,17 @@ import java.io.InputStream;
|
|||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class DirectoryBasedWkdFetcher implements WKDFetcher {
|
||||
public class DirectoryBasedCertificateFetcher implements CertificateFetcher {
|
||||
|
||||
// The directory containing the .well-known subdirectory
|
||||
private final Path rootPath;
|
||||
|
||||
public DirectoryBasedWkdFetcher(Path rootPath) {
|
||||
public DirectoryBasedCertificateFetcher(Path rootPath) {
|
||||
this.rootPath = rootPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream fetch(WKDAddress address, DiscoveryMethod method) throws IOException {
|
||||
public InputStream fetchCertificate(WKDAddress address, DiscoveryMethod method) throws IOException {
|
||||
URI uri = address.getUri(method);
|
||||
String path = uri.getPath();
|
||||
File file = rootPath.resolve(path.substring(1)).toFile(); // get rid of leading slash at start of path
|
|
@ -5,7 +5,9 @@
|
|||
package pgp.wkd.cli.test_suite;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.DynamicTest;
|
||||
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.LoggerFactory;
|
||||
import pgp.wkd.cli.WKDCLI;
|
||||
|
@ -17,6 +19,7 @@ import pgp.wkd.test_suite.TestSuiteGenerator;
|
|||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
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.assertNotEquals;
|
||||
|
@ -39,24 +42,29 @@ public class TestSuiteTestRunner {
|
|||
suite = generator.generateTestSuiteInDirectory(tempFile, TestSuiteGenerator.Method.direct);
|
||||
|
||||
// Fetch certificates from a local directory instead of the internetzzz.
|
||||
Fetch.fetcher = new DirectoryBasedWkdFetcher(tempPath);
|
||||
Fetch.fetcher = new DirectoryBasedCertificateFetcher(tempPath);
|
||||
}
|
||||
|
||||
@Test
|
||||
void runTestsAgainstTestSuite() {
|
||||
for (TestCase testCase : suite.getTestCases()) {
|
||||
LOGGER.info(() -> "Execute Test Case '" + testCase.getTestTitle() + "'");
|
||||
String mail = testCase.getLookupMailAddress();
|
||||
@TestFactory
|
||||
public Iterable<DynamicTest> testsFromTestSuite() {
|
||||
return suite.getTestCases()
|
||||
.stream()
|
||||
.map(testCase -> DynamicTest.dynamicTest(
|
||||
testCase.getTestTitle(),
|
||||
() -> {
|
||||
String mail = testCase.getLookupMailAddress();
|
||||
|
||||
int exitCode = WKDCLI.execute(new String[] {
|
||||
"fetch", "--armor", mail
|
||||
});
|
||||
int exitCode = WKDCLI.execute(new String[] {
|
||||
"fetch", "--armor", mail
|
||||
});
|
||||
|
||||
if (testCase.isExpectSuccess()) {
|
||||
assertEquals(0, exitCode, testCase.getTestDescription());
|
||||
} else {
|
||||
assertNotEquals(0, exitCode, testCase.getTestDescription());
|
||||
}
|
||||
}
|
||||
if (testCase.isExpectSuccess()) {
|
||||
assertEquals(0, exitCode, testCase.getTestDescription());
|
||||
} else {
|
||||
assertNotEquals(0, exitCode, testCase.getTestDescription());
|
||||
}
|
||||
}
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,9 @@ dependencies {
|
|||
|
||||
// Z-Base32
|
||||
implementation 'com.sandinh:zbase32-commons-codec:1.0.0'
|
||||
|
||||
// @Nullable etc.
|
||||
implementation "com.google.code.findbugs:jsr305:3.0.2"
|
||||
}
|
||||
|
||||
test {
|
||||
|
|
|
@ -9,20 +9,33 @@ import pgp.certificate_store.Certificate;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Tuple class which bundles a {@link Certificate} and a list of its valid or expired user ids.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list containing the valid or expired user-ids of the certificate.
|
||||
*
|
||||
* @return user ids
|
||||
*/
|
||||
public List<String> getUserIds() {
|
||||
return new ArrayList<>(userIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the certificate itself.
|
||||
*
|
||||
* @return certificate
|
||||
*/
|
||||
public Certificate getCertificate() {
|
||||
return certificate;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
;
|
||||
}
|
|
@ -4,6 +4,10 @@
|
|||
|
||||
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 MalformedUserIdException(String message) {
|
||||
|
|
|
@ -6,21 +6,36 @@ package pgp.wkd;
|
|||
|
||||
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 {
|
||||
|
||||
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.failure = failure;
|
||||
this.reasonForRejection = reasonForRejection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the certificate.
|
||||
* @return certificate
|
||||
*/
|
||||
public Certificate getCertificate() {
|
||||
return certificate;
|
||||
}
|
||||
|
||||
public Throwable getFailure() {
|
||||
return failure;
|
||||
/**
|
||||
* Return the reason for rejection.
|
||||
* @return rejection reason
|
||||
*/
|
||||
public Throwable getReasonForRejection() {
|
||||
return reasonForRejection;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
package pgp.wkd;
|
||||
|
||||
import org.apache.commons.codec.binary.ZBase32;
|
||||
import pgp.wkd.discovery.DiscoveryMethod;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
|
@ -26,7 +28,7 @@ public final class WKDAddress {
|
|||
private static final String SCHEME = "https://";
|
||||
private static final String ADV_SUBDOMAIN = "openpgpkey.";
|
||||
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/";
|
||||
}
|
||||
|
||||
|
@ -40,6 +42,7 @@ public final class WKDAddress {
|
|||
private static final Pattern PATTERN_DOMAIN_PART = Pattern.compile("[a-zA-Z0-9.-]+$");
|
||||
|
||||
// Android API lvl 10 does not yet know StandardCharsets.UTF_8 :/
|
||||
@SuppressWarnings("CharsetObjectCanBeUsed")
|
||||
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
|
||||
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
|
||||
*
|
||||
* @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()) {
|
||||
throw new IllegalArgumentException("Invalid local part.");
|
||||
}
|
||||
if (!PATTERN_DOMAIN_PART.matcher(domainPart).matches()) {
|
||||
throw new IllegalArgumentException("Invalid domain part.");
|
||||
}
|
||||
|
||||
return new WKDAddress(localPart, domainPart);
|
||||
}
|
||||
|
||||
|
@ -87,20 +93,35 @@ public final class WKDAddress {
|
|||
* @param email email address, case sensitive
|
||||
* @return WKDAddress object
|
||||
*/
|
||||
public static WKDAddress fromEmail(String email) throws MalformedUserIdException {
|
||||
public static WKDAddress fromEmail(@Nonnull String email) throws MalformedUserIdException {
|
||||
MailAddress mailAddress = parseMailAddress(email);
|
||||
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();
|
||||
/**
|
||||
* 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();
|
||||
case direct:
|
||||
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() {
|
||||
return localPart + '@' + domainPart;
|
||||
}
|
||||
|
@ -117,6 +138,7 @@ public final class WKDAddress {
|
|||
*
|
||||
* @return URI using the direct lookup method
|
||||
*/
|
||||
@Nonnull
|
||||
public URI getDirectMethodURI() {
|
||||
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
|
||||
*/
|
||||
@Nonnull
|
||||
public URI getAdvancedMethodURI() {
|
||||
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
|
||||
* @return zbase32 encoded sha1 sum of the string
|
||||
*/
|
||||
private String sha1AndZBase32Encode(String string) {
|
||||
@Nonnull
|
||||
private String sha1AndZBase32Encode(@Nonnull String string) {
|
||||
String lowerCase = string.toLowerCase();
|
||||
byte[] bytes = lowerCase.getBytes(utf8);
|
||||
|
||||
|
@ -166,7 +190,8 @@ public final class WKDAddress {
|
|||
* @param string string
|
||||
* @return percent encoded string
|
||||
*/
|
||||
private String percentEncode(String string) {
|
||||
@Nonnull
|
||||
private String percentEncode(@Nonnull String string) {
|
||||
try {
|
||||
return URLEncoder.encode(string, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
|
@ -181,7 +206,9 @@ public final class WKDAddress {
|
|||
* @param email email address string
|
||||
* @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);
|
||||
if (!matcher.matches()) {
|
||||
throw new MalformedUserIdException("Invalid email address.");
|
||||
|
@ -207,7 +234,7 @@ public final class WKDAddress {
|
|||
* @param localPart local part
|
||||
* @param domainPart domain part
|
||||
*/
|
||||
MailAddress(String localPart, String domainPart) {
|
||||
MailAddress(@Nonnull String localPart, @Nonnull String domainPart) {
|
||||
this.localPart = localPart;
|
||||
this.domainPart = domainPart;
|
||||
}
|
||||
|
@ -218,6 +245,7 @@ public final class WKDAddress {
|
|||
*
|
||||
* @return local part
|
||||
*/
|
||||
@Nonnull
|
||||
public String getLocalPart() {
|
||||
return localPart;
|
||||
}
|
||||
|
@ -228,6 +256,7 @@ public final class WKDAddress {
|
|||
*
|
||||
* @return domain part
|
||||
*/
|
||||
@Nonnull
|
||||
public String getDomainPart() {
|
||||
return domainPart;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
package pgp.wkd;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.regex.Matcher;
|
||||
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
|
||||
* 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);
|
||||
if (!matcher.matches()) {
|
||||
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
|
||||
* @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);
|
||||
return WKDAddress.fromEmail(email);
|
||||
}
|
||||
|
|
|
@ -2,26 +2,27 @@
|
|||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.wkd;
|
||||
package pgp.wkd.discovery;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import pgp.wkd.WKDAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
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
|
||||
public InputStream fetch(WKDAddress address, DiscoveryMethod method) throws IOException {
|
||||
public InputStream fetchCertificate(WKDAddress address, DiscoveryMethod method) throws IOException {
|
||||
URI uri = address.getUri(method);
|
||||
try {
|
||||
return fetchUri(uri);
|
||||
return fetchFromUri(uri);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +35,6 @@ public abstract class AbstractUriWKDFetcher implements WKDFetcher {
|
|||
* @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;
|
||||
protected abstract InputStream fetchFromUri(URI uri) throws IOException;
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -2,29 +2,33 @@
|
|||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.wkd;
|
||||
package pgp.wkd.discovery;
|
||||
|
||||
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.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AbstractDiscover implements Discover {
|
||||
public class CertificateDiscoveryImplementation implements CertificateDiscoverer {
|
||||
|
||||
protected final CertificateReader reader;
|
||||
protected final WKDFetcher fetcher;
|
||||
protected final CertificateParser reader;
|
||||
protected final CertificateFetcher fetcher;
|
||||
|
||||
public AbstractDiscover(CertificateReader reader, WKDFetcher fetcher) {
|
||||
public CertificateDiscoveryImplementation(CertificateParser reader, CertificateFetcher fetcher) {
|
||||
this.reader = reader;
|
||||
this.fetcher = fetcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WKDDiscoveryItem discover(DiscoveryMethod method, WKDAddress address) {
|
||||
public DiscoveryResponse discover(DiscoveryMethod method, WKDAddress address) {
|
||||
try {
|
||||
InputStream inputStream = fetcher.fetch(address, method);
|
||||
InputStream inputStream = fetcher.fetchCertificate(address, method);
|
||||
List<CertificateAndUserIds> fetchedCertificates = reader.read(inputStream);
|
||||
|
||||
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) {
|
||||
return WKDDiscoveryItem.failure(method, address, e);
|
||||
return DiscoveryResponse.failure(method, address, e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,9 @@
|
|||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.wkd;
|
||||
package pgp.wkd.discovery;
|
||||
|
||||
import pgp.wkd.WKDAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -11,7 +13,7 @@ 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 {
|
||||
public interface CertificateFetcher {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
InputStream fetch(WKDAddress address, DiscoveryMethod method) throws IOException;
|
||||
InputStream fetchCertificate(WKDAddress address, DiscoveryMethod method) throws IOException;
|
||||
}
|
|
@ -2,13 +2,15 @@
|
|||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.wkd;
|
||||
package pgp.wkd.discovery;
|
||||
|
||||
import pgp.wkd.CertificateAndUserIds;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
public interface CertificateReader {
|
||||
public interface CertificateParser {
|
||||
|
||||
List<CertificateAndUserIds> read(InputStream inputStream) throws IOException;
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -2,78 +2,95 @@
|
|||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.wkd;
|
||||
package pgp.wkd.discovery;
|
||||
|
||||
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;
|
||||
|
||||
public final class WKDDiscoveryItem {
|
||||
public final class DiscoveryResponse {
|
||||
|
||||
private final DiscoveryMethod method;
|
||||
private final WKDAddress address;
|
||||
private final List<Certificate> certificates;
|
||||
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 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...)
|
||||
* @param fetchingFailure general fetching error (e.g. connection error, 404...)
|
||||
*/
|
||||
private WKDDiscoveryItem(
|
||||
private DiscoveryResponse(
|
||||
DiscoveryMethod method,
|
||||
WKDAddress address,
|
||||
List<Certificate> certificates,
|
||||
List<RejectedCertificate> rejectedCertificates,
|
||||
Throwable failure) {
|
||||
Throwable fetchingFailure) {
|
||||
this.method = method;
|
||||
this.address = address;
|
||||
this.certificates = certificates;
|
||||
this.rejectedCertificates = rejectedCertificates;
|
||||
this.failure = failure;
|
||||
this.fetchingFailure = fetchingFailure;
|
||||
}
|
||||
|
||||
public static WKDDiscoveryItem success(DiscoveryMethod method, WKDAddress address, List<Certificate> certificates, List<RejectedCertificate> rejectedCertificates) {
|
||||
return new WKDDiscoveryItem(method, address, certificates, rejectedCertificates, null);
|
||||
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 WKDDiscoveryItem failure(DiscoveryMethod method, WKDAddress address, Throwable failure) {
|
||||
return new WKDDiscoveryItem(method, address, null, null, failure);
|
||||
public static DiscoveryResponse failure(
|
||||
@Nonnull DiscoveryMethod method,
|
||||
@Nonnull WKDAddress address,
|
||||
@Nonnull Throwable fetchingFailure) {
|
||||
return new DiscoveryResponse(method, address, Collections.emptyList(), Collections.emptyList(), fetchingFailure);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public DiscoveryMethod getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public WKDAddress getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public boolean isSuccessful() {
|
||||
return !hasFailure();
|
||||
return !hasFetchingFailure();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<Certificate> getCertificates() {
|
||||
return certificates;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<RejectedCertificate> getRejectedCertificates() {
|
||||
return rejectedCertificates;
|
||||
}
|
||||
|
||||
public Throwable getFailure() {
|
||||
return failure;
|
||||
@Nullable
|
||||
public Throwable getFetchingFailure() {
|
||||
return fetchingFailure;
|
||||
}
|
||||
|
||||
public boolean hasCertificates() {
|
||||
return certificates != null && !certificates.isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasFailure() {
|
||||
return failure != null;
|
||||
public boolean hasFetchingFailure() {
|
||||
return fetchingFailure != null;
|
||||
}
|
||||
}
|
|
@ -2,25 +2,27 @@
|
|||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.wkd;
|
||||
package pgp.wkd.discovery;
|
||||
|
||||
import pgp.certificate_store.Certificate;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
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;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<Certificate> getCertificates() {
|
||||
List<Certificate> certificates = new ArrayList<>();
|
||||
|
||||
for (WKDDiscoveryItem item : items) {
|
||||
for (DiscoveryResponse item : items) {
|
||||
if (item.isSuccessful()) {
|
||||
certificates.addAll(item.getCertificates());
|
||||
}
|
||||
|
@ -29,7 +31,7 @@ public class WKDDiscoveryResult {
|
|||
}
|
||||
|
||||
public boolean isSuccessful() {
|
||||
for (WKDDiscoveryItem item : items) {
|
||||
for (DiscoveryResponse item : items) {
|
||||
if (item.isSuccessful() && item.hasCertificates()) {
|
||||
return true;
|
||||
}
|
||||
|
@ -37,13 +39,15 @@ public class WKDDiscoveryResult {
|
|||
return false;
|
||||
}
|
||||
|
||||
public List<WKDDiscoveryItem> getItems() {
|
||||
@Nonnull
|
||||
public List<DiscoveryResponse> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public List<WKDDiscoveryItem> getFailedItems() {
|
||||
List<WKDDiscoveryItem> fails = new ArrayList<>();
|
||||
for (WKDDiscoveryItem item : items) {
|
||||
@Nonnull
|
||||
public List<DiscoveryResponse> getFailedItems() {
|
||||
List<DiscoveryResponse> fails = new ArrayList<>();
|
||||
for (DiscoveryResponse item : items) {
|
||||
if (!item.isSuccessful()) {
|
||||
fails.add(item);
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.wkd;
|
||||
package pgp.wkd.discovery;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -12,11 +12,11 @@ import java.net.URI;
|
|||
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();
|
||||
HttpURLConnection con = (HttpURLConnection) url.openConnection();
|
||||
con.setRequestMethod("GET");
|
Loading…
Reference in a new issue