package hostactors import ( "context" "encoding/json" "strings" "testing" "time" "github.com/carlosframework/platform/internal/release" ) func TestPlan11EnrollmentTimestampSignedImmutable(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", "1") ctx := context.Background() d, sg, before := fixture(t) next := Clone(before.Roster) b := next.Boxes["box"] b.BoxID = "second" b.Enrollment = strings.Repeat("c", 32) b.Cloud.InstanceID = "i-second" next.Boxes["second"] = b next.Consoles["console"] = Console{Enrollment: strings.Repeat("d", 32), InstanceID: "console", Namespaces: []string{"platform"}, DeploymentBucket: d.Bucket} started := time.Now().UTC() got, err := d.Replace(ctx, sg, before, next) if err != nil { t.Fatal(err) } if got.Roster.Boxes["second"].EnrolledAt.Before(started) || got.Roster.Boxes["second"].EnrolledAt.After(time.Now()) { t.Fatal("new box has no authoritative enrollment time") } if got.Roster.Consoles["console"].EnrolledAt.IsZero() { t.Fatal("console missing enrollment time") } read, err := d.Read(ctx) if err != nil { t.Fatal(err) } if !read.Roster.Boxes["second"].EnrolledAt.Equal(got.Roster.Boxes["second"].EnrolledAt) { t.Fatal("timestamp not signed and round-tripped") } for _, which := range []string{"box-change", "box-erase", "console-change", "legacy-backfill"} { n := Clone(got.Roster) switch which { case "box-change": b := n.Boxes["second"] b.EnrolledAt = b.EnrolledAt.Add(time.Second) n.Boxes["second"] = b case "box-erase": b := n.Boxes["second"] b.EnrolledAt = time.Time{} n.Boxes["second"] = b case "console-change": c := n.Consoles["console"] c.EnrolledAt = c.EnrolledAt.Add(time.Second) n.Consoles["console"] = c case "legacy-backfill": b := n.Boxes["box"] b.EnrolledAt = time.Now() n.Boxes["box"] = b } if _, err := d.Replace(ctx, sg, got, n); err == nil { t.Fatal("changed immutable timestamp", which) } } retry, err := d.Replace(ctx, sg, got, Clone(got.Roster)) if err != nil { t.Fatal(err) } if !retry.Roster.Boxes["second"].EnrolledAt.Equal(got.Roster.Boxes["second"].EnrolledAt) { t.Fatal("retry reset timestamp") } } func TestPlan11LegacyRosterBytesAndForgedTimestamp(t *testing.T) { ctx := context.Background() d, sg, o := fixture(t) old, _, err := d.Store.Get(ctx, Key) if err != nil { t.Fatal(err) } raw, err := release.SignRootEnvelope(sg, o.Roster) if err != nil { t.Fatal(err) } if string(raw) != string(old) { t.Fatal("legacy signature bytes changed") } payload, _ := json.Marshal(o.Roster) if strings.Contains(string(payload), "enrolled_at") { t.Fatal("zero field changed legacy shape") } next := Clone(o.Roster) b := next.Boxes["box"] b.BoxID = "new" b.Enrollment = strings.Repeat("e", 32) b.Cloud.InstanceID = "i-new" b.EnrolledAt = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) next.Boxes["new"] = b if _, err := d.Replace(ctx, sg, o, next); err == nil { t.Fatal("caller supplied enrollment time") } // A signed unknown nested field still fails the exact roster decoder. var generic map[string]any _ = json.Unmarshal(payload, &generic) generic["boxes"].(map[string]any)["box"].(map[string]any)["enrolled_at_extra"] = "2026-10-09T00:00:00Z" forged, _ := release.SignRootEnvelope(sg, generic) if _, err := d.Store.Put(ctx, Key, forged, o.ETag); err != nil { t.Fatal(err) } if _, err := d.Read(ctx); err == nil { t.Fatal("unknown roster field accepted") } }