From 0fa994f60a597eb3d381916edac3d0641deaacd2 Mon Sep 17 00:00:00 2001 From: Austin Schaefer Date: Fri, 11 Sep 2026 09:01:30 +0200 Subject: [PATCH] Add initial design spec for declarative package-update watcher Captures the verification trust-tier model (pinned-key signatures down to trust-me-bro install scripts) and the daemon architecture discussed: check -> fetch -> tier-aware verify -> PKGBUILD gen -> local repo publish, with weak-tier changes routed to human review instead of auto-publish. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr --- SPEC.md | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 SPEC.md diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..1e2d231 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,176 @@ +# pkgwatch — declarative package-update watcher/publisher (working name) + +Status: design draft, pre-PoC. Captures the design discussion as of 2026-09-11. + +## Problem + +Software not packaged by the distro (Arch/Manjaro here) usually gets installed +one of a few ways: + +- `curl | sh` from the vendor's own install script — the classic "trust me, + bro." The script and any checksum it embeds share a trust boundary, so it + verifies nothing beyond transport corruption. +- Manual download + manual checksum/signature verification, redone by hand + every time you want to update. Tedious enough that people stop doing it. +- A distro package (pacman `extra`, AUR) — trustworthy, but version-lagged + behind upstream, and someone else has to maintain the PKGBUILD. + +There's no local, low-effort way to say "here's how to fetch and verify +package X" once, and have that declaration stay live — checked periodically, +re-verified on every new upstream release, and fed into a normal +`pacman`-based workflow without hand-editing a PKGBUILD each time. + +## Vision + +A Rust binary, run as a systemd service (service + timer, periodic not +persistent), that: + +1. Reads a declarative config of tracked packages — where to check for new + versions, how to fetch the artifact, and how to verify it. +2. On each run, checks each tracked package for a new upstream version. +3. If a new version is found, fetches the artifact and runs the verification + method declared for that package. +4. If verification succeeds (per the package's trust tier — see below), + updates/generates a local PKGBUILD (bump `pkgver`, refresh + `sha256sums`/signature reference) and rebuilds it into a local pacman + repo via `makepkg` + `repo-add`. +5. The next `pacman -Syu` (with the local repo configured) picks up the new + version normally — no separate tooling needed on the consuming side. + +This is conceptually `nvchecker` (version checking) + `updpkgsums` (checksum +refresh) + `repo-add` (local repo publishing), fused into one daemon with a +single declarative source of truth, plus an explicit, surfaced trust model +that those tools don't provide. + +## Verification trust tiers + +The central design problem: from the install UX, a cryptographically strong +verification and a "trust-me-bro" same-domain checksum look identical. The +tool's job is to make that difference legible instead of laundering every +package into an undifferentiated "verified" bucket. + +Tiers, strongest to weakest: + +1. **Pinned-key signature** — GPG/minisign/sigstore-cosign, where the + signing key's fingerprint is pinned in *our* config (not fetched fresh + from the vendor each time). Proves authorship, independent of the + artifact's own hosting. +2. **Build provenance attestation** — GitHub/GitLab attestations, SLSA + provenance. Ties the artifact to a specific CI run and source commit. + Strong, but only as trustworthy as that CI pipeline. +3. **Registry-native signing** — crates.io, PyPI trusted publishing, npm + provenance. Similar strength, scoped to that registry's trust model. +4. **Same-origin checksum file** — a `.sha256`/`.sha256sum` served next to + the artifact by the vendor. Proves transport integrity only. If the + vendor's server or account is compromised, the attacker controls the + artifact and the "verification" in the same move. +5. **Checksum embedded in an install script** — the classic curl|sh case. + The verifier and the thing being verified share a trust boundary. +6. **Nothing** — bare TLS to a domain, no checksum or signature at all. + +### Automation posture per tier + +This is the load-bearing decision, not just a cosmetic label: + +- **Tiers 1–3**: a passing verification is a real trust signal. Safe to + auto-bump, auto-verify, auto-publish unattended. +- **Tiers 4–6**: a passing "verification" only proves internal consistency + of one origin (the checksum and the artifact agree), which tells you + nothing about whether that origin was compromised. For these tiers the + tool should **not** treat a pass as "verified, ship it." Instead: treat a + version/hash *change* as a flag-for-human-review event. The value pkgwatch + adds at these tiers is diff-and-alert (notice something changed, surface + the new hash for a human to look at), not verify-and-trust. + +### Surfacing trust, not just gating on it + +- Every tracked package carries an explicit tier + one-line justification + (e.g. "minisign, key pinned 2024-03" vs. "same-domain sha256, no + independent signer") in a metadata file alongside the generated PKGBUILD — + something `repo-add`/pacman don't touch, but that a human or `pkgwatch + audit` can read. +- `pkgwatch audit` (or similar) lists all tracked packages sorted + worst-tier-first, so weak links don't hide among strong ones in a repo + that otherwise looks uniformly trustworthy. + +## Config schema (draft) + +```toml +[package.uv] +source = "github-release" +repo = "astral-sh/uv" +asset_pattern = "uv-x86_64-unknown-linux-gnu.tar.gz" + +[package.uv.verification] +tier = 4 +method = "same-origin-sha256" +checksum_asset_pattern = "uv-x86_64-unknown-linux-gnu.tar.gz.sha256" + +# Tier 1 example: +[package.somepkg] +source = "url-with-version-regex" +url = "https://example.com/downloads/" +version_regex = 'somepkg-(\d+\.\d+\.\d+)\.tar\.gz' + +[package.somepkg.verification] +tier = 1 +method = "minisign" +pinned_key = "RWQ...base64pubkey..." +``` + +Open questions on the schema: + +- How much of `nvchecker`'s source-type taxonomy (github, gitlab, pypi, + crates.io, regex, htmlparser, ...) to reimplement vs. shell out to + `nvchecker` itself for the version-check step and own only the + verification + publish pipeline. +- PKGBUILD generation: full Jinja-style templates per package vs. a small + fixed set of PKGBUILD "shapes" (single binary tarball, cargo-install, + etc.) parameterized by the config. +- Where the local pacman repo lives and how it's registered in + `pacman.conf` (one-time manual setup step vs. something pkgwatch manages). +- Failure/alerting channel for tier 4–6 change events — log only, or a + notification hook (this box already has a wofi/Mako notification setup — + see `project_wofi_notification_picker` in Claude's memory). + +## Architecture sketch + +- **Config loader**: parses the TOML above into an in-memory package list. +- **Checker**: per source type, resolves "what's the latest version" — + likely reuses `nvchecker`'s logic/sources conceptually, possibly shells + out to it initially for the PoC rather than reimplementing every source + type in Rust. +- **Fetcher**: downloads the artifact (and any checksum/signature/ + attestation companion) for a resolved version. +- **Verifier**: tier-specific verification implementations behind a common + trait; returns a tier + pass/fail + justification string. +- **Publisher**: for tiers 1–3 on pass, generates/updates the PKGBUILD, + runs `makepkg`, runs `repo-add` against the local repo. +- **Reviewer queue**: for tiers 4–6, records the detected change instead of + auto-publishing; a separate `pkgwatch review` command lets a human + approve/reject, which then triggers the publish step. +- **Scheduling**: systemd `.service` (oneshot) + `.timer` running it + periodically, matching the pattern already used for other periodic tasks + on this box. + +## Prior art / reference points + +- `nvchecker` — version-check-only, no verification or publish step. +- `updpkgsums` (pacman-contrib/devtools) — checksum refresh only, manual + trigger. +- `aurutils` — local repo + AUR build automation, but AUR itself carries no + stronger verification guarantee than what each PKGBUILD maintainer does. +- `repology` — cross-distro version tracking, no verification/publish. +- GitHub artifact attestations (`gh attestation verify`) — tier 2 building + block for GitHub-hosted releases. + +## Status / next steps + +- [ ] Refine config schema further (see open questions above). +- [ ] Decide version-check strategy: shell out to `nvchecker` vs. own + implementation, for the PoC. +- [ ] PoC scope: single tier-4 package (e.g. `uv`, ironically) end-to-end — + check, fetch, same-origin-checksum verify, flag-for-review, manual + approve, PKGBUILD generation, local repo publish. +- [ ] Decide on project home: local-only for now, or push to + code.austinschaefer.com (Forgejo) once the spec settles.