carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package hostclaim
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "errors"
9 "strings"
10 "testing"
11
12 "github.com/carlosframework/platform/internal/boxid"
13 "github.com/carlosframework/platform/internal/release"
14 "github.com/carlosframework/platform/internal/termfence"
15)
16
17// storeFence writes a terminated-box fence for box to the store and returns
18// the journal entry that names it and an authority that binds it.
19func storeFence(t *testing.T, s release.Store, box, enrollment string) (string, c7Authority) {
20 t.Helper()
21 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)}}
22 raw, err := json.Marshal(f)
23 if err != nil {
24 t.Fatal(err)
25 }
26 digest := sha256.Sum256(raw)
27 ref := FenceRef{Enrollment: enrollment, Key: "control/box-fences/" + enrollment + ".json", SHA256: hex.EncodeToString(digest[:])}
28 if _, err := s.Put(context.Background(), ref.Key, raw, ""); err != nil {
29 t.Fatal(err)
30 }
31 entry, err := json.Marshal(ref)
32 if err != nil {
33 t.Fatal(err)
34 }
35 return string(entry), c7Authority{want: f}
36}
37
38// Review fix round 1 (Minor): the public fence path end to end, through a
39// stored claim and a stored fence. VerifyReturnFence re-reads the fence the
40// claim journals for the target and the authority binds it; ReturnToSource
41// then claims for the source. A token verified for one journal reference does
42// not authorise a claim whose reference has since changed, and a reference
43// whose digest is not the stored fence's is never verified.
44func TestVerifyReturnFenceThenReturnToSource(t *testing.T) {
45 ctx := context.Background()
46 s := release.NewDirStore(t.TempDir())
47 entry, authority := storeFence(t, s, "uk-box", strings.Repeat("c", 32))
48 otherEntry, _ := storeFence(t, s, "uk-box", strings.Repeat("d", 32))
49
50 fixture := rolledBack(t)
51 fixture.Op.Phase, fixture.Op.Exec = PhaseReleased, "none"
52 fixture.Op.Journal["fence.uk-box"] = entry
53 obs, err := Update(ctx, s, Observed{Host: fixture.Host, Absent: true}, func(c *Claim) error {
54 *c = *fixture
55 c.Gen = 0
56 return nil
57 })
58 if err != nil {
59 t.Fatal(err)
60 }
61
62 // The successful path.
63 fence, err := VerifyReturnFence(ctx, s, authority, obs.Claim.Op, "uk-box")
64 if err != nil {
65 t.Fatalf("verify: %v", err)
66 }
67 claimed, err := Update(ctx, s, obs, ReturnToSource(moveA, "ie-box", true, fence))
68 if err != nil {
69 t.Fatalf("return: %v", err)
70 }
71 got, err := Read(ctx, s, fixture.Host)
72 if err != nil {
73 t.Fatal(err)
74 }
75 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" {
76 t.Fatalf("stored claim after the return: op %+v holder %+v", got.Claim.Op, got.Claim.Holder)
77 }
78
79 // The target box's fence is not the source's: verifying it for another box fails.
80 if _, err := VerifyReturnFence(ctx, s, authority, obs.Claim.Op, "ie-box"); !errors.Is(err, ErrFence) {
81 t.Fatalf("verify for the source: %v", err)
82 }
83
84 // A changed journal reference: the token verified against the old entry
85 // does not authorise the claim that now names another fence.
86 changed, err := Update(ctx, s, got, func(c *Claim) error {
87 c.Op.Phase, c.Op.Exec = PhaseReleased, "none"
88 c.Op.Journal["fence.uk-box"] = otherEntry
89 return nil
90 })
91 if err != nil {
92 t.Fatal(err)
93 }
94 if _, err := Update(ctx, s, changed, ReturnToSource(moveA, "ie-box", true, fence)); !errors.Is(err, ErrWrongState) {
95 t.Fatalf("a token for the old reference: %v", err)
96 }
97 if after, err := Read(ctx, s, fixture.Host); err != nil || after.ETag != changed.ETag {
98 t.Fatalf("a refused return wrote the claim: %v", err)
99 }
100
101 // A reference whose digest is not the stored fence's is never verified.
102 var ref FenceRef
103 if err := json.Unmarshal([]byte(entry), &ref); err != nil {
104 t.Fatal(err)
105 }
106 ref.SHA256 = strings.Repeat("0", 64)
107 forged, err := json.Marshal(ref)
108 if err != nil {
109 t.Fatal(err)
110 }
111 op := changed.Claim.Op
112 op.Journal = map[string]string{"fence.uk-box": string(forged)}
113 if _, err := VerifyReturnFence(ctx, s, authority, op, "uk-box"); !errors.Is(err, ErrFence) {
114 t.Fatalf("a changed digest: %v", err)
115 }
116 // An authority that does not bind the record refuses too.
117 if _, err := VerifyReturnFence(ctx, s, c7Authority{}, changed.Claim.Op, "uk-box"); !errors.Is(err, ErrFence) {
118 t.Fatalf("an unbound authority: %v", err)
119 }
120}
121