Complete SSH transport release audit (#118)
Some checks failed
Dependency security audit / rustsec (push) Has been cancelled

This commit is contained in:
2026-08-25 22:07:47 +02:00
parent a737e74aae
commit a3da9fda69
18 changed files with 489 additions and 212 deletions

View File

@@ -0,0 +1,94 @@
# 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.