Skip to content

Verify SSH host keys in the remote browser (MITM fix) and pin build tarball checksums#4

Open
timc3 wants to merge 2 commits into
ripplethor:mainfrom
timc3:security/ssh-hostkey-verification
Open

Verify SSH host keys in the remote browser (MITM fix) and pin build tarball checksums#4
timc3 wants to merge 2 commits into
ripplethor:mainfrom
timc3:security/ssh-hostkey-verification

Conversation

@timc3

@timc3 timc3 commented Jun 22, 2026

Copy link
Copy Markdown

Summary

Two hardening changes for the SSH/SFTP attack surface, as two separate commits:

  1. fix: Verify SSH host keys in the remote browser — prevents MITM credential theft (security-critical).
  2. build: Pin SHA-256 of the libssh2/OpenSSL source tarballs — build-time supply-chain hardening.

1. Host-key verification in the libssh2 remote browser

Problem

macfusegui_libssh2_open_session() in LibSSH2Bridge.c performs the SSH handshake and then authenticates immediately, with no host-key verification — no libssh2_session_hostkey() check, no known_hosts comparison, no fingerprint trust-on-first-use.

An active network attacker (rogue Wi-Fi AP, ARP/DNS spoofing, a compromised router/VPN) can impersonate the server. libssh2 completes the handshake against the attacker's key and proceeds to auth. In Password mode the keyboard-interactive/password callback then sends the user's plaintext password to the impostor. Browsing or Test-Connection in Password mode over an untrusted network can therefore leak the password.

There's an asymmetry worth noting: the sshfs mount path already sets StrictHostKeyChecking=accept-new, so it pins the key on first use. The libssh2 browser path had no equivalent.

Fix

Add macfusegui_verify_host_key(), called right after the handshake and before any credentials are sent. The policy mirrors OpenSSH accept-new and shares ~/.ssh/known_hosts with the sshfs path:

  • known host + matching key → connect
  • known host + changed key → refuse, with the offered key's SHA-256 fingerprint in the error
  • unknown host → trust on first use, record + persist the key
  • key unreadable / known_hosts unparsable / HOME unset → fail closed

The error propagates through ensureSessionSyncAppError.remoteBrowserError, so a mismatch surfaces in the browser health banner rather than being swallowed.

User-visible behavior change

The browser / Test-Connection will now refuse a server whose host key changed since it was last trusted (e.g. a legitimately rebuilt host). The error tells the user to remove the stale ~/.ssh/known_hosts entry — the same remedy as OpenSSH.

2. Checksum pinning for build dependencies

scripts/build_libssh2.sh fetched the libssh2 and OpenSSL release tarballs over HTTPS with curl but never verified them, so a compromised mirror or intercepted download could inject code into the statically linked binary. This pins the upstream SHA-256 of each tarball and verifies both freshly downloaded and cached files, failing closed on mismatch. Hashes are env-overridable for intentional version bumps.

Testing / validation

  • LibSSH2Bridge.c compiles clean under clang -fsyntax-only -Wall -Wextra -std=c11 against the vendored libssh2 1.11.1 headers (0 warnings); symbol/constant usage was checked against that header.
  • build_libssh2.sh passes bash -n; the pinned hashes match the tarballs in third_party/src/ (and the official upstream release hashes).
  • I have not run the full reliability gate (build.sh + audit_mount_calls.py + xcodebuild test) locally — please run CI / the gate. The browser is exercised via a mock BrowserTransport in the Swift tests, so this C change isn't covered by existing unit tests.

By submitting this PR I agree to license the contribution under GPLv3, per CONTRIBUTING.md.

🤖 Generated with Claude Code

timc3 and others added 2 commits June 22, 2026 10:52
The libssh2-backed remote browser opened a session and authenticated
immediately after the SSH handshake without ever verifying the server's
host key. An active network attacker (rogue Wi-Fi, ARP/DNS spoofing, a
compromised router) could impersonate the server; in Password auth mode
the keyboard-interactive/password callback then sent the user's plaintext
password to the impostor. This path had no protection at all, unlike the
sshfs mount path which already uses StrictHostKeyChecking=accept-new.

Add host-key verification in macfusegui_libssh2_open_session, run after
the handshake and before any credentials are sent. The policy mirrors
OpenSSH accept-new and shares ~/.ssh/known_hosts with the sshfs path:
match connects, a changed key is refused with the offered fingerprint in
the error, and an unknown host is trusted on first use and persisted. It
fails closed when the host key cannot be read or known_hosts cannot be
parsed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build_libssh2.sh fetched the libssh2 and OpenSSL release tarballs over
HTTPS with curl but never verified their integrity, so a compromised
mirror or intercepted download could inject malicious code into the
statically linked binary. Pin the upstream SHA-256 of each tarball and
verify both freshly downloaded and previously cached files, failing the
build on any mismatch. The expected hashes can be overridden via the
environment for intentional version bumps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@timc3

timc3 commented Jun 22, 2026

Copy link
Copy Markdown
Author

Ran the full reliability gate locally on arm64:

  • ARCH_OVERRIDE=arm64 ./scripts/build.sh** BUILD SUCCEEDED ** (OpenSSL + libssh2 built from source against the newly pinned tarballs, then the Xcode app build)
  • python3 scripts/audit_mount_calls.pyPASS: 13 callsite(s) include explicit operationID.
  • xcodebuild … test CODE_SIGNING_ALLOWED=NO** TEST SUCCEEDED **, 162 tests, 0 failures

Note: the existing Swift suite drives the browser via a mock BrowserTransport, so this confirms the C change compiles, links, and breaks nothing — it doesn't exercise the new host-key path directly. Happy to add coverage or address any review feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant