Repository navigation
docs(stack-encrypt): key vendor backend design (CIP-5203) - #1624
freshtonic wants to merge 1 commit into
Conversation
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.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
|
cipherstash-agent
left a comment
There was a problem hiding this comment.
Review attestation
Reviewed 9a17fe4 · 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, review-post
Blocking (1)
docs/plans/2026-11-05-key-vendor-backends-decisions.md:849Azure RSA-OAEP wrap lets anyone with the public key forge a record's root key
Findings (10): 1 critical, 9 minor — 10 inline, 0 below.
This PR is docs-only; findings were checked against the design text at 9a17fe4 (and next where the docs cite code). No existing review feedback carried findings, so none are duplicates.
Generated by Claude Code
| versioned one pins wraps only. Size: 115 bytes for A256KW, 291 bytes for | ||
| RSA-OAEP-256 with a 2048-bit key. **Alternative:** store the full versioned | ||
| `kid` (60 to 100 bytes more per record, and it repeats the key reference). | ||
| - **Decision (no AAD):** the client **always** wraps the bound key of |
There was a problem hiding this comment.
blocking · critical RSA-OAEP-256 is public-key encryption, so the keyless SHA-256(frame[bound-key/v1, aad]) in the bound key gives no authenticity: anyone holding the Key Vault public key and write access to a record can encrypt 1 ‖ digest ‖ K_attacker ‖ 0×7 locally, and unwrapkey plus the digest check accept it as the root key, with no vendor permission or audit event. Non-malleability (the argument at lines 438-440 and here) only stops edits to an existing ciphertext; the same gap affects the guidance at line 120-122 and "the vendor key itself is the binding" at line 137. Refuse RSA/RSA-HSM keys (A256KW on oct-HSM only) or authenticate the binding with key material Azure holds.
Source: rust-security · claude-opus-5-5
Generated by Claude Code
| router already forwards every provided method. | ||
| - **Decision (additive, stack-kms-vendor):** `KeyRouter::with_vendor_default` | ||
| sends "no keyset named" to the vendor. A router test covers it. | ||
| - **Decision:** a new status, `STATUS_NO_ZEROKMS = 29` (`LAST_STATUS` moves), |
There was a problem hiding this comment.
minor STATUS_NO_ZEROKMS = 29 and the vendor_credential_get import (lines 1024-1030, 1092-1095) are superseded by the rebase section (line 1616: status is 30 because next gave 29 to STATUS_NEEDS_IO; the import is gone), but these entries are not marked. A reader who takes 29 from this entry collides with STATUS_NEEDS_IO (packages/stack-guest-abi/src/status.rs:129 on next); mark lines 1024, 1092, 1196 and 1463 as superseded.
Source: code-review · claude-opus-5-5
Generated by Claude Code
| (`with_scope` changes it). **Alternative:** a self-signed JWT sent | ||
| directly as the bearer token (no exchange). **Reason:** the ticket names | ||
| the exchange, and it works for every key the token endpoint accepts. | ||
| - **Decision (RSA crate):** RS256 uses the `rsa` crate 0.9.10 (RustCrypto, |
There was a problem hiding this comment.
minor rsa was chosen because ring/aws-lc-rs do not build for wasm32, but line 1051-1055 makes RS256 the default service-account feature that the guest and the napi binding build without, so rsa (RUSTSEC-2023-0071) only ships on native targets where aws-lc-rs builds and is preferred. Use aws-lc-rs for RS256 on native, or restate the reason.
Source: rust-security · claude-opus-5-5
Generated by Claude Code
| it runs on native Rust and in the WASI guest alike. | ||
| - **Decision:** the vendor transport is `VendorTransport { send(VendorRequest) | ||
| -> VendorResponse, sleep(Duration) }`, futures bounded by `MaybeSend`, the | ||
| shape of `ZeroKMSConnection`, plus `classify_status` (401/403 |
There was a problem hiding this comment.
minor The shared classify_status default maps other 4xx to InvalidCiphertext, while AWS, Vault, GCP and Azure each override it to AccessDenied ("the CIP-5307 rule", lines 298, 486, 652, 870). Make AccessDenied the shared default so a new vendor, or an AWS 4xx with no code, does not report a refused request as damaged ciphertext.
Source: code-review · claude-opus-5-5
Generated by Claude Code
| the alternatives and the reason. | ||
|
|
||
| ## Stack structure | ||
|
|
There was a problem hiding this comment.
minor "Stack structure" still says one stack on main with design PR #1521 rebased onto origin/main; this PR is #1624 on next (line 1593), and the layer list still includes the CLI and CIP-5313. Rewrite it to the chain in the PR body or mark it superseded.
Source: code-review · claude-opus-5-5
Generated by Claude Code
|
|
||
| We therefore use **derived data keys**. The keyset record holds a root key | ||
| wrapped by the vendor key. The vendor key source unwraps it once and caches | ||
| it for a TTL. Each data key is HKDF-SHA256(root key, salt = iv, info = |
There was a problem hiding this comment.
minor The data-key formula here is unframed, unlabelled concatenation (info = keyset id ‖ vendor ‖ mode ‖ descriptor), but the pinned derivation (decision log lines 148-157) is frame["stack-kms-vendor/derived-data-key/v1", …], and the index key uses salt = none with its own label. A reimplementation from the ADR (e.g. the TypeScript follow-up) derives keys that cannot open Rust/Go values; state the framed, labelled formulas here.
Source: rust-security · claude-opus-5-5
Generated by Claude Code
| wrapped by the vendor key. The vendor key source unwraps it once and caches | ||
| it for a TTL. Each data key is HKDF-SHA256(root key, salt = iv, info = | ||
| keyset id ‖ vendor ‖ mode ‖ descriptor), and the index key is derived from | ||
| the same root key under a separate label. Vendor calls are O(keysets), not |
There was a problem hiding this comment.
minor "Vendor calls are O(keysets)" holds only per TTL window: with WithRootKeyTTL(0) the log measures one unwrap per Encrypt and per Decrypt (lines 1213-1226). Say that a zero or short TTL costs one vendor call per operation, so an operator tightening revocation is not surprised by vendor throttling.
Source: code-review · claude-opus-5-5
Generated by Claude Code
| code. Each vendor is a separate crate (`stack-kms-aws` and so on), so that no | ||
| user compiles all four. | ||
|
|
||
| The guest signs each vendor request itself (SigV4, the Google OAuth JWT, the |
There was a problem hiding this comment.
minor The body still says the guest signs SigV4, the Google OAuth JWT, the Azure AD token and the Vault token; the 2026-11-06 amendment (line 45: "the guest still does no RSA") and the decision log (line 1405: "The guest signs SigV4 only") say otherwise, and neither amendment withdraws this sentence. Amend it to: the guest signs SigV4 only; the host supplies ready Google, Azure and Vault tokens.
Source: code-review · claude-opus-5-5
Generated by Claude Code
| > say this. | ||
|
|
||
| The Go SDK reaches Stack Encrypt through a WASI guest. The guest builds the | ||
| ZeroKMS requests and derives the keys. The Go host is only an HTTP pipe |
There was a problem hiding this comment.
minor ADR-0005 does not state that "no key material crosses back to the host" — it covers the credential guest's sandbox and concedes the client key passes through host memory. Cite where the property is actually stated (languages/golang/encrypt/doc.go) or state it as this ADR's premise.
Source: code-review · claude-opus-5-5
Generated by Claude Code
| _Avoid_: vendor KMS, third-party KMS, BYOK (that is ZeroKMS rooted in a | ||
| customer's KMS, not ZeroKMS replaced by it) | ||
|
|
||
| **Keyset record**: |
There was a problem hiding this comment.
minor The Keyset record entry omits the record's optional name, which is unauthenticated and routes ahead of ZeroKMS under KeyRouter (decision log lines 125, 136, 171); "Name binding" (line 426) still says a name is a lookup ZeroKMS answers. Add the name to this entry and widen Name binding to cover names resolved locally from records.
Source: code-review · claude-opus-5-5
Generated by Claude Code
Records the design for CIP-5203: Stack Encrypt with AWS KMS, Azure Key Vault, HashiCorp Vault Transit, Google Cloud KMS and a static key, as alternatives to ZeroKMS.
This PR changes docs only. It changes no code.
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).