| index df07964c0..19d9d28f8 100644 |
| --- a/internal/host/relocationreleases_test.go |
| +++ b/internal/host/relocationreleases_test.go |
| @@ -1,7 +1,9 @@ |
| package host |
| |
| import ( |
| + "bytes" |
| "context" |
| + "crypto/ed25519" |
| "crypto/sha256" |
| "encoding/hex" |
| "errors" |
| @@ -9,28 +11,67 @@ import ( |
| "testing" |
| "time" |
| |
| + "github.com/carlosframework/platform/internal/hostactors" |
| + "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostevidence" |
| "github.com/carlosframework/platform/internal/release" |
| ) |
| |
| +// releaseRoster publishes a root-signed roster naming src and dst with |
| +// their census keys, and returns the directory that reads it. |
| +func releaseRoster(t *testing.T, s release.Store, boxes map[string]hostactors.Box) hostactors.Directory { |
| + t.Helper() |
| + root := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize)) |
| + roster := hostactors.Roster{V: 1, DeploymentBucket: "bucket", Revision: 5, RequestLimitNS: int64(hostbox.RequestLimit), Regions: map[string]hostactors.Region{"ie": {}}, |
| + Boxes: boxes, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} |
| + body, err := release.SignRootEnvelope(release.RootSigner(root), roster) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := s.Put(context.Background(), hostactors.Key, body, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + return hostactors.Directory{Store: s, PublicKey: root.Public().(ed25519.PublicKey), Bucket: "bucket"} |
| +} |
| + |
| +func rosterBox(name, enrollment string, key ed25519.PrivateKey) hostactors.Box { |
| + return hostactors.Box{CensusPublicKey: key.Public().(ed25519.PublicKey), Enrollment: enrollment, Namespace: "platform", BoxID: name, Label: name, Region: "ie", Pool: "default", |
| + Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "ie", InstanceID: "i-" + name}, EvidenceSHA256: strings.Repeat("c", 64), State: "active"} |
| +} |
| + |
| func TestStopReceiptReleasesBindsTheReceiptToTheRelease(t *testing.T) { |
| ctx := context.Background() |
| inc := strings.Repeat("a", 64) |
| - c := hostclaim.Claim{Host: "shop.example", Holder: &hostclaim.Holder{Incarnation: inc}, Op: hostclaim.Op{ID: "rel-1", Kind: hostclaim.KindRelocating, Phase: hostclaim.PhaseReleasing}} |
| - receipt := StopReceipt{V: 1, ID: "r1", Host: "shop.example", Incarnation: inc, WriterSession: 12, OperationID: "rel-1", Phase: hostclaim.PhaseReleasing, ObligationID: RelocationReleaseObligation, |
| - Box: "src", Enrollment: "enr", Kind: "stopped", EvidenceAt: time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC)} |
| - put := func(s release.Store, r StopReceipt) hostclaim.Release { |
| + enr, other := strings.Repeat("e", 32), strings.Repeat("f", 32) |
| + srcKey := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{2}, ed25519.SeedSize)) |
| + dstKey := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{3}, ed25519.SeedSize)) |
| + boxes := func() map[string]hostactors.Box { |
| + return map[string]hostactors.Box{"src": rosterBox("src", enr, srcKey), "dst": rosterBox("dst", other, dstKey)} |
| + } |
| + c := hostclaim.Claim{Host: "shop.example", Gen: 9, Holder: &hostclaim.Holder{Incarnation: inc, Stamp: 3, Session: 12}, Op: hostclaim.Op{ID: "rel-1", Kind: hostclaim.KindRelocating, Phase: hostclaim.PhaseReleasing}} |
| + receipt := StopReceipt{RequestLimitNS: int64(hostbox.RequestLimit), V: 1, ID: "r1", Host: "shop.example", Incarnation: inc, InstallationStamp: 3, WriterSession: 12, OperationID: "rel-1", |
| + Phase: hostclaim.PhaseReleasing, ObligationID: RelocationReleaseObligation, Box: "src", Enrollment: enr, BootID: "boot", ControllerID: "ctl", ClaimGen: 8, DirectoryRevision: "4", |
| + Kind: "stopped", EvidenceAt: time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC), FenceID: "fence", InventorySHA256: strings.Repeat("1", 64), |
| + LocalJournalSHA256: strings.Repeat("2", 64), ProducerDrainDigest: strings.Repeat("3", 64), CgroupIdentities: []string{"cg-1"}} |
| + // put publishes r as root does (hostevidence.Publish: the body and its |
| + // signature); a nil signer writes the body alone. |
| + put := func(s release.Store, r StopReceipt, signer ed25519.PrivateKey) hostclaim.Release { |
| body := mustMarshalStopReceipt(r) |
| - key := "control/hosts/shop.example/evidence/" + r.OperationID + "/" + r.Box + "/" + r.ID + ".json" |
| - if _, err := s.Put(ctx, key, body, ""); err != nil { |
| + key := stopEvidenceKey(r.Host, r.OperationID, r.Box, r.ID) |
| + if signer != nil { |
| + if err := hostevidence.Publish(ctx, s, signer, key, body); err != nil { |
| + t.Fatal(err) |
| + } |
| + } else if _, err := s.Put(ctx, key, body, ""); err != nil { |
| t.Fatal(err) |
| } |
| sum := sha256.Sum256(body) |
| - return hostclaim.Release{ReleasedBy: "src", Enrollment: "enr", Session: 12, Receipt: key, ReceiptSHA256: hex.EncodeToString(sum[:])} |
| + return hostclaim.Release{ReleasedBy: "src", Enrollment: enr, Session: 12, Receipt: key, ReceiptSHA256: hex.EncodeToString(sum[:])} |
| } |
| s := release.NewDirStore(t.TempDir()) |
| - a := StopReceiptReleases{Evidence: s} |
| - if err := a.VerifyRelease(ctx, c, put(s, receipt)); err != nil { |
| + a := StopReceiptReleases{Evidence: s, Directory: releaseRoster(t, s, boxes())} |
| + if err := a.VerifyRelease(ctx, c, put(s, receipt, srcKey)); err != nil { |
| t.Fatal(err) |
| } |
| for _, tc := range []struct { |
| @@ -44,19 +85,78 @@ func TestStopReceiptReleasesBindsTheReceiptToTheRelease(t *testing.T) { |
| {"another phase", func(r *StopReceipt, _ *hostclaim.Release) { r.Phase = hostclaim.PhaseProbeFailed }}, |
| {"another incarnation", func(r *StopReceipt, _ *hostclaim.Release) { r.Incarnation = strings.Repeat("b", 64) }}, |
| {"a digest that is not the bytes", func(_ *StopReceipt, rel *hostclaim.Release) { rel.ReceiptSHA256 = strings.Repeat("0", 64) }}, |
| + // The fields main's console-side receipt check binds (fix round 1). |
| + {"another installation stamp", func(r *StopReceipt, _ *hostclaim.Release) { r.InstallationStamp = 2 }}, |
| + {"no claim generation", func(r *StopReceipt, _ *hostclaim.Release) { r.ClaimGen = 0 }}, |
| + {"a claim generation past the claim's", func(r *StopReceipt, _ *hostclaim.Release) { r.ClaimGen = 10 }}, |
| + {"no directory revision", func(r *StopReceipt, _ *hostclaim.Release) { r.DirectoryRevision = "0" }}, |
| + {"a directory revision past the roster's", func(r *StopReceipt, _ *hostclaim.Release) { r.DirectoryRevision = "6" }}, |
| + {"a directory revision that is not a number", func(r *StopReceipt, _ *hostclaim.Release) { r.DirectoryRevision = "four" }}, |
| + {"no stopped cgroups", func(r *StopReceipt, _ *hostclaim.Release) { r.CgroupIdentities = nil }}, |
| + {"another request limit", func(r *StopReceipt, _ *hostclaim.Release) { r.RequestLimitNS = 1 }}, |
| + {"a malformed inventory digest", func(r *StopReceipt, _ *hostclaim.Release) { r.InventorySHA256 = "inv" }}, |
| + {"a malformed local journal digest", func(r *StopReceipt, _ *hostclaim.Release) { r.LocalJournalSHA256 = "" }}, |
| + {"a malformed producer drain digest", func(r *StopReceipt, _ *hostclaim.Release) { r.ProducerDrainDigest = strings.Repeat("z", 64) }}, |
| + {"no boot", func(r *StopReceipt, _ *hostclaim.Release) { r.BootID = "" }}, |
| + {"no controller", func(r *StopReceipt, _ *hostclaim.Release) { r.ControllerID = "" }}, |
| + {"no fence", func(r *StopReceipt, _ *hostclaim.Release) { r.FenceID = "" }}, |
| } { |
| t.Run(tc.name, func(t *testing.T) { |
| s := release.NewDirStore(t.TempDir()) |
| r := receipt |
| + r.CgroupIdentities = append([]string(nil), receipt.CgroupIdentities...) |
| var mutated hostclaim.Release |
| tc.mutate(&r, &mutated) |
| - rel := put(s, r) |
| + rel := put(s, r, srcKey) |
| if mutated.ReceiptSHA256 != "" { |
| rel.ReceiptSHA256 = mutated.ReceiptSHA256 |
| } |
| - if err := (StopReceiptReleases{Evidence: s}).VerifyRelease(ctx, c, rel); !errors.Is(err, ErrClaimStopReceipt) { |
| + if err := (StopReceiptReleases{Evidence: s, Directory: releaseRoster(t, s, boxes())}).VerifyRelease(ctx, c, rel); !errors.Is(err, ErrClaimStopReceipt) { |
| t.Fatalf("accepted: %v", err) |
| } |
| }) |
| } |
| + // Root's signature (fix round 1, I-1): the receipt counts only under |
| + // the releasing box's census key, from the roster. |
| + t.Run("an unsigned receipt", func(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + rel := put(s, receipt, nil) |
| + if err := (StopReceiptReleases{Evidence: s, Directory: releaseRoster(t, s, boxes())}).VerifyRelease(ctx, c, rel); !errors.Is(err, ErrClaimStopReceipt) { |
| + t.Fatalf("an unsigned receipt counted: %v", err) |
| + } |
| + }) |
| + t.Run("signed by another box's key", func(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + rel := put(s, receipt, dstKey) |
| + if err := (StopReceiptReleases{Evidence: s, Directory: releaseRoster(t, s, boxes())}).VerifyRelease(ctx, c, rel); !errors.Is(err, ErrClaimStopReceipt) { |
| + t.Fatalf("another box's signature counted: %v", err) |
| + } |
| + }) |
| + t.Run("a roster entry of another enrollment", func(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + rel := put(s, receipt, srcKey) |
| + b := boxes() |
| + src := b["src"] |
| + src.Enrollment = strings.Repeat("d", 32) |
| + b["src"] = src |
| + if err := (StopReceiptReleases{Evidence: s, Directory: releaseRoster(t, s, b)}).VerifyRelease(ctx, c, rel); !errors.Is(err, ErrClaimStopIdentity) { |
| + t.Fatalf("another enrollment's key counted: %v", err) |
| + } |
| + }) |
| + t.Run("a box the roster does not name", func(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + rel := put(s, receipt, srcKey) |
| + b := boxes() |
| + delete(b, "src") |
| + if err := (StopReceiptReleases{Evidence: s, Directory: releaseRoster(t, s, b)}).VerifyRelease(ctx, c, rel); !errors.Is(err, ErrClaimStopIdentity) { |
| + t.Fatalf("a box missing from the roster counted: %v", err) |
| + } |
| + }) |
| + t.Run("no roster", func(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + rel := put(s, receipt, srcKey) |
| + if err := (StopReceiptReleases{Evidence: s, Directory: hostactors.Directory{Store: s, Bucket: "bucket"}}).VerifyRelease(ctx, c, rel); err == nil { |
| + t.Fatal("a release counted without a roster to name its key") |
| + } |
| + }) |
| } |