carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3// Door keys, storage half (#175 P7). The console's database is the
4// authority for which door keys an app has; the bucket only ever holds a
5// signed projection of these rows (apidoorkeys.go). Only a key's sha256 is
6// stored, never its secret.
7//
8// Each app has a revision counter, bumped in the same transaction as every
9// insert and revoke, so a published record's revision always names exactly
10// one key set. DoorKeyAdvanceRevision moves it forward past a revision the
11// bucket already carries, which keeps revisions monotonic across a restored
12// database.
13
14import (
15 "context"
16 "database/sql"
17 "errors"
18 "fmt"
19 "os"
20 "time"
21
22 "github.com/carlosframework/platform/internal/doorkey"
23)
24
25// errDoorKeysFull is DoorKeyInsert's refusal when the app already has
26// doorkey.MaxKeys live keys.
27var errDoorKeysFull = errors.New("console: door keys full")
28
29// migrateDoorKeys creates the door-key tables. Additive and idempotent,
30// called from Store.migrate.
31func (s *Store) migrateDoorKeys() error {
32 for _, stmt := range []string{
33 `CREATE TABLE IF NOT EXISTS door_keys (
34 account_id INTEGER NOT NULL,
35 app TEXT NOT NULL,
36 id TEXT NOT NULL,
37 sha256 TEXT NOT NULL,
38 label TEXT NOT NULL DEFAULT '',
39 created_at INTEGER NOT NULL,
40 revoked_at INTEGER NOT NULL DEFAULT 0,
41 PRIMARY KEY (account_id, app, id)
42);`,
43 `CREATE TABLE IF NOT EXISTS door_key_revisions (
44 account_id INTEGER NOT NULL,
45 app TEXT NOT NULL,
46 revision INTEGER NOT NULL,
47 PRIMARY KEY (account_id, app)
48);`,
49 } {
50 if _, err := s.db.Exec(stmt); err != nil {
51 return err
52 }
53 }
54 return nil
55}
56
57// bumpDoorKeyRevision adds one to the app's counter inside tx, creating the
58// row at 1, and returns the new revision.
59func bumpDoorKeyRevision(tx *sql.Tx, accountID int64, app string) (uint64, error) {
60 if _, err := tx.Exec(`INSERT INTO door_key_revisions (account_id, app, revision) VALUES (?, ?, 1)
61ON CONFLICT (account_id, app) DO UPDATE SET revision = revision + 1`, accountID, app); err != nil {
62 return 0, err
63 }
64 var rev uint64
65 err := tx.QueryRow(`SELECT revision FROM door_key_revisions WHERE account_id = ? AND app = ?`,
66 accountID, app).Scan(&rev)
67 return rev, err
68}
69
70// DoorKeyInsert stores a newly minted key's digest and bumps the app's
71// revision in the same transaction. It refuses with errDoorKeysFull when the
72// app already has doorkey.MaxKeys live keys.
73func (s *Store) claimRawDoorKeyInsert(accountID int64, app string, e doorkey.Entry) (uint64, error) {
74 tx, err := s.db.Begin()
75 if err != nil {
76 return 0, err
77 }
78 defer tx.Rollback()
79 var live int
80 if err := tx.QueryRow(`SELECT COUNT(*) FROM door_keys WHERE account_id = ? AND app = ? AND revoked_at = 0`,
81 accountID, app).Scan(&live); err != nil {
82 return 0, err
83 }
84 if live >= doorkey.MaxKeys {
85 return 0, errDoorKeysFull
86 }
87 created := e.CreatedAt
88 if created.IsZero() {
89 created = time.Now()
90 }
91 if _, err := tx.Exec(`INSERT INTO door_keys (account_id, app, id, sha256, label, created_at)
92VALUES (?, ?, ?, ?, ?, ?)`, accountID, app, e.ID, e.SHA256, e.Label, created.UnixNano()); err != nil {
93 return 0, err
94 }
95 rev, err := bumpDoorKeyRevision(tx, accountID, app)
96 if err != nil {
97 return 0, err
98 }
99 return rev, tx.Commit()
100}
101
102// DoorKeyRevoke marks one live key revoked and bumps the app's revision in
103// the same transaction. An unknown or already revoked id is os.ErrNotExist.
104func (s *Store) claimRawDoorKeyRevoke(accountID int64, app, id string) (uint64, error) {
105 tx, err := s.db.Begin()
106 if err != nil {
107 return 0, err
108 }
109 defer tx.Rollback()
110 res, err := tx.Exec(`UPDATE door_keys SET revoked_at = ?
111WHERE account_id = ? AND app = ? AND id = ? AND revoked_at = 0`, time.Now().UnixNano(), accountID, app, id)
112 if err != nil {
113 return 0, err
114 }
115 if n, err := res.RowsAffected(); err != nil {
116 return 0, err
117 } else if n == 0 {
118 return 0, os.ErrNotExist
119 }
120 rev, err := bumpDoorKeyRevision(tx, accountID, app)
121 if err != nil {
122 return 0, err
123 }
124 return rev, tx.Commit()
125}
126
127// revokeAllDoorKeys revokes every live key the account holds under app inside
128// the caller's transaction, bumping the revision when it revoked any. It is
129// for an app name that stops belonging to an app (PurgeAppRow, MoveApp): a
130// key left live there would let a later app of the same name be called with
131// a key nobody minted for it.
132func revokeAllDoorKeys(tx *sql.Tx, accountID int64, app string) error {
133 res, err := tx.Exec(`UPDATE door_keys SET revoked_at = ?
134WHERE account_id = ? AND app = ? AND revoked_at = 0`, time.Now().UnixNano(), accountID, app)
135 if err != nil {
136 return err
137 }
138 if n, err := res.RowsAffected(); err != nil || n == 0 {
139 return err
140 }
141 _, err = bumpDoorKeyRevision(tx, accountID, app)
142 return err
143}
144
145// DoorKeysLive reads the app's live keys and its revision in one
146// transaction, so the two always describe the same key set. An app with no
147// revision row answers no keys at revision 0.
148func (s *Store) claimRawDoorKeysLive(accountID int64, app string) ([]doorkey.Entry, uint64, error) {
149 tx, err := s.db.Begin()
150 if err != nil {
151 return nil, 0, err
152 }
153 defer tx.Rollback()
154 var rev uint64
155 err = tx.QueryRow(`SELECT revision FROM door_key_revisions WHERE account_id = ? AND app = ?`,
156 accountID, app).Scan(&rev)
157 if err != nil && !errors.Is(err, sql.ErrNoRows) {
158 return nil, 0, err
159 }
160 rows, err := tx.Query(`SELECT id, sha256, label, created_at FROM door_keys
161WHERE account_id = ? AND app = ? AND revoked_at = 0 ORDER BY created_at, id`, accountID, app)
162 if err != nil {
163 return nil, 0, err
164 }
165 defer rows.Close()
166 keys := []doorkey.Entry{}
167 for rows.Next() {
168 var e doorkey.Entry
169 var created int64
170 if err := rows.Scan(&e.ID, &e.SHA256, &e.Label, &created); err != nil {
171 return nil, 0, err
172 }
173 e.CreatedAt = time.Unix(0, created).UTC()
174 keys = append(keys, e)
175 }
176 if err := rows.Err(); err != nil {
177 return nil, 0, err
178 }
179 return keys, rev, tx.Commit()
180}
181
182// DoorKeyApp is one app the re-sign loop publishes.
183type DoorKeyApp struct {
184 AccountID int64
185 Sqid, App string
186}
187
188// DoorKeyApps lists every app with a revision row: every app that has ever
189// had a key minted, including one whose keys are now all revoked, because
190// its empty record still has to stay fresh.
191func (s *Store) DoorKeyApps() ([]DoorKeyApp, error) {
192 rows, err := s.db.Query(`SELECT r.account_id, a.sqid, r.app FROM door_key_revisions r
193JOIN accounts a ON a.id = r.account_id ORDER BY a.sqid, r.app`)
194 if err != nil {
195 return nil, err
196 }
197 defer rows.Close()
198 var out []DoorKeyApp
199 for rows.Next() {
200 var d DoorKeyApp
201 if err := rows.Scan(&d.AccountID, &d.Sqid, &d.App); err != nil {
202 return nil, err
203 }
204 out = append(out, d)
205 }
206 return out, rows.Err()
207}
208
209// DoorKeyAdvanceRevision sets the app's revision to past+1 when it is below
210// past, and returns the resulting revision. The publisher calls it when the
211// bucket carries a verified revision above the database's (a restore, or a
212// second writer), so the next record it signs is above anything a box may
213// already have admitted.
214func (s *Store) claimRawDoorKeyAdvanceRevision(accountID int64, app string, past uint64) (uint64, error) {
215 if past >= 1<<63-1 {
216 return 0, fmt.Errorf("console: door-key revision %d out of range", past)
217 }
218 tx, err := s.db.Begin()
219 if err != nil {
220 return 0, err
221 }
222 defer tx.Rollback()
223 if _, err := tx.Exec(`INSERT INTO door_key_revisions (account_id, app, revision) VALUES (?, ?, ?)
224ON CONFLICT (account_id, app) DO UPDATE SET revision = excluded.revision WHERE revision < ?`,
225 accountID, app, int64(past+1), int64(past)); err != nil {
226 return 0, err
227 }
228 var rev uint64
229 if err := tx.QueryRow(`SELECT revision FROM door_key_revisions WHERE account_id = ? AND app = ?`,
230 accountID, app).Scan(&rev); err != nil {
231 return 0, err
232 }
233 return rev, tx.Commit()
234}
235func (s *Store) DoorKeyAdvanceRevision(accountID int64, app string, past uint64) (claimResult0 uint64, claimResult1 error) {
236 return s.DoorKeyAdvanceRevisionContext(context.Background(), accountID, app, past)
237}
238func (s *Store) DoorKeyAdvanceRevisionContext(claimContext context.Context, accountID int64, app string, past uint64) (claimResult0 uint64, claimResult1 error) {
239 claimResult1 = s.withAutoAppSQL(claimContext, accountID, app, "sql.DoorKeyAdvanceRevision", func(claimView *Store) error {
240 claimResult0, claimResult1 = claimView.claimRawDoorKeyAdvanceRevision(accountID, app, past)
241 return claimResult1
242 })
243 return
244}
245func (s *Store) DoorKeyInsert(accountID int64, app string, e doorkey.Entry) (claimResult0 uint64, claimResult1 error) {
246 return s.DoorKeyInsertContext(context.Background(), accountID, app, e)
247}
248func (s *Store) DoorKeyInsertContext(claimContext context.Context, accountID int64, app string, e doorkey.Entry) (claimResult0 uint64, claimResult1 error) {
249 claimResult1 = s.withAutoAppSQL(claimContext, accountID, app, "sql.DoorKeyInsert", func(claimView *Store) error {
250 claimResult0, claimResult1 = claimView.claimRawDoorKeyInsert(accountID, app, e)
251 return claimResult1
252 })
253 return
254}
255func (s *Store) DoorKeyRevoke(accountID int64, app, id string) (claimResult0 uint64, claimResult1 error) {
256 return s.DoorKeyRevokeContext(context.Background(), accountID, app, id)
257}
258func (s *Store) DoorKeyRevokeContext(claimContext context.Context, accountID int64, app, id string) (claimResult0 uint64, claimResult1 error) {
259 claimResult1 = s.withAutoAppSQL(claimContext, accountID, app, "sql.DoorKeyRevoke", func(claimView *Store) error {
260 claimResult0, claimResult1 = claimView.claimRawDoorKeyRevoke(accountID, app, id)
261 return claimResult1
262 })
263 return
264}
265func (s *Store) DoorKeysLive(accountID int64, app string) (claimResult0 []doorkey.Entry, claimResult1 uint64, claimResult2 error) {
266 return s.DoorKeysLiveContext(context.Background(), accountID, app)
267}
268
269// DoorKeysLiveContext is a read: its transaction only SELECTs, so it takes no
270// app mutation and the door-key list stays a read under a foreign operation.
271// The armed triggers still refuse any write it could ever grow.
272func (s *Store) DoorKeysLiveContext(claimContext context.Context, accountID int64, app string) (claimResult0 []doorkey.Entry, claimResult1 uint64, claimResult2 error) {
273 return s.claimRawDoorKeysLive(accountID, app)
274}
275