Skip to content

Commit 016d43d

Browse files
teovlclaude
andcommitted
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>
1 parent a1136fb commit 016d43d

4 files changed

Lines changed: 59 additions & 5 deletions

File tree

catalogue/catalogue.json.sig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
CD3TrFKmG4MxgX0eK85z6jPgqpH3gb6+8px87iVALCYF7x7//FqByN3CCkBtHIgxgDUE5afGKJptWNpddw6SCw==
1+
VuWWD6mSCOQR1LLYWxhdUdDu/RHQQXr0UZJBh6fOmy7VyIgcM/W/yeMNo7ym0pYy70KN19xFbDe9SvPjdeFoBw==

cmd/pilotctl/zz_appstore_view_test.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,40 @@ package main
44

55
import (
66
"crypto/sha256"
7+
"encoding/base64"
78
"encoding/hex"
89
"encoding/json"
910
"os"
1011
"path/filepath"
1112
"strings"
1213
"testing"
14+
15+
"github.com/TeoSlayer/pilotprotocol/internal/catalogtrust"
1316
)
1417

15-
// stageCatalogue writes a catalogue.json to a tempdir and points
16-
// PILOT_APPSTORE_CATALOG_URL at it via file://.
18+
// stageCatalogue writes a catalogue.json to a tempdir, signs it with an
19+
// ephemeral catalogue key (whose public half is swapped into the embedded
20+
// trust anchor for the duration of the test), writes the detached
21+
// <catalogue>.sig, and points PILOT_APPSTORE_CATALOG_URL at it via file://.
22+
//
23+
// loadCatalogue verifies fail-closed against the embedded key, so the
24+
// fixture MUST carry a valid signature — staging an unsigned fixture would
25+
// (correctly) be rejected. Signing with a per-test key, rather than
26+
// disabling the gate, keeps these tests exercising the real verification
27+
// path against a valid signature. The original embedded key is restored via
28+
// t.Cleanup so tests stay isolated.
1729
func stageCatalogue(t *testing.T, catJSON string) {
1830
t.Helper()
19-
p := filepath.Join(t.TempDir(), "catalogue.json")
31+
dir := t.TempDir()
32+
p := filepath.Join(dir, "catalogue.json")
2033
if err := os.WriteFile(p, []byte(catJSON), 0o600); err != nil {
2134
t.Fatal(err)
2235
}
36+
sig, restore := catalogtrust.SignWithEphemeralKey([]byte(catJSON))
37+
t.Cleanup(restore)
38+
if err := os.WriteFile(p+".sig", []byte(base64.StdEncoding.EncodeToString(sig)+"\n"), 0o600); err != nil {
39+
t.Fatal(err)
40+
}
2341
t.Setenv("PILOT_APPSTORE_CATALOG_URL", "file://"+p)
2442
}
2543

internal/catalogtrust/catalogtrust.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222
// -ldflags "-X github.com/TeoSlayer/pilotprotocol/internal/catalogtrust.publicKeyB64=<b64>"
2323
//
2424
// The compiled-in default is the current production catalogue key.
25-
var publicKeyB64 = "5aCD92R0UoZ2lGW6PYZeRrDw63ZNBC5oJZxFB8RNOPQ="
25+
var publicKeyB64 = "iHdBWayA/hYjkwUOZopTXY70qOlR90d6ii/hin0ZMdI="
2626

2727
// ErrNoKey is returned when the embedded key is missing or malformed.
2828
// Fail-closed: with no trust anchor, nothing verifies.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+
package catalogtrust
4+
5+
import (
6+
"crypto/ed25519"
7+
"crypto/rand"
8+
"encoding/base64"
9+
)
10+
11+
// SignWithEphemeralKey is a TEST-ONLY helper for packages outside
12+
// catalogtrust (e.g. cmd/pilotctl) that need to stage a catalogue fixture
13+
// the fail-closed loader will accept. It generates a throwaway ed25519
14+
// keypair, swaps the package's embedded public key (publicKeyB64) to the
15+
// generated public half, signs data with the private half, and returns the
16+
// detached signature plus a restore func that puts the original embedded
17+
// key back.
18+
//
19+
// This exists so fixture-based tests exercise REAL fail-closed verification
20+
// against a VALID signature, rather than disabling or weakening the gate.
21+
// It is not referenced by any production code path.
22+
//
23+
// Usage:
24+
//
25+
// sig, restore := catalogtrust.SignWithEphemeralKey(catalogueBytes)
26+
// defer restore()
27+
// // write catalogueBytes + base64(sig) and point the loader at them
28+
func SignWithEphemeralKey(data []byte) (sig []byte, restore func()) {
29+
pub, priv, err := ed25519.GenerateKey(rand.Reader)
30+
if err != nil {
31+
panic("catalogtrust: ephemeral key generation failed: " + err.Error())
32+
}
33+
orig := publicKeyB64
34+
publicKeyB64 = base64.StdEncoding.EncodeToString(pub)
35+
return ed25519.Sign(priv, data), func() { publicKeyB64 = orig }
36+
}

0 commit comments

Comments
 (0)