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)
7.1 KiB
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_publickeyafter russh's signature verification →ssh_keyfingerprint (HashAlg::Sha256) string parity with a stub-registered identity;resolve_from_fingerprint→Some→ authenticated; unknown key → clean decline underauth_rejection_time/max_auth_attempts.auth_publickey_offereddisposition (accept-and-let-decide) behaves with OpenSSH's probe-then-sign flow.- Decline matrix:
auth_none/password/keyboard-interactivedeclined (default impls are accept — every default must be overridden and proven);Config::methodsadvertisespublickeyonly. ChannelOpenHandledrop = auto-reject semantics; unfeatured channel-type declines observable by vanillassh(ssh hostsession-open refused,-Lrefused, 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-keykey data round-trips (KeypairDataexpects the expanded keypair — seed-to-[u8; 64]expansion is a real step; whether alkvault'sDerivedKeycarries 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 whatPrivateKeyWithHashAlgwants 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/pushoverssh://→ russh server → parse exec command →serve_duplex_resolved(...)over the split channel halves with stubGitRefs/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 —
-Dmapping: russh server, featuredynamic-forward- shaped build →serve_socks5_overover a session channel →curlthrough the SOCKS5 proxy to a local HTTP endpoint. Confirms: the dispatch-split insidesession(parse-then-route vs SOCKS-detect), duplex backpressure under throughput. - Stage C —
-Lmapping:direct-tcpip→ the mapping-table's two alktunnels routes (adapter-sideTunnelSession::open_directvia a realChannelClientvs 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
localfeature; the wire-level questions are already largely answered by A/B/C): shell overdrive_sessionwith the locally-constructedNegotiateRequest, 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
- POC-1 first — everything downstream presumes its auth plumbing.
- POC-2 after POC-1 (reuses POC-1's server loopback).
- POC-3 last; stage A gates phase 1's mapping ADR work — it is the path alkforge phase 0 consumes first.
- 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'sopen-questions.mdas 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.mddocs/sdd_process.md§Phase 0 (POC modes, standalone placement)