| 1 | package hostops |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/ed25519" |
| 6 | "crypto/rand" |
| 7 | "encoding/json" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/hostactors" |
| 13 | "github.com/carlosframework/platform/internal/hostbox" |
| 14 | "github.com/carlosframework/platform/internal/hostclaim" |
| 15 | "github.com/carlosframework/platform/internal/hostevidence" |
| 16 | "github.com/carlosframework/platform/internal/release" |
| 17 | ) |
| 18 | |
| 19 | func TestBothNewParksReuseAuthenticatedPrePurgeStopReceipt(t *testing.T) { |
| 20 | ctx := context.Background() |
| 21 | pub, key, err := ed25519.GenerateKey(rand.Reader) |
| 22 | if err != nil { |
| 23 | t.Fatal(err) |
| 24 | } |
| 25 | boxPub, boxKey, err := ed25519.GenerateKey(rand.Reader) |
| 26 | if err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | store := release.NewDirStore(t.TempDir()) |
| 30 | at := time.Date(2026, 10, 7, 0, 0, 0, 0, time.UTC) |
| 31 | enrollment := strings.Repeat("a", 32) |
| 32 | roster := hostactors.Roster{V: 1, DeploymentBucket: "bucket", Revision: 3, RequestLimitNS: int64(hostbox.RequestLimit), Regions: map[string]hostactors.Region{"ie": {}}, Boxes: map[string]hostactors.Box{ |
| 33 | "box": {CensusPublicKey: boxPub, Enrollment: enrollment, Namespace: "platform", BoxID: "box", Label: "box", Region: "ie", Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "ie", InstanceID: "i-box"}, EvidenceSHA256: strings.Repeat("c", 64), State: "active"}, |
| 34 | }, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} |
| 35 | signed, err := release.SignRootEnvelope(release.RootSigner(key), roster) |
| 36 | if err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | if _, err = store.Put(ctx, hostactors.Key, signed, ""); err != nil { |
| 40 | t.Fatal(err) |
| 41 | } |
| 42 | inc := strings.Repeat("b", 64) |
| 43 | obs := hostclaim.Observed{Host: "x.example", ETag: "claim-etag", Claim: &hostclaim.Claim{V: 2, Host: "x.example", Gen: 10, Holder: &hostclaim.Holder{Incarnation: inc, Stamp: 4, Session: 5, WriterBox: "box"}, Op: hostclaim.Op{Kind: hostclaim.KindParking, ID: "purge", Exec: "box"}}} |
| 44 | receipt := StopReceipt{Region: "ie", ClaimETag: "original-claim-etag", ObservedBootID: "boot-after-recovery", V: 1, ID: "receipt", Host: obs.Host, Incarnation: inc, InstallationStamp: 4, WriterSession: 5, OperationID: "purge", Phase: "stopping", ObligationID: "pre-teardown-stop", Box: "box", Enrollment: enrollment, BootID: "boot", ControllerID: "controller", ClaimGen: 9, DirectoryRevision: "3", Kind: "stopped", EvidenceAt: at, FenceID: "fence", InventorySHA256: strings.Repeat("d", 64), LocalJournalSHA256: strings.Repeat("e", 64), ProducerDrainDigest: strings.Repeat("f", 64), CgroupIdentities: []string{"cgroup"}, RequestLimitNS: int64(hostbox.RequestLimit)} |
| 45 | raw, err := json.Marshal(receipt) |
| 46 | if err != nil { |
| 47 | t.Fatal(err) |
| 48 | } |
| 49 | receiptKey := "control/hosts/x.example/evidence/purge/box/receipt.json" |
| 50 | if err = hostevidence.Publish(ctx, store, boxKey, receiptKey, raw); err != nil { |
| 51 | t.Fatal(err) |
| 52 | } |
| 53 | authority := ProductionAuthority{Store: store, Directory: hostactors.Directory{Store: store, PublicKey: pub, Bucket: "bucket"}, Now: func() time.Time { return at.Add(time.Hour) }} |
| 54 | for _, consumer := range []struct{ id, target string }{{"new-instance-park", "instances/x.example/" + inc}, {"new-staging-park", "moves/x.example"}} { |
| 55 | ref, err := authority.chooseEvidence(ctx, obs, consumer.id, "purge", "box") |
| 56 | if err != nil { |
| 57 | t.Fatalf("%s did not reuse original stop receipt: %v", consumer.id, err) |
| 58 | } |
| 59 | if ref.ObligationID != consumer.id || ref.SourceObligationID != "pre-teardown-stop" || ref.Key != receiptKey { |
| 60 | t.Fatalf("lost source/consumer binding: %+v", ref) |
| 61 | } |
| 62 | scope, err := retirementScope(obs.Host, inc, consumer.target, map[string]hostclaim.RetirementEvidenceRef{"box": ref}) |
| 63 | if err != nil || scope.Location != consumer.target || !scope.EvidenceAt.Equal(at) { |
| 64 | t.Fatalf("scope = %+v, %v", scope, err) |
| 65 | } |
| 66 | for _, field := range []string{"region", "claim_etag", "observed_boot_id", "request_limit_ns"} { |
| 67 | var changed map[string]json.RawMessage |
| 68 | if err := json.Unmarshal(raw, &changed); err != nil { |
| 69 | t.Fatal(err) |
| 70 | } |
| 71 | changed[field] = json.RawMessage(`"changed"`) |
| 72 | if field == "request_limit_ns" { |
| 73 | changed[field] = json.RawMessage(`1`) |
| 74 | } |
| 75 | tampered, err := json.Marshal(changed) |
| 76 | if err != nil { |
| 77 | t.Fatal(err) |
| 78 | } |
| 79 | _, etag, err := store.Get(ctx, receiptKey) |
| 80 | if err != nil { |
| 81 | t.Fatal(err) |
| 82 | } |
| 83 | etag, err = store.Put(ctx, receiptKey, tampered, etag) |
| 84 | if err != nil { |
| 85 | t.Fatal(err) |
| 86 | } |
| 87 | forged := ref |
| 88 | forged.SHA256 = evidenceDigest(tampered) |
| 89 | if _, err := authority.verifyEvidence(ctx, obs, consumer.id, "purge", forged, false); err == nil { |
| 90 | t.Fatalf("accepted tampered %s with original signature", field) |
| 91 | } |
| 92 | if _, err := store.Put(ctx, receiptKey, raw, etag); err != nil { |
| 93 | t.Fatal(err) |
| 94 | } |
| 95 | } |
| 96 | ref.SourceObligationID = "different-source" |
| 97 | if _, err = authority.verifyEvidence(ctx, obs, consumer.id, "purge", ref, false); err == nil { |
| 98 | t.Fatal("accepted a substituted producer obligation") |
| 99 | } |
| 100 | } |
| 101 | } |
| 102 | |