carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package boxid
2
3import (
4 "bytes"
5 "crypto/sha256"
6 "crypto/x509"
7 "encoding/base64"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "io"
13)
14
15// ErrUnverified wraps every reason a record is not evidence for enrollment.
16var ErrUnverified = errors.New("boxid: record does not verify")
17
18// Cloud is the roster entry's cloud field (spec §2, §5.1): the instance a
19// box reference is enrolled as. Enrollment fills it from a verified record;
20// every later check (the termination fence, volume records) compares
21// against it and never against anything a box says about itself.
22type Cloud struct {
23 Provider string `json:"provider"`
24 Account string `json:"account"`
25 Region string `json:"region"`
26 InstanceID string `json:"instance_id"`
27}
28
29// Complete reports whether every field is set and the provider is AWS.
30func (c Cloud) Complete() bool {
31 return c.Provider == "aws" && c.Account != "" && c.Region != "" && c.InstanceID != ""
32}
33
34type Verifier struct {
35 Account string // the deployment's AWS account id
36 Certs map[string]*x509.Certificate // by AWS region
37}
38
39// Enrolled is one roster entry as enrollment needs to see it: its
40// reference and the instance it was enrolled as. Entries in every state
41// count, retired ones included: a retired reference never names a working
42// box again (spec §5.9), and a terminated instance never comes back.
43type Enrolled struct {
44 Box string
45 Cloud Cloud
46}
47
48// Expect is what the enrolling side knows before it reads the record.
49type Expect struct {
50 Box string // the reference being enrolled
51 AWSRegion string // the AWS region of the placement it is enrolled into
52 // TofuInstanceID is the region's tofu region-host instance_id output.
53 // The caller sets it when Box already appears in claims or edges files,
54 // as a box predating region HA does, and leaves it "" otherwise.
55 TofuInstanceID string
56 Roster []Enrolled
57}
58
59// Evidence is what enrollment records: the roster entry's cloud, and the
60// SHA-256 of the exact record bytes it was proven from.
61type Evidence struct {
62 Box string
63 Cloud Cloud
64 RecordSHA256 string
65}
66
67func refuse(format string, a ...any) error {
68 return fmt.Errorf("%w: %s", ErrUnverified, fmt.Sprintf(format, a...))
69}
70
71func decodeRecord(raw []byte) (Record, error) {
72 dec := json.NewDecoder(bytes.NewReader(raw))
73 dec.DisallowUnknownFields()
74 var rec Record
75 if err := dec.Decode(&rec); err != nil {
76 return Record{}, refuse("record is not JSON: %v", err)
77 }
78 if _, err := dec.Token(); err != io.EOF {
79 return Record{}, refuse("trailing data after the record")
80 }
81 return rec, nil
82}
83
84// Enroll checks raw, the exact bytes stored at Key(e.Box), as enrollment
85// evidence (spec §5.1, H11). In order: the reference, the record's own box
86// field, AWS's signature under the certificate for the document's region,
87// the deployment account, the AWS region expected for the placement, the
88// tofu instance id for a reference predating region HA, and the roster:
89// the reference already enrolled as another instance, or the instance
90// enrolled under another reference, is refused. A roster entry for the
91// reference with no proven cloud yet does not refuse it. A record copied
92// from one box to another's key carries the first box's signed instance
93// id, so the last check is what keeps one machine from holding two
94// references.
95//
96// The record's box field is not covered by AWS's signature: anything that
97// can write the key can name any reference. For a reference the roster
98// does not yet hold, Enroll proves only that some instance of the
99// deployment wrote the record, so the caller must bind that instance to
100// the reference independently (operator approval, the tofu id, or H16's
101// group confirmation) before enrolling it.
102func (v Verifier) Enroll(raw []byte, e Expect) (Evidence, error) {
103 if !ValidBox(e.Box) {
104 return Evidence{}, refuse("%q is not a box reference", e.Box)
105 }
106 if e.AWSRegion == "" {
107 return Evidence{}, refuse("no expected AWS region for %s", e.Box)
108 }
109 rec, err := decodeRecord(raw)
110 if err != nil {
111 return Evidence{}, err
112 }
113 if rec.V != 1 {
114 return Evidence{}, refuse("record version %d", rec.V)
115 }
116 if rec.Box != e.Box {
117 return Evidence{}, refuse("the record stored for %q names %q", e.Box, rec.Box)
118 }
119 doc, err := base64.StdEncoding.DecodeString(rec.IdentityDocument)
120 if err != nil {
121 return Evidence{}, refuse("document is not base64")
122 }
123 sig, err := base64.StdEncoding.DecodeString(rec.IdentitySignature)
124 if err != nil {
125 return Evidence{}, refuse("signature is not base64")
126 }
127 var d struct {
128 AccountID string `json:"accountId"`
129 Region string `json:"region"`
130 InstanceID string `json:"instanceId"`
131 }
132 if err := json.Unmarshal(doc, &d); err != nil {
133 return Evidence{}, refuse("document is not JSON")
134 }
135 cert := v.Certs[d.Region]
136 if cert == nil {
137 return Evidence{}, refuse("no signing certificate for region %q", d.Region)
138 }
139 if err := cert.CheckSignature(x509.SHA256WithRSA, doc, sig); err != nil {
140 return Evidence{}, refuse("signature: %v", err)
141 }
142 switch {
143 case d.InstanceID == "":
144 return Evidence{}, refuse("document names no instance")
145 case v.Account == "" || d.AccountID != v.Account:
146 return Evidence{}, refuse("account %q is not the deployment's", d.AccountID)
147 case d.Region != e.AWSRegion:
148 return Evidence{}, refuse("the box is in %q, the placement is in %q", d.Region, e.AWSRegion)
149 case e.TofuInstanceID != "" && e.TofuInstanceID != d.InstanceID:
150 return Evidence{}, refuse("tofu records instance %q for this region, the record %q", e.TofuInstanceID, d.InstanceID)
151 }
152 for _, en := range e.Roster {
153 if en.Box == e.Box && en.Cloud.InstanceID != "" && en.Cloud.InstanceID != d.InstanceID {
154 return Evidence{}, refuse("%s is enrolled as instance %q", e.Box, en.Cloud.InstanceID)
155 }
156 if en.Box != e.Box && en.Cloud.InstanceID == d.InstanceID {
157 return Evidence{}, refuse("instance %s is enrolled as %s", d.InstanceID, en.Box)
158 }
159 }
160 sum := sha256.Sum256(raw)
161 return Evidence{
162 Box: e.Box,
163 Cloud: Cloud{Provider: "aws", Account: d.AccountID, Region: d.Region, InstanceID: d.InstanceID},
164 RecordSHA256: hex.EncodeToString(sum[:]),
165 }, nil
166}
167