diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java index f2b3cae4..cf4570ea 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,18 +15,17 @@ */ package org.springframework.vault.core; -import java.util.function.Function; - import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import org.springframework.lang.Nullable; import org.springframework.vault.VaultException; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultResponseSupport; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientException; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.function.Function; /** * Interface that specifies a basic set of Vault operations executed on a reactive @@ -123,4 +122,17 @@ public interface ReactiveVaultOperations { > T doWithSession(Function sessionCallback) throws VaultException, WebClientException; + /** + * @return the operations interface to interact with the Vault transit backend. + */ + ReactiveVaultTransitOperations opsForTransit(); + + /** + * Return {@link ReactiveVaultTransitOperations} if the transit backend is mounted on + * a different path than {@code transit}. + * @param path the mount path + * @return the operations interface to interact with the Vault transit backend. + */ + ReactiveVaultTransitOperations opsForTransit(String path); + } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java index ac403e8e..ea2f4547 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,17 +15,9 @@ */ package org.springframework.vault.core; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -49,6 +41,12 @@ import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec; import org.springframework.web.reactive.function.client.WebClientException; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; import static org.springframework.web.reactive.function.client.ExchangeFilterFunction.ofRequestProcessor; @@ -367,4 +365,14 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations { } + @Override + public ReactiveVaultTransitOperations opsForTransit() { + return opsForTransit("transit"); + } + + @Override + public ReactiveVaultTransitOperations opsForTransit(String path) { + return new ReactiveVaultTransitTemplate(this, path); + } + } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTransitOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTransitOperations.java new file mode 100644 index 00000000..99ce4a4b --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTransitOperations.java @@ -0,0 +1,278 @@ +/* + * Copyright 2023-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.core; + +import org.springframework.vault.support.Ciphertext; +import org.springframework.vault.support.Hmac; +import org.springframework.vault.support.Plaintext; +import org.springframework.vault.support.RawTransitKey; +import org.springframework.vault.support.Signature; +import org.springframework.vault.support.SignatureValidation; +import org.springframework.vault.support.TransitKeyType; +import org.springframework.vault.support.VaultDecryptionResult; +import org.springframework.vault.support.VaultEncryptionResult; +import org.springframework.vault.support.VaultHmacRequest; +import org.springframework.vault.support.VaultSignRequest; +import org.springframework.vault.support.VaultSignatureVerificationRequest; +import org.springframework.vault.support.VaultTransitContext; +import org.springframework.vault.support.VaultTransitKey; +import org.springframework.vault.support.VaultTransitKeyConfiguration; +import org.springframework.vault.support.VaultTransitKeyCreationRequest; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * Interface that specifies a set of {@code transit} operations executed on a reactive + * infrastructure, implemented by + * {@link org.springframework.vault.core.ReactiveVaultTransitTemplate}. + * + * @author James Luke + */ +public interface ReactiveVaultTransitOperations { + + /** + * Create a new named encryption key given a {@code name} + * @param keyName must not be empty or {@literal null} + */ + Mono createKey(String keyName); + + /** + * Create a new named encryption key given a {@code name} and + * {@link VaultTransitKeyCreationRequest}. The key options set here cannot be changed + * after key creation. + * @param keyName must not be empty or {@literal null}. + * @param createKeyRequest must not be {@literal null}. + */ + Mono createKey(String keyName, VaultTransitKeyCreationRequest createKeyRequest); + + /** + * @return stream of transit key names. + */ + Flux getKeys(); + + /** + * Create a new named encryption key given a {@code name}. + * @param keyName must not be empty or {@literal null}. + * @param keyConfiguration must not be {@literal null}. + */ + Mono configureKey(String keyName, VaultTransitKeyConfiguration keyConfiguration); + + /** + * Returns the value of the named encryption key. Depending on the type of key, + * different information may be returned. The key must be exportable to support this + * operation. + * @param keyName must not be empty or {@literal null}. + * @param type must not be {@literal null}. + * @return the {@link RawTransitKey}. May be empty if key does not exist + */ + Mono exportKey(String keyName, TransitKeyType type); + + /** + * Return information about a named encryption key. + * @param keyName must not be empty or {@literal null}. + * @return the {@link VaultTransitKey}. May be empty if key does not exist + */ + Mono getKey(String keyName); + + /** + * Deletes a named encryption key. It will no longer be possible to decrypt any data + * encrypted with the named key. + * @param keyName must not be empty or {@literal null}. + */ + Mono deleteKey(String keyName); + + /** + * Rotates the version of the named key. After rotation, new plain text requests will + * be encrypted with the new version of the key. To upgrade ciphertext to be encrypted + * with the latest version of the key, use {@link #rewrap(String, String)}. + * @param keyName must not be empty or {@literal null}. + * @see #rewrap(String, String) + */ + Mono rotate(String keyName); + + /** + * Encrypts the provided plain text using the named key. The given {@code plaintext} + * is encoded into bytes using the {@link java.nio.charset.Charset#defaultCharset() + * default charset}. Use + * {@link #encrypt(String, org.springframework.vault.support.Plaintext)} to construct + * a {@link org.springframework.vault.support.Plaintext#of(byte[]) Plaintext} object + * from bytes to avoid {@link java.nio.charset.Charset} mismatches. + * @param keyName must not be empty or {@literal null}. + * @param plaintext must not be empty or {@literal null}. + * @return cipher text. + */ + Mono encrypt(String keyName, String plaintext); + + /** + * Encrypts the provided {@code plaintext} using the named key. + * @param keyName must not be empty or {@literal null}. + * @param plaintext must not be {@literal null}. + * @return cipher text. + */ + Mono encrypt(String keyName, Plaintext plaintext); + + /** + * Encrypts the provided {@code plaintext} using the named key. + * @param keyName must not be empty or {@literal null}. + * @param plaintext must not be empty or {@literal null}. + * @param transitRequest must not be {@literal null}. Use + * {@link VaultTransitContext#empty()} if no request options provided. + * @return cipher text. + */ + Mono encrypt(String keyName, byte[] plaintext, VaultTransitContext transitRequest); + + /** + * Encrypts the provided batch of {@code plaintext} using the named key and context. + * The encryption is done using transit backend's batch operation. + * @param keyName must not be empty or {@literal null}. + * @param batchRequest a list of {@link Plaintext} which includes plain text and an + * optional context. + * @return the encrypted result in the order of {@code batchRequest} plaintexts. + */ + Flux encrypt(String keyName, List batchRequest); + + /** + * Decrypts the provided plain text using the named key. The decoded {@code plaintext} + * is decoded into {@link String} the {@link java.nio.charset.Charset#defaultCharset() + * default charset}. Use + * {@link #decrypt(String, org.springframework.vault.support.Ciphertext)} to obtain a + * {@link org.springframework.vault.support.Ciphertext} object that allows to control + * the {@link java.nio.charset.Charset} for later consumption. + * @param keyName must not be empty or {@literal null}. + * @param ciphertext must not be empty or {@literal null}. + * @return plain text. + */ + Mono<String> decrypt(String keyName, String ciphertext); + + /** + * Decrypts the provided cipher text using the named key. + * @param keyName must not be empty or {@literal null}. + * @param ciphertext must not be {@literal null}. + * @return plain text. + */ + Mono<Plaintext> decrypt(String keyName, Ciphertext ciphertext); + + /** + * Decrypts the provided {@code ciphertext} using the named key. + * @param keyName must not be empty or {@literal null}. + * @param ciphertext must not be empty or {@literal null}. + * @param transitContext must not be {@literal null}. Use + * {@link VaultTransitContext#empty()} if no request options provided. + * @return cipher text. + * @return plain text. + */ + Mono<byte[]> decrypt(String keyName, String ciphertext, VaultTransitContext transitContext); + + /** + * Decrypts the provided batch of cipher text using the named key and context. The* + * decryption is done using transit backend's batch operation. + * @param keyName must not be empty or {@literal null}. + * @param batchRequest a list of {@link Ciphertext} which includes plain text and an + * optional context. + * @return the decrypted result in the order of {@code batchRequest} ciphertexts. + */ + Flux<VaultDecryptionResult> decrypt(String keyName, List<Ciphertext> batchRequest); + + /** + * Rewrap the provided cipher text using the latest version of the named key. Because + * this never returns plain text, it is possible to delegate this functionality to + * untrusted users or scripts. + * @param keyName must not be empty or {@literal null}. + * @param ciphertext must not be empty or {@literal null}. + * @return cipher text. + * @see #rotate(String) + */ + Mono<String> rewrap(String keyName, String ciphertext); + + /** + * Rewrap the provided cipher text using the latest version of the named key. Because + * this never returns plain text, it is possible to delegate this functionality to + * untrusted users or scripts. + * @param keyName must not be empty or {@literal null}. + * @param ciphertext must not be empty or {@literal null}. + * @param transitContext must not be {@literal null}. Use + * {@link VaultTransitContext#empty()} if no request options provided. + * @return cipher text. + * @see #rotate(String) + */ + Mono<String> rewrap(String keyName, String ciphertext, VaultTransitContext transitContext); + + /** + * Create a HMAC using {@code keyName} of given {@link Plaintext} using the default + * hash algorithm. The key can be of any type supported by transit; the raw key will + * be marshaled into bytes to be used for the HMAC function. If the key is of a type + * that supports rotation, the latest (current) version will be used. + * @param keyName must not be empty or {@literal null}. + * @param plaintext must not be {@literal null}. + * @return the digest of given data the default hash algorithm and the named key. + */ + Mono<Hmac> getHmac(String keyName, Plaintext plaintext); + + /** + * Create a HMAC using {@code keyName} of given {@link VaultHmacRequest} using the + * default hash algorithm. The key can be of any type supported by transit; the raw + * key will be marshaled into bytes to be used for the HMAC function. If the key is of + * a type that supports rotation, configured {@link VaultHmacRequest#getKeyVersion()} + * will be used. + * @param keyName must not be empty or {@literal null}. + * @param hmacRequest the {@link VaultHmacRequest}, must not be {@literal null}. + * @return the digest of given data the default hash algorithm and the named key. + */ + Mono<Hmac> getHmac(String keyName, VaultHmacRequest hmacRequest); + + /** + * Create a cryptographic signature using {@code keyName} of the given + * {@link Plaintext} and the default hash algorithm. The key must be of a type that + * supports signing. + * @param keyName must not be empty or {@literal null}. + * @param plaintext must not be empty or {@literal null}. + * @return Signature for {@link Plaintext}. + */ + Mono<Signature> sign(String keyName, Plaintext plaintext); + + /** + * Create a cryptographic signature using {@code keyName} of the given + * {@link VaultSignRequest} and the specified hash algorithm. The key must be of a + * type that supports signing. + * @param keyName must not be empty or {@literal null}. + * @param signRequest {@link VaultSignRequest} must not be empty or {@literal null}. + * @return Signature for {@link VaultSignRequest}. + */ + Mono<Signature> sign(String keyName, VaultSignRequest signRequest); + + /** + * Verify the cryptographic signature using {@code keyName} of the given + * {@link Plaintext} and {@link Signature}. + * @param keyName must not be empty or {@literal null}. + * @param plaintext must not be {@literal null}. + * @param signature Signature to be verified, must not be {@literal null}. + * @return {@literal true} if the signature is valid, {@literal false} otherwise. + */ + Mono<Boolean> verify(String keyName, Plaintext plaintext, Signature signature); + + /** + * Verify the cryptographic signature using {@code keyName} of the given + * {@link VaultSignRequest}. + * @param keyName must not be empty or {@literal null}. + * @param verificationRequest {@link VaultSignatureVerificationRequest} must not be + * {@literal null}. + * @return the resulting {@link SignatureValidation}. + */ + Mono<SignatureValidation> verify(String keyName, VaultSignatureVerificationRequest verificationRequest); + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTransitTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTransitTemplate.java new file mode 100644 index 00000000..421ce2bd --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTransitTemplate.java @@ -0,0 +1,334 @@ +/* + * Copyright 2023-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.core; + +import org.springframework.util.Assert; +import org.springframework.vault.core.VaultTransitTemplate.VaultTransitKeyImpl; +import org.springframework.vault.support.Ciphertext; +import org.springframework.vault.support.Hmac; +import org.springframework.vault.support.Plaintext; +import org.springframework.vault.support.RawTransitKey; +import org.springframework.vault.support.Signature; +import org.springframework.vault.support.SignatureValidation; +import org.springframework.vault.support.TransitKeyType; +import org.springframework.vault.support.VaultDecryptionResult; +import org.springframework.vault.support.VaultEncryptionResult; +import org.springframework.vault.support.VaultHmacRequest; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultResponseSupport; +import org.springframework.vault.support.VaultSignRequest; +import org.springframework.vault.support.VaultSignatureVerificationRequest; +import org.springframework.vault.support.VaultTransitContext; +import org.springframework.vault.support.VaultTransitKey; +import org.springframework.vault.support.VaultTransitKeyConfiguration; +import org.springframework.vault.support.VaultTransitKeyCreationRequest; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Base64; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.springframework.vault.core.VaultTransitTemplate.*; + +/** + * Default implementation of {@link ReactiveVaultTransitOperations} + * + * @author James Luke + */ +public class ReactiveVaultTransitTemplate implements ReactiveVaultTransitOperations { + + private final ReactiveVaultOperations reactiveVaultOperations; + + private final String path; + + public ReactiveVaultTransitTemplate(ReactiveVaultOperations reactiveVaultOperations, String path) { + Assert.notNull(reactiveVaultOperations, "ReactiveVaultOperations must not be null"); + Assert.hasText(path, "Path must not be empty"); + + this.reactiveVaultOperations = reactiveVaultOperations; + this.path = path; + } + + @Override + public Mono<Void> createKey(String keyName) { + Assert.hasText(keyName, "Key name must not be empty"); + + return this.reactiveVaultOperations.write(String.format("%s/keys/%s", this.path, keyName), null).then(); + } + + @Override + public Mono<Void> createKey(String keyName, VaultTransitKeyCreationRequest createKeyRequest) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notNull(createKeyRequest, "VaultTransitKeyCreationRequest must not be empty"); + + return this.reactiveVaultOperations.write(String.format("%s/keys/%s", this.path, keyName), createKeyRequest) + .then(); + } + + @Override + public Mono<Void> rotate(String keyName) { + Assert.hasText(keyName, "Key name must not be empty"); + + return this.reactiveVaultOperations.write(String.format("%s/keys/%s/rotate", this.path, keyName), null).then(); + } + + @Override + public Mono<String> encrypt(String keyName, String plaintext) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notNull(plaintext, "Plaintext must not be null"); + + Map<String, String> request = new LinkedHashMap<>(); + + request.put("plaintext", Base64.getEncoder().encodeToString(plaintext.getBytes())); + + return this.reactiveVaultOperations.write(String.format("%s/encrypt/%s", this.path, keyName), request) + .map(it -> (String) it.getRequiredData().get("ciphertext")); + } + + @Override + public Mono<Void> configureKey(String keyName, VaultTransitKeyConfiguration keyConfiguration) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notNull(keyConfiguration, "VaultKeyConfiguration must not be empty"); + + return this.reactiveVaultOperations + .write(String.format("%s/keys/%s/config", this.path, keyName), keyConfiguration).then(); + } + + @Override + public Mono<Void> deleteKey(String keyName) { + Assert.hasText(keyName, "Key name must not be empty"); + + return this.reactiveVaultOperations.delete(String.format("%s/keys/%s", this.path, keyName)); + } + + @Override + @SuppressWarnings("unchecked") + public Flux<String> getKeys() { + return this.reactiveVaultOperations.read(String.format("%s/keys?list=true", this.path)) + .flatMapIterable(it -> (List<String>) it.getRequiredData().get("keys")); + } + + @Override + public Mono<String> encrypt(String keyName, byte[] plaintext, VaultTransitContext transitContext) { + Assert.notNull(plaintext, "Plaintext must not be null"); + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notNull(transitContext, "VaultTransitContext must not be null"); + + Map<String, String> request = new LinkedHashMap<>(); + + request.put("plaintext", Base64.getEncoder().encodeToString(plaintext)); + + applyTransitOptions(transitContext, request); + + return this.reactiveVaultOperations.write(String.format("%s/encrypt/%s", this.path, keyName), request) + .map(it -> (String) it.getRequiredData().get("ciphertext")); + } + + @Override + public Mono<Ciphertext> encrypt(String keyName, Plaintext plaintext) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notNull(plaintext, "Plaintext must not be null"); + + return encrypt(keyName, plaintext.getPlaintext(), plaintext.getContext()) + .map(ciphertext -> toCiphertext(ciphertext, plaintext.getContext())); + } + + @Override + public Mono<String> decrypt(String keyName, String ciphertext) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.hasText(ciphertext, "Ciphertext must not be empty"); + + Map<String, String> request = new LinkedHashMap<>(); + + request.put("ciphertext", ciphertext); + + return this.reactiveVaultOperations.write(String.format("%s/decrypt/%s", this.path, keyName), request) + .map(it -> (String) it.getRequiredData().get("plaintext")) + .map(plaintext -> new String(Base64.getDecoder().decode(plaintext))); + } + + @Override + public Mono<Plaintext> decrypt(String keyName, Ciphertext ciphertext) { + Assert.hasText(keyName, "Key name must not be null"); + Assert.notNull(ciphertext, "Ciphertext must not be null"); + + return decrypt(keyName, ciphertext.getCiphertext(), ciphertext.getContext()) + .map(plaintext -> Plaintext.of(plaintext).with(ciphertext.getContext())); + } + + @Override + public Mono<byte[]> decrypt(String keyName, String ciphertext, VaultTransitContext transitContext) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.hasText(ciphertext, "Ciphertext must not be empty"); + Assert.notNull(transitContext, "VaultTransitContext must not be null"); + + Map<String, String> request = new LinkedHashMap<>(); + + request.put("ciphertext", ciphertext); + + applyTransitOptions(transitContext, request); + + return this.reactiveVaultOperations.write(String.format("%s/decrypt/%s", this.path, keyName), request) + .map(it -> (String) it.getRequiredData().get("plaintext")).map(Base64.getDecoder()::decode); + } + + @Override + public Mono<String> rewrap(String keyName, String ciphertext) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.hasText(ciphertext, "Ciphertext must not be empty"); + + Map<String, String> request = new LinkedHashMap<>(); + request.put("ciphertext", ciphertext); + + return this.reactiveVaultOperations.write(String.format("%s/rewrap/%s", this.path, keyName), request) + .map(response -> (String) response.getRequiredData().get("ciphertext")); + } + + @Override + public Mono<String> rewrap(String keyName, String ciphertext, VaultTransitContext transitContext) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.hasText(ciphertext, "Ciphertext must not be empty"); + Assert.notNull(transitContext, "VaultTransitContext must not be null"); + + Map<String, String> request = new LinkedHashMap<>(); + + request.put("ciphertext", ciphertext); + + applyTransitOptions(transitContext, request); + + return this.reactiveVaultOperations.write(String.format("%s/rewrap/%s", this.path, keyName), request) + .map(response -> (String) response.getRequiredData().get("ciphertext")); + } + + @Override + public Flux<VaultEncryptionResult> encrypt(String keyName, List<Plaintext> batchRequest) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notEmpty(batchRequest, "BatchRequest must not be null and must have at least one entry"); + + return Flux.fromIterable(batchRequest).map(request -> { + Map<String, String> vaultRequest = new LinkedHashMap<>(2); + vaultRequest.put("plaintext", Base64.getEncoder().encodeToString(request.getPlaintext())); + applyTransitOptions(request.getContext(), vaultRequest); + return vaultRequest; + }).collectList() + .flatMap(batch -> this.reactiveVaultOperations.write(String.format("%s/encrypt/%s", this.path, keyName), + Collections.singletonMap("batch_input", batch))) + .flatMapIterable(vaultResponse -> toEncryptionResults(vaultResponse, batchRequest)); + } + + @Override + public Flux<VaultDecryptionResult> decrypt(String keyName, List<Ciphertext> batchRequest) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notEmpty(batchRequest, "BatchRequest must not be null and must have at least one entry"); + + return Flux.fromIterable(batchRequest).map(request -> { + Map<String, String> vaultRequest = new LinkedHashMap<>(2); + vaultRequest.put("ciphertext", request.getCiphertext()); + applyTransitOptions(request.getContext(), vaultRequest); + return vaultRequest; + }).collectList() + .flatMap(batch -> this.reactiveVaultOperations.write(String.format("%s/decrypt/%s", this.path, keyName), + Collections.singletonMap("batch_input", batch))) + .flatMapIterable(vaultResponse -> toDecryptionResults(vaultResponse, batchRequest)); + } + + @Override + public Mono<Hmac> getHmac(String keyName, Plaintext plaintext) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notNull(plaintext, "Plaintext must not be null"); + + VaultHmacRequest request = VaultHmacRequest.create(plaintext); + + return getHmac(keyName, request); + } + + @Override + public Mono<Hmac> getHmac(String keyName, VaultHmacRequest hmacRequest) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notNull(hmacRequest, "HMAC request must not be null"); + + return this.reactiveVaultOperations.write(String.format("%s/hmac/%s", this.path, keyName), hmacRequest) + .map(vaultResponse -> (String) vaultResponse.getRequiredData().get("hmac")).map(Hmac::of); + } + + @Override + public Mono<Signature> sign(String keyName, Plaintext plaintext) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notNull(plaintext, "Plaintext must not be null"); + + VaultSignRequest request = VaultSignRequest.create(plaintext); + + return sign(keyName, request); + } + + @Override + public Mono<Signature> sign(String keyName, VaultSignRequest signRequest) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notNull(signRequest, "Sign request must not be null"); + + return this.reactiveVaultOperations.write(String.format("%s/sign/%s", this.path, keyName), signRequest) + .map(vaultResponse -> (String) vaultResponse.getRequiredData().get("signature")).map(Signature::of); + } + + @Override + public Mono<Boolean> verify(String keyName, Plaintext plaintext, Signature signature) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notNull(plaintext, "Plaintext must not be null"); + Assert.notNull(signature, "Signature must not be null"); + + VaultSignatureVerificationRequest request = VaultSignatureVerificationRequest.create(plaintext, signature); + + return verify(keyName, request).map(SignatureValidation::isValid); + } + + @Override + public Mono<SignatureValidation> verify(String keyName, VaultSignatureVerificationRequest verificationRequest) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notNull(verificationRequest, "Signature verification request must not be null"); + + return this.reactiveVaultOperations + .write(String.format("%s/verify/%s", this.path, keyName), verificationRequest) + .map(VaultResponse::getRequiredData).map(vaultResponse -> { + if (vaultResponse.containsKey("valid") && (Boolean) vaultResponse.get("valid")) { + return SignatureValidation.valid(); + } + return SignatureValidation.invalid(); + }); + } + + @Override + public Mono<RawTransitKey> exportKey(String keyName, TransitKeyType type) { + Assert.hasText(keyName, "Key name must not be empty"); + Assert.notNull(type, "Key type must not be null"); + + return this.reactiveVaultOperations + .read(String.format("%s/export/%s/%s", this.path, type.getValue(), keyName), + VaultTransitTemplate.RawTransitKeyImpl.class) + .flatMap(vaultResponse -> Mono.justOrEmpty(vaultResponse.getRequiredData())); + } + + @Override + public Mono<VaultTransitKey> getKey(String keyName) { + Assert.hasText(keyName, "Key name must not be empty"); + return this.reactiveVaultOperations + .read(String.format("%s/keys/%s", this.path, keyName), VaultTransitKeyImpl.class) + .map(VaultResponseSupport::getRequiredData); + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java index e73a6d98..1ab84aec 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,15 +15,7 @@ */ package org.springframework.vault.core; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - import com.fasterxml.jackson.annotation.JsonProperty; - import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.Base64Utils; @@ -49,6 +41,13 @@ import org.springframework.vault.support.VaultTransitKey; import org.springframework.vault.support.VaultTransitKeyConfiguration; import org.springframework.vault.support.VaultTransitKeyCreationRequest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + /** * Default implementation of {@link VaultTransitOperations}. * @@ -383,7 +382,6 @@ public class VaultTransitTemplate implements VaultTransitOperations { @Override public Signature sign(String keyName, VaultSignRequest signRequest) { - Assert.hasText(keyName, "Key name must not be empty"); Assert.notNull(signRequest, "Sign request must not be null"); @@ -443,7 +441,7 @@ public class VaultTransitTemplate implements VaultTransitOperations { return SignatureValidation.invalid(); } - private static void applyTransitOptions(VaultTransitContext context, Map<String, String> request) { + public static void applyTransitOptions(VaultTransitContext context, Map<String, String> request) { if (!ObjectUtils.isEmpty(context.getContext())) { request.put("context", Base64Utils.encodeToString(context.getContext())); @@ -454,7 +452,7 @@ public class VaultTransitTemplate implements VaultTransitOperations { } } - private static List<VaultEncryptionResult> toEncryptionResults(VaultResponse vaultResponse, + public static List<VaultEncryptionResult> toEncryptionResults(VaultResponse vaultResponse, List<Plaintext> batchRequest) { List<VaultEncryptionResult> result = new ArrayList<>(batchRequest.size()); @@ -484,7 +482,7 @@ public class VaultTransitTemplate implements VaultTransitOperations { return result; } - private static List<VaultDecryptionResult> toDecryptionResults(VaultResponse vaultResponse, + public static List<VaultDecryptionResult> toDecryptionResults(VaultResponse vaultResponse, List<Ciphertext> batchRequest) { List<VaultDecryptionResult> result = new ArrayList<>(batchRequest.size()); @@ -523,12 +521,12 @@ public class VaultTransitTemplate implements VaultTransitOperations { return new VaultDecryptionResult(Plaintext.empty().with(ciphertext.getContext())); } - private static Ciphertext toCiphertext(String ciphertext, @Nullable VaultTransitContext context) { + public static Ciphertext toCiphertext(String ciphertext, @Nullable VaultTransitContext context) { return context != null ? Ciphertext.of(ciphertext).with(context) : Ciphertext.of(ciphertext); } @SuppressWarnings("unchecked") - private static List<Map<String, String>> getBatchData(VaultResponse vaultResponse) { + public static List<Map<String, String>> getBatchData(VaultResponse vaultResponse) { return (List<Map<String, String>>) vaultResponse.getRequiredData().get("batch_results"); } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/PlaintextToBase64StringConverter.java b/spring-vault-core/src/main/java/org/springframework/vault/support/PlaintextToBase64StringConverter.java new file mode 100644 index 00000000..45cdecaa --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/PlaintextToBase64StringConverter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.support; + +import com.fasterxml.jackson.databind.util.StdConverter; +import org.springframework.util.Base64Utils; + +import java.util.Base64; + +/** + * Converts Plaintext to Base64 encoded string for use with + * {@link com.fasterxml.jackson.databind.ObjectMapper} + * + * @author James Luke + */ +public class PlaintextToBase64StringConverter extends StdConverter<Plaintext, String> { + + @Override + public String convert(Plaintext plaintext) { + return Base64.getEncoder().encodeToString(plaintext.getPlaintext()); + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultHmacRequest.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultHmacRequest.java index 5317413f..06d1af5f 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultHmacRequest.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultHmacRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ */ package org.springframework.vault.support; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -25,12 +28,17 @@ import org.springframework.util.Assert; * @author Mark Paluch * @since 2.0 */ +@JsonInclude(JsonInclude.Include.NON_NULL) public class VaultHmacRequest { + @JsonProperty("input") + @JsonSerialize(converter = PlaintextToBase64StringConverter.class) private final Plaintext plaintext; + @JsonProperty("algorithm") private final @Nullable String algorithm; + @JsonProperty("key_version") private final @Nullable Integer keyVersion; private VaultHmacRequest(Plaintext plaintext, @Nullable String algorithm, @Nullable Integer keyVersion) { diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignRequest.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignRequest.java index 2cbb4343..2041a248 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignRequest.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ */ package org.springframework.vault.support; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -28,6 +30,8 @@ import org.springframework.util.Assert; */ public class VaultSignRequest { + @JsonProperty("input") + @JsonSerialize(converter = PlaintextToBase64StringConverter.class) private final Plaintext plaintext; private final @Nullable String hashAlgorithm; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignatureVerificationRequest.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignatureVerificationRequest.java index 8d315f1a..37f2e45c 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignatureVerificationRequest.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignatureVerificationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,16 @@ */ package org.springframework.vault.support; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.vault.support.VaultSignatureVerificationRequest.VaultSignatureVerificationRequestSerializer; + +import java.io.IOException; /** * Request for a signature verification. @@ -24,8 +32,10 @@ import org.springframework.util.Assert; * @author Luander Ribeiro * @author Mark Paluch * @author My-Lan Aragon + * @author James Luke * @since 2.0 */ +@JsonSerialize(using = VaultSignatureVerificationRequestSerializer.class) public class VaultSignatureVerificationRequest { private final Plaintext plaintext; @@ -135,6 +145,31 @@ public class VaultSignatureVerificationRequest { return getSignatureAlgorithm(); } + static class VaultSignatureVerificationRequestSerializer extends JsonSerializer<VaultSignatureVerificationRequest> { + + static PlaintextToBase64StringConverter plaintextConverter = new PlaintextToBase64StringConverter(); + + @Override + public void serialize(VaultSignatureVerificationRequest request, JsonGenerator gen, + SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeStringField("input", plaintextConverter.convert(request.plaintext)); + if (request.getHmac() != null) { + gen.writeStringField("hmac", request.getHmac().getHmac()); + } + + if (request.getSignature() != null) { + gen.writeStringField("signature", request.getSignature().getSignature()); + } + + if (StringUtils.hasText(request.getAlgorithm())) { + gen.writeStringField("algorithm", request.getAlgorithm()); + } + gen.writeEndObject(); + } + + } + /** * Builder to build a {@link VaultSignatureVerificationRequest}. */ diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultTransitIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultTransitIntegrationTests.java new file mode 100644 index 00000000..9f06ba4c --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultTransitIntegrationTests.java @@ -0,0 +1,725 @@ +/* + * Copyright 2023-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.core; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.vault.VaultException; +import org.springframework.vault.support.Ciphertext; +import org.springframework.vault.support.Plaintext; +import org.springframework.vault.support.SignatureValidation; +import org.springframework.vault.support.TransitKeyType; +import org.springframework.vault.support.VaultDecryptionResult; +import org.springframework.vault.support.VaultHmacRequest; +import org.springframework.vault.support.VaultMount; +import org.springframework.vault.support.VaultSignRequest; +import org.springframework.vault.support.VaultSignatureVerificationRequest; +import org.springframework.vault.support.VaultTransitContext; +import org.springframework.vault.support.VaultTransitKeyConfiguration; +import org.springframework.vault.support.VaultTransitKeyCreationRequest; +import org.springframework.vault.util.IntegrationTestSupport; +import org.springframework.vault.util.RequiresVaultVersion; +import org.springframework.vault.util.Version; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.vault.core.VaultTransitTemplateIntegrationTests.*; + +/** + * Integration tests for {@link ReactiveVaultTransitTemplate} using the {@code generic} + * backend. + * + * @author James Luke + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class) +public class ReactiveVaultTransitIntegrationTests extends IntegrationTestSupport { + + @Autowired + VaultOperations vaultOperations; + + @Autowired + ReactiveVaultOperations reactiveVaultOperations; + + ReactiveVaultTransitOperations reactiveTransitOperations; + + Version vaultVersion; + + @BeforeEach + void before() { + this.reactiveTransitOperations = this.reactiveVaultOperations.opsForTransit(); + + if (!this.vaultOperations.opsForSys().getMounts().containsKey("transit/")) { + this.vaultOperations.opsForSys().mount("transit", VaultMount.create("transit")); + } + + this.vaultVersion = prepare().getVersion(); + + removeKeys(); + } + + @AfterEach + void tearDown() { + removeKeys(); + } + + private Mono<Void> deleteKey(String keyName) { + return this.reactiveTransitOperations + .configureKey(keyName, VaultTransitKeyConfiguration.builder().deletionAllowed(true).build()) + .and(this.reactiveTransitOperations.deleteKey(keyName)).onErrorResume(e -> Mono.empty()); + } + + private void removeKeys() { + reactiveTransitOperations.getKeys().flatMap(this::deleteKey).blockLast(); + } + + @Test + void createKeyShouldCreateKey() { + this.reactiveTransitOperations.createKey("myKey").then(this.reactiveTransitOperations.getKey("myKey")) + .as(StepVerifier::create).assertNext(myKey -> { + assertThat(myKey).isNotNull(); + assertThat(myKey.getType()).startsWith("aes"); + assertThat(myKey.getName()).isEqualTo("myKey"); + assertThat(myKey.isDeletionAllowed()).isFalse(); + assertThat(myKey.isDerived()).isFalse(); + assertThat(myKey.getMinDecryptionVersion()).isEqualTo(1); + assertThat(myKey.getLatestVersion()).isEqualTo(1); + + if (this.vaultVersion.isGreaterThanOrEqualTo(Version.parse("0.7.0"))) { + assertThat(myKey.supportsDecryption()).isTrue(); + assertThat(myKey.supportsEncryption()).isTrue(); + assertThat(myKey.supportsDerivation()).isTrue(); + assertThat(myKey.supportsSigning()).isFalse(); + } + }).verifyComplete(); + } + + @Test + @RequiresVaultVersion("0.6.4") + void createKeyShouldCreateEcDsaKey() { + createEcdsaP256Key().flatMap(keyName -> this.reactiveTransitOperations.getKey(keyName)).as(StepVerifier::create) + .assertNext(myKey -> { + assertThat(myKey).isNotNull(); + assertThat(myKey.getType()).startsWith("ecdsa"); + assertThat(myKey.getKeys()).isNotEmpty(); + }).verifyComplete(); + } + + @Test + @RequiresVaultVersion(ED25519_INTRODUCED_IN_VERSION) + void createKeyShouldCreateEdKey() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.ofKeyType("ed25519"); + + this.reactiveTransitOperations.createKey("ed-key", request) + .then(this.reactiveTransitOperations.getKey("ed-key")).as(StepVerifier::create).assertNext(myKey -> { + assertThat(myKey).isNotNull(); + assertThat(myKey.getType()).startsWith("ed"); + assertThat(myKey.getKeys()).isNotEmpty(); + }).verifyComplete(); + } + + @Test + @RequiresVaultVersion(ECDSA521_INTRODUCED_IN_VERSION) + void createKeyShouldCreateEcdsaKey() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.ofKeyType("ecdsa-p521"); + + this.reactiveTransitOperations.createKey("ecdsa-p521-key", request) + .then(this.reactiveTransitOperations.getKey("ecdsa-p521-key")).as(StepVerifier::create) + .assertNext(myKey -> { + assertThat(myKey.getType()).isEqualTo("ecdsa-p521"); + assertThat(myKey.getKeys()).isNotEmpty(); + }).verifyComplete(); + } + + @Test + @RequiresVaultVersion(RSA3072_INTRODUCED_IN_VERSION) + void createKeyShouldCreateRsa3072Key() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.ofKeyType("rsa-3072"); + + this.reactiveTransitOperations.createKey("rsa-3072-key", request) + .then(this.reactiveTransitOperations.getKey("rsa-3072-key")).as(StepVerifier::create) + .assertNext(myKey -> { + assertThat(myKey.getType()).isEqualTo("rsa-3072"); + assertThat(myKey.getKeys()).isNotEmpty(); + }).verifyComplete(); + } + + @Test + @RequiresVaultVersion(AES256_GCM96_INTRODUCED_IN_VERSION) + void createKeyShouldCreateAes256Gcm96Key() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.ofKeyType("aes256-gcm96"); + + this.reactiveTransitOperations.createKey("aes256-gcm96-key", request) + .then(this.reactiveTransitOperations.getKey("aes256-gcm96-key")).as(StepVerifier::create) + .assertNext(myKey -> { + assertThat(myKey.getType()).isEqualTo("aes256-gcm96"); + assertThat(myKey.getKeys()).isNotEmpty(); + }).verifyComplete(); + } + + @Test + void createKeyShouldCreateKeyWithOptions() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().convergentEncryption(true) + .derived(true).build(); + + this.reactiveTransitOperations.createKey("myKey", request).then(this.reactiveTransitOperations.getKey("myKey")) + .as(StepVerifier::create).assertNext(myKey -> { + assertThat(myKey.getName()).isEqualTo("myKey"); + assertThat(myKey.isDeletionAllowed()).isFalse(); + assertThat(myKey.isDerived()).isTrue(); + assertThat(myKey.getMinDecryptionVersion()).isEqualTo(1); + assertThat(myKey.getLatestVersion()).isEqualTo(1); + }).verifyComplete(); + } + + @Test + void shouldConfigureKey() { + VaultTransitKeyConfiguration configuration = VaultTransitKeyConfiguration.builder().deletionAllowed(true) + .minDecryptionVersion(1).minEncryptionVersion(2).build(); + + this.reactiveTransitOperations.createKey("myKey").then(this.reactiveTransitOperations.rotate("myKey")) + .then(this.reactiveTransitOperations.rotate("myKey")) + .then(this.reactiveTransitOperations.configureKey("myKey", configuration)) + .then(this.reactiveTransitOperations.getKey("myKey")).as(StepVerifier::create).assertNext(myKey -> { + assertThat(myKey.getMinDecryptionVersion()).isEqualTo(1); + if (this.vaultVersion.isGreaterThanOrEqualTo(Version.parse("0.8.0"))) { + assertThat(myKey.getMinEncryptionVersion()).isEqualTo(2); + } + else { + assertThat(myKey.getMinEncryptionVersion()).isEqualTo(0); + } + }).verifyComplete(); + } + + @Test + @RequiresVaultVersion("0.6.4") + void shouldEnumerateKey() { + this.reactiveTransitOperations.getKeys().as(StepVerifier::create).verifyComplete(); + this.reactiveTransitOperations.createKey("myKey").thenMany(this.reactiveTransitOperations.getKeys()) + .as(StepVerifier::create).assertNext(keys -> assertThat(keys).contains("myKey")).verifyComplete(); + } + + @Test + void getKeyShouldReturnEmptyIfKeyNotExists() { + this.reactiveTransitOperations.getKey("myKey").as(StepVerifier::create).verifyComplete(); + } + + @Test + void deleteKeyShouldFailIfKeyNotExists() { + this.reactiveTransitOperations.deleteKey("myKey").as(StepVerifier::create) + .consumeErrorWith(e -> assertThat(e).hasMessageContaining("Status 400")).verify(); + } + + @Test + void deleteKeyShouldDeleteKey() { + VaultTransitKeyConfiguration configuration = VaultTransitKeyConfiguration.builder().deletionAllowed(true) + .build(); + + this.reactiveTransitOperations.createKey("myKey") + .then(this.reactiveTransitOperations.configureKey("myKey", configuration)) + .then(this.reactiveTransitOperations.deleteKey("myKey")) + .then(this.reactiveTransitOperations.getKey("myKey")).as(StepVerifier::create).verifyComplete(); + } + + @Test + void encryptShouldCreateCiphertext() { + this.reactiveTransitOperations.createKey("myKey") + .then(this.reactiveTransitOperations.encrypt("myKey", "hello-world")).as(StepVerifier::create) + .assertNext(ciphertext -> assertThat(ciphertext).startsWith("vault:v")).verifyComplete(); + } + + @Test + void encryptShouldCreateCiphertextWithNonceAndContext() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().convergentEncryption(true) + .derived(true).build(); + + VaultTransitContext context = VaultTransitContext.builder().context("blubb".getBytes()) + .nonce("123456789012".getBytes()).build(); + + this.reactiveTransitOperations.createKey("mykey", request) + .then(this.reactiveTransitOperations.encrypt("myKey", "hello-world".getBytes(), context)) + .as(StepVerifier::create).assertNext(ciphertext -> assertThat(ciphertext).startsWith("vault:v1:")) + .verifyComplete(); + } + + @Test + @RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION) + void encryptShouldEncryptEmptyValues() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().convergentEncryption(true) + .derived(true).build(); + + VaultTransitContext context = VaultTransitContext.builder().context("blubb".getBytes()) + .nonce("123456789012".getBytes()).build(); + + this.reactiveTransitOperations.createKey("myKey", request) + .then(this.reactiveTransitOperations.encrypt("myKey", Plaintext.of("").with(context))) + .as(StepVerifier::create).assertNext(ciphertext -> { + assertThat(ciphertext.getCiphertext()).startsWith("vault:v1:"); + assertThat(ciphertext.getContext()).isEqualTo(context); + }).verifyComplete(); + } + + @Test + void encryptShouldCreateWrappedCiphertextWithNonceAndContext() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().convergentEncryption(true) + .derived(true).build(); + + VaultTransitContext context = VaultTransitContext.builder().context("blubb".getBytes()) + .nonce("123456789012".getBytes()).build(); + + this.reactiveTransitOperations.createKey("myKey", request) + .then(this.reactiveTransitOperations.encrypt("myKey", Plaintext.of("hello-world").with(context))) + .as(StepVerifier::create).assertNext(ciphertext -> { + assertThat(ciphertext.getCiphertext()).startsWith("vault:v1:"); + assertThat(ciphertext.getContext()).isEqualTo(context); + }).verifyComplete(); + } + + @Test + void decryptShouldCreatePlaintext() { + this.reactiveTransitOperations.createKey("myKey") + .then(this.reactiveTransitOperations.encrypt("myKey", "hello-world")) + .flatMap(ciphertext -> this.reactiveTransitOperations.decrypt("myKey", ciphertext)) + .as(StepVerifier::create).assertNext(plaintext -> assertThat(plaintext).isEqualTo("hello-world")) + .verifyComplete(); + } + + @Test + void decryptShouldCreatePlaintextWithNonceAndContext() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().convergentEncryption(true) + .derived(true).build(); + + VaultTransitContext transitRequest = VaultTransitContext.builder().context("blubb".getBytes()) + .nonce("123456789012".getBytes()).build(); + + this.reactiveTransitOperations.createKey("myKey", request) + .then(this.reactiveTransitOperations.encrypt("myKey", "hello-world".getBytes(), transitRequest)) + .flatMap(ciphertext -> this.reactiveTransitOperations.decrypt("myKey", ciphertext, transitRequest)) + .as(StepVerifier::create) + .assertNext(plaintext -> assertThat(new String(plaintext)).isEqualTo("hello-world")).verifyComplete(); + } + + @Test + void decryptShouldCreateWrappedPlaintextWithNonceAndContext() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().convergentEncryption(true) + .derived(true).build(); + + VaultTransitContext context = VaultTransitContext.builder().context("blubb".getBytes()) + .nonce("123456789012".getBytes()).build(); + + this.reactiveTransitOperations.createKey("myKey", request) + .then(this.reactiveTransitOperations.encrypt("myKey", Plaintext.of("hello-world").with(context))) + .flatMap(ciphertext -> this.reactiveTransitOperations.decrypt("myKey", ciphertext)) + .as(StepVerifier::create).assertNext(plaintext -> { + assertThat(plaintext.asString()).isEqualTo("hello-world"); + assertThat(plaintext.getContext()).isEqualTo(context); + }).verifyComplete(); + } + + @Test + void encryptAndRewrapShouldCreateCiphertext() { + String ciphertext = this.reactiveTransitOperations.createKey("myKey") + .then(this.reactiveTransitOperations.encrypt("myKey", "hello-world")).block(); + + assertThat(ciphertext).isNotNull(); + + this.reactiveTransitOperations.rotate("myKey").then(this.reactiveTransitOperations.rewrap("myKey", ciphertext)) + .as(StepVerifier::create).assertNext(rewrapped -> assertThat(rewrapped).startsWith("vault:v2:")) + .verifyComplete(); + } + + @Test + void shouldEncryptBinaryPlaintext() { + this.reactiveTransitOperations.createKey("myKey"); + byte[] plaintext = new byte[] { 1, 2, 3, 4, 5 }; + + this.reactiveTransitOperations.encrypt("myKey", plaintext, VaultTransitContext.empty()) + .flatMap(ciphertext -> this.reactiveTransitOperations.decrypt("myKey", ciphertext, + VaultTransitContext.empty())) + .as(StepVerifier::create).assertNext(decrypted -> assertThat(decrypted).isEqualTo(plaintext)) + .verifyComplete(); + } + + @Test + void encryptAndRewrapShouldCreateCiphertextWithNonceAndContext() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().convergentEncryption(true) + .derived(true).build(); + + VaultTransitContext transitRequest = VaultTransitContext.builder().context("blubb".getBytes()) + .nonce("123456789012".getBytes()).build(); + + String ciphertext = this.reactiveTransitOperations.createKey("myKey", request) + .then(this.reactiveTransitOperations.encrypt("myKey", "hello-world".getBytes(), transitRequest)) + .block(); + + assertThat(ciphertext).isNotNull(); + + this.reactiveTransitOperations.rotate("myKey") + .then(this.reactiveTransitOperations.rewrap("myKey", ciphertext, transitRequest)) + .as(StepVerifier::create).assertNext(rewrapped -> assertThat(rewrapped).startsWith("vault:v2")) + .verifyComplete(); + } + + @Test + @RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION) + void shouldBatchEncrypt() { + this.reactiveTransitOperations.createKey("myKey") + .thenMany(this.reactiveTransitOperations.encrypt("myKey", + Arrays.asList(Plaintext.of("one"), Plaintext.of("two")))) + .as(StepVerifier::create).assertNext(encrypted -> { + assertThat(encrypted.get()).isNotNull(); + assertThat(Objects.requireNonNull(encrypted.get()).getCiphertext()).startsWith("vault:"); + }).assertNext(encrypted -> { + assertThat(encrypted.get()).isNotNull(); + assertThat(Objects.requireNonNull(encrypted.get()).getCiphertext()).startsWith("vault:"); + }).verifyComplete(); + } + + @Test + @RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION) + void shouldBatchDecrypt() { + this.reactiveTransitOperations.createKey("myKey").block(); + Ciphertext one = this.reactiveTransitOperations.encrypt("myKey", Plaintext.of("one")).block(); + Ciphertext two = this.reactiveTransitOperations.encrypt("myKey", Plaintext.of("two")).block(); + + assertThat(one).isNotNull(); + assertThat(two).isNotNull(); + + this.reactiveTransitOperations.decrypt("myKey", Arrays.asList(one, two)) + .zipWith(Flux.merge(this.reactiveTransitOperations.decrypt("myKey", one), + this.reactiveTransitOperations.decrypt("myKey", two))) + .as(StepVerifier::create).assertNext(it -> { + assertThat(it.getT1().getAsString()).isEqualTo(it.getT2().asString()); + assertThat(it.getT1().getAsString()).isEqualTo("one"); + }).assertNext(it -> { + assertThat(it.getT1().getAsString()).isEqualTo(it.getT2().asString()); + assertThat(it.getT1().getAsString()).isEqualTo("two"); + }).verifyComplete(); + } + + @Test + @RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION) + void shouldBatchEncryptWithContext() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().derived(true).build(); + + VaultTransitContext context1 = VaultTransitContext.builder().context("oneContext".getBytes()).build(); + + VaultTransitContext context2 = VaultTransitContext.builder().context("twoContext".getBytes()).build(); + + this.reactiveTransitOperations.createKey("myKey", request) + .thenMany(this.reactiveTransitOperations.encrypt("myKey", + Arrays.asList(Plaintext.of("one").with(context1), Plaintext.of("two").with(context2)))) + .as(StepVerifier::create) + .assertNext(it -> assertThat(Objects.requireNonNull(it.get()).getContext()).isEqualTo(context1)) + .assertNext(it -> assertThat(Objects.requireNonNull(it.get()).getContext()).isEqualTo(context2)) + .verifyComplete(); + } + + @Test + @RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION) + void shouldBatchDecryptWithContext() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().derived(true).build(); + + Plaintext one = Plaintext.of("one") + .with(VaultTransitContext.builder().context("oneContext".getBytes()).build()); + + Plaintext two = Plaintext.of("two") + .with(VaultTransitContext.builder().context("twoContext".getBytes()).build()); + + this.reactiveTransitOperations.createKey("myKey", request) + .thenMany(this.reactiveTransitOperations.encrypt("myKey", Arrays.asList(one, two))) + .flatMap(it -> Mono.justOrEmpty(it.get())).collectList() + .flatMapMany(it -> this.reactiveTransitOperations.decrypt("myKey", it)).as(StepVerifier::create) + .assertNext(it -> assertThat(it.get()).isEqualTo(one)) + .assertNext(it -> assertThat(it.get()).isEqualTo(two)).verifyComplete(); + } + + @Test + @RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION) + void shouldBatchDecryptWithWrongContext() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().derived(true).build(); + + Plaintext one = Plaintext.of("one") + .with(VaultTransitContext.builder().context("oneContext".getBytes()).build()); + + Plaintext two = Plaintext.of("two") + .with(VaultTransitContext.builder().context("twoContext".getBytes()).build()); + + List<Ciphertext> encrypted = this.reactiveTransitOperations.createKey("myKey", request) + .thenMany(this.reactiveTransitOperations.encrypt("myKey", Arrays.asList(one, two))) + .flatMap(it -> Mono.justOrEmpty(it.get())).collectList().block(); + + assertThat(encrypted).isNotNull(); + + Ciphertext encryptedOne = encrypted.get(0); + Ciphertext decryptedTwo = encrypted.get(1); + + Ciphertext tampered = decryptedTwo.with(encryptedOne.getContext()); + + StepVerifier.FirstStep<VaultDecryptionResult> stepVerifier = this.reactiveTransitOperations + .decrypt("myKey", Arrays.asList(encryptedOne, tampered)).as(StepVerifier::create); + + if (this.vaultVersion.isGreaterThanOrEqualTo(Version.parse("1.6.0"))) { + stepVerifier.consumeErrorWith(e -> assertThat(e).hasMessageContaining("error")).verify(); + } + else { + stepVerifier.assertNext(it -> assertThat(it.get()).isEqualTo(one)).assertNext(it -> { + assertThat(it.isSuccessful()).isEqualTo(false); + assertThat(it.getCause()).isInstanceOf(VaultException.class); + }).verifyComplete(); + } + } + + @Test + @RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION) + void shouldBatchDecryptEmptyPlaintext() { + this.reactiveTransitOperations.createKey("myKey") + .then(this.reactiveTransitOperations.encrypt("myKey", Plaintext.empty())) + .flatMapMany(empty -> this.reactiveTransitOperations.decrypt("myKey", Collections.singletonList(empty))) + .as(StepVerifier::create).assertNext(it -> assertThat(it.getAsString()).isEmpty()).verifyComplete(); + } + + @Test + @RequiresVaultVersion(BATCH_INTRODUCED_IN_VERSION) + void shouldBatchDecryptEmptyPlaintextWithContext() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().derived(true).build(); + + Plaintext empty = Plaintext.empty() + .with(VaultTransitContext.builder().context("oneContext".getBytes()).build()); + + this.reactiveTransitOperations.createKey("myKey", request) + .thenMany(this.reactiveTransitOperations.encrypt("myKey", Collections.singletonList(empty))) + .flatMap(it -> Mono.justOrEmpty(it.get())).collectList() + .flatMapMany(it -> this.reactiveTransitOperations.decrypt("myKey", it)).as(StepVerifier::create) + .assertNext(it -> assertThat(it.get()).isEqualTo(empty)).verifyComplete(); + } + + @Test + @RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION) + void generateHmacShouldCreateHmac() { + createEcdsaP256Key() + .flatMap(keyName -> this.reactiveTransitOperations.getHmac(keyName, Plaintext.of("hello-world"))) + .as(StepVerifier::create).assertNext(hmac -> assertThat(hmac.getHmac()).isNotEmpty()).verifyComplete(); + } + + @Test + @RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION) + void generateHmacShouldCreateHmacForRotatedKey() { + VaultHmacRequest request = VaultHmacRequest.builder().plaintext(Plaintext.of("hello-world")).keyVersion(2) + .build(); + + createEcdsaP256Key() + .flatMap(keyName -> this.reactiveTransitOperations.rotate(keyName) + .then(this.reactiveTransitOperations.getHmac(keyName, request))) + .as(StepVerifier::create).assertNext(hmac -> assertThat(hmac.getHmac()).isNotEmpty()).verifyComplete(); + } + + @Test + @RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION) + void generateHmacWithCustomAlgorithmShouldCreateHmac() { + VaultHmacRequest request = VaultHmacRequest.builder().plaintext(Plaintext.of("hello-world")) + .algorithm("sha2-512").build(); + + createEcdsaP256Key().flatMap(keyName -> this.reactiveTransitOperations.getHmac(keyName, request)) + .as(StepVerifier::create).assertNext(hmac -> assertThat(hmac.getHmac()).isNotEmpty()).verifyComplete(); + } + + @Test + void generateHmacWithInvalidAlgorithmShouldFail() { + VaultHmacRequest request = VaultHmacRequest.builder().plaintext(Plaintext.of("hello-world")) + .algorithm("blah-512").build(); + + createEcdsaP256Key().flatMap(keyName -> this.reactiveTransitOperations.getHmac("myKey", request)) + .as(StepVerifier::create).consumeErrorWith(e -> assertThat(e).isInstanceOf(VaultException.class)) + .verify(); + } + + @Test + @RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION) + void signShouldCreateSignature() { + createEcdsaP256Key() + .flatMap(keyName -> this.reactiveTransitOperations.sign(keyName, Plaintext.of("hello-world"))) + .as(StepVerifier::create).assertNext(signature -> assertThat(signature.getSignature()).isNotEmpty()) + .verifyComplete(); + } + + @Test + @RequiresVaultVersion(ED25519_INTRODUCED_IN_VERSION) + void signShouldCreateSignatureUsingEd25519() { + VaultTransitKeyCreationRequest keyCreationRequest = VaultTransitKeyCreationRequest.ofKeyType("ed25519"); + + this.reactiveTransitOperations.createKey("ed-key", keyCreationRequest) + .then(this.reactiveTransitOperations.sign("ed-key", Plaintext.of("hello-world"))) + .as(StepVerifier::create).assertNext(signature -> assertThat(signature.getSignature()).isNotEmpty()) + .verifyComplete(); + } + + @Test + void signWithInvalidKeyFormatShouldFail() { + this.reactiveTransitOperations.createKey("myKey") + .then(this.reactiveTransitOperations.sign("myKey", Plaintext.of("hello-world"))) + .as(StepVerifier::create).consumeErrorWith(e -> assertThat(e).isInstanceOf(VaultException.class)) + .verify(); + } + + @Test + @RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION) + void signWithCustomAlgorithmShouldCreateSignature() { + VaultSignRequest request = VaultSignRequest.builder().plaintext(Plaintext.of("hello-world")) + .signatureAlgorithm("sha2-512").build(); + + createEcdsaP256Key().flatMap(keyName -> this.reactiveTransitOperations.sign(keyName, request)) + .as(StepVerifier::create).assertNext(signature -> assertThat(signature.getSignature()).isNotEmpty()) + .verifyComplete(); + } + + @Test + @RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION) + void shouldVerifyValidSignature() { + Plaintext plaintext = Plaintext.of("hello-world"); + + createEcdsaP256Key() + .flatMap(keyName -> this.reactiveTransitOperations.sign(keyName, plaintext) + .flatMap(signature -> this.reactiveTransitOperations.verify(keyName, plaintext, signature))) + .as(StepVerifier::create).assertNext(valid -> assertThat(valid).isTrue()).verifyComplete(); + } + + @Test + @RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION) + void shouldVerifyValidHmac() { + Plaintext plaintext = Plaintext.of("hello-world"); + + createEcdsaP256Key() + .flatMap(keyName -> this.reactiveTransitOperations.getHmac(keyName, plaintext) + .flatMap(hmac -> this.reactiveTransitOperations.verify(keyName, + VaultSignatureVerificationRequest.create(plaintext, hmac)))) + .as(StepVerifier::create).assertNext(valid -> assertThat(valid).isEqualTo(SignatureValidation.valid())) + .verifyComplete(); + } + + @Test + @RequiresVaultVersion(SIGN_VERIFY_INTRODUCED_IN_VERSION) + void shouldVerifyValidSignatureWithCustomAlgorithm() { + Plaintext plaintext = Plaintext.of("hello-world"); + + VaultSignRequest request = VaultSignRequest.builder().plaintext(plaintext).signatureAlgorithm("sha2-512") + .build(); + + createEcdsaP256Key() + .flatMap((keyName) -> this.reactiveTransitOperations.sign(keyName, request) + .map(signature -> VaultSignatureVerificationRequest.builder().signatureAlgorithm("sha2-512") + .plaintext(plaintext).signature(signature).build()) + .flatMap(verificationRequest -> this.reactiveTransitOperations.verify(keyName, + verificationRequest))) + .as(StepVerifier::create).assertNext(valid -> assertThat(valid).isEqualTo(SignatureValidation.valid())) + .verifyComplete(); + } + + @Test + @RequiresVaultVersion(KEY_EXPORT_INTRODUCED_IN_VERSION) + void shouldCreateNewExportableKey() { + VaultTransitKeyCreationRequest vaultTransitKeyCreationRequest = VaultTransitKeyCreationRequest.builder() + .exportable(true).derived(true).build(); + + reactiveTransitOperations.createKey("myKey", vaultTransitKeyCreationRequest) + .then(reactiveTransitOperations.getKey("myKey")).as(StepVerifier::create) + .assertNext(vaultTransitKey -> { + assertThat(vaultTransitKey.getName()).isEqualTo("myKey"); + assertThat(vaultTransitKey.isExportable()).isTrue(); + }).verifyComplete(); + } + + @Test + @RequiresVaultVersion(KEY_EXPORT_INTRODUCED_IN_VERSION) + void shouldCreateNotExportableKeyByDefault() { + reactiveTransitOperations.createKey("myKey").then(reactiveTransitOperations.getKey("myKey")) + .as(StepVerifier::create).assertNext(vaultTransitKey -> { + assertThat(vaultTransitKey.getName()).isEqualTo("myKey"); + assertThat(vaultTransitKey.isExportable()).isFalse(); + }).verifyComplete(); + } + + @Test + @RequiresVaultVersion(KEY_EXPORT_INTRODUCED_IN_VERSION) + void shouldExportEncryptionKey() { + VaultTransitKeyCreationRequest vaultTransitKeyCreationRequest = VaultTransitKeyCreationRequest.builder() + .exportable(true).build(); + + reactiveTransitOperations.createKey("myKey", vaultTransitKeyCreationRequest) + .then(reactiveTransitOperations.exportKey("myKey", TransitKeyType.ENCRYPTION_KEY)) + .as(StepVerifier::create).assertNext(rawTransitKey -> { + assertThat(rawTransitKey.getName()).isEqualTo("myKey"); + assertThat(rawTransitKey.getKeys()).isNotEmpty(); + assertThat(rawTransitKey.getKeys().get("1")).isNotBlank(); + }).verifyComplete(); + } + + @Test + @RequiresVaultVersion(KEY_EXPORT_INTRODUCED_IN_VERSION) + void shouldNotAllowExportSigningKey() { + + VaultTransitKeyCreationRequest vaultTransitKeyCreationRequest = VaultTransitKeyCreationRequest.builder() + .exportable(true).build(); + + reactiveTransitOperations.createKey("myKey", vaultTransitKeyCreationRequest) + .then(reactiveTransitOperations.exportKey("myKey", TransitKeyType.SIGNING_KEY)).as(StepVerifier::create) + .consumeErrorWith(e -> assertThat(e).isInstanceOf(VaultException.class)).verify(); + } + + @Test + @RequiresVaultVersion(KEY_EXPORT_INTRODUCED_IN_VERSION) + void shouldExportEcDsaKey() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().type("ecdsa-p256") + .exportable(true).build(); + + this.reactiveTransitOperations.createKey("myKey", request) + .thenMany(Flux.merge(this.reactiveTransitOperations.exportKey("myKey", TransitKeyType.HMAC_KEY), + this.reactiveTransitOperations.exportKey("myKey", TransitKeyType.SIGNING_KEY))) + .as(StepVerifier::create).assertNext(hmacKey -> assertThat(hmacKey.getKeys()).isNotEmpty()) + .assertNext(signingKey -> assertThat(signingKey.getKeys()).isNotEmpty()).verifyComplete(); + } + + @Test + @RequiresVaultVersion(ED25519_INTRODUCED_IN_VERSION) + void shouldExportEdKey() { + VaultTransitKeyCreationRequest request = VaultTransitKeyCreationRequest.builder().type("ed25519") + .exportable(true).build(); + + this.reactiveTransitOperations.createKey("myKey", request) + .thenMany(Flux.merge(this.reactiveTransitOperations.exportKey("myKey", TransitKeyType.HMAC_KEY), + this.reactiveTransitOperations.exportKey("myKey", TransitKeyType.SIGNING_KEY))) + .as(StepVerifier::create).assertNext(hmacKey -> assertThat(hmacKey.getKeys()).isNotEmpty()) + .assertNext(signingKey -> assertThat(signingKey.getKeys()).isNotEmpty()).verifyComplete(); + } + + private Mono<String> createEcdsaP256Key() { + String keyName = "ecdsa-key"; + VaultTransitKeyCreationRequest keyCreationRequest = VaultTransitKeyCreationRequest.ofKeyType("ecdsa-p256"); + return this.reactiveTransitOperations.createKey(keyName, keyCreationRequest).thenReturn(keyName); + } + +}