oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package operator
2
3import (
4 "context"
5 "crypto/rand"
6 "crypto/sha256"
7 "database/sql"
8 "encoding/base64"
9 "errors"
10 "math"
11
12 "amadan.net/oficina/home/internal/home/directorydata"
13 "amadan.net/oficina/home/roster"
14 family "amadan.net/rastrillo/rastrillo/crypto"
15 "github.com/go-json-experiment/json/jsontext"
16)
17
18type InputBase struct {
19 Version int `json:"version"`
20 Operation string `json:"operation"`
21 OperationID string `json:"operation_id"`
22}
23
24func (b InputBase) matches(r Request) bool {
25 return b.Version == 1 && b.Operation == r.authorization.Action && b.OperationID == r.authorization.OperationID
26}
27
28type InitializeInput struct {
29 InputBase
30 ExpectedGeneration *string `json:"expected_generation"`
31 Generation string `json:"generation"`
32 Baseline string `json:"baseline_attestation"`
33}
34type SeedInput struct {
35 InputBase
36 Generation string `json:"expected_generation"`
37 TeamID int64 `json:"team_id"`
38 TeamSQID string `json:"team_sqid"`
39 Label string `json:"label"`
40 Name string `json:"display_name"`
41 Receipt jsontext.Value `json:"receipt"`
42}
43type ProvisionInput struct {
44 InputBase
45 Generation string `json:"expected_generation"`
46 TeamSQID string `json:"team_sqid"`
47 App string `json:"app"`
48 Consumer string `json:"consumer"`
49 Audience string `json:"audience"`
50 Sequence int64 `json:"expected_sequence"`
51}
52
53type Result struct {
54 Attempt string `json:"attempt_id,omitempty"`
55 Expires int64 `json:"expires_at,omitempty"`
56 Version int `json:"version"`
57 OperationID string `json:"operation_id"`
58 Outcome string `json:"outcome"`
59 Generation string `json:"generation,omitempty"`
60 TeamID int64 `json:"team_id,omitempty"`
61 TeamSQID string `json:"team_sqid,omitempty"`
62 Epoch int64 `json:"epoch,omitempty"`
63 Record jsontext.Value `json:"record,omitempty"`
64 Bearer string `json:"bearer,omitempty"`
65 Receipt jsontext.Value `json:"receipt,omitempty"`
66}
67
68func resultFor(r Request) Result {
69 return Result{Version: 1, OperationID: r.authorization.OperationID, Outcome: "committed"}
70}
71
72type Directory struct {
73 engine *Engine
74 owner *OwnerVerifier
75 signer *family.Keypair
76 keys []roster.Key
77 names *directorydata.Store
78 policy directorydata.Config
79 apps map[string]bool
80}
81
82func NewDirectory(engine *Engine, owner *OwnerVerifier, signer *family.Keypair, keys []roster.Key, policy directorydata.Config) (*Directory, error) {
83 if engine == nil || engine.verifier.pins.Role != "directory" || owner == nil || owner.directory != engine.verifier.pins.Origin || !signingKey(signer) || len(keys) == 0 || len(keys) > 8 {
84 return nil, ErrRefused
85 }
86 names, e := directorydata.New(engine.db, engine.sealer, policy)
87 if e != nil {
88 return nil, ErrRefused
89 }
90 seen := map[string]bool{}
91 for _, k := range [][]byte{engine.verifier.key, owner.key, signer.SignPub()} {
92 if seen[string(k)] {
93 return nil, ErrRefused
94 }
95 seen[string(k)] = true
96 }
97 ids := map[string]bool{}
98 for _, k := range keys {
99 p, e := public(k.Public)
100 if e != nil || !atom(k.ID, 64) || ids[k.ID] || seen[string(p)] {
101 return nil, ErrRefused
102 }
103 ids[k.ID] = true
104 seen[string(p)] = true
105 }
106 d := &Directory{engine, owner, signer, append([]roster.Key(nil), keys...), names, policy, map[string]bool{}}
107 d.policy.Apps = append([]string(nil), policy.Apps...)
108 for _, app := range policy.Apps {
109 d.apps[app] = true
110 }
111 return d, nil
112}
113func atom(s string, max int) bool {
114 if len(s) == 0 || len(s) > max {
115 return false
116 }
117 for _, c := range s {
118 if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '-') {
119 return false
120 }
121 }
122 return true
123}
124func (d *Directory) Execute(ctx context.Context, raw []byte) ([]byte, error) {
125 return d.engine.Execute(ctx, raw, func(ctx context.Context, c *sql.Conn, r Request) ([]byte, error) {
126 switch r.authorization.Action {
127 case "directory.initialize":
128 return d.initialize(ctx, c, r)
129 case "directory.seed-team":
130 return d.seed(ctx, c, r)
131 case "directory.provision-consumer":
132 return d.provision(ctx, c, r)
133 case "directory.verify-consumer":
134 return d.verifyConsumer(ctx, c, r)
135 case "directory.activate-consumer":
136 return d.activate(ctx, c, r)
137 default:
138 return nil, ErrRefused
139 }
140 })
141}
142func generation(ctx context.Context, c *sql.Conn, expected string) error {
143 var actual string
144 if !CanonicalToken(expected) {
145 return ErrRefused
146 }
147 if e := c.QueryRowContext(ctx, "SELECT generation FROM roster_generation WHERE singleton=1").Scan(&actual); e != nil {
148 return ErrUnavailable
149 }
150 if actual != expected {
151 return ErrRefused
152 }
153 return nil
154}
155func (d *Directory) initialize(ctx context.Context, c *sql.Conn, r Request) ([]byte, error) {
156 var in InitializeInput
157 if DecodeInput(r, &in) != nil || !in.matches(r) || !r.authorization.Initial || in.ExpectedGeneration != nil || !CanonicalToken(in.Generation) || in.Baseline != "operator-attests-never-provisioned" {
158 return nil, ErrRefused
159 }
160 for _, table := range []string{"roster_generation", "roster_consumers", "roster_tokens", "roster_operator_receipts"} {
161 var n int
162 if c.QueryRowContext(ctx, "SELECT count(*) FROM "+table).Scan(&n) != nil {
163 return nil, ErrUnavailable
164 }
165 if n != 0 {
166 return nil, ErrRefused
167 }
168 }
169 if _, e := c.ExecContext(ctx, "INSERT INTO roster_generation VALUES(1,?)", in.Generation); e != nil {
170 return nil, e
171 }
172 result := resultFor(r)
173 result.Generation = in.Generation
174 raw, e := encode(result)
175 if e != nil {
176 return nil, e
177 }
178 h := sha256.Sum256(raw)
179 if _, e = c.ExecContext(ctx, "INSERT INTO roster_operator_receipts VALUES(?,1,'bootstrap',NULL,?,?,?)", r.authorization.OperationID, in.Generation, h[:], d.engine.now().Unix()); e != nil {
180 return nil, e
181 }
182 return raw, nil
183}
184func nextTeam(ctx context.Context, c *sql.Conn) (int64, string, error) {
185 var largest, sequence int64
186 if c.QueryRowContext(ctx, "SELECT coalesce(max(id),0) FROM teams").Scan(&largest) != nil {
187 return 0, "", ErrUnavailable
188 }
189 e := c.QueryRowContext(ctx, "SELECT seq FROM sqlite_sequence WHERE name='teams'").Scan(&sequence)
190 if e != nil && !errors.Is(e, sql.ErrNoRows) {
191 return 0, "", ErrUnavailable
192 }
193 if sequence > largest {
194 largest = sequence
195 }
196 if largest < 0 || largest == math.MaxInt64 {
197 return 0, "", ErrRefused
198 }
199 id := largest + 1
200 s, e := TeamSQID(id)
201 return id, s, e
202}
203func (d *Directory) seed(ctx context.Context, c *sql.Conn, r Request) ([]byte, error) {
204 var in SeedInput
205 if DecodeInput(r, &in) != nil || !in.matches(r) || r.authorization.Initial || !validTeam(in.TeamID, in.TeamSQID) {
206 return nil, ErrRefused
207 }
208 if e := generation(ctx, c, in.Generation); e != nil {
209 return nil, e
210 }
211 owner, e := d.owner.Verify(in.Receipt, d.engine.now())
212 if e != nil || owner.TeamID != in.TeamID || owner.TeamSQID != in.TeamSQID {
213 return nil, ErrRefused
214 }
215 var used int
216 e = c.QueryRowContext(ctx, "SELECT 1 FROM directory_operator_owner_receipts WHERE issuer=? AND owner_operation_id=?", owner.Issuer, owner.OperationID).Scan(&used)
217 if e == nil {
218 return nil, ErrRefused
219 }
220 if !errors.Is(e, sql.ErrNoRows) {
221 return nil, ErrUnavailable
222 }
223 id, sqid, e := nextTeam(ctx, c)
224 if e != nil {
225 return nil, e
226 }
227 if id != in.TeamID || sqid != in.TeamSQID {
228 return nil, ErrRefused
229 }
230 // The unique label belongs to the team across all sibling applications.
231 if !directorydata.ValidLabel(in.Label) || len(in.Name) > 255 {
232 return nil, ErrRefused
233 }
234 name, e := d.names.SealTeamName(id, in.Name)
235 if e != nil {
236 return nil, ErrRefused
237 }
238 var collision int
239 e = c.QueryRowContext(ctx, "SELECT 1 FROM teams WHERE label=? OR sqid=?", in.Label, in.TeamSQID).Scan(&collision)
240 if e == nil {
241 return nil, ErrRefused
242 }
243 if !errors.Is(e, sql.ErrNoRows) {
244 return nil, ErrUnavailable
245 }
246 res, e := c.ExecContext(ctx, "INSERT INTO teams(sqid,label,sealed_name,created_at) VALUES(?,?,?,?)", sqid, in.Label, name, d.engine.now().Unix())
247 if e != nil {
248 return nil, e
249 }
250 inserted, e := res.LastInsertId()
251 if e != nil || inserted != id {
252 return nil, ErrUnavailable
253 }
254 if _, e = c.ExecContext(ctx, "INSERT INTO memberships(team_id,person_ref,role) VALUES(?,?,'owner')", id, owner.PersonRef); e != nil {
255 return nil, e
256 }
257 if _, e = c.ExecContext(ctx, "INSERT INTO directory_operator_owner_receipts VALUES(?,?,?,?)", owner.Issuer, owner.OperationID, r.authorization.OperationID, id); e != nil {
258 return nil, e
259 }
260 result := resultFor(r)
261 result.Generation = in.Generation
262 result.TeamID = id
263 result.TeamSQID = sqid
264 if c.QueryRowContext(ctx, "SELECT roster_epoch FROM teams WHERE id=?", id).Scan(&result.Epoch) != nil {
265 return nil, ErrUnavailable
266 }
267 return encode(result)
268}
269func (d *Directory) provision(ctx context.Context, c *sql.Conn, r Request) ([]byte, error) {
270 var in ProvisionInput
271 if DecodeInput(r, &in) != nil || !in.matches(r) || r.authorization.Initial || in.Sequence != 0 || !d.apps[in.App] || !atom(in.Consumer, 64) || !Origin(in.Audience) {
272 return nil, ErrRefused
273 }
274 if e := generation(ctx, c, in.Generation); e != nil {
275 return nil, e
276 }
277 route := directorydata.Route{TeamSQID: in.TeamSQID, App: in.App, Origin: in.Audience, ConsumerID: in.Consumer}
278 var epoch int64
279 if e := c.QueryRowContext(ctx, "SELECT id,label,roster_epoch FROM teams WHERE sqid=?", in.TeamSQID).Scan(&route.TeamID, &route.Label, &epoch); e != nil {
280 if errors.Is(e, sql.ErrNoRows) {
281 return nil, ErrRefused
282 }
283 return nil, ErrUnavailable
284 }
285 if !validTeam(route.TeamID, route.TeamSQID) || !d.policy.ValidateRoute(route) || epoch <= 0 {
286 return nil, ErrRefused
287 }
288 var owner string
289 if e := c.QueryRowContext(ctx, "SELECT person_ref FROM memberships WHERE team_id=? AND role='owner' LIMIT 1", route.TeamID).Scan(&owner); e != nil || !PersonRef(owner) {
290 return nil, ErrRefused
291 }
292 var conflict int
293 e := c.QueryRowContext(ctx, "SELECT 1 FROM team_instances WHERE (team_id=? AND app=?) OR consumer_id=? OR origin=?", route.TeamID, in.App, in.Consumer, in.Audience).Scan(&conflict)
294 if e == nil {
295 return nil, ErrRefused
296 }
297 if !errors.Is(e, sql.ErrNoRows) {
298 return nil, ErrUnavailable
299 }
300 random := make([]byte, 32)
301 if _, e = rand.Read(random); e != nil {
302 return nil, ErrUnavailable
303 }
304 token := base64.RawURLEncoding.EncodeToString(random)
305 hash := sha256.Sum256([]byte(token))
306 binding := roster.Bindings{Issuer: d.owner.issuer, Directory: d.owner.directory, Consumer: in.Consumer, App: in.App, Team: in.TeamSQID, Audience: in.Audience}
307 p := roster.Provisioning{Version: 1, Operation: r.authorization.OperationID, Sequence: 1, Bindings: binding, Generation: in.Generation, MinEpoch: epoch, Keys: d.keys}
308 record, e := roster.SignProvisioning(d.signer, keyID(d.signer.SignPub()), p)
309 if e != nil {
310 return nil, ErrUnavailable
311 }
312 if _, e = roster.VerifyProvisioning(record, []roster.Key{{ID: keyID(d.signer.SignPub()), Public: base64.RawURLEncoding.EncodeToString(d.signer.SignPub())}}, binding); e != nil {
313 return nil, ErrUnavailable
314 }
315 if _, e = c.ExecContext(ctx, "INSERT INTO team_instances VALUES(?,?,?,?,'pending')", route.TeamID, in.App, in.Audience, in.Consumer); e != nil {
316 return nil, e
317 }
318 if _, e = c.ExecContext(ctx, "INSERT INTO roster_consumers VALUES(?,?,?,?,1)", in.Consumer, route.TeamID, in.App, in.Audience); e != nil {
319 return nil, e
320 }
321 if _, e = c.ExecContext(ctx, "INSERT INTO roster_tokens VALUES(?,?,'current',?,NULL)", hash[:], in.Consumer, d.engine.now().Unix()); e != nil {
322 return nil, e
323 }
324 recordHash := sha256.Sum256(record)
325 if _, e = c.ExecContext(ctx, "INSERT INTO roster_operator_receipts VALUES(?,1,'provision',?,?,?,?)", r.authorization.OperationID, in.Consumer, in.Generation, recordHash[:], d.engine.now().Unix()); e != nil {
326 return nil, e
327 }
328 if _, e = c.ExecContext(ctx, "INSERT INTO directory_operator_consumer_records VALUES(?,?,?,?,?)", in.Consumer, r.authorization.OperationID, int64(1), in.Generation, recordHash[:]); e != nil {
329 return nil, e
330 }
331 result := resultFor(r)
332 result.Generation = in.Generation
333 result.TeamID = route.TeamID
334 result.TeamSQID = in.TeamSQID
335 result.Epoch = epoch
336 result.Record = record
337 result.Bearer = token
338 return encode(result)
339}
340