Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@
# C0MPUTE_RELEASE_BASE=https://staging.c0mpute.com/releases

# Override the minisign pubkey embedded in the installer (for testing
# pre-release signed builds). Production releases use the embedded key.
# pre-release signed builds). Production releases use the embedded key
# (minisign.pub at the repo root).
# C0MPUTE_PUBKEY=RWQ...
# The installer refuses an artifact without a valid .minisig. Escape hatch
# for a private mirror that has no signatures (not recommended):
# C0MPUTE_ALLOW_UNSIGNED=1

# Plugin upstream installer URLs (consumed by plugins/<id>/install.sh
# and scripts/install.sh's chain_install).
Expand Down
64 changes: 60 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ name: release
# published + marked Latest on the first upload, so for ~3 minutes after a
# tag `releases/latest/download/<artifact>` 404'd for every platform still
# building, and `curl -fsSL https://c0mpute.com/install.sh | sh` failed.
# Before publishing, the `publish` job signs every artifact with the
# production minisign key (repo secrets MINISIGN_SECRET_KEY + MINISIGN_PASSWORD;
# public key minisign.pub) and uploads <artifact>.minisig next to it, which
# scripts/install.sh requires. A release is never published unsigned.
#
# A failed platform leaves the draft unpublished (Latest stays on the
# previous release); fix and re-run the failed jobs.
#
Expand Down Expand Up @@ -142,6 +147,55 @@ jobs:
needs: release
runs-on: ubuntu-latest
steps:
# Only for minisign.pub, the public key every signature must verify against.
- uses: actions/checkout@v5
with:
ref: ${{ env.TAG }}
sparse-checkout: minisign.pub
sparse-checkout-cone-mode: false

# minisign from its upstream release, pinned by sha256 (the same pin
# scripts/install.sh uses to verify downloads when minisign is missing).
- name: Install minisign (pinned)
run: |
set -eu
curl -fsSL -o "$RUNNER_TEMP/minisign.tar.gz" https://github.com/jedisct1/minisign/releases/download/0.12/minisign-0.12-linux.tar.gz
echo "9a599b48ba6eb7b1e80f12f36b94ceca7c00b7a5173c95c3efc88d9822957e73 $RUNNER_TEMP/minisign.tar.gz" | sha256sum -c -
tar -xzf "$RUNNER_TEMP/minisign.tar.gz" -C "$RUNNER_TEMP" minisign-linux/x86_64/minisign
install -m 755 "$RUNNER_TEMP/minisign-linux/x86_64/minisign" /usr/local/bin/minisign 2>/dev/null \
|| sudo install -m 755 "$RUNNER_TEMP/minisign-linux/x86_64/minisign" /usr/local/bin/minisign
minisign -v

# Sign every artifact with the production key (public half: minisign.pub,
# embedded in scripts/install.sh). Secret key + password live in the
# MINISIGN_SECRET_KEY / MINISIGN_PASSWORD repo secrets (source of truth:
# logicsrc team vault c0mpute--signing). Only this job sees them.
- name: Sign artifacts and upload .minisig files
env:
GH_TOKEN: ${{ github.token }}
MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }}
MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }}
run: |
set -eu
[ -n "$MINISIGN_SECRET_KEY" ] && [ -n "$MINISIGN_PASSWORD" ] || { echo "MINISIGN_SECRET_KEY / MINISIGN_PASSWORD secrets missing; not publishing unsigned" >&2; exit 1; }
umask 077
pub="$GITHUB_WORKSPACE/minisign.pub"
[ -s "$pub" ] || { echo "minisign.pub missing at $TAG; not publishing" >&2; exit 1; }
work="$RUNNER_TEMP/sign"; mkdir -p "$work"; cd "$work"
printf '%s\n' "$MINISIGN_SECRET_KEY" > "$RUNNER_TEMP/minisign.key"
trap 'rm -f "$RUNNER_TEMP/minisign.key"' EXIT
want="c0mpute-linux-x86_64.tar.gz c0mpute-linux-aarch64.tar.gz c0mpute-darwin-x86_64.tar.gz c0mpute-darwin-aarch64.tar.gz c0mpute-windows-x86_64.zip"
for a in $want; do
gh release download "$TAG" -p "$a" -p "$a.sha256" --clobber
# Sign only what the platform build uploaded: check its checksum first.
sha256sum -c "$a.sha256"
printf '%s\n' "$MINISIGN_PASSWORD" | minisign -S -s "$RUNNER_TEMP/minisign.key" -m "$a" \
-t "timestamp:$(date +%s) version:$TAG file:$a" >/dev/null
# Must verify against the committed public key (minisign.pub).
minisign -V -p "$pub" -m "$a"
gh release upload "$TAG" "$a.minisig" --clobber
done

- name: Check every asset is on the release, then publish it as Latest
env:
GH_TOKEN: ${{ github.token }}
Expand All @@ -150,14 +204,16 @@ jobs:
want="c0mpute-linux-x86_64.tar.gz c0mpute-linux-aarch64.tar.gz c0mpute-darwin-x86_64.tar.gz c0mpute-darwin-aarch64.tar.gz c0mpute-windows-x86_64.zip"
have=$(gh release view "$TAG" --json assets -q '.assets[].name')
for a in $want; do
for f in "$a" "$a.sha256"; do
for f in "$a" "$a.sha256" "$a.minisig"; do
echo "$have" | grep -qx "$f" || { echo "missing asset $f on $TAG; not publishing" >&2; exit 1; }
done
done
gh release edit "$TAG" --draft=false --latest
# The public download URL must now serve every platform.
for a in $want; do
code=$(curl -s -o /dev/null -w '%{http_code}' -L "https://github.com/$GH_REPO/releases/download/$TAG/$a")
echo "$a -> $code"
[ "$code" = "200" ] || { echo "asset $a not downloadable after publish" >&2; exit 1; }
for f in "$a" "$a.minisig"; do
code=$(curl -s -o /dev/null -w '%{http_code}' -L "https://github.com/$GH_REPO/releases/download/$TAG/$f")
echo "$f -> $code"
[ "$code" = "200" ] || { echo "asset $f not downloadable after publish" >&2; exit 1; }
done
done
6 changes: 6 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,9 @@ c429b7a6b38238f67f348c2323c1fbff41348a5e:node/crates/c0mpute-core/src/webhook.rs

# Public ed25519 test vector (docs/protocol/identity.md, seed 0x77) used as a test DID in an earlier commit of PR #107.
7b92c1c6edb562f3ecb746eab039be5b5f1f2135:apps/web/src/lib/chat/chat.test.ts:generic-api-key:17

# The c0mpute release minisign PUBLIC key (minisign.pub), which an earlier
# commit of PR #186 put in a release.yml env var. Public by design; it is also
# in minisign.pub, scripts/install.sh and c0mpute-update. Covers the
# pre-squash commit on branch `fix/release-signing`.
7fb45f20b3a3f28fa802e813cfdceda3ae236d1a:.github/workflows/release.yml:generic-api-key:171
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,27 @@ That installs three CLIs into `~/.c0mpute/bin`:

The c0mpute installer pulls each from its own release feed.

### Verified downloads

Every c0mpute release artifact is signed with [minisign](https://jedisct1.github.io/minisign/).
The public key is [`minisign.pub`](minisign.pub):

```
RWQqOew3QbWhAWH36TA5roreh18RgRqgozSwhE3yZtdza7eeLKmbDbJM
```

The installer checks `<artifact>.minisig` before it unpacks anything and aborts if the
signature is missing or does not verify. If `minisign` is not installed it fetches a pinned
minisign release (checked against a hard-coded sha256) into a temp dir; no root needed.
`c0mpute` self-updates verify the same signature, with the key compiled in, before swapping
the binary. To check a download by hand:

```sh
minisign -Vm c0mpute-linux-x86_64.tar.gz -P RWQqOew3QbWhAWH36TA5roreh18RgRqgozSwhE3yZtdza7eeLKmbDbJM
```

See [SECURITY.md](SECURITY.md).

## What c0mpute does

```sh
Expand Down
46 changes: 46 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Security

## Reporting a vulnerability

Please report security issues privately through
[GitHub security advisories](https://github.com/profullstack/c0mpute/security/advisories/new)
rather than a public issue.

## Release signing

Every artifact on a [c0mpute release](https://github.com/profullstack/c0mpute/releases)
(`c0mpute-<os>-<arch>.tar.gz`, `c0mpute-windows-x86_64.zip`) has a
[minisign](https://jedisct1.github.io/minisign/) signature next to it, `<artifact>.minisig`.

Public key ([`minisign.pub`](minisign.pub), key id `01A1B54137EC392A`):

```
RWQqOew3QbWhAWH36TA5roreh18RgRqgozSwhE3yZtdza7eeLKmbDbJM
```

The signed (trusted) comment reads `timestamp:<unix> version:<tag> file:<artifact>`, so a
signature only vouches for that one file of that one release.

What checks it:

- **`scripts/install.sh`** (served at `https://c0mpute.com/install.sh`) downloads the
artifact and its `.minisig`, verifies the signature with the embedded key and checks the
trusted comment names the artifact (and the release tag when it is known). It aborts on a
missing signature, a failed verification or a mismatched comment. If `minisign` is not
installed it downloads the pinned upstream minisign 0.12 release and checks it against a
hard-coded sha256 before running it (Linux x86_64/aarch64, macOS arm64; on Intel macs
install minisign with `brew install minisign`).
- **`c0mpute` self-update** (`node/crates/c0mpute-update`) verifies the same signature with
the key compiled into the binary, after the sha256 check and before swapping the binary.
No signature or a bad one means no update.
- **The release workflow** (`.github/workflows/release.yml`) signs in its `publish` job and
refuses to publish a release that is missing any `.minisig`.

Verify a download by hand:

```sh
minisign -Vm c0mpute-linux-x86_64.tar.gz -P RWQqOew3QbWhAWH36TA5roreh18RgRqgozSwhE3yZtdza7eeLKmbDbJM
```

The installer script itself is fetched over HTTPS from c0mpute.com; read it before piping it
to `sh` if you prefer (`curl -fsSL https://c0mpute.com/install.sh | less`).
2 changes: 2 additions & 0 deletions minisign.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
untrusted comment: minisign public key 01A1B54137EC392A
RWQqOew3QbWhAWH36TA5roreh18RgRqgozSwhE3yZtdza7eeLKmbDbJM
1 change: 1 addition & 0 deletions node/crates/c0mpute-update/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ serde_json = { workspace = true }
reqwest = { workspace = true }
sha2 = { workspace = true }
hex = { workspace = true }
minisign-verify = "0.3"
Loading
Loading