Repository navigation
feat(kms)!: derive each value's key from a cached authority key, so a batch binds and costs one backend call (CIP-5062) - #510
Open
freshtonic wants to merge 3 commits into
Conversation
freshtonic
added this pull request to stack #356
October 21, 2026 05:38
freshtonic
marked this pull request as ready for review
October 21, 2026 05:39
Contributor
✅ No CRAP threshold violations1645 function(s) analyzed · threshold 30 |
freshtonic
force-pushed
the
claude/cip-5062-authority-keys
branch
from
October 21, 2026 05:51
876a0a8 to
10958c2
Compare
Contributor
🧬 Mutation testing (cargo-mutants,
|
| caught | missed | unviable | timeout |
|---|---|---|---|
| 37 | 0 | 47 | 0 |
✅ Every mutant in the changed lines was caught by a test.
freshtonic
force-pushed
the
claude/cip-5062-authority-keys
branch
2 times, most recently
from
October 21, 2026 10:20
a524bd2 to
cbf2147
Compare
…ey per value Nothing implemented `KeyIsolation::Pooled` after CIP-4981, so the enum had one reachable value. ADR 0007 says a one-key-per-batch source cannot bind, and that the round-trip saving belongs on its own capability, not on a third variant. An enum that always says `PerValue` tells a reader nothing, so the enum and every `ISOLATION` constant go. No crate in this workspace, cipherstash/stack or cipherstash-suite reads the constant. `BatchGenerateDataKey` now states the guarantee in its docs: one distinct key per binding. BREAKING CHANGE: `KeyIsolation`, `KeyProvider::ISOLATION` and `BatchGenerateDataKey::ISOLATION` are removed. Remove the constant from custom provider impls. Every source in the crate delivers one key per value.
…per cache refresh and still binds each value
ADR 0007 keeps the round-trip saving that clone-one-key pooling gave, without
sharing a data key. `AuthorityKeySource<T>` (feature `authority-keys`) caches
an authority key that the inner provider unwraps, and derives each value's
own data key from it locally:
data key = HKDF-SHA256(authority key, 16-byte salt, info(version, N, binding))
KeyId = 0x01 || u32be(version) || salt
The binding is a KDF input, so every value is bound. A wrong binding derives
a different key, and the consumer's AEAD detects it. ADR 0008 records the
wire format, with known-answer vectors from an independent HKDF.
An authority key is a 32-byte data key of any existing source, minted once
per version by `AuthorityKeySource::provision` under a fixed binding that
names the version. The deployment stores each wrapped id in an
`AuthorityKeys` (version -> wrapped id, plus the current version), as it
stores the index key id of `FixedIndexKeySource`. Rotation adds a version
and makes it current; old versions stay for retrieval, and a version the
deployment does not hold is a typed error.
The `DerivesFromAuthorityKey` capability trait carries the
`AuthorityKeyCachePolicy` (TTL and entry capacity) and `purge_expired`.
The cached authority key is `Protected` and zeroizes when it leaves the
cache. Concurrent misses share one fetch. An empty batch makes no backend
call. `CUSTODY` is `BackendHeld`; `RECONSTRUCTION` is the inner provider's.
The source, `CachingKeyProvider`, ADR 0007, `CONTEXT.md` and the README now
state the auditing and in-memory stance: per-value retrieval is not
per-value authorization, and the TTL is a security control.
HKDF is built on the workspace's existing `hmac` 0.13 and `sha2` 0.11,
not on the `hkdf` crate, so the PRK and each expand block are written
into `Protected` memory and wiped on drop. The RFC 5869 SHA-256 vectors
check it. No new crate. The coverage feature lists gain `authority-keys`, so the
CRAP gate does not score the new module as uncovered.
…DF key The mutation gate found three gaps in the authority-key tests. No test read `AuthorityKeyVersion::get` or its `Display`. No test derived a key longer than 287 bytes, so the 255-block limit could change from a product to a sum unseen. One test now reads both accessors. Another derives an 8160-byte key, which runs the HKDF block counter to 255, and checks that the long output starts with the short one under the same `info`. Refs: CIP-5062
freshtonic
force-pushed
the
claude/cip-5062-authority-keys
branch
from
October 22, 2026 01:44
cbf2147 to
c515c77
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR builds the design in ADR 0007 (#491): a pooled source shares the authority to mint keys, never a key. One backend call fetches an authority key, which is cached in memory. Each value then gets its own data key, which is derived locally from the authority key, a fresh 16-byte salt and the value's binding. A batch costs one backend call per cache refresh, and every value still has its own key and its own binding. #508 removed the clone-one-key pooled sources, and this replaces them.
There are two commits. Each one passes clippy with
-D warningson its own.refactor(kms)!: removeKeyIsolation. WithPooledgone, the enum had one value. So the enum,KeyProvider::ISOLATION,BatchGenerateDataKey::ISOLATIONand every assertion are removed. There are no readers in this workspace, incipherstash/stackor incipherstash-suite. ADR 0003 and the dated design survey still name it, because they are historical records.feat(kms): addAuthorityKeySource.API
authority-keysenablescaching,vitaminc-random,hmacandsha2, withzeroizeon both. It adds no new crate. It is off by default. The default, traits-only and wasm32 builds do not change.DerivesFromAuthorityKey, the capability marker trait, withcache_policy()andpurge_expired().AuthorityKeyCachePolicy { max_entries, ttl }. It has noDefault, because the TTL is a security control.AuthorityKeyVersion.AuthorityKeys::new(current, versions). It refuses a duplicate version, and a current version that the map does not hold.AuthorityKeySource<T>:newandprovision.provisionmints the wrapped authority key under the correct binding, so callers never build that binding themselves.KeyProvider<N>,DerivesFromAuthorityKeyandIndexKeyProvider(the last delegates to the inner provider).CUSTODY = BackendHeld, because aKeyIdcarries no key material.RECONSTRUCTION = T::RECONSTRUCTION.AuthorityKeyError.version → wrapped KeyId, the same as theFixedIndexKeySourceid. To rotate, the deployment adds a version, makes it current, and builds the source again. Old versions stay for retrieval.try_get_with).Wire format (new ADR 0008)
"vitaminc-kms/authority-key/v1" || u32be(version). The binding names the version, so the backend refuses a wrapped key that is stored under the wrong version.SafeRandand IKM = the authority key. HKDF is built here onhmac, not on thehkdfcrate. That crate drops the PRK and each expand block without wiping them, and for a 32-byte key the one expand block is the data key. Here each value is written straight intoProtectedmemory, which zeroizes on drop. The RFC 5869 SHA-256 vectors (cases 1 to 3) check the implementation.info = "vitaminc-kms/authority-key/v1/data-key" || u32be(version) || u16be(N) || u64be(len(binding)) || binding.0x01 || u32be(version) || salt, exactly 21 bytes. A malformed id or an unknown version gives a typed error for the whole batch before any backend call.Docs
CONTEXT.mdgets the authority-key term and the three key tiers.CachingKeyProviderdocs now restate the auditing and in-memory stance. Per-value retrieval is no longer per-value authorization. The cached authority key isProtectedand is wiped on eviction and expiry.crap.ymlandmise.tomlgetvitaminc-kms/authority-keys.Closes CIP-5062.
For the reviewer
Protected, and the HMAC states wipe on drop. Only SHA-256's own working memory is out of reach, the same as for every HMAC in the workspace.AuthorityKeyError::DataKeyLengthis removed: the compile-time check on N means derivation cannot fail.Bindingdocs,CONTEXT.mdand ADR 0007 say this.Test plan
purge_expired, rotation, an unknown version and five malformed idscargo test -p vitaminc-kms --all-features --locked: 309 tests and 8 doctests pass. With default features: 58 tests and 4 doctests.vault,azure,gcpandauthority-keyson its own.-D warningson each commit,fmt --check, the workspace check with all features,wasm32-wasip1, andcargo docwith-D warnings. No ADR number is used twice, and every commit is signed.🤖 Generated with Claude Code