Skip to content

fix(release): sign every release artifact with minisign; verify in installer and self-update - #186

Merged
ralyodio merged 2 commits into
masterfrom
fix/release-signing
Nov 9, 2026
Merged

ralyodio merged 2 commits into
masterfrom
fix/release-signing

Conversation

@ralyodio

@ralyodio ralyodio commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes the gap reported on r/LocalLLM: scripts/install.sh carried a placeholder minisign key (RWQ_REPLACE_ME_WITH_PROD_MINISIGN_PUBKEY) and releases had no .minisig files, so the downloaded binary was never verified.

What changes

  • minisign.pub at the repo root: production key RWQqOew3QbWhAWH36TA5roreh18RgRqgozSwhE3yZtdza7eeLKmbDbJM (key id 01A1B54137EC392A). The secret key is password-protected and stored in the team vault c0mpute--signing and in the repo secrets MINISIGN_SECRET_KEY + MINISIGN_PASSWORD.
  • release.yml: the publish job installs minisign 0.12 (pinned by sha256), checks each artifact against its .sha256, signs it with trusted comment timestamp:<t> version:<tag> file:<artifact>, verifies it against the committed key, uploads <artifact>.minisig, and refuses to publish if any signature is missing. The secrets are only exposed to that one job.
  • scripts/install.sh (signature section only): the real key. It fails closed when the signature is missing, fails to verify, or names another file or release in its trusted comment. C0MPUTE_ALLOW_UNSIGNED=1 is the explicit escape hatch. If minisign is not installed, the installer downloads the pinned upstream minisign 0.12 release into the temp dir and checks it against a hard-coded sha256 (Linux x86_64/aarch64, macOS arm64; no root). On Intel macs it asks for brew install minisign.
  • Self-update (c0mpute-update): this closes the auto-update path too. Updates fetch <artifact>.minisig and verify it against the compiled-in RELEASE_PUBKEY using the pure-Rust minisign-verify 0.3. That runs after the sha256 check and before the binary swap; no signature means no update. Unit tests use a test keypair and cover: a good signature passes; tampered bytes, the wrong key, the wrong file or release and a garbage or missing signature all fail; and RELEASE_PUBKEY equals minisign.pub. The key is a const, not include_str!, because Dockerfile.web copies only node/ into the Rust stage.
  • SECURITY.md (new) and the README document the key and how to verify a download by hand.

Backfill

Releases v0.2.83 through v0.2.88 (current Latest) now have .minisig files for all 5 artifacts. That covers the installer's walk-back window, so the fail-closed installer works the moment this deploys.

Tests (run locally against the real releases, pinned-minisign path and system-minisign path)

case result
real latest via c0mpute.com (v0.2.88) verified, installed
real pinned v0.2.86 verified, installed
tampered artifact (1 byte appended) signature verification FAILED, exit 1
another platform's validly signed file signs a different file, exit 1
v0.2.88 files served as v0.2.87 not for release v0.2.87, exit 1
no .minisig refusing to install an unverified binary, exit 1

The Rust unit tests run in CI (build + test). This box has no C toolchain.

How it reaches production

c0mpute.com/install.sh reads scripts/install.sh from the deployed checkout at request time (apps/web/src/app/install.sh/route.ts, cached for 5 min), so the Deploy to dev2 workflow on merge ships the installer change. The release workflow change applies to tags cut after the merge. The self-update check ships in the next release binary.

🤖 Generated with Claude Code

…he installer and self-update

The installer shipped a placeholder key (RWQ_REPLACE_ME...) and releases had
no .minisig files, so downloads were never verified. Now:

- minisign.pub (key id 01A1B54137EC392A) committed at the repo root.
- release.yml: the publish job installs a sha256-pinned minisign 0.12, checks
  each artifact against its .sha256, signs it with the production key
  (secrets MINISIGN_SECRET_KEY + MINISIGN_PASSWORD), trusted comment
  "timestamp:<t> version:<tag> file:<artifact>", verifies against the
  committed key, uploads <artifact>.minisig, and refuses to publish a release
  missing any signature.
- install.sh: real public key; fails closed on a missing signature, failed
  verification, or a trusted comment naming another file/release
  (C0MPUTE_ALLOW_UNSIGNED=1 is the explicit escape hatch). Without minisign
  installed it fetches the pinned upstream minisign 0.12 release, checked by a
  hard-coded sha256, into the temp dir (no root).
- c0mpute-update: self-update fetches <artifact>.minisig and verifies it with
  the compiled-in key (minisign-verify crate, pure Rust) after the sha256
  check and before swapping; no signature, no update. Unit tests with a test
  keypair cover good, tampered, wrong key, wrong file/release, garbage sig,
  and that RELEASE_PUBKEY matches minisign.pub.
- SECURITY.md + README document the key and how to verify by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@socket-security

socket-security Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​minisign-verify@​0.3.010010093100100

View full report

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

13 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 8 | LOW: 4

Severity Rule Location
HIGH js-ssrf-outbound-request apps/web/public/sw.js:24
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:53
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:80
MEDIUM sh-remote-script-execution scripts/dev-setup.sh:25
MEDIUM sh-eval-expansion scripts/dev-setup.sh:35
MEDIUM sh-remote-script-execution scripts/install.sh:181
MEDIUM sh-remote-script-execution scripts/install.sh:206
MEDIUM sh-remote-script-execution scripts/install.sh:399
MEDIUM sh-remote-script-execution scripts/install.sh:441
LOW py-ssrf-outbound-request examples/business-strategist-agent/chat.py:37
LOW py-ssrf-outbound-request examples/business-strategist-agent/train.py:54
LOW secret-generic-credential node/crates/c0mpute-core/src/webhook.rs:1165
LOW py-path-traversal node/crates/c0mpute-core/tests/fixtures/fake-infernet-text:29

Snippets are redacted; ThreatCrush never prints matched credential material.

…v literal

gitleaks' generic-api-key rule flagged the public key in a workflow env var.
The publish job now sparse-checks-out minisign.pub and verifies against it;
.gitleaksignore covers the earlier branch commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 1c3ed52 into master Nov 9, 2026
12 checks passed
@ralyodio
ralyodio deleted the fix/release-signing branch November 9, 2026 08:36
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