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 "bytes"
5 "context"
6 "crypto/ed25519"
7 "crypto/sha256"
8 "database/sql"
9 "encoding/hex"
10 "encoding/json"
11 "errors"
12 "fmt"
13 "strconv"
14 "strings"
15 "time"
16
17 "github.com/carlosframework/platform/internal/hostbox"
18 "github.com/carlosframework/platform/internal/hostclaim"
19 "github.com/carlosframework/platform/internal/hostevidence"
20 "github.com/carlosframework/platform/internal/hostguard"
21 "github.com/carlosframework/platform/internal/release"
22)
23
24var (
25 ErrClaimStopUnavailable = errors.New("claim stop: required root provider unavailable")
26 ErrClaimStopIdentity = errors.New("claim stop: authenticated identity mismatch")
27 ErrClaimStopInventory = errors.New("claim stop: inventory does not match request")
28 ErrClaimStopReceipt = errors.New("claim stop: immutable receipt conflict")
29)
30
31type StopPeer struct {
32 UID uint32
33 PID int
34 ProcessStart string
35 BootID string
36 ControllerID string
37}
38
39type StopPeerProvider interface {
40 AuthenticatedPeer(context.Context) (StopPeer, error)
41}
42
43type StopInventoryEntry = hostbox.StopInventoryEntry
44type StopInventory = hostbox.StopInventory
45
46type StopInventoryReader interface {
47 ReadStopInventory(context.Context, string) (StopInventory, error)
48}
49
50type StopRequest struct {
51 Region string
52 Cleanup bool
53 // Recover names a launch tuple journaled by an earlier controller process
54 // or boot. The live controller asks; root proves that controller dead.
55 Recover bool
56 Host, Incarnation string
57 InstallationStamp, WriterSession uint64
58 OperationID, Phase, ObligationID string
59 Box, Enrollment string
60 BootID, ControllerID string
61 ExpectedClaimETag string
62 ExpectedDirectoryRevision string
63 // RosterRevision is the roster write counter of the roster the requester
64 // read. Root persists it in the fence target; it becomes the receipt's
65 // directory_revision numeral (roster binding N1).
66 RosterRevision uint64
67}
68
69type StopClaimReader interface {
70 Read(context.Context, string) (hostclaim.Observed, error)
71}
72
73type DrainAcknowledgement struct {
74 Completed hostbox.ProducerCompletion `json:",omitzero"`
75 ProducerIDs []string
76 FenceID string
77 ControllerID string
78 ProcessStart string
79 Digest string
80}
81
82type StopDrainer interface {
83 DrainClaim(context.Context, StopRequest, string) (DrainAcknowledgement, error)
84}
85
86type StopKernelEvidence struct {
87 ObservedBootID string
88 InventorySHA256 string
89 LocalJournalSHA256 string
90 CgroupIdentities []string
91 UnitInvocationIDs []string
92}
93
94type StopKernelVerifier interface {
95 VerifyStopped(context.Context, hostguard.FenceTarget, StopInventory, DrainAcknowledgement) (StopKernelEvidence, error)
96}
97
98// StopReceipt is root's signed stop evidence. DirectoryRevision is the
99// roster write counter, a numeral, taken from the fence target's persisted
100// RosterRevision: the v0.28.4 console bounds it by 0 < n <= roster.Revision.
101// Binding is the installation's binding; box-side receipt comparisons use it
102// (roster binding N1, R1).
103type StopReceipt struct {
104 RequestLimitNS int64 `json:"request_limit_ns"`
105 Region string `json:"region,omitempty"`
106 ClaimETag string `json:"claim_etag,omitempty"`
107 ObservedBootID string `json:"observed_boot_id,omitempty"`
108 V int `json:"v"`
109 ID string `json:"id"`
110 Host string `json:"host"`
111 Incarnation string `json:"incarnation"`
112 InstallationStamp uint64 `json:"installation_stamp"`
113 WriterSession uint64 `json:"writer_session"`
114 OperationID string `json:"operation_id"`
115 Phase string `json:"phase"`
116 ObligationID string `json:"obligation_id"`
117 Box string `json:"box"`
118 Enrollment string `json:"enrollment"`
119 BootID string `json:"boot_id"`
120 ControllerID string `json:"controller_id"`
121 ClaimGen uint64 `json:"claim_gen"`
122 DirectoryRevision string `json:"directory_revision"`
123 Binding string `json:"binding,omitempty"`
124 Kind string `json:"kind"`
125 EvidenceAt time.Time `json:"evidence_at"`
126 FenceID string `json:"fence_id"`
127 InventorySHA256 string `json:"inventory_sha256"`
128 LocalJournalSHA256 string `json:"local_journal_sha256"`
129 ProducerDrainDigest string `json:"producer_drain_digest"`
130 CgroupIdentities []string `json:"cgroup_identities"`
131 UnitInvocationIDs []string `json:"unit_invocation_ids"`
132}
133
134type StopResult struct {
135 Target hostguard.FenceTarget
136 Key string
137 Digest string
138 Bytes []byte
139 EvidenceAt time.Time
140}
141
142type ClaimStopCoordinator struct {
143 RetainNoProcess func(context.Context, hostclaim.Observed, StopResult) error
144 // VerifiedRosterRevision is the write counter of the roster root reads
145 // and verifies itself at the moment of the call, quarantined or not.
146 // It bounds an agent-sent RosterRevision before any fence or receipt
147 // takes it (boundRosterRevision); nil refuses every stop.
148 VerifiedRosterRevision func(context.Context) (uint64, error)
149 EvidenceSigner ed25519.PrivateKey
150 RequestLimit time.Duration
151 Guard hostguard.Guard
152 Claims StopClaimReader
153 Peers StopPeerProvider
154 Inventory StopInventoryReader
155 Drainer StopDrainer
156 Kernel StopKernelVerifier
157 Evidence release.Store
158 CarlosUID uint32
159 NewID func() string
160 Now func() time.Time
161}
162
163func stopComponent(s string) bool {
164 return s != "" && s != "." && s != ".." && !strings.ContainsAny(s, "/\\\x00")
165}
166
167func stopDigest(s string) bool {
168 if len(s) != sha256.Size*2 {
169 return false
170 }
171 _, err := hex.DecodeString(s)
172 return err == nil
173}
174
175func (c ClaimStopCoordinator) validateRequest(r StopRequest) error {
176 if r.WriterSession == 0 {
177 return ErrClaimStopUnavailable
178 }
179 return c.validateShape(r)
180}
181
182// validateShape is validateRequest without the session: the no-process
183// witness takes writer session 0.
184func (c ClaimStopCoordinator) validateShape(r StopRequest) error {
185 if len(c.EvidenceSigner) != ed25519.PrivateKeySize || c.RequestLimit != hostbox.RequestLimit {
186 return ErrClaimStopUnavailable
187 }
188 if c.CarlosUID == 0 || c.Peers == nil || c.Claims == nil || c.Inventory == nil || c.Drainer == nil || c.Kernel == nil || c.Evidence == nil || c.NewID == nil || c.Now == nil ||
189 !stopComponent(r.Host) || !stopOperationTuple(r.OperationID, r.Phase, r.ObligationID) || !stopComponent(r.Box) ||
190 !stopComponent(r.Enrollment) || r.Incarnation == "" || r.InstallationStamp == 0 ||
191 r.BootID == "" || r.ControllerID == "" || r.ExpectedClaimETag == "" || r.ExpectedDirectoryRevision == "" || r.RosterRevision == 0 {
192 return ErrClaimStopUnavailable
193 }
194 return nil
195}
196
197// boundRosterRevision refuses an agent-sent receipt numeral outside
198// 0 < n <= the revision of the roster root reads and verifies now (fix
199// round 1, finding 3), the bound the v0.28.4 console verifiers apply
200// against the roster they read. Roster revisions only grow, so a numeral
201// inside it now stays inside it for every later verifier. It runs before a
202// fence is begun or a no-process receipt is made; a retry that resumes a
203// fence still keeps the fence's original numeral (B2).
204func (c ClaimStopCoordinator) boundRosterRevision(ctx context.Context, n uint64) error {
205 if c.VerifiedRosterRevision == nil {
206 return ErrClaimStopUnavailable
207 }
208 current, err := c.VerifiedRosterRevision(ctx)
209 if err != nil {
210 return err
211 }
212 if n == 0 || n > current {
213 return ErrClaimStopIdentity
214 }
215 return nil
216}
217
218// stopReceiptNumeral is a receipt's directory_revision, read from the one
219// persisted source: the fence target's RosterRevision. A target without one
220// (written before roster binding) cannot produce a receipt.
221func stopReceiptNumeral(t hostguard.FenceTarget) (string, error) {
222 if t.RosterRevision == 0 {
223 return "", ErrClaimStopReceipt
224 }
225 return strconv.FormatUint(t.RosterRevision, 10), nil
226}
227
228// receiptRosterRevision parses a receipt's directory_revision numeral: a
229// canonical non-zero decimal.
230func receiptRosterRevision(s string) (uint64, bool) {
231 n, err := strconv.ParseUint(s, 10, 64)
232 if err != nil || n == 0 || strconv.FormatUint(n, 10) != s {
233 return 0, false
234 }
235 return n, true
236}
237
238// The receipt kinds: a stopped writer session, and the no-process witness
239// for an installation whose holder never admitted a writer (R4).
240const (
241 StopReceiptStopped = "stopped"
242 StopReceiptNoProcess = "no-process"
243)
244
245// stopNoProcessKernel is root's kernel half of the session-zero witness: no
246// launch journal entry for the host, its host containment empty or absent,
247// and no process holding any of the installation's files.
248type stopNoProcessKernel interface {
249 VerifyNoProcess(context.Context, hostguard.FenceTarget, StopInventory) (StopKernelEvidence, error)
250}
251
252func (c ClaimStopCoordinator) FenceAndStop(ctx context.Context, req StopRequest) (StopResult, error) {
253 if req.WriterSession == 0 {
254 return c.witnessNoProcess(ctx, req)
255 }
256 if ctx == nil || c.validateRequest(req) != nil || (req.Cleanup && req.Recover) {
257 return StopResult{}, ErrClaimStopUnavailable
258 }
259 var recovery stopRecoveryKernel
260 if req.Recover {
261 var ok bool
262 if recovery, ok = c.Kernel.(stopRecoveryKernel); !ok {
263 return StopResult{}, ErrClaimStopUnavailable
264 }
265 }
266 peer, err := c.Peers.AuthenticatedPeer(ctx)
267 if err != nil {
268 return StopResult{}, err
269 }
270 if peer.UID != c.CarlosUID || peer.PID <= 0 || peer.ProcessStart == "" || peer.BootID == "" || peer.ControllerID == "" {
271 return StopResult{}, ErrClaimStopIdentity
272 }
273 // Only recovery may name another tuple, and only for this box's session.
274 if !req.Recover && (peer.BootID != req.BootID || peer.ControllerID != req.ControllerID) {
275 return StopResult{}, ErrClaimStopIdentity
276 }
277 var observed hostclaim.Observed
278 var cleanupTarget *hostguard.FenceTarget
279 if req.Cleanup {
280 want := hostguard.FenceTarget{Region: req.Region, Host: req.Host, Incarnation: req.Incarnation, Stamp: req.InstallationStamp, Session: req.WriterSession, Op: req.OperationID, Phase: req.Phase, ObligationID: req.ObligationID, Box: req.Box, Enrollment: req.Enrollment, BootID: req.BootID, ControllerID: req.ControllerID, ClaimETag: req.ExpectedClaimETag, DirectoryRevision: req.ExpectedDirectoryRevision, RosterRevision: req.RosterRevision}
281 durable, err := c.Guard.ExistingFence(ctx, want)
282 if err != nil {
283 return StopResult{}, err
284 }
285 cleanupTarget = &durable
286 } else {
287 observed, err = c.Claims.Read(ctx, req.Host)
288 if err != nil {
289 return StopResult{}, err
290 }
291 if observed.Absent || observed.Claim == nil || observed.Claim.Holder == nil || observed.ETag != req.ExpectedClaimETag ||
292 observed.Claim.Host != req.Host || observed.Claim.Holder.Incarnation != req.Incarnation ||
293 observed.Claim.Holder.Stamp != req.InstallationStamp || observed.Claim.Holder.Session != req.WriterSession ||
294 !hostguard.StopAuthorityMatches(observed.Claim, hostguard.FenceTarget{Region: req.Region, Host: req.Host, Incarnation: req.Incarnation, Stamp: req.InstallationStamp, Session: req.WriterSession, Op: req.OperationID, Phase: req.Phase, ObligationID: req.ObligationID, Box: req.Box}) {
295 return StopResult{}, ErrClaimStopIdentity
296 }
297
298 }
299 var inv StopInventory
300 if cleanupTarget != nil {
301 reader, ok := c.Inventory.(exactStopInventory)
302 if !ok {
303 return StopResult{}, ErrClaimStopInventory
304 }
305 inv, err = reader.ReadExactStopInventory(ctx, *cleanupTarget)
306 } else {
307 inv, err = c.Inventory.ReadStopInventory(ctx, req.Host)
308 }
309 if err != nil {
310 return StopResult{}, err
311 }
312 if inv.Host != req.Host || inv.Incarnation != req.Incarnation || inv.InstallationStamp != req.InstallationStamp ||
313 inv.Box != req.Box || inv.Enrollment != req.Enrollment || inv.DirectoryRevision != req.ExpectedDirectoryRevision {
314 return StopResult{}, ErrClaimStopInventory
315 }
316 fenceID, receiptID := c.NewID(), c.NewID()
317 if !stopComponent(fenceID) || !stopComponent(receiptID) {
318 return StopResult{}, ErrClaimStopUnavailable
319 }
320 if err := c.boundRosterRevision(ctx, req.RosterRevision); err != nil {
321 return StopResult{}, err
322 }
323 gen := uint64(0)
324 if observed.Claim != nil {
325 gen = observed.Claim.Gen
326 }
327 target := hostguard.FenceTarget{Region: req.Region, ClaimGen: gen, ObligationID: req.ObligationID, FenceID: fenceID, Host: req.Host, Incarnation: req.Incarnation, Stamp: req.InstallationStamp,
328 Session: req.WriterSession, Op: req.OperationID, Phase: req.Phase, Box: req.Box, Enrollment: req.Enrollment,
329 BootID: req.BootID, ControllerID: req.ControllerID, ClaimETag: req.ExpectedClaimETag, DirectoryRevision: req.ExpectedDirectoryRevision, RosterRevision: req.RosterRevision}
330 if cleanupTarget != nil {
331 target = *cleanupTarget
332 err = c.Guard.ResumeFence(ctx, target)
333 } else {
334 target, err = c.Guard.BeginFence(ctx, target)
335 }
336 if err != nil {
337 return StopResult{}, err
338 }
339 fenceID = target.FenceID
340 // The receipt numeral comes from the persisted fence target, never from
341 // this request: a retry after roster writes resumes the original fence
342 // and its original numeral (B2). A fence persisted without one cannot
343 // produce a receipt and is refused before any drain or kill (N1).
344 numeral, err := stopReceiptNumeral(target)
345 if err != nil {
346 return StopResult{}, err
347 }
348 if durable, err := c.Guard.FenceReceiptFor(ctx, target); err == nil {
349 result, err := stopResultFromDurable(req, durable)
350 if err != nil {
351 return StopResult{}, err
352 }
353 if !req.Cleanup {
354 if err := c.Publish(ctx, result); err != nil {
355 return result, err
356 }
357 }
358 return result, nil
359 } else if !errors.Is(err, hostguard.ErrNoFenceReceipt) {
360 return StopResult{}, err
361 }
362 var ack DrainAcknowledgement
363 var kernel StopKernelEvidence
364 if req.Recover {
365 // Root reads the tuple's own launch record and proves its controller
366 // dead, or the boot gone, before it kills, verifies and settles. A
367 // missing record or a live controller refuses; the fence stays pending.
368 kernel, ack, err = c.recoverKernel(ctx, recovery, target, inv)
369 if err != nil {
370 return StopResult{}, err
371 }
372 if ack.FenceID != fenceID || ack.ControllerID != req.ControllerID || ack.ProcessStart == "" || !stopDigest(ack.Digest) ||
373 (req.BootID == peer.BootID && req.ControllerID == peer.ControllerID && ack.ProcessStart == peer.ProcessStart) {
374 return StopResult{}, ErrClaimStopIdentity
375 }
376 } else if absent, absentErr := c.verifyUnjournaled(ctx, target, inv, sql.ErrNoRows); absentErr == nil {
377 kernel = absent
378 ack = DrainClaimAcknowledgement(fenceID, req.ControllerID, peer.ProcessStart, nil, hostbox.ProducerCompletion{})
379 } else {
380 ack, err = c.Drainer.DrainClaim(ctx, req, fenceID)
381 if err != nil {
382 return StopResult{}, err
383 }
384 if ack.FenceID != fenceID || ack.ControllerID != req.ControllerID || ack.ProcessStart != peer.ProcessStart || !stopDigest(ack.Digest) {
385 return StopResult{}, ErrClaimStopIdentity
386 }
387 kernel, err = c.Kernel.VerifyStopped(ctx, target, inv, ack)
388 if err != nil {
389 kernel, err = c.verifyUnjournaled(ctx, target, inv, err)
390 }
391 if err != nil {
392 return StopResult{}, err
393 }
394 }
395 evidenceAt := c.Now().UTC()
396 if evidenceAt.IsZero() || !stopDigest(kernel.InventorySHA256) || !stopDigest(kernel.LocalJournalSHA256) || len(kernel.CgroupIdentities) == 0 {
397 return StopResult{}, ErrClaimStopReceipt
398 }
399 receipt := StopReceipt{RequestLimitNS: int64(c.RequestLimit), Region: req.Region, ClaimETag: target.ClaimETag, ObservedBootID: kernel.ObservedBootID, V: 1, ID: receiptID, Host: req.Host, Incarnation: req.Incarnation,
400 InstallationStamp: req.InstallationStamp, WriterSession: req.WriterSession, OperationID: req.OperationID,
401 Phase: req.Phase, ObligationID: req.ObligationID, Box: req.Box, Enrollment: req.Enrollment,
402 BootID: req.BootID, ControllerID: req.ControllerID, ClaimGen: target.ClaimGen,
403 DirectoryRevision: numeral, Binding: target.DirectoryRevision, Kind: StopReceiptStopped, EvidenceAt: evidenceAt, FenceID: fenceID,
404 InventorySHA256: kernel.InventorySHA256, LocalJournalSHA256: kernel.LocalJournalSHA256,
405 ProducerDrainDigest: ack.Digest, CgroupIdentities: append([]string(nil), kernel.CgroupIdentities...),
406 UnitInvocationIDs: append([]string(nil), kernel.UnitInvocationIDs...)}
407 body, err := json.Marshal(receipt)
408 if err != nil {
409 return StopResult{}, err
410 }
411 verify := func(_ context.Context, got hostguard.FenceTarget, exact []byte, at time.Time) error {
412 if got != target || !at.Equal(evidenceAt) || !bytes.Equal(exact, body) {
413 return ErrClaimStopReceipt
414 }
415 return nil
416 }
417 if err := c.Guard.CompleteFence(ctx, target, body, evidenceAt, verify); err != nil {
418 return StopResult{}, err
419 }
420 key := stopEvidenceKey(req.Host, req.OperationID, req.Box, receiptID)
421 sum := sha256.Sum256(body)
422 result := StopResult{Target: target, Key: key, Digest: hex.EncodeToString(sum[:]), Bytes: append([]byte(nil), body...), EvidenceAt: evidenceAt}
423 if !req.Cleanup {
424 if err := c.Publish(ctx, result); err != nil {
425 return result, err
426 }
427 }
428 return result, nil
429}
430
431// witnessNoProcess is the positive no-process witness for writer session 0
432// (claim Plan 11, R4): the holder never admitted a writer, root's launch
433// journal has no entry for the host, the host's containment is empty or
434// absent, and root's descriptor scan finds no holder of the installation's
435// files. No session ever ran, so there is no fence and no producer drain:
436// the receipt binds the claim's ETag, which any admission changes, and root
437// re-reads the claim after its observation. The receipt is published like
438// any stop receipt.
439func (c ClaimStopCoordinator) witnessNoProcess(ctx context.Context, req StopRequest) (StopResult, error) {
440 if ctx == nil || req.WriterSession != 0 || req.Cleanup || req.Recover || c.validateShape(req) != nil {
441 return StopResult{}, ErrClaimStopUnavailable
442 }
443 kernel, ok := c.Kernel.(stopNoProcessKernel)
444 if !ok {
445 return StopResult{}, ErrClaimStopUnavailable
446 }
447 peer, err := c.Peers.AuthenticatedPeer(ctx)
448 if err != nil {
449 return StopResult{}, err
450 }
451 if peer.UID != c.CarlosUID || peer.PID <= 0 || peer.ProcessStart == "" || peer.BootID != req.BootID || peer.ControllerID != req.ControllerID {
452 return StopResult{}, ErrClaimStopIdentity
453 }
454 observed, err := c.Claims.Read(ctx, req.Host)
455 if err != nil {
456 return StopResult{}, err
457 }
458 if observed.Absent || observed.Claim == nil || observed.Claim.Holder == nil || observed.ETag != req.ExpectedClaimETag || observed.Claim.Host != req.Host {
459 return StopResult{}, ErrClaimStopIdentity
460 }
461 claim, holder := observed.Claim, observed.Claim.Holder
462 if holder.Incarnation != req.Incarnation || holder.Stamp != req.InstallationStamp || holder.Session != 0 || (holder.WriterBox != "" && (c.RetainNoProcess == nil || holder.WriterBox != req.Box || claim.Op.Kind != hostclaim.KindServing)) || holder.Generation != "" || len(holder.Admissions) != 0 {
463 return StopResult{}, ErrClaimStopIdentity
464 }
465 target := hostguard.FenceTarget{Region: req.Region, ClaimGen: claim.Gen, ObligationID: req.ObligationID, Host: req.Host, Incarnation: req.Incarnation, Stamp: req.InstallationStamp,
466 Op: req.OperationID, Phase: req.Phase, Box: req.Box, Enrollment: req.Enrollment, BootID: req.BootID, ControllerID: req.ControllerID,
467 ClaimETag: req.ExpectedClaimETag, DirectoryRevision: req.ExpectedDirectoryRevision, RosterRevision: req.RosterRevision}
468 if claim.Op.Kind == hostclaim.KindServing {
469 if req.OperationID != "" || req.Region == "" || claim.Op.Exec != req.Box {
470 return StopResult{}, ErrClaimStopIdentity
471 }
472 } else if !hostguard.StopAuthorityMatches(claim, target) {
473 return StopResult{}, ErrClaimStopIdentity
474 }
475 if err := c.boundRosterRevision(ctx, req.RosterRevision); err != nil {
476 return StopResult{}, err
477 }
478 // No session ran, so there is no fence: the witness's own target is the
479 // receipt's one source, retained with the result.
480 numeral, err := stopReceiptNumeral(target)
481 if err != nil {
482 return StopResult{}, err
483 }
484 inv, err := c.Inventory.ReadStopInventory(ctx, req.Host)
485 if err != nil {
486 return StopResult{}, err
487 }
488 if inv.Host != req.Host || inv.Incarnation != req.Incarnation || inv.InstallationStamp != req.InstallationStamp ||
489 inv.Box != req.Box || inv.Enrollment != req.Enrollment || inv.DirectoryRevision != req.ExpectedDirectoryRevision {
490 return StopResult{}, ErrClaimStopInventory
491 }
492 kernelEvidence, err := kernel.VerifyNoProcess(ctx, target, inv)
493 if err != nil {
494 return StopResult{}, err
495 }
496 again, err := c.Claims.Read(ctx, req.Host)
497 if err != nil {
498 return StopResult{}, err
499 }
500 if again.ETag != req.ExpectedClaimETag {
501 return StopResult{}, ErrClaimStopIdentity
502 }
503 evidenceAt := c.Now().UTC()
504 receiptID := c.NewID()
505 if evidenceAt.IsZero() || !stopComponent(receiptID) || !stopDigest(kernelEvidence.InventorySHA256) || !stopDigest(kernelEvidence.LocalJournalSHA256) || len(kernelEvidence.CgroupIdentities) == 0 {
506 return StopResult{}, ErrClaimStopReceipt
507 }
508 receipt := StopReceipt{RequestLimitNS: int64(c.RequestLimit), Region: req.Region, ClaimETag: req.ExpectedClaimETag, ObservedBootID: kernelEvidence.ObservedBootID, V: 1, ID: receiptID, Host: req.Host,
509 Incarnation: req.Incarnation, InstallationStamp: req.InstallationStamp, OperationID: req.OperationID, Phase: req.Phase, ObligationID: req.ObligationID,
510 Box: req.Box, Enrollment: req.Enrollment, BootID: req.BootID, ControllerID: req.ControllerID, ClaimGen: claim.Gen, DirectoryRevision: numeral, Binding: target.DirectoryRevision,
511 Kind: StopReceiptNoProcess, EvidenceAt: evidenceAt, InventorySHA256: kernelEvidence.InventorySHA256, LocalJournalSHA256: kernelEvidence.LocalJournalSHA256,
512 CgroupIdentities: append([]string(nil), kernelEvidence.CgroupIdentities...), UnitInvocationIDs: append([]string{}, kernelEvidence.UnitInvocationIDs...)}
513 body, err := json.Marshal(receipt)
514 if err != nil {
515 return StopResult{}, err
516 }
517 sum := sha256.Sum256(body)
518 result := StopResult{Target: target, Key: stopEvidenceKey(req.Host, req.OperationID, req.Box, receiptID), Digest: hex.EncodeToString(sum[:]), Bytes: body, EvidenceAt: evidenceAt}
519 if c.RetainNoProcess != nil && claim.Op.Kind == hostclaim.KindServing {
520 if err := c.RetainNoProcess(ctx, observed, result); err != nil {
521 return StopResult{}, err
522 }
523 }
524 if err := c.Publish(ctx, result); err != nil {
525 return result, err
526 }
527 return result, nil
528}
529
530func stopResultFromDurable(req StopRequest, durable hostguard.FenceReceipt) (StopResult, error) {
531 var receipt StopReceipt
532 if err := json.Unmarshal(durable.Bytes, &receipt); err != nil {
533 return StopResult{}, ErrClaimStopReceipt
534 }
535 if receipt.Region != req.Region || receipt.V != 1 || receipt.ID == "" || receipt.Host != req.Host || receipt.Incarnation != req.Incarnation ||
536 receipt.InstallationStamp != req.InstallationStamp || receipt.WriterSession != req.WriterSession ||
537 receipt.OperationID != req.OperationID || receipt.Phase != req.Phase || receipt.ObligationID != req.ObligationID ||
538 receipt.Box != req.Box || receipt.Enrollment != req.Enrollment || receipt.BootID != req.BootID ||
539 receipt.ControllerID != req.ControllerID || receipt.FenceID != durable.Target.FenceID ||
540 !receipt.EvidenceAt.Equal(durable.EvidenceAt) || !bytes.Equal(durable.Bytes, mustMarshalStopReceipt(receipt)) {
541 return StopResult{}, ErrClaimStopReceipt
542 }
543 key := stopEvidenceKey(req.Host, req.OperationID, req.Box, receipt.ID)
544 sum := sha256.Sum256(durable.Bytes)
545 return StopResult{Target: durable.Target, Key: key, Digest: hex.EncodeToString(sum[:]), Bytes: append([]byte(nil), durable.Bytes...), EvidenceAt: durable.EvidenceAt}, nil
546}
547
548func mustMarshalStopReceipt(receipt StopReceipt) []byte {
549 b, _ := json.Marshal(receipt)
550 return b
551}
552
553// Publish retries only the exact bytes and key returned by FenceAndStop.
554func (c ClaimStopCoordinator) Publish(ctx context.Context, result StopResult) error {
555 if ctx == nil || c.Evidence == nil || result.Key == "" || len(result.Bytes) == 0 || result.EvidenceAt.IsZero() {
556 return ErrClaimStopUnavailable
557 }
558 var receipt StopReceipt
559 if err := json.Unmarshal(result.Bytes, &receipt); err != nil {
560 return ErrClaimStopReceipt
561 }
562 wantKey := stopEvidenceKey(receipt.Host, receipt.OperationID, receipt.Box, receipt.ID)
563 sum := sha256.Sum256(result.Bytes)
564 if result.Key != wantKey || result.Digest != hex.EncodeToString(sum[:]) || !result.EvidenceAt.Equal(receipt.EvidenceAt) ||
565 !bytes.Equal(result.Bytes, mustMarshalStopReceipt(receipt)) {
566 return ErrClaimStopReceipt
567 }
568 if len(c.EvidenceSigner) != ed25519.PrivateKeySize || c.RequestLimit != hostbox.RequestLimit || receipt.RequestLimitNS != int64(c.RequestLimit) {
569 return ErrClaimStopUnavailable
570 }
571 return hostevidence.Publish(ctx, c.Evidence, c.EvidenceSigner, result.Key, result.Bytes)
572}
573
574func stopOperationTuple(op, phase, obligation string) bool {
575 if op == "" || phase == "" || obligation == "" {
576 return op == "" && phase == "" && obligation == ""
577 }
578 return stopComponent(op) && stopComponent(phase) && stopComponent(obligation)
579}
580func stopEvidenceKey(host, op, box, id string) string {
581 if op == "" {
582 return fmt.Sprintf("control/hosts/%s/serving-evidence/%s/%s.json", host, box, id)
583 }
584 return fmt.Sprintf("control/hosts/%s/evidence/%s/%s/%s.json", host, op, box, id)
585}
586