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 && rootrunner
2
3package host
4
5import (
6 "context"
7 "crypto/ed25519"
8 "crypto/sha256"
9 "encoding/base64"
10 "encoding/hex"
11 "fmt"
12 "os"
13 "os/exec"
14 "path/filepath"
15 "runtime"
16 "strings"
17 "testing"
18 "time"
19
20 "github.com/carlosframework/platform/internal/moves"
21 "github.com/carlosframework/platform/internal/release"
22 "github.com/carlosframework/platform/internal/rootstate"
23 "github.com/carlosframework/platform/internal/seeds"
24)
25
26func TestPlan14RootFloorBootSelectors(t *testing.T) {
27 dir := generationRootRunnerDir(t)
28 self, err := os.Executable()
29 if err != nil {
30 t.Fatal(err)
31 }
32 binary, err := os.ReadFile(self)
33 if err != nil {
34 t.Fatal(err)
35 }
36 runner := filepath.Join(dir, "runner.test")
37 if err := os.WriteFile(runner, binary, 0755); err != nil {
38 t.Fatal(err)
39 }
40 pid1, err := os.ReadFile("/proc/1/comm")
41 if err != nil || strings.TrimSpace(string(pid1)) != "systemd" {
42 t.Fatal("root runner requires systemd", err)
43 }
44 stage := filepath.Join(dir, "stage")
45 if err := bootstrap(stage, nil); err != nil {
46 t.Fatal(err)
47 }
48 for _, unit := range []string{"carlos-system-update.service", "carlos-system-recover.service"} {
49 for _, attack := range []string{"none", "seeds accepted", "seeds below", "seeds legacy", "below", "stale verdict", "missing floor", "corrupt floor", "old reader", "no guard", "missing accepted", "crash-1", "crash-2", "crash-3", "crash-4", "crash-5", "agent high", "signed high", "seeds unpublished", "unknown crashing current"} {
50 t.Run(unit+"/"+attack, func(t *testing.T) {
51 opt, err := os.MkdirTemp(dir, "box-")
52 if err != nil {
53 t.Fatal(err)
54 }
55 if err := os.Chmod(opt, 0755); err != nil {
56 t.Fatal(err)
57 }
58 pub, key, err := ed25519.GenerateKey(nil)
59 if err != nil {
60 t.Fatal(err)
61 }
62 t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub))
63 id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"}
64 f, err := openRootMovesFloors(opt, id, true)
65 if err != nil {
66 t.Fatal(err)
67 }
68 defer f.close()
69 b := moves.Floor{Min: 1, MinRelease: "v2.0.0"}
70 if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &b, Source: "history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil {
71 t.Fatal(err)
72 }
73 mark := filepath.Join(opt, "executed")
74 makeBinary := func(path, version string, reader bool) {
75 body := "#!/bin/sh\nif [ \"$2\" = floor-allows-prev ]; then\n"
76 if reader {
77 body += " PLAN14_P3_GATE=1 exec " + runner + " -test.run=^TestPlan14RootFloorBootPayload$\n"
78 } else {
79 body += " exit 2\n"
80 }
81 body += "fi\nprintf '" + version + "' > " + mark + "\n"
82 rootFloorSignRunnerBytes(t, opt, path, version, []byte(body), key)
83 }
84 makeBinary(systemBinaryPath(opt), "v2.0.0", true)
85 makeBinary(systemPrevPath(opt), "v2.0.0", true)
86 j := rootFloorJudge{floors: f, current: systemBinaryPath(opt), prev: systemPrevPath(opt), binary: func(path string) (rootFloorBinary, error) { return readRootFloorBinary(opt, path) }, idle: func(context.Context) error { return nil }}
87 if err := j.judge(t.Context()); err != nil {
88 t.Fatal(err)
89 }
90 if strings.HasPrefix(attack, "seeds ") {
91 sf, err := openRootProtocolFloors(rootSeedsFloorDir(opt), id, true)
92 if err != nil {
93 t.Fatal(err)
94 }
95 defer sf.close()
96 floor := moves.Floor{Min: 1, MinRelease: "v2.0.0"}
97 if attack != "seeds accepted" {
98 floor.MinRelease = "v3.0.0"
99 }
100 if attack == "seeds legacy" {
101 dd := filepath.Join(opt, "data")
102 if err := os.MkdirAll(dd, 0755); err != nil {
103 t.Fatal(err)
104 }
105 armLocalSeedsFloor(t, dd, seeds.Floor{Min: floor.Min, MinRelease: floor.MinRelease})
106 if err := refreshRootSeedsFloor(t.Context(), release.NewDirStore(t.TempDir()), opt, dd, id); err != nil {
107 t.Fatal(err)
108 }
109 if err := os.RemoveAll(dd); err != nil {
110 t.Fatal(err)
111 }
112 if _, err := sf.projection(); err == nil {
113 t.Fatal("legacy seeds accepted")
114 }
115 } else if err := sf.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &floor, Source: "root history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, sf)}); err != nil {
116 t.Fatal(err)
117 }
118 }
119 switch attack {
120 case "agent high":
121 dd := filepath.Join(opt, "data")
122 if err := os.MkdirAll(filepath.Join(dd, ".moves"), 0755); err != nil {
123 t.Fatal(err)
124 }
125 if err := os.WriteFile(filepath.Join(dd, ".moves", "floor.min-release"), []byte("v99.0.0\n"), 0644); err != nil {
126 t.Fatal(err)
127 }
128 if err := os.Chown(filepath.Join(dd, ".moves", "floor.min-release"), 65534, 65534); err != nil {
129 t.Fatal(err)
130 }
131 legacy, err := rootFloorLegacy(dd)
132 if err != nil {
133 t.Fatal(err)
134 }
135 if err := f.refresh(t.Context(), &b, legacy, nil); err != nil {
136 t.Fatal(err)
137 }
138 case "signed high":
139 if err := f.refresh(t.Context(), &moves.Floor{Min: 1, MinRelease: "v99.0.0"}, nil, nil); err != nil {
140 t.Fatal(err)
141 }
142 case "seeds unpublished":
143 dd := filepath.Join(opt, "data")
144 if err := os.MkdirAll(dd, 0755); err != nil {
145 t.Fatal(err)
146 }
147 armLocalSeedsFloor(t, dd, seeds.Floor{Min: 1, MinRelease: "v99.0.0"})
148 if err := refreshRootSeedsFloor(t.Context(), release.NewDirStore(t.TempDir()), opt, dd, id); err != nil {
149 t.Fatal(err)
150 }
151 if bound, err := readRootSeedsBound(opt, id); err != nil || bound != nil {
152 t.Fatal("unpublished seeds armed", bound, err)
153 }
154 case "unknown crashing current":
155 for _, name := range []string{rootFloorAccepted, rootFloorDeny} {
156 if err := f.private.Remove(name); err != nil {
157 t.Fatal(err)
158 }
159 }
160 for _, name := range []string{rootFloorProjectionFile, rootFloorSelector} {
161 if err := f.public.Remove(name); err != nil {
162 t.Fatal(err)
163 }
164 }
165 rootFloorSignRunnerBytes(t, opt, systemBinaryPath(opt), "v3.0.0", []byte("#!/bin/sh\nexit 99\n"), key)
166 case "below":
167 makeBinary(systemBinaryPath(opt), "v1.0.0", true)
168 makeBinary(systemPrevPath(opt), "v1.0.0", true)
169 case "stale verdict":
170 if err := f.refresh(t.Context(), &b, nil, nil); err != nil {
171 t.Fatal(err)
172 }
173 makeBinary(systemBinaryPath(opt), "v1.0.0", true)
174 case "missing accepted":
175 if err := f.private.Remove(rootFloorAccepted); err != nil {
176 t.Fatal(err)
177 }
178 case "missing floor":
179 if err := f.public.Remove(rootFloorSelector); err != nil {
180 t.Fatal(err)
181 }
182 case "corrupt floor":
183 if err := f.public.Write(rootFloorSelector, []byte("broken")); err != nil {
184 t.Fatal(err)
185 }
186 case "old reader":
187 makeBinary(systemPrevPath(opt), "v2.0.0", false)
188 if err := j.judge(t.Context()); err != nil {
189 t.Fatal(err)
190 }
191 makeBinary(systemBinaryPath(opt), "v1.0.0", true)
192 }
193 if strings.HasPrefix(attack, "crash-") {
194 fail := int(attack[len(attack)-1] - '0')
195 n := 0
196 broken := f
197 broken.private = &floorFaultStore{Store: f.private, n: &n, fail: fail, after: true}
198 broken.public = &floorFaultStore{Store: f.public, n: &n, fail: fail, after: true}
199 if err := broken.refresh(t.Context(), &moves.Floor{Min: 1, MinRelease: "v3.0.0"}, nil, nil); err == nil {
200 t.Fatal("crash injection succeeded")
201 }
202 }
203
204 guard := filepath.Join(opt, "system", "guard", "carlos")
205 if attack != "no guard" {
206 if err := os.WriteFile(guard, []byte("#!/bin/sh\nexec "+runner+" -test.run=^TestPlan14RootFloorBootPayload$\n"), 0755); err != nil {
207 t.Fatal(err)
208 }
209 }
210 if err := os.MkdirAll(filepath.Join(opt, "system", "router"), 0755); err != nil {
211 t.Fatal(err)
212 }
213 if err := os.WriteFile(filepath.Join(opt, "system", "router", "journal.json"), []byte("{}"), 0600); err != nil {
214 t.Fatal(err)
215 }
216 raw, err := os.ReadFile(filepath.Join(stage, "etc", "systemd", "system", unit))
217 if err != nil {
218 t.Fatal(err)
219 }
220 body := strings.ReplaceAll(string(raw), "/opt/carlos", opt)
221 body = strings.ReplaceAll(body, "/data", filepath.Join(opt, "data"))
222 // Preserve the service's hardening and actual selector. Remove only its
223 // deployment dependencies; all paths and identity are fixture-owned.
224 var lines []string
225 for _, line := range strings.Split(body, "\n") {
226 if strings.HasPrefix(line, "OnFailure=") || strings.HasPrefix(line, "Before=") || strings.HasPrefix(line, "After=") || strings.HasPrefix(line, "Wants=") || strings.HasPrefix(line, "EnvironmentFile=") {
227 continue
228 }
229 lines = append(lines, line)
230 if line == "[Service]" {
231 lines = append(lines, "Environment=PLAN14_P3_OPT="+opt, "Environment=CARLOS_RELEASE_PUBKEY="+base64.StdEncoding.EncodeToString(pub))
232 }
233 }
234 name := "plan14-p3-" + filepath.Base(opt) + ".service"
235 path := filepath.Join("/run/systemd/system", name)
236 if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644); err != nil {
237 t.Fatal(err)
238 }
239 t.Cleanup(func() {
240 exec.Command("systemctl", "stop", name).Run()
241 os.Remove(path)
242 exec.Command("systemctl", "daemon-reload").Run()
243 exec.Command("systemctl", "reset-failed", name).Run()
244 })
245 if out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput(); err != nil {
246 t.Fatal(string(out), err)
247 }
248 out, runErr := exec.CommandContext(t.Context(), "systemctl", "start", name).CombinedOutput()
249 got, readErr := os.ReadFile(mark)
250 want := "v2.0.0"
251 if attack == "below" || attack == "stale verdict" || attack == "old reader" {
252 want = "v1.0.0"
253 }
254 if runErr != nil || readErr != nil || string(got) != want {
255 journal, _ := exec.Command("journalctl", "-u", name, "--no-pager").CombinedOutput()
256 t.Fatalf("executor unavailable: %v %s marker=%s err=%v want=%s\n%s", runErr, out, got, readErr, want, journal)
257 }
258 edge := strings.TrimSuffix(name, ".service") + "-edge.service"
259 edgePath := filepath.Join("/run/systemd/system", edge)
260 edgeMark := filepath.Join(opt, "edge-started")
261 body = "[Unit]\nRequires=" + name + "\nAfter=" + name + "\n[Service]\nType=oneshot\nExecStart=/usr/bin/touch " + edgeMark + "\n"
262 if err := os.WriteFile(edgePath, []byte(body), 0644); err != nil {
263 t.Fatal(err)
264 }
265 t.Cleanup(func() {
266 exec.Command("systemctl", "stop", edge).Run()
267 os.Remove(edgePath)
268 exec.Command("systemctl", "daemon-reload").Run()
269 })
270 if out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput(); err != nil {
271 t.Fatal(err, string(out))
272 }
273 if out, err := exec.Command("systemctl", "start", edge).CombinedOutput(); err != nil {
274 t.Fatal("edge dependency", err, string(out))
275 }
276 if _, err := os.Stat(edgeMark); err != nil {
277 t.Fatal("edge did not start", err)
278 }
279
280 })
281 }
282 }
283}
284
285func TestPlan14RootFloorBootPayload(t *testing.T) {
286 opt := os.Getenv("PLAN14_P3_OPT")
287 if opt == "" {
288 t.Fatal("unit payload only")
289 }
290 if os.Getenv("PLAN14_P3_GATE") == "1" {
291 if err := rootFloorAllowsPrevIdentity(opt, rootFloorIdentity{"deploy", "box", "en"}); err != nil {
292 t.Fatal(err)
293 }
294 return
295 }
296 id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"}
297 f, err := openRootMovesFloors(opt, id, false)
298 if err != nil {
299 t.Fatal(err)
300 }
301 defer f.close()
302 j := rootFloorJudge{floors: f, current: systemBinaryPath(opt), prev: systemPrevPath(opt), binary: func(path string) (rootFloorBinary, error) { return readRootFloorBinary(opt, path) }}
303 // Installation/participation is a fixture here; F4 exercises installed shell
304 // selection, the real signed-binary reader, floor records and prev reader.
305 path, err := rootFloorBootCandidate(t.Context(), j, true, func(ctx context.Context, path string) error {
306 cmd := exec.CommandContext(ctx, path, "system", "floor-allows-prev", "--root", opt)
307 cmd.Env = os.Environ()
308 out, err := cmd.CombinedOutput()
309 if err != nil {
310 return fmt.Errorf("%w: %s", err, out)
311 }
312 return nil
313 }, func(context.Context) error { return nil })
314 if err != nil {
315 t.Fatal(err)
316 }
317 if path != j.current {
318 if err := rootSeedsAllowsBinary(opt, id, path); err != nil {
319 path = j.current
320 }
321 }
322 if out, err := exec.CommandContext(t.Context(), path, "system", "apply").CombinedOutput(); err != nil {
323 t.Fatal(err, string(out))
324 }
325}
326
327func rootFloorSignRunnerBytes(t *testing.T, opt, path, version string, raw []byte, key ed25519.PrivateKey) {
328 t.Helper()
329 if err := os.WriteFile(path, raw, 0755); err != nil {
330 t.Fatal(err)
331 }
332 s, err := rootstate.Open(filepath.Join(opt, "system", "guard", "identities"), true, true)
333 if err != nil {
334 t.Fatal(err)
335 }
336 defer s.Close()
337 sum := sha256.Sum256(raw)
338 platform := runtime.GOOS + "-" + runtime.GOARCH
339 p := release.SystemPointer{Schema: 1, Component: "router", Channel: "stable", Spread: "0s", PromotedAt: time.Now(), Current: release.SystemRelease{Version: version, ReleaseTag: version, MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{platform: {Key: release.SystemArtifactKey(version, platform), SHA256: hex.EncodeToString(sum[:])}}}}
340 wire, err := release.SignRootEnvelope(release.RootSigner(key), p)
341 if err != nil {
342 t.Fatal(err)
343 }
344 // Multiple fixture binaries may share a release tag, but never a digest.
345 if err := s.Write(hex.EncodeToString(sum[:])+".json", wire); err != nil {
346 t.Fatal(err)
347 }
348}
349