carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostactors
2
3import (
4 "context"
5 "crypto/ed25519"
6 "encoding/json"
7 "github.com/carlosframework/platform/internal/hostclaim"
8 "github.com/carlosframework/platform/internal/hostfloor"
9 "github.com/carlosframework/platform/internal/release"
10 "github.com/carlosframework/platform/internal/status"
11 "reflect"
12 "slices"
13 "time"
14)
15
16type ComputeBinding struct {
17 Box string `json:"box"`
18 Enrollment string `json:"enrollment"`
19}
20type ConsoleProcess = status.ConsoleProcess
21type Report struct {
22 Compute *ComputeBinding `json:"compute,omitempty"`
23 ConsoleProcesses []ConsoleProcess `json:"console_processes,omitempty"`
24 Component string `json:"component"`
25 SystemChannel string `json:"system_channel,omitempty"`
26 ActivationID string `json:"activation_id,omitempty"`
27 ActivationComplete bool `json:"activation_complete,omitempty"`
28 GeneratedAt time.Time `json:"generated_at"`
29 Actor string `json:"actor"`
30 Enrollment string `json:"enrollment"`
31 InstanceID string `json:"instance_id"`
32 ProcessID string `json:"process_id"`
33 Release string `json:"release"`
34 ReleaseTag string `json:"release_tag"`
35 Platform string `json:"platform"`
36 BinarySHA256 string `json:"binary_sha256"`
37 MovesProtocol int `json:"moves_protocol"`
38 BootID string `json:"boot_id"`
39 FloorSHA256 string `json:"floor_sha256"`
40 FloorGuardReady bool `json:"floor_guard_ready"`
41 HostClaimEnabled bool `json:"host_claim_enabled"`
42 RetirementReceiptSHA256 string `json:"retirement_receipt_sha256,omitempty"`
43}
44type Scope struct {
45 // Only CensusBoxRelocation sets this private permission after eligibility checks.
46 boxRelocation bool
47 StarterBox string
48
49 Namespace string
50 Regions []string
51 Pools []string
52 Participants []string
53 ConsoleID string
54}
55type Evidence interface {
56 BoxReport(context.Context, string, Box) (Report, error)
57 ConsoleReport(context.Context, string, Console) (Report, error)
58 LocalConsole(context.Context) (Report, error)
59 Identity(context.Context, string) ([]byte, error)
60 Fence(context.Context, Observed, string, time.Time) (bool, error)
61}
62
63func VerifyReport(ctx context.Context, e Evidence, pub ed25519.PublicKey, f hostfloor.Verified, r Report, enrollment, instance string, now time.Time) error {
64 return verifyReport(ctx, e, pub, f, r, enrollment, instance, now, false)
65}
66func verifyReport(ctx context.Context, e Evidence, pub ed25519.PublicKey, f hostfloor.Verified, r Report, enrollment, instance string, now time.Time, allowStale bool) error {
67 age := now.Sub(r.GeneratedAt)
68 if r.GeneratedAt.IsZero() || (!allowStale && age > 15*time.Minute) || age < -5*time.Second || r.Enrollment != enrollment || r.InstanceID != instance || r.BootID == "" || !r.FloorGuardReady || r.FloorSHA256 != f.Digest || r.MovesProtocol < f.Floor.Min {
69 return ErrRoster
70 }
71 wire, err := e.Identity(ctx, r.ReleaseTag)
72 if err != nil {
73 return err
74 }
75 p, err := release.VerifySystemIdentity(pub, wire)
76 if err != nil {
77 return err
78 }
79 if p.Current.ReleaseTag != r.ReleaseTag || p.Current.MovesProtocol != r.MovesProtocol {
80 return ErrRoster
81 }
82 return hostfloor.CheckComponent(pub, f, wire, r.Release, r.Platform, r.BinarySHA256, r.Component)
83}
84func Census(ctx context.Context, o Observed, e Evidence, pub ed25519.PublicKey, f hostfloor.Verified, scope Scope, now time.Time) (map[string]Report, error) {
85 if o.Roster.Publication != "published" || o.Roster.FloorSHA256 != f.Digest {
86 return nil, ErrRoster
87 }
88 if scope.Namespace == "" || len(scope.Regions) == 0 || len(scope.Pools) == 0 || (!scope.boxRelocation && scope.ConsoleID == "") || e == nil {
89 return nil, ErrRoster
90 }
91 if err := validateScope(o.Roster, scope); err != nil {
92 return nil, err
93 }
94 reports := map[string]Report{}
95 participants := map[string]bool{}
96 for _, p := range scope.Participants {
97 participants[p] = true
98 }
99 for ref, b := range o.Roster.Boxes {
100 required := participants[ref] || (b.Namespace == scope.Namespace && slices.Contains(scope.Regions, b.Region) && slices.Contains(scope.Pools, b.Pool))
101 if !required {
102 continue
103 }
104 requiredParticipant := participants[ref]
105 delete(participants, ref)
106 fenced, err := e.Fence(ctx, o, ref, now)
107 if err != nil {
108 return nil, err
109 }
110 if fenced {
111 if requiredParticipant {
112 return nil, ErrRoster
113 }
114 continue
115 }
116 if b.State != "active" {
117 if requiredParticipant {
118 return nil, ErrRoster
119 }
120 if len(b.LossIDs) == 0 || b.QuarantineRevision == 0 {
121 return nil, ErrRoster
122 }
123 exact, ok := e.(ExactEvidence)
124 if !ok {
125 return nil, ErrRoster
126 }
127 found := false
128 for _, id := range b.LossIDs {
129 loss, err := hostclaim.ReadLossRecord(ctx, exact.Store, pub, id)
130 if err != nil {
131 return nil, err
132 }
133 if loss.Record.Box != ref || loss.Record.Enrollment != b.Enrollment {
134 return nil, ErrRoster
135 }
136 if loss.Record.QuarantineRevision == b.QuarantineRevision {
137 found = true
138 }
139 }
140 if !found {
141 return nil, ErrRoster
142 }
143 continue
144 }
145 r, err := e.BoxReport(ctx, ref, b)
146 if err != nil {
147 return nil, err
148 }
149 if r.Component != "router" || r.Actor != ref || !r.HostClaimEnabled {
150 return nil, ErrRoster
151 }
152 if err = VerifyReport(ctx, e, pub, f, r, b.Enrollment, b.Cloud.InstanceID, now); err != nil {
153 return nil, err
154 }
155 reports["box:"+ref] = r
156 }
157 if len(participants) != 0 {
158 return nil, ErrRoster
159 }
160 for id, c := range o.Roster.Consoles {
161 if !slices.Contains(c.Namespaces, scope.Namespace) && id != scope.ConsoleID {
162 continue
163 }
164 r, err := e.ConsoleReport(ctx, id, c)
165 if err != nil {
166 return nil, err
167 }
168 if r.Component != "console" || r.Actor != id || r.ProcessID == "" || !r.HostClaimEnabled {
169 return nil, ErrRoster
170 }
171 bound, err := verifyCompute(ctx, o, e, id, c, r)
172 if err != nil {
173 return nil, err
174 }
175 if c.Compute != nil {
176 fenced, err := e.Fence(ctx, o, c.Compute.Box, now)
177 if err != nil {
178 return nil, err
179 }
180 if fenced {
181 if id == scope.ConsoleID {
182 return nil, ErrRoster
183 }
184 continue
185 }
186 }
187 allowStale := false
188 if scope.boxRelocation && c.Compute != nil {
189 // Re-read the one current private key. No caller-selected historical report.
190 exact, ok := e.(ExactEvidence)
191 if ok {
192 newest, readErr := exact.ConsoleReport(ctx, id, c)
193 if readErr != nil {
194 return nil, readErr
195 }
196 if !reflect.DeepEqual(newest, r) {
197 return nil, ErrRoster
198 }
199 box := o.Roster.Boxes[c.Compute.Box]
200 if VerifyReport(ctx, e, pub, f, bound, box.Enrollment, box.Cloud.InstanceID, now) == nil {
201 for _, p := range bound.ConsoleProcesses {
202 if p.ConsoleID == id && p.GuardFloorSHA256 == f.Digest && p.AllLaunchPathsGuarded {
203 allowStale = true
204 }
205 }
206 }
207 }
208 }
209 if err = verifyReport(ctx, e, pub, f, r, c.Enrollment, id, now, allowStale); err != nil {
210 return nil, err
211 }
212 reports["console:"+id] = r
213 }
214 if scope.boxRelocation {
215 local, err := e.LocalConsole(ctx) // box relocation supplies its actual running router here.
216 published, ok := reports["box:"+scope.StarterBox]
217 if err != nil || !ok || local.Component != "router" || !local.HostClaimEnabled || local.Actor != scope.StarterBox || local.BootID != published.BootID {
218 return nil, ErrRoster
219 }
220 box := o.Roster.Boxes[scope.StarterBox]
221 if err := VerifyReport(ctx, e, pub, f, local, box.Enrollment, box.Cloud.InstanceID, now); err != nil {
222 return nil, err
223 }
224 return reports, nil
225 }
226 own, ok := o.Roster.Consoles[scope.ConsoleID]
227 if !ok {
228 return nil, ErrRoster
229 }
230 local, err := e.LocalConsole(ctx)
231 if err != nil {
232 return nil, err
233 }
234 published, ok := reports["console:"+scope.ConsoleID]
235 if !ok || local.Component != "console" || !local.HostClaimEnabled || local.Actor != scope.ConsoleID || local.ProcessID == "" || local.ProcessID != published.ProcessID {
236 return nil, ErrRoster
237 }
238 if _, err := verifyCompute(ctx, o, e, scope.ConsoleID, own, local); err != nil {
239 return nil, err
240 }
241 if err = VerifyReport(ctx, e, pub, f, local, own.Enrollment, scope.ConsoleID, now); err != nil {
242 return nil, err
243 }
244 return reports, nil
245}
246func (d Directory) Reserve(ctx context.Context, sg release.Signer, o Observed, h Reservation, activation bool) (Observed, error) {
247 r := Clone(o.Roster)
248 if h.ID == "" || len(h.Actors) == 0 || len(h.Releases) != len(h.Actors) {
249 return Observed{}, ErrRoster
250 }
251 if _, exists := r.Holds[h.ID]; exists {
252 return Observed{}, ErrRoster
253 }
254 if _, exists := r.Activations[h.ID]; exists {
255 return Observed{}, ErrRoster
256 }
257 if activation {
258 if len(h.Hashes) != len(h.Actors) {
259 return Observed{}, ErrRoster
260 }
261 for key := range h.Actors {
262 if len(h.Hashes[key]) != 3 {
263 return Observed{}, ErrRoster
264 }
265 }
266 for _, other := range r.Holds {
267 if overlaps(h, other) {
268 return Observed{}, ErrRoster
269 }
270 }
271 for _, other := range r.Activations {
272 if h.Publication != nil && other.Publication != nil && h.Publication.Component == other.Publication.Component && h.Publication.Channel == other.Publication.Channel {
273 return Observed{}, ErrRoster
274 }
275 if overlaps(h, other) {
276 return Observed{}, ErrRoster
277 }
278 }
279 r.Activations[h.ID] = h
280 } else {
281 if h.Host == "" || h.OpID == "" {
282 return Observed{}, ErrRoster
283 }
284 for _, other := range r.Activations {
285 if h.Publication != nil && other.Publication != nil && h.Publication.Component == other.Publication.Component && h.Publication.Channel == other.Publication.Channel {
286 return Observed{}, ErrRoster
287 }
288 if overlaps(h, other) {
289 return Observed{}, ErrRoster
290 }
291 }
292 r.Holds[h.ID] = h
293 }
294 return d.Replace(ctx, sg, o, r)
295}
296func (d Directory) CheckBoxStart(ctx context.Context, refs ...string) (uint64, error) {
297 o, err := d.Read(ctx)
298 if err != nil {
299 return 0, err
300 }
301 if len(refs) == 0 {
302 return 0, ErrRoster
303 }
304 for _, ref := range refs {
305 b, ok := o.Roster.Boxes[ref]
306 if !ok || b.State != "active" {
307 return 0, ErrRoster
308 }
309 fenced, err := d.TerminationFence(ctx, o, ref, time.Now())
310 if err != nil || fenced {
311 return 0, ErrRoster
312 }
313 for _, a := range o.Roster.Activations {
314 for _, enrollment := range a.Actors {
315 if enrollment == b.Enrollment {
316 return 0, ErrRoster
317 }
318 }
319 }
320 }
321 return o.Roster.Revision, nil
322}
323
324// ExactEvidence adapts the existing private status object without changing
325// the public status shape. The supplied readers must use exact private keys.
326type ExactEvidence struct {
327 PublicKey ed25519.PublicKey
328 Store release.Store
329 Boxes func(context.Context, string, Box) (Report, error)
330 Self func(context.Context) (Report, error)
331}
332
333func (e ExactEvidence) BoxReport(ctx context.Context, id string, b Box) (Report, error) {
334 if e.Boxes == nil {
335 return Report{}, ErrRoster
336 }
337 return e.Boxes(ctx, id, b)
338}
339func (e ExactEvidence) LocalConsole(ctx context.Context) (Report, error) {
340 if e.Self == nil {
341 return Report{}, ErrRoster
342 }
343 return e.Self(ctx)
344}
345func (e ExactEvidence) ConsoleReport(ctx context.Context, id string, c Console) (Report, error) {
346 if !segment(id) {
347 return Report{}, ErrRoster
348 }
349 raw, _, err := e.Store.Get(ctx, "status/consoles/"+id+".json")
350 if err != nil {
351 return Report{}, err
352 }
353 var r Report
354 if err = json.Unmarshal(raw, &r); err != nil {
355 return Report{}, err
356 }
357 return r, nil
358}
359func (e ExactEvidence) Identity(ctx context.Context, tag string) ([]byte, error) {
360 key, err := release.IdentityKey(tag)
361 if err != nil {
362 return nil, err
363 }
364 raw, _, err := e.Store.Get(ctx, key)
365 return raw, err
366}
367
368func (e ExactEvidence) Fence(ctx context.Context, o Observed, ref string, now time.Time) (bool, error) {
369 return (Directory{Store: e.Store, PublicKey: e.PublicKey, Bucket: o.Roster.DeploymentBucket}).TerminationFence(ctx, o, ref, now)
370}
371func validateScope(r Roster, scope Scope) error {
372 for _, name := range scope.Regions {
373 region, ok := r.Regions[name]
374 if !ok || region.Account == "" || region.AWSRegion == "" || !slices.Contains(region.Namespaces, scope.Namespace) {
375 return ErrRoster
376 }
377 for _, pool := range scope.Pools {
378 if !slices.Contains(region.Pools, pool) {
379 return ErrRoster
380 }
381 }
382 }
383 return nil
384}
385func verifyCompute(ctx context.Context, o Observed, e Evidence, id string, c Console, r Report) (Report, error) {
386 if r.Component != "console" || r.Actor != id || r.Enrollment != c.Enrollment || r.InstanceID != id || r.ProcessID == "" {
387 return Report{}, ErrRoster
388 }
389 if c.Compute == nil {
390 if r.Compute != nil {
391 return Report{}, ErrRoster
392 }
393 return Report{}, nil
394 }
395 binding := *c.Compute
396 box, ok := o.Roster.Boxes[binding.Box]
397 if !ok || binding.Enrollment != box.Enrollment || r.Compute == nil || *r.Compute != binding {
398 return Report{}, ErrRoster
399 }
400 br, err := e.BoxReport(ctx, binding.Box, box)
401 if err != nil {
402 return Report{}, err
403 }
404 if br.Component != "router" || br.Actor != binding.Box || br.Enrollment != box.Enrollment || br.InstanceID != box.Cloud.InstanceID || br.BootID == "" || br.BootID != r.BootID {
405 return Report{}, ErrRoster
406 }
407 matches := 0
408 for _, p := range br.ConsoleProcesses {
409 if p.ConsoleID != id {
410 continue
411 }
412 matches++
413 if p.Enrollment != c.Enrollment || p.ProcessID == "" || p.ProcessID != r.ProcessID || p.BinarySHA256 != r.BinarySHA256 {
414 return Report{}, ErrRoster
415 }
416 }
417 if matches != 1 {
418 return Report{}, ErrRoster
419 }
420 return br, nil
421}
422
423// CensusBoxRelocation is the only entry that grants L20's freshness exception.
424// The caller must be the local relocation controller; it cannot accept a member request.
425func (d Directory) CensusBoxRelocation(ctx context.Context, e Evidence, f hostfloor.Verified, scope Scope, now time.Time) (map[string]Report, error) {
426 if scope.StarterBox == "" {
427 return nil, ErrRoster
428 }
429 refs := append([]string{scope.StarterBox}, scope.Participants...)
430 revision, err := d.CheckBoxStart(ctx, refs...)
431 if err != nil {
432 return nil, err
433 }
434 o, err := d.Read(ctx)
435 if err != nil || o.Roster.Revision != revision {
436 return nil, ErrRoster
437 }
438 scope.boxRelocation = true
439 scope.ConsoleID = ""
440 scope.Participants = refs
441 return Census(ctx, o, e, d.PublicKey, f, scope, now)
442}
443