Skip to content

fix(security): Ed25519-signed auto-update, replacing pipe-to-shell (CWE-494)#1

Open
nullarch wants to merge 3 commits into
mainfrom
security/signed-auto-update
Open

fix(security): Ed25519-signed auto-update, replacing pipe-to-shell (CWE-494)#1
nullarch wants to merge 3 commits into
mainfrom
security/signed-auto-update

Conversation

@nullarch

@nullarch nullarch commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Closes the standing unsigned self-update RCE channel (CWE-494) reported by Sascha Heyer. The session_start auto-updater fetched the installer and ran it with no integrity check (curl … | bash, iwr … | iex); the only validation was a ≤16-char length check on the version string. A compromised vibemon.dev or GitHub release would run arbitrary code on every user within 24h.

Fix

The updater now downloads the installer + a detached Ed25519 signature, verifies it against a public key baked into the client, and executes ONLY on a valid signature — fail-closed on invalid/missing signature or an unconfigured (placeholder) key. The signing seed lives only in the VIBEMON_SIGNING_SEED GitHub Actions secret and is never committed, so a swapped artifact can't be forged into a valid signature. All three unsigned exec paths are removed (Unix bash, Windows PowerShell, python client).

  • src/ed25519.py — pure-stdlib Ed25519 (RFC 8032 reference); no new client prerequisite (python3 already required), cross-platform
  • src/verify_main.py + src/release_pubkey.py — client verifier + baked-in pubkey (default = all-zero placeholder ⇒ fail-closed)
  • scripts/keygen.py / scripts/sign.py — one-time keygen + release-time signing
  • src/notify.sh, src/notify.py — download → verify → exec
  • tests/test_signature.py — RFC 8032 known-answer, tamper-rejection (5 cases), placeholder fail-closed, PyNaCl cross-check
  • SECURITY.md / README.md — document signing; disclose auto-update + telemetry at the install prompt

Verification

  • Crypto: matches RFC 8032 test vector + PyNaCl cross-check (both directions)
  • Real-Linux end-to-end (Docker Ubuntu, real bash+python3+curl): valid signed release runs; tampered + unsigned artifacts are refused (payload never executes)
  • 178 tests pass; dist/ reproducible; bash -n clean

Ships fail-closed

Auto-update stays off (no unsigned exec possible) until the maintainer runs scripts/keygen.py, sets the VIBEMON_SIGNING_SEED secret, and vibemon.dev serves the .sig files.

⚠️ Follow-up commit pending workflow scope

The CI changes (release.yml signing step, test.yml pynacl) are in a separate local commit not yet pushed — pushing .github/workflows/ needs the workflow OAuth scope. Grant with gh auth refresh -s workflow, then push.

🤖 Generated with Claude Code

…WE-494)

The session_start auto-updater fetched the installer and executed it with no
integrity check (`curl … | bash`, `iwr … | iex`) — a standing RCE channel: a
compromised vibemon.dev or GitHub release would run arbitrary code on every
user within 24h. The only validation was a <=16-char length check on the
version string. Reported by Sascha Heyer.

The updater now downloads the installer + a detached Ed25519 signature,
verifies the bytes against a public key baked into the client, and executes
ONLY on a valid signature — fail-closed on an invalid/missing signature or an
unconfigured (placeholder) key. The signing seed lives ONLY in the
VIBEMON_SIGNING_SEED GitHub Actions secret and is never committed.

- src/ed25519.py: pure-stdlib Ed25519 verify/sign (RFC 8032 reference) — no new
  client prerequisite (python3 already required), cross-platform
- src/verify_main.py + src/release_pubkey.py: client verifier + baked-in pubkey
- scripts/keygen.py / scripts/sign.py: one-time keygen + release-time signing
- src/notify.sh, src/notify.py: download → verify → exec (all three unsigned
  exec paths removed: Unix bash, Windows powershell, python client)
- tests/test_signature.py: RFC 8032 KAT, tamper-rejection, placeholder
  fail-closed, PyNaCl cross-check
- SECURITY.md / README.md: document signing; disclose auto-update + telemetry

Verified end-to-end on real Linux (Docker): valid release runs, tampered and
unsigned artifacts are refused. 178 tests pass; dist reproducible. Ships
fail-closed (auto-update stays off until keygen + secret are set).

NOTE: the CI workflow changes (release.yml signing step, test.yml pynacl) are a
follow-up commit — they need the `workflow` OAuth scope to push.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01He8wXLDfXZSoyjf9wrdo3y
@nullarch

nullarch commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

Reviewed the crypto and exec path end-to-end — correct and fail-closed:

  • ed25519.py::checkvalid raises on any invalid signature/length (standard RFC 8032 verification [S]B == R + [h]A), so verify_main.py returns non-zero ⇒ the installer never runs.
  • notify.sh / notify.py execute the installer only on an exit-0 verification of the exact file they downloaded (no re-fetch, no pipe-to-shell), and the all-zero placeholder key keeps auto-update off until a real key is provisioned.
  • Tests cover RFC 8032 KATs, tamper-rejection, placeholder fail-closed, and a PyNaCl cross-check; CI green on macOS/Ubuntu/Windows × 3.10/3.12.

Great work — this closes the CWE-494 channel cleanly. 🎉

Follow-ups to actually activate signed releases (tracked separately, not blocking): run scripts/keygen.py, set the VIBEMON_SIGNING_SEED secret, have vibemon.dev serve the .sig files, and land the pending release.yml signing step (needs workflow scope).

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