carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "crypto/ed25519"
6 "errors"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/hostwrite"
14 "github.com/carlosframework/platform/internal/provision"
15 "github.com/carlosframework/platform/internal/release"
16)
17
18func TestPlan14Incident20261006WithLedger(t *testing.T) {
19 t.Parallel()
20 for _, state := range []string{"refused", "missing", "malformed", "unwritable"} {
21 t.Run(state, func(t *testing.T) {
22 e, rt, u, c, _ := plan14AdoptionFixture(t)
23 h := seedHostRootHistory(t, rt)
24 b := release.HistoryBinding{Host: rt.Host, Account: rt.Account, App: rt.App}
25 if err := h.AddRefusal(b, release.Refusal{Host: rt.Host, App: rt.App, Version: "v2", Kind: release.RefusalWrongProgram, RefusedAt: time.Now()}); err != nil {
26 t.Fatal(err)
27 }
28 mem := h.Store.(*rollbackMemory)
29 switch state {
30 case "missing":
31 mem.files = map[string][]byte{}
32 case "malformed":
33 for k := range mem.files {
34 mem.files[k] = []byte("{")
35 }
36 case "unwritable":
37 mem.failWrite = true
38 }
39 pub, key, err := ed25519.GenerateKey(nil)
40 if err != nil {
41 t.Fatal(err)
42 }
43 if err := e.store.Delete(t.Context(), release.ChannelKey(rt.Account, rt.App, rt.Channel)); err != nil {
44 t.Fatal(err)
45 }
46 if err := release.WriteDigestMarker(t.Context(), e.store, release.RootSigner(key), rt.Account, "test"); err != nil {
47 t.Fatal(err)
48 }
49 if err := release.Promote(t.Context(), e.store, key, rt.Account, rt.App, rt.Channel, "v1"); err != nil {
50 t.Fatal(err)
51 }
52 if _, _, err := release.StageSignedHead(t.Context(), e.store, pub, nil, u.opt, rt); err != nil {
53 t.Fatal(err)
54 }
55 if err := release.StageArtifact(u.opt, rt.Account, "v2", rt.App, []byte("new build")); err != nil {
56 t.Fatal(err)
57 }
58 if err := release.PointLive(u.opt, rt.Host, rt.Account, "v2", rt.App); err != nil {
59 t.Fatal(err)
60 }
61 u.props.ActiveState, u.props.SubState, u.props.MainPID = "failed", "failed", 0
62 source := &proofBindingSource{store: e.store, sys: u, optDir: u.opt, openFile: os.Open, unitDirs: []string{filepath.Dir(u.props.FragmentPath)}}
63 recovery := namedUnitRecovery{source: source, pub: pub, writes: hostwrite.Config{}, readLive: os.Readlink, history: func() (*release.RootHistoryPass, error) { return &release.RootHistoryPass{History: h}, nil }}
64 done, err := recovery.repair(t.Context(), rt.Host)
65 if err != nil || !done {
66 t.Fatalf("signed recovery: %v %v", done, err)
67 }
68 if got, err := release.LiveVersion(u.opt, rt.Host); err != nil || got != "v1" {
69 t.Fatalf("promotion blocked: %s %v", got, err)
70 }
71 if len(u.effects) != 0 {
72 t.Fatalf("recovery started an operator-stopped unit: %v", u.effects)
73 }
74 if err := u.ResetFailed(rt.Unit); err != nil {
75 t.Fatal(err)
76 }
77 u.props.ActiveState, u.props.SubState = "inactive", "dead"
78 if err := u.Start(rt.Unit); err != nil {
79 t.Fatal(err)
80 }
81 if _, err := c.host(t.Context(), rt.Host); err != nil {
82 t.Fatal(err)
83 }
84 if u.props.MainPID <= 0 || u.exe != release.VersionPath(u.opt, rt.Account, "v1", rt.App) {
85 t.Fatal("incident recipe did not restore service")
86 }
87 })
88 }
89}
90
91type namedRecoveryWork struct {
92 before func()
93 deny bool
94}
95
96func (w namedRecoveryWork) Select(context.Context, string, string) (hostwrite.Work, error) {
97 return w, nil
98}
99func (w namedRecoveryWork) With(_ context.Context, id hostwrite.Identity, fn func() error) error {
100 if !strings.HasPrefix(id.Release, "sha256:") {
101 return os.ErrPermission
102 }
103 if w.before != nil {
104 w.before()
105 }
106 if w.deny {
107 return os.ErrPermission
108 }
109 return fn()
110}
111
112func TestPlan14SignedNamedRecoveryAuthority(t *testing.T) {
113 t.Parallel()
114 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"} {
115 t.Run(attack, func(t *testing.T) {
116 e, rt, u, _, pub := plan14AdoptionFixture(t)
117 u.props.ActiveState, u.props.SubState, u.props.MainPID = "failed", "failed", 0
118 source := &proofBindingSource{store: e.store, sys: u, optDir: u.opt, openFile: os.Open, unitDirs: []string{filepath.Dir(u.props.FragmentPath)}}
119 n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink}
120 change := func() {}
121 switch attack {
122 case "claims on":
123 n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{}}
124 case "ordinary":
125 rec, _, err := provision.FindRecordByHost(t.Context(), e.store, rt.Host)
126 if err != nil {
127 t.Fatal(err)
128 }
129 rec.Unit = ""
130 plan14PutRecord(t, e.store, *rec)
131 case "active":
132 u.props.ActiveState, u.props.MainPID = "active", 7
133 case "missing record":
134 if err := e.store.Delete(t.Context(), provision.RecordKey(rt.Account, rt.App, rt.Host)); err != nil {
135 t.Fatal(err)
136 }
137 case "wrong signature":
138 n.pub, _, _ = ed25519.GenerateKey(nil)
139 case "unit path":
140 u.props.ExecPath = "/untrusted"
141 case "untrusted live":
142 n.readLive = func(string) (string, error) { return "", os.ErrPermission }
143 case "foreign live":
144 if err := release.StageArtifact(u.opt, "bap", "v1", rt.App, []byte("foreign")); err != nil {
145 t.Fatal(err)
146 }
147 if err := release.PointLive(u.opt, rt.Host, "bap", "v1", rt.App); err != nil {
148 t.Fatal(err)
149 }
150 case "record changes":
151 change = func() {
152 rec, _, err := provision.FindRecordByHost(t.Context(), e.store, rt.Host)
153 if err != nil {
154 t.Fatal(err)
155 }
156 rec.RecordSeq++
157 plan14PutRecord(t, e.store, *rec)
158 }
159 case "unit changes":
160 change = func() {
161 if err := os.WriteFile(u.props.FragmentPath, []byte("changed"), 0644); err != nil {
162 t.Fatal(err)
163 }
164 }
165 case "live changes":
166 change = func() {
167 if err := os.Remove(release.LiveLink(u.opt, rt.Host)); err != nil {
168 t.Fatal(err)
169 }
170 }
171 case "artifact changes":
172 change = func() {
173 if err := os.WriteFile(release.VersionPath(u.opt, rt.Account, "v2", rt.App), []byte("forged"), 0755); err != nil {
174 t.Fatal(err)
175 }
176 }
177 case "held claim":
178 n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{deny: true}}
179 case "process starts":
180 change = func() { u.props.ActiveState, u.props.MainPID = "active", 7 }
181 }
182 if strings.HasSuffix(attack, "changes") || attack == "process starts" {
183 n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{before: change}}
184 }
185 done, err := n.repair(t.Context(), rt.Host)
186 live, liveErr := release.LiveVersion(u.opt, rt.Host)
187 if attack == "positive" || attack == "claims on" {
188 if err != nil || !done || liveErr != nil || live != "v2" {
189 t.Fatalf("recovery refused: %v %v %s %v", done, err, live, liveErr)
190 }
191 } else if attack == "ordinary" || attack == "active" {
192 if done || err != nil || live != "v1" {
193 t.Fatalf("excluded route changed: %v %v %s", done, err, live)
194 }
195 } else if err == nil || live == "v2" {
196 t.Fatalf("unsafe recovery: %v %s", err, live)
197 }
198 if len(u.effects) != 0 {
199 t.Fatalf("implicit start: %v", u.effects)
200 }
201 if liveErr != nil && !errors.Is(liveErr, os.ErrNotExist) {
202 t.Fatal(liveErr)
203 }
204 })
205 }
206}
207
208func (u *plan14RestartUnits) Start(unit string) error {
209 before := len(u.effects)
210 err := u.Restart(unit)
211 u.effects[before] = "start " + unit
212 return err
213}
214
215type namedRecordSwitchStore struct {
216 release.Store
217 key string
218 after func()
219}
220
221func (s *namedRecordSwitchStore) Get(ctx context.Context, key string) ([]byte, string, error) {
222 raw, etag, err := s.Store.Get(ctx, key)
223 if key == s.key && s.after != nil {
224 fn := s.after
225 s.after = nil
226 fn()
227 }
228 return raw, etag, err
229}
230
231func TestPlan14SignedNamedRecoveryUsesOneCanonicalRecord(t *testing.T) {
232 t.Parallel()
233 e, rt, u, _, _ := plan14AdoptionFixture(t)
234 if _, err := e.store.Put(t.Context(), "accounts/bac/account.json", []byte(`{"id":2,"sqid":"bac"}`), ""); err != nil {
235 t.Fatal(err)
236 }
237 pub, key, err := ed25519.GenerateKey(nil)
238 if err != nil {
239 t.Fatal(err)
240 }
241 pointer := release.ChannelPointer{Schema: 1, App: rt.App, Channel: rt.Channel, Version: "v2", PromotedAt: time.Now().UTC()}
242 raw, err := release.SignRootEnvelope(release.RootSigner(key), pointer)
243 if err != nil {
244 t.Fatal(err)
245 }
246 for _, acct := range []string{rt.Account, "bac"} {
247 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 {
248 t.Fatal(err)
249 }
250 channel := release.ChannelKey(acct, rt.App, rt.Channel)
251 _, etag, err := e.store.Get(t.Context(), channel)
252 if err != nil && !errors.Is(err, os.ErrNotExist) {
253 t.Fatal(err)
254 }
255 if _, err := e.store.Put(t.Context(), channel, raw, etag); err != nil {
256 t.Fatal(err)
257 }
258 if err := release.StageArtifact(u.opt, acct, "v2", rt.App, []byte("new build")); err != nil {
259 t.Fatal(err)
260 }
261 }
262 oldKey := provision.RecordKey(rt.Account, rt.App, rt.Host)
263 st := &namedRecordSwitchStore{Store: e.store, key: oldKey}
264 st.after = func() {
265 rec, _, err := provision.FindRecordByHost(t.Context(), e.store, rt.Host)
266 if err != nil {
267 t.Fatal(err)
268 }
269 if err := e.store.Delete(t.Context(), oldKey); err != nil {
270 t.Fatal(err)
271 }
272 rec.Account = "bac"
273 plan14PutRecord(t, e.store, *rec)
274 }
275 u.props.ActiveState, u.props.SubState, u.props.MainPID = "failed", "failed", 0
276 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}
277 _, err = n.repair(t.Context(), rt.Host)
278 live, _ := release.LiveVersion(u.opt, rt.Host)
279 if err == nil || live != "v1" {
280 t.Fatalf("mixed account authority repaired old account's link: %s %v", live, err)
281 }
282}
283