7.6 KiB
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 '<path>' and
git-receive-pack '<path>' 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.