# Pure-Rust Git-over-SSH release audit Reviewed 2026-08-25. This is the final milestone-07 matrix for the optional storage `ssh` feature. The production transport is a Russh session connected directly to the configured server; it opens only Git upload-pack or receive-pack channels and never delegates to a local Git, SSH, shell, transport helper, or proxy process. ## Remote lifecycle matrix The two spellings enter one `RemoteEndpoint::Ssh` contract. URI paths remain absolute; scp-like paths remain account-relative. The tests below run a pure-Rust in-process SSH server with synthetic keys and Git objects. | Surface | `ssh://` | scp-like | Executable evidence | | --- | --- | --- | --- | | Parse, add, set, get, reject helpers | yes | yes | `ssh_remote_endpoints_parse_to_one_typed_contract`, `ssh_feature_allows_both_remote_forms_through_add_set_and_get`, configuration rejection tests | | Branch discovery and clone | yes | yes | `both_ssh_url_forms_drive_discovery_clone_fetch_fast_forward_merge_and_conflict` | | Explicit fetch and fast-forward pull | yes | yes | same full-lifecycle test, including chunked advertisements and packs | | Three-way merge and typed conflict pull | yes | yes | same full-lifecycle test; conflicts preserve the local worktree | | New, fast-forward, and already-current push | yes | yes | `new_branch_fast_forward_and_already_current_push_are_confirmed` | | Pull-then-push synchronization | yes | yes | `synchronization_pulls_then_pushes_over_both_ssh_url_forms` | | Progress, cancellation, and retry | transport-neutral | transport-neutral | controlled Git tests, slow upload/push cancellation, unknown-host confirm/retry, encrypted-key reject/retry, and frontend interaction tests | | CLI, TUI, and desktop presentation | typed SSH | typed SSH | frontend selection, masked prompt, cancellation, authentication-error, conflict, progress, and HTTPS regression tests | The in-process server implements the same `git-upload-pack ''` and `git-receive-pack ''` exec-channel boundary used by a Gitea SSH endpoint. It validates both service commands, packet-line advertisements, chunked pack streams, receive status, and non-fast-forward behavior without a Git or OpenSSH installation. A real Gitea smoke test is optional and must use a disposable repository and synthetic key; it adds no protocol coverage and must never use or print a developer key. ## Authentication and failure matrix | Risk or compatibility case | Enforced result and evidence | | --- | --- | | Key files | Ed25519, ECDSA P-256/P-384/P-521, and RSA/SHA-2 host and user keys authenticate; encrypted OpenSSH keys require the matching protected passphrase. Identity and known-host files are bounded and symlinks are rejected. | | Existing agent | The agent is opt-in, never launched, and one configured SHA-256 fingerprint is selected from at most 64 identities. Missing socket, missing identity, and rejection are distinct typed failures. | | Host identity | Exact, hashed, default-port, and bracketed non-default-port entries are supported. Unknown keys stop before authentication and require explicit persistence; changed or revoked keys stop before authentication and cannot be replaced through confirmation. Confirmation is bound to the configured host and port. | | Algorithms | The client owns an explicit allowlist: ML-KEM/X25519, Curve25519, SHA-2/SHA-512 finite-field KEX; Ed25519, ECDSA, and RSA/SHA-2 host/auth signatures; ChaCha20-Poly1305 or AES-GCM/CTR; SHA-2 MACs; no compression. DSA, RSA/SHA-1, SHA-1 KEX/MAC, CBC, `none`, and host certificates are excluded. A unit test rejects policy regression. | | Paths and commands | Absolute, relative, and `~user` paths are preserved as one shell-quoted argument for exactly one built-in Git service. Quotes, whitespace, metacharacters, environment-looking text, and leading options cannot select another command. | | Network and server failures | Reserved-name DNS failure and refused connections are typed as network unavailable. Channel rejection, non-zero exit, signal, malformed packet/pack/status, early EOF, and disconnect are separately exercised. Remote diagnostics are control-sanitized and capped at 8 KiB. | | Slow/chunked I/O and cancellation | Channel chunks are capped at 32 KiB with queues of eight chunks. Connection and service completion time out after 30 seconds. Slow upload cancels without a destination; push cancellation after transmission returns unknown outcome and never advances tracking or blindly replays. | | Rollback and ambiguity | Clone uses a private sibling and atomically installs only a complete repository. Fetch/pull failures preserve refs/worktree. Push accepts success only after unpack, ref status, zero exit, and clean close; ambiguous completion requires a fresh fetch. Sync never pushes after a failed or conflicted pull. | | Secrets | Key/passphrase bytes use zeroizing storage types, never configuration, history, arguments, diagnostics, notifications, or clipboard. A prompted passphrase is fingerprint-bound and persisted only after successful authentication and Git work. | ## Dependencies, advisories, and size Russh is exactly pinned to `0.63.1` (Apache-2.0), requires Rust 1.85, and is built without default features using only Ring and RSA support. Tokio is pinned by the lockfile at 1.53.1 (MIT) with only I/O, macros, network, multi-threaded runtime, synchronization, and time features. The selected licenses are compatible with IronStorage's MIT release; `DEPENDENCIES.md` records the full decision. `cargo audit` 0.22.2 reports RUSTSEC-2023-0071 for `rsa` 0.10.0-rc.18 through Russh and for `rsa` 0.9.10 through the pre-existing OpenPGP/packaging graph. RustSec lists no patched release. The finding is not suppressed from the manual audit: it remains documented here and in `DEPENDENCIES.md`. The Gitea security workflow ignores only that named no-fix advisory so every other new vulnerability still fails pushes, pull requests, and the weekly audit. On the reviewed release build, storage `full` without SSH contains 422 unique packages and a 15,961,360-byte optimized rlib. `full,ssh` contains 489 packages and an 18,376,592-byte rlib: 67 packages and 2,415,232 bytes (15.1%) added at the library artifact. Final executable size varies by target and linker, so release package sizes are recorded with the issue verification rather than treated as a stable source invariant. ## Target and packaging contract The CLI, TUI, and desktop manifests explicitly enable `ssh`; the tagged release workflow builds those ordinary artifacts for ARM64/x86-64 Linux and macOS and x86-64 Windows. Target-specific `cargo tree -e features` checks must show Russh through storage `ssh` on all three platforms. The macOS application package must contain all three SSH-enabled binaries. The Apple bridge explicitly selects storage `full` with default features off and does not select `ssh`. Both iPhone simulator and device static libraries therefore exclude Russh while retaining typed SSH endpoint parsing and the pre-connection unsupported-transport error. Apple archives must preserve that graph until an Apple target explicitly opts in and adds a platform-specific host/key interaction design. ## Deliberate OpenSSH differences IronStorage does not read arbitrary OpenSSH configuration, `ProxyCommand`, `Match`, URL rewrites, separate push URLs, host certificates, password or keyboard-interactive authentication, agent forwarding, arbitrary remote commands, local transport helpers, or local-path remotes. These are explicit security boundaries, not partial implementations. HTTPS smart Git remains the unchanged alternative transport and shares the same embedded repository, merge, rollback, progress, and cancellation contracts.