vault/builtin/logical/transit/path_export_test.go
Alexander Scheel daf72aa427
Fix transit import/export of hmac-only keys (#20864)
* Fix export of HMAC typed keys

When initially implemented, exporting HMAC keys resulted in returning
the unused, internal HMACKey value rather than the main Key value that
is used for HMAC operations.

This is a breaking change.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Consistently handle HMAC keys in keysutil

When generating HMAC-typed keys, set HMACKey = Key consistently, to
allow users of HMAC-typed keys to use them backwards compatibly.

Notably, this could discard the (unused) HMACKey field set today.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add test proving export of HMAC keys work

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add changelog entry

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

---------

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
2023-05-31 18:04:08 +00:00

384 lines
9.6 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package transit
import (
"context"
"fmt"
"reflect"
"strconv"
"testing"
"github.com/hashicorp/vault/sdk/logical"
)
func TestTransit_Export_KeyVersion_ExportsCorrectVersion(t *testing.T) {
verifyExportsCorrectVersion(t, "encryption-key", "aes128-gcm96")
verifyExportsCorrectVersion(t, "encryption-key", "aes256-gcm96")
verifyExportsCorrectVersion(t, "encryption-key", "chacha20-poly1305")
verifyExportsCorrectVersion(t, "signing-key", "ecdsa-p256")
verifyExportsCorrectVersion(t, "signing-key", "ecdsa-p384")
verifyExportsCorrectVersion(t, "signing-key", "ecdsa-p521")
verifyExportsCorrectVersion(t, "signing-key", "ed25519")
verifyExportsCorrectVersion(t, "hmac-key", "aes128-gcm96")
verifyExportsCorrectVersion(t, "hmac-key", "aes256-gcm96")
verifyExportsCorrectVersion(t, "hmac-key", "chacha20-poly1305")
verifyExportsCorrectVersion(t, "hmac-key", "ecdsa-p256")
verifyExportsCorrectVersion(t, "hmac-key", "ecdsa-p384")
verifyExportsCorrectVersion(t, "hmac-key", "ecdsa-p521")
verifyExportsCorrectVersion(t, "hmac-key", "ed25519")
verifyExportsCorrectVersion(t, "hmac-key", "hmac")
}
func verifyExportsCorrectVersion(t *testing.T, exportType, keyType string) {
b, storage := createBackendWithSysView(t)
// First create a key, v1
req := &logical.Request{
Storage: storage,
Operation: logical.UpdateOperation,
Path: "keys/foo",
}
req.Data = map[string]interface{}{
"exportable": true,
"type": keyType,
}
if keyType == "hmac" {
req.Data["key_size"] = 32
}
_, err := b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
verifyVersion := func(versionRequest string, expectedVersion int) {
req := &logical.Request{
Storage: storage,
Operation: logical.ReadOperation,
Path: fmt.Sprintf("export/%s/foo/%s", exportType, versionRequest),
}
rsp, err := b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
typRaw, ok := rsp.Data["type"]
if !ok {
t.Fatal("no type returned from export")
}
typ, ok := typRaw.(string)
if !ok {
t.Fatalf("could not find key type, resp data is %#v", rsp.Data)
}
if typ != keyType {
t.Fatalf("key type mismatch; %q vs %q", typ, keyType)
}
keysRaw, ok := rsp.Data["keys"]
if !ok {
t.Fatal("could not find keys value")
}
keys, ok := keysRaw.(map[string]string)
if !ok {
t.Fatal("could not cast to keys map")
}
if len(keys) != 1 {
t.Fatal("unexpected number of keys found")
}
for k := range keys {
if k != strconv.Itoa(expectedVersion) {
t.Fatalf("expected version %q, received version %q", strconv.Itoa(expectedVersion), k)
}
}
}
verifyVersion("v1", 1)
verifyVersion("1", 1)
verifyVersion("latest", 1)
req.Path = "keys/foo/rotate"
// v2
_, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
verifyVersion("v1", 1)
verifyVersion("1", 1)
verifyVersion("v2", 2)
verifyVersion("2", 2)
verifyVersion("latest", 2)
// v3
_, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
verifyVersion("v1", 1)
verifyVersion("1", 1)
verifyVersion("v3", 3)
verifyVersion("3", 3)
verifyVersion("latest", 3)
}
func TestTransit_Export_ValidVersionsOnly(t *testing.T) {
b, storage := createBackendWithSysView(t)
// First create a key, v1
req := &logical.Request{
Storage: storage,
Operation: logical.UpdateOperation,
Path: "keys/foo",
}
req.Data = map[string]interface{}{
"exportable": true,
}
_, err := b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
req.Path = "keys/foo/rotate"
// v2
_, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
// v3
_, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
verifyExport := func(validVersions []int) {
req = &logical.Request{
Storage: storage,
Operation: logical.ReadOperation,
Path: "export/encryption-key/foo",
}
rsp, err := b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if _, ok := rsp.Data["keys"]; !ok {
t.Error("no keys returned from export")
}
keys, ok := rsp.Data["keys"].(map[string]string)
if !ok {
t.Error("could not cast to keys object")
}
if len(keys) != len(validVersions) {
t.Errorf("expected %d key count, received %d", len(validVersions), len(keys))
}
for _, version := range validVersions {
if _, ok := keys[strconv.Itoa(version)]; !ok {
t.Errorf("expecting to find key version %d, not found", version)
}
}
}
verifyExport([]int{1, 2, 3})
req = &logical.Request{
Storage: storage,
Operation: logical.UpdateOperation,
Path: "keys/foo/config",
}
req.Data = map[string]interface{}{
"min_decryption_version": 3,
}
_, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
verifyExport([]int{3})
req = &logical.Request{
Storage: storage,
Operation: logical.UpdateOperation,
Path: "keys/foo/config",
}
req.Data = map[string]interface{}{
"min_decryption_version": 2,
}
_, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
verifyExport([]int{2, 3})
req = &logical.Request{
Storage: storage,
Operation: logical.UpdateOperation,
Path: "keys/foo/rotate",
}
// v4
_, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
verifyExport([]int{2, 3, 4})
}
func TestTransit_Export_KeysNotMarkedExportable_ReturnsError(t *testing.T) {
b, storage := createBackendWithSysView(t)
req := &logical.Request{
Storage: storage,
Operation: logical.UpdateOperation,
Path: "keys/foo",
}
req.Data = map[string]interface{}{
"exportable": false,
}
_, err := b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
req = &logical.Request{
Storage: storage,
Operation: logical.ReadOperation,
Path: "export/encryption-key/foo",
}
rsp, err := b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if !rsp.IsError() {
t.Fatal("Key not marked as exportable but was exported.")
}
}
func TestTransit_Export_SigningDoesNotSupportSigning_ReturnsError(t *testing.T) {
b, storage := createBackendWithSysView(t)
req := &logical.Request{
Storage: storage,
Operation: logical.UpdateOperation,
Path: "keys/foo",
}
req.Data = map[string]interface{}{
"exportable": true,
"type": "aes256-gcm96",
}
_, err := b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
req = &logical.Request{
Storage: storage,
Operation: logical.ReadOperation,
Path: "export/signing-key/foo",
}
_, err = b.HandleRequest(context.Background(), req)
if err == nil {
t.Fatal("Key does not support signing but was exported without error.")
}
}
func TestTransit_Export_EncryptionDoesNotSupportEncryption_ReturnsError(t *testing.T) {
testTransit_Export_EncryptionDoesNotSupportEncryption_ReturnsError(t, "ecdsa-p256")
testTransit_Export_EncryptionDoesNotSupportEncryption_ReturnsError(t, "ecdsa-p384")
testTransit_Export_EncryptionDoesNotSupportEncryption_ReturnsError(t, "ecdsa-p521")
testTransit_Export_EncryptionDoesNotSupportEncryption_ReturnsError(t, "ed25519")
}
func testTransit_Export_EncryptionDoesNotSupportEncryption_ReturnsError(t *testing.T, keyType string) {
b, storage := createBackendWithSysView(t)
req := &logical.Request{
Storage: storage,
Operation: logical.UpdateOperation,
Path: "keys/foo",
}
req.Data = map[string]interface{}{
"exportable": true,
"type": keyType,
}
_, err := b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
req = &logical.Request{
Storage: storage,
Operation: logical.ReadOperation,
Path: "export/encryption-key/foo",
}
_, err = b.HandleRequest(context.Background(), req)
if err == nil {
t.Fatal("Key does not support encryption but was exported without error.")
}
}
func TestTransit_Export_KeysDoesNotExist_ReturnsNotFound(t *testing.T) {
b, storage := createBackendWithSysView(t)
req := &logical.Request{
Storage: storage,
Operation: logical.ReadOperation,
Path: "export/encryption-key/foo",
}
rsp, err := b.HandleRequest(context.Background(), req)
if !(rsp == nil && err == nil) {
t.Fatal("Key does not exist but does not return not found")
}
}
func TestTransit_Export_EncryptionKey_DoesNotExportHMACKey(t *testing.T) {
b, storage := createBackendWithSysView(t)
req := &logical.Request{
Storage: storage,
Operation: logical.UpdateOperation,
Path: "keys/foo",
}
req.Data = map[string]interface{}{
"exportable": true,
"type": "aes256-gcm96",
}
_, err := b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
req = &logical.Request{
Storage: storage,
Operation: logical.ReadOperation,
Path: "export/encryption-key/foo",
}
encryptionKeyRsp, err := b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
req.Path = "export/hmac-key/foo"
hmacKeyRsp, err := b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
}
encryptionKeys, ok := encryptionKeyRsp.Data["keys"].(map[string]string)
if !ok {
t.Error("could not cast to keys object")
}
hmacKeys, ok := hmacKeyRsp.Data["keys"].(map[string]string)
if !ok {
t.Error("could not cast to keys object")
}
if len(hmacKeys) != len(encryptionKeys) {
t.Errorf("hmac (%d) and encryption (%d) key count don't match",
len(hmacKeys), len(encryptionKeys))
}
if reflect.DeepEqual(encryptionKeyRsp.Data, hmacKeyRsp.Data) {
t.Fatal("Encryption key data matched hmac key data")
}
}