mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-03 20:40:45 -05:00
* Vault 42177 Add Backend Field (#12092) * add a new struct for the total number of successful requests for transit and transform * implement tracking for encrypt path * implement tracking in encrypt path * add tracking in rewrap * add tracking to datakey path * add tracking to hmac path * add tracking to sign path * add tracking to verify path * unit tests for verify path * add tracking to cmac path * reset the global counter in each unit test * add tracking to hmac verify * add methods to retrieve and flush transit count * modify the methods that store and update data protection call counts * update the methods * add a helper method to combine replicated and local data call counts * add tracking to the endpoint * fix some formatting errors * add unit tests to path encrypt for tracking * add unit tests to decrypt path * fix linter error * add unit tests to test update and store methods for data protection calls * stub fix: do not create separate files * fix the tracking by coordinating replicated and local data, add unit tests * update all reference to the new data struct * revert to previous design with just one global counter for all calls for each cluster * complete external test * no need to check if current count is greater than 0, remove it * feedback: remove unnacassary comments about atomic addition, standardize comments * leave jira id on todo comment, remove unused method * rename mathods by removing HWM and max in names, update jira id in todo comment, update response field key name * feedback: remove explicit counter in cmac tests, instead put in the expected number * feedback: remove explicit tracking in the rest of the tests * feedback: separate transit testing into its own external test * Update vault/consumption_billing_util_test.go Co-authored-by: divyaac <divya.chandrasekaran@hashicorp.com> * update comment after test name change * fix comments * fix comments in test * another comment fix * feedback: remove incorrect comment * fix a CE test * fix the update method: instead of storing max, increment by the current count value * update the unit test, remove local prefix as argument to the methods since we store only to non-replicated paths * update the external test * Adds a field to backend to track billing data removed file * Changed implementation to use a map instead * Some more comments * Add more implementation * Edited grpc server backend * Refactored a bit * Fix one more test * Modified map: * Revert "Modified map:" This reverts commit 1730fe1f358b210e6abae43fbdca09e585aaaaa8. * Removed some other things * Edited consumption billing files a bit * Testing function * Fix transit stuff and make sure tests pass * Changes * More changes * More changes * Edited external test * Edited some more tests * Edited and fixed tests * One more fix * Fix some more tests * Moved some testing structures around and added error checking * Fixed some nits * Update builtin/logical/transit/path_sign_verify.go Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> * Edited some errors * Fixed error logs * Edited one more thing * Decorate the error * Update vault/consumption_billing.go Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> --------- Co-authored-by: Amir Aslamov <amir.aslamov@hashicorp.com> Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> * Edited stub function --------- Co-authored-by: divyaac <divya.chandrasekaran@hashicorp.com> Co-authored-by: Amir Aslamov <amir.aslamov@hashicorp.com> Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> Co-authored-by: divyaac <divyaac@berkeley.edu>
1106 lines
32 KiB
Go
1106 lines
32 KiB
Go
// Copyright IBM Corp. 2016, 2025
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package transit
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
uuid "github.com/hashicorp/go-uuid"
|
|
"github.com/hashicorp/vault/sdk/helper/keysutil"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
"github.com/mitchellh/mapstructure"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestTransit_MissingPlaintext(t *testing.T) {
|
|
var resp *logical.Response
|
|
var err error
|
|
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
// Create the policy
|
|
policyReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "keys/existing_key",
|
|
Storage: s,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), policyReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
encReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "encrypt/existing_key",
|
|
Storage: s,
|
|
Data: map[string]interface{}{},
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), encReq)
|
|
if resp == nil || !resp.IsError() {
|
|
t.Fatalf("expected error due to missing plaintext in request, err:%v resp:%#v", err, resp)
|
|
}
|
|
// We expect 0 successful calls
|
|
require.Equal(t, uint64(0), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
func TestTransit_MissingPlaintextInBatchInput(t *testing.T) {
|
|
var resp *logical.Response
|
|
var err error
|
|
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
// Create the policy
|
|
policyReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "keys/existing_key",
|
|
Storage: s,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), policyReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
batchInput := []interface{}{
|
|
map[string]interface{}{}, // Note that there is no map entry for plaintext
|
|
}
|
|
|
|
batchData := map[string]interface{}{
|
|
"batch_input": batchInput,
|
|
}
|
|
batchReq := &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "encrypt/upserted_key",
|
|
Storage: s,
|
|
Data: batchData,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), batchReq)
|
|
if err == nil {
|
|
t.Fatalf("expected error due to missing plaintext in request, err:%v resp:%#v", err, resp)
|
|
}
|
|
// We expect 0 successful calls
|
|
require.Equal(t, uint64(0), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Case1: Ensure that batch encryption did not affect the normal flow of
|
|
// encrypting the plaintext with a pre-existing key.
|
|
func TestTransit_BatchEncryptionCase1(t *testing.T) {
|
|
var resp *logical.Response
|
|
var err error
|
|
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
// Create the policy
|
|
policyReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "keys/existing_key",
|
|
Storage: s,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), policyReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA==" // "the quick brown fox"
|
|
|
|
encData := map[string]interface{}{
|
|
"plaintext": plaintext,
|
|
}
|
|
|
|
encReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "encrypt/existing_key",
|
|
Storage: s,
|
|
Data: encData,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), encReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
keyVersion := resp.Data["key_version"].(int)
|
|
if keyVersion != 1 {
|
|
t.Fatalf("unexpected key version; got: %d, expected: %d", keyVersion, 1)
|
|
}
|
|
|
|
ciphertext := resp.Data["ciphertext"]
|
|
|
|
decData := map[string]interface{}{
|
|
"ciphertext": ciphertext,
|
|
}
|
|
decReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "decrypt/existing_key",
|
|
Storage: s,
|
|
Data: decData,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), decReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
if resp.Data["plaintext"] != plaintext {
|
|
t.Fatalf("bad: plaintext. Expected: %q, Actual: %q", plaintext, resp.Data["plaintext"])
|
|
}
|
|
|
|
// We expect 2 successful requests (1 for encrypt, 1 for decrypt)
|
|
require.Equal(t, uint64(2), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Case2: Ensure that batch encryption did not affect the normal flow of
|
|
// encrypting the plaintext with the key upserted.
|
|
func TestTransit_BatchEncryptionCase2(t *testing.T) {
|
|
var resp *logical.Response
|
|
var err error
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
// Upsert the key and encrypt the data
|
|
plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA=="
|
|
|
|
encData := map[string]interface{}{
|
|
"plaintext": plaintext,
|
|
}
|
|
|
|
encReq := &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "encrypt/upserted_key",
|
|
Storage: s,
|
|
Data: encData,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), encReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
keyVersion := resp.Data["key_version"].(int)
|
|
if keyVersion != 1 {
|
|
t.Fatalf("unexpected key version; got: %d, expected: %d", keyVersion, 1)
|
|
}
|
|
|
|
ciphertext := resp.Data["ciphertext"]
|
|
decData := map[string]interface{}{
|
|
"ciphertext": ciphertext,
|
|
}
|
|
|
|
policyReq := &logical.Request{
|
|
Operation: logical.ReadOperation,
|
|
Path: "keys/upserted_key",
|
|
Storage: s,
|
|
}
|
|
|
|
resp, err = b.HandleRequest(context.Background(), policyReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
decReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "decrypt/upserted_key",
|
|
Storage: s,
|
|
Data: decData,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), decReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
if resp.Data["plaintext"] != plaintext {
|
|
t.Fatalf("bad: plaintext. Expected: %q, Actual: %q", plaintext, resp.Data["plaintext"])
|
|
}
|
|
|
|
// We expect 2 successful requests (1 for encrypt, 1 for decrypt)
|
|
require.Equal(t, uint64(2), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Case3: If batch encryption input is not base64 encoded, it should fail.
|
|
func TestTransit_BatchEncryptionCase3(t *testing.T) {
|
|
var err error
|
|
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
batchInput := `[{"plaintext":"dGhlIHF1aWNrIGJyb3duIGZveA=="}]`
|
|
batchData := map[string]interface{}{
|
|
"batch_input": batchInput,
|
|
}
|
|
|
|
batchReq := &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "encrypt/upserted_key",
|
|
Storage: s,
|
|
Data: batchData,
|
|
}
|
|
_, err = b.HandleRequest(context.Background(), batchReq)
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
|
|
// We expect 0 successful requests
|
|
require.Equal(t, uint64(0), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Case4: Test batch encryption with an existing key (and test references)
|
|
func TestTransit_BatchEncryptionCase4(t *testing.T) {
|
|
var resp *logical.Response
|
|
var err error
|
|
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
policyReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "keys/existing_key",
|
|
Storage: s,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), policyReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
batchInput := []interface{}{
|
|
map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "reference": "b"},
|
|
map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "reference": "a"},
|
|
}
|
|
|
|
batchData := map[string]interface{}{
|
|
"batch_input": batchInput,
|
|
}
|
|
batchReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "encrypt/existing_key",
|
|
Storage: s,
|
|
Data: batchData,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), batchReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
batchResponseItems := resp.Data["batch_results"].([]EncryptBatchResponseItem)
|
|
|
|
decReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "decrypt/existing_key",
|
|
Storage: s,
|
|
}
|
|
|
|
plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA=="
|
|
|
|
for i, item := range batchResponseItems {
|
|
if item.KeyVersion != 1 {
|
|
t.Fatalf("unexpected key version; got: %d, expected: %d", item.KeyVersion, 1)
|
|
}
|
|
|
|
decReq.Data = map[string]interface{}{
|
|
"ciphertext": item.Ciphertext,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), decReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
if resp.Data["plaintext"] != plaintext {
|
|
t.Fatalf("bad: plaintext. Expected: %q, Actual: %q", plaintext, resp.Data["plaintext"])
|
|
}
|
|
inputItem := batchInput[i].(map[string]interface{})
|
|
if item.Reference != inputItem["reference"] {
|
|
t.Fatalf("reference mismatch. Expected %s, Actual: %s", inputItem["reference"], item.Reference)
|
|
}
|
|
}
|
|
|
|
// We expect 4 successful requests (2 batch requests + 2 decrypt requests)
|
|
require.Equal(t, uint64(4), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Case5: Test batch encryption with an existing derived key
|
|
func TestTransit_BatchEncryptionCase5(t *testing.T) {
|
|
var resp *logical.Response
|
|
var err error
|
|
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
policyData := map[string]interface{}{
|
|
"derived": true,
|
|
}
|
|
|
|
policyReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "keys/existing_key",
|
|
Storage: s,
|
|
Data: policyData,
|
|
}
|
|
|
|
resp, err = b.HandleRequest(context.Background(), policyReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
batchInput := []interface{}{
|
|
map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "context": "dmlzaGFsCg=="},
|
|
map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "context": "dmlzaGFsCg=="},
|
|
}
|
|
|
|
batchData := map[string]interface{}{
|
|
"batch_input": batchInput,
|
|
}
|
|
|
|
batchReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "encrypt/existing_key",
|
|
Storage: s,
|
|
Data: batchData,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), batchReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
batchResponseItems := resp.Data["batch_results"].([]EncryptBatchResponseItem)
|
|
|
|
decReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "decrypt/existing_key",
|
|
Storage: s,
|
|
}
|
|
|
|
plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA=="
|
|
|
|
for _, item := range batchResponseItems {
|
|
if item.KeyVersion != 1 {
|
|
t.Fatalf("unexpected key version; got: %d, expected: %d", item.KeyVersion, 1)
|
|
}
|
|
|
|
decReq.Data = map[string]interface{}{
|
|
"ciphertext": item.Ciphertext,
|
|
"context": "dmlzaGFsCg==",
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), decReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
if resp.Data["plaintext"] != plaintext {
|
|
t.Fatalf("bad: plaintext. Expected: %q, Actual: %q", plaintext, resp.Data["plaintext"])
|
|
}
|
|
}
|
|
// We expect 4 successful transit requests (2 for batch encryption, 2 for batch decryption)
|
|
require.Equal(t, uint64(4), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Case6: Test batch encryption with an upserted non-derived key
|
|
func TestTransit_BatchEncryptionCase6(t *testing.T) {
|
|
var resp *logical.Response
|
|
var err error
|
|
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
batchInput := []interface{}{
|
|
map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA=="},
|
|
map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA=="},
|
|
}
|
|
|
|
batchData := map[string]interface{}{
|
|
"batch_input": batchInput,
|
|
}
|
|
batchReq := &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "encrypt/upserted_key",
|
|
Storage: s,
|
|
Data: batchData,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), batchReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
batchResponseItems := resp.Data["batch_results"].([]EncryptBatchResponseItem)
|
|
|
|
decReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "decrypt/upserted_key",
|
|
Storage: s,
|
|
}
|
|
|
|
plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA=="
|
|
|
|
for _, responseItem := range batchResponseItems {
|
|
var item EncryptBatchResponseItem
|
|
if err := mapstructure.Decode(responseItem, &item); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if item.KeyVersion != 1 {
|
|
t.Fatalf("unexpected key version; got: %d, expected: %d", item.KeyVersion, 1)
|
|
}
|
|
|
|
decReq.Data = map[string]interface{}{
|
|
"ciphertext": item.Ciphertext,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), decReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
if resp.Data["plaintext"] != plaintext {
|
|
t.Fatalf("bad: plaintext. Expected: %q, Actual: %q", plaintext, resp.Data["plaintext"])
|
|
}
|
|
}
|
|
|
|
// We expect 4 successful transit requests (2 for batch encryption, 2 for batch decryption)
|
|
require.Equal(t, uint64(4), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Case7: Test batch encryption with an upserted derived key
|
|
func TestTransit_BatchEncryptionCase7(t *testing.T) {
|
|
var resp *logical.Response
|
|
var err error
|
|
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
batchInput := []interface{}{
|
|
map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "context": "dmlzaGFsCg=="},
|
|
map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "context": "dmlzaGFsCg=="},
|
|
}
|
|
|
|
batchData := map[string]interface{}{
|
|
"batch_input": batchInput,
|
|
}
|
|
batchReq := &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "encrypt/upserted_key",
|
|
Storage: s,
|
|
Data: batchData,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), batchReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
batchResponseItems := resp.Data["batch_results"].([]EncryptBatchResponseItem)
|
|
|
|
decReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "decrypt/upserted_key",
|
|
Storage: s,
|
|
}
|
|
|
|
plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA=="
|
|
|
|
for _, item := range batchResponseItems {
|
|
if item.KeyVersion != 1 {
|
|
t.Fatalf("unexpected key version; got: %d, expected: %d", item.KeyVersion, 1)
|
|
}
|
|
|
|
decReq.Data = map[string]interface{}{
|
|
"ciphertext": item.Ciphertext,
|
|
"context": "dmlzaGFsCg==",
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), decReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
if resp.Data["plaintext"] != plaintext {
|
|
t.Fatalf("bad: plaintext. Expected: %q, Actual: %q", plaintext, resp.Data["plaintext"])
|
|
}
|
|
}
|
|
// We expect 4 successful transit requests (2 for batch encryption, 2 for batch decryption)
|
|
require.Equal(t, uint64(4), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Case8: If plaintext is not base64 encoded, encryption should fail
|
|
func TestTransit_BatchEncryptionCase8(t *testing.T) {
|
|
var resp *logical.Response
|
|
var err error
|
|
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
// Create the policy
|
|
policyReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "keys/existing_key",
|
|
Storage: s,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), policyReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
batchInput := []interface{}{
|
|
map[string]interface{}{"plaintext": "simple_plaintext"},
|
|
}
|
|
batchData := map[string]interface{}{
|
|
"batch_input": batchInput,
|
|
}
|
|
batchReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "encrypt/existing_key",
|
|
Storage: s,
|
|
Data: batchData,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), batchReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
plaintext := "simple plaintext"
|
|
|
|
encData := map[string]interface{}{
|
|
"plaintext": plaintext,
|
|
}
|
|
|
|
encReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "encrypt/existing_key",
|
|
Storage: s,
|
|
Data: encData,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), encReq)
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
// We expect 0 successful transit requests
|
|
require.Equal(t, uint64(0), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Case9: If both plaintext and batch inputs are supplied, plaintext should be
|
|
// ignored.
|
|
func TestTransit_BatchEncryptionCase9(t *testing.T) {
|
|
var resp *logical.Response
|
|
var err error
|
|
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
batchInput := []interface{}{
|
|
map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA=="},
|
|
map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA=="},
|
|
}
|
|
plaintext := "dGhlIHF1aWNrIGJyb3duIGZveA=="
|
|
batchData := map[string]interface{}{
|
|
"batch_input": batchInput,
|
|
"plaintext": plaintext,
|
|
}
|
|
batchReq := &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "encrypt/upserted_key",
|
|
Storage: s,
|
|
Data: batchData,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), batchReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
|
}
|
|
|
|
_, ok := resp.Data["ciphertext"]
|
|
if ok {
|
|
t.Fatal("ciphertext field should not be set")
|
|
}
|
|
|
|
// We expect 2 successful batch encryptions
|
|
require.Equal(t, uint64(2), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Case10: Inconsistent presence of 'context' in batch input should be caught
|
|
func TestTransit_BatchEncryptionCase10(t *testing.T) {
|
|
var err error
|
|
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
batchInput := []interface{}{
|
|
map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA=="},
|
|
map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "context": "dmlzaGFsCg=="},
|
|
}
|
|
|
|
batchData := map[string]interface{}{
|
|
"batch_input": batchInput,
|
|
}
|
|
|
|
batchReq := &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "encrypt/upserted_key",
|
|
Storage: s,
|
|
Data: batchData,
|
|
}
|
|
_, err = b.HandleRequest(context.Background(), batchReq)
|
|
if err == nil {
|
|
t.Fatalf("expected an error")
|
|
}
|
|
// We expect no successful transit requests
|
|
require.Equal(t, uint64(0), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Case11: Incorrect inputs for context and nonce should not fail the operation
|
|
func TestTransit_BatchEncryptionCase11(t *testing.T) {
|
|
var err error
|
|
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
batchInput := []interface{}{
|
|
map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "context": "dmlzaGFsCg=="},
|
|
map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "context": "not-encoded"},
|
|
}
|
|
|
|
batchData := map[string]interface{}{
|
|
"batch_input": batchInput,
|
|
}
|
|
batchReq := &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "encrypt/upserted_key",
|
|
Storage: s,
|
|
Data: batchData,
|
|
}
|
|
_, err = b.HandleRequest(context.Background(), batchReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// We expect 1 successful encryption out of the 2-item batch
|
|
require.Equal(t, uint64(1), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Case12: Invalid batch input
|
|
func TestTransit_BatchEncryptionCase12(t *testing.T) {
|
|
var err error
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
batchInput := []interface{}{
|
|
map[string]interface{}{},
|
|
"unexpected_interface",
|
|
}
|
|
|
|
batchData := map[string]interface{}{
|
|
"batch_input": batchInput,
|
|
}
|
|
batchReq := &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "encrypt/upserted_key",
|
|
Storage: s,
|
|
Data: batchData,
|
|
}
|
|
_, err = b.HandleRequest(context.Background(), batchReq)
|
|
if err == nil {
|
|
t.Fatalf("expected an error")
|
|
}
|
|
// We expect no successful requests
|
|
require.Equal(t, uint64(0), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Case13: Incorrect input for nonce when we aren't in convergent encryption should fail the operation
|
|
func TestTransit_EncryptionCase13(t *testing.T) {
|
|
var err error
|
|
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
// Non-batch first
|
|
data := map[string]interface{}{"plaintext": "bXkgc2VjcmV0IGRhdGE=", "nonce": "R80hr9eNUIuFV52e"}
|
|
req := &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "encrypt/my-key",
|
|
Storage: s,
|
|
Data: data,
|
|
}
|
|
resp, err := b.HandleRequest(context.Background(), req)
|
|
if err == nil {
|
|
t.Fatal("expected invalid request")
|
|
}
|
|
|
|
batchInput := []interface{}{
|
|
map[string]interface{}{"plaintext": "bXkgc2VjcmV0IGRhdGE=", "nonce": "R80hr9eNUIuFV52e"},
|
|
}
|
|
|
|
batchData := map[string]interface{}{
|
|
"batch_input": batchInput,
|
|
}
|
|
batchReq := &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "encrypt/my-key",
|
|
Storage: s,
|
|
Data: batchData,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), batchReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if v, ok := resp.Data["http_status_code"]; !ok || v.(int) != http.StatusBadRequest {
|
|
t.Fatal("expected request error")
|
|
}
|
|
// We expect no successful transit requests
|
|
require.Equal(t, uint64(0), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Case14: Incorrect input for nonce when we are in convergent version 3 should fail
|
|
func TestTransit_EncryptionCase14(t *testing.T) {
|
|
var err error
|
|
|
|
b, s := createBackendWithStorage(t)
|
|
|
|
cReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "keys/my-key",
|
|
Storage: s,
|
|
Data: map[string]interface{}{
|
|
"convergent_encryption": "true",
|
|
"derived": "true",
|
|
},
|
|
}
|
|
resp, err := b.HandleRequest(context.Background(), cReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Non-batch first
|
|
data := map[string]interface{}{"plaintext": "bXkgc2VjcmV0IGRhdGE=", "context": "SGVsbG8sIFdvcmxkCg==", "nonce": "R80hr9eNUIuFV52e"}
|
|
req := &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "encrypt/my-key",
|
|
Storage: s,
|
|
Data: data,
|
|
}
|
|
|
|
resp, err = b.HandleRequest(context.Background(), req)
|
|
if err == nil {
|
|
t.Fatal("expected invalid request")
|
|
}
|
|
|
|
batchInput := []interface{}{
|
|
data,
|
|
}
|
|
|
|
batchData := map[string]interface{}{
|
|
"batch_input": batchInput,
|
|
}
|
|
batchReq := &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: "encrypt/my-key",
|
|
Storage: s,
|
|
Data: batchData,
|
|
}
|
|
resp, err = b.HandleRequest(context.Background(), batchReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if v, ok := resp.Data["http_status_code"]; !ok || v.(int) != http.StatusBadRequest {
|
|
t.Fatal("expected request error")
|
|
}
|
|
// We expect no successful transit requests
|
|
require.Equal(t, uint64(0), b.billingDataCounts.Transit.Load())
|
|
}
|
|
|
|
// Test that the fast path function decodeBatchRequestItems behave like mapstructure.Decode() to decode []BatchRequestItem.
|
|
func TestTransit_decodeBatchRequestItems(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
src interface{}
|
|
requirePlaintext bool
|
|
requireCiphertext bool
|
|
dest []BatchRequestItem
|
|
wantErrContains string
|
|
}{
|
|
// basic edge cases of nil values
|
|
{name: "nil-nil", src: nil, dest: nil},
|
|
{name: "nil-empty", src: nil, dest: []BatchRequestItem{}},
|
|
{name: "empty-nil", src: []interface{}{}, dest: nil},
|
|
{
|
|
name: "src-nil",
|
|
src: []interface{}{map[string]interface{}{}},
|
|
dest: nil,
|
|
},
|
|
// empty src & dest
|
|
{
|
|
name: "src-dest",
|
|
src: []interface{}{map[string]interface{}{}},
|
|
dest: []BatchRequestItem{},
|
|
},
|
|
// empty src but with already populated dest, mapstructure discard pre-populated data.
|
|
{
|
|
name: "src-dest_pre_filled",
|
|
src: []interface{}{map[string]interface{}{}},
|
|
dest: []BatchRequestItem{{}},
|
|
},
|
|
// two test per properties to test valid and invalid input
|
|
{
|
|
name: "src_plaintext-dest",
|
|
src: []interface{}{map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA=="}},
|
|
dest: []BatchRequestItem{},
|
|
},
|
|
{
|
|
name: "src_plaintext_invalid-dest",
|
|
src: []interface{}{map[string]interface{}{"plaintext": 666}},
|
|
dest: []BatchRequestItem{},
|
|
wantErrContains: "expected type 'string', got unconvertible type 'int'",
|
|
},
|
|
{
|
|
name: "src_ciphertext-dest",
|
|
src: []interface{}{map[string]interface{}{"ciphertext": "dGhlIHF1aWNrIGJyb3duIGZveA=="}},
|
|
dest: []BatchRequestItem{},
|
|
},
|
|
{
|
|
name: "src_ciphertext_invalid-dest",
|
|
src: []interface{}{map[string]interface{}{"ciphertext": 666}},
|
|
dest: []BatchRequestItem{},
|
|
wantErrContains: "expected type 'string', got unconvertible type 'int'",
|
|
},
|
|
{
|
|
name: "src_key_version-dest",
|
|
src: []interface{}{map[string]interface{}{"key_version": 1}},
|
|
dest: []BatchRequestItem{},
|
|
},
|
|
{
|
|
name: "src_key_version_invalid-dest",
|
|
src: []interface{}{map[string]interface{}{"key_version": "666"}},
|
|
dest: []BatchRequestItem{},
|
|
wantErrContains: "expected type 'int', got unconvertible type 'string'",
|
|
},
|
|
{
|
|
name: "src_key_version_invalid-number-dest",
|
|
src: []interface{}{map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "key_version": json.Number("1.1")}},
|
|
dest: []BatchRequestItem{},
|
|
wantErrContains: "error decoding json.Number into [0].key_version",
|
|
},
|
|
{
|
|
name: "src_nonce-dest",
|
|
src: []interface{}{map[string]interface{}{"nonce": "dGVzdGNvbnRleHQ="}},
|
|
dest: []BatchRequestItem{},
|
|
},
|
|
{
|
|
name: "src_nonce_invalid-dest",
|
|
src: []interface{}{map[string]interface{}{"nonce": 666}},
|
|
dest: []BatchRequestItem{},
|
|
wantErrContains: "expected type 'string', got unconvertible type 'int'",
|
|
},
|
|
{
|
|
name: "src_context-dest",
|
|
src: []interface{}{map[string]interface{}{"context": "dGVzdGNvbnRleHQ="}},
|
|
dest: []BatchRequestItem{},
|
|
},
|
|
{
|
|
name: "src_context_invalid-dest",
|
|
src: []interface{}{map[string]interface{}{"context": 666}},
|
|
dest: []BatchRequestItem{},
|
|
wantErrContains: "expected type 'string', got unconvertible type 'int'",
|
|
},
|
|
{
|
|
name: "src_multi_order-dest",
|
|
src: []interface{}{
|
|
map[string]interface{}{"context": "1"},
|
|
map[string]interface{}{"context": "2"},
|
|
map[string]interface{}{"context": "3"},
|
|
},
|
|
dest: []BatchRequestItem{},
|
|
},
|
|
{
|
|
name: "src_multi_with_invalid-dest",
|
|
src: []interface{}{
|
|
map[string]interface{}{"context": "1"},
|
|
map[string]interface{}{"context": "2", "key_version": "666"},
|
|
map[string]interface{}{"context": "3"},
|
|
},
|
|
dest: []BatchRequestItem{},
|
|
wantErrContains: "expected type 'int', got unconvertible type 'string'",
|
|
},
|
|
{
|
|
name: "src_multi_with_multi_invalid-dest",
|
|
src: []interface{}{
|
|
map[string]interface{}{"context": "1"},
|
|
map[string]interface{}{"context": "2", "key_version": "666"},
|
|
map[string]interface{}{"context": "3", "key_version": "1337"},
|
|
},
|
|
dest: []BatchRequestItem{},
|
|
wantErrContains: "expected type 'int', got unconvertible type 'string'",
|
|
},
|
|
{
|
|
name: "src_plaintext-nil-nonce",
|
|
src: []interface{}{map[string]interface{}{"plaintext": "dGhlIHF1aWNrIGJyb3duIGZveA==", "nonce": "null"}},
|
|
dest: []BatchRequestItem{},
|
|
},
|
|
// required fields
|
|
{
|
|
name: "required_plaintext_present",
|
|
src: []interface{}{map[string]interface{}{"plaintext": ""}},
|
|
requirePlaintext: true,
|
|
dest: []BatchRequestItem{},
|
|
},
|
|
{
|
|
name: "required_plaintext_missing",
|
|
src: []interface{}{map[string]interface{}{}},
|
|
requirePlaintext: true,
|
|
dest: []BatchRequestItem{},
|
|
wantErrContains: "missing plaintext",
|
|
},
|
|
{
|
|
name: "required_ciphertext_present",
|
|
src: []interface{}{map[string]interface{}{"ciphertext": "dGhlIHF1aWNrIGJyb3duIGZveA=="}},
|
|
requireCiphertext: true,
|
|
dest: []BatchRequestItem{},
|
|
},
|
|
{
|
|
name: "required_ciphertext_missing",
|
|
src: []interface{}{map[string]interface{}{}},
|
|
requireCiphertext: true,
|
|
dest: []BatchRequestItem{},
|
|
wantErrContains: "missing ciphertext",
|
|
},
|
|
{
|
|
name: "required_plaintext_and_ciphertext_missing",
|
|
src: []interface{}{map[string]interface{}{}},
|
|
requirePlaintext: true,
|
|
requireCiphertext: true,
|
|
dest: []BatchRequestItem{},
|
|
wantErrContains: "missing ciphertext",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
expectedDest := append(tt.dest[:0:0], tt.dest...) // copy of the dest state
|
|
expectedErr := mapstructure.Decode(tt.src, &expectedDest) != nil || tt.wantErrContains != ""
|
|
|
|
gotErr := decodeBatchRequestItems(tt.src, tt.requirePlaintext, tt.requireCiphertext, &tt.dest)
|
|
gotDest := tt.dest
|
|
|
|
if expectedErr {
|
|
if gotErr == nil {
|
|
t.Fatal("decodeBatchRequestItems unexpected error value; expected error but got none")
|
|
}
|
|
if tt.wantErrContains == "" {
|
|
t.Fatal("missing error condition")
|
|
}
|
|
if !strings.Contains(gotErr.Error(), tt.wantErrContains) {
|
|
t.Errorf("decodeBatchRequestItems unexpected error value, want err contains: '%v', got: '%v'", tt.wantErrContains, gotErr)
|
|
}
|
|
}
|
|
|
|
if !reflect.DeepEqual(expectedDest, gotDest) {
|
|
t.Errorf("decodeBatchRequestItems unexpected dest value, want: '%v', got: '%v'", expectedDest, gotDest)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestShouldWarnAboutNonceUsage(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
keyTypes []keysutil.KeyType
|
|
nonce []byte
|
|
convergentEncryption bool
|
|
convergentVersion int
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "-NoConvergent-WithNonce",
|
|
keyTypes: []keysutil.KeyType{keysutil.KeyType_AES256_GCM96, keysutil.KeyType_AES128_GCM96, keysutil.KeyType_ChaCha20_Poly1305},
|
|
nonce: []byte("testnonce"),
|
|
convergentEncryption: false,
|
|
convergentVersion: -1,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "-NoConvergent-NoNonce",
|
|
keyTypes: []keysutil.KeyType{keysutil.KeyType_AES256_GCM96, keysutil.KeyType_AES128_GCM96, keysutil.KeyType_ChaCha20_Poly1305},
|
|
nonce: []byte{},
|
|
convergentEncryption: false,
|
|
convergentVersion: -1,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "-Convergentv1-WithNonce",
|
|
keyTypes: []keysutil.KeyType{keysutil.KeyType_AES256_GCM96, keysutil.KeyType_AES128_GCM96, keysutil.KeyType_ChaCha20_Poly1305},
|
|
nonce: []byte("testnonce"),
|
|
convergentEncryption: true,
|
|
convergentVersion: 1,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "-Convergentv2-WithNonce",
|
|
keyTypes: []keysutil.KeyType{keysutil.KeyType_AES256_GCM96, keysutil.KeyType_AES128_GCM96, keysutil.KeyType_ChaCha20_Poly1305},
|
|
nonce: []byte("testnonce"),
|
|
convergentEncryption: true,
|
|
convergentVersion: 2,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "-Convergentv3-WithNonce",
|
|
keyTypes: []keysutil.KeyType{keysutil.KeyType_AES256_GCM96, keysutil.KeyType_AES128_GCM96, keysutil.KeyType_ChaCha20_Poly1305},
|
|
nonce: []byte("testnonce"),
|
|
convergentEncryption: true,
|
|
convergentVersion: 3,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "-NoConvergent-WithNonce",
|
|
keyTypes: []keysutil.KeyType{keysutil.KeyType_RSA2048, keysutil.KeyType_RSA4096},
|
|
nonce: []byte("testnonce"),
|
|
convergentEncryption: false,
|
|
convergentVersion: -1,
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
for _, keyType := range tt.keyTypes {
|
|
testName := keyType.String() + tt.name
|
|
t.Run(testName, func(t *testing.T) {
|
|
p := keysutil.Policy{
|
|
ConvergentEncryption: tt.convergentEncryption,
|
|
ConvergentVersion: tt.convergentVersion,
|
|
Type: keyType,
|
|
}
|
|
|
|
actual := shouldWarnAboutNonceUsage(&p, tt.nonce)
|
|
|
|
if actual != tt.expected {
|
|
t.Errorf("Expected actual '%v' but got '%v'", tt.expected, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTransit_EncryptWithRSAPublicKey(t *testing.T) {
|
|
generateKeys(t)
|
|
b, s := createBackendWithStorage(t)
|
|
keyType := "rsa-2048"
|
|
keyID, err := uuid.GenerateUUID()
|
|
if err != nil {
|
|
t.Fatalf("failed to generate key ID: %s", err)
|
|
}
|
|
|
|
// Get key
|
|
privateKey := getKey(t, keyType)
|
|
publicKeyBytes, err := getPublicKey(privateKey, keyType)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Import key
|
|
req := &logical.Request{
|
|
Storage: s,
|
|
Operation: logical.UpdateOperation,
|
|
Path: fmt.Sprintf("keys/%s/import", keyID),
|
|
Data: map[string]interface{}{
|
|
"public_key": publicKeyBytes,
|
|
"type": keyType,
|
|
},
|
|
}
|
|
_, err = b.HandleRequest(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("failed to import public key: %s", err)
|
|
}
|
|
|
|
req = &logical.Request{
|
|
Operation: logical.CreateOperation,
|
|
Path: fmt.Sprintf("encrypt/%s", keyID),
|
|
Storage: s,
|
|
Data: map[string]interface{}{
|
|
"plaintext": "bXkgc2VjcmV0IGRhdGE=",
|
|
},
|
|
}
|
|
_, err = b.HandleRequest(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|