Files
alkssh/tasks/implementation/w1-key-sources.md
T
glm-5.3-flash 0b1bc284fa docs(plans): add wave-based implementation plan; decompose wave 1
- 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
2026-10-28 06:13:26 +00:00

3.0 KiB
Raw Blame History

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
w1-manifest
w1-error-model
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, EncryptedData opaque to the crate (ADR-005 decision 5 — the crate never interprets key_version or 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-time decode_secret_key(plain, None) → the decoded PrivateKey lives 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 PrivateKey set the server Config::keys will consume (the Config construction 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 KeySourceError variant (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 Zeroizing wrap 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; EncryptedData opaque (no key_version interpretation anywhere in src/)
  • 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 → 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 --check pass

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