Skip to content

Commit 288871a

Browse files
nbaarsmp911de
authored andcommitted
Add support for versioned transit keys.
Closes gh-726 Original pull request: gh-792
1 parent 5ef39d5 commit 288871a

File tree

3 files changed

+114
-23
lines changed

3 files changed

+114
-23
lines changed

spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,10 @@ static void applyTransitOptions(VaultTransitContext context, Map<String, String>
477477
if (!ObjectUtils.isEmpty(context.getNonce())) {
478478
request.put("nonce", Base64.getEncoder().encodeToString(context.getNonce()));
479479
}
480+
481+
if (context.getKeyVersion() != 0) {
482+
request.put("key_version", "" + context.getKeyVersion());
483+
}
480484
}
481485

482486
static List<VaultEncryptionResult> toEncryptionResults(VaultResponse vaultResponse, List<Plaintext> batchRequest) {

spring-vault-core/src/main/java/org/springframework/vault/support/VaultTransitContext.java

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
*/
1616
package org.springframework.vault.support;
1717

18-
import java.util.Arrays;
19-
2018
import org.springframework.util.Assert;
2119

20+
import java.util.Arrays;
21+
2222
/**
2323
* Transit backend encryption/decryption/rewrapping context.
2424
*
@@ -30,15 +30,18 @@ public class VaultTransitContext {
3030
* Empty (default) {@link VaultTransitContext} without a {@literal context} and
3131
* {@literal nonce}.
3232
*/
33-
private static final VaultTransitContext EMPTY = new VaultTransitContext(new byte[0], new byte[0]);
33+
private static final VaultTransitContext EMPTY = new VaultTransitContext(new byte[0], new byte[0], 0);
3434

3535
private final byte[] context;
3636

3737
private final byte[] nonce;
3838

39-
VaultTransitContext(byte[] context, byte[] nonce) {
39+
private final int keyVersion;
40+
41+
VaultTransitContext(byte[] context, byte[] nonce, int keyVersion) {
4042
this.context = context;
4143
this.nonce = nonce;
44+
this.keyVersion = keyVersion;
4245
}
4346

4447
/**
@@ -89,20 +92,30 @@ public byte[] getNonce() {
8992
return this.nonce;
9093
}
9194

95+
/**
96+
* @return the version of the key to use for the operation. If not set, uses the
97+
* latest version. Must be greater than or equal to the key's min_encryption_version,
98+
* if set.
99+
*/
100+
public int getKeyVersion() {
101+
return this.keyVersion;
102+
}
103+
92104
@Override
93105
public boolean equals(Object o) {
94106
if (this == o)
95107
return true;
96108
if (!(o instanceof VaultTransitContext))
97109
return false;
98110
VaultTransitContext that = (VaultTransitContext) o;
99-
return Arrays.equals(this.context, that.context) && Arrays.equals(this.nonce, that.nonce);
111+
return Arrays.equals(this.context, that.context) && Arrays.equals(this.nonce, that.nonce)
112+
&& this.keyVersion == that.keyVersion;
100113
}
101114

102115
@Override
103116
public int hashCode() {
104117
int result = Arrays.hashCode(this.context);
105-
result = 31 * result + Arrays.hashCode(this.nonce);
118+
result = 31 * result + Arrays.hashCode(this.nonce) + this.keyVersion;
106119
return result;
107120
}
108121

@@ -115,6 +128,8 @@ public static class VaultTransitRequestBuilder {
115128

116129
private byte[] nonce = new byte[0];
117130

131+
private int keyVersion;
132+
118133
VaultTransitRequestBuilder() {
119134
}
120135

@@ -149,12 +164,19 @@ public VaultTransitRequestBuilder nonce(byte[] nonce) {
149164
return this;
150165
}
151166

167+
public VaultTransitRequestBuilder keyVersion(int keyVersion) {
168+
Assert.isTrue(keyVersion >= 0, "Key version must have a positive value");
169+
170+
this.keyVersion = keyVersion;
171+
return this;
172+
}
173+
152174
/**
153175
* Build a new {@link VaultTransitContext} instance.
154176
* @return a new {@link VaultTransitContext}.
155177
*/
156178
public VaultTransitContext build() {
157-
return new VaultTransitContext(this.context, this.nonce);
179+
return new VaultTransitContext(this.context, this.nonce, this.keyVersion);
158180
}
159181

160182
}

spring-vault-core/src/test/java/org/springframework/vault/core/VaultTransitTemplateIntegrationTests.java

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,48 @@
1515
*/
1616
package org.springframework.vault.core;
1717

18-
import java.util.Arrays;
19-
import java.util.Collections;
20-
import java.util.List;
21-
18+
import org.assertj.core.api.Assertions;
2219
import org.junit.jupiter.api.AfterEach;
2320
import org.junit.jupiter.api.BeforeEach;
2421
import org.junit.jupiter.api.Test;
2522
import org.junit.jupiter.api.extension.ExtendWith;
26-
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.Arguments;
25+
import org.junit.jupiter.params.provider.MethodSource;
2726
import org.springframework.beans.factory.annotation.Autowired;
2827
import org.springframework.test.context.ContextConfiguration;
2928
import org.springframework.test.context.junit.jupiter.SpringExtension;
3029
import org.springframework.vault.VaultException;
31-
import org.springframework.vault.support.*;
30+
import org.springframework.vault.support.Ciphertext;
31+
import org.springframework.vault.support.Hmac;
32+
import org.springframework.vault.support.Plaintext;
33+
import org.springframework.vault.support.RawTransitKey;
34+
import org.springframework.vault.support.Signature;
35+
import org.springframework.vault.support.SignatureValidation;
36+
import org.springframework.vault.support.TransitKeyType;
37+
import org.springframework.vault.support.VaultDecryptionResult;
38+
import org.springframework.vault.support.VaultEncryptionResult;
39+
import org.springframework.vault.support.VaultHmacRequest;
40+
import org.springframework.vault.support.VaultMount;
41+
import org.springframework.vault.support.VaultSignRequest;
42+
import org.springframework.vault.support.VaultSignatureVerificationRequest;
43+
import org.springframework.vault.support.VaultTransitContext;
44+
import org.springframework.vault.support.VaultTransitKey;
45+
import org.springframework.vault.support.VaultTransitKeyConfiguration;
46+
import org.springframework.vault.support.VaultTransitKeyCreationRequest;
3247
import org.springframework.vault.util.IntegrationTestSupport;
3348
import org.springframework.vault.util.RequiresVaultVersion;
3449
import org.springframework.vault.util.Version;
3550

36-
import static org.assertj.core.api.Assertions.*;
51+
import java.util.Arrays;
52+
import java.util.Collections;
53+
import java.util.List;
54+
import java.util.stream.IntStream;
55+
import java.util.stream.Stream;
56+
57+
import static org.assertj.core.api.Assertions.assertThat;
58+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
59+
import static org.assertj.core.api.Assertions.fail;
3760

3861
/**
3962
* Integration tests for {@link VaultTransitTemplate} through
@@ -327,19 +350,29 @@ void encryptShouldCreateCiphertext() {
327350
assertThat(ciphertext).startsWith("vault:v");
328351
}
329352

330-
@Test
331-
void encryptShouldCreateCiphertextWithNonceAndContext() {
353+
private static Stream<Arguments> encryptWithKeyVersion() {
354+
return Stream.of(Arguments.of(1, 1, "v1"), Arguments.of(2, 2, "v2"), Arguments.of(1, 2, ""),
355+
Arguments.of(2, 1, "v1"), Arguments.of("2", "0", "v2"));
356+
}
332357

333-
this.transitOperations.createKey("mykey",
334-
VaultTransitKeyCreationRequest.builder().convergentEncryption(true).derived(true).build());
358+
@ParameterizedTest
359+
@MethodSource
360+
void encryptWithKeyVersion(int keyVersion, int usedKeyVersionWhileEncrypting, String expectedKeyPrefix) {
361+
this.transitOperations.createKey("mykey", VaultTransitKeyCreationRequest.builder().build());
362+
// rotate the key to get the right version
363+
IntStream.range(0, keyVersion - 1).forEach(__ -> this.transitOperations.rotate("mykey"));
335364

336365
VaultTransitContext transitRequest = VaultTransitContext.builder()
337-
.context("blubb".getBytes()) //
338-
.nonce("123456789012".getBytes()) //
366+
.keyVersion(usedKeyVersionWhileEncrypting)
339367
.build();
340368

341-
String ciphertext = this.transitOperations.encrypt("mykey", "hello-world".getBytes(), transitRequest);
342-
assertThat(ciphertext).startsWith("vault:v1:");
369+
try {
370+
String ciphertext = this.transitOperations.encrypt("mykey", "hello-world".getBytes(), transitRequest);
371+
assertThat(ciphertext).startsWith("vault:%s:".formatted(expectedKeyPrefix));
372+
}
373+
catch (Exception e) {
374+
Assertions.assertThat(expectedKeyPrefix).isNullOrEmpty();
375+
}
343376
}
344377

345378
@Test
@@ -388,6 +421,38 @@ void decryptShouldCreatePlaintext() {
388421
assertThat(plaintext).isEqualTo("hello-world");
389422
}
390423

424+
private static Stream<Arguments> decryptWithKeyVersion() {
425+
return Stream.of(Arguments.of(1, 1, true), Arguments.of(2, 2, true), Arguments.of(1, 2, false),
426+
Arguments.of(2, 1, true), Arguments.of("2", "0", true));
427+
}
428+
429+
@ParameterizedTest
430+
@MethodSource
431+
void decryptWithKeyVersion(int keyVersion, int usedKeyVersionWhileEncrypting, boolean shouldPass) {
432+
this.transitOperations.createKey("mykey");
433+
// rotate the key to get the right version
434+
IntStream.range(0, keyVersion - 1).forEach(__ -> this.transitOperations.rotate("mykey"));
435+
436+
VaultTransitContext transitRequest = VaultTransitContext.builder()
437+
.keyVersion(usedKeyVersionWhileEncrypting)
438+
.build();
439+
440+
try {
441+
String ciphertext = this.transitOperations
442+
.encrypt("mykey", Plaintext.of("hello-world").with(transitRequest))
443+
.getCiphertext();
444+
String plaintext = Plaintext.of(this.transitOperations.decrypt("mykey", ciphertext, transitRequest))
445+
.asString();
446+
447+
assertThat(shouldPass).isTrue();
448+
assertThat(plaintext).isEqualTo("hello-world");
449+
450+
}
451+
catch (VaultException e) {
452+
assertThat(shouldPass).isFalse();
453+
}
454+
}
455+
391456
@Test
392457
void decryptShouldCreatePlaintextWithNonceAndContext() {
393458

@@ -580,7 +645,7 @@ void shouldBatchDecryptWithWrongContext() {
580645
}
581646
catch (VaultException e) {
582647
assertThat(e).hasMessageContaining("error"); // Vault 1.6 behavior is
583-
// different
648+
// different
584649
}
585650
}
586651

0 commit comments

Comments
 (0)