carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
4 "bytes"
5 "context"
6 "crypto/ed25519"
7 "crypto/sha256"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "os"
13 "sort"
14
15 "github.com/carlosframework/platform/internal/hostactors"
16 "github.com/carlosframework/platform/internal/release"
17)
18
19// Publish freezes exact report bytes into #232's already-fenced backfill.
20// The collecting manifest is the global create/record-writer freeze. Its ETag
21// is supplied by the #232 coordinator, which retains that freeze through done.
22func (b *Backfill) Publish(ctx context.Context, signer release.Signer, collectingETag string) error {
23 raw, tag, err := b.Store.Get(ctx, BackfillKey)
24 if err != nil {
25 return err
26 }
27 var p BackfillPlan
28 if tag != collectingETag || tag == "" || release.VerifyRootEnvelope(b.Directory.PublicKey, raw, &p) != nil || p.V != 1 || p.Phase != "collecting" || p.ID == "" || p.CreatedAt.IsZero() || p.Hosts == nil {
29 return ErrContract
30 }
31 a := ProductionAuthority{Store: b.Store, Directory: b.Directory}
32 census, err := a.census(ctx, "", "", p.ID, p.DirectoryRevision, nil)
33 if err != nil {
34 return err
35 }
36 p.Reports = map[string]string{}
37 for box, report := range census.Reports {
38 if report.CapturedAt.Before(p.CreatedAt) {
39 return ErrIdentity
40 }
41 key, err := reportKey(p.ID, box)
42 if err != nil {
43 return err
44 }
45 body, _, err := b.Store.Get(ctx, key)
46 if err != nil {
47 return err
48 }
49 var envelope CensusEnvelope
50 if json.Unmarshal(body, &envelope) != nil || envelope.Report.Reservation != report.Reservation || !envelope.Report.Complete {
51 return ErrIdentity
52 }
53 sum := sha256.Sum256(body)
54 p.Reports[box] = hex.EncodeToString(sum[:])
55 }
56 // No activation over unfinished seed state (seeded-instances; merge
57 // review S2): with claims on nothing can commit, abort or finish it.
58 // Checked here, under the collecting freeze, on the census just read.
59 if err := activationSeedRefusal(census); err != nil {
60 return err
61 }
62 for _, r := range census.Records {
63 if err := CheckHostQuarantine(ctx, b.Store, r.Holder.Host); err != nil {
64 return err
65 }
66 selected, ok := p.Hosts[r.Holder.Host]
67 if !ok || selected.Key != r.Holder.Key || selected.ETag != r.ETag || selected.Acquisition == "" || r.Holder.Acquisition != selected.Acquisition {
68 return ErrIdentity
69 }
70 // Each host's box and region come from the signed census, never from
71 // a typed list: the one box whose report names the host, and that
72 // box's enrolled region. Adoption checks the same ownership again.
73 // A host no box reports keeps no box, so it is never adopted. A box
74 // or region the collecting plan already named must agree.
75 var owners []string
76 for ref, report := range census.Reports {
77 for _, i := range report.Installations {
78 if i.Host == r.Holder.Host {
79 owners = append(owners, ref)
80 }
81 }
82 }
83 if len(owners) > 1 {
84 return ErrIdentity
85 }
86 if len(owners) == 1 {
87 enrolled := census.Roster.Roster.Boxes[owners[0]]
88 if (selected.Box != "" && selected.Box != owners[0]) || (selected.Region != "" && selected.Region != enrolled.Region) || enrolled.Region == "" {
89 return ErrIdentity
90 }
91 selected.Box, selected.Region = owners[0], enrolled.Region
92 p.Hosts[r.Holder.Host] = selected
93 }
94 }
95 if len(p.Hosts) != len(census.Records) {
96 return ErrIdentity
97 }
98 p.Phase = "adopting"
99 body, err := release.SignRootEnvelope(signer, p)
100 if err != nil {
101 return err
102 }
103 _, err = b.Store.Put(ctx, BackfillKey, body, tag)
104 return err
105}
106
107// Backfill.Done's refusals, one per condition. They are diagnostics: the
108// claims run phase puts its own refusal in front of the ones an operator
109// acts on (the plan, the records, the installations).
110var (
111 ErrDoneETag = fmt.Errorf("%w: backfill done: the plan's etag is not the collecting plan's", ErrContract)
112 ErrDoneEnvelope = fmt.Errorf("%w: backfill done: the plan does not verify", ErrContract)
113 ErrDonePhase = fmt.Errorf("%w: backfill done: the plan is not collecting", ErrContract)
114 ErrDoneRecords = fmt.Errorf("%w: backfill done: the record listing is not the plan's", ErrIdentity)
115 ErrDoneClaims = fmt.Errorf("%w: backfill done: control/hosts/ lists a claim", ErrIdentity)
116 ErrDoneAdmission = fmt.Errorf("%w: backfill done: an app admission exists", ErrIdentity)
117 ErrDoneReport = fmt.Errorf("%w: backfill done: a keyed box's census report does not verify", ErrIdentity)
118 ErrDoneStale = fmt.Errorf("%w: backfill done: a census report was captured before the plan", ErrIdentity)
119 ErrDoneInstallations = fmt.Errorf("%w: backfill done: a census report lists installations", ErrIdentity)
120 // ErrDoneRecordsChanged is Done's third listing failing after the write:
121 // done is written, over a record set that is no longer the plan's. It
122 // wraps ErrDoneRecords, so the claims run puts the records refusal in
123 // front of it.
124 ErrDoneRecordsChanged = fmt.Errorf("%w: done was written over a changed record set", ErrDoneRecords)
125)
126
127// Done closes an empty cohort's collecting plan straight to done (claim
128// trial Option B, Paul's D1 of 2026-10-20). It adopts nothing, so it needs
129// no report from a box that cannot report: what it proves is that every box
130// that can report owns nothing, not that the others own everything else.
131//
132// Each of these is its own refusal, and a refusal writes nothing:
133// - the plan's etag is collectingETag;
134// - its root envelope verifies;
135// - it is collecting;
136// - the live record listing (Apps, Records) is the plan's hosts, by key,
137// etag and acquisition, both before the reports are read and again
138// immediately before the write;
139// - control/hosts/ lists nothing;
140// - no app has an admission.json;
141// - every roster box with a census key has a report for the reservation
142// that verifies exactly as the census verifies it, captured no earlier
143// than the plan, listing no installation.
144//
145// Boxes with no census key are skipped and returned as unreported. The plan
146// is then written as done under If-Match, with the reporting boxes' digests,
147// read back, and CheckCreateFreeze must accept it. The records are listed a
148// third time after that; a change there is ErrDoneRecordsChanged, with done
149// already written.
150func (b *Backfill) Done(ctx context.Context, signer release.Signer, collectingETag string) ([]string, error) {
151 raw, tag, err := b.Store.Get(ctx, BackfillKey)
152 if err != nil {
153 return nil, err
154 }
155 if tag == "" || tag != collectingETag {
156 return nil, ErrDoneETag
157 }
158 var p BackfillPlan
159 if release.VerifyRootEnvelope(b.Directory.PublicKey, raw, &p) != nil || p.V != 1 || !safeSegment(p.ID) || p.CreatedAt.IsZero() || p.Hosts == nil {
160 return nil, ErrDoneEnvelope
161 }
162 if p.Phase != "collecting" {
163 return nil, ErrDonePhase
164 }
165 roster, err := b.Directory.Read(ctx)
166 if err != nil {
167 return nil, err
168 }
169 if roster.Roster.Revision != p.DirectoryRevision {
170 return nil, ErrPending
171 }
172
173 // The live record listing, as the census and collect list it.
174 apps, records, err := b.doneRecords(ctx, p.Hosts)
175 if err != nil {
176 return nil, err
177 }
178
179 // No host claim, under the prefix the migration census lists.
180 claims, err := b.Store.List(ctx, "control/hosts/")
181 if err != nil {
182 return nil, err
183 }
184 if len(claims) != 0 {
185 return nil, ErrDoneClaims
186 }
187
188 // No app admission (apps/<account>/<app>/admission.json).
189 for _, app := range apps {
190 key, err := AdmissionKey(app.Account, app.Name)
191 if err != nil {
192 return nil, err
193 }
194 if _, _, err := b.Store.Get(ctx, key); err == nil {
195 return nil, ErrDoneAdmission
196 } else if !errors.Is(err, os.ErrNotExist) {
197 return nil, err
198 }
199 }
200
201 // Every keyed box reports, and reports nothing.
202 refs := make([]string, 0, len(roster.Roster.Boxes))
203 for ref := range roster.Roster.Boxes {
204 refs = append(refs, ref)
205 }
206 sort.Strings(refs)
207 unreported := []string{}
208 p.Reports = map[string]string{}
209 for _, ref := range refs {
210 box := roster.Roster.Boxes[ref]
211 if len(box.CensusPublicKey) == 0 {
212 unreported = append(unreported, ref)
213 continue
214 }
215 key, err := reportKey(p.ID, ref)
216 if err != nil {
217 return nil, err
218 }
219 body, reportTag, err := b.Store.Get(ctx, key)
220 if err != nil {
221 return nil, errors.Join(ErrDoneReport, err)
222 }
223 report, err := verifyCensusReport(body, reportTag, ref, box, p.ID, roster.Roster.Revision, records)
224 if err != nil {
225 return nil, errors.Join(ErrDoneReport, err)
226 }
227 if report.CapturedAt.Before(p.CreatedAt) {
228 return nil, ErrDoneStale
229 }
230 if len(report.Installations) != 0 {
231 return nil, ErrDoneInstallations
232 }
233 sum := sha256.Sum256(body)
234 p.Reports[ref] = hex.EncodeToString(sum[:])
235 }
236 // A done with no report at all would prove nothing.
237 if len(p.Reports) == 0 {
238 return nil, ErrDoneReport
239 }
240 fresh, err := b.Directory.Read(ctx)
241 if err != nil {
242 return nil, err
243 }
244 if fresh.ETag != roster.ETag {
245 return nil, ErrPending
246 }
247
248 p.Phase = "done"
249 body, err := release.SignRootEnvelope(signer, p)
250 if err != nil {
251 return nil, err
252 }
253 // The second listing, after every report was fetched and verified and
254 // immediately before the write: a record created while the reports were
255 // read is refused here, and nothing is written. The plan's If-Match
256 // covers only the plan, so this narrows the window; the trial closes it
257 // by stopping the console's record writes for the window.
258 if _, _, err := b.doneRecords(ctx, p.Hosts); err != nil {
259 return nil, err
260 }
261 if _, err = b.Store.Put(ctx, BackfillKey, body, tag); err != nil {
262 return nil, err
263 }
264 got, _, err := b.Store.Get(ctx, BackfillKey)
265 if err != nil {
266 return nil, err
267 }
268 if !bytes.Equal(got, body) {
269 return nil, ErrContract
270 }
271 if err := CheckCreateFreeze(ctx, b.Store, b.Directory.PublicKey); err != nil {
272 return nil, err
273 }
274 // The third listing, after the write and its readback: a record that
275 // appeared between the second listing and now is not in the done plan,
276 // so done was written over a changed record set. Say so; the write
277 // cannot be taken back here.
278 if _, _, err := b.doneRecords(ctx, p.Hosts); err != nil {
279 if errors.Is(err, ErrDoneRecords) {
280 return nil, ErrDoneRecordsChanged
281 }
282 return nil, err
283 }
284 return unreported, nil
285}
286
287// doneRecords lists the live records as collect and the census list them
288// (Apps, then Records per app) and refuses, with ErrDoneRecords, unless
289// they are hosts by key, etag and acquisition, no more and no fewer. Done
290// runs it before the reports, again immediately before its write and once
291// more after it.
292func (b *Backfill) doneRecords(ctx context.Context, hosts map[string]BackfillHost) ([]App, map[string]Record, error) {
293 apps, err := Apps(ctx, b.Store)
294 if err != nil {
295 return nil, nil, err
296 }
297 records := map[string]Record{}
298 for _, app := range apps {
299 rs, err := Records(ctx, b.Store, app.Account, app.Name)
300 if err != nil {
301 return nil, nil, err
302 }
303 for _, r := range rs {
304 if _, dup := records[r.Holder.Host]; dup {
305 return nil, nil, ErrDoneRecords
306 }
307 records[r.Holder.Host] = r
308 }
309 }
310 if len(records) != len(hosts) {
311 return nil, nil, ErrDoneRecords
312 }
313 for host, r := range records {
314 frozen, ok := hosts[host]
315 if !ok || frozen.Key != r.Holder.Key || frozen.ETag != r.ETag || frozen.Acquisition == "" || frozen.Acquisition != r.Holder.Acquisition {
316 return nil, nil, ErrDoneRecords
317 }
318 }
319 return apps, records, nil
320}
321
322// verifyCensusReport is the census's acceptance of one roster box's report
323// for the reservation, for Backfill.Done: the envelope verifies under the
324// box's enrolled census key, the report is complete and names this box,
325// enrollment, reservation and roster revision, and every installation names
326// a listed record, once. It is ProductionAuthority.census's per-box check
327// line for line; census is an accepted-legacy unit and is left untouched,
328// and TestDoneVerifiesReportsAsTheCensusDoes holds the two together.
329func verifyCensusReport(raw []byte, tag, ref string, box hostactors.Box, reservation string, revision uint64, records map[string]Record) (CensusReport, error) {
330 var envelope CensusEnvelope
331 if decode(raw, &envelope) != nil {
332 return CensusReport{}, ErrIdentity
333 }
334 report := envelope.Report
335 canonical, e := json.Marshal(report)
336 if e != nil || len(box.CensusPublicKey) != ed25519.PublicKeySize || !ed25519.Verify(box.CensusPublicKey, canonical, envelope.Signature) {
337 return CensusReport{}, ErrIdentity
338 }
339 if tag == "" || !report.Complete || report.Box != ref || report.Enrollment != box.Enrollment || report.Reservation != reservation || report.DirectoryRevision != revision || report.BootID == "" || report.CapturedAt.IsZero() || report.Installations == nil {
340 return CensusReport{}, ErrIdentity
341 }
342 seen := map[string]bool{}
343 for _, i := range report.Installations {
344 if i.Host == "" || seen[i.Host] || (!i.Route && !i.Local) {
345 return CensusReport{}, ErrIdentity
346 }
347 seen[i.Host] = true
348 r, ok := records[i.Host]
349 if !ok || i.Account != r.Holder.Account || i.App != r.Holder.App || i.Key != r.Holder.Key || i.Family != r.Holder.Family {
350 return CensusReport{}, ErrIdentity
351 }
352 inc, err := r.Incarnation()
353 if err != nil || (i.Incarnation != "" && i.Incarnation != inc) {
354 return CensusReport{}, ErrIdentity
355 }
356 }
357 return report, nil
358}
359
360// VerifyAdopting is the adopters' own acceptance of a published plan, run
361// by the signer as readback after Publish: the signed plan reads adopting,
362// and the complete census it froze verifies against its report digests, as
363// Adoption and AuthorizeBackfillApp require. It writes nothing.
364func (b *Backfill) VerifyAdopting(ctx context.Context) (BackfillPlan, error) {
365 p, tag, err := b.read(ctx)
366 if err != nil {
367 return p, err
368 }
369 a := ProductionAuthority{Store: b.Store, Directory: b.Directory}
370 if _, err = a.census(ctx, "", "", p.ID, p.DirectoryRevision, p.Reports); err != nil {
371 return p, err
372 }
373 _, fresh, err := b.read(ctx)
374 if err != nil {
375 return p, err
376 }
377 if fresh != tag {
378 return p, ErrPending
379 }
380 return p, nil
381}
382
383// PublishCensusReport is called by the guarded box census collector. Each box
384// owns its private signing key; the root-signed roster binds its public key to
385// verified enrollment. The console has only that public key.
386func PublishCensusReport(ctx context.Context, store release.Store, d hostactors.Directory, report CensusReport, private ed25519.PrivateKey) error {
387 if len(private) != ed25519.PrivateKeySize {
388 return ErrContract
389 }
390 roster, err := d.Read(ctx)
391 if err != nil {
392 return err
393 }
394 box, ok := roster.Roster.Boxes[report.Box]
395 if !ok || report.Enrollment != box.Enrollment || report.DirectoryRevision != roster.Roster.Revision || !bytes.Equal(private.Public().(ed25519.PublicKey), box.CensusPublicKey) || report.BootID == "" || !report.Complete || report.Installations == nil || report.CapturedAt.IsZero() {
396 return ErrIdentity
397 }
398 canonical, err := json.Marshal(report)
399 if err != nil {
400 return err
401 }
402 raw, err := json.Marshal(CensusEnvelope{Report: report, Signature: ed25519.Sign(private, canonical)})
403 if err != nil {
404 return err
405 }
406 key, err := reportKey(report.Reservation, report.Box)
407 if err != nil {
408 return err
409 }
410 if _, err = store.Put(ctx, key, raw, ""); err == nil {
411 return nil
412 }
413 if !errors.Is(err, release.ErrCASConflict) {
414 return err
415 }
416 previous, _, err := store.Get(ctx, key)
417 if err != nil {
418 return err
419 }
420 if !bytes.Equal(previous, raw) {
421 return ErrIdentity
422 }
423 return nil
424}
425
426// AuthorizeBackfillApp is the only legacy app-admission initialization proof.
427// It accepts the same complete frozen census as host adoption, including apps
428// with no host records. Empty results from a failed listing never qualify.
429func (a ProductionAuthority) AuthorizeBackfillApp(ctx context.Context, account, app string) (string, error) {
430 if a.Backfill == nil || !safeSegment(account) || !safeSegment(app) {
431 return "", ErrContract
432 }
433 p, tag, err := a.Backfill.read(ctx)
434 if err != nil {
435 return "", err
436 }
437 census, err := a.census(ctx, account, app, p.ID, p.DirectoryRevision, p.Reports)
438 if err != nil {
439 return "", err
440 }
441 apps, err := Apps(ctx, a.Store)
442 if err != nil {
443 return "", err
444 }
445 found := false
446 for _, candidate := range apps {
447 if candidate.Account == account && candidate.Name == app {
448 found = true
449 }
450 }
451 if !found {
452 return "", ErrIdentity
453 }
454 for _, r := range census.Records {
455 planned, ok := p.Hosts[r.Holder.Host]
456 if !ok || planned.Key != r.Holder.Key || planned.ETag != r.ETag || planned.Acquisition == "" || planned.Acquisition != r.Holder.Acquisition {
457 return "", ErrIdentity
458 }
459 if err = CheckHostQuarantine(ctx, a.Store, r.Holder.Host); err != nil {
460 return "", err
461 }
462 }
463 _, fresh, err := a.Backfill.read(ctx)
464 if err != nil {
465 return "", err
466 }
467 if fresh != tag {
468 return "", ErrPending
469 }
470 return p.ID, nil
471}
472