carlos / platform Public

hostclaim: the public fence path, VerifyReturnFence then ReturnToSource, end to end (Task 2 fix round 1)

A stored claim and a stored terminated-box fence with a binding authority:
the verified token claims for the source; a token for the old journal
reference is refused once the claim names another fence; a changed digest,
the wrong box and an unbound authority are never verified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 0522c11bbe8a1faf74838f17aec203a3bea78d89 parent 3d6a233
1 file changed, +120 −0
  • internal/hostclaim/move_return_fence_test.go +120 −0
diff --git a/internal/hostclaim/move_return_fence_test.go b/internal/hostclaim/move_return_fence_test.go
new file mode 100644
index 000000000..0be7320a0
--- /dev/null
+++ b/internal/hostclaim/move_return_fence_test.go
@@ -0,0 +1,120 @@
+package hostclaim
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/boxid"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/termfence"
+)
+
+// storeFence writes a terminated-box fence for box to the store and returns
+// the journal entry that names it and an authority that binds it.
+func storeFence(t *testing.T, s release.Store, box, enrollment string) (string, c7Authority) {
+ t.Helper()
+ f := termfence.Record{V: 1, Box: box, Enrollment: enrollment, Cloud: boxid.Cloud{Provider: "aws", Account: "123", Region: "eu-west-2", InstanceID: "i-" + box}, State: "terminated", ObservedAt: "2026-10-06T00:00:00Z", Via: "describe", RequestID: "request", WrittenBy: termfence.WrittenBy{Box: "worker", Enrollment: strings.Repeat("b", 32)}}
+ raw, err := json.Marshal(f)
+ if err != nil {
+ t.Fatal(err)
+ }
+ digest := sha256.Sum256(raw)
+ ref := FenceRef{Enrollment: enrollment, Key: "control/box-fences/" + enrollment + ".json", SHA256: hex.EncodeToString(digest[:])}
+ if _, err := s.Put(context.Background(), ref.Key, raw, ""); err != nil {
+ t.Fatal(err)
+ }
+ entry, err := json.Marshal(ref)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return string(entry), c7Authority{want: f}
+}
+
+// Review fix round 1 (Minor): the public fence path end to end, through a
+// stored claim and a stored fence. VerifyReturnFence re-reads the fence the
+// claim journals for the target and the authority binds it; ReturnToSource
+// then claims for the source. A token verified for one journal reference does
+// not authorise a claim whose reference has since changed, and a reference
+// whose digest is not the stored fence's is never verified.
+func TestVerifyReturnFenceThenReturnToSource(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ entry, authority := storeFence(t, s, "uk-box", strings.Repeat("c", 32))
+ otherEntry, _ := storeFence(t, s, "uk-box", strings.Repeat("d", 32))
+
+ fixture := rolledBack(t)
+ fixture.Op.Phase, fixture.Op.Exec = PhaseReleased, "none"
+ fixture.Op.Journal["fence.uk-box"] = entry
+ obs, err := Update(ctx, s, Observed{Host: fixture.Host, Absent: true}, func(c *Claim) error {
+ *c = *fixture
+ c.Gen = 0
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // The successful path.
+ fence, err := VerifyReturnFence(ctx, s, authority, obs.Claim.Op, "uk-box")
+ if err != nil {
+ t.Fatalf("verify: %v", err)
+ }
+ claimed, err := Update(ctx, s, obs, ReturnToSource(moveA, "ie-box", true, fence))
+ if err != nil {
+ t.Fatalf("return: %v", err)
+ }
+ got, err := Read(ctx, s, fixture.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got.ETag != claimed.ETag || got.Claim.Op.Exec != "ie-box" || got.Claim.Op.Phase != PhaseClaimed || got.Claim.Holder.Stamp != 3 || got.Claim.Holder.Generation != "gen-7" {
+ t.Fatalf("stored claim after the return: op %+v holder %+v", got.Claim.Op, got.Claim.Holder)
+ }
+
+ // The target box's fence is not the source's: verifying it for another box fails.
+ if _, err := VerifyReturnFence(ctx, s, authority, obs.Claim.Op, "ie-box"); !errors.Is(err, ErrFence) {
+ t.Fatalf("verify for the source: %v", err)
+ }
+
+ // A changed journal reference: the token verified against the old entry
+ // does not authorise the claim that now names another fence.
+ changed, err := Update(ctx, s, got, func(c *Claim) error {
+ c.Op.Phase, c.Op.Exec = PhaseReleased, "none"
+ c.Op.Journal["fence.uk-box"] = otherEntry
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := Update(ctx, s, changed, ReturnToSource(moveA, "ie-box", true, fence)); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("a token for the old reference: %v", err)
+ }
+ if after, err := Read(ctx, s, fixture.Host); err != nil || after.ETag != changed.ETag {
+ t.Fatalf("a refused return wrote the claim: %v", err)
+ }
+
+ // A reference whose digest is not the stored fence's is never verified.
+ var ref FenceRef
+ if err := json.Unmarshal([]byte(entry), &ref); err != nil {
+ t.Fatal(err)
+ }
+ ref.SHA256 = strings.Repeat("0", 64)
+ forged, err := json.Marshal(ref)
+ if err != nil {
+ t.Fatal(err)
+ }
+ op := changed.Claim.Op
+ op.Journal = map[string]string{"fence.uk-box": string(forged)}
+ if _, err := VerifyReturnFence(ctx, s, authority, op, "uk-box"); !errors.Is(err, ErrFence) {
+ t.Fatalf("a changed digest: %v", err)
+ }
+ // An authority that does not bind the record refuses too.
+ if _, err := VerifyReturnFence(ctx, s, c7Authority{}, changed.Claim.Op, "uk-box"); !errors.Is(err, ErrFence) {
+ t.Fatalf("an unbound authority: %v", err)
+ }
+}