package hostactors import ( "context" "crypto/rand" "encoding/hex" "github.com/carlosframework/platform/internal/boxid" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" "time" ) type Region struct { Namespaces []string `json:"namespaces"` Pools []string `json:"pools"` Account string `json:"account"` AWSRegion string `json:"aws_region"` GroupName string `json:"group_name"` GroupARN string `json:"group_arn"` // Role is the role every box automatic enrollment admits from the // group takes (roles.go): "" (default) or legacy. Role string `json:"role,omitempty"` } // EnrollAutomatic admits a box its region's group launched (H16, H28). The // console confirms the box's group membership itself, here: Member is // asked, through r, and every store call of the enrolling write, // checkMembership's reads and Replace's Put included, runs on the store // writeBound binds to that read's deadline, the single enforcement point: // none is issued at or after it. No box writes or contributes membership // evidence, and no caller can hand a confirmation in: membershipread.go // builds it from r's answer alone. The evidence comes back whenever the // cloud answered, so every read can be logged. 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) { pinned, c, err := d.checkConfirmed(ctx, o, v, in, raw, instance, r, now) if err != nil { return Observed{}, c.evidence(), err } bd, bctx, cancel := writeBound(ctx, d, c, now) defer cancel() next, err := bd.enroll(bctx, sg, o, v, pinned, raw, true) if err != nil { return Observed{}, c.evidence(), err } // Callers go on with the store itself, not the bound one. return next, c.evidence(), nil } // checkConfirmed is EnrollAutomatic's admission check, made without // writing: the attempt in the roster matches the request and the record, // the request's reference is the instance's group reference // (GroupBoxReference), and a describe made now confirms the attempt's // instance is a current member of the region's group. It returns in // pinned to that instance and the confirmation the read built, which // never leaves this package. The console's enrollment pass asks it // immediately before it invites a box, and makes the invitation on the // store writeBound binds to that confirmation (autoenroll.go). 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) { if err := validateScope(o.Roster, Scope{Namespace: in.Namespace, Regions: []string{in.Region}, Pools: []string{in.Pool}}); err != nil { return EnrollmentRequest{}, membershipConfirmation{}, err } region, ok := o.Roster.Regions[in.Region] if !ok || region.Account != v.Account || region.AWSRegion != in.AWSRegion || region.GroupARN == "" || region.GroupName == "" || !segment(instance) || !segment(in.Reference) || in.ExistingReference { return EnrollmentRequest{}, membershipConfirmation{}, ErrRoster } // A group box is enrolled only under the reference derived from its // instance, never one it chose (census ruling region-ha-2b). if in.Reference != GroupBoxReference(region.Account, instance) { return EnrollmentRequest{}, membershipConfirmation{}, ErrRoster } attempt, ok := o.Roster.EnrollmentAttempts[in.Reference] 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 { return EnrollmentRequest{}, membershipConfirmation{}, ErrRoster } c, err := confirmMembership(ctx, r, region, instance, now) if err != nil { return EnrollmentRequest{}, c, err } if err := c.fresh(now()); err != nil { return EnrollmentRequest{}, c, err } // Pin the verified IMDS identity to the instance that membership proved. in.TofuInstanceID = instance in.ExistingReference = true return in, c, nil } type EnrollmentAttempt struct { AttemptID string `json:"attempt_id"` ConfirmationID string `json:"confirmation_id"` InstanceID string `json:"instance_id"` EvidenceSHA256 string `json:"evidence_sha256"` Region string `json:"region"` Account string `json:"account"` AWSRegion string `json:"aws_region"` GroupName string `json:"group_name"` GroupARN string `json:"group_arn"` Consumed bool `json:"consumed"` } // BeginEnrollmentAttempt supersedes evidence by CAS in the signed authority. // A retry after expiry obtains new IDs; old immutable confirmations remain. func (d Directory) BeginEnrollmentAttempt(ctx context.Context, sg release.Signer, o Observed, in EnrollmentRequest, evidence []byte, instance string) (Observed, EnrollmentAttempt, error) { region, ok := o.Roster.Regions[in.Region] if !ok || !segment(in.Reference) || !segment(instance) || region.Account == "" || region.AWSRegion == "" || region.GroupName == "" || region.GroupARN == "" { return Observed{}, EnrollmentAttempt{}, ErrRoster } if _, exists := o.Roster.Boxes[in.Reference]; exists { return Observed{}, EnrollmentAttempt{}, ErrRoster } var nonce [32]byte if _, err := rand.Read(nonce[:]); err != nil { return Observed{}, EnrollmentAttempt{}, err } 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} if a.AttemptID == a.ConfirmationID { return Observed{}, EnrollmentAttempt{}, ErrRoster } next := Clone(o.Roster) if next.EnrollmentAttempts == nil { next.EnrollmentAttempts = map[string]EnrollmentAttempt{} } next.EnrollmentAttempts[in.Reference] = a updated, err := d.Replace(ctx, sg, o, next) return updated, a, err }