release: the per-app production flag — root-signed, missing means not production
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VioXzogWf1Xi4Rswz2B6EK
2 files changed,
+312
−0
internal/release/production.go+159 −0internal/release/production_test.go+153 −0
diff --git a/internal/release/production.go b/internal/release/production.go| new file mode 100644 |
| index 000000000..d22c4f010 |
| --- /dev/null |
| +++ b/internal/release/production.go |
| @@ -0,0 +1,159 @@ |
| +// The per-app production flag — the console bake controls spec |
| +// (docs/superpowers/specs/2026-08-04-console-bake-controls-design.md, §2–§3). |
| +// |
| +// production: true means the supply-chain bake window (bake.go) applies to |
| +// this app's held channels; false — and MISSING, the default — means its |
| +// boxes adopt immediately. The object is root-signed in the pointer's own |
| +// signedWrapper envelope, and root-ONLY: a wrapper carrying a key grant is |
| +// refused before its signature is even checked, because a scoped key that |
| +// could flip the flag could skip the very window it bakes under. |
| +// |
| +// What verification here does NOT decide: whether a missing or unreadable |
| +// flag downgrades a box that once saw production: true. It never does — |
| +// that memory, the ratchet and the replay floor are the box's own |
| +// (bake.go's production memory), because anyone with bucket write but no |
| +// key can delete an object, and deletion must not be a downgrade (spec §4). |
| +package release |
| + |
| +import ( |
| + "context" |
| + "crypto/ed25519" |
| + "encoding/base64" |
| + "encoding/json" |
| + "errors" |
| + "fmt" |
| + "os" |
| + "time" |
| +) |
| + |
| +// ProductionFlag is the signed payload: one app's declaration that it is (or |
| +// is not) in production, and therefore whether its releases bake. |
| +type ProductionFlag struct { |
| + Schema int `json:"schema"` // 1 |
| + Account string `json:"account"` |
| + App string `json:"app"` |
| + Production bool `json:"production"` |
| + // Actor is who flipped it — a console member's address. Recorded for the |
| + // same reason the pointer's audit trail exists: an invisible bypass is a |
| + // broken gate. |
| + Actor string `json:"actor,omitempty"` |
| + SetAt time.Time `json:"set_at"` |
| +} |
| + |
| +// ProductionKey is where an app's flag lives: beside channels/ and |
| +// releases/, deliberately NEVER under ChannelsPrefix — those listings are |
| +// parsed as channel pointers by the console and the CLI. |
| +func ProductionKey(acct, app string) string { |
| + return appPrefix(acct, app) + "production.json" |
| +} |
| + |
| +// SetProduction writes the flag, root key only, CAS over whatever is there |
| +// (a concurrent flip surfaces as ErrCASConflict — re-ask, never overwrite |
| +// blind). SetAt is stamped here; boxes use their OWN sighting for the |
| +// ratchet and this stamp only as a replay floor. |
| +func SetProduction(ctx context.Context, s Store, priv ed25519.PrivateKey, acct, app string, production bool, actor string) error { |
| + if len(priv) != ed25519.PrivateKeySize { |
| + return fmt.Errorf("set production: no usable release signing key") |
| + } |
| + if !ValidAccount(acct) { |
| + return fmt.Errorf("set production: invalid account %q", acct) |
| + } |
| + if !ValidApp(app) { |
| + return fmt.Errorf("set production: invalid app %q", app) |
| + } |
| + key := ProductionKey(acct, app) |
| + _, etag, err := s.Get(ctx, key) |
| + if errors.Is(err, os.ErrNotExist) { |
| + etag = "" // first write: create-only |
| + } else if err != nil { |
| + return fmt.Errorf("set production: read %s: %w", key, err) |
| + } |
| + f := &ProductionFlag{ |
| + Schema: 1, Account: acct, App: app, Production: production, |
| + Actor: actor, SetAt: time.Now().UTC().Truncate(time.Second), |
| + } |
| + if err := validateProductionFlag(f, acct, app); err != nil { |
| + return fmt.Errorf("set production: %w", err) |
| + } |
| + payload, err := json.MarshalIndent(f, "", " ") |
| + if err != nil { |
| + return err |
| + } |
| + b64 := base64.StdEncoding.EncodeToString(payload) |
| + wrapper, err := json.MarshalIndent(signedWrapper{ |
| + Payload: b64, |
| + Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(priv, []byte(b64))), |
| + }, "", " ") |
| + if err != nil { |
| + return err |
| + } |
| + _, err = s.Put(ctx, key, wrapper, etag) |
| + return err |
| +} |
| + |
| +// GetProduction resolves an app's flag. A MISSING object is (nil, nil): the |
| +// default, not production — but never let a nil collapse the distinction the |
| +// spec draws: any OTHER failure (unreadable, unverifiable, misnamed) is the |
| +// error, and callers on the adopt path treat it as production=true, failing |
| +// toward baking. |
| +func GetProduction(ctx context.Context, s Store, pub ed25519.PublicKey, acct, app string) (*ProductionFlag, error) { |
| + raw, _, err := s.Get(ctx, ProductionKey(acct, app)) |
| + if errors.Is(err, os.ErrNotExist) { |
| + return nil, nil |
| + } |
| + if err != nil { |
| + return nil, err |
| + } |
| + return verifyProductionFlag(pub, raw, acct, app) |
| +} |
| + |
| +// verifyProductionFlag is verifyPointer's discipline for the flag: refuse a |
| +// grant outright, check the signature against the ROOT key alone, decode |
| +// after, validate the payload names the account/app whose key it sits at. |
| +func verifyProductionFlag(pub ed25519.PublicKey, wrapper []byte, wantAcct, wantApp string) (*ProductionFlag, error) { |
| + var sw signedWrapper |
| + if err := json.Unmarshal(wrapper, &sw); err != nil { |
| + return nil, fmt.Errorf("production flag wrapper: %v", err) |
| + } |
| + if sw.Grant != nil { |
| + return nil, fmt.Errorf("production flag %s/%s: signed by a scoped key — the flag takes the root key, because a delegate that could flip it could skip the window it bakes under", wantAcct, wantApp) |
| + } |
| + if len(pub) != ed25519.PublicKeySize { |
| + return nil, fmt.Errorf("production flag: no usable release public key") |
| + } |
| + sig, err := base64.StdEncoding.DecodeString(sw.Sig) |
| + if err != nil { |
| + return nil, fmt.Errorf("production flag sig: %v", err) |
| + } |
| + if !ed25519.Verify(pub, []byte(sw.Payload), sig) { |
| + return nil, fmt.Errorf("production flag signature verification failed") |
| + } |
| + payload, err := base64.StdEncoding.DecodeString(sw.Payload) |
| + if err != nil { |
| + return nil, fmt.Errorf("production flag payload: %v", err) |
| + } |
| + var f ProductionFlag |
| + if err := json.Unmarshal(payload, &f); err != nil { |
| + return nil, fmt.Errorf("production flag payload: %v", err) |
| + } |
| + if err := validateProductionFlag(&f, wantAcct, wantApp); err != nil { |
| + return nil, err |
| + } |
| + return &f, nil |
| +} |
| + |
| +func validateProductionFlag(f *ProductionFlag, wantAcct, wantApp string) error { |
| + if f.Schema != 1 { |
| + return fmt.Errorf("unsupported production flag schema %d", f.Schema) |
| + } |
| + if f.Account != wantAcct || f.App != wantApp { |
| + return fmt.Errorf("production flag names %s/%s, expected %s/%s", f.Account, f.App, wantAcct, wantApp) |
| + } |
| + if !ValidAccount(f.Account) || !ValidApp(f.App) { |
| + return fmt.Errorf("production flag has invalid account/app %s/%s", f.Account, f.App) |
| + } |
| + if f.SetAt.IsZero() { |
| + return fmt.Errorf("production flag has no set_at") |
| + } |
| + return nil |
| +} |
diff --git a/internal/release/production_test.go b/internal/release/production_test.go| new file mode 100644 |
| index 000000000..da712bf89 |
| --- /dev/null |
| +++ b/internal/release/production_test.go |
| @@ -0,0 +1,153 @@ |
| +package release |
| + |
| +// The production flag (2026-08-04 console bake controls spec §2–§3): a |
| +// root-signed per-app object deciding whether the supply-chain bake applies. |
| +// The properties tested here, in the order they'd hurt if lost: missing is |
| +// the default (nil, nil); a set flag round-trips; a scoped key's wrapper is |
| +// refused even with a valid root grant; a tampered payload is refused; the |
| +// key sits OUTSIDE channels/ so pointer listings never try to parse it. |
| + |
| +import ( |
| + "context" |
| + "crypto/ed25519" |
| + "crypto/rand" |
| + "encoding/base64" |
| + "encoding/json" |
| + "strings" |
| + "testing" |
| + "time" |
| +) |
| + |
| +func TestProductionFlagDefaultIsMissing(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + _, pub := testKeys(t) |
| + f, err := GetProduction(context.Background(), s, pub, "bab", "kass") |
| + if err != nil || f != nil { |
| + t.Fatalf("GetProduction on empty store = %v, %v; want nil, nil (the default: not production)", f, err) |
| + } |
| +} |
| + |
| +func TestProductionFlagRoundTrips(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.Fatalf("SetProduction: %v", err) |
| + } |
| + f, err := GetProduction(ctx, s, pub, "bab", "kass") |
| + if err != nil { |
| + t.Fatalf("GetProduction: %v", err) |
| + } |
| + if f == nil || !f.Production || f.Actor != "paul@keymail.dev" || f.SetAt.IsZero() { |
| + t.Fatalf("flag = %+v, want production on, actor recorded, set_at stamped", f) |
| + } |
| + // Flip it off over the existing object — the CAS path. |
| + if err := SetProduction(ctx, s, priv, "bab", "kass", false, "paul@keymail.dev"); err != nil { |
| + t.Fatalf("SetProduction off: %v", err) |
| + } |
| + if f, _ = GetProduction(ctx, s, pub, "bab", "kass"); f == nil || f.Production { |
| + t.Fatalf("flag = %+v after flipping off, want production false", f) |
| + } |
| +} |
| + |
| +// The flag key lives beside channels/, never inside it: ChannelsPrefix |
| +// listings are parsed as pointers by the console and CLI. |
| +func TestProductionKeyIsOutsideChannels(t *testing.T) { |
| + if k := ProductionKey("bab", "kass"); k != "apps/bab/kass/production.json" { |
| + t.Fatalf("ProductionKey = %q", k) |
| + } |
| + if strings.HasPrefix(ProductionKey("bab", "kass"), ChannelsPrefix("bab", "kass")) { |
| + t.Fatal("production flag key sits under channels/ — pointer listings would try to parse it") |
| + } |
| +} |
| + |
| +// A scoped key must not be able to mint the flag: a wrapper carrying ANY |
| +// grant is refused before signature checking, and a wrapper signed by a key |
| +// other than the root fails signature checking. Together: root only. |
| +func TestProductionFlagRefusesScopedAndForeignKeys(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + |
| + // Build a validly-shaped flag payload signed by a DIFFERENT key. |
| + otherPub, otherPriv, err := ed25519.GenerateKey(rand.Reader) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + _ = otherPub |
| + payload, _ := json.Marshal(ProductionFlag{Schema: 1, Account: "bab", App: "kass", |
| + Production: false, SetAt: timeNowTestHelper()}) |
| + b64 := base64.StdEncoding.EncodeToString(payload) |
| + wrapper, _ := json.Marshal(signedWrapper{ |
| + Payload: b64, |
| + Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(otherPriv, []byte(b64))), |
| + }) |
| + if _, err := s.Put(ctx, ProductionKey("bab", "kass"), wrapper, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := GetProduction(ctx, s, pub, "bab", "kass"); err == nil { |
| + t.Fatal("a flag signed by a non-root key verified") |
| + } |
| + |
| + // A wrapper WITH a grant is refused outright — even a grant the root |
| + // really signed. Mint a real scoped key and have it sign the flag. |
| + seed, grantB64, err := MintKeyGrant(priv, "builder", []string{RungEdge}, maxGrantTTL) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + scopedKey, err := ParseSigningKey(seed, "test") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + sg, err := parseGrantBlob(grantB64, "test") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + wrapper, _ = json.Marshal(signedWrapper{ |
| + Payload: b64, |
| + Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(scopedKey, []byte(b64))), |
| + Grant: sg, |
| + }) |
| + if _, err := s.Put(ctx, ProductionKey("bab", "kass"), wrapper, etagOfKey(t, s, "bab", "kass")); err != nil { |
| + t.Fatal(err) |
| + } |
| + _, err = GetProduction(ctx, s, pub, "bab", "kass") |
| + if err == nil || !strings.Contains(err.Error(), "root") { |
| + t.Fatalf("a grant-bearing flag wrapper verified (err=%v), want a root-only refusal", err) |
| + } |
| +} |
| + |
| +// etagOfKey reads the current etag of an app's flag so a test can CAS over it. |
| +func etagOfKey(t *testing.T, s Store, acct, app string) string { |
| + t.Helper() |
| + _, etag, err := s.Get(context.Background(), ProductionKey(acct, app)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return etag |
| +} |
| + |
| +func TestProductionFlagRefusesWrongApp(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + if err := SetProduction(ctx, s, priv, "bab", "kass", true, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + // Copy kass's flag to keymail's key — a cross-app replay must be refused |
| + // because the SIGNED payload names the app. |
| + raw, _, err := s.Get(ctx, ProductionKey("bab", "kass")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := s.Put(ctx, ProductionKey("bab", "keymail"), raw, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := GetProduction(ctx, s, pub, "bab", "keymail"); err == nil { |
| + t.Fatal("another app's flag verified under keymail's key") |
| + } |
| +} |
| + |
| +// 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) } |