package host import ( "context" "crypto/ed25519" "errors" "os" "path/filepath" "strings" "testing" "time" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) func TestPlan14Incident20261006WithLedger(t *testing.T) { t.Parallel() for _, state := range []string{"refused", "missing", "malformed", "unwritable"} { t.Run(state, func(t *testing.T) { e, rt, u, c, _ := plan14AdoptionFixture(t) h := seedHostRootHistory(t, rt) b := release.HistoryBinding{Host: rt.Host, Account: rt.Account, App: rt.App} if err := h.AddRefusal(b, release.Refusal{Host: rt.Host, App: rt.App, Version: "v2", Kind: release.RefusalWrongProgram, RefusedAt: time.Now()}); err != nil { t.Fatal(err) } mem := h.Store.(*rollbackMemory) switch state { case "missing": mem.files = map[string][]byte{} case "malformed": for k := range mem.files { mem.files[k] = []byte("{") } case "unwritable": mem.failWrite = true } pub, key, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } if err := e.store.Delete(t.Context(), release.ChannelKey(rt.Account, rt.App, rt.Channel)); err != nil { t.Fatal(err) } if err := release.WriteDigestMarker(t.Context(), e.store, release.RootSigner(key), rt.Account, "test"); err != nil { t.Fatal(err) } if err := release.Promote(t.Context(), e.store, key, rt.Account, rt.App, rt.Channel, "v1"); err != nil { t.Fatal(err) } if _, _, err := release.StageSignedHead(t.Context(), e.store, pub, nil, u.opt, rt); err != nil { t.Fatal(err) } if err := release.StageArtifact(u.opt, rt.Account, "v2", rt.App, []byte("new build")); err != nil { t.Fatal(err) } if err := release.PointLive(u.opt, rt.Host, rt.Account, "v2", rt.App); err != nil { t.Fatal(err) } u.props.ActiveState, u.props.SubState, u.props.MainPID = "failed", "failed", 0 source := &proofBindingSource{store: e.store, sys: u, optDir: u.opt, openFile: os.Open, unitDirs: []string{filepath.Dir(u.props.FragmentPath)}} recovery := namedUnitRecovery{source: source, pub: pub, writes: hostwrite.Config{}, readLive: os.Readlink, history: func() (*release.RootHistoryPass, error) { return &release.RootHistoryPass{History: h}, nil }} done, err := recovery.repair(t.Context(), rt.Host) if err != nil || !done { t.Fatalf("signed recovery: %v %v", done, err) } if got, err := release.LiveVersion(u.opt, rt.Host); err != nil || got != "v1" { t.Fatalf("promotion blocked: %s %v", got, err) } if len(u.effects) != 0 { t.Fatalf("recovery started an operator-stopped unit: %v", u.effects) } if err := u.ResetFailed(rt.Unit); err != nil { t.Fatal(err) } u.props.ActiveState, u.props.SubState = "inactive", "dead" if err := u.Start(rt.Unit); err != nil { t.Fatal(err) } if _, err := c.host(t.Context(), rt.Host); err != nil { t.Fatal(err) } if u.props.MainPID <= 0 || u.exe != release.VersionPath(u.opt, rt.Account, "v1", rt.App) { t.Fatal("incident recipe did not restore service") } }) } } type namedRecoveryWork struct { before func() deny bool } func (w namedRecoveryWork) Select(context.Context, string, string) (hostwrite.Work, error) { return w, nil } func (w namedRecoveryWork) With(_ context.Context, id hostwrite.Identity, fn func() error) error { if !strings.HasPrefix(id.Release, "sha256:") { return os.ErrPermission } if w.before != nil { w.before() } if w.deny { return os.ErrPermission } return fn() } func TestPlan14SignedNamedRecoveryAuthority(t *testing.T) { t.Parallel() for _, attack := range []string{"positive", "claims on", "ordinary", "active", "missing record", "wrong signature", "unit path", "untrusted live", "foreign live", "record changes", "unit changes", "live changes", "artifact changes", "held claim", "process starts"} { t.Run(attack, func(t *testing.T) { e, rt, u, _, pub := plan14AdoptionFixture(t) u.props.ActiveState, u.props.SubState, u.props.MainPID = "failed", "failed", 0 source := &proofBindingSource{store: e.store, sys: u, optDir: u.opt, openFile: os.Open, unitDirs: []string{filepath.Dir(u.props.FragmentPath)}} n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink} change := func() {} switch attack { case "claims on": n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{}} case "ordinary": rec, _, err := provision.FindRecordByHost(t.Context(), e.store, rt.Host) if err != nil { t.Fatal(err) } rec.Unit = "" plan14PutRecord(t, e.store, *rec) case "active": u.props.ActiveState, u.props.MainPID = "active", 7 case "missing record": if err := e.store.Delete(t.Context(), provision.RecordKey(rt.Account, rt.App, rt.Host)); err != nil { t.Fatal(err) } case "wrong signature": n.pub, _, _ = ed25519.GenerateKey(nil) case "unit path": u.props.ExecPath = "/untrusted" case "untrusted live": n.readLive = func(string) (string, error) { return "", os.ErrPermission } case "foreign live": if err := release.StageArtifact(u.opt, "bap", "v1", rt.App, []byte("foreign")); err != nil { t.Fatal(err) } if err := release.PointLive(u.opt, rt.Host, "bap", "v1", rt.App); err != nil { t.Fatal(err) } case "record changes": change = func() { rec, _, err := provision.FindRecordByHost(t.Context(), e.store, rt.Host) if err != nil { t.Fatal(err) } rec.RecordSeq++ plan14PutRecord(t, e.store, *rec) } case "unit changes": change = func() { if err := os.WriteFile(u.props.FragmentPath, []byte("changed"), 0644); err != nil { t.Fatal(err) } } case "live changes": change = func() { if err := os.Remove(release.LiveLink(u.opt, rt.Host)); err != nil { t.Fatal(err) } } case "artifact changes": change = func() { if err := os.WriteFile(release.VersionPath(u.opt, rt.Account, "v2", rt.App), []byte("forged"), 0755); err != nil { t.Fatal(err) } } case "held claim": n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{deny: true}} case "process starts": change = func() { u.props.ActiveState, u.props.MainPID = "active", 7 } } if strings.HasSuffix(attack, "changes") || attack == "process starts" { n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{before: change}} } done, err := n.repair(t.Context(), rt.Host) live, liveErr := release.LiveVersion(u.opt, rt.Host) if attack == "positive" || attack == "claims on" { if err != nil || !done || liveErr != nil || live != "v2" { t.Fatalf("recovery refused: %v %v %s %v", done, err, live, liveErr) } } else if attack == "ordinary" || attack == "active" { if done || err != nil || live != "v1" { t.Fatalf("excluded route changed: %v %v %s", done, err, live) } } else if err == nil || live == "v2" { t.Fatalf("unsafe recovery: %v %s", err, live) } if len(u.effects) != 0 { t.Fatalf("implicit start: %v", u.effects) } if liveErr != nil && !errors.Is(liveErr, os.ErrNotExist) { t.Fatal(liveErr) } }) } } func (u *plan14RestartUnits) Start(unit string) error { before := len(u.effects) err := u.Restart(unit) u.effects[before] = "start " + unit return err } type namedRecordSwitchStore struct { release.Store key string after func() } func (s *namedRecordSwitchStore) Get(ctx context.Context, key string) ([]byte, string, error) { raw, etag, err := s.Store.Get(ctx, key) if key == s.key && s.after != nil { fn := s.after s.after = nil fn() } return raw, etag, err } func TestPlan14SignedNamedRecoveryUsesOneCanonicalRecord(t *testing.T) { t.Parallel() e, rt, u, _, _ := plan14AdoptionFixture(t) if _, err := e.store.Put(t.Context(), "accounts/bac/account.json", []byte(`{"id":2,"sqid":"bac"}`), ""); err != nil { t.Fatal(err) } pub, key, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } pointer := release.ChannelPointer{Schema: 1, App: rt.App, Channel: rt.Channel, Version: "v2", PromotedAt: time.Now().UTC()} raw, err := release.SignRootEnvelope(release.RootSigner(key), pointer) if err != nil { t.Fatal(err) } for _, acct := range []string{rt.Account, "bac"} { if err := release.Ship(t.Context(), e.store, acct, release.Manifest{App: rt.App, Version: "v2", Kind: "binary", Artifacts: map[string]release.Artifact{release.AdoptPlatform(): {}}}, map[string][]byte{release.AdoptPlatform(): []byte("new build")}); err != nil { t.Fatal(err) } channel := release.ChannelKey(acct, rt.App, rt.Channel) _, etag, err := e.store.Get(t.Context(), channel) if err != nil && !errors.Is(err, os.ErrNotExist) { t.Fatal(err) } if _, err := e.store.Put(t.Context(), channel, raw, etag); err != nil { t.Fatal(err) } if err := release.StageArtifact(u.opt, acct, "v2", rt.App, []byte("new build")); err != nil { t.Fatal(err) } } oldKey := provision.RecordKey(rt.Account, rt.App, rt.Host) st := &namedRecordSwitchStore{Store: e.store, key: oldKey} st.after = func() { rec, _, err := provision.FindRecordByHost(t.Context(), e.store, rt.Host) if err != nil { t.Fatal(err) } if err := e.store.Delete(t.Context(), oldKey); err != nil { t.Fatal(err) } rec.Account = "bac" plan14PutRecord(t, e.store, *rec) } u.props.ActiveState, u.props.SubState, u.props.MainPID = "failed", "failed", 0 n := namedUnitRecovery{source: &proofBindingSource{store: st, sys: u, optDir: u.opt, openFile: os.Open, unitDirs: []string{filepath.Dir(u.props.FragmentPath)}}, pub: pub, readLive: os.Readlink} _, err = n.repair(t.Context(), rt.Host) live, _ := release.LiveVersion(u.opt, rt.Host) if err == nil || live != "v1" { t.Fatalf("mixed account authority repaired old account's link: %s %v", live, err) } }