carlos / platform Public

release: pin the tampered-payload refusal, add schema to the flag spec snippet

Review found the production_test.go header comment claimed a tampered
payload is refused with no test pinning it — a refactor that decoded first
and verified re-marshalled bytes would have left every test green. Add
TestProductionFlagRefusesTamperedPayload: flip the decoded payload's
production bit, re-encode, keep the ORIGINAL signature, and assert
GetProduction refuses it. Also add the missing "schema": 1 field to the
flag object's JSON snippet in the design doc — the implementation requires
it but the doc's example omitted it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VioXzogWf1Xi4Rswz2B6EK
Paul Campbell 32e8411af5837999a8f798840582f5e330167d3c parent 507d2e4
2 files changed, +55 −1
  • docs/superpowers/specs/2026-08-04-console-bake-controls-design.md +1 −1
  • internal/release/production_test.go +54 −0
diff --git a/docs/superpowers/specs/2026-08-04-console-bake-controls-design.md b/docs/superpowers/specs/2026-08-04-console-bake-controls-design.md
index b815d616f..baf25c278 100644
--- a/docs/superpowers/specs/2026-08-04-console-bake-controls-design.md
+++ b/docs/superpowers/specs/2026-08-04-console-bake-controls-design.md
@@ -52,7 +52,7 @@ keeps baking.
envelope as channel pointers, root-key-signed:
```json
-{"account": "<sqid>", "app": "<app>", "production": true,
+{"schema": 1, "account": "<sqid>", "app": "<app>", "production": true,
"actor": "<address>", "set_at": "<rfc3339>"}
```
diff --git a/internal/release/production_test.go b/internal/release/production_test.go
index da712bf89..d37e79d12 100644
--- a/internal/release/production_test.go
+++ b/internal/release/production_test.go
@@ -148,6 +148,60 @@ func TestProductionFlagRefusesWrongApp(t *testing.T) {
}
}
+// The signature binds the exact base64 payload STRING, not the decoded JSON
+// meaning — so a payload that is decoded, edited, and re-encoded (even
+// leaving the original Sig in place) must fail verification. A verifier that
+// decoded first and checked a signature over re-marshalled bytes would let
+// this through if the re-marshalling happened to reproduce the tampered
+// bytes; this test pins the "no canonicalisation" wire rule directly against
+// the field an attacker would actually want to flip.
+func TestProductionFlagRefusesTamperedPayload(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ if err := SetProduction(ctx, s, priv, "bab", "kass", true, "paul@keymail.dev"); err != nil {
+ t.Fatal(err)
+ }
+ raw, etag, err := s.Get(ctx, ProductionKey("bab", "kass"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var sw signedWrapper
+ if err := json.Unmarshal(raw, &sw); err != nil {
+ t.Fatal(err)
+ }
+ payload, err := base64.StdEncoding.DecodeString(sw.Payload)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var f ProductionFlag
+ if err := json.Unmarshal(payload, &f); err != nil {
+ t.Fatal(err)
+ }
+ if !f.Production {
+ t.Fatal("expected production=true before tampering")
+ }
+ f.Production = false // flip the bit an attacker would want to flip
+ tampered, err := json.Marshal(&f)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Keep the ORIGINAL Sig — it covers the ORIGINAL payload string. A
+ // re-encoded payload, even with identical decoded meaning, must fail
+ // verification without a fresh signature.
+ sw.Payload = base64.StdEncoding.EncodeToString(tampered)
+ wrapper, err := json.Marshal(sw)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, ProductionKey("bab", "kass"), wrapper, etag); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := GetProduction(ctx, s, pub, "bab", "kass"); err == nil {
+ t.Fatal("a tampered payload (re-encoded, original sig) verified")
+ }
+}
+
// timeNowTestHelper is a stamped, truncated now — what signProductionFlag
// itself writes — for tests that hand-build payloads.
func timeNowTestHelper() time.Time { return time.Now().UTC().Truncate(time.Second) }