Skip to content

Signed app-store catalogue + Pages catalogue site#249

Merged
TeoSlayer merged 4 commits into
mainfrom
feat/signed-catalogue-and-pages
Jun 16, 2026
Merged

Signed app-store catalogue + Pages catalogue site#249
TeoSlayer merged 4 commits into
mainfrom
feat/signed-catalogue-and-pages

Conversation

@TeoSlayer

@TeoSlayer TeoSlayer commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the two app-store catalogue tasks on the web4 side:

1. Signed catalogue, verified fail-closed (replaces the all-zeros trust anchor)

  • New internal/catalogtrust holds the embedded ed25519 catalogue
    public key (override at build time via
    -ldflags -X …/catalogtrust.publicKeyB64=<b64> to rotate).
  • pilotctl loadCatalogue now fetches a detached <url>.sig and
    verifies it against the embedded key before trusting any entry.
    Unsigned, missing-signature, or tampered catalogues are refused.
  • New pilotctl appstore sign-catalogue --key <key> <catalogue.json>
    writes the detached signature and refuses to sign with a key that
    doesn't match the embedded public key (no dead signatures).
  • cmd/daemon passes the embedded key into appstore.Config.CatalogPubkey,
    retiring the all-zeros placeholder at the wire-up point.
  • catalogue.json is signed (catalogue.json.sig committed); README
    documents the signing + key-rotation flow.

2. Static catalogue site (GitHub Pages)

Testing

  • go build ./... clean (daemon + pilotctl + all packages).
  • go test ./internal/catalogtrust/ ./cmd/pilotctl/ green, incl. the
    signed-catalogue load + fail-closed (missing sig / tamper) tests and the
    catalogtrust verify unit tests.

⚠️ Follow-up: Pages deploy workflow (needs workflow scope to push)

The deploy workflow below could not be pushed from this environment —
the push token lacks the workflow OAuth scope. Please add it as
.github/workflows/deploy-catalogue-site.yml:

name: Deploy app catalogue site

# Publishes the static app-store catalogue site to GitHub Pages. The page
# renders the live catalogue.json (copied alongside it, so the fetch is
# same-origin), and redeploys whenever the catalogue or the site changes.

on:
  push:
    branches: [main]
    paths:
      - "catalogue/**"
      - ".github/workflows/deploy-catalogue-site.yml"
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

# Allow one concurrent deployment; let an in-progress run finish.
concurrency:
  group: pages
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Assemble site
        run: |
          set -euo pipefail
          mkdir -p _site
          cp catalogue/site/index.html _site/index.html
          # Copy the catalogue + detached signature so the page fetches
          # them same-origin (no CORS dependency on raw.githubusercontent).
          cp catalogue/catalogue.json _site/catalogue.json
          cp catalogue/catalogue.json.sig _site/catalogue.json.sig
          # Fail loudly if the catalogue is missing/empty.
          test -s _site/catalogue.json

      - name: Upload Pages artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: _site

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

It assembles _site (page + catalogue.json + .sig) and deploys to
Pages on changes under catalogue/.

@TeoSlayer TeoSlayer requested a review from Alexgodoroja as a code owner June 15, 2026 14:52
teovl and others added 3 commits June 16, 2026 11:27
Replace the placeholder catalogue trust anchor with a real embedded
ed25519 key (internal/catalogtrust, ldflags-overridable for rotation):

- pilotctl loadCatalogue fetches a detached <url>.sig and verifies it
  against the embedded key before trusting any entry; unsigned, missing,
  or tampered catalogues are refused.
- New 'pilotctl appstore sign-catalogue' writes the detached signature and
  refuses to sign with a key that doesn't match the embedded public key.
- cmd/daemon passes the embedded key into appstore.Config.CatalogPubkey,
  retiring the all-zeros placeholder at the wire-up point.
- Sign catalogue.json; document signing + key rotation in catalogue README.

Tests: catalogtrust verify (happy/tamper/short/no-key), signed-catalogue
load + fail-closed (missing sig, tamper).
Dependency-free page (catalogue/site/index.html) that fetches
catalogue.json same-origin and renders one card per app (id, version,
description, install command, bundle link; methods when an entry provides
them). The Pages deploy workflow is added separately (requires workflow
scope on the push token).
…st fixtures

The previous catalogue trust anchor's private key
(publicKeyB64 = "5aCD92R0UoZ2lGW6PYZeRrDw63ZNBC5oJZxFB8RNOPQ=") was lost and is
unrecoverable, so the committed catalogue.json.sig could never be regenerated
to match an edited catalogue. This anchor existed only on this PR branch — not
on main and not in any shipped binary — so rotating it now has zero
installed-base impact.

Changes:
- Generate a fresh ed25519 release keypair (private key stored only outside the
  repo, never committed) and embed its public half as the new trust anchor:
  publicKeyB64 = "iHdBWayA/hYjkwUOZopTXY70qOlR90d6ii/hin0ZMdI="
- Re-sign catalogue/catalogue.json with the new key (catalogue/catalogue.json.sig),
  which now verifies fail-closed against the embedded key.
- Fix the test fixtures #255 left unsigned: stageCatalogue now signs each
  fixture with a per-test ephemeral key (swapped into the embedded anchor for
  the test's duration via catalogtrust.SignWithEphemeralKey, restored on
  cleanup). Tests exercise REAL fail-closed verification against a VALID
  signature — the gate is not skipped, disabled, or weakened; the negative
  tests (missing-signature, tamper) still pass.

Rebased onto origin/main, reconciling #255's v2 metadata schema with this
branch's catalogue signing (kept the fail-closed sig gate in loadCatalogue,
both `view` and `sign-catalogue` subcommands, and main's metadata pin
discipline in catalogue/README.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@TeoSlayer TeoSlayer force-pushed the feat/signed-catalogue-and-pages branch from 15f1ea9 to 016d43d Compare June 16, 2026 08:31
@TeoSlayer TeoSlayer merged commit 3c7f6eb into main Jun 16, 2026
9 checks passed
@TeoSlayer TeoSlayer deleted the feat/signed-catalogue-and-pages branch June 16, 2026 08:38
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.

2 participants