Skip to content

fix(aws-kms): InvalidMarkerException on list operations, key ARN in DisabledException - #1682

Merged
Satyam-Trivedi-ZS merged 2 commits into
stackshy:developmentfrom
Satyam-Trivedi-ZS:fix/aws-kms-followups-1668
Oct 23, 2026
Merged

Satyam-Trivedi-ZS merged 2 commits into
stackshy:developmentfrom
Satyam-Trivedi-ZS:fix/aws-kms-followups-1668

Conversation

@Satyam-Trivedi-ZS

@Satyam-Trivedi-ZS Satyam-Trivedi-ZS commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Builds on #1647 (merged); rebased onto development, so the diff is now only this PR's changes.

Summary

  • A Marker the server never issued now gives InvalidMarkerException on the KMS list operations that model it. ListKeyPolicies doesn't model it, so it keeps ValidationException.
  • DisabledException now carries the message real KMS sends: <key ARN> is disabled.

Changes

Item 1: InvalidMarkerException. Status: FIXED (commit 69ebfe0e)

KMS pagination lives in server/aws/kms/paginate.go. The driver list methods return the full set, and the server cuts pages and issues the opaque Marker. The Marker is a wire token that only the server creates, so checking it stays in that file. The provider has no Marker concept.

  • pageWindow takes a markerFault that each operation sets from its modeled error list.
  • A Marker that breaks the modeled MarkerType constraint (longer than 1024 characters, or a character outside [ -ÿ]*) gives ValidationException on every operation, with the AWS constraint message.
  • A Marker that passes the constraint but doesn't decode gives InvalidMarkerException, or ValidationException on ListKeyPolicies.
  • ExInvalidMarker is back in services/kms/driver/errors.go. fix(aws-kms): DeriveSharedSecret, custom key stores, reserved aws: tags, grant principal validation #1647 removed it because nothing used it, and now it is used.

I checked each operation's error list in the botocore model (kms/2014-11-01/service-2.json) and in the API reference:

Operation InvalidMarkerException modeled Unknown Marker now gives API reference
ListKeys yes InvalidMarkerException ListKeys
ListAliases yes InvalidMarkerException ListAliases
ListGrants yes InvalidMarkerException ListGrants
ListRetirableGrants yes InvalidMarkerException ListRetirableGrants
ListKeyRotations yes InvalidMarkerException ListKeyRotations
ListResourceTags yes InvalidMarkerException ListResourceTags
DescribeCustomKeyStores yes InvalidMarkerException DescribeCustomKeyStores
ListKeyPolicies no ValidationException (unchanged) ListKeyPolicies

These are the only KMS operations with a Marker input in the model. All of them use MarkerType (min 1, max 1024, pattern [ -ÿ]*). The InvalidMarkerException message is the exception's documented description, because no captured real response text was available.

Item 2: DisabledException text. Status: FIXED (commit 3db06062)

  • Evidence: a real KMS response captured in hashicorp/terraform#5688 reads DisabledException: arn:aws:kms:us-west-2:187416307283:key/7e5a8e30-... is disabled. (status 400), for EnableKeyRotation on a disabled key. The DisabledException reference only documents the meaning, not the text.
  • requireUsable in providers/aws/kms/crypto.go now returns driver.WithMessage(driver.ErrKeyDisabled, "%s is disabled.", ARN). Every operation that returns DisabledException goes through requireUsable: Encrypt, Decrypt, ReEncrypt, the GenerateDataKey family, Sign, Verify, GenerateMac, VerifyMac, DeriveSharedSecret, GetPublicKey, Enable/DisableKeyRotation and RotateKeyOnDemand. So they all get the same text.
  • errors.Is(err, driver.ErrKeyDisabled) still matches, so Secrets Manager's isKeyStateErr and other callers still work.

Provider Coverage

  • AWS
  • Azure (not applicable: KMS-specific exception names and messages)
  • GCP (not applicable)
  • OCI (not applicable)

Checklist

  • All tests pass (go test ./...)
  • Linter passes (no new findings in touched packages)
  • Every provider the change applies to implements the same behavior
  • Integration tests added to cloudemu_test.go (not needed: covered by provider and SDK wire tests)
  • Unit tests added to provider test files
  • Regenerated docs (go generate ./... leaves no diff, since no driver interface changed)

Test Plan

New tests. Each one fails on e7c262cb and passes now:

  • server/aws/kms/pagination_test.go TestSDKListBadMarker: uses the real aws-sdk-go-v2 client. It runs all 8 list operations with an unknown Marker (InvalidMarkerException, or ValidationException for ListKeyPolicies), a 1025-character Marker and a Marker with U+0100 (both ValidationException). Before the fix, the 7 InvalidMarkerException subtests failed with got ValidationException: invalid Marker.
  • providers/aws/kms/key_state_test.go TestDisabledKeyMessageNamesKey: Encrypt, GenerateDataKey and EnableKeyRotation on a disabled key. It checks errors.Is(err, driver.ErrKeyDisabled) and the message <ARN> is disabled.
  • server/aws/kms/key_state_sdk_test.go TestSDKDisabledExceptionMessage: the typed *types.DisabledException with ErrorMessage() == "<ARN> is disabled.".

Checks:

  • go build ./...: ok
  • go vet on the kms packages: ok
  • go generate ./...: no diff
  • go test -race on providers/aws/kms, server/aws/kms, services/kms, persist, providers/aws/kmscrypto, providers/aws/secretsmanager, providers/aws/ssm, server/aws/secretsmanager and providers/aws: all ok
  • Full go test -p 2 over all 657 packages, in 22 chunks of 30: all ok
  • golangci-lint run --new-from-rev=origin/fix/aws-kms-parity-1523 on providers/aws/kms, server/aws/kms and services/kms: 0 issues, so no new findings in touched packages

AWS CLI v2 against cloudemu serve -providers aws -aws-port 4612, built from this branch:

$ aws --endpoint-url http://127.0.0.1:4612 kms list-keys --starting-token bm90LWEtbWFya2Vy
An error occurred (InvalidMarkerException) when calling the ListKeys operation: The request was rejected because the marker that specifies where pagination should next begin is not valid.
$ aws --endpoint-url http://127.0.0.1:4612 kms list-aliases --starting-token bm90LWEtbWFya2Vy
An error occurred (InvalidMarkerException) when calling the ListAliases operation: ...
$ aws --endpoint-url http://127.0.0.1:4612 kms list-key-policies --key-id $K --starting-token bm90LWEtbWFya2Vy
An error occurred (ValidationException) when calling the ListKeyPolicies operation: invalid Marker

Raw X-Amz-Target requests with "Marker":"not-a-marker":

  • ListKeys, ListAliases, ListGrants, ListRetirableGrants, ListKeyRotations, ListResourceTags and DescribeCustomKeyStores: InvalidMarkerException
  • ListKeyPolicies: ValidationException
  • A 1025-character Marker and badĀmarker: ValidationException with 1 validation error detected: Value '...' at 'marker' failed to satisfy constraint: ...
  • Normal list-keys --page-size 1 auto-pagination still returns every key.
$ aws --endpoint-url http://127.0.0.1:4612 kms disable-key --key-id $K
$ aws --endpoint-url http://127.0.0.1:4612 kms encrypt --key-id $K --plaintext aGk=
An error occurred (DisabledException) when calling the Encrypt operation: arn:aws:kms:us-east-1:000000000000:key/00000010-0000-4000-8000-000000000010 is disabled.
$ aws --endpoint-url http://127.0.0.1:4612 kms enable-key-rotation --key-id $K
An error occurred (DisabledException) when calling the EnableKeyRotation operation: arn:aws:kms:us-east-1:000000000000:key/00000010-... is disabled.
$ aws --endpoint-url http://127.0.0.1:4612 kms generate-data-key --key-id $K --key-spec AES_256
An error occurred (DisabledException) when calling the GenerateDataKey operation: arn:aws:kms:us-east-1:000000000000:key/00000010-... is disabled.

Known limits:

  • The InvalidMarkerException message is the documented description of the exception, not text captured from real KMS.
  • Real KMS's response to an undecodable Marker on ListKeyPolicies isn't documented. ValidationException is the only modeled error that fits, so it stays.
  • An explicit empty "Marker":"" is still treated as no Marker. Real KMS would reject it under min: 1, but SDKs never send it.
  • KMSInvalidStateException for other key states still uses a generic message. That wasn't in this issue.

Related Issues

Part of #1668

@NitinKumar004 NitinKumar004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I checked

  • Issue #1668 coverage. Both items are addressed. The unknown-Marker exception is now set per operation through a markerFault argument, and requireUsable names the key ARN in its message.
  • Architecture. The Marker is an opaque wire token that only the server issues, and the driver list methods return the full set. So validating the Marker in server/aws/kms/paginate.go is the right layer, and the provider stays free of paging. ExInvalidMarker goes back into services/kms/driver/errors.go. No driver interface changed, and go generate ./... leaves no diff.
  • Parity against the KMS API reference and the SDK model:
    • InvalidMarkerException is modeled on ListKeys, ListAliases, ListGrants, ListRetirableGrants, ListKeyRotations, ListResourceTags and DescribeCustomKeyStores. It is not modeled on ListKeyPolicies, and the PR maps all of them correctly.
    • MarkerType is min 1, max 1024, with pattern [ -ÿ]*. The length check counts runes, which is correct. Invalid UTF-8 bytes decode to U+FFFD and get ValidationException.
    • The DisabledException text <key ARN> is disabled. matches the captured real response. errors.Is(err, driver.ErrKeyDisabled) still matches, so Secrets Manager's isKeyStateErr is unaffected.
  • Races. The new message reads kd.meta.ARN, which never changes after create. Every caller of requireUsable holds kd.mu (an RLock in the crypto paths, mutateKey for Enable/DisableKeyRotation and RotateKeyOnDemand). The PR adds no check-then-act sequence.
  • Gates on the head:
    • go build ./..., go vet on the kms packages and gofmt are clean.
    • go test -race passes on providers/aws/kms, server/aws/kms, providers/aws/secretsmanager and server/aws/secretsmanager.
    • go mod tidy and go generate ./... leave no diff.
    • golangci-lint, run without --new-from-rev on providers/aws/kms, server/aws/kms and services/kms, reports 0 issues on the head and 0 on the e7c262c baseline.
  • AWS CLI against cloudemu serve:
    • The unknown Marker bm90LWEtbWFya2Vy gives InvalidMarkerException on all 7 modeled operations and ValidationException on ListKeyPolicies.
    • --page-size 1 auto-paging returns every item on ListKeys, ListAliases, ListGrants, ListRetirableGrants and ListResourceTags.
    • A 1025-character Marker gives ValidationException.
    • Encrypt, EnableKeyRotation and Sign on a disabled key return DisabledException arn:aws:kms:...:key/... is disabled. with HTTP 400. Encrypt works again after EnableKey.
  • Terraform hashicorp/aws 5.x with aws_kms_key, aws_kms_alias, aws_kms_grant and the aws_kms_alias data source: apply, plan clean, update, plan clean and destroy all succeed.
  • The PR body and commits contain no attribution lines.

High

None.

Medium

None.

Low

  1. Limit bounds are not enforced on any list operation. This predates the PR. pageWindow treats Limit <= 0 as the default and applies no upper bound. Against serve, ListAliases accepts Limit values of 0, -1, 101 and 5000 without error, and ListResourceTags accepts Limit: 51. The documented ranges are:

    Operation Limit range
    ListKeys, ListKeyPolicies, ListKeyRotations, DescribeCustomKeyStores 1-1000
    ListAliases, ListGrants, ListRetirableGrants 1-100
    ListResourceTags 1-50

    Real KMS rejects a value outside its range with ValidationException. Since this PR already threads a per-operation argument through pageWindow, a per-operation (min, max, default) would fit next to markerFault.

  2. The default page size is wrong for four operations. This also predates the PR. defaultPageLimit = 100 is used everywhere, but ListAliases, ListGrants, ListRetirableGrants and ListResourceTags default to 50. With 60 aliases, ListAliases {} returns all 60 with Truncated: false, where real KMS returns 50 with Truncated: true. SDK paginators still work. A client that reads only the first page sees a different count from real KMS.

  3. A forged Marker that is in the right format is still accepted. The PR body says "a Marker the server never issued now gives InvalidMarkerException", but base64("999") (OTk5) on ListKeys returns an empty page with Truncated: false instead of an error, because pageWindow clamps start to total. Rejecting start > total when a Marker was sent would make the claim true. One caveat: a Marker that pointed exactly at the end of a list that later shrank would then also error.

  4. The order of validation differs from real KMS. The MarkerType constraint is checked after the provider lookup. So a 1025-character Marker with a KeyId that doesn't exist gives NotFoundException, where real KMS fails request validation first with ValidationException. The constraint check needs no state, so it could run before h.kms.ListGrants(...), and likewise in the other handlers that look up a key.

)

// defaultPageLimit is the page size KMS list operations use when Limit is unset.
const defaultPageLimit = 100

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This default is right for ListKeys, ListKeyPolicies, ListKeyRotations and DescribeCustomKeyStores. ListAliases, ListGrants, ListRetirableGrants and ListResourceTags default to 50 in the API reference. With 60 aliases, ListAliases {} here returns 60 with Truncated false, where real KMS returns 50 with Truncated true. This predates the PR, but a per-operation default could go next to markerFault.

start, err = decodeMarker(marker)
// fault picks the exception for a Marker that decodes to no position.
func pageWindow(
marker string, limit int32, total int, fault markerFault,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Limit isn't range-checked. <= 0 falls back to the default and there's no upper bound, so ListAliases accepts Limit 0, -1, 101 and 5000, and ListResourceTags accepts 51. Real KMS returns ValidationException outside 1-1000 (ListKeys, ListKeyPolicies, ListKeyRotations, DescribeCustomKeyStores), 1-100 (ListAliases, ListGrants, ListRetirableGrants) or 1-50 (ListResourceTags). Since this signature already takes a per-operation argument, a small per-operation limits struct would cover both the bounds and the defaults. This predates the PR, so it's a follow-up and doesn't block.

// Marker that passes them but was not issued by this server is
// InvalidMarkerException where the operation models it.
func decodeMarker(marker string, fault markerFault) (int, error) {
if err := validateMarkerConstraint(marker); err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Marker in the right format that this server never issued still passes. OTk5 (base64 of 999) on ListKeys returns an empty page with Truncated false, because pageWindow clamps start to total. If the goal is that an unissued Marker gives InvalidMarkerException, rejecting start > total when a Marker was sent would cover it.

sort.Slice(grants, func(i, j int) bool { return grants[i].GrantID < grants[j].GrantID })

start, end, next, truncated, perr := pageWindow(req.Marker, req.Limit, len(grants))
start, end, next, truncated, perr := pageWindow(req.Marker, req.Limit, len(grants), markerInvalid)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MarkerType constraint check runs after h.kms.ListGrants, so a 1025-character Marker with a KeyId that doesn't exist gives NotFoundException. Real KMS validates the request shape first and returns ValidationException. The constraint check needs no state, so it could run before the provider call here and in the other handlers that look up a key.

…list operations

A Marker the server never issued now gives InvalidMarkerException on
ListKeys, ListAliases, ListGrants, ListRetirableGrants, ListKeyRotations,
ListResourceTags and DescribeCustomKeyStores, which all model it.
ListKeyPolicies does not model it, so it keeps ValidationException.

A Marker longer than 1024 characters or with a character outside
U+0020-U+00FF fails the modeled MarkerType constraint and gives
ValidationException on every operation.
Real KMS answers an operation on a Disabled key with
"<key ARN> is disabled." instead of a generic "key is disabled". The
error is built with driver.WithMessage, so errors.Is still matches
driver.ErrKeyDisabled for Secrets Manager and other callers. Every
operation that returns DisabledException goes through requireUsable,
so they all get the same message.
@Satyam-Trivedi-ZS
Satyam-Trivedi-ZS force-pushed the fix/aws-kms-followups-1668 branch from 3db0606 to f52de43 Compare October 23, 2026 21:15
@Satyam-Trivedi-ZS
Satyam-Trivedi-ZS marked this pull request as ready for review October 23, 2026 21:15
@Satyam-Trivedi-ZS
Satyam-Trivedi-ZS merged commit 689b30b into stackshy:development Oct 23, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants