aboutsummaryrefslogtreecommitdiffstats
path: root/vespaclient-java
diff options
context:
space:
mode:
authorTor Brede Vekterli <vekterli@yahooinc.com>2022-11-11 12:58:46 +0100
committerTor Brede Vekterli <vekterli@yahooinc.com>2022-11-11 13:17:28 +0100
commit7855a0d37241e87afe514ec25bf7a00289b556d7 (patch)
treec21ced3800c3f846cdd55bcf5c15355330a7eb42 /vespaclient-java
parentf2ce165982217902ea84bcb12e7a10fe008bacd4 (diff)
Add support for token resealing
Adds underlying support--and 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 recipient (or having to send the raw underlying secret key over a potentially insecure channel). Key ID can/should change as part of this operation.
Diffstat (limited to 'vespaclient-java')
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java3
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/DecryptTool.java36
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/EncryptTool.java2
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ResealTool.java104
-rw-r--r--vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ToolUtils.java25
-rw-r--r--vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java81
-rw-r--r--vespaclient-java/src/test/resources/expected-decrypt-help-output.txt22
-rw-r--r--vespaclient-java/src/test/resources/expected-encrypt-help-output.txt2
-rw-r--r--vespaclient-java/src/test/resources/expected-help-output.txt3
-rw-r--r--vespaclient-java/src/test/resources/expected-reseal-help-output.txt15
10 files changed, 232 insertions, 61 deletions
diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java
index 0498154aa91..26868207bd3 100644
--- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java
+++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java
@@ -5,6 +5,7 @@ import com.yahoo.vespa.security.tool.crypto.ConvertBaseTool;
import com.yahoo.vespa.security.tool.crypto.DecryptTool;
import com.yahoo.vespa.security.tool.crypto.EncryptTool;
import com.yahoo.vespa.security.tool.crypto.KeygenTool;
+import com.yahoo.vespa.security.tool.crypto.ResealTool;
import com.yahoo.vespa.security.tool.crypto.TokenInfoTool;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
@@ -47,7 +48,7 @@ public class Main {
private static final List<Tool> TOOLS = List.of(
new KeygenTool(), new EncryptTool(), new DecryptTool(), new TokenInfoTool(),
- new ConvertBaseTool());
+ new ConvertBaseTool(), new ResealTool());
private static Optional<Tool> toolFromCliArgs(String[] args) {
if (args.length == 0) {
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 f1c166ba934..2cc724538d4 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
@@ -27,10 +27,10 @@ import java.util.Optional;
*/
public class DecryptTool implements Tool {
- static final String OUTPUT_FILE_OPTION = "output-file";
- static final String RECIPIENT_PRIVATE_KEY_FILE_OPTION = "recipient-private-key-file";
- static final String KEY_ID_OPTION = "key-id";
- static final String TOKEN_OPTION = "token";
+ 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";
private static final List<Option> OPTIONS = List.of(
Option.builder("o")
@@ -41,17 +41,16 @@ public class DecryptTool implements Tool {
"quotes) to write plaintext to STDOUT instead of a file.")
.build(),
Option.builder("k")
- .longOpt(RECIPIENT_PRIVATE_KEY_FILE_OPTION)
+ .longOpt(PRIVATE_KEY_FILE_OPTION)
.hasArg(true)
.required(false)
- .desc("Recipient private key file in Base58 encoded format")
+ .desc("Private key file in Base58 encoded format")
.build(),
- Option.builder("i")
- .longOpt(KEY_ID_OPTION)
+ Option.builder("e")
+ .longOpt(EXPECTED_KEY_ID_OPTION)
.hasArg(true)
.required(false)
- .desc("Numeric ID of recipient key. If this is not provided, " +
- "the key ID stored as part of the token is not verified.")
+ .desc("Expected key ID in token. If this is not provided, the key ID is not verified.")
.build(),
Option.builder("t")
.longOpt(TOKEN_OPTION)
@@ -85,21 +84,15 @@ public class DecryptTool implements Tool {
throw new IllegalArgumentException("Expected exactly 1 file argument to decrypt");
}
var inputArg = leftoverArgs[0];
- var maybeKeyId = Optional.ofNullable(arguments.hasOption(KEY_ID_OPTION)
- ? arguments.getOptionValue(KEY_ID_OPTION)
+ var maybeKeyId = Optional.ofNullable(arguments.hasOption(EXPECTED_KEY_ID_OPTION)
+ ? arguments.getOptionValue(EXPECTED_KEY_ID_OPTION)
: null);
var outputArg = CliUtils.optionOrThrow(arguments, OUTPUT_FILE_OPTION);
- var privKeyPath = Paths.get(CliUtils.optionOrThrow(arguments, RECIPIENT_PRIVATE_KEY_FILE_OPTION));
var tokenString = CliUtils.optionOrThrow(arguments, TOKEN_OPTION);
var sealedSharedKey = SealedSharedKey.fromTokenString(tokenString.strip());
- if (maybeKeyId.isPresent()) {
- var myKeyId = KeyId.ofString(maybeKeyId.get());
- if (!myKeyId.equals(sealedSharedKey.keyId())) {
- // Don't include raw key bytes array verbatim in message (may contain control chars etc).
- throw new IllegalArgumentException("Key ID specified with --key-id does not match key ID " +
- "used when generating the supplied token");
- }
- }
+ ToolUtils.verifyExpectedKeyId(sealedSharedKey, maybeKeyId);
+
+ var privKeyPath = Paths.get(CliUtils.optionOrThrow(arguments, PRIVATE_KEY_FILE_OPTION));
var privateKey = KeyUtils.fromBase58EncodedX25519PrivateKey(Files.readString(privKeyPath).strip());
var secretShared = SharedKeyGenerator.fromSealedKey(sealedSharedKey, privateKey);
var cipher = SharedKeyGenerator.makeAesGcmDecryptionCipher(secretShared);
@@ -108,7 +101,6 @@ public class DecryptTool implements Tool {
var outStream = CliUtils.outputStreamToFileOrStream(outputArg, invocation.stdOut())) {
CipherUtils.streamEncipher(inStream, outStream, cipher);
}
-
} catch (IOException e) {
throw new RuntimeException(e);
}
diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/EncryptTool.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/EncryptTool.java
index 886433f00f8..962b42f4c22 100644
--- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/EncryptTool.java
+++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/EncryptTool.java
@@ -46,7 +46,7 @@ public class EncryptTool implements Tool {
.longOpt(KEY_ID_OPTION)
.hasArg(true)
.required(false)
- .desc("Numeric ID of recipient key")
+ .desc("ID of recipient key")
.build());
@Override
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
new file mode 100644
index 00000000000..e9bc0ae8fee
--- /dev/null
+++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ResealTool.java
@@ -0,0 +1,104 @@
+// 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;
+import com.yahoo.vespa.security.tool.Tool;
+import com.yahoo.vespa.security.tool.ToolDescription;
+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;
+
+/**
+ * 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
+ * recipient.
+ *
+ * @author vekterli
+ */
+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";
+
+ private static final List<Option> OPTIONS = List.of(
+ Option.builder("k")
+ .longOpt(PRIVATE_KEY_FILE_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("Private key file in Base58 encoded format")
+ .build(),
+ Option.builder("e")
+ .longOpt(EXPECTED_KEY_ID_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("Expected key ID in token. If this is not provided, the key ID is not verified.")
+ .build(),
+ Option.builder("r")
+ .longOpt(RECIPIENT_PUBLIC_KEY_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("Recipient X25519 public key in Base58 encoded format")
+ .build(),
+ Option.builder("i")
+ .longOpt(RECIPIENT_KEY_ID_OPTION)
+ .hasArg(true)
+ .required(false)
+ .desc("ID of recipient key")
+ .build());
+
+ @Override
+ public String name() {
+ return "reseal";
+ }
+
+ @Override
+ public ToolDescription description() {
+ return new ToolDescription(
+ "<token> <options>",
+ "Reseals the input token for another recipient, allowing that recipient to " +
+ "decrypt the file that the input token was originally created for.\n" +
+ "Prints new token to STDOUT.",
+ "Note: this is a BETA tool version; its interface may be changed at any time",
+ OPTIONS);
+ }
+
+ @Override
+ public int invoke(ToolInvocation invocation) {
+ try {
+ var arguments = invocation.arguments();
+ var leftoverArgs = arguments.getArgs();
+ if (leftoverArgs.length != 1) {
+ throw new IllegalArgumentException("Expected exactly 1 token argument to re-seal");
+ }
+ var tokenString = leftoverArgs[0];
+ var maybeKeyId = Optional.ofNullable(arguments.hasOption(EXPECTED_KEY_ID_OPTION)
+ ? arguments.getOptionValue(EXPECTED_KEY_ID_OPTION)
+ : null);
+ var sealedSharedKey = SealedSharedKey.fromTokenString(tokenString.strip());
+ ToolUtils.verifyExpectedKeyId(sealedSharedKey, maybeKeyId);
+
+ 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 secretShared = SharedKeyGenerator.fromSealedKey(sealedSharedKey, privateKey);
+ var resealedShared = SharedKeyGenerator.reseal(secretShared, recipientPubKey, recipientKeyId);
+
+ invocation.stdOut().println(resealedShared.sealedSharedKey().toTokenString());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return 0;
+ }
+}
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
new file mode 100644
index 00000000000..32e9c6679f6
--- /dev/null
+++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ToolUtils.java
@@ -0,0 +1,25 @@
+// 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.SealedSharedKey;
+
+import java.util.Optional;
+
+/**
+ * @author vekterli
+ */
+public class ToolUtils {
+
+ static void verifyExpectedKeyId(SealedSharedKey sealedSharedKey, Optional<String> maybeKeyId) {
+ if (maybeKeyId.isPresent()) {
+ var myKeyId = KeyId.ofString(maybeKeyId.get());
+ if (!myKeyId.equals(sealedSharedKey.keyId())) {
+ // Don't include raw key bytes array verbatim in message (may contain control chars etc.)
+ throw new IllegalArgumentException("Key ID specified with --expected-key-id does not match key ID " +
+ "used when generating the supplied token");
+ }
+ }
+ }
+
+}
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 d4992e89802..42b747df678 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
@@ -1,6 +1,10 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.security.tool;
+import com.yahoo.security.KeyId;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SealedSharedKey;
+import com.yahoo.security.SharedKeyGenerator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -16,6 +20,7 @@ import java.nio.file.attribute.PosixFilePermissions;
import java.util.List;
import java.util.Map;
+import static com.yahoo.security.ArrayUtils.hex;
import static com.yahoo.security.ArrayUtils.toUtf8Bytes;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -27,7 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
*/
public class CryptoToolsTest {
- private static record ProcessOutput(int exitCode, String stdOut, String stdErr) {}
+ private record ProcessOutput(int exitCode, String stdOut, String stdErr) {}
private static final byte[] EMPTY_BYTES = new byte[0];
@@ -88,6 +93,11 @@ public class CryptoToolsTest {
}
@Test
+ void reseal_help_printed_if_help_option_given_to_subtool() throws IOException {
+ verifyStdoutMatchesFile(List.of("reseal", "--help"), "expected-reseal-help-output.txt");
+ }
+
+ @Test
void missing_required_parameter_prints_error_message() throws IOException {
// We don't test all possible input arguments to all tools, since it'd be too closely
// bound to the order in which the implementation checks for argument presence.
@@ -174,6 +184,7 @@ public class CryptoToolsTest {
private static final String TEST_TOKEN = "OntP9gRVAjXeZIr4zkYqRJFcnA993v7ZEE7VbcNs1NcR3HdE7Mp" +
"wlwi3r3anF1kVa5fn7O1CyeHQpBWpdayUTKkrtyFepG6WJrZdE";
private static final String TEST_TOKEN_KEY_ID = "my key ID";
+ private static final String TEST_TOKEN_SECRET = "1b33b4dcd6a94e5a4a1ee6d208197d01";
@Test
void encrypt_fails_with_error_message_if_no_input_file_is_given() throws IOException {
@@ -200,10 +211,10 @@ public class CryptoToolsTest {
Files.writeString(privKeyFile, TEST_PRIV_KEY);
verifyStderrEquals(List.of("decrypt",
- "--output-file", "foo",
- "--recipient-private-key-file", absPathOf(privKeyFile),
- "--token", TEST_TOKEN,
- "--key-id", TEST_TOKEN_KEY_ID),
+ "--output-file", "foo",
+ "--private-key-file", absPathOf(privKeyFile),
+ "--token", TEST_TOKEN,
+ "--expected-key-id", TEST_TOKEN_KEY_ID),
"Invalid command line arguments: Expected exactly 1 file argument to decrypt\n");
}
@@ -214,10 +225,10 @@ public class CryptoToolsTest {
verifyStderrEquals(List.of("decrypt",
"no-such-file",
- "--output-file", "foo",
- "--recipient-private-key-file", absPathOf(privKeyFile),
- "--token", TEST_TOKEN,
- "--key-id", TEST_TOKEN_KEY_ID),
+ "--output-file", "foo",
+ "--private-key-file", absPathOf(privKeyFile),
+ "--token", TEST_TOKEN,
+ "--expected-key-id", TEST_TOKEN_KEY_ID),
"Invalid command line arguments: Input file 'no-such-file' does not exist\n");
}
@@ -231,11 +242,11 @@ public class CryptoToolsTest {
verifyStderrEquals(List.of("decrypt",
absPathOf(inputFile),
- "--output-file", "foo",
- "--recipient-private-key-file", absPathOf(privKeyFile),
- "--token", TEST_TOKEN,
- "--key-id", TEST_TOKEN_KEY_ID + "-wrong"),
- "Invalid command line arguments: Key ID specified with --key-id does not " +
+ "--output-file", "foo",
+ "--private-key-file", absPathOf(privKeyFile),
+ "--token", TEST_TOKEN,
+ "--expected-key-id", TEST_TOKEN_KEY_ID + "-wrong"),
+ "Invalid command line arguments: Key ID specified with --expected-key-id does not " +
"match key ID used when generating the supplied token\n");
}
@@ -271,6 +282,32 @@ public class CryptoToolsTest {
}
@Test
+ void can_reseal_a_token_to_another_recipient() throws IOException {
+ String recipientPrivKeyStr = "GdgfBZzPDqrCVs5f1xaYJpXVGwJzgdTAF1NNWiDk16YZ";
+ String recipientPubKeyStr = "AiUirFvFuLJ6s71QBNxiRcctB4umzM6r2roP4Rf8WDKM";
+
+ Path privKeyFile = pathInTemp("my-priv.txt");
+ Files.writeString(privKeyFile, TEST_PRIV_KEY);
+
+ var procOut = runMain(List.of(
+ "reseal",
+ TEST_TOKEN,
+ "--private-key-file", absPathOf(privKeyFile),
+ "--recipient-public-key", recipientPubKeyStr,
+ "--expected-key-id", TEST_TOKEN_KEY_ID,
+ "--key-id", "some-recipient-key-id"));
+ assertEquals(0, procOut.exitCode());
+ assertEquals("", procOut.stdErr());
+ var resealedToken = procOut.stdOut().strip();
+
+ // Verify that the resealed token wraps the same secret as the original one
+ var recipientPrivKey = KeyUtils.fromBase58EncodedX25519PrivateKey(recipientPrivKeyStr);
+ var recvShared = SharedKeyGenerator.fromSealedKey(SealedSharedKey.fromTokenString(resealedToken), recipientPrivKey);
+ assertEquals(KeyId.ofString("some-recipient-key-id"), recvShared.sealedSharedKey().keyId());
+ assertEquals(TEST_TOKEN_SECRET, hex(recvShared.secretKey().getEncoded()));
+ }
+
+ @Test
void can_end_to_end_keygen_encrypt_and_decrypt_via_files() throws IOException {
String greatSecret = "Dogs can't look up";
@@ -310,10 +347,10 @@ public class CryptoToolsTest {
procOut = runMain(List.of(
"decrypt",
absPathOf(encryptedPath),
- "--output-file", absPathOf(decryptedPath),
- "--recipient-private-key-file", absPathOf(privPath),
- "--key-id", "1234",
- "--token", token
+ "--output-file", absPathOf(decryptedPath),
+ "--private-key-file", absPathOf(privPath),
+ "--expected-key-id", "1234",
+ "--token", token
));
assertEquals(0, procOut.exitCode());
assertEquals("", procOut.stdOut());
@@ -359,10 +396,10 @@ public class CryptoToolsTest {
procOut = runMain(List.of(
"decrypt",
"-", // Decrypt stdin
- "--output-file", "-", // Plaintext to stdout
- "--recipient-private-key-file", absPathOf(privPath),
- "--key-id", "1234",
- "--token", token
+ "--output-file", "-", // Plaintext to stdout
+ "--private-key-file", absPathOf(privPath),
+ "--expected-key-id", "1234",
+ "--token", token
), Files.readAllBytes(encryptedPath));
assertEquals(0, procOut.exitCode());
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 ddf91c779e2..291fc3f4b96 100644
--- a/vespaclient-java/src/test/resources/expected-decrypt-help-output.txt
+++ b/vespaclient-java/src/test/resources/expected-decrypt-help-output.txt
@@ -5,18 +5,14 @@ given private key.
To decrypt the contents of STDIN, specify an input file of '-' (without
the quotes).
- -h,--help Show help
- -i,--key-id <arg> Numeric ID of recipient key. If
- this is not provided, the key ID
- stored as part of the token is
- not verified.
- -k,--recipient-private-key-file <arg> Recipient private key file in
- Base58 encoded format
- -o,--output-file <arg> Output file for decrypted
- plaintext. Specify '-' (without
- the quotes) to write plaintext to
- STDOUT instead of a file.
- -t,--token <arg> Token generated when the input
- file was encrypted
+ -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
+ -k,--private-key-file <arg> Private key file in Base58 encoded format
+ -o,--output-file <arg> Output file for decrypted plaintext.
+ Specify '-' (without the quotes) to write
+ plaintext to STDOUT instead of a file.
+ -t,--token <arg> Token generated when the input file was
+ encrypted
Note: this is a BETA tool version; its interface may be changed at any
time
diff --git a/vespaclient-java/src/test/resources/expected-encrypt-help-output.txt b/vespaclient-java/src/test/resources/expected-encrypt-help-output.txt
index beddc69855b..46185b29986 100644
--- a/vespaclient-java/src/test/resources/expected-encrypt-help-output.txt
+++ b/vespaclient-java/src/test/resources/expected-encrypt-help-output.txt
@@ -7,7 +7,7 @@ kept secret.
To encrypt the contents of STDIN, specify an input file of '-' (without
the quotes).
-h,--help Show help
- -i,--key-id <arg> Numeric ID of recipient key
+ -i,--key-id <arg> ID of recipient key
-o,--output-file <arg> Output file (will be truncated if it
already exists)
-r,--recipient-public-key <arg> Recipient X25519 public key in Base58
diff --git a/vespaclient-java/src/test/resources/expected-help-output.txt b/vespaclient-java/src/test/resources/expected-help-output.txt
index 8cea1366973..e3183f54a7c 100644
--- a/vespaclient-java/src/test/resources/expected-help-output.txt
+++ b/vespaclient-java/src/test/resources/expected-help-output.txt
@@ -1,4 +1,5 @@
usage: vespa-security <tool> [TOOL OPTIONS]
-Where <tool> is one of: keygen, encrypt, decrypt, token-info, convert-base
+Where <tool> is one of: keygen, encrypt, decrypt, token-info,
+convert-base, reseal
-h,--help Show help
Invoke vespa-security <tool> --help for tool-specific 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
new file mode 100644
index 00000000000..dcfea6c28bc
--- /dev/null
+++ b/vespaclient-java/src/test/resources/expected-reseal-help-output.txt
@@ -0,0 +1,15 @@
+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.
+ -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
+ -i,--key-id <arg> ID of recipient key
+ -k,--private-key-file <arg> Private key file in Base58 encoded
+ format
+ -r,--recipient-public-key <arg> Recipient X25519 public key in Base58
+ encoded format
+Note: this is a BETA tool version; its interface may be changed at any
+time