diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 1ce8187..34a6497 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -36,6 +36,8 @@ below are being written. No implementation until specs reach | ADR | Title | Status | |---|---|---| | [ADR-001](decisions/001-alkssh-alpn-pin.md) | The `alk/ssh` ALPN pin — edge-termination contract for `SshAdapter` | Accepted | +| [ADR-002](decisions/002-russh-pin.md) | The russh pin — 0.65.0, aws-lc-rs, no compression, RSA in | Accepted | +| [ADR-003](decisions/003-feature-graph.md) | The capability feature graph — flat, independent, two-direction | Accepted | | [ADR-004](decisions/004-auth-dispositions.md) | Auth dispositions — publickey-only, fail-closed, fingerprint identity | Accepted | | [ADR-005](decisions/005-at-rest-key-custody.md) | At-rest key custody — vault-wrapped OpenSSH-PEM, decrypt-at-use | Accepted | diff --git a/docs/architecture/decisions/002-russh-pin.md b/docs/architecture/decisions/002-russh-pin.md new file mode 100644 index 0000000..46d1199 --- /dev/null +++ b/docs/architecture/decisions/002-russh-pin.md @@ -0,0 +1,99 @@ +--- +status: draft +last_updated: 2026-10-26 +--- + +# ADR-002: The russh pin — 0.65.0, aws-lc-rs, no compression, RSA in + +## Status + +**Accepted** — 2026-10-26. A one-way door in practice (SSH negotiation +behavior crosses crate boundaries; bumps are deliberate, tested, and +recorded per the repo's wire-stability convention). + +## Context + +`russh.md` documents the crate inventory and the pin questions; the +2026-10-26 re-verification section carries the current facts. The POCs +validated against 0.64.1 (then-newest) with the `ring` backend; since +then **0.65.0** has been published (2026-10-25) with six security +fixes — three in classes an internet-facing SSH door cares about +(client EXT_INFO unbounded allocation, peer zero-max-packet-size +channel stall, pre-auth nested-certificate stack exhaustion) — and two +breaking changes confined to handler-signature shapes +(`PublicKeyOffer` / `VerifiedPublicKey`; `async-trait` feature +removed). + +The family's crypto-posture context: `alktls`, `alkhttp`, and +`alknet` all standardize on `rustls` with the **`aws_lc_rs`** feature; +no family crate links `ring`. The provider choice matters only as +binary-composition hygiene — when alkforge's binary links alkssh +*and* the family's rustls stack, one shared primitive library is the +goal — and has nothing to do with TLS on the SSH wire. + +## Decision + +1. **Pin `russh = "0.65"`** (caret-compatible within 0.65.x, matching + the family's `"0.10"`-style convention; russh minors have broken + API before, so the caret stops at the minor). 0.65.0 over 0.64.1: + current published + the three security fixes above; the breaking + changes land inside the handler surface alkssh implements anyway. + POC evidence transfers (wire behavior unchanged); the + implementation phase re-verifies the POC-1 decline matrix against + 0.65 as contract tests. +2. **Crypto backend: `aws-lc-rs`.** Family-confirmed (`alktls` / + `alkhttp` / `alknet` all `rustls(aws_lc_rs)`; nothing in-family + links `ring`) and it is russh's own default. Verified in-scratch: + builds clean in ~50 s with no external provisioning (prebuilt + bindings), tree `aws-lc-rs 1.18.2` / `aws-lc-sys 0.46.0`. The + POCs' `ring` build is recorded as the fallback if a target + platform ever fails to provision aws-lc — a deliberate, + recorded deviation, never drift. +3. **`flate2` off — no compression.** The POCs already ran + compression-free and vanilla OpenSSH was untroubled; SSH + compression is largely pointless at modern link layers and is the + CRIME-adjacent attack surface OpenSSH itself defaults off. + Bounded-resources and surface-reduction posture agree. +4. **`rsa` on.** This crate is the family's vanilla-SSH *front door*: + alkforge users bring their own keys, and RSA remains common in the + wild. Fingerprints work identically for RSA keys (the identity + model doesn't care), so enabling `rsa` buys real compatibility at + the cost of two RC-pinned deps (`rsa =0.10.0-rc.18`, + `pkcs1 =0.8.0-rc.4`). The RC-pinned tree (`ssh-key =0.7.0-rc.11` + exact-pinned by russh, plus these two) is **accepted for the + 0.1.0 line** with the revisit trigger recorded: bump/relax when + `rsa` and `ssh-key` hit stable — a deliberate, recorded posture, + not drift. The earlier "rsa-off leaning" from the research docs is + superseded: it served the RC-avoidance goal and would have cost + front-door compatibility; front-door compatibility wins. +5. **`des` / `dsa` never.** russh's own "Danger: insecure" features — + no alkssh configuration may enable them. +6. **Manifest lands with the dependency**: `rust-version = "1.89"` + (russh's MSRV; supersedes the current 1.88) and, with it, the + edition-2024 bump the POCs compiled under. + +## Consequences + +- **Positive**: the pin is current, carries the security fixes, and + matches the family crypto posture; the tree is fully inventoried + (RC set identical to the POC tree); MSRV/licensing are explicit. +- **Negative**: the RC deps ship in a published crate's tree until + the revisit trigger fires; aws-lc-rs is the heavier build (proven + acceptable here, with `ring` as the recorded fallback). +- **Neutral**: ADR-004's dispositions are signature-agnostic — at + 0.65.0 the key arrives as `VerifiedPublicKey.public_key` and the + new `algorithm` field is available for future hardening (server-side + reaction to client-signature-algorithm choice); no disposition + changes. + +## References + +- `docs/research/russh.md` (inventory + the 2026-10-26 + re-verification section with the scratch-build record) +- `docs/research/poc-1-auth-bridge.md` (the 0.64.1 wire evidence; + backend note), `docs/research/poc-2-vault-keys.md`, + `docs/research/poc-3-mapping-bakeoff.md` (0.64.1 runtime facts) +- ADR-001 (the pin context), ADR-003 (feature graph the russh + features compose under), ADR-004 (auth dispositions on this surface) +- Family: `alktls`/`alkhttp`/`alknet` manifests (the aws-lc-rs + posture), family ADR-002 decision 5 \ No newline at end of file diff --git a/docs/architecture/decisions/003-feature-graph.md b/docs/architecture/decisions/003-feature-graph.md new file mode 100644 index 0000000..4c8ca95 --- /dev/null +++ b/docs/architecture/decisions/003-feature-graph.md @@ -0,0 +1,99 @@ +--- +status: draft +last_updated: 2026-10-26 +--- + +# ADR-003: The capability feature graph — flat, independent, two-direction + +## Status + +**Accepted** — 2026-10-26. Builds the fail-closed capability contract +(family ADR-003 decision 3) into the crate's own feature names; the +decline paths are contract territory on every side of every feature. + +## Context + +`mapping-table.md` (POC-3-validated) split the SSH `session` channel +one channel type carries shell, exec, git, and SOCKS traffic, and +vanilla SSH multiplexes them all through session channels. The feature +graph is how alkforge gets an endpoint that is *structurally incapable* +of serving what it didn't enable: a git-only build accepts no shell, no +tunnels, no SOCKS — and, in the opposite direction, no capabilities a +client build didn't opt into. + +Phase 0 validated the seams end to end (POC-3 stages A–D: git exec, +SOCKS-in-channel, direct-tcpip via the alktunnels bridge, session/pty, +all against published siblings) and the user's direction confirmed the +scope: full feature set except sftp (placeholder until alkfs lands). + +## Decision + +1. **The feature set** (cargo features on `alkssh`, all off by + default): + + | Feature | Serves (inbound) | Consumes (outbound) | Family seam | + |---|---|---|---| + | `session` | session channels: pty/shell, pipe-mode exec | client pty/shell/exec/subsystem opens | alktty | + | `git` | git-parseable exec (dispatched out of session channels) | client git consumption of upstream servers | alkgit | + | `tunnel` | `direct-tcpip`, `forwarded-tcpip`, `direct-streamlocal`, the `tcpip_forward`/`streamlocal_forward` listen flow | client `direct-tcpip` opens; the client `-R` leg (listen request + forwarded-channel acceptor) | alktunnels | + | `dynamic-forward` | SOCKS5-in-channel on session channels (family-native client shape) | client-side SOCKS consumption | alksocks | + | `sftp` | *placeholder — declines until alkfs lands (OQ-13)* | client sftp half, also deferred | alkfs (future) | + +2. **Flat and independent.** No feature implies another. `default = []` + — a default build has no capabilities at all; every unfeatured + channel type/open/request declines with a clean error. `git` does + not imply `session` (a git-only build serves the git-parseable exec + path by declining to parse to anything else — the parse-then-fail + contract from POC-3 stage A); `dynamic-forward` does not imply + `session` (SOCKS-in-channel rides session channels but is gated at + dispatch, before any serve). +3. **Two-direction symmetry.** Each feature name gates *both* the + server-direction (what an embedded alkssh serves vanilla clients) + and the client-direction (what a family app drives when dialing an + upstream SSH server, including the forwarded-channel acceptor rows). + One name, one capability, both doors — this is the symmetry rule, + now structural: a `git`-only build cannot dial a tunnel upstream + any more than it can serve one. +4. **Client-side fail-closed at the acceptor.** Server-initiated + channel opens (`forwarded-tcpip` is the real one — the `-R` data + path) are served only under a registered feature's acceptor + handler; unhandled/unfeatured accepted opens are **declined**, not + accept-and-dropped (russh's client default is accept-then-drop — + ours to fix; the reverse-runner extraction names it). +5. **Never features.** `x11` and `auth-agent` forwarding are not + feature-gated at all — always declined, at any configuration + (agent forwarding is the *anti-pattern* of the alkvault custody + model; x11 has no family seam). An ADR amendment is required to + ever feature-gate them. +6. **Honest advertisement per build.** What the wire advertises + mirrors what features are on: auth `methods` is always + `{publickey}` (ADR-004) server-side; client-side capabilities are + invoked only by the assembly, and unfeatured rows decline. There + is no "advertise and see" path. + +## Consequences + +- **Positive**: alkforge's ssh endpoint shape is a straight cargo + selection; the failure matrix (git-only, session-only, none) is a + tested decline matrix rather than a hope; the client and server + surfaces can't drift apart on capability naming. +- **Negative**: the split of `session` makes feature interplay a + real test matrix (per-feature decline paths on both directions are + contract tests — the AGENTS.md verification convention covers this + via `--all-features` runs plus the decline-matrix suite). +- **Neutral**: feature *names* are now contractual (downstream builds + specify them); renames would be semver-visible — hence the ADR, not + incidental naming. + +## References + +- `mapping-table.md` (the validated rows under these names + the + feature-split notes + the three POC-3 corrections) +- Family ADR-003 (decision 3 — the fail-closed capability features), + family ADR-002 (why exclusion matters for alkforge) +- `docs/research/reverse-runner.md` (client-direction surface the + symmetry rule serves; the acceptor default fact) +- `docs/research/poc-3-mapping-bakeoff.md` (stages A–D: the seams at + these names work end to end) +- OQ-13 (sftp placeholder tracking), OQ-02 (dispatch precedence + inside `session`) \ No newline at end of file diff --git a/docs/research/russh.md b/docs/research/russh.md index 592111c..9a6692f 100644 --- a/docs/research/russh.md +++ b/docs/research/russh.md @@ -6,10 +6,30 @@ time before any pin is recorded. ## Version landscape -- Current published: **russh 0.64.1** (2026-10-05, docs.rs). 0.64.1 is - a security-fix-only patch of 0.64.0 (GHSA-4wc5-f2rc-q74m — unbounded - memory allocation in the agent protocol server from crafted - constraint records; no API change). +**Re-verified at architecture time (2026-10-26) — see the section at +the end of this file.** russh **0.65.0** was published 2026-10-25 +(after the POCs verified 0.64.1 as newest); the pin ADR is written +against 0.65.0. The POC wire-behavior evidence transfers: the 0.65.0 +breaking changes are handler-signature shapes, not wire behavior. + +- Current published: **russh 0.65.0** (2026-10-25, GitHub releases). + Breaking: `Handler::auth_publickey_offered` now takes a + `server::PublicKeyOffer` and `Handler::auth_publickey` a + `server::VerifiedPublicKey` (read the key from the `.public_key` + field; a new `.algorithm` field exposes the client's signature + algorithm, allowing server-side reaction to it); the `async-trait` + dep/feature was removed. Security fixes in classes this crate cares + about: GHSA-4p9f-7p9m-5vfg (client: unbounded allocations via + EXT_INFO packets), GHSA-fw5c-8223-6h42 (peer-declared zero maximum + packet size permanently stalls channel writes), + GHSA-3hm4-p7h4-rvwp (pre-auth stack exhaustion via nested OpenSSH + certificates); the agent/pageant fixes are off our path (agent + forwarding declines, ADR-003). +- 0.64.1 (2026-10-05) was the POC pin — security-fix-only patch of + 0.64.0 (GHSA-4wc5-f2rc-q74m — unbounded memory allocation in the + agent protocol server; no API change). The POC findings' evidence + and this file's earlier inventory facts were verified against + 0.64.1 and carry to 0.65.0 as noted above. - 0.64.0 (2026-10-04) broke API: dropped the `legacy-ed25519-pkcs8-parser` feature (pre-0.43 non-compliant ed25519 keys now hard-error); fixed GHSA-j7h2-66j4-ghjf (server @@ -232,4 +252,26 @@ exists but is out of family scope (agent forwarding declines; see - russh GitHub releases 0.64.0 / 0.64.1 (GHSA ids quoted above) - alknet `main` `crates/alknet-core/src/auth/` — rough reference only, explicitly not decision input (0.49-era API; verify nothing against - it) \ No newline at end of file + it) + +## Re-verification at architecture time (2026-10-26) + +The "versions re-verified at architecture time" clause, executed. Facts +verified today via `cargo info` / GitHub release notes / a scratch +build (`/tmp/opencode/alkssh-pin-check`, disposable): + +| Fact | Verified value | +|---|---| +| Current published | **0.65.0** (2026-10-25) — see Version landscape above | +| Backend feature | `aws-lc-rs` (dashed feature name; also russh's own default alongside `flate2`, `rsa`) | +| Scratch build | `default-features = false, features = ["aws-lc-rs", "rsa"]` — clean, ~50 s, **no external provisioning** (prebuilt aws-lc bindings cover this platform; `aws-lc-rs 1.18.2` / `aws-lc-sys 0.46.0`) | +| Tree (the pin set) | `ssh-key =0.7.0-rc.11` (exact), `rsa =0.10.0-rc.18`, `pkcs1 =0.8.0-rc.4`, `getrandom 0.4.3`, `rand_core 0.10.1` — same RC set as the POCs' 0.64.1 tree | +| MSRV | 1.89 (both 0.64.1 and 0.65.0) | +| License | Apache-2.0 only | + +The earlier sections describe the 0.64.1-era surfaces (POC-verified); +`server::PublicKeyOffer`/`VerifiedPublicKey` and the removed +`async-trait` feature are the only inventory deltas. Wire behavior +(evidence base for ADR-001/004/005) is unchanged per the release +comparison; the re-verification of the decline matrix against the +actual pin is implementation-phase contract-test territory. \ No newline at end of file