//go:build linux package host import ( "context" "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootparticipation" "github.com/carlosframework/platform/internal/rootstate" "golang.org/x/sys/unix" ) func TestPlan14GenerationRequiredArtifacts(t *testing.T) { plan14GenerationRequiredArtifacts(t, false) } func TestPlan14GenerationLocalRecoveryObligations(t *testing.T) { plan14GenerationRequiredArtifacts(t, true) } func plan14GenerationRequiredArtifacts(t *testing.T, local bool) { 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"} { if local && (strings.HasPrefix(attack, "record-") || attack == "changed-record-during-kernel") { continue } t.Run(attack, func(t *testing.T) { f, disk, rec := generationLaunchFixture(t) if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { t.Fatal(err) } r := readGenerationLaunch(t, disk) views, err := f.root.Guard.Root.GenerationInstallations(t.Context()) if err != nil { t.Fatal(err) } launch, err := f.journal.Read(t.Context(), f.target) if err != nil { t.Fatal(err) } t.Setenv("CARLOS_DATA_DIR", t.TempDir()) path := provision.InstanceDir(dataDir(), rec.Host) if err := os.MkdirAll(path, 0700); err != nil { t.Fatal(err) } var st unix.Stat_t if err := unix.Stat(path, &st); err != nil { t.Fatal(err) } rel, err := filepath.Rel(dataDir(), path) if err != nil { t.Fatal(err) } 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)} 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} snapshot.Payloads[0].Inventory.Entries = append(snapshot.Payloads[0].Inventory.Entries, entry) store := f.root.generations.store rec.Schema = 1 plan14PutRecord(t, store, rec) key := provision.RecordKey(rec.Account, rec.App, rec.Host) original, _, err := store.Get(t.Context(), key) if err != nil { t.Fatal(err) } put := func(raw []byte) { t.Helper() _, etag, err := store.Get(t.Context(), key) if err != nil && !errors.Is(err, os.ErrNotExist) { t.Fatal(err) } if _, err := store.Put(t.Context(), key, raw, etag); err != nil { t.Fatal(err) } } changeRecord := func() { t.Helper() var changed provision.Record if err := json.Unmarshal(original, &changed); err != nil { t.Fatal(err) } switch attack { case "record-schema": changed.Schema = 2 case "record-kind": changed.Kind = "unknown" case "record-account": changed.Account = "bap" case "record-app": changed.App = "other" case "record-host": changed.Host = "other.example" case "record-incarnation": changed.CreatedAt = changed.CreatedAt.Add(time.Hour) default: changed.RecordSeq++ } raw, err := json.Marshal(changed) if err != nil { t.Fatal(err) } put(raw) } attackActive := false v := generationReconciler{read: func(ctx context.Context) (generationInventory, error) { if local { return generationLocalObligations(ctx, snapshot, nil) } return generationRequiredInventory(ctx, store, snapshot, nil) }, kernel: func(context.Context) (string, string, error) { if attackActive && attack == "changed-record-during-kernel" { changeRecord() } return "boot", r.Binding.Digest, nil }} source := generationPinSource{open: func() (rootstate.Store, error) { return disk, nil }, reconcile: v.reconcile} ledger := &release.RootGenerations{Store: disk} accept := func() { t.Helper() pins, err := source.StagedPins(t.Context(), f.root.generations.optDir) if err != nil { t.Fatal(err) } if pinned, err := pins.Refers(r.Binding.Path); err != nil || !pinned { t.Fatalf("reservation not pinned: %v", err) } if c, err := ledger.Completeness(); err != nil || !c.Complete { t.Fatalf("valid required artifacts refused: %+v %v", c, err) } } if attack == "service" { snapshot.Installations[0].Installation.Artifacts.StateDirectory = false rec.Kind, rec.Addr = "service", "127.0.0.1:8080" plan14PutRecord(t, store, rec) snapshot.Payloads[0].Inventory.Entries = snapshot.Payloads[0].Inventory.Entries[:1] } accept() if attack == "none" || attack == "service" { return } initial, _ := ledger.Completeness() if attack == "record-next-pass" { changeRecord() accept() next, err := ledger.Completeness() if err != nil || next.Evidence.Installations == initial.Evidence.Installations { t.Fatalf("record revision absent from certificate: %+v %v", next, err) } return } saved, err := json.Marshal(snapshot) if err != nil { t.Fatal(err) } entries := &snapshot.Payloads[0].Inventory.Entries switch attack { case "requirements-missing": snapshot.Installations[0].Installation.Artifacts = hostguard.InstallationArtifacts{} case "requirements-schema": snapshot.Installations[0].Installation.Artifacts.Schema++ case "requirements-malformed": snapshot.Installations[0].Installation.Artifacts.Timers = "bad/path" case "route-omitted": *entries = (*entries)[1:] case "historical-state-omitted": rec.Kind, rec.Addr = "service", "127.0.0.1:8080" plan14PutRecord(t, store, rec) *entries = (*entries)[:1] case "state-omitted": *entries = (*entries)[:1] case "state-child-only", "state-wrong-host": name := filepath.Join(path, "child") if attack == "state-wrong-host" { name = provision.InstanceDir(dataDir(), "other.example") } if err := os.MkdirAll(name, 0700); err != nil { t.Fatal(err) } if err := unix.Stat(name, &st); err != nil { t.Fatal(err) } (*entries)[1].RelativePath, err = filepath.Rel(dataDir(), name) if err != nil { t.Fatal(err) } (*entries)[1].Device, (*entries)[1].Inode = uint64(st.Dev), st.Ino case "state-wrong-root": (*entries)[1].ManagedRoot = "opt" case "state-file": (*entries)[1].Type = unix.S_IFREG case "duplicate-route": *entries = append(*entries, (*entries)[0]) case "duplicate-state": *entries = append(*entries, (*entries)[1]) case "false-empty": *entries = nil snapshot.Installations[0].Installation.Empty = true case "predecessor-omitted": snapshot.Installations[0].Retired = append(snapshot.Installations[0].Retired, hostguard.Captured{Incarnation: f.target.Incarnation, Stamp: f.target.Stamp - 1}) old := snapshot.Payloads[0] old.Inventory.InstallationStamp-- old.Inventory.Entries = append([]hostbox.StopInventoryEntry(nil), (*entries)[:1]...) snapshot.Payloads = append(snapshot.Payloads, old) case "record-missing": if err := store.Delete(t.Context(), key); err != nil { t.Fatal(err) } case "record-malformed": put([]byte("{")) case "record-schema", "record-kind", "record-account", "record-app", "record-host", "record-incarnation": changeRecord() } attackActive = true if pins, err := source.StagedPins(t.Context(), f.root.generations.optDir); err == nil || pins != nil { t.Fatalf("required-artifact omission or record change certified: %v %v", pins, err) } if c, err := ledger.Completeness(); err != nil || c.Complete || c.Revision <= initial.Revision { t.Fatalf("invalid source retained completeness: %+v %v", c, err) } if got, err := ledger.Read(r.Binding); err != nil || got.Binding != r.Binding || got.Closed { t.Fatal("required-artifact refusal altered the reservation") } attackActive = false put(original) if err := json.Unmarshal(saved, &snapshot); err != nil { t.Fatal(err) } accept() }) } }