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 "bytes"
7 "context"
8 "crypto/sha256"
9 "database/sql"
10 "encoding/base64"
11 "encoding/hex"
12 "encoding/json"
13 "io"
14 "os"
15 "os/exec"
16 "path/filepath"
17 "testing"
18 "time"
19
20 "amadan.net/oficina/home/internal/home/seal"
21 "amadan.net/oficina/home/internal/operator"
22 "amadan.net/rastrillo/rastrillo/migrate"
23 "golang.org/x/sys/unix"
24)
25
26const replicationCompatibilityDDL = "CREATE TABLE IF NOT EXISTS _litestream_seq (id INTEGER PRIMARY KEY, seq INTEGER); CREATE TABLE IF NOT EXISTS _litestream_lock (id INTEGER);"
27
28func compatibilityFixture(t *testing.T, role string) *sql.DB {
29 t.Helper()
30 set := compatibilitySchema(role)
31 d, e := migrate.Replay(context.Background(), set.All())
32 if e != nil {
33 t.Fatal(e)
34 }
35 for _, q := range []string{migrate.LedgerDDL, replicationCompatibilityDDL} {
36 if _, e = d.Exec(q); e != nil {
37 t.Fatal(e)
38 }
39 }
40 for _, m := range set.All() {
41 if m.Fn != nil {
42 t.Fatal("unsupported Go migration")
43 }
44 if _, e = d.Exec("INSERT INTO rastrillo_migrations VALUES(?,?,?)", m.ID, "2026-01-01T00:00:00Z", migrate.Checksum(m.SQL)); e != nil {
45 t.Fatal(e)
46 }
47 }
48 return d
49}
50func readCompatibilityProfile(t *testing.T, d *sql.DB) []schemaObject {
51 t.Helper()
52 rows, e := d.Query("SELECT type,name,tbl_name,coalesce(sql,'') FROM sqlite_schema ORDER BY type,name")
53 if e != nil {
54 t.Fatal(e)
55 }
56 defer rows.Close()
57 var objects []schemaObject
58 for rows.Next() {
59 var p schemaObject
60 if e = rows.Scan(&p.Type, &p.Name, &p.Table, &p.SQL); e != nil {
61 t.Fatal(e)
62 }
63 objects = append(objects, p)
64 }
65 if rows.Err() != nil {
66 t.Fatal(rows.Err())
67 }
68 return objects
69}
70func TestCompatibilityFrozenProfiles(t *testing.T) {
71 for _, role := range []string{"credential", "directory"} {
72 t.Run(role, func(t *testing.T) {
73 d := compatibilityFixture(t, role)
74 defer d.Close()
75 raw, e := json.MarshalIndent(readCompatibilityProfile(t, d), "", " ")
76 if e != nil {
77 t.Fatal(e)
78 }
79 raw = append(raw, '\n')
80 path := "compatibility_profiles/" + role + ".json"
81 if os.Getenv("OFICINA_GENERATE_COMPATIBILITY_PROFILE") == "1" {
82 if e = os.WriteFile(path, raw, 0600); e != nil {
83 t.Fatal(e)
84 }
85 return
86 }
87 expected, e := compatibilityProfiles.ReadFile(path)
88 if e != nil || !bytes.Equal(raw, expected) {
89 t.Fatal("frozen selected-release schema changed; review and regenerate")
90 }
91 })
92 }
93}
94func compatibilityTestConfig(role string) compatibilityConfig {
95 cfg := compatibilityConfig{role: role, origin: "https://id.example.test", dek: bytes.Repeat([]byte{1}, 32), mac: bytes.Repeat([]byte{2}, 32), expectedKeys: map[string]string{}, witness: "pr_aaaaaaaaaaaaaaaaaaaaaaaaaa", generation: base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{5}, 32)), team: 1}
96 if role == "credential" {
97 cfg.pepper = bytes.Repeat([]byte{3}, 32)
98 cfg.signin = bytes.Repeat([]byte{4}, 32)
99 } else {
100 cfg.origin = "https://home.example.test"
101 }
102 for name, key := range map[string][]byte{"dek": cfg.dek, "mac": cfg.mac, "pepper": cfg.pepper, "signin": cfg.signin} {
103 if len(key) == 0 {
104 continue
105 }
106 h := sha256.Sum256(key)
107 cfg.expectedKeys[name] = hex.EncodeToString(h[:])
108 }
109 cfg.state = compatibilityState{cfg.origin, "installation", "registry", "device", cfg.generation, "key", 1, 1}
110 cfg.verify = func(context.Context, operator.ReadonlyExpectation) error { return nil }
111 return cfg
112}
113func seedCompatibilityData(t *testing.T, d *sql.DB, cfg compatibilityConfig) {
114 t.Helper()
115 must := func(q string, args ...any) {
116 t.Helper()
117 if _, e := d.Exec(q, args...); e != nil {
118 t.Fatal(e)
119 }
120 }
121 must("INSERT INTO "+cfg.role+"_operator_state(singleton,origin,installation,registry,device,sequence,head,completed_at,key_id) VALUES(1,?,?,?,?,?,?,?,?)", cfg.origin, cfg.state.Installation, cfg.state.Registry, cfg.state.Device, cfg.state.Sequence, cfg.state.Head, cfg.state.Completed, cfg.state.KeyID)
122 s, _ := seal.New(cfg.dek)
123 result, _ := s.Seal("operator.result/v1/"+cfg.origin+"/"+cfg.generation+"/owner-seed", []byte(`{"ok":true}`))
124 must("INSERT INTO "+cfg.role+"_operator_results(operation_id,action,sequence,authorization_head,request_mac,sealed_result,completed_at,operator_ref) VALUES(?,?,1,?,?,?,1,'test')", cfg.generation, "owner-seed", cfg.generation, bytes.Repeat([]byte{7}, 32), result)
125 if cfg.role == "credential" {
126 h := sha256.Sum256([]byte("synthetic"))
127 must("INSERT INTO people VALUES(?,?,?)", cfg.witness, hex.EncodeToString(h[:]), "2026-01-01T00:00:00Z")
128 sealed, _ := s.Seal("signin.address", []byte("synthetic@example.invalid"))
129 must("INSERT INTO signin_links VALUES(?,?,?,?)", "hash", sealed, "signin", "2020-01-01T00:00:00Z")
130 } else {
131 sealed, _ := s.Seal("directory.team-name/1", []byte("Synthetic"))
132 must("INSERT INTO teams(id,sqid,label,sealed_name,created_at) VALUES(1,'abc','synthetic',?,1)", sealed)
133 must("INSERT INTO memberships VALUES(1,?,'owner')", cfg.witness)
134 must("INSERT INTO roster_generation VALUES(1,?)", cfg.generation)
135 hash := hex.EncodeToString(bytes.Repeat([]byte{6}, 32))
136 sealed, _ = s.Seal("handoff.path."+hash, []byte("oficina-handoff-fresh-v1:/account"))
137 must("INSERT INTO oficina_handoff_pending VALUES(?,?,?,1,2)", hash, hash, sealed)
138 sealed, _ = s.Seal("handoff.token."+hash, []byte("expired-synthetic-token"))
139 must("INSERT INTO oficina_handoff_inbox VALUES(?,?,?,1,2)", hash, hash, sealed)
140 }
141}
142func TestCompatibilityDatabaseSubprocess(t *testing.T) {
143 if os.Getenv("OFICINA_COMPATIBILITY_PROBE") == "1" {
144 var in struct {
145 Expected operator.ReadonlyExpectation
146 Role, Mode string
147 }
148 if json.NewDecoder(io.LimitReader(os.Stdin, 16384)).Decode(&in) != nil {
149 t.Fatal("input")
150 }
151 cfg := compatibilityTestConfig(in.Role)
152 switch in.Mode {
153 case "missing-proof":
154 cfg.verify = nil
155 case "missing-key":
156 delete(cfg.expectedKeys, "dek")
157 case "wrong-key":
158 cfg.dek = bytes.Repeat([]byte{9}, 32)
159 case "wrong-key-enrolled":
160 cfg.dek = bytes.Repeat([]byte{9}, 32)
161 sum := sha256.Sum256(cfg.dek)
162 cfg.expectedKeys["dek"] = hex.EncodeToString(sum[:])
163 case "wrong-state":
164 cfg.state.Sequence++
165 case "wrong-witness":
166 cfg.witness = "pr_bbbbbbbbbbbbbbbbbbbbbbbbbb"
167 }
168 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
169 defer cancel()
170 e := inspectCompatibilityDatabase(ctx, in.Expected, cfg)
171 if in.Mode == "valid" {
172 if e != nil {
173 t.Fatal(e)
174 }
175 } else if e == nil {
176 t.Fatal("unsafe success", in.Mode)
177 }
178 return
179 }
180 for _, role := range []string{"credential", "directory"} {
181 for _, mode := range []string{"valid", "oversized-ciphertext", "wrong-key-enrolled", "operator-ciphertext", "path-ciphertext", "missing-proof", "missing-key", "wrong-key", "wrong-state", "wrong-witness", "schema", "ledger", "ciphertext", "replication", "foreign-key", "empty-state"} {
182 t.Run(role+"/"+mode, func(t *testing.T) {
183 cfg := compatibilityTestConfig(role)
184 d := compatibilityFixture(t, role)
185 seedCompatibilityData(t, d, cfg)
186 q := ""
187 switch mode {
188 case "oversized-ciphertext":
189 if role == "credential" {
190 q = "UPDATE signin_links SET sealed_address=zeroblob(65536)"
191 } else {
192 q = "UPDATE oficina_handoff_inbox SET sealed_token=zeroblob(65536)"
193 }
194 case "operator-ciphertext":
195 q = "UPDATE " + role + "_operator_results SET sealed_result=zeroblob(40)"
196 case "path-ciphertext":
197 if role == "directory" {
198 q = "UPDATE oficina_handoff_pending SET sealed_path=zeroblob(40)"
199 } else {
200 q = "UPDATE signin_links SET sealed_address=zeroblob(40)"
201 }
202 case "schema":
203 q = "CREATE TRIGGER unexpected AFTER DELETE ON " + role + "_operator_state BEGIN SELECT 1; END"
204 case "ledger":
205 q = "UPDATE rastrillo_migrations SET checksum='bad'"
206 case "replication":
207 q = "INSERT INTO _litestream_lock VALUES(1)"
208 case "empty-state":
209 q = "DELETE FROM " + role + "_operator_state"
210 case "ciphertext":
211 if role == "credential" {
212 q = "UPDATE signin_links SET sealed_address=zeroblob(40)"
213 } else {
214 q = "UPDATE oficina_handoff_inbox SET sealed_token=zeroblob(40)"
215 }
216 case "foreign-key":
217 if role == "directory" {
218 q = "INSERT INTO memberships VALUES(999,'pr_bbbbbbbbbbbbbbbbbbbbbbbbbb','member')"
219 } else {
220 q = "UPDATE people SET person_ref='broken'"
221 }
222 }
223 if q != "" {
224 if _, e := d.Exec(q); e != nil {
225 t.Fatal(e)
226 }
227 }
228 path := filepath.Join(t.TempDir(), "role.db")
229 if _, e := d.Exec("VACUUM INTO ?", path); e != nil {
230 t.Fatal(e)
231 }
232 if e := d.Close(); e != nil {
233 t.Fatal(e)
234 }
235 raw, e := os.ReadFile(path)
236 if e != nil {
237 t.Fatal(e)
238 }
239 h := sha256.Sum256(raw)
240 var st unix.Stat_t
241 if unix.Stat(path, &st) != nil {
242 t.Fatal("stat")
243 }
244 expected := operator.ReadonlyExpectation{Path: path, Device: uint64(st.Dev), Inode: st.Ino, Length: int64(len(raw)), SHA256: hex.EncodeToString(h[:])}
245 input, _ := json.Marshal(struct {
246 Expected operator.ReadonlyExpectation
247 Role, Mode string
248 }{expected, role, mode})
249 cmd := exec.Command(os.Args[0], "-test.run=^TestCompatibilityDatabaseSubprocess$")
250 cmd.Env = append(os.Environ(), "OFICINA_COMPATIBILITY_PROBE=1")
251 cmd.Stdin = bytes.NewReader(input)
252 if out, e := cmd.CombinedOutput(); e != nil {
253 t.Fatalf("child: %v %s", e, out)
254 }
255 after, e := os.ReadFile(path)
256 if e != nil || !bytes.Equal(raw, after) {
257 t.Fatal("database changed")
258 }
259 })
260 }
261 }
262}
263