aboutsummaryrefslogtreecommitdiffstats
path: root/vespaclient-java/src/main/java/com
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/src/main/java/com
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/src/main/java/com')
-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
5 files changed, 109 insertions, 17 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));
+ }
+ }
+
}