| 1 | // Package boxid is the evidence a platform box presents to be enrolled in |
| 2 | // the roster (region HA spec 1 §5.1, ruling H11): the box's own AWS-signed |
| 3 | // instance identity document, published once under its box reference. The |
| 4 | // record grants nothing by itself. Enrollment verifies it (Verifier.Enroll) |
| 5 | // and copies the proven account, region and instance id into the roster |
| 6 | // entry's cloud field; from then on the roster, never this record, is what |
| 7 | // binds a reference to an instance. |
| 8 | package boxid |
| 9 | |
| 10 | import ( |
| 11 | "bytes" |
| 12 | "context" |
| 13 | "encoding/json" |
| 14 | "errors" |
| 15 | "fmt" |
| 16 | |
| 17 | "github.com/carlosframework/platform/internal/controlname" |
| 18 | "github.com/carlosframework/platform/internal/release" |
| 19 | ) |
| 20 | |
| 21 | const Prefix = "control/boxes/" |
| 22 | |
| 23 | // ValidBox reports whether ref can be a box reference: one DNS label, so |
| 24 | // it can never contain "." or "/" and no two references share a key. |
| 25 | func ValidBox(ref string) bool { return controlname.ValidLabel(ref) } |
| 26 | |
| 27 | // Key is where a box's record lives. Callers check ValidBox first. |
| 28 | func Key(box string) string { return Prefix + box + ".json" } |
| 29 | |
| 30 | type Record struct { |
| 31 | V int `json:"v"` |
| 32 | Box string `json:"box"` |
| 33 | IdentityDocument string `json:"identity_document"` |
| 34 | IdentitySignature string `json:"identity_signature"` |
| 35 | WrittenAt string `json:"written_at"` |
| 36 | } |
| 37 | |
| 38 | // Publish writes a box's record create-only. Writing the same bytes again |
| 39 | // succeeds; a different record for the same box is ErrCASConflict, because |
| 40 | // a record is never replaced. |
| 41 | func Publish(ctx context.Context, s release.Store, rec Record) error { |
| 42 | if !ValidBox(rec.Box) { |
| 43 | return fmt.Errorf("boxid: %q is not a box reference", rec.Box) |
| 44 | } |
| 45 | body, err := json.Marshal(rec) |
| 46 | if err != nil { |
| 47 | return fmt.Errorf("boxid: marshal: %w", err) |
| 48 | } |
| 49 | _, err = s.Put(ctx, Key(rec.Box), body, "") |
| 50 | if !errors.Is(err, release.ErrCASConflict) { |
| 51 | return err |
| 52 | } |
| 53 | cur, _, gerr := s.Get(ctx, Key(rec.Box)) |
| 54 | if gerr != nil { |
| 55 | return fmt.Errorf("boxid: %s exists and cannot be read: %w", rec.Box, gerr) |
| 56 | } |
| 57 | if bytes.Equal(cur, body) { |
| 58 | return nil |
| 59 | } |
| 60 | return fmt.Errorf("boxid: %s already has a different record: %w", rec.Box, release.ErrCASConflict) |
| 61 | } |
| 62 | |
| 63 | // Load returns the exact bytes of a box's record, which is what enrollment |
| 64 | // verifies and hashes. A missing record is os.ErrNotExist-wrapped. |
| 65 | func Load(ctx context.Context, s release.Store, box string) ([]byte, error) { |
| 66 | if !ValidBox(box) { |
| 67 | return nil, fmt.Errorf("%w: %q is not a box reference", ErrUnverified, box) |
| 68 | } |
| 69 | raw, _, err := s.Get(ctx, Key(box)) |
| 70 | return raw, err |
| 71 | } |
| 72 | |