//go:build unix package hostguard import ( "bytes" "context" "encoding/json" "os" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" ) func TestPlan14PublicRootTamperMatrix(t *testing.T) { operations := []string{"Capture", "With", "BeginFence", "CompleteFence", "ExistingFence", "ResumeFence", "FenceReceiptFor", "PendingFences", "CompletedColdPredecessor", "CompletedWriterStops", "CleanupWith", "CleanupRetireInstallationWith", "RetireInstallation", "RetireInstallationWith", "WithNoInstallation", "BeginColdHandover", "CompleteColdHandover"} for _, attack := range []string{"delete", "rename", "locked replacement", "forged receipt"} { for _, operation := range operations { t.Run(attack+"/"+operation, func(t *testing.T) { f, first, captured := fenceFixture(t) store := &rootTestStore{dir: t.TempDir()} f.g.Root = NewRootAuthority(store) f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "app" 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} if err := f.g.EstablishRoot(t.Context(), in, allow); err != nil { t.Fatal(err) } first.ObligationID = WriterStopObligation(first.Session) first.Region = "uk" f.obs.Claim.Holder.WriterBox = first.Box f.obs.Claim.Holder.Generation = "gen" f.obs.Claim.Holder.Admissions = []hostclaim.AdmissionRecord{{Session: first.Session, Box: first.Box, Region: first.Region, Generation: "gen", Mode: "cold"}} if _, err := f.g.BeginFence(t.Context(), first); err != nil { t.Fatal(err) } at := time.Now().UTC() receipt := []byte("root receipt") if err := f.g.CompleteFence(t.Context(), first, receipt, at, func(context.Context, FenceTarget, []byte, time.Time) error { return nil }); err != nil { t.Fatal(err) } target := first target.Session++ target.FenceID = "pending" target.ObligationID = WriterStopObligation(target.Session) f.obs.Claim.Holder.Session = target.Session 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"}) if _, err := f.g.BeginFence(t.Context(), target); err != nil { t.Fatal(err) } before, _ := store.Read(target.Host + ".json") path := filepath.Join(f.g.DataDir, "hostguard", target.Host+".json") switch attack { case "delete": if err := os.Remove(path); err != nil { t.Fatal(err) } case "rename": if err := os.Rename(filepath.Dir(path), filepath.Dir(path)+".old"); err != nil { t.Fatal(err) } case "locked replacement": unlock, err := acquire(t.Context(), f.g.LockDir, target.Host) if err != nil { t.Fatal(err) } defer unlock() if err = os.Rename(f.g.LockDir, f.g.LockDir+".old"); err != nil { t.Fatal(err) } if err = os.Mkdir(f.g.LockDir, 0700); err != nil { t.Fatal(err) } case "forged receipt": forged := record{V: 1, Host: target.Host, Incarnation: captured.Incarnation, ActiveStamp: 99, FenceJournal: []FenceReceipt{{Target: target, Bytes: []byte("forged"), EvidenceAt: at}}} raw, _ := json.Marshal(forged) if err := os.WriteFile(path, raw, 0660); err != nil { t.Fatal(err) } } effects := 0 effect := func() error { effects++; return nil } verify := func(_ context.Context, r FenceReceipt) error { if r.Target != first || !bytes.Equal(r.Bytes, receipt) || !r.EvidenceAt.Equal(at) { return ErrReceiptChanged } return nil } bad := target bad.FenceID = "forged" stale := Captured{Incarnation: captured.Incarnation, Stamp: 99} ctx, cancel := context.WithTimeout(t.Context(), time.Second) defer cancel() var err error success := false switch operation { case "Capture": var got Captured got, err = f.g.Capture(target.Host) if got != captured { t.Fatal(got) } success = true case "With": err = f.g.With(ctx, target.Host, captured, allow, effect) case "BeginFence": var got FenceTarget got, err = f.g.BeginFence(ctx, target) if got != target { t.Fatal(got) } success = true case "CompleteFence": err = f.g.CompleteFence(ctx, bad, []byte("forged"), at, func(context.Context, FenceTarget, []byte, time.Time) error { return nil }) case "ExistingFence": var got FenceTarget got, err = f.g.ExistingFence(ctx, target) if got != target { t.Fatal(got) } success = true case "ResumeFence": err = f.g.ResumeFence(ctx, target) success = true case "FenceReceiptFor": var got FenceReceipt got, err = f.g.FenceReceiptFor(ctx, first) if verify(ctx, got) != nil { t.Fatal(got) } success = true case "PendingFences": var got []FenceTarget got, err = f.g.PendingFences(ctx) if len(got) != 1 || got[0] != target { t.Fatal(got) } success = true case "CompletedColdPredecessor": var got FenceReceipt got, err = f.g.CompletedColdPredecessor(ctx, first.Host, captured, first.Session, first.Op, first.Phase) if verify(ctx, got) != nil { t.Fatal(got) } success = true case "CompletedWriterStops": var got []FenceReceipt got, err = f.g.CompletedWriterStops(ctx, first.Host, captured, first.Op, first.Box) if len(got) != 1 || verify(ctx, got[0]) != nil { t.Fatal(got) } success = true case "CleanupWith": err = f.g.CleanupWith(ctx, bad, verify, effect) case "CleanupRetireInstallationWith": err = f.g.CleanupRetireInstallationWith(ctx, bad, verify, effect) case "RetireInstallation": err = f.g.RetireInstallation(ctx, target.Host, func(*hostclaim.Claim) error { return ErrFenced }) case "RetireInstallationWith": err = f.g.RetireInstallationWith(ctx, target.Host, stale, allow, effect) case "WithNoInstallation": err = f.g.WithNoInstallation(ctx, target.Host, captured.Incarnation, allow, effect) case "BeginColdHandover": bad.Stamp = 99 bad.Session = 99 f.obs.Claim.Holder.Stamp = 99 f.obs.Claim.Holder.Session = 99 err = f.g.BeginColdHandover(ctx, target, bad, allow) case "CompleteColdHandover": err = f.g.CompleteColdHandover(ctx, bad, false, allow, effect) } if success && err != nil || !success && err == nil || effects != 0 { t.Fatal(operation, err, effects) } after, _ := store.Read(target.Host + ".json") if !bytes.Equal(before, after) { t.Fatal("root bytes changed") } pending, err := f.g.PendingFences(ctx) if err != nil || len(pending) != 1 || pending[0] != target { t.Fatal("lost recovery", pending, err) } }) } } } func TestPlan14RootUnavailableNeverUsesAgentRecord(t *testing.T) { for _, damage := range []string{"missing", "corrupt"} { t.Run(damage, func(t *testing.T) { f, target, captured := fenceFixture(t) disk := &rootTestStore{dir: t.TempDir()} f.g.Root = NewRootAuthority(disk) if damage == "corrupt" { if err := disk.Write(target.Host+".json", []byte("{")); err != nil { t.Fatal(err) } } effects := 0 effect := func() error { effects++; return nil } verify := func(context.Context, FenceReceipt) error { return nil } calls := map[string]func() error{ "Capture": func() error { _, err := f.g.Capture(target.Host); return err }, "With": func() error { return f.g.With(t.Context(), target.Host, captured, allow, effect) }, "BeginFence": func() error { _, err := f.g.BeginFence(t.Context(), target); return err }, "CompleteFence": func() error { return f.g.CompleteFence(t.Context(), target, []byte("receipt"), time.Now(), func(context.Context, FenceTarget, []byte, time.Time) error { return nil }) }, "ExistingFence": func() error { _, err := f.g.ExistingFence(t.Context(), target); return err }, "ResumeFence": func() error { return f.g.ResumeFence(t.Context(), target) }, "FenceReceiptFor": func() error { _, err := f.g.FenceReceiptFor(t.Context(), target); return err }, "CompletedColdPredecessor": func() error { _, err := f.g.CompletedColdPredecessor(t.Context(), target.Host, captured, target.Session, target.Op, target.Phase) return err }, "CompletedWriterStops": func() error { _, err := f.g.CompletedWriterStops(t.Context(), target.Host, captured, target.Op, target.Box) return err }, "CleanupWith": func() error { return f.g.CleanupWith(t.Context(), target, verify, effect) }, "CleanupRetireInstallationWith": func() error { return f.g.CleanupRetireInstallationWith(t.Context(), target, verify, effect) }, "RetireInstallation": func() error { return f.g.RetireInstallation(t.Context(), target.Host, allow) }, "RetireInstallationWith": func() error { return f.g.RetireInstallationWith(t.Context(), target.Host, captured, allow, effect) }, "WithNoInstallation": func() error { return f.g.WithNoInstallation(t.Context(), target.Host, captured.Incarnation, allow, effect) }, "CompleteColdHandover": func() error { return f.g.CompleteColdHandover(t.Context(), target, false, allow, effect) }, } for name, call := range calls { if err := call(); err == nil || effects != 0 { t.Fatal(name, err, effects) } } if damage == "corrupt" { if _, err := f.g.PendingFences(t.Context()); err == nil { t.Fatal("corrupt recovery accepted") } } }) } }