Skip to content

docs(stack-encrypt): key vendor backend design (CIP-5203) - #1645

Open
freshtonic wants to merge 1 commit into
nextfrom
james/cip-5203-key-vendor-backend-design
Open

freshtonic wants to merge 1 commit into
nextfrom
james/cip-5203-key-vendor-backend-design

Conversation

@freshtonic

@freshtonic freshtonic commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Scope (2026-11-12, resubmitted 2026-11-13 on the latest next). The stack is: #1645 (design) → #1646 (stack-kms) → #1647 (stack-kms-vendor) → #1648 (AWS) → #1649 (Vault) → #1650 (GCP) → #1651 (Azure) → #1652 (Go). #1653 (CLI) is a separate PR on #1651. stack-encrypt does not change. Until #1653 merges, a keyset record can be made only from Rust (KeysetRecord::create), so the Rust crates of this stack are not usable by a customer alone.

Records the design for CIP-5203: Stack Encrypt with AWS KMS, Azure Managed HSM, HashiCorp Vault Transit, Google Cloud KMS and a static key, as alternatives to ZeroKMS.

This PR changes docs only. It changes no code.

  • ADR-0014: derived data keys for key vendor backends. A vendor-wrapped root key is cached for a TTL, and each data key and the index key are derived from it. Vendor calls are O(keysets), not O(values). Search is required on every backend.
  • ADR-0015: hand-written vendor REST clients, used by native Rust and the Go WASI guest. The guest signs requests with credentials that the host supplies. The vendor traffic, which holds the root key, crosses to the host.
  • CONTEXT.md: widens the Keyset term so it is not ZeroKMS-only. Adds these terms: key backend, key vendor backend, keyset record, derived data key, root key, settle.
  • Decision log: docs/plans/2026-11-05-key-vendor-backends-decisions.md.

Implementation is tracked in the sub-issues of CIP-5203 (CIP-5304 to CIP-5313). The follow-ups are CIP-5314 (TypeScript) and CIP-5315 (migration from a vendor to ZeroKMS).

Replaces #1624, which was closed. The review on #1624 is addressed in this version.

ADR-0013 (derived data keys for key vendor backends) and ADR-0014
(hand-written vendor clients signed in the guest), the domain terms, and
the decision log.
@freshtonic
freshtonic requested a review from a team as a code owner November 13, 2026 11:36
@changeset-bot

changeset-bot Bot commented Nov 13, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 34f6ef2

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI; Enforced overrides: Organization
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d5c3347e-bd19-4836-b12a-49cfc43a9191

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review

@freshtonic
freshtonic added this pull request to stack #1654 November 13, 2026 11:38

@cipherstash-agent cipherstash-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review attestation

Reviewed 34f6ef2 · posted from Claude Code (scheduled routine) · claude-opus-5-5 · effort medium
Sources: code-review · claude-opus-5-5; rust-security · claude-opus-5-5
Skills: code-review, rust-security, review-cross-check

Findings (10): 4 major, 6 minor — 10 inline, 0 below.

Docs-only PR: findings are places where the ADRs, glossary and decision log contradict each other or the tree. Every finding was verified at 34f6ef2 and cross-checked against existing feedback (none carried findings). rust-security was applied to the recorded crypto design, not to code. One finding was refuted and dropped: the claim that ADR-0015's one-crate-per-vendor reason is false for every SDK.


Generated by Claude Code

Comment on lines +1726 to +1728
- **Open:** Vault Transit has the same rule. `KeyDerivation::Detect` refuses
a Transit key that is not an AEAD, so it refuses an RSA Transit key. The
explicit `KeyDerivation` settings read no key, so they do not refuse one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

major An RSA Transit key configured KeyDerivation::NotDerived or Derived is never type-checked, so anyone with its public key (GET transit/keys/<name>) can RSA-OAEP-encrypt a bound key holding a root key they chose and write it into a keyset record — the same forgery that removed Azure Key Vault. This is recorded only here: ADR-0014's bound-key consequence acts on the public-key rule for Azure alone, the "Summary for review" Open list omits it, and the CIP-5310 note at :448-452 still calls RSA-OAEP a sound bound-key wrap. Refuse non-AEAD Transit key types under every KeyDerivation setting, or at minimum record the hole in ADR-0014 and the summary.

Source: code-review · claude-opus-5-5


Generated by Claude Code

Comment on lines +436 to +438
an unauthenticated label in the record. Under a key router a record's name
is looked up first, so selecting a keyset by name is only as trustworthy as
the records the process loads. Select by id where that matters.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

major A record name wins over a ZeroKMS keyset of the same name under KeyRouter, and the name is not bound to the wrapped key, so whoever can edit the record store can rename a valid vendor record to tenant-a and silently redirect new Keyset::name("tenant-a") seals from ZeroKMS to that vendor keyset. ADR-0002 (:45, :56, :109-112) still says ZeroKMS is the authority on what a name means, and ADR-0014 does not record this rule or its risk. Amend ADR-0002 or add an ADR-0014 consequence — and decide whether the router should refuse a record name that collides with a ZeroKMS keyset rather than letting it win.

Source: code-review · claude-opus-5-5


Generated by Claude Code

Comment on lines +79 to +81
- A vendor with no associated data (Azure Managed HSM, a Vault Transit
key that is not derived) gets a bound key: the key with a digest of the
associated data beside it, which the client checks after the unwrap.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

major This says only "a Vault Transit key that is not derived" gets a bound key, but CIP-5310 decided the Vault client always wraps a 65-byte bound key and also sends context to derived keys (log :462-471), rejecting exactly this split. An implementation built from this ADR (which :39-41 says must match byte for byte) sends a raw 32-byte root key to a derived key and cannot open Rust-written records, nor Rust its. Say the Vault client always wraps a bound key, and sends the associated data as context to a derived key.

Source: code-review · claude-opus-5-5


Generated by Claude Code

Comment on lines +1579 to +1581
- **CIP-5308 (Revisit, security):** the GCP service account login uses the
`rsa` crate (RUSTSEC-2023-0071); a security reviewer must sign it off
before the crate is armed; revisit when `rsa` 0.10 is stable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

major Stale security item: since 2026-11-13 native RS256 uses aws-lc-rs and no native or guest build ships rsa (:709-715, :813-815), so the only remaining RUSTSEC-2023-0071 exposure is a wasm32 build with service-account on, which this bullet does not say. The Azure client-certificate deferral (:950-951, :1019, :1592) also still cites a missing constant-time RSA signer that aws-lc-rs now provides natively. Rewrite this bullet to the wasm32 + service-account case and restate the Azure reason.

Source: code-review · claude-opus-5-5


Generated by Claude Code

Comment on lines +431 to +436
A name is a *lookup*, not an identity, so a binding is trusted only within
a window, a keyset holds at most one at a time, and no binding outlives the
id it names. Two places answer the lookup. ZeroKMS answers it for its own
keysets, and allows renames. For a keyset in a key vendor backend, the
loaded keyset records answer it, locally and with no window: the name is
an unauthenticated label in the record. Under a key router a record's name

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor "With no window" contradicts the first sentence: stack-encrypt is unchanged, so the cipher caches a vendor name binding for DEFAULT_NAME_TTL like any other; it is the record set that cannot change within a process. Pending (:348-352, "ZeroKMS key requests") and Freshness window ("asks ZeroKMS again") were not widened and still contradict Settle and Key backend. Say the cipher's window applies to every binding, and use "the key backend" in Pending and Freshness window.

Source: code-review · claude-opus-5-5


Generated by Claude Code

Comment on lines +98 to +102
We own the request signing code for four vendors, and its tests. Recorded
HTTP fixtures cover every vendor. LocalStack and `vault server -dev` run on
every PR. Live Google Cloud and Azure tests run in an opt-in, credentialed
workflow. The Azure one has not run yet: we have no live Managed HSM, so it
stays off until an instance is available.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor Overstates test coverage: only Vault's fixtures were recorded — AWS responses, and all GCP and Azure fixtures, were written by hand (log :354-358, :735-736, :958-959); the LocalStack and Vault jobs live in tests-crates.yml, which runs only on its listed paths, not every PR; and the Google Cloud live job has not run either (log :777-779), not just Azure's. State what the log records.

Source: code-review · claude-opus-5-5


Generated by Claude Code

Comment on lines +15 to +21
- **Decision:** the work is GitHub stack 1633 on `next`, bottom up: #1624
(design: ADRs, `CONTEXT.md` and this log), #1632 (CIP-5304,
`stack-kms`), #1625 (CIP-5306 and CIP-5313, `stack-kms-vendor`), #1626
(CIP-5307, AWS), #1627 (CIP-5310, Vault), #1628 (CIP-5308, GCP), #1629
(CIP-5309, Azure) and #1630 (CIP-5311, Go). #1631 (CIP-5312, `stash
keyset`) is a separate PR on #1629, outside the stack. **Reason:** the
code layers cite the ADRs and the glossary terms, so the design is the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor "Rewritten 2026-11-13" but still names "stack 1633" and the closed #1624, #1625–#1632 and #1631; the live stack in the PR body is #1645–#1652 plus #1653, and this PR is missing from its own log. Renumber.

Source: code-review · claude-opus-5-5


Generated by Claude Code

Comment on lines +1124 to +1128
- **Decision:** the deterministic test build keeps the vendor clients, so it
now imports `transport_send` and `vendor_credential_get` (not
`token_get`). Its config is the bare 32-byte seed, as before, or an object
`{"seed", key_vendor, keyset_records, ...}`. The mise import gates of all
four builds now require `vendor_credential_get`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor Never marked superseded: the deterministic build still "imports transport_send and vendor_credential_get" (both gone, :1643). Same for azure-key-vault as a stable vendor name (:115), AzureKeyVault (:1155), AzureKeyVaultError (:1131), DefaultAzureKeyVault::from_env (:1305) and the Azure Key Vault RSA wrap at :128 and :591-592. Add inline superseded notes, as the CIP-5309 section does.

Source: code-review · claude-opus-5-5


Generated by Claude Code

Comment on lines +1036 to +1037
| deterministic test build | n/a | 1,657,639 B | |
| deterministic test build, EQL | n/a | 1,726,501 B | |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor These two rows follow a blank line and a paragraph, so GFM ends the table and renders them as literal pipe text. Move them up after :1032 and put the parenthetical after the table.

Source: code-review · claude-opus-5-5


Generated by Claude Code

Comment on lines +190 to +193
- **Decision:** the static key backend (`StaticKeyVendor`) wraps with
AES-256-GCM under a random 96-bit nonce: `version(1)=1 ‖ nonce(12) ‖
ciphertext ‖ tag`. It holds several keys by reference, for rotation. It
uses `aes-gcm` 0.10.3, already in the lockfile.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor The static-key wrap uses RustCrypto aes-gcm with no recorded reason, though vitaminc-encrypt 0.9.0 (AES-256-GCM, aws-lc-rs native / RustCrypto on wasm32) is already a workspace dependency and already in the guest's Cargo.lock; the HKDF entry at :157-166 likewise gives no reason for hkdf/sha2 (vitaminc has no HKDF and aws-lc-rs does not build for wasm32 — say so). Record why vitaminc was passed over, or wrap with vitaminc-encrypt.

Source: rust-security · claude-opus-5-5


Generated by Claude Code

This branch has not been deployed

No deployments
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