| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/hostbox" |
| 11 | "github.com/carlosframework/platform/internal/hostclaim" |
| 12 | "github.com/carlosframework/platform/internal/hostguard" |
| 13 | "github.com/carlosframework/platform/internal/provision" |
| 14 | "github.com/carlosframework/platform/internal/release" |
| 15 | "github.com/carlosframework/platform/internal/rootparticipation" |
| 16 | "github.com/carlosframework/platform/internal/rootstate" |
| 17 | ) |
| 18 | |
| 19 | func TestPlan14GenerationInventoryCompleteness(t *testing.T) { |
| 20 | for _, attack := range []string{"none", "empty-reservations", "retired-only", "missing-installation", "missing-launch", "extra-launch", "extra-installation", "wrong-controller", "wrong-inputs", "wrong-installation", "wrong-process", "wrong-census", "unknown-migration", "incomplete-census", "kernel", "changed-snapshot"} { |
| 21 | t.Run(attack, func(t *testing.T) { |
| 22 | f, disk, _ := generationLaunchFixture(t) |
| 23 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | r := readGenerationLaunch(t, disk) |
| 27 | view, err := f.root.Guard.Root.View(t.Context(), f.target.Host, hostguard.Captured{Incarnation: f.target.Incarnation, Stamp: f.target.Stamp}) |
| 28 | if err != nil { |
| 29 | t.Fatal(err) |
| 30 | } |
| 31 | launch, err := f.journal.Read(t.Context(), f.target) |
| 32 | if err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | snapshot := generationInventory{Census: rootparticipation.Census{Schema: 1, Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}, Payloads: generationPayloadFixture([]hostguard.FenceView{view}), Installations: []hostguard.FenceView{view}, Launches: []hostbox.LaunchInventory{launch}} |
| 36 | records := []release.GenerationReservation{r} |
| 37 | switch attack { |
| 38 | case "empty-reservations": |
| 39 | records = nil |
| 40 | case "retired-only": |
| 41 | records[0].Closed = true |
| 42 | records[0].Retirement = r.Binding.Digest |
| 43 | case "missing-installation": |
| 44 | snapshot.Installations = nil |
| 45 | case "missing-launch": |
| 46 | snapshot.Launches = nil |
| 47 | case "extra-launch": |
| 48 | extra := launch |
| 49 | extra.Target.Host = "other.example" |
| 50 | snapshot.Launches = append(snapshot.Launches, extra) |
| 51 | case "extra-installation": |
| 52 | extra := view |
| 53 | extra.Host = "other.example" |
| 54 | extra.Installation.Host = extra.Host |
| 55 | extra.Installation.Record = provision.RecordKey(extra.Installation.Account, extra.Installation.App, extra.Host) |
| 56 | snapshot.Installations = append(snapshot.Installations, extra) |
| 57 | case "wrong-controller": |
| 58 | snapshot.Launches[0].ControllerPID++ |
| 59 | case "wrong-inputs": |
| 60 | snapshot.Launches[0].Reservations[0].Inputs = "other" |
| 61 | case "wrong-installation": |
| 62 | records[0].Binding.Installation = "other" |
| 63 | case "wrong-process": |
| 64 | records[0].Process.PID++ |
| 65 | case "wrong-census": |
| 66 | snapshot.Census.Enrollment = "other" |
| 67 | case "unknown-migration": |
| 68 | snapshot.Census.Migration = 0 |
| 69 | case "incomplete-census": |
| 70 | snapshot.Census.Complete = false |
| 71 | } |
| 72 | reads, scans := 0, 0 |
| 73 | verifier := generationReconciler{read: func(context.Context) (generationInventory, error) { |
| 74 | reads++ |
| 75 | out := snapshot |
| 76 | if attack == "changed-snapshot" && reads > 1 { |
| 77 | out.Census.Revision++ |
| 78 | } |
| 79 | return out, nil |
| 80 | }, kernel: func(context.Context) (string, string, error) { |
| 81 | scans++ |
| 82 | if attack == "kernel" { |
| 83 | return "", "", os.ErrPermission |
| 84 | } |
| 85 | return "boot-now", r.Binding.Digest, nil |
| 86 | }} |
| 87 | evidence, err := verifier.reconcile(t.Context(), records) |
| 88 | if attack == "none" { |
| 89 | if err != nil || scans != 1 || reads != 2 || evidence.Box != f.target.Box || evidence.Installations == "" || evidence.Launches == "" { |
| 90 | t.Fatalf("positive: %+v %v reads=%d scans=%d", evidence, err, reads, scans) |
| 91 | } |
| 92 | } else if err == nil { |
| 93 | t.Fatalf("%s certified: %+v", attack, evidence) |
| 94 | } |
| 95 | }) |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | func TestPlan14GenerationInventoryEmptyNeedsIndependentSources(t *testing.T) { |
| 100 | snapshot := generationInventory{Census: rootparticipation.Census{Schema: 1, Box: "box", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}} |
| 101 | for _, missing := range []string{"none", "inventory", "kernel", "census"} { |
| 102 | t.Run(missing, func(t *testing.T) { |
| 103 | v := generationReconciler{read: func(context.Context) (generationInventory, error) { |
| 104 | if missing == "census" { |
| 105 | return generationInventory{}, nil |
| 106 | } |
| 107 | return snapshot, nil |
| 108 | }, kernel: func(context.Context) (string, string, error) { return "boot", strings.Repeat("a", 64), nil }} |
| 109 | if missing == "inventory" { |
| 110 | v.read = nil |
| 111 | } |
| 112 | if missing == "kernel" { |
| 113 | v.kernel = nil |
| 114 | } |
| 115 | _, err := v.reconcile(t.Context(), nil) |
| 116 | if (err == nil) != (missing == "none") { |
| 117 | t.Fatalf("empty: %v", err) |
| 118 | } |
| 119 | }) |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | func TestPlan14GenerationInventoryPinConsumer(t *testing.T) { |
| 124 | for _, attack := range []string{"none", "omitted-reservation", "kernel", "lost-census", "missing-payload", "payload-revision"} { |
| 125 | t.Run(attack, func(t *testing.T) { |
| 126 | f, disk, _ := generationLaunchFixture(t) |
| 127 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| 128 | t.Fatal(err) |
| 129 | } |
| 130 | r := readGenerationLaunch(t, disk) |
| 131 | view, err := f.root.Guard.Root.View(t.Context(), f.target.Host, hostguard.Captured{Incarnation: f.target.Incarnation, Stamp: f.target.Stamp}) |
| 132 | if err != nil { |
| 133 | t.Fatal(err) |
| 134 | } |
| 135 | launch, err := f.journal.Read(t.Context(), f.target) |
| 136 | if err != nil { |
| 137 | t.Fatal(err) |
| 138 | } |
| 139 | reads := 0 |
| 140 | v := generationReconciler{read: func(context.Context) (generationInventory, error) { |
| 141 | reads++ |
| 142 | c := rootparticipation.Census{Schema: 1, Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true} |
| 143 | if attack == "lost-census" { |
| 144 | c.Complete = false |
| 145 | } |
| 146 | payloads := generationPayloadFixture([]hostguard.FenceView{view}) |
| 147 | if attack == "missing-payload" { |
| 148 | payloads = nil |
| 149 | } |
| 150 | if attack == "payload-revision" { |
| 151 | payloads[0].Revision++ |
| 152 | } |
| 153 | return generationInventory{Census: c, Payloads: payloads, Installations: []hostguard.FenceView{view}, Launches: []hostbox.LaunchInventory{launch}}, nil |
| 154 | }, kernel: func(context.Context) (string, string, error) { |
| 155 | if attack == "kernel" { |
| 156 | return "", "", os.ErrPermission |
| 157 | } |
| 158 | return "boot", r.Binding.Digest, nil |
| 159 | }} |
| 160 | if attack == "omitted-reservation" { |
| 161 | names, err := disk.Names() |
| 162 | if err != nil { |
| 163 | t.Fatal(err) |
| 164 | } |
| 165 | for _, name := range names { |
| 166 | if err := disk.Remove(name); err != nil { |
| 167 | t.Fatal(err) |
| 168 | } |
| 169 | } |
| 170 | } |
| 171 | s := generationPinSource{open: func() (rootstate.Store, error) { return disk, nil }, reconcile: v.reconcile} |
| 172 | pins, err := s.StagedPins(t.Context(), f.root.generations.optDir) |
| 173 | if attack != "none" { |
| 174 | if err == nil { |
| 175 | t.Fatalf("unsafe snapshot: %+v", pins) |
| 176 | } |
| 177 | return |
| 178 | } |
| 179 | if err != nil || reads != 2 { |
| 180 | t.Fatalf("pins: %v reads=%d", err, reads) |
| 181 | } |
| 182 | if pinned, err := pins.Refers(r.Binding.Path); err != nil || !pinned { |
| 183 | t.Fatalf("missing exact pin: %v", err) |
| 184 | } |
| 185 | c, err := (&release.RootGenerations{Store: disk}).Completeness() |
| 186 | if err != nil || !c.Complete { |
| 187 | t.Fatalf("certificate: %+v %v", c, err) |
| 188 | } |
| 189 | }) |
| 190 | } |
| 191 | s, ok := productionRootGenerationPins().(generationPinSource) |
| 192 | if !ok || s.reconcile == nil { |
| 193 | t.Fatal("production reconciliation not connected") |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | func TestPlan14GenerationInventoryLifecycle(t *testing.T) { |
| 198 | for _, mode := range []string{"pending", "pending-wrong-controller", "retired", "retained-rebind", "unrecognized-predecessor"} { |
| 199 | t.Run(mode, func(t *testing.T) { |
| 200 | f, disk, _ := generationLaunchFixture(t) |
| 201 | prior, err := f.journal.Read(t.Context(), f.target) |
| 202 | if err != nil { |
| 203 | t.Fatal(err) |
| 204 | } |
| 205 | journal, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "launch.db")) |
| 206 | if err != nil { |
| 207 | t.Fatal(err) |
| 208 | } |
| 209 | defer journal.Close() |
| 210 | if err := journal.Prelaunch(t.Context(), prior); err != nil { |
| 211 | t.Fatal(err) |
| 212 | } |
| 213 | f.root.Journal, f.journal = journal, journal |
| 214 | pending := strings.HasPrefix(mode, "pending") |
| 215 | if pending { |
| 216 | if _, err := f.root.generations.reserve(t.Context(), f.root, f.peer, f.req); err != nil { |
| 217 | t.Fatal(err) |
| 218 | } |
| 219 | } else { |
| 220 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| 221 | t.Fatal(err) |
| 222 | } |
| 223 | if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil { |
| 224 | t.Fatal(err) |
| 225 | } |
| 226 | if err := journal.CloseWriters(t.Context(), f.target); err != nil { |
| 227 | t.Fatal(err) |
| 228 | } |
| 229 | } |
| 230 | r := readGenerationLaunch(t, disk) |
| 231 | if mode == "retired" { |
| 232 | v := generationRetirement{guard: f.root.Guard, journal: journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { return r.Binding.Digest, nil }} |
| 233 | if err := v.retire(t.Context(), &release.RootGenerations{Store: disk}, r); err != nil { |
| 234 | t.Fatal(err) |
| 235 | } |
| 236 | } |
| 237 | if mode == "retained-rebind" || mode == "unrecognized-predecessor" { |
| 238 | captured := hostguard.Captured{Incarnation: f.target.Incarnation, Stamp: f.target.Stamp} |
| 239 | in, err := f.root.Guard.Root.Installation(t.Context(), f.target.Host, captured) |
| 240 | if err != nil { |
| 241 | t.Fatal(err) |
| 242 | } |
| 243 | next := in |
| 244 | next.Stamp += 3 |
| 245 | next.InventoryRevision++ |
| 246 | f.claims.set(func(c *hostclaim.Claim) { c.Holder.Stamp = next.Stamp }) |
| 247 | if err := f.root.Guard.RebindRootInventory(t.Context(), captured, in.InventoryRevision, next, []byte("verified-rebind"), func(*hostclaim.Claim) error { return nil }, func() error { return nil }); err != nil { |
| 248 | t.Fatal(err) |
| 249 | } |
| 250 | } |
| 251 | v := generationReconciler{read: func(ctx context.Context) (generationInventory, error) { |
| 252 | views, err := f.root.Guard.Root.GenerationInstallations(ctx) |
| 253 | if err != nil { |
| 254 | return generationInventory{}, err |
| 255 | } |
| 256 | launches, err := journal.GenerationLaunches(ctx) |
| 257 | if err != nil { |
| 258 | return generationInventory{}, err |
| 259 | } |
| 260 | if mode == "unrecognized-predecessor" { |
| 261 | views[0].Retired = nil |
| 262 | } |
| 263 | if mode == "pending-wrong-controller" { |
| 264 | launches[0].ControllerPID++ |
| 265 | } |
| 266 | return generationInventory{Census: rootparticipation.Census{Schema: 1, Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}, Payloads: generationPayloadFixture(views), Installations: views, Launches: launches}, nil |
| 267 | }, kernel: func(context.Context) (string, string, error) { return "boot", r.Binding.Digest, nil }} |
| 268 | source := generationPinSource{open: func() (rootstate.Store, error) { return disk, nil }, reconcile: v.reconcile} |
| 269 | pins, err := source.StagedPins(t.Context(), f.root.generations.optDir) |
| 270 | if mode == "pending-wrong-controller" || mode == "unrecognized-predecessor" { |
| 271 | if err == nil { |
| 272 | t.Fatal("unbound generation certified") |
| 273 | } |
| 274 | return |
| 275 | } |
| 276 | if err != nil { |
| 277 | t.Fatal(err) |
| 278 | } |
| 279 | pinned, err := pins.Refers(r.Binding.Path) |
| 280 | if err != nil || pinned != (mode != "retired") { |
| 281 | t.Fatalf("exact lifecycle pin=%v: %v", pinned, err) |
| 282 | } |
| 283 | }) |
| 284 | } |
| 285 | } |
| 286 | |