package host import ( "bytes" "context" "crypto/ed25519" "crypto/sha256" "database/sql" "encoding/hex" "encoding/json" "errors" "fmt" "strconv" "strings" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostevidence" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/release" ) var ( ErrClaimStopUnavailable = errors.New("claim stop: required root provider unavailable") ErrClaimStopIdentity = errors.New("claim stop: authenticated identity mismatch") ErrClaimStopInventory = errors.New("claim stop: inventory does not match request") ErrClaimStopReceipt = errors.New("claim stop: immutable receipt conflict") ) type StopPeer struct { UID uint32 PID int ProcessStart string BootID string ControllerID string } type StopPeerProvider interface { AuthenticatedPeer(context.Context) (StopPeer, error) } type StopInventoryEntry = hostbox.StopInventoryEntry type StopInventory = hostbox.StopInventory type StopInventoryReader interface { ReadStopInventory(context.Context, string) (StopInventory, error) } type StopRequest struct { Region string Cleanup bool // Recover names a launch tuple journaled by an earlier controller process // or boot. The live controller asks; root proves that controller dead. Recover bool Host, Incarnation string InstallationStamp, WriterSession uint64 OperationID, Phase, ObligationID string Box, Enrollment string BootID, ControllerID string ExpectedClaimETag string ExpectedDirectoryRevision string // RosterRevision is the roster write counter of the roster the requester // read. Root persists it in the fence target; it becomes the receipt's // directory_revision numeral (roster binding N1). RosterRevision uint64 } type StopClaimReader interface { Read(context.Context, string) (hostclaim.Observed, error) } type DrainAcknowledgement struct { Completed hostbox.ProducerCompletion `json:",omitzero"` ProducerIDs []string FenceID string ControllerID string ProcessStart string Digest string } type StopDrainer interface { DrainClaim(context.Context, StopRequest, string) (DrainAcknowledgement, error) } type StopKernelEvidence struct { ObservedBootID string InventorySHA256 string LocalJournalSHA256 string CgroupIdentities []string UnitInvocationIDs []string } type StopKernelVerifier interface { VerifyStopped(context.Context, hostguard.FenceTarget, StopInventory, DrainAcknowledgement) (StopKernelEvidence, error) } // StopReceipt is root's signed stop evidence. DirectoryRevision is the // roster write counter, a numeral, taken from the fence target's persisted // RosterRevision: the v0.28.4 console bounds it by 0 < n <= roster.Revision. // Binding is the installation's binding; box-side receipt comparisons use it // (roster binding N1, R1). type StopReceipt struct { RequestLimitNS int64 `json:"request_limit_ns"` Region string `json:"region,omitempty"` ClaimETag string `json:"claim_etag,omitempty"` ObservedBootID string `json:"observed_boot_id,omitempty"` V int `json:"v"` ID string `json:"id"` Host string `json:"host"` Incarnation string `json:"incarnation"` InstallationStamp uint64 `json:"installation_stamp"` WriterSession uint64 `json:"writer_session"` OperationID string `json:"operation_id"` Phase string `json:"phase"` ObligationID string `json:"obligation_id"` Box string `json:"box"` Enrollment string `json:"enrollment"` BootID string `json:"boot_id"` ControllerID string `json:"controller_id"` ClaimGen uint64 `json:"claim_gen"` DirectoryRevision string `json:"directory_revision"` Binding string `json:"binding,omitempty"` Kind string `json:"kind"` EvidenceAt time.Time `json:"evidence_at"` FenceID string `json:"fence_id"` InventorySHA256 string `json:"inventory_sha256"` LocalJournalSHA256 string `json:"local_journal_sha256"` ProducerDrainDigest string `json:"producer_drain_digest"` CgroupIdentities []string `json:"cgroup_identities"` UnitInvocationIDs []string `json:"unit_invocation_ids"` } type StopResult struct { Target hostguard.FenceTarget Key string Digest string Bytes []byte EvidenceAt time.Time } type ClaimStopCoordinator struct { RetainNoProcess func(context.Context, hostclaim.Observed, StopResult) error // VerifiedRosterRevision is the write counter of the roster root reads // and verifies itself at the moment of the call, quarantined or not. // It bounds an agent-sent RosterRevision before any fence or receipt // takes it (boundRosterRevision); nil refuses every stop. VerifiedRosterRevision func(context.Context) (uint64, error) EvidenceSigner ed25519.PrivateKey RequestLimit time.Duration Guard hostguard.Guard Claims StopClaimReader Peers StopPeerProvider Inventory StopInventoryReader Drainer StopDrainer Kernel StopKernelVerifier Evidence release.Store CarlosUID uint32 NewID func() string Now func() time.Time } func stopComponent(s string) bool { return s != "" && s != "." && s != ".." && !strings.ContainsAny(s, "/\\\x00") } func stopDigest(s string) bool { if len(s) != sha256.Size*2 { return false } _, err := hex.DecodeString(s) return err == nil } func (c ClaimStopCoordinator) validateRequest(r StopRequest) error { if r.WriterSession == 0 { return ErrClaimStopUnavailable } return c.validateShape(r) } // validateShape is validateRequest without the session: the no-process // witness takes writer session 0. func (c ClaimStopCoordinator) validateShape(r StopRequest) error { if len(c.EvidenceSigner) != ed25519.PrivateKeySize || c.RequestLimit != hostbox.RequestLimit { return ErrClaimStopUnavailable } 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 || !stopComponent(r.Host) || !stopOperationTuple(r.OperationID, r.Phase, r.ObligationID) || !stopComponent(r.Box) || !stopComponent(r.Enrollment) || r.Incarnation == "" || r.InstallationStamp == 0 || r.BootID == "" || r.ControllerID == "" || r.ExpectedClaimETag == "" || r.ExpectedDirectoryRevision == "" || r.RosterRevision == 0 { return ErrClaimStopUnavailable } return nil } // boundRosterRevision refuses an agent-sent receipt numeral outside // 0 < n <= the revision of the roster root reads and verifies now (fix // round 1, finding 3), the bound the v0.28.4 console verifiers apply // against the roster they read. Roster revisions only grow, so a numeral // inside it now stays inside it for every later verifier. It runs before a // fence is begun or a no-process receipt is made; a retry that resumes a // fence still keeps the fence's original numeral (B2). func (c ClaimStopCoordinator) boundRosterRevision(ctx context.Context, n uint64) error { if c.VerifiedRosterRevision == nil { return ErrClaimStopUnavailable } current, err := c.VerifiedRosterRevision(ctx) if err != nil { return err } if n == 0 || n > current { return ErrClaimStopIdentity } return nil } // stopReceiptNumeral is a receipt's directory_revision, read from the one // persisted source: the fence target's RosterRevision. A target without one // (written before roster binding) cannot produce a receipt. func stopReceiptNumeral(t hostguard.FenceTarget) (string, error) { if t.RosterRevision == 0 { return "", ErrClaimStopReceipt } return strconv.FormatUint(t.RosterRevision, 10), nil } // receiptRosterRevision parses a receipt's directory_revision numeral: a // canonical non-zero decimal. func receiptRosterRevision(s string) (uint64, bool) { n, err := strconv.ParseUint(s, 10, 64) if err != nil || n == 0 || strconv.FormatUint(n, 10) != s { return 0, false } return n, true } // The receipt kinds: a stopped writer session, and the no-process witness // for an installation whose holder never admitted a writer (R4). const ( StopReceiptStopped = "stopped" StopReceiptNoProcess = "no-process" ) // stopNoProcessKernel is root's kernel half of the session-zero witness: no // launch journal entry for the host, its host containment empty or absent, // and no process holding any of the installation's files. type stopNoProcessKernel interface { VerifyNoProcess(context.Context, hostguard.FenceTarget, StopInventory) (StopKernelEvidence, error) } func (c ClaimStopCoordinator) FenceAndStop(ctx context.Context, req StopRequest) (StopResult, error) { if req.WriterSession == 0 { return c.witnessNoProcess(ctx, req) } if ctx == nil || c.validateRequest(req) != nil || (req.Cleanup && req.Recover) { return StopResult{}, ErrClaimStopUnavailable } var recovery stopRecoveryKernel if req.Recover { var ok bool if recovery, ok = c.Kernel.(stopRecoveryKernel); !ok { return StopResult{}, ErrClaimStopUnavailable } } peer, err := c.Peers.AuthenticatedPeer(ctx) if err != nil { return StopResult{}, err } if peer.UID != c.CarlosUID || peer.PID <= 0 || peer.ProcessStart == "" || peer.BootID == "" || peer.ControllerID == "" { return StopResult{}, ErrClaimStopIdentity } // Only recovery may name another tuple, and only for this box's session. if !req.Recover && (peer.BootID != req.BootID || peer.ControllerID != req.ControllerID) { return StopResult{}, ErrClaimStopIdentity } var observed hostclaim.Observed var cleanupTarget *hostguard.FenceTarget if req.Cleanup { 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} durable, err := c.Guard.ExistingFence(ctx, want) if err != nil { return StopResult{}, err } cleanupTarget = &durable } else { observed, err = c.Claims.Read(ctx, req.Host) if err != nil { return StopResult{}, err } if observed.Absent || observed.Claim == nil || observed.Claim.Holder == nil || observed.ETag != req.ExpectedClaimETag || observed.Claim.Host != req.Host || observed.Claim.Holder.Incarnation != req.Incarnation || observed.Claim.Holder.Stamp != req.InstallationStamp || observed.Claim.Holder.Session != req.WriterSession || !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}) { return StopResult{}, ErrClaimStopIdentity } } var inv StopInventory if cleanupTarget != nil { reader, ok := c.Inventory.(exactStopInventory) if !ok { return StopResult{}, ErrClaimStopInventory } inv, err = reader.ReadExactStopInventory(ctx, *cleanupTarget) } else { inv, err = c.Inventory.ReadStopInventory(ctx, req.Host) } if err != nil { return StopResult{}, err } if inv.Host != req.Host || inv.Incarnation != req.Incarnation || inv.InstallationStamp != req.InstallationStamp || inv.Box != req.Box || inv.Enrollment != req.Enrollment || inv.DirectoryRevision != req.ExpectedDirectoryRevision { return StopResult{}, ErrClaimStopInventory } fenceID, receiptID := c.NewID(), c.NewID() if !stopComponent(fenceID) || !stopComponent(receiptID) { return StopResult{}, ErrClaimStopUnavailable } if err := c.boundRosterRevision(ctx, req.RosterRevision); err != nil { return StopResult{}, err } gen := uint64(0) if observed.Claim != nil { gen = observed.Claim.Gen } target := hostguard.FenceTarget{Region: req.Region, ClaimGen: gen, ObligationID: req.ObligationID, FenceID: fenceID, Host: req.Host, Incarnation: req.Incarnation, Stamp: req.InstallationStamp, Session: req.WriterSession, Op: req.OperationID, Phase: req.Phase, Box: req.Box, Enrollment: req.Enrollment, BootID: req.BootID, ControllerID: req.ControllerID, ClaimETag: req.ExpectedClaimETag, DirectoryRevision: req.ExpectedDirectoryRevision, RosterRevision: req.RosterRevision} if cleanupTarget != nil { target = *cleanupTarget err = c.Guard.ResumeFence(ctx, target) } else { target, err = c.Guard.BeginFence(ctx, target) } if err != nil { return StopResult{}, err } fenceID = target.FenceID // The receipt numeral comes from the persisted fence target, never from // this request: a retry after roster writes resumes the original fence // and its original numeral (B2). A fence persisted without one cannot // produce a receipt and is refused before any drain or kill (N1). numeral, err := stopReceiptNumeral(target) if err != nil { return StopResult{}, err } if durable, err := c.Guard.FenceReceiptFor(ctx, target); err == nil { result, err := stopResultFromDurable(req, durable) if err != nil { return StopResult{}, err } if !req.Cleanup { if err := c.Publish(ctx, result); err != nil { return result, err } } return result, nil } else if !errors.Is(err, hostguard.ErrNoFenceReceipt) { return StopResult{}, err } var ack DrainAcknowledgement var kernel StopKernelEvidence if req.Recover { // Root reads the tuple's own launch record and proves its controller // dead, or the boot gone, before it kills, verifies and settles. A // missing record or a live controller refuses; the fence stays pending. kernel, ack, err = c.recoverKernel(ctx, recovery, target, inv) if err != nil { return StopResult{}, err } if ack.FenceID != fenceID || ack.ControllerID != req.ControllerID || ack.ProcessStart == "" || !stopDigest(ack.Digest) || (req.BootID == peer.BootID && req.ControllerID == peer.ControllerID && ack.ProcessStart == peer.ProcessStart) { return StopResult{}, ErrClaimStopIdentity } } else if absent, absentErr := c.verifyUnjournaled(ctx, target, inv, sql.ErrNoRows); absentErr == nil { kernel = absent ack = DrainClaimAcknowledgement(fenceID, req.ControllerID, peer.ProcessStart, nil, hostbox.ProducerCompletion{}) } else { ack, err = c.Drainer.DrainClaim(ctx, req, fenceID) if err != nil { return StopResult{}, err } if ack.FenceID != fenceID || ack.ControllerID != req.ControllerID || ack.ProcessStart != peer.ProcessStart || !stopDigest(ack.Digest) { return StopResult{}, ErrClaimStopIdentity } kernel, err = c.Kernel.VerifyStopped(ctx, target, inv, ack) if err != nil { kernel, err = c.verifyUnjournaled(ctx, target, inv, err) } if err != nil { return StopResult{}, err } } evidenceAt := c.Now().UTC() if evidenceAt.IsZero() || !stopDigest(kernel.InventorySHA256) || !stopDigest(kernel.LocalJournalSHA256) || len(kernel.CgroupIdentities) == 0 { return StopResult{}, ErrClaimStopReceipt } 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, InstallationStamp: req.InstallationStamp, WriterSession: req.WriterSession, OperationID: req.OperationID, Phase: req.Phase, ObligationID: req.ObligationID, Box: req.Box, Enrollment: req.Enrollment, BootID: req.BootID, ControllerID: req.ControllerID, ClaimGen: target.ClaimGen, DirectoryRevision: numeral, Binding: target.DirectoryRevision, Kind: StopReceiptStopped, EvidenceAt: evidenceAt, FenceID: fenceID, InventorySHA256: kernel.InventorySHA256, LocalJournalSHA256: kernel.LocalJournalSHA256, ProducerDrainDigest: ack.Digest, CgroupIdentities: append([]string(nil), kernel.CgroupIdentities...), UnitInvocationIDs: append([]string(nil), kernel.UnitInvocationIDs...)} body, err := json.Marshal(receipt) if err != nil { return StopResult{}, err } verify := func(_ context.Context, got hostguard.FenceTarget, exact []byte, at time.Time) error { if got != target || !at.Equal(evidenceAt) || !bytes.Equal(exact, body) { return ErrClaimStopReceipt } return nil } if err := c.Guard.CompleteFence(ctx, target, body, evidenceAt, verify); err != nil { return StopResult{}, err } key := stopEvidenceKey(req.Host, req.OperationID, req.Box, receiptID) sum := sha256.Sum256(body) result := StopResult{Target: target, Key: key, Digest: hex.EncodeToString(sum[:]), Bytes: append([]byte(nil), body...), EvidenceAt: evidenceAt} if !req.Cleanup { if err := c.Publish(ctx, result); err != nil { return result, err } } return result, nil } // witnessNoProcess is the positive no-process witness for writer session 0 // (claim Plan 11, R4): the holder never admitted a writer, root's launch // journal has no entry for the host, the host's containment is empty or // absent, and root's descriptor scan finds no holder of the installation's // files. No session ever ran, so there is no fence and no producer drain: // the receipt binds the claim's ETag, which any admission changes, and root // re-reads the claim after its observation. The receipt is published like // any stop receipt. func (c ClaimStopCoordinator) witnessNoProcess(ctx context.Context, req StopRequest) (StopResult, error) { if ctx == nil || req.WriterSession != 0 || req.Cleanup || req.Recover || c.validateShape(req) != nil { return StopResult{}, ErrClaimStopUnavailable } kernel, ok := c.Kernel.(stopNoProcessKernel) if !ok { return StopResult{}, ErrClaimStopUnavailable } peer, err := c.Peers.AuthenticatedPeer(ctx) if err != nil { return StopResult{}, err } if peer.UID != c.CarlosUID || peer.PID <= 0 || peer.ProcessStart == "" || peer.BootID != req.BootID || peer.ControllerID != req.ControllerID { return StopResult{}, ErrClaimStopIdentity } observed, err := c.Claims.Read(ctx, req.Host) if err != nil { return StopResult{}, err } if observed.Absent || observed.Claim == nil || observed.Claim.Holder == nil || observed.ETag != req.ExpectedClaimETag || observed.Claim.Host != req.Host { return StopResult{}, ErrClaimStopIdentity } claim, holder := observed.Claim, observed.Claim.Holder 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 { return StopResult{}, ErrClaimStopIdentity } target := hostguard.FenceTarget{Region: req.Region, ClaimGen: claim.Gen, ObligationID: req.ObligationID, Host: req.Host, Incarnation: req.Incarnation, Stamp: req.InstallationStamp, Op: req.OperationID, Phase: req.Phase, Box: req.Box, Enrollment: req.Enrollment, BootID: req.BootID, ControllerID: req.ControllerID, ClaimETag: req.ExpectedClaimETag, DirectoryRevision: req.ExpectedDirectoryRevision, RosterRevision: req.RosterRevision} if claim.Op.Kind == hostclaim.KindServing { if req.OperationID != "" || req.Region == "" || claim.Op.Exec != req.Box { return StopResult{}, ErrClaimStopIdentity } } else if !hostguard.StopAuthorityMatches(claim, target) { return StopResult{}, ErrClaimStopIdentity } if err := c.boundRosterRevision(ctx, req.RosterRevision); err != nil { return StopResult{}, err } // No session ran, so there is no fence: the witness's own target is the // receipt's one source, retained with the result. numeral, err := stopReceiptNumeral(target) if err != nil { return StopResult{}, err } inv, err := c.Inventory.ReadStopInventory(ctx, req.Host) if err != nil { return StopResult{}, err } if inv.Host != req.Host || inv.Incarnation != req.Incarnation || inv.InstallationStamp != req.InstallationStamp || inv.Box != req.Box || inv.Enrollment != req.Enrollment || inv.DirectoryRevision != req.ExpectedDirectoryRevision { return StopResult{}, ErrClaimStopInventory } kernelEvidence, err := kernel.VerifyNoProcess(ctx, target, inv) if err != nil { return StopResult{}, err } again, err := c.Claims.Read(ctx, req.Host) if err != nil { return StopResult{}, err } if again.ETag != req.ExpectedClaimETag { return StopResult{}, ErrClaimStopIdentity } evidenceAt := c.Now().UTC() receiptID := c.NewID() if evidenceAt.IsZero() || !stopComponent(receiptID) || !stopDigest(kernelEvidence.InventorySHA256) || !stopDigest(kernelEvidence.LocalJournalSHA256) || len(kernelEvidence.CgroupIdentities) == 0 { return StopResult{}, ErrClaimStopReceipt } receipt := StopReceipt{RequestLimitNS: int64(c.RequestLimit), Region: req.Region, ClaimETag: req.ExpectedClaimETag, ObservedBootID: kernelEvidence.ObservedBootID, V: 1, ID: receiptID, Host: req.Host, Incarnation: req.Incarnation, InstallationStamp: req.InstallationStamp, OperationID: req.OperationID, Phase: req.Phase, ObligationID: req.ObligationID, Box: req.Box, Enrollment: req.Enrollment, BootID: req.BootID, ControllerID: req.ControllerID, ClaimGen: claim.Gen, DirectoryRevision: numeral, Binding: target.DirectoryRevision, Kind: StopReceiptNoProcess, EvidenceAt: evidenceAt, InventorySHA256: kernelEvidence.InventorySHA256, LocalJournalSHA256: kernelEvidence.LocalJournalSHA256, CgroupIdentities: append([]string(nil), kernelEvidence.CgroupIdentities...), UnitInvocationIDs: append([]string{}, kernelEvidence.UnitInvocationIDs...)} body, err := json.Marshal(receipt) if err != nil { return StopResult{}, err } sum := sha256.Sum256(body) result := StopResult{Target: target, Key: stopEvidenceKey(req.Host, req.OperationID, req.Box, receiptID), Digest: hex.EncodeToString(sum[:]), Bytes: body, EvidenceAt: evidenceAt} if c.RetainNoProcess != nil && claim.Op.Kind == hostclaim.KindServing { if err := c.RetainNoProcess(ctx, observed, result); err != nil { return StopResult{}, err } } if err := c.Publish(ctx, result); err != nil { return result, err } return result, nil } func stopResultFromDurable(req StopRequest, durable hostguard.FenceReceipt) (StopResult, error) { var receipt StopReceipt if err := json.Unmarshal(durable.Bytes, &receipt); err != nil { return StopResult{}, ErrClaimStopReceipt } if receipt.Region != req.Region || receipt.V != 1 || receipt.ID == "" || receipt.Host != req.Host || receipt.Incarnation != req.Incarnation || receipt.InstallationStamp != req.InstallationStamp || receipt.WriterSession != req.WriterSession || receipt.OperationID != req.OperationID || receipt.Phase != req.Phase || receipt.ObligationID != req.ObligationID || receipt.Box != req.Box || receipt.Enrollment != req.Enrollment || receipt.BootID != req.BootID || receipt.ControllerID != req.ControllerID || receipt.FenceID != durable.Target.FenceID || !receipt.EvidenceAt.Equal(durable.EvidenceAt) || !bytes.Equal(durable.Bytes, mustMarshalStopReceipt(receipt)) { return StopResult{}, ErrClaimStopReceipt } key := stopEvidenceKey(req.Host, req.OperationID, req.Box, receipt.ID) sum := sha256.Sum256(durable.Bytes) return StopResult{Target: durable.Target, Key: key, Digest: hex.EncodeToString(sum[:]), Bytes: append([]byte(nil), durable.Bytes...), EvidenceAt: durable.EvidenceAt}, nil } func mustMarshalStopReceipt(receipt StopReceipt) []byte { b, _ := json.Marshal(receipt) return b } // Publish retries only the exact bytes and key returned by FenceAndStop. func (c ClaimStopCoordinator) Publish(ctx context.Context, result StopResult) error { if ctx == nil || c.Evidence == nil || result.Key == "" || len(result.Bytes) == 0 || result.EvidenceAt.IsZero() { return ErrClaimStopUnavailable } var receipt StopReceipt if err := json.Unmarshal(result.Bytes, &receipt); err != nil { return ErrClaimStopReceipt } wantKey := stopEvidenceKey(receipt.Host, receipt.OperationID, receipt.Box, receipt.ID) sum := sha256.Sum256(result.Bytes) if result.Key != wantKey || result.Digest != hex.EncodeToString(sum[:]) || !result.EvidenceAt.Equal(receipt.EvidenceAt) || !bytes.Equal(result.Bytes, mustMarshalStopReceipt(receipt)) { return ErrClaimStopReceipt } if len(c.EvidenceSigner) != ed25519.PrivateKeySize || c.RequestLimit != hostbox.RequestLimit || receipt.RequestLimitNS != int64(c.RequestLimit) { return ErrClaimStopUnavailable } return hostevidence.Publish(ctx, c.Evidence, c.EvidenceSigner, result.Key, result.Bytes) } func stopOperationTuple(op, phase, obligation string) bool { if op == "" || phase == "" || obligation == "" { return op == "" && phase == "" && obligation == "" } return stopComponent(op) && stopComponent(phase) && stopComponent(obligation) } func stopEvidenceKey(host, op, box, id string) string { if op == "" { return fmt.Sprintf("control/hosts/%s/serving-evidence/%s/%s.json", host, box, id) } return fmt.Sprintf("control/hosts/%s/evidence/%s/%s/%s.json", host, op, box, id) }