package hostclaim import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "strings" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/termfence" ) var ErrFence = errors.New("hostclaim: invalid termination fence") type FenceRef struct { Enrollment string `json:"enrollment"` Key string `json:"key"` SHA256 string `json:"sha256"` } type FenceBinding struct { FleetHomeRegion string Region string RosterRevision uint64 RosterETag string } // FenceAuthority checks and identifies one authenticated roster observation. // Implementations must bind both the target and reporting worker enrollments, // cloud account, cloud region and terminal instance identity to that roster. type FenceAuthority interface { ValidateFence(context.Context, termfence.Record) (FenceBinding, error) } type VerifiedFence struct { ref FenceRef record termfence.Record binding FenceBinding } func validFenceRef(ref FenceRef) bool { if len(ref.Enrollment) != 32 || ref.Enrollment != strings.ToLower(ref.Enrollment) { return false } if _, err := hex.DecodeString(ref.Enrollment); err != nil { return false } return ref.Key == "control/box-fences/"+ref.Enrollment+".json" && isHex64(ref.SHA256) && ref.SHA256 == strings.ToLower(ref.SHA256) } // VerifyFence re-reads the immutable worker-owned object, including on replay. // A previously verified reference alone never substitutes for verification. func VerifyFence(ctx context.Context, s release.Store, authority FenceAuthority, ref FenceRef) (VerifiedFence, error) { if s == nil || authority == nil || !validFenceRef(ref) { return VerifiedFence{}, ErrFence } raw, etag, err := s.Get(ctx, ref.Key) if err != nil { return VerifiedFence{}, err } sum := sha256.Sum256(raw) if etag == "" || hex.EncodeToString(sum[:]) != ref.SHA256 { return VerifiedFence{}, ErrFence } var f termfence.Record if json.Unmarshal(raw, &f) != nil || f.Enrollment != ref.Enrollment || f.Box == "" || f.State != "terminated" || f.V != 1 || f.WrittenBy.Box == f.Box { return VerifiedFence{}, ErrFence } binding, err := authority.ValidateFence(ctx, f) if err != nil { return VerifiedFence{}, err } if binding.Region == "" || binding.RosterRevision == 0 || binding.RosterETag == "" { return VerifiedFence{}, ErrFence } return VerifiedFence{ref: ref, record: f, binding: binding}, nil } // VerifyOperationFence binds the journal key as well as the immutable record. // Return and recovery paths use this form rather than trusting a decoded ref. func VerifyOperationFence(ctx context.Context, s release.Store, authority FenceAuthority, op Op, box string) error { if !validBoxReference(box) || op.ID == "" { return ErrFence } var ref FenceRef if json.Unmarshal([]byte(op.Journal["fence."+box]), &ref) != nil { return ErrFence } verified, err := VerifyFence(ctx, s, authority, ref) if err != nil { return err } if verified.record.Box != box { return ErrFence } return nil } func VerifyFenceForBox(ctx context.Context, s release.Store, authority FenceAuthority, ref FenceRef, box string) error { verified, err := VerifyFence(ctx, s, authority, ref) if err != nil { return err } if !validBoxReference(box) || verified.record.Box != box { return ErrFence } return nil } func (f VerifiedFence) FleetHomeRegion() string { return f.binding.FleetHomeRegion }