summaryrefslogtreecommitdiffstats
path: root/vespaclient-java
diff options
context:
space:
mode:
authorTor Brede Vekterli <vekterli@yahooinc.com>2022-11-17 14:51:24 +0100
committerTor Brede Vekterli <vekterli@yahooinc.com>2022-11-17 14:58:27 +0100
commitbb9b37dd7fe05a54ec90f03fdea96a571a76451f (patch)
tree593a45db8ed0b0b6d1e9d3f7678b6dd66eaf182b /vespaclient-java
parent9eb4b08fdb6e01f23ebc68b1085a7241ad5824f2 (diff)
Support auto-resolving private key files based on token key ID
Lets a user specify a private key directory either with a command line argument or via an environment variable. If a directory is provided, the private key to use will be attempted auto-resolved based on the key ID stored in the token. This only applies if the key ID is comprised of exclusively path-safe characters.
Diffstat (limited to 'vespaclient-java')
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ToolInvocation.java7
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java2
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java24
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ResealTool.java16
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ToolUtils.java77
-rw-r--r--vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java133
-rw-r--r--vespaclient-java/src/test/resources/expected-decrypt-help-output.txt3
-rw-r--r--vespaclient-java/src/test/resources/expected-reseal-help-output.txt4
8 files changed, 240 insertions, 26 deletions
diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ToolInvocation.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ToolInvocation.java
index d1ff2687137..13df714a268 100644
--- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ToolInvocation.java
+++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/ToolInvocation.java
@@ -6,6 +6,7 @@ import org.apache.commons.cli.CommandLine;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Map;
+import java.util.function.Supplier;
/**
* @author vekterli
@@ -17,4 +18,10 @@ public record ToolInvocation(CommandLine arguments,
PrintStream stdError,
boolean debugMode) {
+ public void printIfDebug(Supplier<String> stringSupplier) {
+ if (debugMode) {
+ stdError.println(stringSupplier.get());
+ }
+ }
+
}
diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java
index 87d3cb4d9f0..5cb40aa8f3b 100644
--- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java
+++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java
@@ -19,7 +19,7 @@ public class CipherUtils {
*
* @param input source stream to read from
* @param output destination stream to write to
- * @param cipher an {@link AeadCipher} created with for either encryption or decryption
+ * @param cipher an {@link AeadCipher} created for either encryption or decryption
* @throws IOException if any file operation fails
*/
public static void streamEncipher(InputStream input, OutputStream output, AeadCipher cipher) throws IOException {
diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java
index 2cc724538d4..b22afb7e5fb 100644
--- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java
+++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java
@@ -1,8 +1,6 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.security.tool.crypto;
-import com.yahoo.security.KeyId;
-import com.yahoo.security.KeyUtils;
import com.yahoo.security.SealedSharedKey;
import com.yahoo.security.SharedKeyGenerator;
import com.yahoo.vespa.security.tool.CliUtils;
@@ -12,11 +10,12 @@ import com.yahoo.vespa.security.tool.ToolInvocation;
import org.apache.commons.cli.Option;
import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
+import static com.yahoo.vespa.security.tool.crypto.ToolUtils.PRIVATE_KEY_DIR_OPTION;
+import static com.yahoo.vespa.security.tool.crypto.ToolUtils.PRIVATE_KEY_FILE_OPTION;
+
/**
* Tooling for decrypting a file using a private key that corresponds to the public key used
* to originally encrypt the file.
@@ -28,7 +27,6 @@ import java.util.Optional;
public class DecryptTool implements Tool {
static final String OUTPUT_FILE_OPTION = "output-file";
- static final String PRIVATE_KEY_FILE_OPTION = "private-key-file";
static final String EXPECTED_KEY_ID_OPTION = "expected-key-id";
static final String TOKEN_OPTION = "token";
@@ -46,6 +44,13 @@ public class DecryptTool implements Tool {
.required(false)
.desc("Private key file in Base58 encoded format")
.build(),
+ Option.builder("d")
+ .longOpt(PRIVATE_KEY_DIR_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("Private key file directory used for automatically looking up " +
+ "private keys based on the key ID specified as part of a token.")
+ .build(),
Option.builder("e")
.longOpt(EXPECTED_KEY_ID_OPTION)
.hasArg(true)
@@ -83,17 +88,14 @@ public class DecryptTool implements Tool {
if (leftoverArgs.length != 1) {
throw new IllegalArgumentException("Expected exactly 1 file argument to decrypt");
}
- var inputArg = leftoverArgs[0];
- var maybeKeyId = Optional.ofNullable(arguments.hasOption(EXPECTED_KEY_ID_OPTION)
- ? arguments.getOptionValue(EXPECTED_KEY_ID_OPTION)
- : null);
+ var inputArg = leftoverArgs[0];
+ var maybeKeyId = Optional.ofNullable(arguments.getOptionValue(EXPECTED_KEY_ID_OPTION));
var outputArg = CliUtils.optionOrThrow(arguments, OUTPUT_FILE_OPTION);
var tokenString = CliUtils.optionOrThrow(arguments, TOKEN_OPTION);
var sealedSharedKey = SealedSharedKey.fromTokenString(tokenString.strip());
ToolUtils.verifyExpectedKeyId(sealedSharedKey, maybeKeyId);
- var privKeyPath = Paths.get(CliUtils.optionOrThrow(arguments, PRIVATE_KEY_FILE_OPTION));
- var privateKey = KeyUtils.fromBase58EncodedX25519PrivateKey(Files.readString(privKeyPath).strip());
+ var privateKey = ToolUtils.resolvePrivateKeyFromInvocation(invocation, sealedSharedKey.keyId());
var secretShared = SharedKeyGenerator.fromSealedKey(sealedSharedKey, privateKey);
var cipher = SharedKeyGenerator.makeAesGcmDecryptionCipher(secretShared);
diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ResealTool.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ResealTool.java
index e9bc0ae8fee..83fdf6998df 100644
--- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ResealTool.java
+++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ResealTool.java
@@ -12,11 +12,12 @@ import com.yahoo.vespa.security.tool.ToolInvocation;
import org.apache.commons.cli.Option;
import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
+import static com.yahoo.vespa.security.tool.crypto.ToolUtils.PRIVATE_KEY_DIR_OPTION;
+import static com.yahoo.vespa.security.tool.crypto.ToolUtils.PRIVATE_KEY_FILE_OPTION;
+
/**
* Tooling for resealing a token for another recipient. This allows for delegating
* decryption to another party without having to reveal the private key of the original
@@ -26,7 +27,6 @@ import java.util.Optional;
*/
public class ResealTool implements Tool {
- static final String PRIVATE_KEY_FILE_OPTION = "private-key-file";
static final String EXPECTED_KEY_ID_OPTION = "expected-key-id";
static final String RECIPIENT_KEY_ID_OPTION = "key-id";
static final String RECIPIENT_PUBLIC_KEY_OPTION = "recipient-public-key";
@@ -38,6 +38,13 @@ public class ResealTool implements Tool {
.required(false)
.desc("Private key file in Base58 encoded format")
.build(),
+ Option.builder("d")
+ .longOpt(PRIVATE_KEY_DIR_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("Private key file directory used for automatically looking up " +
+ "private keys based on the key ID specified as part of a token.")
+ .build(),
Option.builder("e")
.longOpt(EXPECTED_KEY_ID_OPTION)
.hasArg(true)
@@ -90,8 +97,7 @@ public class ResealTool implements Tool {
var recipientPubKey = KeyUtils.fromBase58EncodedX25519PublicKey(CliUtils.optionOrThrow(arguments, RECIPIENT_PUBLIC_KEY_OPTION).strip());
var recipientKeyId = KeyId.ofString(CliUtils.optionOrThrow(arguments, RECIPIENT_KEY_ID_OPTION));
- var privKeyPath = Paths.get(CliUtils.optionOrThrow(arguments, PRIVATE_KEY_FILE_OPTION));
- var privateKey = KeyUtils.fromBase58EncodedX25519PrivateKey(Files.readString(privKeyPath).strip());
+ var privateKey = ToolUtils.resolvePrivateKeyFromInvocation(invocation, sealedSharedKey.keyId());
var secretShared = SharedKeyGenerator.fromSealedKey(sealedSharedKey, privateKey);
var resealedShared = SharedKeyGenerator.reseal(secretShared, recipientPubKey, recipientKeyId);
diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ToolUtils.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ToolUtils.java
index 32e9c6679f6..2a25832708c 100644
--- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ToolUtils.java
+++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ToolUtils.java
@@ -2,15 +2,31 @@
package com.yahoo.vespa.security.tool.crypto;
import com.yahoo.security.KeyId;
+import com.yahoo.security.KeyUtils;
import com.yahoo.security.SealedSharedKey;
+import com.yahoo.vespa.security.tool.CliUtils;
+import com.yahoo.vespa.security.tool.ToolInvocation;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFilePermission;
+import java.security.interfaces.XECPrivateKey;
import java.util.Optional;
+import java.util.regex.Pattern;
/**
* @author vekterli
*/
public class ToolUtils {
+ static final String PRIVATE_KEY_FILE_OPTION = "private-key-file";
+ static final String PRIVATE_KEY_DIR_OPTION = "private-key-dir";
+ static final String PRIVATE_KEY_DIR_ENV_VAR = "VESPA_CRYPTO_CLI_PRIVATE_KEY_DIR";
+
+ static final Pattern SAFE_KEY_ID_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+$");
+
static void verifyExpectedKeyId(SealedSharedKey sealedSharedKey, Optional<String> maybeKeyId) {
if (maybeKeyId.isPresent()) {
var myKeyId = KeyId.ofString(maybeKeyId.get());
@@ -22,4 +38,65 @@ public class ToolUtils {
}
}
+ private static void verifyKeyIdIsPathSafe(KeyId keyId) {
+ String keyIdStr = keyId.asString();
+ if (!SAFE_KEY_ID_PATTERN.matcher(keyIdStr).matches()) {
+ throw new IllegalArgumentException("The token key ID is not comprised of path-safe characters; refusing " +
+ "to auto-deduce key file name");
+ }
+ }
+
+ private static void verifyPrivateKeyFileNotWorldReadable(Path keyPath) throws IOException {
+ var privKeyPerms = Files.getPosixFilePermissions(keyPath);
+ if (privKeyPerms.contains(PosixFilePermission.OTHERS_READ)) {
+ throw new IllegalArgumentException("Private key file '%s' is insecurely world-readable; refusing to read it"
+ .formatted(keyPath.toAbsolutePath()));
+ }
+ }
+
+ private static XECPrivateKey attemptResolvePrivateKeyFromDir(Path privKeyDirPath, KeyId tokenKeyId) throws IOException {
+ if (!Files.isDirectory(privKeyDirPath)) {
+ throw new IllegalArgumentException("'%s' is not a valid directory".formatted(privKeyDirPath.toAbsolutePath()));
+ }
+ verifyKeyIdIsPathSafe(tokenKeyId);
+ var keyPath = privKeyDirPath.resolve(tokenKeyId.asString() + ".key");
+ if (!Files.exists(keyPath)) {
+ // We've verified the key ID contents, so we know it's safe to print here
+ throw new IllegalArgumentException("Could not find a private key file matching token key ID '%s'"
+ .formatted(tokenKeyId.asString()));
+ }
+ verifyPrivateKeyFileNotWorldReadable(keyPath);
+ return KeyUtils.fromBase58EncodedX25519PrivateKey(Files.readString(keyPath).strip());
+ }
+
+ public static XECPrivateKey resolvePrivateKeyFromInvocation(ToolInvocation invocation, KeyId tokenKeyId) throws IOException {
+ var arguments = invocation.arguments();
+ var envVars = invocation.envVars();
+
+ if (arguments.hasOption(PRIVATE_KEY_FILE_OPTION)) {
+ if (arguments.hasOption(PRIVATE_KEY_DIR_OPTION)) {
+ throw new IllegalArgumentException("--%s and --%s cannot be specified at the same time"
+ .formatted(PRIVATE_KEY_FILE_OPTION, PRIVATE_KEY_DIR_OPTION));
+ }
+ var privKeyFilePath = Paths.get(arguments.getOptionValue(PRIVATE_KEY_FILE_OPTION));
+ invocation.printIfDebug(() -> "Using private key file '%s'".formatted(privKeyFilePath));
+ if (!Files.exists(privKeyFilePath)) {
+ throw new IllegalArgumentException("Specified private key file '%s' does not exist"
+ .formatted(privKeyFilePath.toAbsolutePath()));
+ }
+ verifyPrivateKeyFileNotWorldReadable(privKeyFilePath);
+ return KeyUtils.fromBase58EncodedX25519PrivateKey(Files.readString(privKeyFilePath).strip());
+ } else if (arguments.hasOption(PRIVATE_KEY_DIR_OPTION) || envVars.containsKey(PRIVATE_KEY_DIR_ENV_VAR)) {
+ // Explicitly provided command line directory is preferred over env var, if set
+ var privKeyDirPath = Paths.get(arguments.hasOption(PRIVATE_KEY_DIR_OPTION)
+ ? arguments.getOptionValue(PRIVATE_KEY_DIR_OPTION)
+ : envVars.get(PRIVATE_KEY_DIR_ENV_VAR));
+ invocation.printIfDebug(() -> "Using private key lookup directory '%s'".formatted(privKeyDirPath));
+ return attemptResolvePrivateKeyFromDir(privKeyDirPath, tokenKeyId);
+ } else {
+ throw new IllegalArgumentException("No private key specified. Must specify either --%s or --%s"
+ .formatted(PRIVATE_KEY_FILE_OPTION, PRIVATE_KEY_DIR_OPTION));
+ }
+ }
+
}
diff --git a/vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java b/vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java
index 42b747df678..d6dd65e1b2c 100644
--- a/vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java
+++ b/vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java
@@ -17,6 +17,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermissions;
+import java.util.Base64;
import java.util.List;
import java.util.Map;
@@ -57,6 +58,12 @@ public class CryptoToolsTest {
assertEquals(expectedMessage, procOut.stdErr());
}
+ private static void writePrivateKeyFile(Path keyPath, String contents) throws IOException {
+ var privFilePerms = PosixFilePermissions.fromString("rw-------");
+ Files.createFile(keyPath, PosixFilePermissions.asFileAttribute(privFilePerms));
+ Files.writeString(keyPath, contents);
+ }
+
@Test
void top_level_help_page_printed_if_help_option_given() throws IOException {
verifyStdoutMatchesFile(List.of("--help"), "expected-help-output.txt");
@@ -114,7 +121,7 @@ public class CryptoToolsTest {
void keygen_fails_by_default_if_output_file_exists() throws IOException {
Path privKeyFile = pathInTemp("priv.txt");
Path pubKeyFile = pathInTemp("pub.txt");
- Files.writeString(privKeyFile, TEST_PRIV_KEY);
+ writePrivateKeyFile(privKeyFile, TEST_PRIV_KEY);
verifyStderrEquals(List.of("keygen",
"--private-out-file", absPathOf(privKeyFile),
@@ -149,7 +156,7 @@ public class CryptoToolsTest {
void keygen_allowed_if_output_file_exists_and_explicit_overwrite_option_specified() throws IOException {
Path privKeyFile = pathInTemp("priv.txt");
Path pubKeyFile = pathInTemp("pub.txt");
- Files.writeString(privKeyFile, TEST_PRIV_KEY);
+ writePrivateKeyFile(privKeyFile, TEST_PRIV_KEY);
Files.writeString(pubKeyFile, TEST_PUB_KEY);
var procOut = runMain(List.of("keygen",
@@ -208,7 +215,7 @@ public class CryptoToolsTest {
@Test
void decrypt_fails_with_error_message_if_no_input_file_is_given() throws IOException {
Path privKeyFile = pathInTemp("priv.txt");
- Files.writeString(privKeyFile, TEST_PRIV_KEY);
+ writePrivateKeyFile(privKeyFile, TEST_PRIV_KEY);
verifyStderrEquals(List.of("decrypt",
"--output-file", "foo",
@@ -221,7 +228,7 @@ public class CryptoToolsTest {
@Test
void decrypt_fails_with_error_message_if_input_file_does_not_exist() throws IOException {
Path privKeyFile = pathInTemp("priv.txt");
- Files.writeString(privKeyFile, TEST_PRIV_KEY);
+ writePrivateKeyFile(privKeyFile, TEST_PRIV_KEY);
verifyStderrEquals(List.of("decrypt",
"no-such-file",
@@ -235,7 +242,7 @@ public class CryptoToolsTest {
@Test
void decrypt_fails_with_error_message_if_expected_key_id_does_not_match_key_id_in_token() throws IOException {
Path privKeyFile = pathInTemp("priv.txt");
- Files.writeString(privKeyFile, TEST_PRIV_KEY);
+ writePrivateKeyFile(privKeyFile, TEST_PRIV_KEY);
Path inputFile = pathInTemp("input.txt");
Files.writeString(inputFile, "dummy-not-actually-encrypted-data");
@@ -251,6 +258,23 @@ public class CryptoToolsTest {
}
@Test
+ void decrypt_fails_with_error_message_if_private_key_file_is_world_readable() throws IOException {
+ Path privKeyFile = pathInTemp("priv.txt");
+ Files.writeString(privKeyFile, TEST_PRIV_KEY); // Don't restrict file permissions
+
+ Path inputFile = pathInTemp("input.txt");
+ Files.writeString(inputFile, "dummy-not-actually-encrypted-data");
+
+ verifyStderrEquals(List.of("decrypt",
+ absPathOf(inputFile),
+ "--output-file", "foo",
+ "--private-key-file", absPathOf(privKeyFile),
+ "--token", TEST_TOKEN),
+ ("Invalid command line arguments: Private key file '%s' is insecurely " +
+ "world-readable; refusing to read it\n").formatted(absPathOf(privKeyFile)));
+ }
+
+ @Test
void token_info_fails_with_error_message_if_no_token_string_given() throws IOException {
verifyStderrEquals(List.of("token-info"),
"Invalid command line arguments: Expected exactly 1 token string argument\n");
@@ -287,7 +311,7 @@ public class CryptoToolsTest {
String recipientPubKeyStr = "AiUirFvFuLJ6s71QBNxiRcctB4umzM6r2roP4Rf8WDKM";
Path privKeyFile = pathInTemp("my-priv.txt");
- Files.writeString(privKeyFile, TEST_PRIV_KEY);
+ writePrivateKeyFile(privKeyFile, TEST_PRIV_KEY);
var procOut = runMain(List.of(
"reseal",
@@ -407,14 +431,105 @@ public class CryptoToolsTest {
assertEquals(greatSecret, procOut.stdOut());
}
+ @Test
+ void can_look_up_private_key_by_token_key_id() throws Exception {
+ var bobPrivKeyStr = "DDYLatvM8NAs2hvccBPCJf7GvxK97AYhG4vf4Vz61mKX";
+ var alicePrivKeyStr = "AiKHsqgVXVe1dDYAtH23Hn1m82iCJSseiS9aZpam9sPG";
+ var plaintextData = "the seagulls will attack the discarded french fries at dawn\n";
+ var encryptedDataB64 = "tLh3dL7Ecq/l4E35IHjS9/oCU2wTVv4KSk+dBVIQxl9PFnCLgpTF" +
+ "OkhpCLQC9hqHfvco1bsV/+Gq6x2W8tpUoIR1X8GU04rIyBDYaw==";
+ // Token with key id "bobs-key-1"
+ var tokenToBob = "1emu5Os1qeuJSkPeyYFKKBQl3r7a9GKyBR0k7QAcqTSmdm6XAPSVn" +
+ "JJYI2RKVLodmB1ZUAwLDMMrvKtmY2d8Seo0VwA8rzUHTPDI8jO";
+ // Token with key id "alices-key-1"
+ var tokenToAlice = "STZC6ERSqAu1wWbV0Dvsw68iMMTye15sNbXz7cU8cuGMARZX5HGBO" +
+ "qiGwz3O4CmjaaeYfMnDqMJ7rKAA2GUIsKLio8Wp2gjgf3rJjZ3ha";
+ // Token with key id "eves-key"
+ var tokenToEve = "5z0i4pTaWzlYFDDyIPl0wO2WWRuL0RTlP3fmM1mcWBYUg28C7jY3R" +
+ "Kc6ymz3omho75jWR0v6AmcCHioRUwfOGMskAQGG27Lqfhvp";
+ // Token with key id "/etc/passwd"...
+ var unsafeIdToken = "6rh1141HZxvToeRYZsTE7K6vmihcrLgBHj75XvTBdkw9nMVvAqXU3" +
+ "7TkL8vuR40iCQYQFpyDqoorTF4HcfKLNsl7vqgkcje3kE6wCtPa";
+
+ var keyDirPath = pathInTemp("my-priv-keys");
+ Files.createDirectory(keyDirPath);
+ writePrivateKeyFile(keyDirPath.resolve("bobs-key-1.key"), bobPrivKeyStr);
+ writePrivateKeyFile(keyDirPath.resolve("alices-key-1.key"), alicePrivKeyStr);
+
+ Path encryptedFile = pathInTemp("secret.enc");
+ Files.write(encryptedFile, Base64.getDecoder().decode(encryptedDataB64));
+
+ // First test with explicit key dir argument.
+ // This shall look up 'bobs-key-1.key' in the private key directory
+ var procOut = runMain(List.of(
+ "decrypt",
+ absPathOf(encryptedFile),
+ "--output-file", "-",
+ "--private-key-dir", absPathOf(keyDirPath),
+ "--token", tokenToBob
+ ));
+ assertEquals("", procOut.stdErr());
+ assertEquals(0, procOut.exitCode());
+ assertEquals(plaintextData, procOut.stdOut());
+
+ // This shall look up 'alices-key-1.key' in the private key directory
+ procOut = runMain(List.of(
+ "decrypt",
+ absPathOf(encryptedFile),
+ "--output-file", "-",
+ "--private-key-dir", absPathOf(keyDirPath),
+ "--token", tokenToAlice
+ ));
+ assertEquals("", procOut.stdErr());
+ assertEquals(0, procOut.exitCode());
+ assertEquals(plaintextData, procOut.stdOut());
+
+ // This shall look up 'eves-key.key' in the private key directory, which does not exist.
+ procOut = runMain(List.of(
+ "decrypt",
+ absPathOf(encryptedFile),
+ "--output-file", "-",
+ "--private-key-dir", absPathOf(keyDirPath),
+ "--token", tokenToEve
+ ));
+ assertEquals("Invalid command line arguments: Could not find a private key " +
+ "file matching token key ID 'eves-key'\n",
+ procOut.stdErr());
+ assertEquals(1, procOut.exitCode());
+ assertEquals("", procOut.stdOut());
+
+ // Should also work if environment variable is set instead of key dir argument
+ var env = Map.of("VESPA_CRYPTO_CLI_PRIVATE_KEY_DIR", absPathOf(keyDirPath));
+ procOut = runMain(List.of(
+ "decrypt",
+ absPathOf(encryptedFile),
+ "--output-file", "-",
+ "--token", tokenToBob
+ ), EMPTY_BYTES, env);
+ assertEquals("", procOut.stdErr());
+ assertEquals(0, procOut.exitCode());
+ assertEquals(plaintextData, procOut.stdOut());
+
+ // Path-unsafe token key IDs are not automatically looked up, but failed.
+ procOut = runMain(List.of(
+ "decrypt",
+ absPathOf(encryptedFile),
+ "--output-file", "-",
+ "--token", unsafeIdToken
+ ), EMPTY_BYTES, env);
+ assertEquals("Invalid command line arguments: The token key ID is not comprised " +
+ "of path-safe characters; refusing to auto-deduce key file name\n",
+ procOut.stdErr());
+ assertEquals(1, procOut.exitCode());
+ assertEquals("", procOut.stdOut());
+ }
+
private ProcessOutput runMain(List<String> args) {
return runMain(args, EMPTY_BYTES);
}
private ProcessOutput runMain(List<String> args, byte[] stdInBytes) {
- // Expect that this is used for running a command that is not supposed to fail. But if it does,
- // include exception trace in stderr to make it easier to debug.
- return runMain(args, stdInBytes, Map.of("VESPA_DEBUG", "true"));
+ return runMain(args, stdInBytes, Map.of());
}
private ProcessOutput runMain(List<String> args, byte[] stdInBytes, Map<String, String> env) {
diff --git a/vespaclient-java/src/test/resources/expected-decrypt-help-output.txt b/vespaclient-java/src/test/resources/expected-decrypt-help-output.txt
index 291fc3f4b96..2ccb93e9a33 100644
--- a/vespaclient-java/src/test/resources/expected-decrypt-help-output.txt
+++ b/vespaclient-java/src/test/resources/expected-decrypt-help-output.txt
@@ -5,6 +5,9 @@ given private key.
To decrypt the contents of STDIN, specify an input file of '-' (without
the quotes).
+ -d,--private-key-dir <arg> Private key file directory used for
+ automatically looking up private keys based
+ on the key ID specified as part of a token.
-e,--expected-key-id <arg> Expected key ID in token. If this is not
provided, the key ID is not verified.
-h,--help Show help
diff --git a/vespaclient-java/src/test/resources/expected-reseal-help-output.txt b/vespaclient-java/src/test/resources/expected-reseal-help-output.txt
index dcfea6c28bc..7aad08b3f9c 100644
--- a/vespaclient-java/src/test/resources/expected-reseal-help-output.txt
+++ b/vespaclient-java/src/test/resources/expected-reseal-help-output.txt
@@ -2,6 +2,10 @@ usage: vespa-security reseal <token> <options>
Reseals the input token for another recipient, allowing that recipient to
decrypt the file that the input token was originally created for.
Prints new token to STDOUT.
+ -d,--private-key-dir <arg> Private key file directory used for
+ automatically looking up private keys
+ based on the key ID specified as part
+ of a token.
-e,--expected-key-id <arg> Expected key ID in token. If this is
not provided, the key ID is not
verified.