package host import ( "context" "os" "testing" "github.com/carlosframework/platform/internal/moves" ) func TestPlan14RootFloorBootDecision(t *testing.T) { for _, attack := range []string{"none", "current below", "prev below", "stale verdict", "missing floor", "corrupt floor", "old reader", "pending participation", "replaced binary"} { t.Run(attack, func(t *testing.T) { f := rootMovesFloors{private: &rollbackMemory{files: map[string][]byte{}}, public: &rollbackMemory{files: map[string][]byte{}}, identity: rootFloorIdentity{Deployment: "d", Box: "b", Enrollment: "e"}} b := moves.Floor{Min: 1, MinRelease: "v2.0.0"} if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: f.identity, Floor: &b, Source: "history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil { t.Fatal(err) } current := rootFloorBinary{Version: "v2.0.0", Protocol: 1, Digest: floorTestDigest} prev := current j := rootFloorJudge{floors: f, current: "current", prev: "prev", binary: func(path string) (rootFloorBinary, error) { if path == "prev" { return prev, nil } return current, nil }, idle: func(context.Context) error { return nil }} if err := j.judge(t.Context()); err != nil { t.Fatal(err) } switch attack { case "current below": current.Version = "v1.0.0" prev = current case "prev below": prev.Version = "v1.0.0" case "stale verdict": if err := f.refresh(t.Context(), &b, nil, nil); err != nil { t.Fatal(err) } case "missing floor": f.public.Remove(rootFloorSelector) case "corrupt floor": f.public.Write(rootFloorSelector, []byte("broken")) case "replaced binary": prev.Digest = "replacement" } calls := 0 gate := func(context.Context, string) error { calls++ if attack == "old reader" { return os.ErrPermission } return nil } idle := func(context.Context) error { if attack == "pending participation" { return os.ErrPermission } return nil } got, err := rootFloorBootCandidate(t.Context(), j, true, gate, idle) if err != nil { t.Fatal(err) } want := "current" if attack == "none" { want = "prev" } if got != want { t.Fatal(got, want) } if attack == "prev below" && calls != 0 { t.Fatal("below-floor previous binary executed for gate") } }) } } func TestPlan14PrevRebootSafeVerdict(t *testing.T) { f := rootMovesFloors{private: &rollbackMemory{files: map[string][]byte{}}, public: &rollbackMemory{files: map[string][]byte{}}, identity: rootFloorIdentity{"d", "b", "e"}} bound := moves.Floor{Min: 1, MinRelease: "v2.0.0"} if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: f.identity, Floor: &bound, Source: "accepted", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil { t.Fatal(err) } prev := rootFloorBinary{Version: "v2.0.0", Protocol: 1, Digest: floorTestDigest} broken := false j := rootFloorJudge{floors: f, current: "current", prev: "prev", binary: func(path string) (rootFloorBinary, error) { if path == "current" && broken { return rootFloorBinary{}, os.ErrPermission } return prev, nil }, idle: func(context.Context) error { return nil }} if err := j.judge(t.Context()); err != nil { t.Fatal(err) } prev.File.Device++ broken = true got, err := rootFloorBootCandidate(t.Context(), j, true, func(context.Context, string) error { return nil }, func(context.Context) error { return nil }) if err != nil || got != "prev" { t.Fatal("interrupted update lost unchanged previous image after device renumber", got, err) } prev.Digest = "substituted" if err := rootFloorPrevVerdict(j); err == nil { t.Fatal("substituted bytes accepted") } }