| 1 | package hostclaim |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/sha256" |
| 6 | "encoding/hex" |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | "strings" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/release" |
| 12 | "github.com/carlosframework/platform/internal/termfence" |
| 13 | ) |
| 14 | |
| 15 | var ErrFence = errors.New("hostclaim: invalid termination fence") |
| 16 | |
| 17 | type FenceRef struct { |
| 18 | Enrollment string `json:"enrollment"` |
| 19 | Key string `json:"key"` |
| 20 | SHA256 string `json:"sha256"` |
| 21 | } |
| 22 | |
| 23 | type FenceBinding struct { |
| 24 | FleetHomeRegion string |
| 25 | Region string |
| 26 | RosterRevision uint64 |
| 27 | RosterETag string |
| 28 | } |
| 29 | |
| 30 | // FenceAuthority checks and identifies one authenticated roster observation. |
| 31 | // Implementations must bind both the target and reporting worker enrollments, |
| 32 | // cloud account, cloud region and terminal instance identity to that roster. |
| 33 | type FenceAuthority interface { |
| 34 | ValidateFence(context.Context, termfence.Record) (FenceBinding, error) |
| 35 | } |
| 36 | |
| 37 | type VerifiedFence struct { |
| 38 | ref FenceRef |
| 39 | record termfence.Record |
| 40 | binding FenceBinding |
| 41 | } |
| 42 | |
| 43 | func validFenceRef(ref FenceRef) bool { |
| 44 | if len(ref.Enrollment) != 32 || ref.Enrollment != strings.ToLower(ref.Enrollment) { |
| 45 | return false |
| 46 | } |
| 47 | if _, err := hex.DecodeString(ref.Enrollment); err != nil { |
| 48 | return false |
| 49 | } |
| 50 | return ref.Key == "control/box-fences/"+ref.Enrollment+".json" && isHex64(ref.SHA256) && ref.SHA256 == strings.ToLower(ref.SHA256) |
| 51 | } |
| 52 | |
| 53 | // VerifyFence re-reads the immutable worker-owned object, including on replay. |
| 54 | // A previously verified reference alone never substitutes for verification. |
| 55 | func VerifyFence(ctx context.Context, s release.Store, authority FenceAuthority, ref FenceRef) (VerifiedFence, error) { |
| 56 | if s == nil || authority == nil || !validFenceRef(ref) { |
| 57 | return VerifiedFence{}, ErrFence |
| 58 | } |
| 59 | raw, etag, err := s.Get(ctx, ref.Key) |
| 60 | if err != nil { |
| 61 | return VerifiedFence{}, err |
| 62 | } |
| 63 | sum := sha256.Sum256(raw) |
| 64 | if etag == "" || hex.EncodeToString(sum[:]) != ref.SHA256 { |
| 65 | return VerifiedFence{}, ErrFence |
| 66 | } |
| 67 | var f termfence.Record |
| 68 | if json.Unmarshal(raw, &f) != nil || f.Enrollment != ref.Enrollment || f.Box == "" || f.State != "terminated" || f.V != 1 || f.WrittenBy.Box == f.Box { |
| 69 | return VerifiedFence{}, ErrFence |
| 70 | } |
| 71 | binding, err := authority.ValidateFence(ctx, f) |
| 72 | if err != nil { |
| 73 | return VerifiedFence{}, err |
| 74 | } |
| 75 | if binding.Region == "" || binding.RosterRevision == 0 || binding.RosterETag == "" { |
| 76 | return VerifiedFence{}, ErrFence |
| 77 | } |
| 78 | return VerifiedFence{ref: ref, record: f, binding: binding}, nil |
| 79 | } |
| 80 | |
| 81 | // VerifyOperationFence binds the journal key as well as the immutable record. |
| 82 | // Return and recovery paths use this form rather than trusting a decoded ref. |
| 83 | func VerifyOperationFence(ctx context.Context, s release.Store, authority FenceAuthority, op Op, box string) error { |
| 84 | if !validBoxReference(box) || op.ID == "" { |
| 85 | return ErrFence |
| 86 | } |
| 87 | var ref FenceRef |
| 88 | if json.Unmarshal([]byte(op.Journal["fence."+box]), &ref) != nil { |
| 89 | return ErrFence |
| 90 | } |
| 91 | verified, err := VerifyFence(ctx, s, authority, ref) |
| 92 | if err != nil { |
| 93 | return err |
| 94 | } |
| 95 | if verified.record.Box != box { |
| 96 | return ErrFence |
| 97 | } |
| 98 | return nil |
| 99 | } |
| 100 | |
| 101 | func VerifyFenceForBox(ctx context.Context, s release.Store, authority FenceAuthority, ref FenceRef, box string) error { |
| 102 | verified, err := VerifyFence(ctx, s, authority, ref) |
| 103 | if err != nil { |
| 104 | return err |
| 105 | } |
| 106 | if !validBoxReference(box) || verified.record.Box != box { |
| 107 | return ErrFence |
| 108 | } |
| 109 | return nil |
| 110 | } |
| 111 | |
| 112 | func (f VerifiedFence) FleetHomeRegion() string { return f.binding.FleetHomeRegion } |
| 113 | |