settings-2 task 2: reader-side refusal tests for promotion records
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+215
−0
internal/release/promotion_test.go+215 −0
diff --git a/internal/release/promotion_test.go b/internal/release/promotion_test.go| index 99444b8de..525b74276 100644 |
| --- a/internal/release/promotion_test.go |
| +++ b/internal/release/promotion_test.go |
| @@ -2,8 +2,12 @@ package release |
| import ( |
| "context" |
| + "crypto/ed25519" |
| + "encoding/base64" |
| + "encoding/json" |
| "errors" |
| "os" |
| + "strings" |
| "testing" |
| "time" |
| ) |
| @@ -142,3 +146,214 @@ func TestWriteKeepsAnIdenticalRecordAndReplacesAnother(t *testing.T) { |
| t.Fatal("a record for different bytes did not replace the old one") |
| } |
| } |
| + |
| +// ---- the reader's own refusals ------------------------------------------------ |
| +// |
| +// WritePromotionRecord refuses every input below before it signs, so these |
| +// tests hand-build the wrapper and Put it directly: the only thing standing |
| +// between each record and a pin is ReadPromotionRecord's own check. |
| + |
| +// recordPayload is rec as the base64 payload string a wrapper signs. |
| +func recordPayload(t *testing.T, rec PromotionRecord) string { |
| + t.Helper() |
| + payload, err := json.MarshalIndent(rec, "", " ") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return base64.StdEncoding.EncodeToString(payload) |
| +} |
| + |
| +// putRecordWrapper writes sw at rec's own record key, replacing whatever is |
| +// there. |
| +func putRecordWrapper(t *testing.T, s Store, rec PromotionRecord, sw signedWrapper) { |
| + t.Helper() |
| + ctx := context.Background() |
| + key := PromotionRecordKey(rec.Account, rec.App, rec.Version) |
| + _, etag, err := s.Get(ctx, key) |
| + if errors.Is(err, os.ErrNotExist) { |
| + etag = "" |
| + } else if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := s.Put(ctx, key, mustJSON(t, sw), etag); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +// putSignedRecord signs rec with key, attaches grant (nil for a root-style |
| +// wrapper) and writes it, with none of WritePromotionRecord's refusals. |
| +func putSignedRecord(t *testing.T, s Store, rec PromotionRecord, key ed25519.PrivateKey, grant *signedGrant) signedWrapper { |
| + t.Helper() |
| + b64 := recordPayload(t, rec) |
| + sw := signedWrapper{ |
| + Payload: b64, |
| + Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(key, []byte(b64))), |
| + Grant: grant, |
| + } |
| + putRecordWrapper(t, s, rec, sw) |
| + return sw |
| +} |
| + |
| +// handBuiltRecord ships bab/km/v1 and returns a record for it that every |
| +// reader check accepts, dated an hour into goodGrant's window. |
| +func handBuiltRecord(t *testing.T) (Store, PromotionRecord) { |
| + t.Helper() |
| + s := NewDirStore(t.TempDir()) |
| + shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| + raw, _, err := s.Get(context.Background(), ManifestKey("bab", "km", "v1")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return s, PromotionRecord{ |
| + Schema: 1, Account: "bab", App: "km", Version: "v1", |
| + ManifestDigest: ManifestDigest(raw), Channel: "canary/x", Actor: "test", |
| + At: goodGrant(nil).MintedAt.Add(time.Hour), |
| + } |
| +} |
| + |
| +// assertRecordRefused wants ReadPromotionRecord to refuse with an error that |
| +// names want and is never "absent", and AuthorizedManifest to refuse too. |
| +func assertRecordRefused(t *testing.T, s Store, pub ed25519.PublicKey, rec PromotionRecord, want string) { |
| + t.Helper() |
| + ctx := context.Background() |
| + got, err := ReadPromotionRecord(ctx, s, pub, rec.Account, rec.App, rec.Version) |
| + if err == nil || errors.Is(err, ErrNoPromotionRecord) || !strings.Contains(err.Error(), want) { |
| + t.Fatalf("ReadPromotionRecord = %+v, %v; want a refusal naming %q that is not 'absent'", got, err, want) |
| + } |
| + if m, err := AuthorizedManifest(ctx, s, pub, rec.Account, rec.App, rec.Version); err == nil || errors.Is(err, ErrNoPromotionRecord) { |
| + t.Fatalf("AuthorizedManifest = %+v, %v; want a refusal that is not 'absent'", m, err) |
| + } |
| +} |
| + |
| +// assertRecordReads is the control: the hand-built wrapper is a real record, |
| +// so each refusal beside it is the reader's check and not a fixture artefact. |
| +func assertRecordReads(t *testing.T, s Store, pub ed25519.PublicKey, rec PromotionRecord) { |
| + t.Helper() |
| + ctx := context.Background() |
| + got, err := ReadPromotionRecord(ctx, s, pub, rec.Account, rec.App, rec.Version) |
| + if err != nil || got.Channel != rec.Channel || got.ManifestDigest != rec.ManifestDigest { |
| + t.Fatalf("control: ReadPromotionRecord = %+v, %v", got, err) |
| + } |
| + if m, err := AuthorizedManifest(ctx, s, pub, rec.Account, rec.App, rec.Version); err != nil || m.Version != rec.Version { |
| + t.Fatalf("control: AuthorizedManifest = %+v, %v", m, err) |
| + } |
| +} |
| + |
| +// The reader's signature check: a payload edited after signing, and a record |
| +// signed by a key that is neither the root nor the key its grant names. |
| +func TestReaderRefusesABadRecordSignature(t *testing.T) { |
| + root, rootPub := testKeys(t) |
| + other, otherPub := testKeys(t) |
| + t.Run("control", func(t *testing.T) { |
| + s, rec := handBuiltRecord(t) |
| + putSignedRecord(t, s, rec, root, nil) |
| + assertRecordReads(t, s, rootPub, rec) |
| + }) |
| + t.Run("tampered payload", func(t *testing.T) { |
| + s, rec := handBuiltRecord(t) |
| + sw := putSignedRecord(t, s, rec, root, nil) |
| + edited := rec |
| + edited.Channel = "stable" |
| + sw.Payload = recordPayload(t, edited) |
| + putRecordWrapper(t, s, rec, sw) |
| + assertRecordRefused(t, s, rootPub, rec, "signature verification failed") |
| + }) |
| + t.Run("signed by a key that is not the root", func(t *testing.T) { |
| + s, rec := handBuiltRecord(t) |
| + putSignedRecord(t, s, rec, other, nil) |
| + assertRecordRefused(t, s, rootPub, rec, "signature verification failed") |
| + }) |
| + t.Run("signed by a key its grant does not name", func(t *testing.T) { |
| + s, rec := handBuiltRecord(t) |
| + putSignedRecord(t, s, rec, root, forgeGrantSub(t, root, goodGrant(otherPub))) |
| + assertRecordRefused(t, s, rootPub, rec, "signature verification failed") |
| + }) |
| +} |
| + |
| +// The reader's verifyGrant call: a grant signed by anything but the pinned |
| +// root makes the record it carries worthless. |
| +func TestReaderRefusesAGrantNotSignedByTheRoot(t *testing.T) { |
| + root, rootPub := testKeys(t) |
| + impostor, _ := testKeys(t) |
| + scoped, scopedPub := testKeys(t) |
| + t.Run("control", func(t *testing.T) { |
| + s, rec := handBuiltRecord(t) |
| + putSignedRecord(t, s, rec, scoped, forgeGrantSub(t, root, goodGrant(scopedPub))) |
| + assertRecordReads(t, s, rootPub, rec) |
| + }) |
| + t.Run("grant signed by another key", func(t *testing.T) { |
| + s, rec := handBuiltRecord(t) |
| + putSignedRecord(t, s, rec, scoped, forgeGrantSub(t, impostor, goodGrant(scopedPub))) |
| + assertRecordRefused(t, s, rootPub, rec, "key grant signature verification failed") |
| + }) |
| +} |
| + |
| +// The reader's "authorization signed by a scoped key" refusal: a grant |
| +// covers channels, never an authorization, whatever channel rides beside it. |
| +// |
| +// Mutation-mindedness: delete that refusal in ReadPromotionRecord and the |
| +// first case reads as a valid record (canary/x is in scope and in window), |
| +// while the second fails with scope.allows' "not a rung" instead. |
| +func TestReaderRefusesAScopedAuthorization(t *testing.T) { |
| + root, rootPub := testKeys(t) |
| + scoped, scopedPub := testKeys(t) |
| + grant := forgeGrantSub(t, root, goodGrant(scopedPub)) |
| + t.Run("control", func(t *testing.T) { |
| + s, rec := handBuiltRecord(t) |
| + putSignedRecord(t, s, rec, scoped, grant) |
| + assertRecordReads(t, s, rootPub, rec) |
| + }) |
| + for _, tc := range []struct { |
| + name string |
| + channel string |
| + authorized bool |
| + want string |
| + }{ |
| + {"authorized with an in-scope channel", "canary/x", true, "an authorization signed by a scoped key"}, |
| + {"authorized with no channel", "", true, "an authorization signed by a scoped key"}, |
| + {"no channel and not authorized", "", false, "names no channel"}, |
| + } { |
| + t.Run(tc.name, func(t *testing.T) { |
| + s, rec := handBuiltRecord(t) |
| + rec.Channel, rec.Authorized = tc.channel, tc.authorized |
| + putSignedRecord(t, s, rec, scoped, grant) |
| + assertRecordRefused(t, s, rootPub, rec, tc.want) |
| + }) |
| + } |
| +} |
| + |
| +// The reader's scope.allows check: a scoped record must name a channel on a |
| +// rung its grant covers, dated inside the grant's window. |
| +// |
| +// Mutation-mindedness: delete that check in ReadPromotionRecord and every |
| +// case below reads as a valid record. |
| +func TestReaderHoldsAScopedRecordToItsGrant(t *testing.T) { |
| + root, rootPub := testKeys(t) |
| + scoped, scopedPub := testKeys(t) |
| + g := goodGrant(scopedPub) // canary and edge |
| + grant := forgeGrantSub(t, root, g) |
| + t.Run("control", func(t *testing.T) { |
| + s, rec := handBuiltRecord(t) |
| + rec.Channel, rec.At = "edge", g.ExpiresAt |
| + putSignedRecord(t, s, rec, scoped, grant) |
| + assertRecordReads(t, s, rootPub, rec) |
| + }) |
| + for _, tc := range []struct { |
| + name string |
| + channel string |
| + at time.Time |
| + want string |
| + }{ |
| + {"stable", "stable", g.MintedAt.Add(time.Hour), "never delegable"}, |
| + {"a rung the grant does not cover", "beta", g.MintedAt.Add(time.Hour), "is on rung beta"}, |
| + {"dated after the grant expired", "canary/x", g.ExpiresAt.Add(time.Second), "expired"}, |
| + {"dated before the grant was minted", "canary/x", g.MintedAt.Add(-time.Second), "predates"}, |
| + } { |
| + t.Run(tc.name, func(t *testing.T) { |
| + s, rec := handBuiltRecord(t) |
| + rec.Channel, rec.At = tc.channel, tc.at |
| + putSignedRecord(t, s, rec, scoped, grant) |
| + assertRecordRefused(t, s, rootPub, rec, tc.want) |
| + }) |
| + } |
| +} |