package host import ( "crypto/ed25519" "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" ) // The regional enrollment check runs on the production roster adapter: a // roster revision bump (a create's actor hold is one) passes, and a // quarantine and readmission since startup still refuses. func TestClaimRegionalEnrollmentSurvivesRosterRevision(t *testing.T) { key := ed25519.NewKeyFromSeed(make([]byte, 32)) s := release.NewDirStore(t.TempDir()) bucket := "deployment" if err := hostfloor.Publish(t.Context(), s, release.RootSigner(key), bucket, hostfloor.Floor{Min: 1, MinRelease: "v0.1.0"}); err != nil { t.Fatal(err) } raw, _, _ := s.Get(t.Context(), hostfloor.Key) auth, _, _ := s.Get(t.Context(), hostfloor.AuthKey) dir := t.TempDir() body, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: bucket, Mode: "enforced", Floor: raw, Authorization: auth}) if err := os.WriteFile(filepath.Join(dir, "policy.json"), body, 0600); err != nil { t.Fatal(err) } identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box", Enrollment: strings.Repeat("a", 32), Region: "ie", ControlBucket: bucket, Revision: 1, Binding: "1"}) row := hostactors.Box{BoxID: "box", Enrollment: strings.Repeat("a", 32), Namespace: identity.Namespace, Label: "box", Region: "ie", Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: identity.Account, Region: identity.AWSRegion, InstanceID: "i-0abc"}, EvidenceSHA256: strings.Repeat("c", 64), State: "active"} roster := hostactors.Roster{V: 1, DeploymentBucket: bucket, Revision: 1, RequestLimitNS: int64(hostbox.RequestLimit), Publication: "published", FloorSHA256: hostfloor.Digest(raw), Boxes: map[string]hostactors.Box{"box": row}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} write := func() { t.Helper() wire, err := release.SignRootEnvelope(release.RootSigner(key), roster) if err != nil { t.Fatal(err) } _, etag, _ := s.Get(t.Context(), hostactors.Key) if _, err = s.Put(t.Context(), hostactors.Key, wire, etag); err != nil { t.Fatal(err) } } write() identity.Roster = productionClaimRoster{Directory: hostactors.Directory{Store: s, PublicKey: key.Public().(ed25519.PublicKey), Bucket: bucket}, DataBucket: identity.Bucket, FloorDir: dir} actor, err := identity.Verify(t.Context()) if err != nil { t.Fatal(err) } e := claimRegionalEnrollment{identity: identity, actor: actor} check := func(step string, want error) { t.Helper() if err := e.VerifyRegional(t.Context(), actor.Region, actor.Bucket, hostbox.RequestLimit); !errors.Is(err, want) && !(want == nil && err == nil) { t.Fatalf("%s: VerifyRegional = %v, want %v", step, err, want) } } check("startup", nil) roster.Revision++ write() check("roster revision bump", nil) row.State, row.QuarantineRevision, row.LossIDs = "quarantined", 1, []string{"loss"} roster.Boxes["box"], roster.Revision = row, roster.Revision+1 write() check("quarantined", hostbox.ErrChanged) row.State = "active" roster.Boxes["box"], roster.Revision = row, roster.Revision+1 write() check("readmitted since startup", hostbox.ErrChanged) } // Each enrollment field the roster carries still refuses when it changes // under a running assembly; only the revision bump passes. func TestClaimRegionalEnrollmentFieldChanges(t *testing.T) { base := hostbox.Actor{Box: "box", Enrollment: "enrolled", Region: "region", FleetHomeRegion: "region", ControlBucket: "control", Revision: 1, Binding: "1"} identity := handoverRecoveryIdentity(t, base) actor, err := identity.Verify(t.Context()) if err != nil { t.Fatal(err) } e := claimRegionalEnrollment{identity: identity, actor: actor} for name, edit := range map[string]func(*hostbox.Actor){ "revision": func(a *hostbox.Actor) { a.Revision++ }, "eligible": func(a *hostbox.Actor) { a.Eligible = false }, "quarantined": func(a *hostbox.Actor) { a.Quarantined = true }, "binding": func(a *hostbox.Actor) { a.Binding += "-changed" }, "box": func(a *hostbox.Actor) { a.Box = "other" }, "enrollment": func(a *hostbox.Actor) { a.Enrollment = "re-enrolled" }, "region": func(a *hostbox.Actor) { a.Region = "other" }, "bucket": func(a *hostbox.Actor) { a.Bucket = "other" }, "namespace": func(a *hostbox.Actor) { a.Namespace = "other" }, "control bucket": func(a *hostbox.Actor) { a.ControlBucket = "other" }, "fleet home region": func(a *hostbox.Actor) { a.FleetHomeRegion = "other" }, "cloud account": func(a *hostbox.Actor) { a.Cloud.Account = "444455556666" }, "cloud region": func(a *hostbox.Actor) { a.Cloud.Region = "eu-west-2" }, "cloud instance": func(a *hostbox.Actor) { a.Cloud.InstanceID = "i-0def" }, } { t.Run(name, func(t *testing.T) { changed := identity row := actor row.HomeRegion = "" edit(&row) changed.Roster = handoverRecoveryRoster{row} e := e e.identity = changed err := e.VerifyRegional(t.Context(), actor.Region, actor.Bucket, hostbox.RequestLimit) if name == "revision" { if err != nil { t.Fatalf("revision bump refused: %v", err) } return } // A new instance ID fails Verify's signed cloud evidence first, // with that check's own error; every other change is ErrChanged. if err == nil || (name != "cloud instance" && !errors.Is(err, hostbox.ErrChanged)) { t.Fatalf("changed enrollment: %v, want ErrChanged", err) } }) } }