feat(desktop): verify signed update manifest + binary hash before self-replace [operator-action] (LC-210-BINARY-INTEGRITY) #280
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-210-binary-integrity-manifest-signing"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #277 (LC-210-BINARY-INTEGRITY). Implements the deferral from LC-210 (#276): the SSRF guard made the update fetch safe, but the downloaded artifact was still unverified. A redirect to a public attacker host, a compromised mirror, or a TLS-trust break would serve a binary that
update::applywouldchmod +xandself_replaceinto the running process. This PR adds authenticity + integrity verification, fail-closed.Chain of trust
A build-embedded Ed25519 public key verifies a detached signature (
latest.json.sig) over the raw manifest bytes BEFORE the JSON is parsed. The now-signed manifest carries a per-artifact SHA-256. The downloaded binary is hashed and compared against that signed value BEFORE anything is written or the running binary is replaced. An attacker who swaps the binary OR rewrites the hash fails verification, because the hash lives inside the signed manifest.Client (
desktop/src/update_verify.rs, new)verify_manifest_signatureusesverify_strict(rejects malleable signatures and small-order public keys), returning aNotConfiguredfail-closed error when no key is embedded (the state before the first signed release).verify_artifact_sha256compares decoded-byte digests (case-insensitive hex).update::fetch_manifestnow fetches manifest + signature and verifies before parsing, so BOTH the GUI background check and--updaterefuse an unsigned/unverifiable manifest.update::applybuffers the download (capped to refuse an oversized/hostile body so we do not OOM hashing it), verifies the hash, then writes.just test-desktop: 13 passed (7 new + 6 existing net_guard).Release pipeline (
publish-release.yml)hash sha256.Sign update manifeststep signslatest.jsonwithopenssl pkeyutl -sign -rawinusing theDESKTOP_UPDATE_SIGNING_KEYsecret and publisheslatest.json.sig; it refuses to ship an unsigned manifest if the secret is missing.LETS_CHAT_UPDATE_PUBLIC_KEY(the non-secret public key, a repo/org variable) into the build so the shipped binary enforces verification.Operator action
Before the next desktop release, provision update signing or the updater fails closed and the publish workflow refuses to ship. Generate an Ed25519 keypair, set the
DESKTOP_UPDATE_SIGNING_KEYsecret (private PEM) and theDESKTOP_UPDATE_PUBLIC_KEYvariable (public key hex). Full runbook, including key rotation, indocs/desktop-update-signing.md. Security fix: upgrade promptly once a signed release is cut; the vector is an unverified binary served via update-mirror compromise or a redirect to a public attacker host.What this PR does NOT close
is_neweronly advertises strictly-higher versions, but there is no anti-rollback floor).docs/desktop-update-signing.md.Validation
just test-desktop13/13.just checkclean: server standalone + saas clippy (-D warnings), desktop clippy (-D warnings),cargo fmt --check. No server source touched, so server suites are unaffected. The CI signing/build-arg paths cannot be exercised here (no runner, no provisioned key); they are implemented to the documented contract and gated by the fail-closed secret check.🤖 Generated with Claude Code