package boxid import ( "bytes" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "io" ) // ErrUnverified wraps every reason a record is not evidence for enrollment. var ErrUnverified = errors.New("boxid: record does not verify") // Cloud is the roster entry's cloud field (spec §2, §5.1): the instance a // box reference is enrolled as. Enrollment fills it from a verified record; // every later check (the termination fence, volume records) compares // against it and never against anything a box says about itself. type Cloud struct { Provider string `json:"provider"` Account string `json:"account"` Region string `json:"region"` InstanceID string `json:"instance_id"` } // Complete reports whether every field is set and the provider is AWS. func (c Cloud) Complete() bool { return c.Provider == "aws" && c.Account != "" && c.Region != "" && c.InstanceID != "" } type Verifier struct { Account string // the deployment's AWS account id Certs map[string]*x509.Certificate // by AWS region } // Enrolled is one roster entry as enrollment needs to see it: its // reference and the instance it was enrolled as. Entries in every state // count, retired ones included: a retired reference never names a working // box again (spec §5.9), and a terminated instance never comes back. type Enrolled struct { Box string Cloud Cloud } // Expect is what the enrolling side knows before it reads the record. type Expect struct { Box string // the reference being enrolled AWSRegion string // the AWS region of the placement it is enrolled into // TofuInstanceID is the region's tofu region-host instance_id output. // The caller sets it when Box already appears in claims or edges files, // as a box predating region HA does, and leaves it "" otherwise. TofuInstanceID string Roster []Enrolled } // Evidence is what enrollment records: the roster entry's cloud, and the // SHA-256 of the exact record bytes it was proven from. type Evidence struct { Box string Cloud Cloud RecordSHA256 string } func refuse(format string, a ...any) error { return fmt.Errorf("%w: %s", ErrUnverified, fmt.Sprintf(format, a...)) } func decodeRecord(raw []byte) (Record, error) { dec := json.NewDecoder(bytes.NewReader(raw)) dec.DisallowUnknownFields() var rec Record if err := dec.Decode(&rec); err != nil { return Record{}, refuse("record is not JSON: %v", err) } if _, err := dec.Token(); err != io.EOF { return Record{}, refuse("trailing data after the record") } return rec, nil } // Enroll checks raw, the exact bytes stored at Key(e.Box), as enrollment // evidence (spec §5.1, H11). In order: the reference, the record's own box // field, AWS's signature under the certificate for the document's region, // the deployment account, the AWS region expected for the placement, the // tofu instance id for a reference predating region HA, and the roster: // the reference already enrolled as another instance, or the instance // enrolled under another reference, is refused. A roster entry for the // reference with no proven cloud yet does not refuse it. A record copied // from one box to another's key carries the first box's signed instance // id, so the last check is what keeps one machine from holding two // references. // // The record's box field is not covered by AWS's signature: anything that // can write the key can name any reference. For a reference the roster // does not yet hold, Enroll proves only that some instance of the // deployment wrote the record, so the caller must bind that instance to // the reference independently (operator approval, the tofu id, or H16's // group confirmation) before enrolling it. func (v Verifier) Enroll(raw []byte, e Expect) (Evidence, error) { if !ValidBox(e.Box) { return Evidence{}, refuse("%q is not a box reference", e.Box) } if e.AWSRegion == "" { return Evidence{}, refuse("no expected AWS region for %s", e.Box) } rec, err := decodeRecord(raw) if err != nil { return Evidence{}, err } if rec.V != 1 { return Evidence{}, refuse("record version %d", rec.V) } if rec.Box != e.Box { return Evidence{}, refuse("the record stored for %q names %q", e.Box, rec.Box) } doc, err := base64.StdEncoding.DecodeString(rec.IdentityDocument) if err != nil { return Evidence{}, refuse("document is not base64") } sig, err := base64.StdEncoding.DecodeString(rec.IdentitySignature) if err != nil { return Evidence{}, refuse("signature is not base64") } var d struct { AccountID string `json:"accountId"` Region string `json:"region"` InstanceID string `json:"instanceId"` } if err := json.Unmarshal(doc, &d); err != nil { return Evidence{}, refuse("document is not JSON") } cert := v.Certs[d.Region] if cert == nil { return Evidence{}, refuse("no signing certificate for region %q", d.Region) } if err := cert.CheckSignature(x509.SHA256WithRSA, doc, sig); err != nil { return Evidence{}, refuse("signature: %v", err) } switch { case d.InstanceID == "": return Evidence{}, refuse("document names no instance") case v.Account == "" || d.AccountID != v.Account: return Evidence{}, refuse("account %q is not the deployment's", d.AccountID) case d.Region != e.AWSRegion: return Evidence{}, refuse("the box is in %q, the placement is in %q", d.Region, e.AWSRegion) case e.TofuInstanceID != "" && e.TofuInstanceID != d.InstanceID: return Evidence{}, refuse("tofu records instance %q for this region, the record %q", e.TofuInstanceID, d.InstanceID) } for _, en := range e.Roster { if en.Box == e.Box && en.Cloud.InstanceID != "" && en.Cloud.InstanceID != d.InstanceID { return Evidence{}, refuse("%s is enrolled as instance %q", e.Box, en.Cloud.InstanceID) } if en.Box != e.Box && en.Cloud.InstanceID == d.InstanceID { return Evidence{}, refuse("instance %s is enrolled as %s", d.InstanceID, en.Box) } } sum := sha256.Sum256(raw) return Evidence{ Box: e.Box, Cloud: Cloud{Provider: "aws", Account: d.AccountID, Region: d.Region, InstanceID: d.InstanceID}, RecordSHA256: hex.EncodeToString(sum[:]), }, nil }