diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 178458c..8768b45 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -7,9 +7,9 @@ last_updated: 2026-06-19 ## Current State -**Pre-implementation.** The project has completed a pivot from a three-layer model to an ALPN-as-service model. The greenfield workspace contains only `alknet-vault` (stable — implementation exists) and research/reference material. Foundational ADRs (001–020) are in place, including the BiStream type definition (ADR-007), vault integration (ADR-008), ALPN router/endpoint (ADR-010), AuthContext structure (ADR-011), call protocol stream model (ADR-012), Rust as canonical implementation language (ADR-013), secret material flow with capability injection (ADR-014), privilege model with authority context (ADR-015), abort cascade for nested calls (ADR-016), call protocol client and adapter contract (ADR-017), vault standalone crate (ADR-018), vault assembly-layer-only access (ADR-019), and HD derivation for encryption keys (ADR-020). The alknet-core, alknet-call, and alknet-vault crate specs are in draft. +**Pre-implementation.** The project has completed a pivot from a three-layer model to an ALPN-as-service model. The greenfield workspace contains only `alknet-vault` (stable — implementation exists) and research/reference material. Foundational ADRs (001–021) are in place, including the BiStream type definition (ADR-007), vault integration (ADR-008), ALPN router/endpoint (ADR-010), AuthContext structure (ADR-011), call protocol stream model (ADR-012), Rust as canonical implementation language (ADR-013), secret material flow with capability injection (ADR-014), privilege model with authority context (ADR-015), abort cascade for nested calls (ADR-016), call protocol client and adapter contract (ADR-017), vault standalone crate (ADR-018), vault assembly-layer-only access (ADR-019), HD derivation for encryption keys (ADR-020), and key rotation via version-indexed paths (ADR-021). The alknet-core, alknet-call, and alknet-vault crate specs are in draft. -**Next step**: Review the vault spec documents, then begin implementation. All open questions for the core and call crates are resolved; the vault crate has two open/deferred OQs (OQ-21, OQ-22) that do not block implementation. +**Next step**: Review the vault spec documents, then begin implementation. All open questions for the core and call crates are resolved; the vault crate has one deferred OQ (OQ-21, remote vault administration) that does not block implementation. ## Architecture Documents @@ -55,6 +55,7 @@ last_updated: 2026-06-19 | [018](decisions/018-vault-standalone-crate.md) | Vault as Standalone Crate | Accepted | | [019](decisions/019-vault-assembly-layer-only.md) | Vault Assembly-Layer-Only Access | Accepted | | [020](decisions/020-hd-derivation-for-encryption-keys.md) | HD Derivation for Encryption Keys | Accepted | +| [021](decisions/021-key-rotation-via-version-indexed-paths.md) | Key Rotation via Version-Indexed Paths | Accepted | ## Open Questions @@ -81,9 +82,7 @@ See [open-questions.md](open-questions.md) for the full tracker. - **OQ-14**: Batch operation semantics — multiple correlated `call.requested` events is the correct protocol design, not a simplification - **OQ-19**: Session-scoped registries — agent-written operations via `OperationEnv` trait layering; protocol doesn't need changes; `OperationEnv` must remain a trait - **OQ-20**: Encryption key derivation — HD derivation from BIP39 seed, not PBKDF2; salt field unused in v2 (wire-format compat) (ADR-020) - -**Open (low priority, does not block implementation):** -- **OQ-22**: Key rotation mechanism — key versioning is in place; the rotation workflow is not specced. Two-way door. +- **OQ-22**: Key rotation — version-indexed derivation paths; `rotate` method re-encrypts (ADR-021) **Deferred (not active):** - **OQ-09**: WASM target boundaries — design constraint, not deliverable diff --git a/docs/architecture/crates/vault/README.md b/docs/architecture/crates/vault/README.md index c839067..0e9e502 100644 --- a/docs/architecture/crates/vault/README.md +++ b/docs/architecture/crates/vault/README.md @@ -44,6 +44,7 @@ cross the network. | [018](../../decisions/018-vault-standalone-crate.md) | Vault as Standalone Crate | Zero alknet crate dependencies | | [019](../../decisions/019-vault-assembly-layer-only.md) | Vault Assembly-Layer-Only Access | The assembly layer is the sole caller | | [020](../../decisions/020-hd-derivation-for-encryption-keys.md) | HD Derivation for Encryption Keys | SLIP-0010 derivation, not PBKDF2; salt unused in v2 | +| [021](../../decisions/021-key-rotation-via-version-indexed-paths.md) | Key Rotation via Version-Indexed Paths | Version-indexed paths; `rotate` re-encrypts | ## Relevant Open Questions @@ -51,7 +52,7 @@ cross the network. |----|-------|--------|-----------| | OQ-20 | Encryption key derivation | resolved (ADR-020) | HD derivation from seed; salt field unused in v2 | | OQ-21 | Remote vault administration | deferred | Network unlock not supported; needs ADR if ever needed | -| OQ-22 | Key rotation mechanism | open | Key versioning is in place; rotation workflow is not specced | +| OQ-22 | Key rotation mechanism | resolved (ADR-021) | Version-indexed paths; `rotate` method | ## Key Design Principles diff --git a/docs/architecture/crates/vault/encryption.md b/docs/architecture/crates/vault/encryption.md index a44f427..a13a386 100644 --- a/docs/architecture/crates/vault/encryption.md +++ b/docs/architecture/crates/vault/encryption.md @@ -136,7 +136,7 @@ pub fn decrypt(encrypted: &EncryptedData, key: &EncryptionKey) -> Result Result; +``` + +Rotation decrypts with the old version's key and re-encrypts with the new +version's key. No new mnemonic needed — the same seed produces all version +keys via different paths. See ADR-021 for the full mechanism. + +**The current source uses `CURRENT_KEY_VERSION = 1` with HD derivation and +does not implement version-indexed paths or `rotate`.** These are drift +items to be corrected during implementation sync. See ADR-020 (version +bump to 2) and ADR-021 (rotation mechanism). ## Errors @@ -201,6 +217,7 @@ implementer should not expect this variant to fire in v2. | HD derivation, not PBKDF2 | [ADR-020](../../decisions/020-hd-derivation-for-encryption-keys.md) | Seed-derived key; no password; deterministic | | Salt unused in v2 (wire-format compat) | [ADR-020](../../decisions/020-hd-derivation-for-encryption-keys.md) | Kept for TS compat; not used in key derivation | | Key derived at `m/74'/2'/0'/0'` | — | Dedicated account for encryption keys | +| Version-indexed paths for rotation | [ADR-021](../../decisions/021-key-rotation-via-version-indexed-paths.md) | `m/74'/2'/0'/{version-2}'` | | Key versioning (v1=TS PBKDF2, v2=vault HD) | [ADR-020](../../decisions/020-hd-derivation-for-encryption-keys.md) | Distinguishes derivation methods | | All fields base64-encoded | — | JSON serialization compatibility | @@ -210,9 +227,8 @@ See [open-questions.md](../../open-questions.md) for full details. - **OQ-20** (resolved by ADR-020): Salt/KDF — HD derivation is the method; the salt field is unused in v2 (wire-format compatibility only). -- **OQ-22** (open): Key rotation mechanism — the key versioning is in place, - but the rotation workflow (re-encrypt all data, update storage) is not - specced. +- **OQ-22** (resolved by ADR-021): Key rotation — version-indexed paths; + `rotate` method decrypts old, re-encrypts new. ## Security Constraints diff --git a/docs/architecture/crates/vault/mnemonic-derivation.md b/docs/architecture/crates/vault/mnemonic-derivation.md index 6e45f26..5da83be 100644 --- a/docs/architecture/crates/vault/mnemonic-derivation.md +++ b/docs/architecture/crates/vault/mnemonic-derivation.md @@ -211,46 +211,22 @@ pub fn site_password_path(site_hash: &str) -> String; // m/74'/1'/0'/{site_hash} | `m/74'/0'/0'/{n}'` | Worker/device identity | Ed25519 | Multi-device nodes, workers | | `m/74'/0'/1'/0'` | SSH host key | Ed25519 | SSH handler | | `m/74'/1'/0'/{hash}'` | Site-specific deterministic password | Ed25519 bytes | Per-site passwords (not cached) | -| `m/74'/2'/0'/0'` | Encryption key for external credentials | AES-256-GCM | Credential encryption (see [encryption.md](encryption.md)) | +| `m/74'/2'/0'/0'` | Encryption key for external credentials | AES-256-GCM | Credential encryption (v2, see [encryption.md](encryption.md)) | | `m/44'/60'/0'/0/0` | Ethereum signing key | secp256k1 | Ethereum signing (feature-gated) | -### Path namespace discipline - -The `74'` coin type is alknet's namespace. Sub-paths follow a convention: - -| Account (`/X'`) | Purpose | -|-----------------|---------| -| `0'` | Identity keys (node, devices, SSH) | -| `1'` | Deterministic passwords | -| `2'` | Encryption keys (external credentials) | - -New key purposes should allocate a new account index, not reuse an -existing one. This prevents cross-purpose key collisions. - -### The `74'` coin type is a one-way door - -The `74'` coin type is **committed** — once keys are derived at `m/74'/...`, -changing the coin type breaks every existing key. Every node's identity, -SSH host key, and encryption key is derived at a `74'`-rooted path. This is -effectively a one-way door per ADR-009: reversal requires re-deriving every -key from the seed at a new coin type and re-deploying all nodes. The -reservation is documented inline rather than in a separate ADR because it -is a single, self-evident commitment (the coin type is the alknet -namespace; there is no alternative to evaluate). The SLIP-0044 registry -lists `74'` as unallocated, so there is no collision risk with other -projects. - -## Key Types +Helper functions construct parameterized paths: ```rust -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum KeyType { - Ed25519, // SLIP-0010 derivation - Aes256Gcm, // Symmetric key (derived from seed, used for encryption) - Secp256k1, // BIP-0032 derivation (Ethereum, feature-gated) -} +pub fn device_path(index: u32) -> String; // m/74'/0'/0'/{index}' +pub fn site_password_path(site_hash: &str) -> String; // m/74'/1'/0'/{site_hash}' +pub fn encryption_path_for_version(version: u32) -> String; // m/74'/2'/0'/{version-2}' ``` +`encryption_path_for_version` maps a key version to its derivation path +(ADR-021). v2 (current) maps to `m/74'/2'/0'/0'` (which is `PATHS::ENCRYPTION`); +v3 maps to `m/74'/2'/0'/1'`; etc. This is the rotation mechanism — each +version gets a cryptographically independent key from the same seed. + `KeyType` tags `DerivedKey` (see [protocol.md](protocol.md)) and `CachedKey` (see [service.md](service.md)) so consumers know what they received without inspecting byte lengths. diff --git a/docs/architecture/crates/vault/service.md b/docs/architecture/crates/vault/service.md index 030a3a3..286251a 100644 --- a/docs/architecture/crates/vault/service.md +++ b/docs/architecture/crates/vault/service.md @@ -172,12 +172,27 @@ cryptographic details. pub fn decrypt(&self, encrypted: &EncryptedData) -> Result; ``` -Decrypt an `EncryptedData` blob. Derives (and caches) the encryption key at -`PATHS::ENCRYPTION` if not already cached. The `encrypted.key_version` is -stamped onto the `EncryptionKey` for forward compatibility but **does not -select a different derivation path in v1** — the same key (at -`m/74'/2'/0'/0'`) decrypts any version. Path-per-version routing is a Phase -B concern (OQ-22). See [encryption.md](encryption.md). +Decrypt an `EncryptedData` blob. Derives (and caches) the encryption key +at the version-indexed path indicated by `encrypted.key_version` (ADR-021). +Each version maps to a distinct path (`m/74'/2'/0'/{version-2}'`), so old +and new keys can coexist during partial rotation. See +[encryption.md](encryption.md). + +### rotate(encrypted, to_version) → EncryptedData + +```rust +pub fn rotate(&self, encrypted: &EncryptedData, to_version: u32) -> Result; +``` + +Re-encrypt an `EncryptedData` blob from its current key version to a new +version. Decrypts with the old version's key, re-encrypts with the new +version's key. Returns the new `EncryptedData` — the caller replaces the +blob in storage. No new mnemonic needed; the same seed produces all +version keys via different derivation paths (ADR-021). + +This is the rotation primitive. The assembly layer or a migration tool +iterates stored blobs and calls `rotate` on each. The vault does not +self-rotate — rotation is an operational action. ## Cache @@ -303,6 +318,7 @@ error types — the CLI binary converts at the assembly boundary (ADR-018). |----------|-----|---------| | Assembly layer is the sole caller | [ADR-019](../../decisions/019-vault-assembly-layer-only.md) | Handlers never hold a vault reference | | Encryption key via HD derivation | [ADR-020](../../decisions/020-hd-derivation-for-encryption-keys.md) | Seed-derived key at `m/74'/2'/0'/0'`, not PBKDF2 | +| Version-indexed paths for rotation | [ADR-021](../../decisions/021-key-rotation-via-version-indexed-paths.md) | `decrypt` selects key by version; `rotate` re-encrypts | | RwLock for thread safety | — | Multiple readers (derive), exclusive writer (unlock/lock) | | TTL + LRU cache | — | Bounded memory, fresh keys, zeroized eviction | | Actor for in-process irpc dispatch | [ADR-005](../../decisions/005-irpc-as-call-protocol-foundation.md) | irpc message dispatch; not on the call protocol | diff --git a/docs/architecture/decisions/021-key-rotation-via-version-indexed-paths.md b/docs/architecture/decisions/021-key-rotation-via-version-indexed-paths.md new file mode 100644 index 0000000..44df886 --- /dev/null +++ b/docs/architecture/decisions/021-key-rotation-via-version-indexed-paths.md @@ -0,0 +1,253 @@ +# ADR-021: Key Rotation via Version-Indexed Derivation Paths + +## Status + +Accepted + +## Context + +ADR-020 established that the vault derives the AES-256-GCM encryption key +from the BIP39 seed via SLIP-0010 HD derivation at path `m/74'/2'/0'/0'`. +The `EncryptedData.key_version` field exists for rotation tracking, but +the current implementation always derives at the same path regardless of +version — `key_version` is metadata, not a functional selector. + +OQ-22 asked: how does key rotation work? The key versioning is in place, +but the rotation mechanism — how a new key is derived, how existing data +is re-encrypted, and how the vault selects the right key for decryption — +is not specified. + +### Why rotation matters + +Key rotation is a fundamental security hygiene practice. The scenarios +that require it: + +1. **Suspected key compromise**: the encryption key may have leaked + (memory dump, process compromise, log accident). All data encrypted + with that key must be re-encrypted with a new key. +2. **Periodic rotation**: security policy mandates key rotation every N + months. The vault must support this without re-deriving from a new + mnemonic (which would require re-deploying all nodes). +3. **Version transition**: moving from TS PBKDF2 data (v1) to vault HD + data (v2, per ADR-020) is itself a rotation. The mechanism should + generalize — it's the same operation. + +### What "rotation" means concretely + +Rotating from key version N to N+1: + +1. Derive a new encryption key at a new derivation path +2. For each existing `EncryptedData` blob with `key_version: N`: + - Decrypt with the v-N key + - Re-encrypt the plaintext with the v-(N+1) key + - Replace the blob in storage with `key_version: N+1` +3. New encryptions use `key_version: N+1` +4. Old keys remain available for decrypting any data that hasn't been + rotated yet (partial rotation is safe) + +The question is: **how is the new key derived?** The options: + +- **Option A: New derivation path per version.** `m/74'/2'/0'/0'` for v2, + `m/74'/2'/0'/1'` for v3, etc. Each version gets its own HD key. No + new seed needed. +- **Option B: New mnemonic (new seed).** Generate a new mnemonic, unlock + with it, re-encrypt everything. This is heavy — it changes *all* derived + keys (identity, SSH host, etc.), not just the encryption key. +- **Option C: KDF from the existing key.** Use HKDF or PBKDF2 with the + existing derived key + the salt as input. This is the salt field's + potential use (OQ-20 mentioned this), but it adds KDF complexity and + the salt becomes load-bearing. + +## Decision + +### 1. Version-indexed derivation paths + +Each key version maps to a unique derivation path. The last hardened index +in the encryption path is the key version: + +``` +v2: m/74'/2'/0'/0' ← PATHS::ENCRYPTION (current) +v3: m/74'/2'/0'/1' +v4: m/74'/2'/0'/2' +... +``` + +The `encryption_path_for_version(version)` function constructs the path: + +```rust +pub fn encryption_path_for_version(version: u32) -> String { + // v1 is the TS PBKDF2 legacy — not an HD path. The vault starts at v2. + // v2 → m/74'/2'/0'/0', v3 → m/74'/2'/0'/1', etc. + let index = version.saturating_sub(2); + format!("m/74'/2'/0'/{}'", index) +} +``` + +`PATHS::ENCRYPTION` remains `m/74'/2'/0'/0'` — it's the v2 path, and v2 +is the current version. When the vault is rotated to v3, +`encryption_path_for_version(3)` produces `m/74'/2'/0'/1'`. + +This means: +- No new mnemonic needed — rotation uses the same seed, different path +- Each version's key is cryptographically independent (HD derivation + ensures this) +- The derivation path is self-documenting (`m/74'/2'/0'/1'` is clearly + "encryption key, version 3") +- Old keys are always derivable (the seed doesn't change), so partial + rotation is safe — the vault can decrypt any version + +### 2. `encrypt_key(version)` and `decrypt_key(version)` methods + +The `VaultServiceHandle` gains version-aware key derivation: + +```rust +impl VaultServiceHandle { + /// Derive the encryption key for the given version. Cached. + fn derive_encryption_key_for_version( + &self, + version: u32, + ) -> Result { + let path = encryption_path_for_version(version); + // ... derive at path, cache by path ... + } + + /// Encrypt with the current key version. + pub fn encrypt(&self, plaintext: &str, key_version: u32) -> Result; + + /// Decrypt by deriving the key at the version indicated by the blob. + pub fn decrypt(&self, encrypted: &EncryptedData) -> Result { + let key = self.derive_encryption_key_for_version(encrypted.key_version)?; + encryption::decrypt(encrypted, &key) + } +} +``` + +`decrypt` now derives the key at the path **indicated by +`encrypted.key_version`** — not always at `PATHS::ENCRYPTION`. This is +the fix for the W1 drift issue from the vault review: the current source +ignores `key_version` for key selection; the spec now makes it functional. + +### 3. `rotate` method + +```rust +impl VaultServiceHandle { + /// Re-encrypt an EncryptedData blob from one key version to another. + /// + /// Decrypts with the key at the blob's current key_version, + /// re-encrypts with the key at `to_version`. Returns the new + /// EncryptedData. Does not update storage — the caller replaces the + /// blob in storage. + pub fn rotate( + &self, + encrypted: &EncryptedData, + to_version: u32, + ) -> Result { + let plaintext = self.decrypt(encrypted)?; + self.encrypt(&plaintext, to_version) + } +} +``` + +`rotate` is a vault method, not a storage operation. It decrypts and +re-encrypts; the caller (the assembly layer or a migration tool) handles +replacing the blob in storage. This keeps the vault focused on crypto and +the storage system focused on storage. + +### 4. `CURRENT_KEY_VERSION` and rotation policy + +```rust +pub const CURRENT_KEY_VERSION: u32 = 2; +``` + +`encrypt()` stamps `CURRENT_KEY_VERSION` (or the explicitly-passed version) +onto new `EncryptedData` blobs. The assembly layer decides when to rotate: + +- **Manual rotation**: an operator triggers rotation (e.g., a CLI command + `alknet vault rotate --to v3` that loads all blobs, calls `rotate` on + each, and writes them back to storage). +- **No automatic rotation**: the vault does not self-rotate. Rotation is + an operational action, not a runtime behavior. The vault provides the + mechanism; the policy is external. + +### 5. Cache implications + +The `KeyCache` is keyed by derivation path. Since each version has a +distinct path, the cache naturally holds multiple versions simultaneously. +This is correct — during a rotation, the vault may need to decrypt old +blobs (v2) and encrypt new blobs (v3), and both keys should be cached. + +The cache's TTL and LRU eviction still apply. If the cache evicts an old +version's key during a long rotation, the next `decrypt` of an old blob +re-derives it (the seed hasn't changed). This is correct but slightly +slower — the rotation tool should be aware that cache misses on old keys +are expected. + +## Consequences + +**Positive:** +- Key rotation is a vault method (`rotate`), not a storage operation or a + full mnemonic change. It's cheap (HD derivation) and local. +- Partial rotation is safe. Old and new keys coexist — the vault can + decrypt any version. This means a rotation can be performed incrementally + (rotate some blobs, verify, rotate the rest). +- No new mnemonic needed. The same seed produces all version keys. A + backup node with the same mnemonic can decrypt any version. +- The derivation path is self-documenting. `m/74'/2'/0'/1'` is clearly + "encryption key version 3." +- The `salt` field remains unused — no KDF complexity. Rotation is pure HD + path indexing. +- The mechanism generalizes the TS→vault migration (v1→v2 is a rotation, + though v1 requires the TS PBKDF2 `decrypt`, not the vault's `decrypt`). + +**Negative:** +- `decrypt` now derives the key at the version-indicated path, which means + a cache miss on an old version re-derives from the seed. This is a few + HMAC operations — negligible, but the path construction and cache lookup + add a small amount of complexity over the current "always use + `PATHS::ENCRYPTION`" approach. +- The rotation tool (CLI command or migration script) must iterate all + stored blobs and call `rotate` on each. This is an operational concern, + not a vault concern — but the vault spec should document the expected + usage pattern so the tool implementer knows the contract. +- Old version keys are always derivable (the seed doesn't change). This is + a feature (partial rotation is safe) but also means a compromised seed + allows decrypting all versions. If the seed itself is compromised, all + versions are compromised — rotation doesn't help. This is inherent to + HD derivation and not specific to this design. + +## Assumptions + +1. **The seed is not compromised.** If the seed is compromised, rotating + the encryption key path doesn't help — the attacker can derive all + version keys. Seed compromise requires a full mnemonic change (new + seed, re-derive everything, re-deploy). This ADR covers encryption key + rotation, not seed rotation. Seed rotation is an operational procedure + (generate new mnemonic, unlock with it, re-encrypt all data) that is + outside the vault's API. + +2. **Rotation is infrequent.** The vault does not optimize for frequent + rotation (e.g., per-request key derivation). Rotation is an operational + event triggered by policy or incident. The cache and path-indexed + approach are efficient for this usage pattern. + +3. **The storage system tracks which blobs to rotate.** The vault's `rotate` + method handles one blob at a time. Iterating all stored + `EncryptedData` blobs is the storage system's job (or the CLI's). The + vault doesn't know what's in storage — it only knows how to rotate a + blob it's given. + +4. **v1 (TS PBKDF2) data is not rotated through the vault.** v1 data is + decrypted by the TS `decrypt()` function (PBKDF2), not the vault's + `decrypt()` (which uses HD derivation). The v1→v2 migration is a + separate tool that has access to both. Once data is at v2, future + rotations (v2→v3, etc.) use the vault's `rotate` method. + +## References + +- ADR-020: HD derivation for encryption keys (this ADR builds on the + version-indexed path scheme) +- OQ-22: Key rotation mechanism (resolved by this ADR) +- [encryption.md](../crates/vault/encryption.md) — AES-256-GCM, EncryptedData +- [service.md](../crates/vault/service.md) — encrypt, decrypt, rotate methods +- [mnemonic-derivation.md](../crates/vault/mnemonic-derivation.md) — + derivation paths, `PATHS::ENCRYPTION` \ No newline at end of file diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index b94d577..753ce8b 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -261,8 +261,8 @@ These questions are acknowledged but not active. They will be promoted to open w ### OQ-22: Key Rotation Mechanism - **Origin**: [encryption.md](crates/vault/encryption.md) -- **Status**: open -- **Door type**: Two-way -- **Priority**: low -- **Resolution**: Key versioning is in place (`EncryptedData.key_version`, `CURRENT_KEY_VERSION = 1`), but the rotation workflow — re-encrypt all existing data with a new key version, update storage — is not specced. The mechanism is straightforward (derive a new key at a new path or from a new seed, decrypt with v1, re-encrypt with v2), but the operational workflow (when to rotate, how to track which data is at which version, how to handle partially-rotated state) needs design. Low priority: keys don't rotate frequently, and v1 is stable. Two-way door — rotation is additive; a v2 key doesn't break v1 data. -- **Cross-references**: [encryption.md](crates/vault/encryption.md) \ No newline at end of file +- **Status**: resolved +- **Door type**: One-way (path scheme), two-way (rotation policy) +- **Priority**: medium +- **Resolution**: Key rotation uses version-indexed derivation paths. Each key version maps to a distinct SLIP-0010 path: `m/74'/2'/0'/{version-2}'`. v2 (current) is at `m/74'/2'/0'/0'`; v3 is at `m/74'/2'/0'/1'`; etc. The `decrypt` method derives the key at the path indicated by `encrypted.key_version` (not always at `PATHS::ENCRYPTION`). The `rotate` method decrypts with the old version's key and re-encrypts with the new version's key — no new mnemonic needed. The assembly layer or a migration tool iterates stored blobs and calls `rotate` on each; the vault does not self-rotate. Partial rotation is safe (old keys remain derivable). See ADR-021. +- **Cross-references**: ADR-020, ADR-021, [encryption.md](crates/vault/encryption.md), [service.md](crates/vault/service.md) \ No newline at end of file diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index bf5cf21..f3e4d30 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -211,6 +211,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/). | [018](decisions/018-vault-standalone-crate.md) | Vault as Standalone Crate | Zero alknet crate dependencies; vault defines own types and errors | | [019](decisions/019-vault-assembly-layer-only.md) | Vault Assembly-Layer-Only Access | The assembly layer (CLI binary) is the sole direct caller; handlers never hold a vault reference | | [020](decisions/020-hd-derivation-for-encryption-keys.md) | HD Derivation for Encryption Keys | SLIP-0010 derivation from seed, not PBKDF2; salt field unused in v2 | +| [021](decisions/021-key-rotation-via-version-indexed-paths.md) | Key Rotation via Version-Indexed Paths | Version-indexed derivation paths; `rotate` re-encrypts between versions | ## Open Questions @@ -224,7 +225,7 @@ Open questions are tracked in [open-questions.md](open-questions.md). Key questi - **OQ-16**: Safe vault operations for call protocol exposure (resolved: none for now — see ADR-014) - **OQ-20**: Encryption key derivation (resolved: HD derivation, not PBKDF2 — see ADR-020) - **OQ-21**: Remote vault administration (deferred: network unlock not supported — see ADR-019) -- **OQ-22**: Key rotation mechanism (open: versioning in place, workflow not specced — see [encryption.md](crates/vault/encryption.md)) +- **OQ-22**: Key rotation (resolved: version-indexed paths, `rotate` method — see ADR-021) ## Failure Modes