Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
81 changes: 80 additions & 1 deletion otdfctl/cmd/policy/kasKeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,62 @@ func policyUpdateKasKey(cmd *cobra.Command, args []string) {
common.HandleSuccess(cmd, kasKey.GetKey().GetId(), t, kasKey)
}

func unsafeUpdateKasKeyMode(modeArg string) (policy.KeyMode, error) {
if modeArg == "" {
return policy.KeyMode_KEY_MODE_UNSPECIFIED, nil
}

mode, err := modeToEnum(modeArg)
if err != nil {
return policy.KeyMode_KEY_MODE_UNSPECIFIED, err
}

switch mode { //nolint:exhaustive // only remote and public_key are supported for unsafe update.
case policy.KeyMode_KEY_MODE_REMOTE, policy.KeyMode_KEY_MODE_PUBLIC_KEY_ONLY:
return mode, nil
default:
return policy.KeyMode_KEY_MODE_UNSPECIFIED, fmt.Errorf("mode must be %q or %q", keyModeRemote, keyModePublicKeyOnly)
}
}

func policyUnsafeUpdateKasKey(cmd *cobra.Command, args []string) {
c := cli.New(cmd, args)

id := c.Flags.GetRequiredID("id")
modeArg := c.Flags.GetOptionalString("mode")
providerConfigID := c.Flags.GetOptionalID("provider-config-id")
force := c.Flags.GetOptionalBool("force")

mode, err := unsafeUpdateKasKeyMode(modeArg)
if err != nil {
cli.ExitWithError("Invalid key mode", err)
}

h := common.NewHandler(c)
defer h.Close()

existingKasKey, err := h.GetKasKey(c.Context(), id, nil)
if err != nil {
cli.ExitWithError("Failed to get kas key", err)
}

existingKey := existingKasKey.GetKey()
Comment thread
c-r33d marked this conversation as resolved.
confirmID := fmt.Sprintf("Id: %s\n\tKAS URI: %s\n\tKID: %s", existingKey.GetId(), existingKasKey.GetKasUri(), existingKey.GetKeyId())
cli.ConfirmAction(cli.ActionUpdateUnsafe, "key", confirmID, force)

kasKey, err := h.UnsafeUpdateKasKey(c.Context(), id, mode, providerConfigID)
if err != nil {
cli.ExitWithError("Failed to update kas key", err)
}

rows := getTableRows(kasKey)
if mdRows := getMetadataRows(kasKey.GetKey().GetMetadata()); mdRows != nil {
rows = append(rows, mdRows...)
}
t := cli.NewTabular(rows...)
common.HandleSuccess(cmd, kasKey.GetKey().GetId(), t, kasKey)
}

func policyListKasKeys(cmd *cobra.Command, args []string) {
c := cli.New(cmd, args)
h := common.NewHandler(c)
Expand Down Expand Up @@ -1137,7 +1193,30 @@ func initKASKeysCommands() {
unsafeDeleteDoc.GetDocFlag("kas-uri").Description,
)

unsafeCmd.AddSubcommands(unsafeDeleteDoc)
unsafeUpdateDoc := man.Docs.GetCommand(
"policy/kas-registry/key/unsafe/update",
man.WithRun(policyUnsafeUpdateKasKey),
)
unsafeUpdateDoc.Flags().StringP(
unsafeUpdateDoc.GetDocFlag("id").Name,
unsafeUpdateDoc.GetDocFlag("id").Shorthand,
unsafeUpdateDoc.GetDocFlag("id").Default,
unsafeUpdateDoc.GetDocFlag("id").Description,
)
unsafeUpdateDoc.Flags().StringP(
unsafeUpdateDoc.GetDocFlag("mode").Name,
unsafeUpdateDoc.GetDocFlag("mode").Shorthand,
unsafeUpdateDoc.GetDocFlag("mode").Default,
unsafeUpdateDoc.GetDocFlag("mode").Description,
)
unsafeUpdateDoc.Flags().StringP(
unsafeUpdateDoc.GetDocFlag("provider-config-id").Name,
unsafeUpdateDoc.GetDocFlag("provider-config-id").Shorthand,
unsafeUpdateDoc.GetDocFlag("provider-config-id").Default,
unsafeUpdateDoc.GetDocFlag("provider-config-id").Description,
)

unsafeCmd.AddSubcommands(unsafeDeleteDoc, unsafeUpdateDoc)
policyKasRegistryKeysCmd.AddSubcommands(createDoc, getDoc, updateDoc, listDoc, rotateDoc, importDoc, mappingsDoc, unsafeCmd)
KasRegistryCmd.AddCommand(&policyKasRegistryKeysCmd.Command)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ Make sure you know what you are doing.
## Example

```shell
otdfctl policy kas-keys unsafe delete --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --kas-uri https://kas.example.com --key-id "key-1"
otdfctl policy kas-registry key unsafe delete --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --kas-uri https://kas.example.com --key-id "key-1"
```
45 changes: 45 additions & 0 deletions otdfctl/docs/man/policy/kas-registry/key/unsafe/update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
title: Unsafely update a key
command:
name: update
flags:
- name: id
shorthand: i
description: System-given ID of the key to update.
required: true
- name: mode
shorthand: m
description: Target key mode. Only "remote" and "public_key" are supported.
- name: provider-config-id
shorthand: p
description: Configuration ID for the key provider. Required when changing to "remote" mode, or when updating only the provider configuration for an existing remote key.
---

# Unsafe Update Warning

Updating a key in place is a dangerous support operation. It can retroactively change decryptability for existing TDFs.

This command is limited to switching a key between `remote` and `public_key` modes, or updating the provider configuration
for an existing `remote` key. The key ID, KAS URI, and public key are preserved.

Make sure you know what you are doing.

## Examples

Change a key of mode `public_key` key to one of mode `remote`:

```shell
otdfctl policy kas-registry key unsafe update --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --mode remote --provider-config-id 298c9446-ef71-49eb-a6ef-960149095a76
```

Change a `remote` key to `public_key` mode:

```shell
otdfctl policy kas-registry key unsafe update --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --mode public_key
```

Update only the provider configuration for an existing remote key:

```shell
otdfctl policy kas-registry key unsafe update --id 3c51a593-cbf8-419d-b7dc-b656d0bedfbb --provider-config-id 298c9446-ef71-49eb-a6ef-960149095a76
```
169 changes: 164 additions & 5 deletions otdfctl/e2e/kas-keys.bats
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@ setup_file() {
assert_success
export KAS_REGISTRY_ID=$(echo "$output" | jq -r '.id')

if [ "$RUN_EXPERIMENTAL_TESTS" == "true" ]; then
run_otdfctl_provider_create --name "test-provider-config-kas-keys" --config '{}' --json
assert_success
export PC_ID=$(echo "$output" | jq -r '.id')
fi
run_otdfctl_provider_create --name "test-provider-config-kas-keys" --manager "fake-manager" --config '{}' --json
assert_success
export PC_ID=$(echo "$output" | jq -r '.id')
export WRAPPING_KEY=$(openssl rand -hex 32)
# Generate valid public keys and base64 encode (single-line)
export PEM_B64_RSA=$(openssl genrsa 2048 2>/dev/null | openssl rsa -pubout 2>/dev/null | base64 | tr -d '\n')
Expand Down Expand Up @@ -698,6 +696,167 @@ format_kas_name_as_uri() {
assert_equal "$(echo "$output" | jq -r .message)" "Flag '--id' is required"
}

@test "kas-keys: unsafe update key mode remote to public_key" {
KEY_ID_UNSAFE_UPDATE=$(generate_key_id)
run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_UNSAFE_UPDATE}" --algorithm "rsa:2048" --mode "remote" --public-key-pem "${PEM_B64}" --provider-config-id "${PC_ID}" --wrapping-key-id "wrapping-key-remote" --json
assert_success
local original_key_json="$output"
local key_system_id=$(echo "$output" | jq -r .key.id)
local original_stable_key_json=$(echo "$original_key_json" | jq -S -c 'del(.key.key_mode, .key.provider_config, .key.metadata.updated_at)')
local original_update_time=$(echo "$original_key_json" | jq -r .key.metadata.updated_at)
assert_equal "$(echo "$original_key_json" | jq -r .key.key_mode)" "3" # remote
assert_equal "$(echo "$original_key_json" | jq -r .key.provider_config.id)" "${PC_ID}"

run_otdfctl_key unsafe update --id "${key_system_id}" --mode public_key --json --force
assert_success
local updated_key_json="$output"
assert_equal "$(echo "$output" | jq -r .key.key_mode)" "4" # public_key
assert_equal "$(echo "$output" | jq -r .key.provider_config)" "null"
assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "${original_update_time}"
assert_equal "$(echo "$updated_key_json" | jq -S -c 'del(.key.key_mode, .key.provider_config, .key.metadata.updated_at)')" "${original_stable_key_json}"

run_otdfctl_key get --key "${key_system_id}" --json
assert_success
local persisted_key_json="$output"
assert_equal "$(echo "$output" | jq -r .key.key_mode)" "4" # public_key
assert_equal "$(echo "$output" | jq -r .key.provider_config)" "null"
assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "${original_update_time}"
assert_equal "$(echo "$persisted_key_json" | jq -S -c 'del(.key.key_mode, .key.provider_config, .key.metadata.updated_at)')" "${original_stable_key_json}"
}

@test "kas-keys: unsafe update public key to remote" {
KEY_ID_UNSAFE_UPDATE=$(generate_key_id)
run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_UNSAFE_UPDATE}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json
assert_success
local original_key_json="$output"
local key_system_id=$(echo "$output" | jq -r .key.id)
local original_stable_key_json=$(echo "$original_key_json" | jq -S -c 'del(.key.key_mode, .key.provider_config, .key.metadata.updated_at)')
local original_update_time=$(echo "$original_key_json" | jq -r .key.metadata.updated_at)
assert_equal "$(echo "$original_key_json" | jq -r .key.key_mode)" "4" # public_key
assert_equal "$(echo "$original_key_json" | jq -r .key.provider_config)" "null"

run_otdfctl_key unsafe update --id "${key_system_id}" --mode remote --provider-config-id "${PC_ID}" --json --force
assert_success
local updated_key_json="$output"
assert_equal "$(echo "$output" | jq -r .key.key_mode)" "3" # remote
assert_equal "$(echo "$output" | jq -r .key.provider_config.id)" "${PC_ID}"
assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "${original_update_time}"
assert_equal "$(echo "$updated_key_json" | jq -S -c 'del(.key.key_mode, .key.provider_config, .key.metadata.updated_at)')" "${original_stable_key_json}"

run_otdfctl_key get --key "${key_system_id}" --json
assert_success
local persisted_key_json="$output"
assert_equal "$(echo "$output" | jq -r .key.key_mode)" "3" # remote
assert_equal "$(echo "$output" | jq -r .key.provider_config.id)" "${PC_ID}"
assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "${original_update_time}"
assert_equal "$(echo "$persisted_key_json" | jq -S -c 'del(.key.key_mode, .key.provider_config, .key.metadata.updated_at)')" "${original_stable_key_json}"
}

@test "kas-keys: unsafe update remote key provider config only" {
KEY_ID_UNSAFE_UPDATE=$(generate_key_id)
run_otdfctl_provider_create --name "test-provider-config-kas-keys-update-${KEY_ID_UNSAFE_UPDATE}" --manager "fake-manager" --config '{}' --json
assert_success
local original_provider_config_id=$(echo "$output" | jq -r '.id')

run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_UNSAFE_UPDATE}" --algorithm "rsa:2048" --mode "remote" --public-key-pem "${PEM_B64}" --provider-config-id "${original_provider_config_id}" --wrapping-key-id "wrapping-key-remote" --json
assert_success
local original_key_json="$output"
local key_system_id=$(echo "$output" | jq -r .key.id)
local original_stable_key_json=$(echo "$original_key_json" | jq -S -c 'del(.key.provider_config, .key.metadata.updated_at)')
local original_update_time=$(echo "$original_key_json" | jq -r .key.metadata.updated_at)
assert_equal "$(echo "$output" | jq -r .key.key_mode)" "3" # remote
assert_equal "$(echo "$output" | jq -r .key.provider_config.id)" "${original_provider_config_id}"

run_otdfctl_key unsafe update --id "${key_system_id}" --provider-config-id "${PC_ID}" --json --force
assert_success
local updated_key_json="$output"
assert_equal "$(echo "$output" | jq -r .key.key_mode)" "3" # remote
assert_equal "$(echo "$output" | jq -r .key.provider_config.id)" "${PC_ID}"
assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "${original_update_time}"
assert_equal "$(echo "$updated_key_json" | jq -S -c 'del(.key.provider_config, .key.metadata.updated_at)')" "${original_stable_key_json}"

run_otdfctl_key get --key "${key_system_id}" --json
assert_success
local persisted_key_json="$output"
assert_equal "$(echo "$output" | jq -r .key.key_mode)" "3" # remote
assert_equal "$(echo "$output" | jq -r .key.provider_config.id)" "${PC_ID}"
assert_not_equal "$(echo "$output" | jq -r .key.metadata.updated_at)" "${original_update_time}"
assert_equal "$(echo "$persisted_key_json" | jq -S -c 'del(.key.provider_config, .key.metadata.updated_at)')" "${original_stable_key_json}"

delete_provider_config "$original_provider_config_id"
}

@test "kas-keys: unsafe update key failure - (invalid provider config, not UUID)" {
KEY_ID_UNSAFE_UPDATE=$(generate_key_id)
run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_UNSAFE_UPDATE}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json
assert_success
local key_system_id=$(echo "$output" | jq -r .key.id)

run_otdfctl_key unsafe update --id "${key_system_id}" --mode remote --provider-config-id "not-a-uuid" --force
assert_failure
assert_output --partial "Optional flag '--provider-config-id' received value 'not-a-uuid' and must be a valid UUID if used"
}

@test "kas-keys: unsafe update key failure - (missing id)" {
run_otdfctl_key unsafe update --mode public_key --force
assert_failure
assert_output --partial "Flag '--id' is required"
}

@test "kas-keys: unsafe update key failure - (missing provider configuration)" {
KEY_ID_UNSAFE_UPDATE=$(generate_key_id)
run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_UNSAFE_UPDATE}" --algorithm "rsa:2048" --mode "remote" --public-key-pem "${PEM_B64}" --provider-config-id "${PC_ID}" --wrapping-key-id "wrapping-key-remote" --json
assert_success
local key_system_id=$(echo "$output" | jq -r .key.id)

run_otdfctl_key unsafe update --id "${key_system_id}" --force
assert_failure
assert_output --partial "Failed to update kas key"
assert_output --partial "provider_config_id is required for requested key mode"
}

@test "kas-keys: unsafe update key failure - (remote mode missing provider config)" {
KEY_ID_UNSAFE_UPDATE=$(generate_key_id)
run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_UNSAFE_UPDATE}" --algorithm "rsa:2048" --mode "public_key" --public-key-pem "${PEM_B64}" --json
assert_success
local key_system_id=$(echo "$output" | jq -r .key.id)

run_otdfctl_key unsafe update --id "${key_system_id}" --mode remote --force
assert_failure
assert_output --partial "Failed to update kas key"
assert_output --partial "provider_config_id is required for requested key mode"
}

@test "kas-keys: unsafe update key failure - (public_key mode with provider config)" {
KEY_ID_UNSAFE_UPDATE=$(generate_key_id)
run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_UNSAFE_UPDATE}" --algorithm "rsa:2048" --mode "remote" --public-key-pem "${PEM_B64}" --provider-config-id "${PC_ID}" --wrapping-key-id "wrapping-key-remote" --json
assert_success
local key_system_id=$(echo "$output" | jq -r .key.id)

run_otdfctl_key unsafe update --id "${key_system_id}" --mode public_key --provider-config-id "${PC_ID}" --force
assert_failure
assert_output --partial "Failed to update kas key"
assert_output --partial "provider_config_id must be empty for requested key mode"
}

@test "kas-keys: unsafe update key failure - (existing key mode unsupported)" {
KEY_ID_UNSAFE_UPDATE=$(generate_key_id)
run_otdfctl_key create --kas "${KAS_REGISTRY_ID}" --key-id "${KEY_ID_UNSAFE_UPDATE}" --algorithm "rsa:2048" --mode local --wrapping-key-id "wrapping-key-local" --wrapping-key "${WRAPPING_KEY}" --json
assert_success
local key_system_id=$(echo "$output" | jq -r .key.id)

run_otdfctl_key unsafe update --id "${key_system_id}" --mode public_key --force
assert_failure
assert_output --partial "Failed to update kas key"
assert_output --partial "existing key mode cannot be updated"
}

@test "kas-keys: unsafe update key failure - (unsupported mode)" {
run_otdfctl_key unsafe update --id "ded32e6d-9fec-4a4c-a391-13158c52e5f2" --mode local --force
assert_failure
assert_output --partial "mode must be \"remote\" or \"public_key\""
}

# LIST Tests
@test "kas-keys: list keys (default limit and offset)" {
# Create a few keys to ensure there\'s something to list and to check structure
Expand Down
2 changes: 1 addition & 1 deletion otdfctl/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ require (
github.com/opentdf/platform/lib/identifier v0.4.0
github.com/opentdf/platform/lib/ocrypto v0.14.0
github.com/opentdf/platform/protocol/go v0.39.0
github.com/opentdf/platform/sdk v0.25.0
github.com/opentdf/platform/sdk v0.27.0
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/zitadel/oidc/v3 v3.45.1
Expand Down
4 changes: 2 additions & 2 deletions otdfctl/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,8 @@ github.com/opentdf/platform/lib/ocrypto v0.14.0 h1:qAIOFpz72/QDhYC0oLRXgA5uEnyv1
github.com/opentdf/platform/lib/ocrypto v0.14.0/go.mod h1:TLaMvVE1aTqrgW8AZz0ZuxX/ZpI7l5IQOHyX99fdKl0=
github.com/opentdf/platform/protocol/go v0.39.0 h1:02a+1yzDp6INhCSRvWpDYe2lvE8DLiIg0Cv89s2T8CE=
github.com/opentdf/platform/protocol/go v0.39.0/go.mod h1:6A0vQJ5D4ZTLReWAp8Y/7jTFzlYCmL/IfMJWDHZS6M0=
github.com/opentdf/platform/sdk v0.25.0 h1:SuHHgdFzaklDCx8zP3p9d06tDgeCbCuNhU/cUKjFd6w=
github.com/opentdf/platform/sdk v0.25.0/go.mod h1:kl4a4PoV3YoumaL5LTJzdB+SZINyCo4A9IdnLZN9aM4=
github.com/opentdf/platform/sdk v0.27.0 h1:ZwKzy9sUGHXuQkU9LNovvVP0CdOIGzKX5rRAenLGnkU=
github.com/opentdf/platform/sdk v0.27.0/go.mod h1:terqpXZ8ZuHt5rKFyLcGUPe1V+XkAtlzbdz6//G+gkk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
13 changes: 13 additions & 0 deletions otdfctl/pkg/handlers/kas-keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ func (h Handler) UpdateKasKey(ctx context.Context, id string, metadata *common.M
return resp.GetKasKey(), nil
}

func (h Handler) UnsafeUpdateKasKey(ctx context.Context, id string, mode policy.KeyMode, providerConfigID string) (*policy.KasKey, error) {
resp, err := h.sdk.Unsafe.UnsafeUpdateKey(ctx, &unsafe.UnsafeUpdateKeyRequest{
Id: id,
TargetKeyMode: mode,
ProviderConfigId: providerConfigID,
})
if err != nil {
return nil, err
}

return resp.GetKey(), nil
Comment thread
c-r33d marked this conversation as resolved.
}

func (h Handler) ListKasKeys(
ctx context.Context,
limit, offset int32,
Expand Down
Loading