From 9abe217de09bc99d2d5432cee1922766b06e3a88 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 21 Feb 2022 14:16:55 +0100 Subject: [PATCH] Create basic WKDCLI implementation --- build.gradle | 2 +- wkd-java-cli/build.gradle | 25 ++++++ .../pgp/wkd/cli/MissingUserIdException.java | 12 +++ .../src/main/java/pgp/wkd/cli/WKDCLI.java | 21 +++++ .../main/java/pgp/wkd/cli/command/Fetch.java | 88 +++++++++++++++++++ .../pgp/wkd/cli/command/package-info.java | 8 ++ .../main/java/pgp/wkd/cli/package-info.java | 8 ++ .../pgp/wkd/JavaHttpRequestWKDFetcher.java | 34 ++----- 8 files changed, 168 insertions(+), 30 deletions(-) create mode 100644 wkd-java-cli/src/main/java/pgp/wkd/cli/MissingUserIdException.java create mode 100644 wkd-java-cli/src/main/java/pgp/wkd/cli/command/Fetch.java create mode 100644 wkd-java-cli/src/main/java/pgp/wkd/cli/command/package-info.java create mode 100644 wkd-java-cli/src/main/java/pgp/wkd/cli/package-info.java diff --git a/build.gradle b/build.gradle index 39ab399..a8e6c78 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ allprojects { project.ext { junitVersion = '5.8.2' slf4jVersion = '1.7.32' - logbackVersion = '1.2.9' + logbackVersion = '1.2.10' rootConfigDir = new File(rootDir, 'config') gitCommit = getGitCommit() isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI')) diff --git a/wkd-java-cli/build.gradle b/wkd-java-cli/build.gradle index 66b2a65..5935544 100644 --- a/wkd-java-cli/build.gradle +++ b/wkd-java-cli/build.gradle @@ -16,12 +16,37 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + // https://todd.ginsberg.com/post/testing-system-exit/ + testImplementation 'com.ginsberg:junit5-system-exit:1.1.2' + testImplementation 'org.mockito:mockito-core:4.3.1' + + implementation("org.pgpainless:pgpainless-core:1.1.0") implementation project(':wkd-java') + implementation "info.picocli:picocli:4.6.3" // Logging testImplementation "ch.qos.logback:logback-classic:$logbackVersion" + implementation 'org.slf4j:slf4j-nop:1.7.36' } test { useJUnitPlatform() } + + +mainClassName = 'pgp.wkd.cli.WKDCLI' + +jar { + duplicatesStrategy(DuplicatesStrategy.EXCLUDE) + manifest { + attributes 'Main-Class': "$mainClassName" + } + + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } { + exclude "META-INF/*.SF" + exclude "META-INF/*.DSA" + exclude "META-INF/*.RSA" + } +} \ No newline at end of file diff --git a/wkd-java-cli/src/main/java/pgp/wkd/cli/MissingUserIdException.java b/wkd-java-cli/src/main/java/pgp/wkd/cli/MissingUserIdException.java new file mode 100644 index 0000000..394283c --- /dev/null +++ b/wkd-java-cli/src/main/java/pgp/wkd/cli/MissingUserIdException.java @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd.cli; + +public class MissingUserIdException extends Exception { + + public MissingUserIdException() { + super(); + } +} diff --git a/wkd-java-cli/src/main/java/pgp/wkd/cli/WKDCLI.java b/wkd-java-cli/src/main/java/pgp/wkd/cli/WKDCLI.java index 6909f6e..91b3b5d 100644 --- a/wkd-java-cli/src/main/java/pgp/wkd/cli/WKDCLI.java +++ b/wkd-java-cli/src/main/java/pgp/wkd/cli/WKDCLI.java @@ -4,6 +4,27 @@ package pgp.wkd.cli; +import pgp.wkd.cli.command.Fetch; +import picocli.CommandLine; + +@CommandLine.Command( + subcommands = { + CommandLine.HelpCommand.class, + Fetch.class + } +) public class WKDCLI { + public static void main(String[] args) { + int exitCode = execute(args); + if (exitCode != 0) { + System.exit(exitCode); + } + } + + public static int execute(String[] args) { + return new CommandLine(WKDCLI.class) + .setCommandName("wkdcli") + .execute(args); + } } diff --git a/wkd-java-cli/src/main/java/pgp/wkd/cli/command/Fetch.java b/wkd-java-cli/src/main/java/pgp/wkd/cli/command/Fetch.java new file mode 100644 index 0000000..efd9d3f --- /dev/null +++ b/wkd-java-cli/src/main/java/pgp/wkd/cli/command/Fetch.java @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd.cli.command; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; +import pgp.wkd.IWKDFetcher; +import pgp.wkd.JavaHttpRequestWKDFetcher; +import pgp.wkd.WKDAddress; +import pgp.wkd.WKDAddressHelper; +import pgp.wkd.cli.MissingUserIdException; +import picocli.CommandLine; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +@CommandLine.Command( + name = "fetch", + description = "Fetch an OpenPGP Certificate via the Web Key Directory" +) +public class Fetch implements Runnable { + + @CommandLine.Parameters( + index = "0", + arity = "1", + paramLabel = "USERID", + description = "Certificate User-ID" + ) + String userId; + + @CommandLine.Option( + names = {"-a", "--armor"}, + description = "ASCII Armor the output" + ) + boolean armor = false; + + IWKDFetcher fetcher = new JavaHttpRequestWKDFetcher(); + + @Override + public void run() { + String email; + try { + email = WKDAddressHelper.emailFromUserId(userId); + } catch (IllegalArgumentException e) { + email = userId; + } + + WKDAddress address = WKDAddress.fromEmail(email); + try (InputStream inputStream = fetcher.fetch(address)) { + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(inputStream); + KeyRingInfo info = PGPainless.inspectKeyRing(cert); + + List 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(); + } + + if (armor) { + OutputStream out = new ArmoredOutputStream(System.out); + cert.encode(out); + out.close(); + } else { + cert.encode(System.out); + } + + } catch (IOException e) { + System.err.println("Could not fetch certificate."); + e.printStackTrace(); + System.exit(1); + } catch (MissingUserIdException e) { + System.err.println("Fetched certificate does not contain email address " + email); + System.exit(1); + } + } +} diff --git a/wkd-java-cli/src/main/java/pgp/wkd/cli/command/package-info.java b/wkd-java-cli/src/main/java/pgp/wkd/cli/command/package-info.java new file mode 100644 index 0000000..36cb5b7 --- /dev/null +++ b/wkd-java-cli/src/main/java/pgp/wkd/cli/command/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Subcommands of the WKDCLI tool. + */ +package pgp.wkd.cli.command; diff --git a/wkd-java-cli/src/main/java/pgp/wkd/cli/package-info.java b/wkd-java-cli/src/main/java/pgp/wkd/cli/package-info.java new file mode 100644 index 0000000..996147c --- /dev/null +++ b/wkd-java-cli/src/main/java/pgp/wkd/cli/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Command Line Interface for fetching OpenPGP Certificates from the Web Key Directory. + */ +package pgp.wkd.cli; diff --git a/wkd-java/src/main/java/pgp/wkd/JavaHttpRequestWKDFetcher.java b/wkd-java/src/main/java/pgp/wkd/JavaHttpRequestWKDFetcher.java index 95be337..2d9c565 100644 --- a/wkd-java/src/main/java/pgp/wkd/JavaHttpRequestWKDFetcher.java +++ b/wkd-java/src/main/java/pgp/wkd/JavaHttpRequestWKDFetcher.java @@ -26,16 +26,17 @@ public class JavaHttpRequestWKDFetcher implements IWKDFetcher { return tryFetchUri(advanced); } catch (IOException e) { advancedException = e; - LOGGER.debug("Could not fetch key using advanced method from " + advanced.toString(), e); + LOGGER.debug("Could not fetch key using advanced method from " + advanced.toString(), advancedException); } URI direct = address.getDirectMethodURI(); try { return tryFetchUri(direct); } catch (IOException e) { - advancedException.addSuppressed(e); + // we would like to use addSuppressed eventually, but Android API 10 does no support it + // e.addSuppressed(advancedException); LOGGER.debug("Could not fetch key using direct method from " + direct.toString(), e); - throw advancedException; + throw e; } } @@ -49,9 +50,8 @@ public class JavaHttpRequestWKDFetcher implements IWKDFetcher { int status = con.getResponseCode(); if (status != 200) { - throw new ConnectException("Connection was unsuccessful"); + throw new ConnectException("Connecting to '" + uri + "' failed. Status: " + status); } - LOGGER.debug("Successfully fetched key from " + uri); return con.getInputStream(); } @@ -59,28 +59,4 @@ public class JavaHttpRequestWKDFetcher implements IWKDFetcher { URL url = uri.toURL(); return (HttpURLConnection) url.openConnection(); } - - public static void main(String[] args) { - if (args.length != 1) { - throw new IllegalArgumentException("Expect a single argument email address"); - } - - String email = args[0]; - WKDAddress address = WKDAddress.fromEmail(email); - - JavaHttpRequestWKDFetcher fetch = new JavaHttpRequestWKDFetcher(); - try { - InputStream inputStream = fetch.fetch(address); - byte[] buf = new byte[4096]; - int read; - while ((read = inputStream.read(buf)) != -1) { - System.out.write(buf, 0, read); - } - inputStream.close(); - System.exit(0); - } catch (IOException e) { - LOGGER.debug("Could not fetch key.", e); - System.exit(1); - } - } }