Skip to content

docs(adr): pool the authority to mint keys, never a data key (CIP-5062) - #491

Open
freshtonic wants to merge 1 commit into
claude/cip-4980-key-handle-kindfrom
claude/cip-5062-pooling-adr
Open

freshtonic wants to merge 1 commit into
claude/cip-4980-key-handle-kindfrom
claude/cip-5062-pooling-adr

Conversation

@freshtonic

Copy link
Copy Markdown
Contributor

Summary

First deliverable for CIP-5062. Stacked on #451.

Docs only — one ADR, no code. It records how pooling survives mandatory
binding, because as built it does not.

pooled_key_generate mints one data key and clones it across the batch. That
is the sole reason PooledDataKeySource and the three vendor pooled sources
are Unbound: one key cannot carry a batch's several bindings. So CIP-4981
deletes that form of pooling, and the round-trip saving goes with it. The
saving is worth keeping, and this is how.

What it decides

Share the authority to mint keys, never a key. One backend call fetches an
authority key the adapter caches; each value then gets its own data key,
derived locally from that key and a fresh per-value salt, with the binding as
an input to the derivation:

data key = KDF(authority key, salt, binding)
KeyId    = (authority key version, salt)

Per-value isolation is intact, binding is intact — in fact stronger, since a
wrong binding cannot derive the key rather than being refused by the backend —
and the backend is called once per cache refresh instead of once per value.

It follows AWS's Hierarchical keyring with one tier fewer. AWS derives a
wrapping key and uses it to encrypt a separately generated data key, which an
ESDK data key needs because several keyrings wrap the same one. A data key
source returns material to one caller, so we derive the data key itself rather
than adding an indirection nothing uses and a second piece of key material in
memory.

Two things it argues rather than assumes

KeyIsolation is the wrong axis to hang strategies off. A source built
this way reports PerValue; what differs is round trips and what sits in
memory. Today's constant bundles keys-per-batch, round-trips and
can-it-bind together only because clone-one-key makes them move together. So
the capability belongs on its own trait, and Pooled should go once nothing
implements it.

It inverts CachingKeyProvider's documented stance, so the ADR restates it
rather than dropping it quietly. That type says skipping per-retrieval
auditing must be opt-in and never a default; a cached authority key makes it
the normal path, authorizing once per refresh. And plaintext key material now
lives for a TTL instead of fetch-use-drop, which makes the TTL a security
control rather than a memory bound — the opposite of how CachingKeyProvider
documents its own limits.

Left open on purpose

Where the authority-key store lives, how versioning and rotation work, and
whether an authority key is per backend key, per tenant or per keyset. Those
want the implementation to answer them honestly. The CONTEXT.md term lands
with the code too, so the glossary keeps describing what the crate does today.

Test plan

  • No code changed; nothing to run.
  • ADR numbering checked — 0007 is next, no duplicates, and main has only 0001.
  • Every claim checked against this branch: the four pooled types exist as named, pooled_key_generate exists, KeyIsolation is {PerValue, Pooled}, KeyCustody is {ClientHeld, BackendHeld}.
  • The CachingKeyProvider passage is quoted verbatim from provider/caching.rs.
  • The AWS mechanism is quoted from the Hierarchical keyring docs, not from memory.

🤖 Generated with Claude Code

`pooled_key_generate` mints one data key and clones it across the batch. That
is the only reason the pooled sources are `Unbound` — one key cannot carry a
batch's several bindings — so making binding mandatory (CIP-4981) deletes that
form of pooling and the round-trip saving with it.

This records how to keep the saving. One backend call fetches an authority key
the adapter caches; each value then gets its own data key, derived locally from
that key and a per-value salt, with the binding as an input to the derivation.
Nothing is shared between values except the authority to mint keys. It follows
AWS's Hierarchical keyring, with one tier fewer: AWS derives a wrapping key and
uses it to encrypt a separately generated data key, which an ESDK data key
needs because several keyrings wrap the same one. A data key source returns
material to one caller, so we derive the data key itself.

Two consequences the ADR argues rather than assumes. `KeyIsolation` is the
wrong axis — such a source is `PerValue`, and what differs is round trips and
what sits in memory, so the capability belongs on its own trait and `Pooled`
should go once nothing implements it. And `CachingKeyProvider`'s stance that
skipping per-retrieval auditing must be opt-in and never a default is inverted
here, so it is restated instead of dropped: a cached authority key means the
backend authorizes once per refresh, and its TTL becomes a security control
rather than a memory one.

Left open on purpose, because the implementation should answer them: where the
authority-key store lives, how rotation works, and whether an authority key is
per backend key, per tenant or per keyset. The glossary term lands with the
code, so CONTEXT.md keeps describing what the crate does today.

Refs CIP-5062
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