carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostclaim
2
3import (
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
15var ErrFence = errors.New("hostclaim: invalid termination fence")
16
17type FenceRef struct {
18 Enrollment string `json:"enrollment"`
19 Key string `json:"key"`
20 SHA256 string `json:"sha256"`
21}
22
23type 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.
33type FenceAuthority interface {
34 ValidateFence(context.Context, termfence.Record) (FenceBinding, error)
35}
36
37type VerifiedFence struct {
38 ref FenceRef
39 record termfence.Record
40 binding FenceBinding
41}
42
43func 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.
55func 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.
83func 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
101func 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
112func (f VerifiedFence) FleetHomeRegion() string { return f.binding.FleetHomeRegion }
113