oficina / home Public

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

Plain git — no account needed to clone.

Download

Download this file

1package roster
2
3import (
4 "bytes"
5 "context"
6 "encoding/base64"
7 "net/http"
8 "strconv"
9 "testing"
10 "time"
11
12 family "amadan.net/rastrillo/rastrillo/crypto"
13 "github.com/go-json-experiment/json"
14)
15
16func TestCanonicalWireAndDecodedSignature(t *testing.T) {
17 f := setup(t)
18 if CursorHash("") != "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU" {
19 t.Fatal("page zero hash")
20 }
21 for _, s := range []string{"", fixed(1) + "=", fixed(1) + "\n", fixed(1)[:42], fixed(1) + "a"} {
22 if ValidChallenge(s) {
23 t.Fatal("noncanonical challenge")
24 }
25 }
26 req, _ := http.NewRequest("GET", "https://home.example/roster?fetch="+fixed(1), nil)
27 p := f.page(req, 1, []MemberEntry{}, true, "")
28 raw, e := SignPage(f.key, "roster1", p)
29 if e != nil {
30 t.Fatal(e)
31 }
32 var env envelope
33 if strict(raw, &env) != nil {
34 t.Fatal("envelope")
35 }
36 payload, _ := canonical(env.Payload, -1)
37 sig, _ := canonical(env.Signature, 64)
38 if !family.Verify(f.key.SignPub(), PageContext, payload, sig) || family.Verify(f.key.SignPub(), PageContext, []byte(env.Payload), sig) {
39 t.Fatal("wrong signed bytes")
40 }
41 if !bytes.Contains(payload, []byte(`"members":[]`)) || bytes.Contains(payload, []byte(`"cursor"`)) {
42 t.Fatalf("empty final shape %s", payload)
43 }
44}
45func TestProvisioningStrictBindingsAndKeys(t *testing.T) {
46 for _, name := range []string{"issuer", "audience", "directory", "team", "version", "sequence", "epoch", "generation", "operation", "key", "duplicate-key", "owner", "unknown-field", "duplicate-field", "wrong-context", "unsigned", "case", "number"} {
47 t.Run(name, func(t *testing.T) {
48 f := setup(t)
49 p := f.record
50 p.Keys = append([]Key(nil), p.Keys...)
51 switch name {
52 case "issuer":
53 p.Bindings.Issuer = "https://other.example"
54 case "audience":
55 p.Bindings.Audience = "https://other.example"
56 case "directory":
57 p.Bindings.Directory = "https://other.example"
58 case "team":
59 p.Bindings.Team = "TeamB"
60 case "version":
61 p.Version = 2
62 case "sequence":
63 p.Sequence = 0
64 case "epoch":
65 p.MinEpoch = 0
66 case "generation":
67 p.Generation += "="
68 case "operation":
69 p.Operation = "unsafe/value"
70 case "key":
71 p.Keys[0].Public = fixed(1)
72 case "duplicate-key":
73 p.Keys = append(p.Keys, p.Keys[0])
74 case "owner":
75 p.OwnerConfirmation = "unsafe/value"
76 }
77 raw, _ := SignProvisioning(f.op, "operator", p)
78 if name == "unknown-field" || name == "duplicate-field" || name == "case" || name == "number" {
79 b, _ := json.Marshal(p)
80 switch name {
81 case "unknown-field":
82 b = append(b[:len(b)-1], []byte(`,"surprise":true}`)...)
83 case "duplicate-field":
84 b = append(b[:len(b)-1], []byte(`,"version":1}`)...)
85 case "case":
86 b = bytes.Replace(b, []byte(`"version"`), []byte(`"Version"`), 1)
87 case "number":
88 b = bytes.Replace(b, []byte(`"version":1`), []byte(`"version":1.0`), 1)
89 }
90 sig, _ := family.Sign(f.op, ProvisioningContext, b)
91 raw, _ = json.Marshal(envelope{1, "operator", base64.RawURLEncoding.EncodeToString(b), base64.RawURLEncoding.EncodeToString(sig)})
92 }
93 if name == "wrong-context" {
94 raw, _ = encode(f.op, "operator", PageContext, p)
95 }
96 if name == "unsigned" {
97 raw, _ = json.Marshal(p)
98 }
99 if _, e := VerifyProvisioning(raw, []Key{{"operator", base64.RawURLEncoding.EncodeToString(f.op.SignPub())}}, f.record.Bindings); e == nil {
100 t.Fatal("bad record accepted")
101 }
102 })
103 }
104}
105func TestRecordOperationAndRestartRollback(t *testing.T) {
106 f := setup(t)
107 c := f.cache()
108 n := f.record
109 n.Sequence = 2
110 n.Operation = "op2"
111 if e := c.UpdateRecord(context.Background(), f.verify(n)); e != nil {
112 t.Fatal(e)
113 }
114 if _, e := New(context.Background(), f.cfg); e == nil {
115 t.Fatal("old signed record on restart accepted")
116 }
117 n.Sequence = 3
118 n.Operation = "op1"
119 if c.UpdateRecord(context.Background(), f.verify(n)) == nil {
120 t.Fatal("operation reused")
121 }
122 f.cfg.Record = VerifiedRecord{}
123 if _, e := New(context.Background(), f.cfg); e == nil {
124 t.Fatal("zero verified record accepted")
125 }
126}
127func TestBoundedCompletePagination(t *testing.T) {
128 for _, count := range []int{101, 10000, 10001} {
129 t.Run(strconv.Itoa(count), func(t *testing.T) {
130 f := setup(t)
131 f.set(func(r *http.Request) (*http.Response, error) {
132 index := 0
133 if c := r.URL.Query().Get("cursor"); c != "" {
134 b, _ := base64.RawURLEncoding.DecodeString(c)
135 index, _ = strconv.Atoi(string(b))
136 }
137 end := (index + 1) * 100
138 if end > count {
139 end = count
140 }
141 m := make([]MemberEntry, end-index*100)
142 for i := range m {
143 m[i] = MemberEntry{person(index*100 + i + 1), "member"}
144 }
145 cursor := base64.RawURLEncoding.EncodeToString([]byte(strconv.Itoa(index + 1)))
146 p := f.page(r, 3, m, end == count, cursor)
147 p.Index = &index
148 return f.signed(p, 200), nil
149 })
150 c := f.cache()
151 d := f.check(c, 1)
152 if count <= MemberLimit {
153 state(t, d, Member)
154 state(t, f.check(c, count), Member)
155 } else {
156 state(t, d, Unknown)
157 }
158 })
159 }
160}
161func TestPaginationRefusals(t *testing.T) {
162 for _, name := range []string{"epoch", "order", "repeat-cursor", "short-page", "conflict", "generation-later"} {
163 t.Run(name, func(t *testing.T) {
164 f := setup(t)
165 first := make([]MemberEntry, 100)
166 for i := range first {
167 first[i] = MemberEntry{person(i + 1), "member"}
168 }
169 f.set(func(r *http.Request) (*http.Response, error) {
170 if r.URL.Query().Get("cursor") == "" {
171 m := first
172 if name == "short-page" {
173 m = m[:99]
174 }
175 return f.response(r, 2, m, false, fixed(4)), nil
176 }
177 p := f.page(r, 2, []MemberEntry{{person(101), "member"}}, true, "")
178 status := 200
179 switch name {
180 case "epoch":
181 p.Epoch = 3
182 case "order":
183 (*p.Members)[0].Subject = person(1)
184 case "repeat-cursor":
185 m := make([]MemberEntry, 100)
186 for i := range m {
187 m[i] = MemberEntry{person(i + 101), "member"}
188 }
189 p.Members = &m
190 p.Complete = ptr(false)
191 p.Cursor = ptr(fixed(4))
192 case "conflict":
193 p.Type = "stale-cursor"
194 p.Index = nil
195 p.Members = nil
196 p.Complete = nil
197 status = 409
198 case "generation-later":
199 p.Generation = fixed(2)
200 }
201 return f.signed(p, status), nil
202 })
203 c := f.cache()
204 if c.Refresh(context.Background()) == nil {
205 t.Fatal("bad pagination accepted")
206 }
207 state(t, f.check(c, 1), Unknown)
208 })
209 }
210}
211func TestSignedConflictGenerationFence(t *testing.T) {
212 f := setup(t)
213 c := f.cache()
214 d := f.check(c, 1)
215 f.clock.step(30 * time.Second)
216 f.set(func(r *http.Request) (*http.Response, error) {
217 p := f.page(r, 2, nil, false, "")
218 p.Type = "stale-cursor"
219 p.Index = nil
220 p.Members = nil
221 p.Complete = nil
222 p.Cursor = nil
223 p.Generation = fixed(2)
224 return f.signed(p, 409), nil
225 })
226 if c.Refresh(context.Background()) == nil {
227 t.Fatal("conflict published")
228 }
229 if d.Lease.Valid() {
230 t.Fatal("conflict generation did not fence")
231 }
232}
233func TestEvidenceTamperAndPurpose(t *testing.T) {
234 f := setup(t)
235 c := f.cache()
236 state(t, f.check(c, 1), Member)
237 raw, _ := f.disk.Load(context.Background())
238 raw[len(raw)-1] ^= 1
239 _ = f.disk.Save(context.Background(), raw)
240 if _, e := New(context.Background(), f.cfg); e == nil {
241 t.Fatal("tampered durable evidence accepted")
242 }
243 f = setup(t)
244 c = f.cache()
245 state(t, f.check(c, 1), Member)
246 p := f.record
247 p.Bindings.Audience = "https://other.example"
248 f.record.Bindings = p.Bindings
249 f.cfg.Record = f.verify(p)
250 if _, e := New(context.Background(), f.cfg); e == nil {
251 t.Fatal("cross-origin durable evidence accepted")
252 }
253}
254func TestElapsedFetchIncludedInLease(t *testing.T) {
255 f := setup(t)
256 f.set(func(r *http.Request) (*http.Response, error) {
257 f.clock.step(4 * time.Second)
258 return f.response(r, 1, []MemberEntry{{person(1), "member"}}, true, ""), nil
259 })
260 c := f.cache()
261 d := f.check(c, 1)
262 state(t, d, Member)
263 if d.Lease.Remaining() != 56*time.Second {
264 t.Fatalf("remaining %v", d.Lease.Remaining())
265 }
266 f.clock.step(56 * time.Second)
267 if d.Lease.Valid() {
268 t.Fatal("transit extended age")
269 }
270}
271func TestActualTransportDeadline(t *testing.T) {
272 f := setup(t)
273 f.set(func(r *http.Request) (*http.Response, error) { <-r.Context().Done(); return nil, r.Context().Err() })
274 c := f.cache()
275 start := time.Now()
276 if c.Refresh(context.Background()) != ErrUnavailable {
277 t.Fatal("deadline error")
278 }
279 elapsed := time.Since(start)
280 if elapsed < FetchTimeout || elapsed > FetchTimeout+time.Second {
281 t.Fatalf("refresh budget %v", elapsed)
282 }
283}
284func TestNoMissingRequiredOrAlternateBody(t *testing.T) {
285 f := setup(t)
286 c := f.cache()
287 f.set(func(r *http.Request) (*http.Response, error) {
288 res := f.response(r, 1, []MemberEntry{}, true, "")
289 res.Header.Set("Content-Type", "application/json; charset=utf-8")
290 return res, nil
291 })
292 if c.Refresh(context.Background()) == nil {
293 t.Fatal("alternate type accepted")
294 }
295}
296