oficina / home Public

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

Plain git — no account needed to clone.

Download

Download this file

1package home
2
3import (
4 "bytes"
5 "context"
6 "database/sql"
7 "encoding/base64"
8 "errors"
9 "os"
10 "path/filepath"
11 "testing"
12 "time"
13
14 "amadan.net/oficina/home/internal/home/directorydata"
15 "amadan.net/oficina/home/internal/home/seal"
16 "amadan.net/oficina/home/internal/home/sealtest"
17 "amadan.net/oficina/home/internal/operator"
18 "amadan.net/oficina/home/roster"
19 family "amadan.net/rastrillo/rastrillo/crypto"
20 "amadan.net/rastrillo/rastrillo/db"
21 "amadan.net/rastrillo/rastrillo/migrate"
22 "github.com/go-json-experiment/json"
23)
24
25func opToken(b byte) string { return base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{b}, 32)) }
26
27type failedOperatorSeal struct{}
28
29func (failedOperatorSeal) Seal(string, []byte) ([]byte, error) {
30 return nil, errors.New("seal unavailable")
31}
32func (failedOperatorSeal) Open(string, []byte) ([]byte, error) {
33 return nil, errors.New("seal unavailable")
34}
35func TestOperatorSeedUsesNormalIdentityAndAtomicPrivateReceipt(t *testing.T) {
36 ctx := context.Background()
37 path := filepath.Join(t.TempDir(), "credential.db")
38 handle, e := db.Open(path, nil)
39 if e != nil {
40 t.Fatal(e)
41 }
42 defer handle.Close()
43 if _, e = migrate.Apply(ctx, handle, migrate.Merge(BootSchema, operator.CredentialSchema)); e != nil {
44 t.Fatal(e)
45 }
46 provisioner, e := family.Generate()
47 if e != nil {
48 t.Fatal(e)
49 }
50 owner, e := family.Generate()
51 if e != nil {
52 t.Fatal(e)
53 }
54 browser, e := family.Generate()
55 if e != nil {
56 t.Fatal(e)
57 }
58 pins := operator.Pins{Role: "credential", Origin: "https://id.oficina.works", Installation: opToken(1), Registry: opToken(2), Device: opToken(3), PublicKey: base64.RawURLEncoding.EncodeToString(provisioner.SignPub()), Actions: []string{"credential.owner-seed"}}
59 verifier, e := operator.NewVerifier(pins)
60 if e != nil {
61 t.Fatal(e)
62 }
63 sealer, e := seal.New(bytes.Repeat([]byte{4}, 32))
64 if e != nil {
65 t.Fatal(e)
66 }
67 now := time.Unix(2000000000, 0)
68 clock := func() time.Time { return now }
69 mac := bytes.Repeat([]byte{5}, 32)
70 engine, e := operator.NewEngine(handle.Writer(), verifier, sealer, mac, clock)
71 if e != nil {
72 t.Fatal(e)
73 }
74 pepper := bytes.Repeat([]byte{6}, 32)
75 resolve, e := NewOperatorPersonResolver(pepper)
76 if e != nil {
77 t.Fatal(e)
78 }
79 credential, e := operator.NewCredential(engine, "https://oficina.works", owner, [][]byte{browser.SignPub()}, resolve)
80 if e != nil {
81 t.Fatal(e)
82 }
83 sequence := int64(0)
84 head := ""
85 attempt := byte(10)
86 request := func(address string, existing bool) []byte {
87 t.Helper()
88 a := operator.Authorization{Version: 1, Purpose: "operator-input", Role: pins.Role, Origin: pins.Origin, Installation: pins.Installation, Registry: pins.Registry, Device: pins.Device, Sequence: sequence + 1, Initial: sequence == 0, Previous: head, JournalSequence: int64(attempt), OperationID: opToken(attempt), Action: "credential.owner-seed", Issued: now.Unix(), Expires: now.Unix() + 300}
89 attempt++
90 input := operator.OwnerInput{InputBase: operator.InputBase{Version: 1, Operation: a.Action, OperationID: a.OperationID}, Directory: "https://oficina.works", TeamID: 1, TeamSQID: "Uk", Address: address, ExistingOnly: existing}
91 raw, e := operator.SignRequest(provisioner, a, input)
92 if e != nil {
93 t.Fatal(e)
94 }
95 return raw
96 }
97 accept := func(raw []byte) operator.Result {
98 t.Helper()
99 out, e := credential.Execute(ctx, raw)
100 if e != nil {
101 t.Fatal(e)
102 }
103 var result operator.Result
104 if json.Unmarshal(out, &result) != nil || len(result.Receipt) == 0 {
105 t.Fatal("missing receipt")
106 }
107 r, e := verifier.Verify(raw)
108 if e != nil {
109 t.Fatal(e)
110 }
111 sequence = r.Authorization().Sequence
112 head = r.Head()
113 return result
114 }
115 address := "Synthetic-Owner@Example.test"
116 first := accept(request(address, false))
117 ov, e := operator.NewOwnerVerifier(base64.RawURLEncoding.EncodeToString(owner.SignPub()), pins.Origin, "https://oficina.works")
118 if e != nil {
119 t.Fatal(e)
120 }
121 receipt, e := ov.Verify(first.Receipt, now)
122 if e != nil {
123 t.Fatal(e)
124 }
125 var person, index, created string
126 if e = handle.Writer().QueryRow("SELECT person_ref,blind_index,created_at FROM people").Scan(&person, &index, &created); e != nil || person != receipt.PersonRef || index != blindIndex(pepper, address) || created != now.UTC().Format(dayFormat) {
127 t.Fatal("identity changed", e)
128 }
129 second := accept(request("synthetic-owner@example.test", true))
130 again, e := ov.Verify(second.Receipt, now)
131 if e != nil || again.PersonRef != person {
132 t.Fatal("existing identity changed", e)
133 }
134 if _, e = credential.Execute(ctx, request("absent-owner@example.test", true)); e != operator.ErrRefused {
135 t.Fatal("missing existing owner accepted", e)
136 }
137 failedEngine, e := operator.NewEngine(handle.Writer(), verifier, failedOperatorSeal{}, mac, clock)
138 if e != nil {
139 t.Fatal(e)
140 }
141 failed, e := operator.NewCredential(failedEngine, "https://oficina.works", owner, [][]byte{browser.SignPub()}, resolve)
142 if e != nil {
143 t.Fatal(e)
144 }
145 if _, e = failed.Execute(ctx, request("rollback-owner@example.test", false)); e != operator.ErrUnavailable {
146 t.Fatal("seal failure", e)
147 }
148 var count int
149 if e = handle.Writer().QueryRow("SELECT count(*) FROM people").Scan(&count); e != nil || count != 1 {
150 t.Fatal("orphan person", count, e)
151 }
152 if e = handle.Writer().QueryRow("SELECT count(*) FROM credential_operator_results").Scan(&count); e != nil || count != 2 {
153 t.Fatal("partial result", count, e)
154 }
155 for _, suffix := range []string{"", "-wal"} {
156 disk, e := os.ReadFile(path + suffix)
157 if e != nil && !errors.Is(e, os.ErrNotExist) {
158 t.Fatal(e)
159 }
160 for _, value := range []string{address, "synthetic-owner@example.test", "absent-owner@example.test", "rollback-owner@example.test", string(first.Receipt)} {
161 if bytes.Contains(disk, []byte(value)) {
162 t.Fatal("operator private input/result at rest")
163 }
164 }
165 }
166 var rows *sql.Rows
167 rows, e = handle.Writer().Query("SELECT name FROM sqlite_master WHERE type='table'")
168 if e != nil {
169 t.Fatal(e)
170 }
171 defer rows.Close()
172 for rows.Next() {
173 var name string
174 if rows.Scan(&name) != nil {
175 t.Fatal("schema read")
176 }
177 if name == "teams" || name == "memberships" {
178 t.Fatal("directory schema on credential role")
179 }
180 }
181}
182
183// exerciseOperatorSeedPrivacy extends the original sign-in gate through the
184// credential receipt and directory owner seed, using separate role databases.
185func exerciseOperatorSeedPrivacy(t *testing.T, app *testApp) {
186 t.Helper()
187 ctx := context.Background()
188 if _, e := migrate.Apply(ctx, app.db, operator.CredentialSchema); e != nil {
189 t.Fatal(e)
190 }
191 signer, e := family.Generate()
192 if e != nil {
193 t.Fatal(e)
194 }
195 owner, e := family.Generate()
196 if e != nil {
197 t.Fatal(e)
198 }
199 browser, e := family.Generate()
200 if e != nil {
201 t.Fatal(e)
202 }
203 page, e := family.Generate()
204 if e != nil {
205 t.Fatal(e)
206 }
207 pins := operator.Pins{Role: "credential", Origin: Origin, Installation: opToken(20), Registry: opToken(21), Device: opToken(22), PublicKey: base64.RawURLEncoding.EncodeToString(signer.SignPub()), Actions: []string{"credential.owner-seed"}}
208 verifier, e := operator.NewVerifier(pins)
209 if e != nil {
210 t.Fatal(e)
211 }
212 now := time.Now()
213 clock := func() time.Time { return now }
214 engine, e := operator.NewEngine(app.db.Writer(), verifier, app.secrets.Sealer, bytes.Repeat([]byte{23}, 32), clock)
215 if e != nil {
216 t.Fatal(e)
217 }
218 resolve, e := NewOperatorPersonResolver(app.secrets.Pepper)
219 if e != nil {
220 t.Fatal(e)
221 }
222 credential, e := operator.NewCredential(engine, directoryOrigin, owner, [][]byte{browser.SignPub()}, resolve)
223 if e != nil {
224 t.Fatal(e)
225 }
226 auth := operator.Authorization{Version: 1, Purpose: "operator-input", Role: pins.Role, Origin: pins.Origin, Installation: pins.Installation, Registry: pins.Registry, Device: pins.Device, Sequence: 1, Initial: true, JournalSequence: 1, OperationID: opToken(24), Action: "credential.owner-seed", Issued: now.Unix(), Expires: now.Unix() + 300}
227 input := operator.OwnerInput{InputBase: operator.InputBase{Version: 1, Operation: auth.Action, OperationID: auth.OperationID}, Directory: directoryOrigin, TeamID: 1, TeamSQID: "Uk", Address: sealtest.CanaryAddress, ExistingOnly: true}
228 raw, e := operator.SignRequest(signer, auth, input)
229 if e != nil {
230 t.Fatal(e)
231 }
232 out, e := credential.Execute(ctx, raw)
233 if e != nil {
234 t.Fatal(e)
235 }
236 var receipt operator.Result
237 if e = json.Unmarshal(out, &receipt); e != nil {
238 t.Fatal(e)
239 }
240 path := filepath.Join(t.TempDir(), "directory.db")
241 directoryDB, e := db.Open(path, nil)
242 if e != nil {
243 t.Fatal(e)
244 }
245 defer directoryDB.Close()
246 if _, e = migrate.Apply(ctx, directoryDB, migrate.Merge(directorydata.Schema, directorydata.RosterSchema, directorydata.OperatorSchema, operator.DirectorySchema)); e != nil {
247 t.Fatal(e)
248 }
249 pins.Role = "directory"
250 pins.Origin = directoryOrigin
251 pins.Installation = opToken(25)
252 pins.Actions = []string{"directory.initialize", "directory.seed-team"}
253 verifier, e = operator.NewVerifier(pins)
254 if e != nil {
255 t.Fatal(e)
256 }
257 engine, e = operator.NewEngine(directoryDB.Writer(), verifier, app.secrets.Sealer, bytes.Repeat([]byte{26}, 32), clock)
258 if e != nil {
259 t.Fatal(e)
260 }
261 ov, e := operator.NewOwnerVerifier(base64.RawURLEncoding.EncodeToString(owner.SignPub()), Origin, directoryOrigin)
262 if e != nil {
263 t.Fatal(e)
264 }
265 rosterSigner, e := family.Generate()
266 if e != nil {
267 t.Fatal(e)
268 }
269 directory, e := operator.NewDirectory(engine, ov, rosterSigner, []roster.Key{{ID: "page", Public: base64.RawURLEncoding.EncodeToString(page.SignPub())}}, directorydata.Config{Apps: []string{"docs"}, ValidateRoute: func(r directorydata.Route) bool { return r.Origin == "https://"+r.Label+".docs.oficina.works" }})
270 if e != nil {
271 t.Fatal(e)
272 }
273 auth.Role = pins.Role
274 auth.Origin = pins.Origin
275 auth.Installation = pins.Installation
276 auth.OperationID = opToken(27)
277 auth.Action = "directory.initialize"
278 raw, e = operator.SignRequest(signer, auth, operator.InitializeInput{InputBase: operator.InputBase{Version: 1, Operation: auth.Action, OperationID: auth.OperationID}, Generation: opToken(28), Baseline: "operator-attests-never-provisioned"})
279 if e != nil {
280 t.Fatal(e)
281 }
282 if _, e = directory.Execute(ctx, raw); e != nil {
283 t.Fatal(e)
284 }
285 request, e := verifier.Verify(raw)
286 if e != nil {
287 t.Fatal(e)
288 }
289 auth.Sequence = 2
290 auth.Initial = false
291 auth.Previous = request.Head()
292 auth.JournalSequence = 2
293 auth.OperationID = opToken(29)
294 auth.Action = "directory.seed-team"
295 raw, e = operator.SignRequest(signer, auth, operator.SeedInput{InputBase: operator.InputBase{Version: 1, Operation: auth.Action, OperationID: auth.OperationID}, Generation: opToken(28), TeamID: 1, TeamSQID: "Uk", Label: "alpha", Name: "CANARY-TEAM-NAME-2b71", Receipt: receipt.Receipt})
296 if e != nil {
297 t.Fatal(e)
298 }
299 if _, e = directory.Execute(ctx, raw); e != nil {
300 t.Fatal(e)
301 }
302 var members int
303 if e = directoryDB.Writer().QueryRow("SELECT count(*) FROM memberships WHERE role='owner'").Scan(&members); e != nil || members != 1 {
304 t.Fatal("owner not seeded", e)
305 }
306 sealtest.AssertNoPlaintext(t, path)
307 sealtest.AssertNoPlaintext(t, app.dbPath)
308}
309