Skip to content

Reports a refused GCP Decrypt as kms_refused - #122

Merged
johnnyt merged 1 commit into
mainfrom
enc-hpx-gcp-decrypt-failure-class
Sep 30, 2026
Merged

johnnyt merged 1 commit into
mainfrom
enc-hpx-gcp-decrypt-failure-class

Conversation

@johnnyt

@johnnyt johnnyt commented Sep 30, 2026

Copy link
Copy Markdown
Member

Refs: enc-hpx

What changes

Encryptor.Provider.GcpKms now tells a refused Decrypt apart from an outage. A Decrypt that Cloud KMS refuses with HTTP 400 (an AAD mismatch) or 404 (a key or version that is not there) answers {:invalid_key_descriptor, {:kms_refused, status}}. Every other failure keeps {:key_unavailable, selector}: a 403, a 429, a 5xx or any other status, a transport failure, a token the server would not mint, and a malformed response. The term was ruled by the operator, 2026-09-30.

  • lib/encryptor/provider/gcp_kms.ex: unwrap/3 maps its failure through the new private decrypt_failure/2 over the module attribute @refused_statuses [400, 404]. encryption_key/2 and decryption_keys/2 both reach unwrap/3, so both answer the same way. provision/2 and mint/4 are unchanged. A new moduledoc section, "What a failed Decrypt answers", gives the table. It names the reversible 400 of a disabled version and says where an operator finds the status, since Encryptor.Error's message renders only the family.
  • lib/encryptor/provider.ex: the reason doc's {:invalid_key_descriptor, detail} bullet gains a sentence: a provider may also answer it for a permanent refusal by its backing service.
  • docs/adr/0007-gcp-kms-wrap-provider.md: Amendment B, with its own Status line, proposed. It answers open question 4 for 400 and 404 and leaves 403 with ADR-0005 Amendment A's open question A-2. It names the typespec-section sentences it supersedes by line (:731, :736-741).
  • docs/adr/0002-key-providers.md: a dated foot Note on decision 6 that points at the Amendment.
  • changelog.d/enc-hpx.md: under **Breaking**. The README's pre-1.0 banner records every change to the error vocabulary under that heading, and a caller that matched :key_unavailable for a refused row now gets a different term.
  • Tests: test/encryptor/provider/gcp_kms_test.exs and a new fake, Encryptor.GcpKms.DecryptStatusKms, in test/support/gcp_kms_fakes.ex.

Why 403 stays key_unavailable

A revoked IAM binding on a scope's CryptoKey is the provider-level locus of a suspension. ADR-0005 Amendment A (accepted) fixes that both loci "surface the same reason to the caller" (its A3) and gives a suspended scope {:key_unavailable, selector} (its A4). ADR-0010 repeats it ("They compose as A3 says"). This PR edits neither record and neither guide.

Tests over both classes

  • "reports a Decrypt 400 or 404 as a permanent kms_refused on both paths"
  • "keeps key_unavailable for an IAM denial, a throttle and a server error" (403, 429, 500, 503)
  • "names exactly the statuses 400 and 404 in a kms_refused reason": the disclosed set, a literal list checked over a spread of statuses
  • the existing token test now also asserts on decryption_keys/2
  • the existing tests that reach the fake's 400 on a wrong AAD now expect {:kms_refused, 400}, as ruled: "fails closed on a row whose claimed version does not match its blob", "fails closed on a wrapping moved to another scope's row" and "refuses the whole candidate list when one row will not unwrap"

Sabotage, each check reverted from a copy and restored byte-equal:

  • adding 403 to @refused_statuses fails the disclosed-set test and the 403 test
  • removing 404 fails the disclosed-set test and the 400/404 test
  • matching no status fails the wrong-AAD tests above, the 400/404 test and the disclosed-set test

Gate

Full mix quality is green on the committed tree: 676 of 676 tests, 96.5% coverage, Dialyzer, Credo, Docs and Doc links all clean. The commit was made on the tree the gate ran on, with the gate lock held.

Provenance

  • The scope covers encryption_key/2 as well as decryption_keys/2, because both share unwrap/3. Ruled by the operator, 2026-09-30.
  • The operator-facing message is addressed in documentation only: the moduledoc and the Amendment say where the status is. Encryptor.Error's rendered message is unchanged.
  • docs/adr/README.md already reads "accepted (2026-09-13, amended)" for ADR-0007, so it is not touched.

Record check: every cite in the Amendment and the Note was re-read by anchor at 0bf205e. The appended sections sit after each file's last heading, so no cited line moved. git diff origin/main -- docs/adr/ shows zero removed lines.

A Decrypt that Cloud KMS refuses with HTTP 400 or 404 (an AAD
mismatch, a key or version that is not there) now answers
{:invalid_key_descriptor, {:kms_refused, status}} from both
encryption_key/2 and decryption_keys/2, which share unwrap/3. A 403,
a 429, a 5xx, and transport, token and malformed-response failures
still answer {:key_unavailable, selector}, so a provider-level
suspension keeps the answer ADR-0005 Amendment A and ADR-0010 give
it. provision/2 is unchanged.

ADR-0007 Amendment B (proposed) records the term, ruled by the
operator, 2026-09-30, and names the reversible 400 of a disabled
version; a foot Note on ADR-0002 points at it from decision 6.

Refs: enc-hpx
@johnnyt
johnnyt merged commit 9a399a0 into main Sep 30, 2026
1 check passed
@johnnyt
johnnyt deleted the enc-hpx-gcp-decrypt-failure-class branch September 30, 2026 11:43
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.

1 participant