feat(key-source): land the vault-backed key sources (ADR-005 custody in code)

- Two injected-handle seams: KeySource (dial-config, decrypt-at-use ->
  one PrivateKey) and HostKeySource (serve-config, decrypt-at-construction
  -> the Config::keys set); async_trait object-safe, the HostKeyPolicy
  handle shape
- Crate-provided ADR-005 implementations: VaultKeySource (injected
  vault handle + Option<EncryptedData>, None -> Absent) and
  VaultHostKeySource (record set, empty -> Absent) over the shared
  decrypt_and_decode chain: vault.decrypt -> Zeroizing<String> seam
  wrap -> one-time decode_secret_key(plain, None)
- EncryptedData opaque in src/ (no key_version interpretation); vault
  error taxonomy reshaped into the five-variant KeySourceError matrix,
  never collapsed; plaintext never persists
- 12 in-module tests on the real in-memory alkvault handle: round-trip
  (fingerprint-identical ed25519 key that signs), full failure matrix,
  locked host-bootstrap refusal, lock-mid-session lifecycle,
  v2->v3 rotation identity; three committed throwaway PEM fixtures

Verified: cargo test (23 + 4 parity), cargo test --all-features,
clippy -D warnings (default + all-features), fmt --check, taskgraph
validate
This commit is contained in:
glm-5.3-flash committed 2026-10-28 06:45:56 +00:00
1 parent 149f11d4b2
commit 4d529dbacf
3 files changed
+580 -9

No files matched your search

+65 -9
View File
@@ -1,7 +1,7 @@
---
id: w1-key-sources
name: Wave 1 — vault-backed key sources (ADR-005 custody in code)
status: pending
status: completed
depends_on: [w1-manifest, w1-error-model]
scope: moderate
risk: medium
@@ -42,18 +42,18 @@ conversion.
## Acceptance Criteria
- [ ] Source trait(s) take the vault handle injected; `EncryptedData`
- [x] Source trait(s) take the vault handle injected; `EncryptedData`
opaque (no `key_version` interpretation anywhere in `src/`)
- [ ] Decrypt → `Zeroizing<String>` → one-time `decode_secret_key`
- [x] Decrypt → `Zeroizing<String>` → one-time `decode_secret_key`
chain in place; the plaintext never outlives the wrap
- [ ] Host-key load path returns the decoded key set; locked vault →
- [x] Host-key load path returns the decoded key set; locked vault →
`KeySourceError::Locked` (no server construction possible)
- [ ] Round-trip test: encrypt(PEM) → source → decoded ed25519 key
- [x] Round-trip test: encrypt(PEM) → source → decoded ed25519 key
usable by russh
- [ ] Failure-mode tests: locked / absent / tampered ciphertext /
- [x] Failure-mode tests: locked / absent / tampered ciphertext /
unknown version / undecodable plaintext each map to their
distinct variant
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
- [x] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
@@ -67,8 +67,64 @@ conversion.
## Notes
> To be filled by implementation agent
> `src/key_source.rs` landed: two injected-handle seams and the
> crate-provided vault-backed implementations over them.
>
> - **Traits** (`async_trait`, the `HostKeyPolicy` handle shape):
> `KeySource::load() -> Result<PrivateKey, KeySourceError>` (the
> wave-4 dial-config carries `Arc<dyn KeySource>`) and
> `HostKeySource::load() -> Result<Vec<PrivateKey>,
> KeySourceError>` (wave-2 serve-config; decrypt-at-construction,
> a *set* because `Config::keys` takes multiple). An assembly may
> implement either trait itself; the crate ships the ADR-005
> implementations and the custody machinery.
> - **`VaultKeySource`** — decrypt-at-use over an injected clone of
> the assembly's `VaultServiceHandle` + one injected
> `Option<EncryptedData>` record (`None` → `Absent`, the POC-2 §1
> `KeySource { blob: Option<EncryptedData> }` shape).
> **`VaultHostKeySource`** — same model over a record set (empty set
> → `Absent`). Both are decrypt-at-use chains
> (`decrypt_and_decode`, shared): `vault.decrypt(blob)` →
> **`Zeroizing<String>` at the seam** → one-time
> `decode_secret_key(plain, None)`. Blobs are never interpreted in
> `src/` — no `key_version` reads, no record internals.
> - **Failure mapping** (`map_vault_error`): `VaultLocked` →
> `Locked`; `InvalidPath` → `UnknownKeyVersion`; `Encryption` →
> `Decryption`. The vault's decrypt-path-unreachable variants
> (`Derivation`/`Mnemonic`/`AlreadyUnlocked`/`UnsupportedKeyType`)
> land in the `Decryption` refuse-at-crypto class with honest
> message text — the five-variant matrix stays closed, nothing
> collapses a real custody condition into success.
> - **Tests** (12, all in-module with the real in-memory
> alkvault handle — `VaultServiceHandle::new()` is the in-memory
> construction, so no local vault double was needed):
> round-trip (encrypt PEM → source → fingerprint-identical decodable
> ed25519 key that signs), dyn-handle load, host-key set order,
> load-works-after-lock (POC-2 §2 lifecycle), locked / absent /
> tampered-ciphertext / wrong-mnemonic → `Decryption` (distinct
> from tampered only by message — both GCM-open) / version 0+1 →
> `UnknownKeyVersion` / non-key plaintext → `Decode`, locked
> host-bootstrap refusal, empty host set, v2→v3 rotation-preserves
> -identity through the source. Key fixtures: three committed
> throwaway ed25519 PEMs embedded as test constants
> (w1-test-doubles' committed-static-PEM posture; generated at
> test-fixture time with ssh-keygen, never deployed/registered).
>
> Interpretation pinned for test-doubles: alkvault 0.1 *does* ship
> the in-memory construction (`VaultServiceHandle`), so the double
> wraps the real handle — no local decrypt/decrypt double.
## Summary
> To be filled on completion
> `src/key_source.rs` (lib re-exports `KeySource`, `HostKeySource`,
> `VaultKeySource`, `VaultHostKeySource`) — ADR-005 in code: the
> injected-handle source seams both directions consume, the crate's
> vault-backed implementations carrying decrypt-at-use (vault.decrypt
> → `Zeroizing<String>` → one-time `decode_secret_key`), opaque
> `EncryptedData`, and the full five-variant failure mapping.
> 12 tests cover the round-trip (usable-by-russh via signing), the
> full failure matrix, the host-key decrypt-at-construction refusal
> (locked → no server constructed), lock-mid-session lifecycle, and
> rotation identity preservation. Verified: cargo test (23 + 4
> parity), cargo test --all-features, clippy -D warnings (default +
> all-features), fmt --check, taskgraph validate.