From 9f443e982bbccafb21c9a69cb37aaf46e8a11fcf Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Wed, 4 Nov 2026 09:45:33 +0000 Subject: [PATCH] =?UTF-8?q?docs(architecture):=20close=20the=20OQ-07/OQ-10?= =?UTF-8?q?=20upstream=20watch=20items;=20amend=20ADR-005=20=C2=A73=20+=20?= =?UTF-8?q?the=20mapping=20rows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ADR-005 decision 3 amended (the gap is closed upstream: alkvault 0.2.0's Zeroizing return on every path incl. the error one; the adapter wrap dropped — the double wrap would be dead posture); the Negative consequence amended; the vault-surface note updated. - mapping.md: the forwarded-tcpip row rides the gate shape (typed call-plane miss, residual race re-scoped); derivation-contract rule 5 amended (the PTY flattening constraint retired, alktty 0.8.2 — no contract change); the exit_signal row + OQ anchors updated. - open-questions.md: OQ-07 + OQ-10 amended with the upstream closures; the two closed watch items struck with pointers. - client.md: the OQ-07 anchor updated. --- docs/architecture/client.md | 5 ++- .../decisions/005-at-rest-key-custody.md | 44 ++++++++++++------- docs/architecture/mapping.md | 33 ++++++++------ docs/architecture/open-questions.md | 22 +++++----- 4 files changed, 61 insertions(+), 43 deletions(-) diff --git a/docs/architecture/client.md b/docs/architecture/client.md index e21cca4..4119f8c 100644 --- a/docs/architecture/client.md +++ b/docs/architecture/client.md @@ -200,8 +200,9 @@ None open or deferred — see [open-questions.md](open-questions.md) (all of OQ-01…13 resolved). Doc-local contract anchors: OQ-05 (the host-key policy — §The dial surface), OQ-12 (lifecycle primitives only — §Lifecycle primitives), OQ-13 (the sftp decline placeholder — §op -rows and §not-include), OQ-07 (the adapter-side `Zeroizing` wrap — -ADR-005 decision 3). +rows and §not-include), OQ-07 (closed upstream — the vault returns a +`Zeroizing` from alkvault 0.2.0; ADR-005 decision 3 amended, +the adapter-side wrap dropped). ## References diff --git a/docs/architecture/decisions/005-at-rest-key-custody.md b/docs/architecture/decisions/005-at-rest-key-custody.md index e2c0d14..0a72d2a 100644 --- a/docs/architecture/decisions/005-at-rest-key-custody.md +++ b/docs/architecture/decisions/005-at-rest-key-custody.md @@ -28,8 +28,9 @@ the server direction it holds the host key. Family rules in force: — that path is the assembly-layer `IdentityProvider` (scope note in `pocs.md` §POC-2). -alkvault 0.1.0's `encrypt`/`decrypt` surface is `&str` in, `String` -out (`EncryptedData` as the wrapper) — a decisive constraint on any +alkvault's `encrypt`/`decrypt` surface is `&str` in, `Zeroizing` +out since 0.2.0 (0.1.0 returned a plain `String`; the `EncryptedData` +wrapper unchanged) — a decisive constraint on any binary inner format (POC-2 measured: it would force base64 layers on both ends). @@ -55,17 +56,25 @@ both ends). only at dial time: locking it mid-session keeps live sessions streaming while new dials fail (POC-2 verified); unlock is side-effect-free on key identity. -3. **Zeroization ownership (the known gap).** ssh-key zeroizes - ed25519 key bytes on `Drop` (decoded keys, and their clones, clean - themselves). The *decrypted plaintext* is the gap: alkvault's - `decrypt` returns a plain `String`, so from the vault's return to - the adapter's wrap the plaintext sits in unfenced heap. The - adapter owns the wrap (proven in POC-2) — **that is the contract on - this path**; the vault-side improvement (a `Zeroizing`-out - API) is an upstream ask filed in alkvault's own tracker, not an - alkssh obligation — when it lands, this ADR's pointer updates as a - one-line doc touch (alkssh OQ-07 is closed at the - adapter-convention level). +3. **Zeroization ownership (the gap — closed upstream).** ssh-key + zeroizes ed25519 key bytes on `Drop` (decoded keys, and their + clones, clean themselves). The *decrypted plaintext* was the gap: + alkvault's `decrypt` once returned a plain `String`, leaving the + plaintext in unfenced heap between the vault's return and the + adapter's wrap — the adapter owned the wrap then (proven in POC-2; + **that was the contract on this path**) and the vault-side + improvement (a `Zeroizing`-out API) was an upstream ask + filed in alkvault's own tracker. The ask shipped in **alkvault + 0.2.0** (2026-11-04, the tracker's review 001 — this ADR is the + origin it cites): `decrypt` returns `Zeroizing`, the buffer + zeroizes on every path (including the UTF-8 error path), and + `rotate` transits zeroized output too. The adapter seam wrap is + thereby **dropped** (the alkssh 0.2.0 adoption): `decrypt_and_decode` + consumes the vault's zeroized return directly — the double wrap + would be dead posture. Decision 3's ownership now sits entirely + with alkvault; the adapter's obligation shrinks to consuming the + zeroized value in one pass (the `decode_key_arm` signature keeps + riding the `Zeroizing` type as the proof). 4. **Failure modes are all fail-closed, with distinct error shapes.** Locked vault → no dial (and no server: a host key that can't load prevents server construction — verified at bootstrap). Absent @@ -107,10 +116,11 @@ both ends). future held material; every failure mode is proven fail-closed; the at-rest bytes are plain OpenSSH key files, so alkvault stores and rotates them with no SSH-specific logic. -- **Negative**: decrypted plaintext has one unfenced window (before - the adapter's wrap) until alkvault changes its API — the gap is - recorded, owned, and filed with its owner (the alkvault tracker; - OQ-07 resolved at the adapter level), not silently absorbed. +- **Negative**: ~~decrypted plaintext has one unfenced window (before + the adapter's wrap) until alkvault changes its API~~ — the gap is + resolved (alkvault 0.2.0's `Zeroizing` return; the adapter + wrap dropped, OQ-07 amended). No plaintext window remains on this + ADR's path. - **Neutral**: the seed-variant size optimization remains noted as a legitimate future option if blob size ever matters; adopting it would be an ADR amendment (it changes what a stored record is). diff --git a/docs/architecture/mapping.md b/docs/architecture/mapping.md index 7a82409..13119de 100644 --- a/docs/architecture/mapping.md +++ b/docs/architecture/mapping.md @@ -34,7 +34,7 @@ bridge primitives every mapping uses (POC-1 §7). | `session` → `exec_request` git | `git clone/push ssh://` | `git` | alkgit: parse command → `Service` + repo → `serve_duplex_resolved(repo_id, identity, service, duplex, …)` | **exec-only** (ADR-006 decision 2): the repo rides inside the exec command string (`git-upload-pack '/repo'`); vanilla git never speaks subsystem for git; process-mode dialect (server-speaks-first advertisement); exit-status proven both directions | | `session` → `subsystem_request` | `sftp` clients; manual `ssh -s` | `sftp` (placeholder) / `git` rows | — | **`sftp` name**: declines until alkfs lands — the feature exists, the route doesn't (OQ-13 resolved: the tested decline placeholder is the shipped behavior until an alkfs-mapping ADR changes it). **git service names**: decline with reason — a subsystem request carries only a name, never a repo id (POC-3 §A.3); never half-serves | | `direct-tcpip` | `ssh -L`; all `ssh -D` traffic | `tunnel` | alktunnels **route 1**: adapter-side bridge — `TunnelSession::open_direct` on a held `ChannelClient`, pump `take_halves()` vs the channel | target = `SubstrateAddr::HostPort`/`Path`; `Substrate` = Tcp/Unix; accept only **after** the fabric open succeeds — failure maps to an explicit `ConnectFailed` reject (POC-3 §C.2); vanilla `-D`'s dynamic targets dispatch here (POC-3 correction 2) | -| `forwarded-tcpip` | `ssh -R` data channels | `tunnel` | the `register_tunnel_forwarded_openable`'s `ForwardedHandler` shape — hand the channel duplex to the handler | per fabric arrival the door opens a `forwarded-tcpip` channel **toward the vanilla client** (a vanilla client never initiates this type — sshd parity; an inbound open is the unvanilla class, declined `ConnectFailed`); echo args = the original `tcpip_forward` request's wire string verbatim + the actually-bound port (the sshd `c->path` + `get_local_port` parity — a `port = 0` mint echoes the allocated number); originator = `TunnelForwardedParams.peer`; correlation-miss → teardown-at-birth (never a half-open channel); the accept is the crate's per-session channel cap (the acceptor vocabulary) | +| `forwarded-tcpip` | `ssh -R` data channels | `tunnel` | the `register_tunnel_forwarded_openable_with_establisher` gate shape (alktunnels 0.5.0, review-003 W2 adoption): the router's correlation lookup rides the establisher's `ForwardedGate`; the handler's `ForwardedHandler` shape hands the channel duplex to the pump | per fabric arrival the door opens a `forwarded-tcpip` channel **toward the vanilla client** (a vanilla client never initiates this type — sshd parity; an inbound open is the unvanilla class, declined `ConnectFailed`); echo args = the original `tcpip_forward` request's wire string verbatim + the actually-bound port (the sshd `c->path` + `get_local_port` parity — a `port = 0` mint echoes the allocated number); originator = `TunnelForwardedParams.peer`; a stale/missing correlation token fails the open **typed at the call plane** (`channel:open_failed`, `unknown_resource`) before any channel is born (the gate's miss class); the narrow gate-pass→handler-run residual race and the cap-admission declines still teardown-at-birth (never a half-open channel); the accept is the crate's per-session channel cap (the acceptor vocabulary) | | `direct-streamlocal` | unix-socket forward | `tunnel` | `SubstrateAddr::Path` treatment | compiles wherever `tunnel` compiles; serving is **runtime-Unix-gated** — on Unix serves, on other platforms a clean open-decline (OQ-11 resolved: universal compile + runtime decline, no per-platform feature fork) | | `x11` | X forwarding | — (never) | — | always declines; not feature-gated (ADR-003 §5) | | `auth-agent` | agent forwarding | — (never) | — | always declines; the anti-pattern of the alkvault custody posture (ADR-003 §5) | @@ -58,8 +58,10 @@ command → parse-then-fail). `Handler::tcpip_forward(address, &mut port) -> bool` + `Handler::streamlocal_forward` (+ both cancel forms) — gate on `tunnel` (unfeatured declines both the listen requests and the data -channels). The served flow (`w3-tunnel-listen`, decidable against -alktunnels 0.4.1's actual surfaces with POC-3 §C.4 as the template): +channels). The served flow (`w3-tunnel-listen`; the route-1 classes +decided against alktunnels 0.4.1's recorded surfaces, the +establisher shape against 0.5.0 — the review-003 W2 adoption — with +POC-3 §C.4 as the template): - **The listen dial** — family-ward through the consumer surface: `TunnelListener::open(channel_client, TunnelParams {resource: ` wrap at the seam (POC-2-proven; bounds the plaintext from adapter-takeover onward; ssh-key zeroizes the decoded key bytes on drop). The remaining unfenced pre-wrap window is **alkvault's API shape to close** — the ask is filed in alkvault's own tracker (downstream/family-space concern; alkforge and every secrets-bearing crate benefits, not just alkssh). alkssh does not carry it, does not gate on it, and ADR-005 records only the adapter obligation. When alkvault ships a `Zeroizing`-out API, ADR-005's reference here updates as a one-line doc touch, nothing else. +- **Status**: resolved — the alkssh-level answer was the adapter-side wrap; the vault-side improvement belongs upstream, not here. **Superseded upstream 2026-11-04 (alkvault 0.2.0)**: the ask shipped — `decrypt` returns `Zeroizing`, zeroizing the buffer on every path including the error one, and the `rotate` path never holds a bare plaintext either. +- **Resolution**: **ADR-005 decision 3 was the contract on this path**: the adapter owned a `Zeroizing` wrap at the seam (POC-2-proven; bounds the plaintext from adapter-takeover onward; ssh-key zeroizes the decoded key bytes on drop). The remaining unfenced pre-wrap window was **alkvault's API shape to close** — the ask was filed in alkvault's own tracker (downstream/family-space concern; alkforge and every secrets-bearing crate benefits, not just alkssh). alkssh did not carry it, did not gate on it, and ADR-005 recorded only the adapter obligation. With alkvault 0.2.0 the window is closed upstream: the adapter-side wrap is dropped (a double wrap would be dead posture), `decrypt_and_decode` consumes the vault's zeroized return directly, and ADR-005's decision-3 pointer is amended — the predicted one-line doc touch, exactly. - **Cross-references**: ADR-005 §3, OQ-05 ## Theme D — session-request mapping details @@ -99,7 +99,7 @@ noted at the end and carry their work where their owners are. - **Status**: resolved — as asked, this was never a decision to make: the convention alkssh consumes does not change (alktty's exit-chunk convention, alktty ADR-004 / alknet ADR-055 lineage — no widening proposed or justified; "easier for alkssh" is not a tty/pty-grounded justification, and any widening ripples `ControlMessage::Exit`'s parsed shape, the backend API, and that ADR lineage, so it needs its own alktty ADR). What the entry surfaced was (a) a *derivation contract* on alkssh's side and (b) an upstream *fidelity limitation* — both now recorded where they constrain. - **Resolution**: - **The derivation contract** is formalized as `mapping.md` §"Exit chunk → SSH exit reporting" (the contract behind the `exit_status`/`exit_signal` rows): `code ≥ 0` → `exit-status(N)`; negative `−N` on the Unix pipe path → `exit-signal(name)` via a server-side Unix signal-number→name table; `−1` → `exit-status(255)` and never a signal claim (the sentinel collides with SIGHUP; fabricating `exit-signal("HUP")` for a wait failure invents a signal that never happened); non-Unix negatives → the `−1` rule; `core_dumped` never asserted. POC-3 §D.2 observed the pipe-path derivation live (`−15` → `exit-signal("TERM")`). - - **The upstream constraint**: the PTY path loses the signal number (portable-pty 0.9 reaps a signal-terminated Unix child as a plain code 1 through its trait surface — verified against its source) → PTY-row signal-terminated sessions arrive as `exit-status(1)`. Faithful `exit-signal` on the PTY row is an **alktty/portable-pty ask with tty/pty-grounded justification**; alkssh's mapping adapts automatically with no contract change if the convention ever holds on that path. Recorded as rule 5 of `mapping.md`'s derivation contract (not tracked here — it constrains alktty, not alkssh). + - **The upstream constraint** (superseded 2026-11-04, alktty 0.8.2): the PTY path used to lose the signal number (portable-pty 0.9 reaps a signal-terminated Unix child as a plain code 1 through its trait surface — verified against its source). alktty 0.8.2 retired that constraint consumer-input-driven (alktty review 004, this entry's bookkeeping): its Unix PTY waiter now reaps the direct child via `waitpid` and decodes signal deaths as `−N`, so the convention holds on both paths and the PTY-row signal-terminated session reports `exit-signal(name)` with **no alkssh contract change** (the negative code is consumed verbatim; `mapping.md` rule 5 amended). The `-1` sentinel collision (SIGHUP vs wait-failure) remains on the pipe path and gains the cancel path — the rule-3 reading stands unchanged. - **Cross-references**: OQ-08, [mapping.md](mapping.md) (derivation contract + `exit_signal`/`exit_status`/`signal` rows), ADR-006, alktty ADR-004 / alknet ADR-055 ### OQ-11: ~~`direct-streamlocal` platform surface — compile-time or runtime decline shape?~~ — resolved @@ -130,13 +130,15 @@ noted at the end and carry their work where their owners are. Recorded so the dependency is visible, with the work carried where their owners are: -- **PTY-path exit fidelity** — needs alktty/portable-pty work (with - tty/pty-grounded justification) before faithful `exit-signal` can - hold on the PTY row; alkssh adapts with no contract change - (`mapping.md` derivation contract, rule 5; OQ-10). - **alkfs landing** — the `sftp` placeholder's eventual replacement (family ADR-002); alkssh writes the sftp mapping + ADR when the protocol exists (OQ-13). -- **alkvault `Zeroizing`-out `decrypt`** — closes the pre-wrap - plaintext window upstream (OQ-07 / ADR-005 §3); a one-line doc touch - for alkssh when it ships. \ No newline at end of file +- ~~**PTY-path exit fidelity**~~ — closed upstream (alktty 0.8.2, + review 004's resolution): the PTY waiter now reports signal deaths + as `−N`; the derivation contract needed no alkssh change + (`mapping.md` rule 5 amended; OQ-10). +- ~~**alkvault `Zeroizing`-out `decrypt`**~~ — closed upstream + (alkvault 0.2.0, review 001's resolution; also closes the `rotate` + window and zeroizes the error path): the adapter seam wrap is + dropped, ADR-005 decision 3 + OQ-07 amended as the predicted + one-line doc touch. \ No newline at end of file