| 1 | //go:build linux && rootrunner |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "crypto/ed25519" |
| 8 | "crypto/sha256" |
| 9 | "encoding/base64" |
| 10 | "encoding/hex" |
| 11 | "encoding/json" |
| 12 | "github.com/carlosframework/platform/internal/release" |
| 13 | "github.com/carlosframework/platform/internal/rootparticipation" |
| 14 | "net/http" |
| 15 | "net/http/httptest" |
| 16 | "os" |
| 17 | "os/exec" |
| 18 | "path/filepath" |
| 19 | "runtime" |
| 20 | "sync/atomic" |
| 21 | "syscall" |
| 22 | "testing" |
| 23 | "time" |
| 24 | |
| 25 | "github.com/carlosframework/platform/internal/moves" |
| 26 | "github.com/carlosframework/platform/internal/rootstate" |
| 27 | ) |
| 28 | |
| 29 | func TestPlan14RootFloorCrashOrderingRootRunner(t *testing.T) { |
| 30 | for _, after := range []bool{false, true} { |
| 31 | for fail := 1; fail <= 5; fail++ { |
| 32 | dir := generationRootRunnerDir(t) |
| 33 | id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"} |
| 34 | f, err := openRootMovesFloors(dir, id, true) |
| 35 | if err != nil { |
| 36 | t.Fatal(err) |
| 37 | } |
| 38 | a := moves.Floor{Min: 1, MinRelease: "v1.0.0"} |
| 39 | b := moves.Floor{Min: 2, MinRelease: "v2.0.0"} |
| 40 | if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &a, Source: "root observation", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil { |
| 41 | t.Fatal(err) |
| 42 | } |
| 43 | n := 0 |
| 44 | broken := f |
| 45 | broken.private = &floorFaultStore{Store: f.private, n: &n, fail: fail, after: after} |
| 46 | broken.public = &floorFaultStore{Store: f.public, n: &n, fail: fail, after: after} |
| 47 | if err := broken.refresh(t.Context(), &b, nil, nil); err == nil { |
| 48 | t.Fatal("injected write succeeded") |
| 49 | } |
| 50 | f.close() |
| 51 | f, err = openRootMovesFloors(dir, id, false) |
| 52 | if err != nil { |
| 53 | t.Fatal(err) |
| 54 | } |
| 55 | if p, e := readRootFloorProjection(dir); e == nil && p.Floor != nil && *p.Floor == b { |
| 56 | t.Fatal("failed write acknowledged B") |
| 57 | } |
| 58 | if err := f.refresh(t.Context(), &b, nil, nil); err != nil { |
| 59 | t.Fatal("retry", err) |
| 60 | } |
| 61 | if p, e := readRootFloorProjection(dir); e != nil || p.Floor == nil || *p.Floor != b { |
| 62 | t.Fatal("durable retry", p, e) |
| 63 | } |
| 64 | f.close() |
| 65 | // Root-owned leaf under a service-owned ancestor must not open. |
| 66 | if err := os.Chown(rootFloorDir(dir), 65534, 65534); err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | if _, err := rootstate.Open(filepath.Join(rootFloorDir(dir), "private"), false, false); err == nil { |
| 70 | t.Fatal("hostile parent accepted") |
| 71 | } |
| 72 | if err := os.Chown(rootFloorDir(dir), 0, 0); err != nil { |
| 73 | t.Fatal(err) |
| 74 | } |
| 75 | } |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | func TestPlan14FloorRiseWithoutPlatformUpdateRootRunner(t *testing.T) { |
| 80 | for _, prev := range []string{"none", "v2.0.0", "v1.0.0"} { |
| 81 | t.Run(prev, func(t *testing.T) { |
| 82 | opt := generationRootRunnerDir(t) |
| 83 | dd := filepath.Join(opt, "data") |
| 84 | if err := os.Mkdir(dd, 0755); err != nil { |
| 85 | t.Fatal(err) |
| 86 | } |
| 87 | t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "deploy") |
| 88 | pub, key, err := ed25519.GenerateKey(nil) |
| 89 | if err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub)) |
| 93 | rootFloorRunnerBinary(t, opt, systemBinaryPath(opt), "v2.0.0", key) |
| 94 | if prev != "none" { |
| 95 | rootFloorRunnerBinary(t, opt, systemPrevPath(opt), prev, key) |
| 96 | } |
| 97 | id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"} |
| 98 | f, err := openRootMovesFloors(opt, id, true) |
| 99 | if err != nil { |
| 100 | t.Fatal(err) |
| 101 | } |
| 102 | defer f.close() |
| 103 | a := moves.Floor{Min: 1, MinRelease: "v1.0.0"} |
| 104 | b := moves.Floor{Min: 1, MinRelease: "v2.0.0"} |
| 105 | if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &a, Source: "history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil { |
| 106 | t.Fatal(err) |
| 107 | } |
| 108 | store := release.NewDirStore(t.TempDir()) |
| 109 | raw, _ := json.Marshal(b) |
| 110 | if _, err := store.Put(t.Context(), moves.FloorKey, raw, ""); err != nil { |
| 111 | t.Fatal(err) |
| 112 | } |
| 113 | if _, err := syncLocalFloor(t.Context(), store, dd); err != nil { |
| 114 | t.Fatal(err) |
| 115 | } |
| 116 | if ad := withMovesFields(edgesFile{Box: "box"}, dd, opt); ad.MovesFloorMinRelease == b.MinRelease { |
| 117 | t.Fatal("agent mirror acknowledged B") |
| 118 | } |
| 119 | gets := 0 |
| 120 | j := rootFloorJudge{floors: f, current: systemBinaryPath(opt), prev: systemPrevPath(opt), idle: func(context.Context) error { return nil }, binary: func(path string) (rootFloorBinary, error) { return readRootFloorBinary(opt, path) }, legacy: func() ([]*moves.Floor, error) { return rootFloorLegacy(dd) }, remote: func(ctx context.Context) (*moves.Floor, error) { gets++; return moves.ReadFloor(ctx, store) }} |
| 121 | if err := j.refresh(t.Context()); err != nil { |
| 122 | t.Fatal(err) |
| 123 | } |
| 124 | ad := withMovesFields(edgesFile{Box: "box"}, dd, opt) |
| 125 | if gets != 1 || ad.MovesFloorState != "accepted" || ad.MovesFloorMinRelease != b.MinRelease || ad.PrevRelease != prev { |
| 126 | t.Fatal("root projection", gets, ad) |
| 127 | } |
| 128 | if (ad.PrevAllowedFor == b.MinRelease) != (prev == "v2.0.0") { |
| 129 | t.Fatal("prev verdict", ad) |
| 130 | } |
| 131 | mirror := filepath.Join(dd, ".moves") |
| 132 | if err := os.Rename(mirror, mirror+"-moved"); err != nil { |
| 133 | t.Fatal(err) |
| 134 | } |
| 135 | if err := os.Mkdir(mirror, 0700); err != nil { |
| 136 | t.Fatal(err) |
| 137 | } |
| 138 | renamed := withMovesFields(edgesFile{Box: "box"}, dd, opt) |
| 139 | if renamed.MovesFloorMinRelease != ad.MovesFloorMinRelease || renamed.PrevRelease != ad.PrevRelease { |
| 140 | t.Fatal("mirror rename changed acceptance") |
| 141 | } |
| 142 | if err := os.RemoveAll(filepath.Join(dd, ".moves")); err != nil { |
| 143 | t.Fatal(err) |
| 144 | } |
| 145 | again := withMovesFields(edgesFile{Box: "box"}, dd, opt) |
| 146 | if again.MovesFloorMinRelease != ad.MovesFloorMinRelease || again.PrevRelease != ad.PrevRelease { |
| 147 | t.Fatal("mirror deletion changed acceptance") |
| 148 | } |
| 149 | if err := os.WriteFile(systemBinaryPath(opt), []byte("forged"), 0755); err != nil { |
| 150 | t.Fatal(err) |
| 151 | } |
| 152 | if j.refresh(t.Context()) == nil { |
| 153 | t.Fatal("unsigned binary accepted") |
| 154 | } |
| 155 | if withMovesFields(edgesFile{Box: "box"}, dd, opt).MovesFloorState == "accepted" { |
| 156 | t.Fatal("unsigned current acknowledged") |
| 157 | } |
| 158 | }) |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | func rootFloorRunnerBinary(t *testing.T, opt, path, version string, key ed25519.PrivateKey) { |
| 163 | t.Helper() |
| 164 | raw := []byte("#!/bin/sh\n# " + version + "\nexit 0\n") |
| 165 | if err := os.WriteFile(path, raw, 0755); err != nil { |
| 166 | t.Fatal(err) |
| 167 | } |
| 168 | identities, err := rootstate.Open(filepath.Join(opt, "system", "guard", "identities"), true, true) |
| 169 | if err != nil { |
| 170 | t.Fatal(err) |
| 171 | } |
| 172 | defer identities.Close() |
| 173 | sum := sha256.Sum256(raw) |
| 174 | platform := runtime.GOOS + "-" + runtime.GOARCH |
| 175 | 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[:])}}}} |
| 176 | wire, err := release.SignRootEnvelope(release.RootSigner(key), p) |
| 177 | if err != nil { |
| 178 | t.Fatal(err) |
| 179 | } |
| 180 | if _, err := release.VerifySystemIdentity(key.Public().(ed25519.PublicKey), wire); err != nil { |
| 181 | t.Fatal(err) |
| 182 | } |
| 183 | if err := identities.Write(version+".json", wire); err != nil { |
| 184 | t.Fatal(err) |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | func TestPlan14RootFloorAgentProjectionPayload(t *testing.T) { |
| 189 | opt := os.Getenv("PLAN14_P3_AGENT_OPT") |
| 190 | if opt == "" || os.Geteuid() == 0 { |
| 191 | t.Fatal("service-UID payload only") |
| 192 | } |
| 193 | ad := withMovesFields(edgesFile{Box: "box"}, filepath.Join(opt, "data"), opt) |
| 194 | if ad.MovesFloorState != "accepted" || ad.MovesFloorMinRelease != "v2.0.0" { |
| 195 | t.Fatal("service projection refused", ad) |
| 196 | } |
| 197 | // The same UID must be unable to change either projection or private state. |
| 198 | for _, path := range []string{filepath.Join(rootFloorDir(opt), rootFloorProjectionFile), filepath.Join(rootFloorDir(opt), "private", rootFloorAccepted)} { |
| 199 | if err := os.WriteFile(path, []byte("forged"), 0644); err == nil { |
| 200 | t.Fatal("service changed root floor") |
| 201 | } |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | func TestPlan14RootFloorAdoptRefreshRootRunner(t *testing.T) { |
| 206 | // This fixture owns its whole namespace; refuse any existing root census. |
| 207 | if err := os.Mkdir("/var/lib/carlos-claims", 0700); err != nil { |
| 208 | t.Fatal(err) |
| 209 | } |
| 210 | t.Cleanup(func() { |
| 211 | if err := os.RemoveAll("/var/lib/carlos-claims"); err != nil { |
| 212 | t.Error(err) |
| 213 | } |
| 214 | }) |
| 215 | census, err := rootstate.Open(rootparticipation.Namespace, true, false) |
| 216 | if err != nil { |
| 217 | t.Fatal(err) |
| 218 | } |
| 219 | raw, _ := json.Marshal(rootparticipation.Census{Schema: 1, Box: "box", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}) |
| 220 | if err := census.Write("census.json", raw); err != nil { |
| 221 | t.Fatal(err) |
| 222 | } |
| 223 | census.Close() |
| 224 | opt := generationRootRunnerDir(t) |
| 225 | dd := filepath.Join(opt, "data") |
| 226 | if err := os.Mkdir(dd, 0755); err != nil { |
| 227 | t.Fatal(err) |
| 228 | } |
| 229 | storeDir := filepath.Join(opt, "deployment") |
| 230 | if err := os.Mkdir(storeDir, 0755); err != nil { |
| 231 | t.Fatal(err) |
| 232 | } |
| 233 | t.Setenv("CARLOS_OPT_DIR", opt) |
| 234 | t.Setenv("CARLOS_DATA_DIR", dd) |
| 235 | t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "deploy") |
| 236 | t.Setenv("CARLOS_DEPLOYMENT_DIR", storeDir) |
| 237 | pub, key, err := ed25519.GenerateKey(nil) |
| 238 | if err != nil { |
| 239 | t.Fatal(err) |
| 240 | } |
| 241 | t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub)) |
| 242 | rootFloorRunnerBinary(t, opt, systemBinaryPath(opt), "v2.0.0", key) |
| 243 | id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"} |
| 244 | f, err := openRootMovesFloors(opt, id, true) |
| 245 | if err != nil { |
| 246 | t.Fatal(err) |
| 247 | } |
| 248 | defer f.close() |
| 249 | a := moves.Floor{Min: 1, MinRelease: "v1.0.0"} |
| 250 | b := moves.Floor{Min: 1, MinRelease: "v2.0.0"} |
| 251 | if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &a, Source: "history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil { |
| 252 | t.Fatal(err) |
| 253 | } |
| 254 | store := release.NewDirStore(storeDir) |
| 255 | raw, _ = json.Marshal(b) |
| 256 | if _, err := store.Put(t.Context(), moves.FloorKey, raw, ""); err != nil { |
| 257 | t.Fatal(err) |
| 258 | } |
| 259 | if _, err := syncLocalFloor(t.Context(), store, dd); err != nil { |
| 260 | t.Fatal(err) |
| 261 | } |
| 262 | // The real provider reads only this loopback S3 fixture; it makes no AWS call. |
| 263 | var gets atomic.Int32 |
| 264 | endpoint := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 265 | if r.Method != "GET" || r.URL.Path != "/deploy/"+moves.FloorKey { |
| 266 | http.NotFound(w, r) |
| 267 | return |
| 268 | } |
| 269 | gets.Add(1) |
| 270 | w.Header().Set("ETag", "fixture") |
| 271 | w.Write(raw) |
| 272 | })) |
| 273 | defer endpoint.Close() |
| 274 | t.Setenv("AWS_REGION", "eu-west-1") |
| 275 | t.Setenv("AWS_DEFAULT_REGION", "eu-west-1") |
| 276 | t.Setenv("AWS_ACCESS_KEY_ID", "fixture") |
| 277 | t.Setenv("AWS_SECRET_ACCESS_KEY", "fixture") |
| 278 | t.Setenv("AWS_SESSION_TOKEN", "") |
| 279 | t.Setenv("AWS_EC2_METADATA_DISABLED", "true") |
| 280 | t.Setenv("AWS_ENDPOINT_URL_S3", endpoint.URL) |
| 281 | t.Setenv("CARLOS_S3_ENDPOINT", endpoint.URL) |
| 282 | t.Setenv("AWS_PROFILE", "") |
| 283 | t.Setenv("AWS_DEFAULT_PROFILE", "") |
| 284 | if err := withClaimRootRelease(t.Context(), true, func() (rootstate.Store, error) { return rootstate.Open(filepath.Join(opt, "release"), true, false) }, func() error { return rootJudgePrev(nil) }); err != nil { |
| 285 | t.Fatal(err) |
| 286 | } |
| 287 | if gets.Load() == 0 { |
| 288 | t.Fatal("no authoritative GET before no-prev return") |
| 289 | } |
| 290 | ad := withMovesFields(edgesFile{Box: "box"}, dd, opt) |
| 291 | if ad.MovesFloorState != "accepted" || ad.MovesFloorMinRelease != b.MinRelease || ad.PrevRelease != "none" { |
| 292 | t.Fatal("actual adopt refresh", ad) |
| 293 | } |
| 294 | rawBinary, err := os.ReadFile(os.Args[0]) |
| 295 | if err != nil { |
| 296 | t.Fatal(err) |
| 297 | } |
| 298 | agentBinary := filepath.Join(opt, "agent.test") |
| 299 | if err := os.WriteFile(agentBinary, rawBinary, 0755); err != nil { |
| 300 | t.Fatal(err) |
| 301 | } |
| 302 | cmd := exec.CommandContext(t.Context(), agentBinary, "-test.run=^TestPlan14RootFloorAgentProjectionPayload$", "-test.v") |
| 303 | cmd.Dir = opt |
| 304 | cmd.Env = append(os.Environ(), "PLAN14_P3_AGENT_OPT="+opt) |
| 305 | cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: 65534, Gid: 65534}} |
| 306 | if out, err := cmd.CombinedOutput(); err != nil { |
| 307 | t.Fatalf("service UID: %v\n%s", err, out) |
| 308 | } |
| 309 | // Direct apply/recovery adapters cannot use erased legacy files to go back. |
| 310 | if err := os.RemoveAll(filepath.Join(dd, ".moves")); err != nil { |
| 311 | t.Fatal(err) |
| 312 | } |
| 313 | app := systemApplier{optDir: opt, dataDir: dd, rootMoves: rootMovesFloorIdentity} |
| 314 | if app.floorAllowsForward("v1.0.0") == nil { |
| 315 | t.Fatal("legacy deletion allowed downgrade") |
| 316 | } |
| 317 | if err := app.floorAllowsForward("v2.0.0"); err != nil { |
| 318 | t.Fatal("same floor refused", err) |
| 319 | } |
| 320 | } |
| 321 | |
| 322 | func TestPlan14RootFloorSignedTargetRootRunner(t *testing.T) { |
| 323 | opt := generationRootRunnerDir(t) |
| 324 | pub, key, err := ed25519.GenerateKey(nil) |
| 325 | if err != nil { |
| 326 | t.Fatal(err) |
| 327 | } |
| 328 | t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub)) |
| 329 | id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"} |
| 330 | f, err := openRootMovesFloors(opt, id, true) |
| 331 | if err != nil { |
| 332 | t.Fatal(err) |
| 333 | } |
| 334 | defer f.close() |
| 335 | b := moves.Floor{Min: 1, MinRelease: "v2.0.0"} |
| 336 | if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &b, Source: "history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil { |
| 337 | t.Fatal(err) |
| 338 | } |
| 339 | store := release.NewDirStore(t.TempDir()) |
| 340 | platform := runtime.GOOS + "-" + runtime.GOARCH |
| 341 | bytes := []byte("new signed target without earlier root history") |
| 342 | sum := sha256.Sum256(bytes) |
| 343 | p, err := release.PromoteSystem(t.Context(), store, release.RootSigner(key), "router", "stable", release.SystemRelease{Version: "v3.0.0", ReleaseTag: "v3.0.0", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{platform: {Key: release.SystemArtifactKey("v3.0.0", platform), SHA256: hex.EncodeToString(sum[:])}}}, release.SystemPromoteOptions{}) |
| 344 | if err != nil { |
| 345 | t.Fatal(err) |
| 346 | } |
| 347 | staged := filepath.Join(opt, "staging", "carlos") |
| 348 | if err := os.Mkdir(filepath.Dir(staged), 0700); err != nil { |
| 349 | t.Fatal(err) |
| 350 | } |
| 351 | if err := os.WriteFile(staged, bytes, 0755); err != nil { |
| 352 | t.Fatal(err) |
| 353 | } |
| 354 | calls := 0 |
| 355 | a := systemApplier{optDir: opt, channel: "stable", rootMoves: func() (rootFloorIdentity, bool, error) { return id, true, nil }, openStore: func(context.Context) (release.Store, ed25519.PublicKey, error) { return store, pub, nil }, ops: sysOps{run: func(context.Context, time.Duration, string, ...string) (string, error) { calls++; return "ran", nil }}} |
| 356 | if _, err := a.verifyPointer(t.Context()); err != nil { |
| 357 | t.Fatal(err) |
| 358 | } |
| 359 | if _, err := a.guardRun(t.Context(), time.Second, staged, "version"); err != nil || calls != 1 { |
| 360 | t.Fatal("signed target with unknown history refused", err, calls) |
| 361 | } |
| 362 | for _, storeName := range []struct { |
| 363 | store rootstate.Store |
| 364 | name string |
| 365 | }{{f.private, rootFloorAccepted}, {f.private, rootFloorDeny}, {f.public, rootFloorSelector}, {f.public, rootFloorProjectionFile}} { |
| 366 | if err := storeName.store.Remove(storeName.name); err != nil { |
| 367 | t.Fatal(err) |
| 368 | } |
| 369 | } |
| 370 | if _, err := a.guardRun(t.Context(), time.Second, staged, "version"); err != nil || calls != 2 { |
| 371 | t.Fatal("unknown floor blocked signed version", err, calls) |
| 372 | } |
| 373 | changed := *p |
| 374 | changed.Current.Version = "other" |
| 375 | if rememberRootFloorSystemIdentity(t.Context(), store, pub, opt, &changed) == nil { |
| 376 | t.Fatal("pointer substitution accepted") |
| 377 | } |
| 378 | if err := os.WriteFile(staged, []byte("substitution"), 0755); err != nil { |
| 379 | t.Fatal(err) |
| 380 | } |
| 381 | if _, err := a.guardRun(t.Context(), time.Second, staged, "version"); err == nil || calls != 2 { |
| 382 | t.Fatal("unsigned target executed", err, calls) |
| 383 | } |
| 384 | } |
| 385 | |