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 linux
2
3package host
4
5import (
6 "context"
7 "encoding/json"
8 "errors"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/hostbox"
16 "github.com/carlosframework/platform/internal/hostguard"
17 "github.com/carlosframework/platform/internal/provision"
18 "github.com/carlosframework/platform/internal/release"
19 "github.com/carlosframework/platform/internal/rootparticipation"
20 "github.com/carlosframework/platform/internal/rootstate"
21 "golang.org/x/sys/unix"
22)
23
24func TestPlan14GenerationRequiredArtifacts(t *testing.T) {
25 plan14GenerationRequiredArtifacts(t, false)
26}
27
28func TestPlan14GenerationLocalRecoveryObligations(t *testing.T) {
29 plan14GenerationRequiredArtifacts(t, true)
30}
31
32func plan14GenerationRequiredArtifacts(t *testing.T, local bool) {
33 for _, attack := range []string{"none", "service", "record-next-pass", "route-omitted", "state-omitted", "historical-state-omitted", "requirements-missing", "requirements-schema", "requirements-malformed", "state-child-only", "state-wrong-host", "state-wrong-root", "state-file", "duplicate-route", "duplicate-state", "false-empty", "predecessor-omitted", "record-schema", "record-kind", "record-missing", "record-malformed", "record-account", "record-app", "record-host", "record-incarnation", "changed-record-during-kernel"} {
34 if local && (strings.HasPrefix(attack, "record-") || attack == "changed-record-during-kernel") {
35 continue
36 }
37 t.Run(attack, func(t *testing.T) {
38 f, disk, rec := generationLaunchFixture(t)
39 if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil {
40 t.Fatal(err)
41 }
42 r := readGenerationLaunch(t, disk)
43 views, err := f.root.Guard.Root.GenerationInstallations(t.Context())
44 if err != nil {
45 t.Fatal(err)
46 }
47 launch, err := f.journal.Read(t.Context(), f.target)
48 if err != nil {
49 t.Fatal(err)
50 }
51 t.Setenv("CARLOS_DATA_DIR", t.TempDir())
52 path := provision.InstanceDir(dataDir(), rec.Host)
53 if err := os.MkdirAll(path, 0700); err != nil {
54 t.Fatal(err)
55 }
56 var st unix.Stat_t
57 if err := unix.Stat(path, &st); err != nil {
58 t.Fatal(err)
59 }
60 rel, err := filepath.Rel(dataDir(), path)
61 if err != nil {
62 t.Fatal(err)
63 }
64 snapshot := generationInventory{Census: rootparticipation.Census{Schema: 1, Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}, Installations: views, Launches: []hostbox.LaunchInventory{launch}, Payloads: generationPayloadFixture(views)}
65 entry := hostbox.StopInventoryEntry{Identity: inventoryIdentityFixture(t, dataDir(), rel), Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: rel, Ownership: "owner", Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: unix.S_IFDIR}
66 snapshot.Payloads[0].Inventory.Entries = append(snapshot.Payloads[0].Inventory.Entries, entry)
67 store := f.root.generations.store
68 rec.Schema = 1
69 plan14PutRecord(t, store, rec)
70 key := provision.RecordKey(rec.Account, rec.App, rec.Host)
71 original, _, err := store.Get(t.Context(), key)
72 if err != nil {
73 t.Fatal(err)
74 }
75 put := func(raw []byte) {
76 t.Helper()
77 _, etag, err := store.Get(t.Context(), key)
78 if err != nil && !errors.Is(err, os.ErrNotExist) {
79 t.Fatal(err)
80 }
81 if _, err := store.Put(t.Context(), key, raw, etag); err != nil {
82 t.Fatal(err)
83 }
84 }
85 changeRecord := func() {
86 t.Helper()
87 var changed provision.Record
88 if err := json.Unmarshal(original, &changed); err != nil {
89 t.Fatal(err)
90 }
91 switch attack {
92 case "record-schema":
93 changed.Schema = 2
94 case "record-kind":
95 changed.Kind = "unknown"
96 case "record-account":
97 changed.Account = "bap"
98 case "record-app":
99 changed.App = "other"
100 case "record-host":
101 changed.Host = "other.example"
102 case "record-incarnation":
103 changed.CreatedAt = changed.CreatedAt.Add(time.Hour)
104 default:
105 changed.RecordSeq++
106 }
107 raw, err := json.Marshal(changed)
108 if err != nil {
109 t.Fatal(err)
110 }
111 put(raw)
112 }
113 attackActive := false
114 v := generationReconciler{read: func(ctx context.Context) (generationInventory, error) {
115 if local {
116 return generationLocalObligations(ctx, snapshot, nil)
117 }
118 return generationRequiredInventory(ctx, store, snapshot, nil)
119 }, kernel: func(context.Context) (string, string, error) {
120 if attackActive && attack == "changed-record-during-kernel" {
121 changeRecord()
122 }
123 return "boot", r.Binding.Digest, nil
124 }}
125 source := generationPinSource{open: func() (rootstate.Store, error) { return disk, nil }, reconcile: v.reconcile}
126 ledger := &release.RootGenerations{Store: disk}
127 accept := func() {
128 t.Helper()
129 pins, err := source.StagedPins(t.Context(), f.root.generations.optDir)
130 if err != nil {
131 t.Fatal(err)
132 }
133 if pinned, err := pins.Refers(r.Binding.Path); err != nil || !pinned {
134 t.Fatalf("reservation not pinned: %v", err)
135 }
136 if c, err := ledger.Completeness(); err != nil || !c.Complete {
137 t.Fatalf("valid required artifacts refused: %+v %v", c, err)
138 }
139 }
140 if attack == "service" {
141 snapshot.Installations[0].Installation.Artifacts.StateDirectory = false
142 rec.Kind, rec.Addr = "service", "127.0.0.1:8080"
143 plan14PutRecord(t, store, rec)
144 snapshot.Payloads[0].Inventory.Entries = snapshot.Payloads[0].Inventory.Entries[:1]
145 }
146 accept()
147 if attack == "none" || attack == "service" {
148 return
149 }
150 initial, _ := ledger.Completeness()
151 if attack == "record-next-pass" {
152 changeRecord()
153 accept()
154 next, err := ledger.Completeness()
155 if err != nil || next.Evidence.Installations == initial.Evidence.Installations {
156 t.Fatalf("record revision absent from certificate: %+v %v", next, err)
157 }
158 return
159 }
160 saved, err := json.Marshal(snapshot)
161 if err != nil {
162 t.Fatal(err)
163 }
164 entries := &snapshot.Payloads[0].Inventory.Entries
165 switch attack {
166 case "requirements-missing":
167 snapshot.Installations[0].Installation.Artifacts = hostguard.InstallationArtifacts{}
168 case "requirements-schema":
169 snapshot.Installations[0].Installation.Artifacts.Schema++
170 case "requirements-malformed":
171 snapshot.Installations[0].Installation.Artifacts.Timers = "bad/path"
172 case "route-omitted":
173 *entries = (*entries)[1:]
174 case "historical-state-omitted":
175 rec.Kind, rec.Addr = "service", "127.0.0.1:8080"
176 plan14PutRecord(t, store, rec)
177 *entries = (*entries)[:1]
178 case "state-omitted":
179 *entries = (*entries)[:1]
180 case "state-child-only", "state-wrong-host":
181 name := filepath.Join(path, "child")
182 if attack == "state-wrong-host" {
183 name = provision.InstanceDir(dataDir(), "other.example")
184 }
185 if err := os.MkdirAll(name, 0700); err != nil {
186 t.Fatal(err)
187 }
188 if err := unix.Stat(name, &st); err != nil {
189 t.Fatal(err)
190 }
191 (*entries)[1].RelativePath, err = filepath.Rel(dataDir(), name)
192 if err != nil {
193 t.Fatal(err)
194 }
195 (*entries)[1].Device, (*entries)[1].Inode = uint64(st.Dev), st.Ino
196 case "state-wrong-root":
197 (*entries)[1].ManagedRoot = "opt"
198 case "state-file":
199 (*entries)[1].Type = unix.S_IFREG
200 case "duplicate-route":
201 *entries = append(*entries, (*entries)[0])
202 case "duplicate-state":
203 *entries = append(*entries, (*entries)[1])
204 case "false-empty":
205 *entries = nil
206 snapshot.Installations[0].Installation.Empty = true
207 case "predecessor-omitted":
208 snapshot.Installations[0].Retired = append(snapshot.Installations[0].Retired, hostguard.Captured{Incarnation: f.target.Incarnation, Stamp: f.target.Stamp - 1})
209 old := snapshot.Payloads[0]
210 old.Inventory.InstallationStamp--
211 old.Inventory.Entries = append([]hostbox.StopInventoryEntry(nil), (*entries)[:1]...)
212 snapshot.Payloads = append(snapshot.Payloads, old)
213 case "record-missing":
214 if err := store.Delete(t.Context(), key); err != nil {
215 t.Fatal(err)
216 }
217 case "record-malformed":
218 put([]byte("{"))
219 case "record-schema", "record-kind", "record-account", "record-app", "record-host", "record-incarnation":
220 changeRecord()
221 }
222 attackActive = true
223 if pins, err := source.StagedPins(t.Context(), f.root.generations.optDir); err == nil || pins != nil {
224 t.Fatalf("required-artifact omission or record change certified: %v %v", pins, err)
225 }
226 if c, err := ledger.Completeness(); err != nil || c.Complete || c.Revision <= initial.Revision {
227 t.Fatalf("invalid source retained completeness: %+v %v", c, err)
228 }
229 if got, err := ledger.Read(r.Binding); err != nil || got.Binding != r.Binding || got.Closed {
230 t.Fatal("required-artifact refusal altered the reservation")
231 }
232 attackActive = false
233 put(original)
234 if err := json.Unmarshal(saved, &snapshot); err != nil {
235 t.Fatal(err)
236 }
237 accept()
238 })
239 }
240}
241