Files
IronStorage/docs/git-synchronization.md
Georg Bauer 300ccf5f1f
Some checks failed
Dependency security audit / rustsec (push) Has been cancelled
Use repository Git remotes and SSH config
2026-08-26 09:32:21 +02:00

132 lines
7.7 KiB
Markdown

# Embedded Git and synchronization
All Git behavior is implemented in `crates/storage`. IronStorage never launches
`git`, a credential helper, an SSH client, a hook, a filter, or a merge driver.
Repositories are opened with isolated configuration and environment access;
repository-local configuration that can affect an operation is rejected.
Upstream `pass git init` deliberately writes `diff.gpg.binary` and a
`diff.gpg.textconv` GnuPG command. IronStorage preserves these passive keys for
compatibility but never evaluates them: it reads Git blobs and renders decrypted
diffs through Rust storage APIs. Frontends cannot add or change diff drivers.
This behavior follows the
[upstream password-store initialization](https://git.zx2c4.com/password-store/tree/src/password-store.sh)
while keeping runtime helper execution disabled.
`GitRepository` initializes and opens password-store worktrees, selects the
innermost repository for a nested entry, and implements status, log, diff, add,
commit, remote, and local-config operations. Successful insert, edit, generate,
recipient-policy, remove, move, and copy transactions use the same concrete
committer. Only their affected paths are staged, unrelated index state is
preserved, no-op mutations create no commit, and commit failures restore the
index so the storage transaction can roll back its files.
## Remote endpoint contract
Storage parses credential-free HTTPS URLs plus feature-gated `ssh://` and
scp-like SSH URLs into one typed endpoint contract. Local paths, `git://`,
`file://`, helper transports, URL rewrites, separate push URLs, embedded
credentials, and unknown schemes are rejected before transport. A build
without the `ssh` feature reports SSH as unsupported before connection or
repository mutation instead of treating its configuration as malformed.
## HTTPS transport
HTTPS credentials are requested with the configured server ID and application
ID and remain outside Git configuration.
Fetch uses the embedded Rust smart-HTTP client with an explicit credential
callback, so Git's credential cascade is never entered. Push implements the
receive-pack protocol directly: it validates the advertisement, checks the
remote tip is an ancestor, creates a complete Git pack with a SHA-1 trailer,
requests `report-status`, and accepts the update only after both unpack and ref
status succeed. HTTP redirects are disabled so authorization cannot cross an
origin boundary.
## SSH authentication
With the optional `ssh` feature, storage opens an in-process Russh client and
verifies the server key before any authentication or command. Known-host files
are read with size and line bounds; exact, hashed, and bracketed non-default
port entries are supported. Unknown keys require an explicit call to persist
the confirmed key atomically. Changed keys always fail and are never replaced.
Desktop applications take remote names and URLs from the password-store's
`.git/config`. Authentication uses the matching `~/.ssh/config` identity or an
explicit OpenSSH private-key file (Ed25519, ECDSA, or RSA), or one exact SHA-256
fingerprint from an already-running SSH agent.
Encrypted-key passphrases come from `SecretBytes` in the operating-system
secret store. Identity attempts are bounded and deterministic; IronStorage
does not spray keys, use passwords or keyboard-interactive authentication,
start/probe an agent process or run proxy/helper commands. The in-process
client reads `Host`, `HostName`, `User`, `Port`, and `IdentityFile`; executable
OpenSSH directives remain disabled. The explicit client allowlist excludes SHA-1 key exchange/MAC,
RSA/SHA-1 signatures, DSA, CBC, `none`, compression, and host certificates.
Cancellation interrupts connection and authentication without changing Git,
known hosts, or secure storage.
The standard CLI, TUI, and desktop applications compile this transport and pass
their Git actions through storage's typed endpoint selection. They display both
SSH URL forms without reparsing them. An unknown key opens an explicit native
confirmation showing the host, port, algorithm, and fingerprint; a changed key
is a non-bypassable error. Encrypted-key prompts use hidden or masked input and
retain a supplied passphrase only after SSH authentication and the requested Git
operation succeed. Cancelling either prompt leaves the repository, known hosts,
and secure storage unchanged.
## SSH upload-pack
With the same feature enabled, branch discovery, clone, fetch, and pull open a
session channel without a PTY and request only `git-upload-pack '<path>'`.
Storage single-quotes the repository as one shell argument, preserving the
absolute, account-relative, and tilde path semantics of the two supported SSH
URL forms while preventing whitespace, quotes, metacharacters, environment
assignments, or options from selecting another command. No local Git, SSH,
shell, transport helper, or fetch-pack process is involved.
The channel is a bounded adapter into gix's existing pack-protocol client.
Stdout carries only protocol bytes; stderr is retained only as a bounded,
single-line remote-service diagnostic. Storage requires a zero exit status and
clean channel completion, distinguishes malformed advertisements and packs from
network, trust, authentication, service, and cancellation failures, and closes
the channel promptly when cancellation is requested. Gix continues to own pack
verification, object limits, and atomic ref updates, while the existing
IronStorage code continues to own clone staging, checkout, merge, conflict, and
rollback behavior.
SSH push opens the same verified and authenticated session-channel boundary for
`git-receive-pack '<path>'`. Storage validates the advertisement, rejects
non-fast-forward updates before sending, constructs the complete reachable
object pack, then half-closes channel input and drains the status and bounded
stderr streams. The remote-tracking ref advances only after `unpack ok`, an
`ok` for the selected ref, a zero service exit, and clean channel completion.
Unpack and ref-policy rejections remain typed failures; a malformed response,
disconnect, or cancellation after sending is an unknown outcome that requires
a fresh fetch before retrying and is never replayed automatically. Synchronize
always completes pull first and cannot open receive-pack after a failed,
cancelled, or conflicted pull.
Pull refuses a dirty worktree. It fast-forwards when possible and otherwise
uses the embedded three-way tree merge. Unresolved paths are returned as typed
`MergeConflicts`; no conflict markers or partial checkout are written. Checkout
prevalidates tree entries, rejects links and submodules, writes private files
atomically, updates the real Git index, and rolls the worktree/index back if the
reference update fails. `sync` performs pull before push, while clone builds in
a private sibling directory and installs the completed vault with a rename.
Commit signing is optional. The embedded OpenPGP key store signs the canonical
unsigned commit bytes and adds an ASCII-armored `gpgsig` header compatible with
Git/GPG without invoking `gpg`.
The transport boundary is injectable for deterministic compatibility tests.
HTTPS tests inspect credentials, advertisements, receive-pack commands, object
counts, pack checksums, non-fast-forward behavior, and server status. SSH tests
use a pure-Rust in-process Russh server and upload-pack fixture to exercise
chunked reference and pack streams, end-to-end clone/fetch/pull, command
quoting, cancellation, and rollback without an external Git or SSH executable.
Frontend tests cover typed transport display, prompt masking and cancellation,
retry routing, conflicts, authentication failures, and successful HTTPS
regression paths without duplicating protocol logic.
The final two-URL lifecycle, adversarial failure, dependency, target, and
artifact matrix is maintained in `ssh-transport-audit.md`.