oficina / home Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package home
4
5import (
6 "context"
7 "crypto/sha256"
8 "database/sql"
9 "embed"
10 "encoding/base64"
11 "encoding/hex"
12 "encoding/json"
13 "strconv"
14 "strings"
15 "unicode"
16 "unicode/utf8"
17
18 "amadan.net/oficina/home/handoff"
19 "amadan.net/oficina/home/internal/home/directorydata"
20 "amadan.net/oficina/home/internal/home/seal"
21 "amadan.net/oficina/home/internal/operator"
22 "amadan.net/rastrillo/rastrillo/migrate"
23)
24
25// This component has no command or serving entry point. Only the future verified
26// launch/enrollment adapter may supply these expectations. Synthetic tests are
27// currently the sole caller; a non-nil callback is not platform authorization.
28type compatibilityConfig struct {
29 role, origin string
30 dek, pepper, signin, mac []byte
31 expectedKeys map[string]string
32 state compatibilityState
33 witness, generation string
34 team int64
35 verify func(context.Context, operator.ReadonlyExpectation) error
36}
37type compatibilityState struct {
38 Origin, Installation, Registry, Device, Head, KeyID string
39 Sequence, Completed int64
40}
41type schemaObject struct{ Type, Name, Table, SQL string }
42
43//go:embed compatibility_profiles/*.json
44var compatibilityProfiles embed.FS
45
46func compatibilitySchema(role string) *migrate.Set {
47 if role == "credential" {
48 return migrate.Merge(BootSchema, operator.CredentialSchema)
49 }
50 if role == "directory" {
51 return migrate.Merge(directorySchema, directorydata.OperatorSchema, operator.DirectorySchema)
52 }
53 return nil
54}
55
56// inspectCompatibilityDatabase implements only the database portion of the
57// approved checker. Config-facts, launch parsing and enrollment are not mounted.
58// It conservatively accepts initialized operator state only; a reviewed caller
59// for the separately enrolled empty baseline does not exist yet.
60func inspectCompatibilityDatabase(ctx context.Context, expected operator.ReadonlyExpectation, cfg compatibilityConfig) error {
61 schema := compatibilitySchema(cfg.role)
62 if schema == nil || !ValidPersonRef(cfg.witness) || !directorydata.ValidOrigin(cfg.origin) || cfg.verify == nil || cfg.state.Origin != cfg.origin || cfg.state.Sequence <= 0 || cfg.state.Completed <= 0 || !canonicalHash(cfg.state.Head) || cfg.state.Installation == "" || cfg.state.Registry == "" || cfg.state.Device == "" || cfg.state.KeyID == "" {
63 return operator.ErrRefused
64 }
65 keys := map[string][]byte{"dek": cfg.dek, "mac": cfg.mac}
66 if cfg.role == "credential" {
67 keys["pepper"] = cfg.pepper
68 keys["signin"] = cfg.signin
69 } else if len(cfg.pepper) != 0 || len(cfg.signin) != 0 {
70 return operator.ErrRefused
71 }
72 if len(cfg.expectedKeys) != len(keys) {
73 return operator.ErrRefused
74 }
75 for name, key := range keys {
76 if len(key) != 32 {
77 return operator.ErrRefused
78 }
79 h := sha256.Sum256(key)
80 if cfg.expectedKeys[name] != hex.EncodeToString(h[:]) {
81 return operator.ErrRefused
82 }
83 }
84 sealer, e := seal.New(cfg.dek)
85 if e != nil {
86 return operator.ErrRefused
87 }
88 raw, e := compatibilityProfiles.ReadFile("compatibility_profiles/" + cfg.role + ".json")
89 if e != nil {
90 return operator.ErrRefused
91 }
92 var profile []schemaObject
93 if json.Unmarshal(raw, &profile) != nil || len(profile) == 0 {
94 return operator.ErrRefused
95 }
96 return operator.InspectReadonly(ctx, expected, cfg.verify, func(ctx context.Context, c *sql.Conn) error {
97 if e := checkCompatibilitySchema(ctx, c, schema, profile); e != nil {
98 return e
99 }
100 prefix := cfg.role + "_operator"
101 var state compatibilityState
102 if c.QueryRowContext(ctx, "SELECT origin,installation,registry,device,sequence,head,completed_at,key_id FROM "+prefix+"_state WHERE singleton=1").Scan(&state.Origin, &state.Installation, &state.Registry, &state.Device, &state.Sequence, &state.Head, &state.Completed, &state.KeyID) != nil || state != cfg.state {
103 return operator.ErrRefused
104 }
105 budget := compatibilityBudget{}
106 if cfg.role == "credential" {
107 var ref string
108 if c.QueryRowContext(ctx, "SELECT person_ref FROM people WHERE person_ref=?", cfg.witness).Scan(&ref) != nil {
109 return operator.ErrRefused
110 }
111 if e := budget.scan(ctx, c, "SELECT person_ref,blind_index FROM people", func(rows *sql.Rows) error {
112 var ref, index string
113 if rows.Scan(&ref, &index) != nil || !ValidPersonRef(ref) || !hexHash(index) {
114 return operator.ErrRefused
115 }
116 return nil
117 }); e != nil {
118 return e
119 }
120 } else {
121 var generation, role string
122 if !canonicalHash(cfg.generation) || cfg.team <= 0 || c.QueryRowContext(ctx, "SELECT generation FROM roster_generation WHERE singleton=1").Scan(&generation) != nil || generation != cfg.generation {
123 return operator.ErrRefused
124 }
125 if c.QueryRowContext(ctx, "SELECT role FROM memberships WHERE team_id=? AND person_ref=?", cfg.team, cfg.witness).Scan(&role) != nil || role != "owner" {
126 return operator.ErrRefused
127 }
128 }
129 if e := budget.scan(ctx, c, "SELECT operation_id,action,CASE WHEN typeof(sealed_result)='blob' AND length(sealed_result)<=49180 THEN sealed_result END FROM "+prefix+"_results", func(rows *sql.Rows) error {
130 var id, action string
131 var ciphertext []byte
132 if rows.Scan(&id, &action, &ciphertext) != nil || !canonicalHash(id) || len(action) == 0 || len(action) > 64 {
133 return operator.ErrRefused
134 }
135 return budget.open(sealer, "operator.result/v1/"+state.Origin+"/"+id+"/"+action, ciphertext, 49180, func(raw []byte) bool { return len(raw) > 0 && json.Valid(raw) })
136 }); e != nil {
137 return e
138 }
139 if cfg.role == "credential" {
140 return budget.scan(ctx, c, "SELECT CASE WHEN typeof(sealed_address)='blob' AND length(sealed_address)<=4096 THEN sealed_address END FROM signin_links", func(rows *sql.Rows) error {
141 var ciphertext []byte
142 if rows.Scan(&ciphertext) != nil {
143 return operator.ErrRefused
144 }
145 return budget.open(sealer, "signin.address", ciphertext, 4096, func(raw []byte) bool { return len(raw) > 0 && utf8.Valid(raw) })
146 })
147 }
148 if e := budget.scan(ctx, c, "SELECT id,sqid,label,roster_epoch,CASE WHEN typeof(sealed_name)='blob' AND length(sealed_name)<=284 THEN sealed_name END FROM teams", func(rows *sql.Rows) error {
149 var id, epoch int64
150 var sqid, label string
151 var ciphertext []byte
152 if rows.Scan(&id, &sqid, &label, &epoch, &ciphertext) != nil || id <= 0 || epoch <= 0 || !directorydata.ValidLabel(label) || len(sqid) == 0 || len(sqid) > 64 {
153 return operator.ErrRefused
154 }
155 for _, r := range sqid {
156 if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9') {
157 return operator.ErrRefused
158 }
159 }
160 return budget.open(sealer, "directory.team-name/"+strconv.FormatInt(id, 10), ciphertext, 284, func(raw []byte) bool {
161 if len(raw) > 256 || !utf8.Valid(raw) || strings.TrimSpace(string(raw)) == "" {
162 return false
163 }
164 for _, r := range string(raw) {
165 if unicode.IsControl(r) {
166 return false
167 }
168 }
169 return true
170 })
171 }); e != nil {
172 return e
173 }
174 if e := budget.scan(ctx, c, "SELECT person_ref,role FROM memberships", func(rows *sql.Rows) error {
175 var ref, role string
176 if rows.Scan(&ref, &role) != nil || !ValidPersonRef(ref) || (role != "owner" && role != "member") {
177 return operator.ErrRefused
178 }
179 return nil
180 }); e != nil {
181 return e
182 }
183 if e := budget.scan(ctx, c, "SELECT t.id,t.sqid,t.label,i.app,i.origin,i.consumer_id FROM team_instances i JOIN teams t ON t.id=i.team_id", func(rows *sql.Rows) error {
184 var route directorydata.Route
185 if rows.Scan(&route.TeamID, &route.TeamSQID, &route.Label, &route.App, &route.Origin, &route.ConsumerID) != nil || !directoryRouteAllowed(route) || !consumerName(route.ConsumerID) {
186 return operator.ErrRefused
187 }
188 return nil
189 }); e != nil {
190 return e
191 }
192 for _, query := range []string{
193 "SELECT count(*) FROM roster_consumers r JOIN team_instances i ON i.consumer_id=r.consumer_id WHERE r.team_id!=i.team_id OR r.app!=i.app OR r.origin!=i.origin",
194 "SELECT count(*) FROM directory_operator_consumer_records r JOIN roster_operator_receipts p ON p.operation_id=r.operation_id WHERE r.generation!=p.generation OR r.sequence!=p.sequence OR r.record_digest!=p.record_digest OR r.consumer_id!=p.consumer_id",
195 } {
196 var bad int
197 if c.QueryRowContext(ctx, query).Scan(&bad) != nil || bad != 0 {
198 return operator.ErrRefused
199 }
200 }
201 for _, item := range []struct {
202 query, purpose string
203 limit int
204 path bool
205 }{
206 {"SELECT secret_hash,CASE WHEN typeof(sealed_path)='blob' AND length(sealed_path)<=8192 THEN sealed_path END FROM oficina_handoff_pending", "handoff.path.", 8192, true},
207 {"SELECT handle_hash,CASE WHEN typeof(sealed_token)='blob' AND length(sealed_token)<=16384 THEN sealed_token END FROM oficina_handoff_inbox", "handoff.token.", 16384, false},
208 } {
209 if e := budget.scan(ctx, c, item.query, func(rows *sql.Rows) error {
210 var hash string
211 var ciphertext []byte
212 if rows.Scan(&hash, &ciphertext) != nil || !hexHash(hash) {
213 return operator.ErrRefused
214 }
215 return budget.open(sealer, item.purpose+hash, ciphertext, item.limit, func(raw []byte) bool {
216 if !item.path {
217 return len(raw) > 0 && len(raw) <= 12288 && utf8.Valid(raw)
218 }
219 path := strings.TrimPrefix(string(raw), "oficina-handoff-fresh-v1:")
220 return handoff.SafePath(path)
221 })
222 }); e != nil {
223 return e
224 }
225 }
226 return nil
227 })
228}
229
230type compatibilityBudget struct{ rows, bytes int }
231
232func (b *compatibilityBudget) scan(ctx context.Context, c *sql.Conn, query string, visit func(*sql.Rows) error) error {
233 rows, e := c.QueryContext(ctx, query+" LIMIT 100001")
234 if e != nil {
235 return operator.ErrUnavailable
236 }
237 defer rows.Close()
238 for rows.Next() {
239 b.rows++
240 if b.rows > 100000 || ctx.Err() != nil {
241 return operator.ErrUnavailable
242 }
243 if e = visit(rows); e != nil {
244 return e
245 }
246 }
247 if rows.Err() != nil {
248 return operator.ErrUnavailable
249 }
250 return rows.Close()
251}
252func (b *compatibilityBudget) open(s *seal.Sealer, purpose string, ciphertext []byte, max int, valid func([]byte) bool) error {
253 b.bytes += len(ciphertext)
254 if len(ciphertext) < 29 || len(ciphertext) > max || b.bytes > 64<<20 {
255 return operator.ErrRefused
256 }
257 raw, e := s.Open(purpose, ciphertext)
258 if e != nil {
259 return operator.ErrRefused
260 }
261 defer clear(raw)
262 if !valid(raw) {
263 return operator.ErrRefused
264 }
265 return nil
266}
267func checkCompatibilitySchema(ctx context.Context, c *sql.Conn, set *migrate.Set, profile []schemaObject) error {
268 if err := operator.VerifyLedger(ctx, c, set); err != nil {
269 return err
270 }
271 objects := map[string]schemaObject{}
272 for _, p := range profile {
273 objects[p.Type+"/"+p.Name] = p
274 }
275 rows, e := c.QueryContext(ctx, "SELECT type,name,tbl_name,coalesce(sql,'') FROM sqlite_schema LIMIT 1001")
276 if e != nil {
277 return operator.ErrRefused
278 }
279 for rows.Next() {
280 var p schemaObject
281 if rows.Scan(&p.Type, &p.Name, &p.Table, &p.SQL) != nil {
282 rows.Close()
283 return operator.ErrRefused
284 }
285 key := p.Type + "/" + p.Name
286 if objects[key] != p {
287 rows.Close()
288 return operator.ErrRefused
289 }
290 delete(objects, key)
291 }
292 if rows.Err() != nil || rows.Close() != nil || len(objects) != 0 {
293 return operator.ErrRefused
294 }
295 total := 0
296 for _, p := range profile {
297 if p.Type != "table" {
298 continue
299 }
300 var count int
301 query := "SELECT count(*) FROM (SELECT 1 FROM \"" + strings.ReplaceAll(p.Name, "\"", "\"\"") + "\" LIMIT 100001)"
302 if c.QueryRowContext(ctx, query).Scan(&count) != nil {
303 return operator.ErrRefused
304 }
305 total += count
306 if total > 100000 {
307 return operator.ErrUnavailable
308 }
309 }
310 rows, e = c.QueryContext(ctx, "PRAGMA foreign_key_check")
311 if e != nil {
312 return operator.ErrRefused
313 }
314 bad := rows.Next()
315 e = rows.Err()
316 closeErr := rows.Close()
317 if bad || e != nil || closeErr != nil {
318 return operator.ErrRefused
319 }
320 var n int
321 if c.QueryRowContext(ctx, "SELECT count(*) FROM (SELECT 1 FROM _litestream_lock LIMIT 1)").Scan(&n) != nil || n != 0 {
322 return operator.ErrRefused
323 }
324 if c.QueryRowContext(ctx, "SELECT count(*) FROM (SELECT 1 FROM _litestream_seq LIMIT 2)").Scan(&n) != nil || n > 1 {
325 return operator.ErrRefused
326 }
327 if n == 1 {
328 var valid int
329 if c.QueryRowContext(ctx, "SELECT typeof(id)='integer' AND id=1 AND typeof(seq)='integer' AND seq>=1 FROM _litestream_seq").Scan(&valid) != nil || valid != 1 {
330 return operator.ErrRefused
331 }
332 }
333 return nil
334}
335
336func canonicalHash(s string) bool {
337 raw, e := base64.RawURLEncoding.DecodeString(s)
338 return e == nil && len(raw) == 32 && base64.RawURLEncoding.EncodeToString(raw) == s
339}
340func hexHash(s string) bool {
341 raw, e := hex.DecodeString(s)
342 return e == nil && len(raw) == 32 && hex.EncodeToString(raw) == s
343}
344