| 1 | package hostactors |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/rand" |
| 6 | "encoding/hex" |
| 7 | "github.com/carlosframework/platform/internal/boxid" |
| 8 | "github.com/carlosframework/platform/internal/hostfloor" |
| 9 | "github.com/carlosframework/platform/internal/release" |
| 10 | "time" |
| 11 | ) |
| 12 | |
| 13 | type Region struct { |
| 14 | Namespaces []string `json:"namespaces"` |
| 15 | Pools []string `json:"pools"` |
| 16 | Account string `json:"account"` |
| 17 | AWSRegion string `json:"aws_region"` |
| 18 | GroupName string `json:"group_name"` |
| 19 | GroupARN string `json:"group_arn"` |
| 20 | // Role is the role every box automatic enrollment admits from the |
| 21 | // group takes (roles.go): "" (default) or legacy. |
| 22 | Role string `json:"role,omitempty"` |
| 23 | } |
| 24 | |
| 25 | // EnrollAutomatic admits a box its region's group launched (H16, H28). The |
| 26 | // console confirms the box's group membership itself, here: Member is |
| 27 | // asked, through r, and every store call of the enrolling write, |
| 28 | // checkMembership's reads and Replace's Put included, runs on the store |
| 29 | // writeBound binds to that read's deadline, the single enforcement point: |
| 30 | // none is issued at or after it. No box writes or contributes membership |
| 31 | // evidence, and no caller can hand a confirmation in: membershipread.go |
| 32 | // builds it from r's answer alone. The evidence comes back whenever the |
| 33 | // cloud answered, so every read can be logged. |
| 34 | func (d Directory) EnrollAutomatic(ctx context.Context, sg release.Signer, o Observed, v boxid.Verifier, in EnrollmentRequest, raw []byte, instance string, r MembershipReader, now func() time.Time) (Observed, MembershipEvidence, error) { |
| 35 | pinned, c, err := d.checkConfirmed(ctx, o, v, in, raw, instance, r, now) |
| 36 | if err != nil { |
| 37 | return Observed{}, c.evidence(), err |
| 38 | } |
| 39 | bd, bctx, cancel := writeBound(ctx, d, c, now) |
| 40 | defer cancel() |
| 41 | next, err := bd.enroll(bctx, sg, o, v, pinned, raw, true) |
| 42 | if err != nil { |
| 43 | return Observed{}, c.evidence(), err |
| 44 | } |
| 45 | // Callers go on with the store itself, not the bound one. |
| 46 | return next, c.evidence(), nil |
| 47 | } |
| 48 | |
| 49 | // checkConfirmed is EnrollAutomatic's admission check, made without |
| 50 | // writing: the attempt in the roster matches the request and the record, |
| 51 | // the request's reference is the instance's group reference |
| 52 | // (GroupBoxReference), and a describe made now confirms the attempt's |
| 53 | // instance is a current member of the region's group. It returns in |
| 54 | // pinned to that instance and the confirmation the read built, which |
| 55 | // never leaves this package. The console's enrollment pass asks it |
| 56 | // immediately before it invites a box, and makes the invitation on the |
| 57 | // store writeBound binds to that confirmation (autoenroll.go). |
| 58 | func (d Directory) checkConfirmed(ctx context.Context, o Observed, v boxid.Verifier, in EnrollmentRequest, raw []byte, instance string, r MembershipReader, now func() time.Time) (EnrollmentRequest, membershipConfirmation, error) { |
| 59 | if err := validateScope(o.Roster, Scope{Namespace: in.Namespace, Regions: []string{in.Region}, Pools: []string{in.Pool}}); err != nil { |
| 60 | return EnrollmentRequest{}, membershipConfirmation{}, err |
| 61 | } |
| 62 | region, ok := o.Roster.Regions[in.Region] |
| 63 | if !ok || region.Account != v.Account || region.AWSRegion != in.AWSRegion || region.GroupARN == "" || region.GroupName == "" || !segment(instance) || !segment(in.Reference) || in.ExistingReference { |
| 64 | return EnrollmentRequest{}, membershipConfirmation{}, ErrRoster |
| 65 | } |
| 66 | // A group box is enrolled only under the reference derived from its |
| 67 | // instance, never one it chose (census ruling region-ha-2b). |
| 68 | if in.Reference != GroupBoxReference(region.Account, instance) { |
| 69 | return EnrollmentRequest{}, membershipConfirmation{}, ErrRoster |
| 70 | } |
| 71 | attempt, ok := o.Roster.EnrollmentAttempts[in.Reference] |
| 72 | if !ok || attempt.Consumed || !token.MatchString(in.AttemptID) || !token.MatchString(in.ConfirmationID) || in.AttemptID == in.ConfirmationID || attempt.AttemptID != in.AttemptID || attempt.ConfirmationID != in.ConfirmationID || attempt.InstanceID != instance || attempt.EvidenceSHA256 != hostfloor.Digest(raw) || attempt.Region != in.Region || attempt.Account != region.Account || attempt.AWSRegion != region.AWSRegion || attempt.GroupARN != region.GroupARN || attempt.GroupName != region.GroupName { |
| 73 | return EnrollmentRequest{}, membershipConfirmation{}, ErrRoster |
| 74 | } |
| 75 | c, err := confirmMembership(ctx, r, region, instance, now) |
| 76 | if err != nil { |
| 77 | return EnrollmentRequest{}, c, err |
| 78 | } |
| 79 | if err := c.fresh(now()); err != nil { |
| 80 | return EnrollmentRequest{}, c, err |
| 81 | } |
| 82 | // Pin the verified IMDS identity to the instance that membership proved. |
| 83 | in.TofuInstanceID = instance |
| 84 | in.ExistingReference = true |
| 85 | return in, c, nil |
| 86 | } |
| 87 | |
| 88 | type EnrollmentAttempt struct { |
| 89 | AttemptID string `json:"attempt_id"` |
| 90 | ConfirmationID string `json:"confirmation_id"` |
| 91 | InstanceID string `json:"instance_id"` |
| 92 | EvidenceSHA256 string `json:"evidence_sha256"` |
| 93 | Region string `json:"region"` |
| 94 | Account string `json:"account"` |
| 95 | AWSRegion string `json:"aws_region"` |
| 96 | GroupName string `json:"group_name"` |
| 97 | GroupARN string `json:"group_arn"` |
| 98 | Consumed bool `json:"consumed"` |
| 99 | } |
| 100 | |
| 101 | // BeginEnrollmentAttempt supersedes evidence by CAS in the signed authority. |
| 102 | // A retry after expiry obtains new IDs; old immutable confirmations remain. |
| 103 | func (d Directory) BeginEnrollmentAttempt(ctx context.Context, sg release.Signer, o Observed, in EnrollmentRequest, evidence []byte, instance string) (Observed, EnrollmentAttempt, error) { |
| 104 | region, ok := o.Roster.Regions[in.Region] |
| 105 | if !ok || !segment(in.Reference) || !segment(instance) || region.Account == "" || region.AWSRegion == "" || region.GroupName == "" || region.GroupARN == "" { |
| 106 | return Observed{}, EnrollmentAttempt{}, ErrRoster |
| 107 | } |
| 108 | if _, exists := o.Roster.Boxes[in.Reference]; exists { |
| 109 | return Observed{}, EnrollmentAttempt{}, ErrRoster |
| 110 | } |
| 111 | var nonce [32]byte |
| 112 | if _, err := rand.Read(nonce[:]); err != nil { |
| 113 | return Observed{}, EnrollmentAttempt{}, err |
| 114 | } |
| 115 | a := EnrollmentAttempt{AttemptID: hex.EncodeToString(nonce[:16]), ConfirmationID: hex.EncodeToString(nonce[16:]), InstanceID: instance, EvidenceSHA256: hostfloor.Digest(evidence), Region: in.Region, Account: region.Account, AWSRegion: region.AWSRegion, GroupName: region.GroupName, GroupARN: region.GroupARN} |
| 116 | if a.AttemptID == a.ConfirmationID { |
| 117 | return Observed{}, EnrollmentAttempt{}, ErrRoster |
| 118 | } |
| 119 | next := Clone(o.Roster) |
| 120 | if next.EnrollmentAttempts == nil { |
| 121 | next.EnrollmentAttempts = map[string]EnrollmentAttempt{} |
| 122 | } |
| 123 | next.EnrollmentAttempts[in.Reference] = a |
| 124 | updated, err := d.Replace(ctx, sg, o, next) |
| 125 | return updated, a, err |
| 126 | } |
| 127 | |