package hostactors import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "errors" "os" "strings" "github.com/carlosframework/platform/internal/boxid" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" ) type Invitation struct { V int `json:"v"` Bucket string `json:"bucket"` Reference string `json:"reference"` Enrollment string `json:"enrollment"` EvidenceSHA256 string `json:"evidence_sha256"` } // Invite grants no membership. It gives an enrolled-identity candidate a // stable nonce with which to install its root guard and publish readiness. // A fleet caller passes an empty reference, then changes only boxid.Record.Box // to the returned Reference and marshals it again before publishing or Enroll. // That field is outside the AWS-signed identity document and signature. func (d Directory) Invite(ctx context.Context, sg release.Signer, reference string, evidence []byte) (Invitation, error) { fleet := reference == "" if !fleet && !segment(reference) { return Invitation{}, ErrRoster } if !fleet { key := "control/host-actor-invitations/" + reference + ".json" raw, _, err := d.Store.Get(ctx, key) if err == nil { var invite Invitation if release.VerifyRootEnvelope(d.PublicKey, raw, &invite) != nil || invite.V != 1 || invite.Bucket != d.Bucket || invite.Reference != reference || !token.MatchString(invite.Enrollment) || invite.EvidenceSHA256 != hostfloor.Digest(evidence) || (strings.HasPrefix(reference, "fleet-") && reference != "fleet-"+invite.Enrollment) { return Invitation{}, ErrRoster } return invite, nil } if !errors.Is(err, os.ErrNotExist) { return Invitation{}, err } // Fleet references are minted from their enrollment nonce. A caller may // retry one returned by this function, but cannot choose a fresh one. if strings.HasPrefix(reference, "fleet-") { return Invitation{}, ErrRoster } } var id [16]byte if _, err := rand.Read(id[:]); err != nil { return Invitation{}, err } enrollment := hex.EncodeToString(id[:]) if fleet { reference = "fleet-" + enrollment var rec boxid.Record if json.Unmarshal(evidence, &rec) != nil || rec.V != 1 { return Invitation{}, ErrRoster } rec.Box = reference var err error evidence, err = json.Marshal(rec) if err != nil { return Invitation{}, err } } invite := Invitation{1, d.Bucket, reference, enrollment, hostfloor.Digest(evidence)} raw, err := release.SignRootEnvelope(sg, invite) if err != nil { return Invitation{}, err } key := "control/host-actor-invitations/" + reference + ".json" if _, err = d.Store.Put(ctx, key, raw, ""); err != nil { return Invitation{}, err } return invite, nil } func (d Directory) enrollmentNonce(ctx context.Context, in EnrollmentRequest, evidence []byte) (string, error) { key := "control/host-actor-invitations/" + in.Reference + ".json" raw, _, err := d.Store.Get(ctx, key) if err != nil { return "", err } var invite Invitation if release.VerifyRootEnvelope(d.PublicKey, raw, &invite) != nil || invite.V != 1 || invite.Bucket != d.Bucket || invite.Reference != in.Reference || !token.MatchString(invite.Enrollment) || ((strings.HasPrefix(in.Namespace, "fleet:") || strings.HasPrefix(in.Reference, "fleet-")) && in.Reference != "fleet-"+invite.Enrollment) || invite.EvidenceSHA256 != hostfloor.Digest(evidence) { return "", ErrRoster } return invite.Enrollment, nil }