carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build unix
2
3package hostguard
4
5import (
6 "bytes"
7 "context"
8 "encoding/json"
9 "os"
10 "path/filepath"
11 "testing"
12 "time"
13
14 "github.com/carlosframework/platform/internal/hostclaim"
15)
16
17func TestPlan14PublicRootTamperMatrix(t *testing.T) {
18 operations := []string{"Capture", "With", "BeginFence", "CompleteFence", "ExistingFence", "ResumeFence", "FenceReceiptFor", "PendingFences", "CompletedColdPredecessor", "CompletedWriterStops", "CleanupWith", "CleanupRetireInstallationWith", "RetireInstallation", "RetireInstallationWith", "WithNoInstallation", "BeginColdHandover", "CompleteColdHandover"}
19 for _, attack := range []string{"delete", "rename", "locked replacement", "forged receipt"} {
20 for _, operation := range operations {
21 t.Run(attack+"/"+operation, func(t *testing.T) {
22 f, first, captured := fenceFixture(t)
23 store := &rootTestStore{dir: t.TempDir()}
24 f.g.Root = NewRootAuthority(store)
25 f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "app"
26 in := RootInstallation{Host: first.Host, Incarnation: captured.Incarnation, Stamp: captured.Stamp, Migration: 1, InventoryRevision: 1, Account: "bab", App: "app", Record: "record", Box: first.Box, Enrollment: first.Enrollment}
27 if err := f.g.EstablishRoot(t.Context(), in, allow); err != nil {
28 t.Fatal(err)
29 }
30 first.ObligationID = WriterStopObligation(first.Session)
31 first.Region = "uk"
32 f.obs.Claim.Holder.WriterBox = first.Box
33 f.obs.Claim.Holder.Generation = "gen"
34 f.obs.Claim.Holder.Admissions = []hostclaim.AdmissionRecord{{Session: first.Session, Box: first.Box, Region: first.Region, Generation: "gen", Mode: "cold"}}
35 if _, err := f.g.BeginFence(t.Context(), first); err != nil {
36 t.Fatal(err)
37 }
38 at := time.Now().UTC()
39 receipt := []byte("root receipt")
40 if err := f.g.CompleteFence(t.Context(), first, receipt, at, func(context.Context, FenceTarget, []byte, time.Time) error { return nil }); err != nil {
41 t.Fatal(err)
42 }
43 target := first
44 target.Session++
45 target.FenceID = "pending"
46 target.ObligationID = WriterStopObligation(target.Session)
47 f.obs.Claim.Holder.Session = target.Session
48 f.obs.Claim.Holder.Admissions = append(f.obs.Claim.Holder.Admissions, hostclaim.AdmissionRecord{Session: target.Session, Box: target.Box, Region: target.Region, Generation: "gen", Mode: "cold"})
49 if _, err := f.g.BeginFence(t.Context(), target); err != nil {
50 t.Fatal(err)
51 }
52 before, _ := store.Read(target.Host + ".json")
53 path := filepath.Join(f.g.DataDir, "hostguard", target.Host+".json")
54 switch attack {
55 case "delete":
56 if err := os.Remove(path); err != nil {
57 t.Fatal(err)
58 }
59 case "rename":
60 if err := os.Rename(filepath.Dir(path), filepath.Dir(path)+".old"); err != nil {
61 t.Fatal(err)
62 }
63 case "locked replacement":
64 unlock, err := acquire(t.Context(), f.g.LockDir, target.Host)
65 if err != nil {
66 t.Fatal(err)
67 }
68 defer unlock()
69 if err = os.Rename(f.g.LockDir, f.g.LockDir+".old"); err != nil {
70 t.Fatal(err)
71 }
72 if err = os.Mkdir(f.g.LockDir, 0700); err != nil {
73 t.Fatal(err)
74 }
75 case "forged receipt":
76 forged := record{V: 1, Host: target.Host, Incarnation: captured.Incarnation, ActiveStamp: 99, FenceJournal: []FenceReceipt{{Target: target, Bytes: []byte("forged"), EvidenceAt: at}}}
77 raw, _ := json.Marshal(forged)
78 if err := os.WriteFile(path, raw, 0660); err != nil {
79 t.Fatal(err)
80 }
81 }
82 effects := 0
83 effect := func() error { effects++; return nil }
84 verify := func(_ context.Context, r FenceReceipt) error {
85 if r.Target != first || !bytes.Equal(r.Bytes, receipt) || !r.EvidenceAt.Equal(at) {
86 return ErrReceiptChanged
87 }
88 return nil
89 }
90 bad := target
91 bad.FenceID = "forged"
92 stale := Captured{Incarnation: captured.Incarnation, Stamp: 99}
93 ctx, cancel := context.WithTimeout(t.Context(), time.Second)
94 defer cancel()
95 var err error
96 success := false
97 switch operation {
98 case "Capture":
99 var got Captured
100 got, err = f.g.Capture(target.Host)
101 if got != captured {
102 t.Fatal(got)
103 }
104 success = true
105 case "With":
106 err = f.g.With(ctx, target.Host, captured, allow, effect)
107 case "BeginFence":
108 var got FenceTarget
109 got, err = f.g.BeginFence(ctx, target)
110 if got != target {
111 t.Fatal(got)
112 }
113 success = true
114 case "CompleteFence":
115 err = f.g.CompleteFence(ctx, bad, []byte("forged"), at, func(context.Context, FenceTarget, []byte, time.Time) error { return nil })
116 case "ExistingFence":
117 var got FenceTarget
118 got, err = f.g.ExistingFence(ctx, target)
119 if got != target {
120 t.Fatal(got)
121 }
122 success = true
123 case "ResumeFence":
124 err = f.g.ResumeFence(ctx, target)
125 success = true
126 case "FenceReceiptFor":
127 var got FenceReceipt
128 got, err = f.g.FenceReceiptFor(ctx, first)
129 if verify(ctx, got) != nil {
130 t.Fatal(got)
131 }
132 success = true
133 case "PendingFences":
134 var got []FenceTarget
135 got, err = f.g.PendingFences(ctx)
136 if len(got) != 1 || got[0] != target {
137 t.Fatal(got)
138 }
139 success = true
140 case "CompletedColdPredecessor":
141 var got FenceReceipt
142 got, err = f.g.CompletedColdPredecessor(ctx, first.Host, captured, first.Session, first.Op, first.Phase)
143 if verify(ctx, got) != nil {
144 t.Fatal(got)
145 }
146 success = true
147 case "CompletedWriterStops":
148 var got []FenceReceipt
149 got, err = f.g.CompletedWriterStops(ctx, first.Host, captured, first.Op, first.Box)
150 if len(got) != 1 || verify(ctx, got[0]) != nil {
151 t.Fatal(got)
152 }
153 success = true
154 case "CleanupWith":
155 err = f.g.CleanupWith(ctx, bad, verify, effect)
156 case "CleanupRetireInstallationWith":
157 err = f.g.CleanupRetireInstallationWith(ctx, bad, verify, effect)
158 case "RetireInstallation":
159 err = f.g.RetireInstallation(ctx, target.Host, func(*hostclaim.Claim) error { return ErrFenced })
160 case "RetireInstallationWith":
161 err = f.g.RetireInstallationWith(ctx, target.Host, stale, allow, effect)
162 case "WithNoInstallation":
163 err = f.g.WithNoInstallation(ctx, target.Host, captured.Incarnation, allow, effect)
164 case "BeginColdHandover":
165 bad.Stamp = 99
166 bad.Session = 99
167 f.obs.Claim.Holder.Stamp = 99
168 f.obs.Claim.Holder.Session = 99
169 err = f.g.BeginColdHandover(ctx, target, bad, allow)
170 case "CompleteColdHandover":
171 err = f.g.CompleteColdHandover(ctx, bad, false, allow, effect)
172 }
173 if success && err != nil || !success && err == nil || effects != 0 {
174 t.Fatal(operation, err, effects)
175 }
176 after, _ := store.Read(target.Host + ".json")
177 if !bytes.Equal(before, after) {
178 t.Fatal("root bytes changed")
179 }
180 pending, err := f.g.PendingFences(ctx)
181 if err != nil || len(pending) != 1 || pending[0] != target {
182 t.Fatal("lost recovery", pending, err)
183 }
184 })
185 }
186 }
187}
188
189func TestPlan14RootUnavailableNeverUsesAgentRecord(t *testing.T) {
190 for _, damage := range []string{"missing", "corrupt"} {
191 t.Run(damage, func(t *testing.T) {
192 f, target, captured := fenceFixture(t)
193 disk := &rootTestStore{dir: t.TempDir()}
194 f.g.Root = NewRootAuthority(disk)
195 if damage == "corrupt" {
196 if err := disk.Write(target.Host+".json", []byte("{")); err != nil {
197 t.Fatal(err)
198 }
199 }
200 effects := 0
201 effect := func() error { effects++; return nil }
202 verify := func(context.Context, FenceReceipt) error { return nil }
203 calls := map[string]func() error{
204 "Capture": func() error { _, err := f.g.Capture(target.Host); return err },
205 "With": func() error { return f.g.With(t.Context(), target.Host, captured, allow, effect) },
206 "BeginFence": func() error { _, err := f.g.BeginFence(t.Context(), target); return err },
207 "CompleteFence": func() error {
208 return f.g.CompleteFence(t.Context(), target, []byte("receipt"), time.Now(), func(context.Context, FenceTarget, []byte, time.Time) error { return nil })
209 },
210 "ExistingFence": func() error { _, err := f.g.ExistingFence(t.Context(), target); return err },
211 "ResumeFence": func() error { return f.g.ResumeFence(t.Context(), target) },
212 "FenceReceiptFor": func() error { _, err := f.g.FenceReceiptFor(t.Context(), target); return err },
213 "CompletedColdPredecessor": func() error {
214 _, err := f.g.CompletedColdPredecessor(t.Context(), target.Host, captured, target.Session, target.Op, target.Phase)
215 return err
216 },
217 "CompletedWriterStops": func() error {
218 _, err := f.g.CompletedWriterStops(t.Context(), target.Host, captured, target.Op, target.Box)
219 return err
220 },
221 "CleanupWith": func() error { return f.g.CleanupWith(t.Context(), target, verify, effect) },
222 "CleanupRetireInstallationWith": func() error { return f.g.CleanupRetireInstallationWith(t.Context(), target, verify, effect) },
223 "RetireInstallation": func() error { return f.g.RetireInstallation(t.Context(), target.Host, allow) },
224 "RetireInstallationWith": func() error { return f.g.RetireInstallationWith(t.Context(), target.Host, captured, allow, effect) },
225 "WithNoInstallation": func() error {
226 return f.g.WithNoInstallation(t.Context(), target.Host, captured.Incarnation, allow, effect)
227 },
228 "CompleteColdHandover": func() error { return f.g.CompleteColdHandover(t.Context(), target, false, allow, effect) },
229 }
230 for name, call := range calls {
231 if err := call(); err == nil || effects != 0 {
232 t.Fatal(name, err, effects)
233 }
234 }
235 if damage == "corrupt" {
236 if _, err := f.g.PendingFences(t.Context()); err == nil {
237 t.Fatal("corrupt recovery accepted")
238 }
239 }
240 })
241 }
242}
243