Files
alkssh/docs/research/pocs.md
T
glm-5.3-flash ecdf836eeb docs(research): phase 0 research set — vision, russh, auth bridge, mapping table, POC plan
The phase 0 document set (documented only; no POCs run yet):

- docs/research/vision.md — guiding principles (russh owns the wire,
  door-not-backend, fail-closed everything, one seam per mapping,
  family identity not SSH identity); non-goals (no sshd, no sftp until
  alkfs, no password auth); the five scope questions phase 0/1 must
  answer
- docs/research/russh.md — 0.64.1-era inventory: server/client
  Handler method tables, Config knobs, keys module (ssh-key re-export,
  fingerprint API), the RC-pinned dependency tree (ssh-key, rsa,
  pkcs1), MSRV/edition/license posture; alknet-main referenced as a
  rough example only, explicitly not decision input
- docs/research/auth-bridge.md — the alkcall mapping: no
  resolve_from_signature exists; the actual surface is
  IdentityProvider::resolve_from_fingerprint fed by russh-verified key
  fingerprints in auth_publickey; auth-method disposition matrix
  (only publickey implemented, all defaults overridden); client side
  mirrors alkhttp ADR-014 (key sources, not static keys; vault
  client-side only); the vault-to-russh-key bridge named as the
  missing design piece
- docs/research/mapping-table.md — the spine: channel-type rows with
  feature candidates and the verified sibling serve/consume entries
  (alktty drive_session, alkgit serve_duplex_resolved, alksocks
  serve_socks5_over, alktunnels consumer wrappers + the no-serve-over-
  stream absence note); request-level rows; parse-then-route dispatch
  inside the session channel; the alk/ssh ALPN topology question
  (ADR-004 reserved / ADR-034 stream_types [0,1] / ADR-042 hub never
  runs it); alktunnels route choice flagged for ADR
- docs/research/pocs.md — POC-1 (auth bridge + fail-closed declines
  against vanilla OpenSSH), POC-2 (vault-to-russh key bridge:
  PrivateKey extraction vs vault-backed Signer), POC-3 (mapping
  bake-off stages A-D: git exec, socks -D, tunnel -L, pty); all
  standalone mode; findings docs are the deliverables

Verification: cargo doc --no-deps, cargo fmt --check, cargo test
(docs-only change; lib untouched)
2026-10-25 10:13:17 +00:00

7.1 KiB
Raw Blame History

POCs — phase 0 validation plan

Status: Draft (phase 0 research). Deliverables of each POC are findings docs (docs/research/poc-N-*.md when run); POC source is disposable and never merges to main. Mode for all: standalone (repos have no code to build on yet) — scratch cargo projects at /workspace/<poc-name> unless a POC explicitly builds on repo code (then branch mode per docs/sdd_process.md).

The hypotheses below are what phase 1's ADRs will cite. If a POC refutes a hypothesis, the finding doc records the refutation — that is the point of running it.

POC-1 — auth bridge + fail-closed declines end to end

Question: does the russh 0.64-era server auth surface compose with alkcall's fingerprint-based identity model as cleanly as auth-bridge.md assumes, and does the fail-closed decline contract hold under a real vanilla client?

Construction: standalone crate. russh server loopback bound to a local listener + vanilla ssh (OpenSSH) as the client (the point is to validate against the vanilla wire, not just russh's own client). A config-backed IdentityProvider stub registered with two ed25519 keys; russh client available as a secondary client for Signer-shaped checks.

Validates:

  • auth_publickey after russh's signature verification → ssh_key fingerprint (HashAlg::Sha256) string parity with a stub-registered identity; resolve_from_fingerprint → Some → authenticated; unknown key → clean decline under auth_rejection_time/max_auth_attempts.
  • auth_publickey_offered disposition (accept-and-let-decide) behaves with OpenSSH's probe-then-sign flow.
  • Decline matrix: auth_none/password/keyboard-interactive declined (default impls are accept — every default must be overridden and proven); Config::methods advertises publickey only.
  • ChannelOpenHandle drop = auto-reject semantics; unfeatured channel-type declines observable by vanilla ssh (ssh host session-open refused, -L refused, clean diagnostics).
  • Channel ↔ AsyncRead/AsyncWrite conversion (into_stream/split) behaves under a trivial echo (the plumbing POC-2 leans on).

Hypothesized outcome: the auth-bridge design holds; the only uncertainty is fingerprint-format parity details, which this either confirms or corrects.

POC-2 — client-side vault → russh key bridge (separate by nature)

Question: how does an alkvault-derived key become a russh client key without the vault leaking the bytes — PrivateKey extraction vs a vault-backed Signer?

Construction: standalone. alkvault VaultServiceHandle + derive_ed25519 (host/device paths) → two candidate shapes: (a) ssh-key KeypairData::Ed25519 from raw seed bytes → russh::keys::PrivateKey → authenticate_publickey; (b) a vault- backed russh::Signer fed to authenticate_publickey_with (bytes never leave the vault struct).

Validates:

  • Whether ed25519 seed bytes → ssh-key key data round-trips (KeypairData expects the expanded keypair — seed-to-[u8; 64] expansion is a real step; whether alkvault's DerivedKey carries the expanded form or just the seed is an open sub-question).
  • Whether Signer + authenticate_publickey_with + PublicKey (public half held outside the vault) is ergonomic, and what PrivateKeyWithHashAlg wants for non-RSA keys.
  • Server side of the same loopback re-uses POC-1's fingerprint path — client key derived in vault, server resolves its fingerprint.
  • Lifecycle correctness: derived key zeroization across the boundary; vault lock/unlock races.

Scope note (per the design split): the vault is for secrets we hold — client-side outgoing keys and the server's own host key — and is explicitly not involved in resolving clients' fingerprints (that path is the assembly-layer IdentityProvider). Kept as its own POC because its failure modes (key-material lifecycle) are unrelated to the auth-flow failure modes of POC-1.

Hypothesized outcome: shape (b) (vault-backed Signer) is the preferable design; if Signer friction proves high, shape (a) with a zeroizing wrapper is documented with its exposure caveat.

POC-3 — the mapping bake-off: channel bridge through a real sibling seam

Question: does "russh channel duplex → family serve-over-a-stream entry" actually work against the real published seams — and which of the mapping-table's route choices survives contact with reality?

Construction: standalone, but depending on the real sibling crates (alktty/alksocks/alkgit + alkcall types via their published forms). russh server loopback + vanilla clients; per stage:

  • Stage A — git exec mapping (the alkforge-critical path): vanilla git clone/push over ssh:// → russh server → parse exec command → serve_duplex_resolved(...) over the split channel halves with stub GitRefs/GitPackGen/GitPackIngest (or the substrate composition if a real repo is wanted late in the POC). Confirms: the Parse-mode (server-speaks-first advertisement) dialect satisfies vanilla git's expectations over exec; channel EOF/close maps to session termination; exit-status flows.
  • Stage B — -D mapping: russh server, feature dynamic-forward- shaped build → serve_socks5_over over a session channel → curl through the SOCKS5 proxy to a local HTTP endpoint. Confirms: the dispatch-split inside session (parse-then-route vs SOCKS-detect), duplex backpressure under throughput.
  • Stage C — -L mapping: direct-tcpip → the mapping-table's two alktunnels routes (adapter-side TunnelSession::open_direct via a real ChannelClient vs local-dial stand-in). Whichever route the POC exercises informs the phase 1 ADR on the presence/absence of a serve-over-stream entry in alktunnels.
  • Stage D — session/pty mapping, last and optional (a real PTY backend needs alktty's local feature; the wire-level questions are already largely answered by A/B/C): shell over drive_session with the locally-constructed NegotiateRequest, window-change and signal mapping.

Validates: the one-seam-per-mapping hypothesis; the parse-then- route dispatch; stderr semantics in pipe vs PTY mode; EOF/close/signal fidelity end to end; throughput sanity (window sizes vs the 1 MiB channels reassembly cap on the bridged route).

Hypothesized outcome: stages A–C confirm the design with local fixups recorded; stage D may surface PTY-merge stderr surprises that reshape the request-level mapping rows.

Sequencing and gates

  1. POC-1 first — everything downstream presumes its auth plumbing.
  2. POC-2 after POC-1 (reuses POC-1's server loopback).
  3. POC-3 last; stage A gates phase 1's mapping ADR work — it is the path alkforge phase 0 consumes first.
  4. Phase 0 completes when the findings docs exist and the OQ list in vision.md §scope questions is either answered by findings or carried into phase 1's open-questions.md as seeded OQs.

References

  • vision.md (principles the POCs must honor — including that POC code carries relaxed constraints but never merges)
  • russh.md (API shapes the POCs exercise), auth-bridge.md, mapping-table.md
  • docs/sdd_process.md §Phase 0 (POC modes, standalone placement)