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:
1 parent
149f11d4b2
commit
4d529dbacf
3 files changed
+580
-9
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user