- docs/plans/implementation.md: five-wave decomposition of the architecture (foundations -> server stack -> server mappings || client direction -> validation & release), following the alkgit sibling pattern (decompose-implement-gate rhythm, per-wave decomposition records, review gates at wave boundaries) - scope authority: v0.1.0 publishes the crate; assembly wiring stays out; sftp decline placeholder is shipped behavior - wave 1 decomposed into eight task files under tasks/implementation/ (manifest, error model, key sources, host-key policy, fingerprint, config surface, test doubles, review gate) with recorded interpretations flagged for the gate Verification: taskgraph validate (12 tasks, no cycles); cargo test, clippy -D warnings, fmt --check green
3.0 KiB
3.0 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | ||
|---|---|---|---|---|---|---|---|---|---|
| w1-key-sources | Wave 1 — vault-backed key sources (ADR-005 custody in code) | pending |
|
moderate | medium | project | implementation |
Description
Land the ADR-005 custody model in code — the key-source seam both directions consume (client dial keys, server host keys):
- The source seam — the adapter takes key sources, not static
keys (the alkhttp ADR-014 pattern): a source trait the assembly
implements over an injected alkvault handle,
EncryptedDataopaque to the crate (ADR-005 decision 5 — the crate never interpretskey_versionor record internals). - At-use lifecycle (decision 2) —
vault.decrypt(blob)→Zeroizing<String>wrap at the adapter seam (decision 3 — the known-gap contract; the vault-side improvement is upstream, not here) → one-timedecode_secret_key(plain, None)→ the decodedPrivateKeylives for the dial's lifetime. The vault is needed only at dial time; locking mid-session keeps live sessions streaming. - Host-key load path (decision 6) — decrypt-at-construction
returning the
PrivateKeyset the serverConfig::keyswill consume (theConfigconstruction itself is wave 2; locked vault → the load fails → no server is constructed, POC-2 §5). - Failure mapping — every vault failure mode maps to its distinct
KeySourceErrorvariant (locked / absent / decryption / unknown version / decode); never collapsed, never a fallback dial. - Plaintext never persists (decision 7) — no disk spill on this
path, ever; the
Zeroizingwrap bounds the plaintext from the adapter seam onward.
Inner format is OpenSSH private-key PEM, unencrypted (decision 1) — the source contract assumes it; the decode call is the single conversion.
Acceptance Criteria
- Source trait(s) take the vault handle injected;
EncryptedDataopaque (nokey_versioninterpretation anywhere insrc/) - Decrypt →
Zeroizing<String>→ one-timedecode_secret_keychain in place; the plaintext never outlives the wrap - 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 usable by russh
- 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,cargo fmt --checkpass
References
- docs/architecture/decisions/005-at-rest-key-custody.md (decisions 1–7)
- docs/architecture/auth.md §Key custody
- docs/research/poc-2-vault-keys.md (§1 the 4-way table, §2 lifecycle
- zeroization bounds, §3 failure matrix, §5 host keys end to end)
- docs/research/auth-bridge.md §The gap (the source-shape reasoning)
Notes
To be filled by implementation agent
Summary
To be filled on completion