# russh: crate inventory and pin considerations Status: Draft (phase 0 research). Facts from docs.rs and the GitHub release notes as of 2026-10-25; versions re-verified at architecture time before any pin is recorded. ## Version landscape **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, a 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 pubkey auth accepted mismatched signature algorithms, e.g. rsa-sha2-512 silently downgraded to rsa-sha1); added `Handle::hostkeys_prove` and `Handle::send_global_request`; fixed `Limits` not being fully enforced. - There are older russh families (0.4x–0.5x) with materially different handler shapes in circulation; one rough family-internal integration example exists (alknet `main`, pinned 0.49) but it is a reading aid only: a rough working example, months stale, and **not decision input** for anything here. The pin is chosen against the current published russh, verified at architecture time. ## Requirement posture - **License: Apache-2.0 only.** The family crates are MIT OR Apache-2.0. Depending on an Apache-2.0-only dependency from a dual- licensed crate is clean (distribution under the Apache-2.0 branch of our license satisfies it); record this in the pin ADR so the audit trail is explicit. - **MSRV 1.89, edition 2024.** Our crate's `rust-version` must be ≥ russh's once the dependency lands (currently `1.88` in the manifest, a pending decision, deferred to the pin ADR). - **Crypto backend: exactly one of `aws-lc-rs` (default) or `ring`.** The build errors if zero or both are selected. Feature choice is a phase 1 decision (alktls family precedent may pull one way). - Other features: `flate2` (compression, default), `rsa` (default; pulls **RC-pinned** `rsa =0.10.0-rc.18` + `pkcs1 =0.8.0-rc.4`), `async-trait`, `des`/`dsa` ("Danger: insecure"; never enable), `serde`. The `rsa` question interacts with the RC-pin question below. - **RC dependencies in the current pin set:** `ssh-key =0.7.0-rc.11` (exact-pinned by russh itself via `russh::keys::ssh_key` re-export), `rsa =0.10.0-rc.18`, `pkcs1 =0.8.0-rc.4`, `getrandom 0.4`. Exact pins mean consumers get the same RCs; but an RC in the tree of a crate we intend to publish is a supply-chain/stability posture point the pin ADR must weigh (wait for ssh-key 0.7.0 stable? accept the RC? which russh family avoids it?). ## Server surface (`russh::server`) ### `server::Handler` trait (async trait, not dyn-compatible) Required: `type Error: From + Send`. Everything else is provided methods; at 0.64.1 the default auth impls are **reject** (`Auth::reject()`) for `auth_none`, `auth_password`, `auth_keyboard_interactive`, `auth_publickey`, and `auth_openssh_certificate`; only `auth_publickey_offered` defaults to accept (verified in 0.64.1 source; POC-1 corrected this doc's earlier accept-defaults assumption). Non-auth defaults decline/no-op. The fail-closed discipline is nonetheless *ours to implement explicitly* as a family contract (the pin is a one-way door; defaults could change on a bump), and POC-1 proves the explicit implementations. Key inventory: - Auth: `auth_none(user)`, `auth_password(user, password)`, `auth_publickey_offered(user, &PublicKey)` (pre-auth probe, a cheap rejection hook; key ownership not yet verified), `auth_publickey(user, &PublicKey)` (called **after** russh verified the signature and key ownership), `auth_openssh_certificate(user, &Certificate)`, `auth_keyboard_interactive(user, submethods, Option)`. All return `Result`; russh holds rejection to constant time via `Config::auth_rejection_time`. `auth_succeeded(&mut Session)` fires on success. - Channel opens (each receives `channel: Channel` + `reply: ChannelOpenHandle`; **dropping the handle without calling `reply.accept()`/`reply.reject()` auto-rejects the open**, a natural fail-closed default): `channel_open_session`, `channel_open_direct_tcpip(host_to_connect, port_to_connect, originator_address, originator_port)`, `channel_open_forwarded_tcpip(same params)`, `channel_open_direct_streamlocal(socket_path)`, `channel_open_x11(...).` - Session-channel requests: `pty_request(term, col_width, row_height, pix_width, pix_height, &[(Pty, u32)])`, `shell_request`, `exec_request(data: &[u8])`, `subsystem_request(name: &str)`, `env_request(name, value)`, `window_change_request(...)`, `signal(Sig)`, `x11_request`, `agent_request` (returns whether accepted). Success/failure must be signaled explicitly via `session.channel_success(channel)` / `channel_failure(channel)`. - Data plane: `data(channel, &[u8])`, `extended_data(channel, code, &[u8])` (code 1 = stderr), `channel_eof`, `channel_close`, `window_adjusted`, `adjust_window` (returns next target window). - Global: `tcpip_forward(address, &mut port) -> bool`, `cancel_tcpip_forward`, `streamlocal_forward`, `cancel_streamlocal_forward` (the `-R` listen flow), and `lookup_dh_gex_group`. **Dispatch/runtime facts (POC-3, load-bearing for the adapter):** the session's event loop invokes handler callbacks *inline*: a blocking handler suspends all packet routing for the connection (channel data is only routed by that same loop; an inline serve deadlocks against a peer's follow-up bytes, e.g. a receive-pack push). Every serving path must `tokio::spawn` and return fast. Exit reporting from spawned tasks rides `Session::handle()`, a cloneable server-side `Handle` with `data` / `extended_data(channel, ext, data)` / `eof` / `close` / `exit_status_request` / `exit_signal_request` (send `exit-status` before `close`). Client-side: `Channel::exec()` (no `request_exec`), `window_change`/`signal` are fire-and-forget and must be sent before `into_stream()`, and request-level failures surface as `ChannelMsg::Failure`/`Success` on `wait()`, not as `Err` from the request call; drain past unexpected messages (`continue`), a client `Handler::check_server_key` default that refuses must be accepted explicitly. Details and wire evidence: `poc-3-mapping-bakeoff.md` §A.4/§D.3. ### `server::Config` knobs (bounded-resources posture) `server_id`, `methods: MethodSet` (advertised auth methods; honest advertisement means this must mirror the feature graph), `keys: Vec` (host keys), `certificates: Vec`, `auth_rejection_time` + `auth_rejection_time_initial`, `max_auth_attempts`, `limits: Limits` (bytes/time before rekey), `window_size`, `maximum_packet_size`, `channel_buffer_size`, `event_buffer_size`, `inactivity_timeout`, `keepalive_interval`, `keepalive_max`, `nodelay`, `preferred: Preferred` (algorithm preferences, the place any algorithm restriction would be enforced). Most family bounded-resource conventions map directly: max concurrent channels is additionally ours (alkcall-side per-identity policy when bridged; in-crate when serving locally), session timeout = `inactivity_timeout`, auth attempt limit = `max_auth_attempts`. ## Client surface (`russh::client`) ### `client::Handler` trait Same shape as server: `type Error: From + Send + Debug`; provided methods default sensibly, but **`check_server_key` defaults to reject-all and must be overridden**; host-key policy is a phase 1 design point (for the family client: pin the expected server fingerprint at the assembly layer, per the no-trust-on-first-use direction this implies). Inventory: `check_server_key` (&PublicKeyOrCertificate) → bool; `kex_done`; the full server-initiated channel-open set (`server_channel_open_session / _direct_tcpip / _forwarded_tcpip / _forwarded_streamlocal / _x11 / _agent_forward / _unknown`; the client is an acceptor for forwarded channels, i.e. the `-R` data path) with the same `ChannelOpenHandle` accept/reject semantics; `should_accept_unknown_server_channel`; data plane (`data`, `extended_data`, `exit_status`, `exit_signal`, `xon_xoff`, `window_adjusted`, `channel_eof/close/...`); `disconnected`; `openssh_ext_host_keys_announced` (do-not-await-inside warning; proofs go through `Handle::hostkeys_prove`). ### `client::Handle` Authentication methods: `authenticate_publickey(user, PrivateKeyWithHashAlg)`, `authenticate_password`, `authenticate_none`, `authenticate_keyboard_interactive_start/ respond`, `authenticate_openssh_cert`, the `*_with` signer-injected variants (`authenticate_publickey_with(user, PublicKey, hash_alg, &mut Signer)`, a signer-injection seam; under alkssh's at-rest storage model (held keys are bearer-token-class secrets decrypted at use; see `auth-bridge.md`) it is not presumed to be the vault integration shape, but it exists and POC-2 evaluates it against the simple decrypt-and-decode flow; POC-2 verdict: dismissed, since no provided `impl Signer` exists anywhere in 0.64.1 (the doc's "SSH agent" implementation claim is stale), a custom signer must replicate the splice convention (`to_sign || string{sig}`), and an in-process vault gains nothing over one-time decode at dial; see `poc-2-vault-keys.md` §4), `authenticate_gssapi_with_mic`, `best_supported_rsa_hash` (waits ≤1s for EXT_INFO `server-sig-algs`). Channel opening: only the four typed opens exist: `channel_open_session`, `channel_open_x11`, `channel_open_direct_tcpip(host, port, originator...)`, `channel_open_direct_streamlocal(socket_path)`; there is no generic `channel_open(name)`. On a server **reject**, the typed open call itself resolves to `Err(Error::ChannelOpenFailure(reason))`, and no `wait()`-and-match pattern needed (POC-1 verified with vanilla declines); the session stays usable after declines. Channels expose `wait() -> Option`, `split()`, `into_stream() -> ChannelStream` (AsyncRead + AsyncWrite), `make_reader()/make_reader_ext(ext)/make_writer()/make_writer_ext()` (tokio duplex adapters, **the bridge primitives for every mapping**), `request_pty/request_shell/exec/request_subsystem/set_env/ window_change/signal/eof/close/exit_status`, `writable_packet_size()`. Global: `tcpip_forward(address, port) -> u32` (0 → server picks), `cancel_tcpip_forward`, `streamlocal_forward`, `disconnect`, `send_keepalive`, `rekey_soon`, `no_more_sessions`, `send_global_request(name, data, want_reply)`, `hostkeys_prove`. `client::Config`: `window_size`, `maximum_packet_size`, `limits`, `channel_buffer_size`, `preferred`, `inactivity_timeout`, `keepalive_interval`/`keepalive_max`, `anonymous`, `gex`, `nodelay`, `client_id`. (No client-side auth-attempt limit knob.) ## Keys module (`russh::keys`) Re-exports: the whole `ssh-key` crate (`ssh_key`, pinned `=0.7.0-rc.11`) plus `PrivateKey`, `PublicKey`, `Certificate`, `HashAlg`, `PublicKeyOrCertificate`, `PrivateKeyWithHashAlg`, and loaders (`load_secret_key`, `decode_secret_key`, `load_public_key`, `parse_public_key_base64`, `load_openssh_certificate`, `decode_openssh`). Fingerprint computation lives in `ssh-key` (`PublicKey::fingerprint`, SHA-256 OpenSSH format `SHA256:base64`), relevant to `auth-bridge.md`. `known_hosts::check_known_hosts[_path]` exists for host-key verification. OpenSSH agent interop (`keys::agent`) exists but is out of family scope (agent forwarding declines; see `mapping-table.md`). ## Open pin questions for the ADR All resolved by ADR-002 (2026-10-26, amended 2026-10-27); items below are the questions as phase 0 posed them: 1. Which family/russh line (current stable 0.64.1 is the default candidate) and whether the RC-pinned `ssh-key`/`rsa`/`pkcs1` tree is acceptable or gates the pin. 2. Crypto backend (`aws-lc-rs` vs `ring`); align with alktls. 3. `rust-version` bump in our manifest (≥ 1.89), landing with the dependency. 4. Whether to enable the `rsa` feature at all (family keys are ed25519-first; disabling `rsa` drops two RC deps from the tree). 5. Confirm compatibility with the vanilla clients alkforge needs (ssh + git + scp-era behavior); POC-1's deliverable. ## References - docs.rs russh 0.64.1: server `Handler`, client `Handler`, `Config`s, `Handle`, `keys` module (sections above quote their inventories) - 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) ## 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.