release: pin the pointer wire shape before touching the signer
Scoped signing keys add a field to the signed wrapper. Every deployment bucket holds pointers written by binaries that predate it, and the rule this repo has paid for is that a wire-shape change is additive: those objects must still parse and still verify, byte for byte. So the shape gets pinned FIRST, from the signing path as it stands, over a payload carrying every optional field a real pointer has ever had (history and an audit trail). Three properties: the fixture verifies and decodes to exactly what it says; a root-signed wrapper — fixture or freshly promoted through the store — has exactly the keys payload and sig, which is what catches a forgotten omitempty on whatever is added next; and the signature covers the base64 payload STRING rather than the decoded JSON, asserted in both directions so a "canonicalise then sign" refactor cannot pass. The fixture is not regenerated when the shape changes. A diff in it means live pointers stopped verifying.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+143
−0
internal/release/golden_test.go+143 −0
diff --git a/internal/release/golden_test.go b/internal/release/golden_test.go| new file mode 100644 |
| index 000000000..02a478959 |
| --- /dev/null |
| +++ b/internal/release/golden_test.go |
| @@ -0,0 +1,143 @@ |
| +package release |
| + |
| +import ( |
| + "context" |
| + "crypto/ed25519" |
| + "encoding/base64" |
| + "encoding/json" |
| + "reflect" |
| + "sort" |
| + "testing" |
| + "time" |
| +) |
| + |
| +// The wire shape of a channel pointer, pinned. Every deployment bucket in |
| +// production holds objects that look EXACTLY like this right now, written by |
| +// binaries that predate whatever is being changed today; they have to keep |
| +// parsing and keep verifying, byte for byte, forever. So this fixture is not |
| +// regenerated when the shape changes — a diff here means live pointers stopped |
| +// verifying, which is a fleet that cannot resolve a channel. |
| +// |
| +// Captured from the signing path on f1cc914 with a deterministic seed |
| +// (bytes 1..32) and a frozen promoted_at, over a payload carrying every |
| +// optional field a real pointer has ever had: history and an audit trail. |
| +const goldenRootSignedWrapper = `{ |
| + "payload": "ewogICJzY2hlbWEiOiAxLAogICJhcHAiOiAia2FzcyIsCiAgImNoYW5uZWwiOiAiY2FuYXJ5L3BsYXRmb3JtLWFkb3B0aW9uIiwKICAidmVyc2lvbiI6ICI5ZjNjMWFkIiwKICAicHJvbW90ZWRfYXQiOiAiMjAyNi0wNy0zMFQxMTowNDoxN1oiLAogICJoaXN0b3J5IjogWwogICAgIjFiMmU0YzciLAogICAgIjBhYzkxZGUiCiAgXSwKICAiYXVkaXQiOiBbCiAgICB7CiAgICAgICJ2ZXJzaW9uIjogIjlmM2MxYWQiLAogICAgICAiZnJvbSI6ICJlZGdlIiwKICAgICAgImFjdG9yIjogInBhdWxAcnNsdy5jb20iLAogICAgICAiYXQiOiAiMjAyNi0wNy0zMFQxMTowNDoxN1oiCiAgICB9CiAgXQp9", |
| + "sig": "yiutCm6KdqvIc4jqX2fCo9cYY3R6OITWoHtPNk8xO0mUFFxD4RZ5XLT4CHUAlUlRQfwEOKUm7ii46qlhjdQFCg==" |
| +}` |
| + |
| +// goldenRootPub is the public half of the fixture's signer. |
| +const goldenRootPub = "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=" |
| + |
| +// A pointer written by an older binary — no grant field at all — verifies |
| +// against the root public key and decodes to exactly what it said. |
| +// |
| +// Mutation-mindedness: if grant handling were ever made mandatory (require a |
| +// grant, or reject a wrapper without one), this test goes red immediately, |
| +// because the entire live fleet is on wrappers shaped like this one. |
| +func TestGoldenRootSignedWrapperStillVerifies(t *testing.T) { |
| + pub, err := ParsePublicKey(goldenRootPub, "golden") |
| + if err != nil { |
| + t.Fatalf("ParsePublicKey: %v", err) |
| + } |
| + p, err := verifyPointer(pub, []byte(goldenRootSignedWrapper), "kass", "canary/platform-adoption") |
| + if err != nil { |
| + t.Fatalf("golden root-signed pointer no longer verifies: %v", err) |
| + } |
| + want := &ChannelPointer{ |
| + Schema: 1, |
| + App: "kass", |
| + Channel: "canary/platform-adoption", |
| + Version: "9f3c1ad", |
| + PromotedAt: time.Date(2026, 7, 30, 11, 4, 17, 0, time.UTC), |
| + History: []string{"1b2e4c7", "0ac91de"}, |
| + Audit: []Promotion{{ |
| + Version: "9f3c1ad", |
| + From: "edge", |
| + Actor: "paul@rslw.com", |
| + At: time.Date(2026, 7, 30, 11, 4, 17, 0, time.UTC), |
| + }}, |
| + } |
| + if !reflect.DeepEqual(p, want) { |
| + t.Fatalf("golden pointer decoded as\n%+v\nwant\n%+v", p, want) |
| + } |
| +} |
| + |
| +// The wrapper a root key writes TODAY has exactly two keys. A new wrapper |
| +// field that is not omitted when empty would change every pointer the fleet |
| +// writes and break old readers' strictness assumptions; this is the assertion |
| +// that catches a forgotten `omitempty`. |
| +// |
| +// Mutation-mindedness: drop `,omitempty` from any wrapper field added later |
| +// and this fails with the extra key named. |
| +func TestRootSignedWrapperHasOnlyPayloadAndSig(t *testing.T) { |
| + for _, tc := range []struct { |
| + name string |
| + wrapper []byte |
| + }{ |
| + {"golden fixture", []byte(goldenRootSignedWrapper)}, |
| + {"freshly signed", freshRootWrapper(t)}, |
| + } { |
| + var keys map[string]json.RawMessage |
| + if err := json.Unmarshal(tc.wrapper, &keys); err != nil { |
| + t.Fatalf("%s: %v", tc.name, err) |
| + } |
| + got := make([]string, 0, len(keys)) |
| + for k := range keys { |
| + got = append(got, k) |
| + } |
| + sort.Strings(got) |
| + if want := []string{"payload", "sig"}; !reflect.DeepEqual(got, want) { |
| + t.Errorf("%s: wrapper keys = %v, want %v", tc.name, got, want) |
| + } |
| + } |
| +} |
| + |
| +// freshRootWrapper promotes with a root key through the real store path and |
| +// returns the bytes that landed in the bucket — the shape a fleet reads. |
| +func freshRootWrapper(t *testing.T) []byte { |
| + t.Helper() |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, _ := testKeys(t) |
| + shipRelease(t, s, "bab", "kass", "9f3c1ad", "linux-arm64", []byte("bytes")) |
| + if err := Promote(ctx, s, priv, "bab", "kass", "canary/platform-adoption", "9f3c1ad"); err != nil { |
| + t.Fatalf("Promote: %v", err) |
| + } |
| + raw, _, err := s.Get(ctx, ChannelKey("bab", "kass", "canary/platform-adoption")) |
| + if err != nil { |
| + t.Fatalf("read back pointer: %v", err) |
| + } |
| + return raw |
| +} |
| + |
| +// The signature covers the base64 payload STRING, not the decoded JSON, so no |
| +// canonicalisation question can ever arise. Pinned because a refactor to |
| +// "verify the decoded bytes" would silently invalidate every live pointer. |
| +// |
| +// Mutation-mindedness: change signPointer/verifyPointer to sign the decoded |
| +// payload and the golden fixture stops verifying; this test says why. |
| +func TestSignatureCoversTheBase64PayloadString(t *testing.T) { |
| + pub, err := ParsePublicKey(goldenRootPub, "golden") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var sw signedWrapper |
| + if err := json.Unmarshal([]byte(goldenRootSignedWrapper), &sw); err != nil { |
| + t.Fatal(err) |
| + } |
| + sig, err := base64.StdEncoding.DecodeString(sw.Sig) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if !ed25519.Verify(pub, []byte(sw.Payload), sig) { |
| + t.Fatal("golden signature does not cover the base64 payload string") |
| + } |
| + decoded, err := base64.StdEncoding.DecodeString(sw.Payload) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if ed25519.Verify(pub, decoded, sig) { |
| + t.Fatal("golden signature covers the DECODED payload — the scheme changed") |
| + } |
| +} |