Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions test/library/encryption/assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"encoding/hex"
"fmt"
"reflect"
"strconv"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -179,3 +181,91 @@ func encryptionModeFromEtcdValue(data []byte) (string, bool) {
func hasPrefixAndTrailingData(data, prefix []byte) bool {
return bytes.HasPrefix(data, prefix) && len(data) > len(prefix)
}

// kmsPluginNameFromEtcdValue extracts the KMS provider name from etcd data encrypted with KMS v2.
// In OpenShift the name is "{operatorKeyID}_{resource}", e.g. "2_secrets".
func kmsPluginNameFromEtcdValue(data []byte) (string, bool) {
if !bytes.HasPrefix(data, []byte(kmsTransformerPrefixV2)) {
return "", false
}
rest := data[len(kmsTransformerPrefixV2):]
i := bytes.IndexByte(rest, ':')
if i <= 0 {
return "", false
}
return string(rest[:i]), true
}

// operatorKeyIDFromKMSPluginName extracts the operator key ID from a KMS plugin name.
func operatorKeyIDFromKMSPluginName(pluginName string) (string, bool) {
keyID, _, ok := strings.Cut(pluginName, "_")
if !ok || keyID == "" {
return "", false
}
return keyID, true
}

// kmsPrefixForDiagnostics returns the readable KMS encryption prefix from etcd data.
// Only the ASCII prefix is included (transformer + plugin name); encrypted payload bytes are omitted.
func kmsPrefixForDiagnostics(data []byte) string {
if len(data) == 0 {
return "<empty>"
}
pluginName, ok := kmsPluginNameFromEtcdValue(data)
if ok {
return kmsTransformerPrefixV2 + pluginName + ":"
}
if bytes.HasPrefix(data, []byte(kmsTransformerPrefixV2)) {
return kmsTransformerPrefixV2 + "<malformed plugin name>"
}
return "<not kms-encrypted>"
}

func expectedKMSPrefix(operatorKeyID, resource string) string {
return fmt.Sprintf("%s%s_%s:", kmsTransformerPrefixV2, operatorKeyID, resource)
}

// OperatorKeyIDFromEncryptionKeyMeta returns the numeric operator key ID from an encryption-key secret name.
func OperatorKeyIDFromEncryptionKeyMeta(t testing.TB, meta EncryptionKeyMeta) string {
t.Helper()
keyID, ok := encryptionKeyNameToKeyID(meta.Name)
require.True(t, ok, "invalid encryption key secret name %q", meta.Name)
return strconv.FormatUint(keyID, 10)
}

// AssertEtcdValueEncryptedWithOperatorKeyID verifies raw etcd data starts with
// k8s:enc:kms:v2:{expectedKeyID}_{gr.Resource}:.
func AssertEtcdValueEncryptedWithOperatorKeyID(t testing.TB, raw []byte, expectedKeyID string, gr schema.GroupResource) {
t.Helper()
// OpenShift builds KMS plugin names as "{keyID}_{resource}" where resource is the
// bare resource name (gr.Resource), not gr.String() — see stateToProviders in encryptiondata/config.go.
resource := gr.Resource
want := expectedKMSPrefix(expectedKeyID, resource)
t.Logf("Verifying etcd encryption prefix for resource %q matches operator keyID %q", resource, expectedKeyID)

if !bytes.HasPrefix(raw, []byte(want)) {
gotPrefix := kmsPrefixForDiagnostics(raw)
gotKeyID := "<unknown>"
if pluginName, ok := kmsPluginNameFromEtcdValue(raw); ok {
if keyID, ok := operatorKeyIDFromKMSPluginName(pluginName); ok {
gotKeyID = keyID
}
}
t.Errorf("etcd data for resource %q: want prefix %q, got %q (operator keyID %q, expected %q)",
resource, want, gotPrefix, gotKeyID, expectedKeyID)
} else {
t.Logf("etcd data for resource %q matches operator keyID %q", resource, expectedKeyID)
}
}

// AssertKMSEncryptedWithWriteKey verifies raw etcd data uses the current write key for the given resource.
func AssertKMSEncryptedWithWriteKey(t testing.TB, kubeClient kubernetes.Interface, namespace, labelSelector string, raw []byte, gr schema.GroupResource) {
t.Helper()
lastMigratedKeyMeta, err := GetLastKeyMeta(t, kubeClient, namespace, labelSelector)
require.NoError(t, err)
require.NotEmpty(t, lastMigratedKeyMeta.Name, "no encryption key found to verify KMS keyID")

keyID := OperatorKeyIDFromEncryptionKeyMeta(t, lastMigratedKeyMeta)
t.Logf("Checking if etcd data for resource %v uses write key with operator keyID %q in namespace %q", gr, keyID, namespace)
AssertEtcdValueEncryptedWithOperatorKeyID(t, raw, keyID, gr)
}