You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Signed app-store catalogue + Pages catalogue site (#249)
* feat(appstore): sign the catalogue and verify it fail-closed
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).
* feat(catalogue): static site rendering the live catalogue
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).
* fix(catalogue): rotate signing key + re-sign after #255; sign #255 test 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>
---------
Co-authored-by: Teodor Calin <teodor@vulturelabs.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
To rotate: generate a new keypair, store the private key securely, update
185
+
the embedded public key (source default or `-ldflags`), and re-sign the
186
+
catalogue. `sign-catalogue` refuses to sign with a key that doesn't match
187
+
the embedded public key, so a mismatch is caught before publishing a dead
188
+
signature.
153
189
154
190
## Trust model
155
191
156
192
| Layer | Trust anchor | Verifies |
157
193
|---|---|---|
158
194
| User trusts pilotctl | Project release pipeline (signed pilotctl binary) | The catalogue URL is correct |
159
-
| pilotctl trusts the catalogue |Future: signed against `EmbeddedCatalogPubkey`; today: the raw URL itself | App IDs map to specific bundle URLs + SHAs |
195
+
| pilotctl trusts the catalogue |Detached ed25519 signature against the embedded catalogue key (`internal/catalogtrust`) | The app list (IDs → bundle URLs + SHAs) is authentic; a substituted catalogue is rejected|
160
196
| pilotctl trusts the bundle | Embedded `bundle_sha256` matches downloaded bytes | A CDN substitute is rejected |
161
197
| pilotctl trusts the detail doc | Index `metadata_sha256` matches fetched `metadata.json`| A substituted listing is rejected (`view` falls back to the teaser) |
162
198
| Daemon trusts the manifest | Embedded ed25519 publisher pubkey verifies the signature | The bundle's manifest hasn't been tampered with |
// Guard: refuse to sign with a key that doesn't match the embedded
207
+
// trust anchor — the resulting .sig would never verify in the wild.
208
+
embed:=catalogtrust.PublicKey()
209
+
ifembed==nil {
210
+
fatalHint("internal_error", "rebuild pilotctl with a valid embedded catalogue key", "embedded catalogue public key is missing/malformed")
211
+
}
212
+
if!bytes.Equal(pub, embed) {
213
+
fatalHint("invalid_argument",
214
+
"this key does not match the embedded catalogue public key; pilotctl would reject the signature. Use the release catalogue key, or rebuild pilotctl with -ldflags overriding catalogtrust.publicKeyB64",
0 commit comments