ALL

For reading and verification only. You may not copy, use, modify or redistribute this code. Full notice on the main page.

tunnel/contracts_test.go

SHA-256: 8b9de12e4326163907501aeb883220d87f579558753280db5b0843dd8efafa23
378 lines · 14084 bytes · View on GitHub

1package main
2
3// ALL 2.0, WP01: the Go side of the contract tests. The golden vectors of contracts/vectors/ are the spec: this file runs every one of them
4// through the Go reference verifier (contracts_verify_test.go) and demands the same verdict, and the same error word, as the Python and
5// TypeScript verifiers. The node written by WP05 and WP06 is tested against the same functions and files; nothing here is node code.
6
7import (
8 "os"
9 "path/filepath"
10 "reflect"
11 "strings"
12 "testing"
13)
14
15// ctVectorsDir is contracts/vectors, or the folder ALL_CONTRACT_VECTORS_DIR names (vectors made with another salt: nobody has seen them).
16func ctVectorsDir() string {
17 if dir := os.Getenv("ALL_CONTRACT_VECTORS_DIR"); dir != "" {
18 return dir
19 }
20 return filepath.Join(ctContracts, "vectors")
21}
22
23func ctVectors(t *testing.T, name string) map[string]any {
24 t.Helper()
25 value, err := ctLoadFile(filepath.Join(ctVectorsDir(), name))
26 if err != nil {
27 t.Fatalf("%s: %v", name, err)
28 }
29 return value.(map[string]any)
30}
31
32func ctList(t *testing.T, doc map[string]any, key string) []map[string]any {
33 t.Helper()
34 items, ok := doc[key].([]any)
35 if !ok || len(items) == 0 {
36 t.Fatalf("no vectors under %q", key)
37 }
38 out := make([]map[string]any, len(items))
39 for i, item := range items {
40 out[i] = item.(map[string]any)
41 }
42 return out
43}
44
45func ctVerdict(r ctResult) map[string]any {
46 if r.ok {
47 return map[string]any{"ok": true}
48 }
49 return map[string]any{"ok": false, "error": r.err}
50}
51
52func ctCheckVerdict(t *testing.T, vector map[string]any, got map[string]any) {
53 t.Helper()
54 if !ctEqual(got, vector["expect"]) {
55 t.Errorf("%s: got %v, want %v", vector["id"], got, vector["expect"])
56 }
57}
58
59func ctStrings(value any) []string {
60 var out []string
61 for _, item := range value.([]any) {
62 out = append(out, item.(string))
63 }
64 return out
65}
66
67func TestContractsPolicyHoldsThePlansNumbers(t *testing.T) {
68 want := map[string]int{
69 "timing.stream_idle_s": 1800, "timing.stream_max_age_s": 14400, "timing.ping_interval_s": 20, "timing.dead_peer_s": 45,
70 "authorization.valid_until_paid_s": 14400, "authorization.valid_until_trial_s": 900, "authorization.valid_until_gated_s": 900,
71 "authorization.grace_paid_s": 900, "authorization.access_ttl_paid_s": 14400, "authorization.access_ttl_trial_s": 600,
72 "timing.drain_quiet_s": 1200, "pairing.window_s": 600, "budget.classes.paid": 20, "budget.classes.trial": 12, "budget.classes.flex": 8,
73 "gated.bytes_per_day": 10 * 1024 * 1024, "gated.streams_per_day": 200, "gated.cutoff_days": 14, "identity.label_length": 20,
74 "timing.renew_before_valid_until_s": 300, "jwks.max_keys": 16, "node_limits.label_streams_max": 64, "node_limits.client_hello_deadline_s": 5,
75 "authorization.session_ttl_s": 300, "authorization.proof_ttl_s": 60, "authorization.node_assertion_ttl_s": 60, "protocol.version": 2,
76 }
77 for key, value := range want {
78 if got := ctPolInt(key); got != value {
79 t.Errorf("policy %s = %d, want %d", key, got, value)
80 }
81 }
82 if ctPolStr("protocol.alpn") != "all-tunnel/2" {
83 t.Errorf("alpn = %s", ctPolStr("protocol.alpn"))
84 }
85}
86
87func TestContractsLabelPatternComesFromThePolicyOnly(t *testing.T) {
88 // the schema, the policy and the compiled verifier pattern are one text; this file never types the pattern itself
89 label := ctPolStr("identity.label_regex")
90 defs := ctSchemaSet.docs["common.schema.json"].(map[string]any)["$defs"].(map[string]any)
91 if defs["label"].(map[string]any)["pattern"] != label || ctLabelRE.String() != label {
92 t.Fatalf("label pattern differs: policy %q schema %v compiled %q", label, defs["label"], ctLabelRE.String())
93 }
94 if alphabet := ctPolStr("identity.label_alphabet"); len(alphabet) != 32 {
95 t.Errorf("alphabet %q", alphabet)
96 }
97 // the detector a node builds for its own addresses accepts exactly the labels of the vectors
98 detector := ctHostPattern("devices.all-e2e.test")
99 for _, v := range ctList(t, ctVectors(t, "labels.json"), "labels") {
100 got := detector.MatchString(v["value"].(string) + ".devices.all-e2e.test")
101 if got != (v["ok"] == true) {
102 t.Errorf("%v: detector says %v", v["id"], got)
103 }
104 }
105}
106
107func TestContractsSchemaConstantsEqualThePolicy(t *testing.T) {
108 defs := ctSchemaSet.docs["common.schema.json"].(map[string]any)["$defs"].(map[string]any)
109 pattern := func(name string) any { return defs[name].(map[string]any)["pattern"] }
110 for name, key := range map[string]string{"label": "identity.label_regex", "fqdn": "identity.fqdn_regex", "device_id": "identity.device_id_regex",
111 "stream_id": "identity.stream_id_regex", "attach_secret": "identity.attach_secret_regex", "nonce": "identity.nonce_regex", "jti": "identity.jti_regex",
112 "kid": "identity.kid_regex"} {
113 if pattern(name) != ctPolStr(key) {
114 t.Errorf("schema %s pattern differs from policy %s", name, key)
115 }
116 }
117 for name, key := range map[string]string{"error_code": "protocol.error_codes", "close_reason": "protocol.close_reasons", "source_class": "protocol.source_classes",
118 "authorize_status": "enums.authorize_status", "budget_class": "enums.budget_class"} {
119 if !reflect.DeepEqual(ctStrings(defs[name].(map[string]any)["enum"]), ctPolStrings(key)) {
120 t.Errorf("schema %s enum differs from policy %s", name, key)
121 }
122 }
123 grammar := ctGrammar["types"].(map[string]any)
124 for _, kind := range []string{"stream_id", "attach_secret", "nonce"} {
125 if grammar[kind].(map[string]any)["pattern"] != pattern(kind) {
126 t.Errorf("grammar type %s differs from the schema", kind)
127 }
128 }
129}
130
131func TestContractsLabelVectors(t *testing.T) {
132 doc := ctVectors(t, "labels.json")
133 for _, v := range ctList(t, doc, "labels") {
134 if got := ctLabelOK(v["value"].(string)); got != (v["ok"] == true) {
135 t.Errorf("%v: label %q gave %v", v["id"], v["value"], got)
136 }
137 }
138 for _, v := range ctList(t, doc, "fqdns") {
139 var zones []string
140 if z, ok := v["zones"]; ok {
141 zones = ctStrings(z)
142 }
143 if got := ctFQDNOK(v["value"].(string), zones); got != (v["ok"] == true) {
144 t.Errorf("%v: host %q gave %v", v["id"], v["value"], got)
145 }
146 }
147}
148
149func TestContractsAddressVectors(t *testing.T) {
150 for _, v := range ctList(t, ctVectors(t, "lines.json"), "ips") {
151 if got := ctValidIP(v["value"].(string)); got != (v["ok"] == true) {
152 t.Errorf("%v: %q gave %v", v["id"], v["value"], got)
153 }
154 }
155}
156
157func TestContractsLineVectors(t *testing.T) {
158 for _, v := range ctList(t, ctVectors(t, "lines.json"), "lines") {
159 line, direction := v["line"].(string), v["dir"].(string)
160 parsed, err := ctParseLine(line, direction)
161 if v["ok"] == true {
162 if err != nil {
163 t.Errorf("%v: %v", v["id"], err)
164 continue
165 }
166 if !ctEqual(parsed, v["parsed"]) {
167 t.Errorf("%v: parsed %v, want %v", v["id"], parsed, v["parsed"])
168 }
169 if v["canonical"] == true {
170 if again, err := ctFormatLine(parsed); err != nil || again != line {
171 t.Errorf("%v: canonical form %q (%v), want %q", v["id"], again, err, line)
172 }
173 }
174 continue
175 }
176 if err == nil {
177 t.Errorf("%v: accepted %q", v["id"], truncate(line))
178 continue
179 }
180 if code := err.(*ctProtoErr).code; code != v["error"] {
181 t.Errorf("%v: error %q, want %q", v["id"], code, v["error"])
182 }
183 for _, suffix := range []string{"\n", "\x00", "\r"} {
184 if _, err := ctParseLine(line+suffix, direction); err == nil {
185 t.Errorf("%v: accepted with suffix %q", v["id"], suffix)
186 }
187 }
188 }
189}
190
191func truncate(s string) string {
192 if len(s) > 80 {
193 return s[:80] + "..."
194 }
195 return s
196}
197
198func TestContractsBodyVectors(t *testing.T) {
199 doc := ctVectors(t, "bodies.json")
200 for _, v := range ctList(t, doc, "bodies") {
201 errs := ctSchemaSet.validate(v["schema"].(string), v["body"])
202 if (len(errs) == 0) != (v["ok"] == true) {
203 t.Errorf("%v: errors %v", v["id"], errs)
204 }
205 }
206 for _, v := range ctList(t, doc, "feeds") {
207 errs := ctCheckFeed(v["body"], v["since"].(float64))
208 if (len(errs) == 0) != (v["ok"] == true) {
209 t.Errorf("%v: errors %v", v["id"], errs)
210 }
211 }
212}
213
214func TestContractsSessionVectors(t *testing.T) {
215 for _, v := range ctList(t, ctVectors(t, "tokens.json"), "sessions") {
216 ctx := v["ctx"].(map[string]any)
217 keys, code := ctParseJWKS(ctx["jwks"])
218 if code != "" {
219 t.Fatalf("%v: %s", v["id"], code)
220 }
221 token, _ := v["token"].(string) // a null token is "not a string": malformed
222 var got ctResult
223 if _, isString := v["token"].(string); !isString {
224 got = ctFail("malformed_token")
225 } else {
226 got = ctVerifySession(token, ctx["now"].(float64), ctx["audience"].(string), keys, ctx["skew"].(float64))
227 }
228 ctCheckVerdict(t, v, ctVerdict(got))
229 }
230}
231
232func TestContractsProofVectors(t *testing.T) {
233 for _, v := range ctList(t, ctVectors(t, "tokens.json"), "proofs") {
234 ctx := v["ctx"].(map[string]any)
235 keys, _ := ctParseJWKS(ctx["jwks"])
236 now, skew := ctx["now"].(float64), ctx["skew"].(float64)
237 session := ctVerifySession(ctx["session"].(string), now, ctPolStr("identity.aud_tunnel"), keys, skew)
238 if !session.ok {
239 t.Fatalf("%v: session %s", v["id"], session.err)
240 }
241 seen := map[string]bool{}
242 for _, id := range ctStrings(ctx["seen"]) {
243 seen[id] = true
244 }
245 got := ctVerifyProof(v["proof"].(string), now, ctx["session"].(string), session.claims, ctx["nonce"].(string), ctx["htu"].(string), skew, seen)
246 ctCheckVerdict(t, v, ctVerdict(got))
247 }
248}
249
250func TestContractsNodeAssertionVectors(t *testing.T) {
251 for _, v := range ctList(t, ctVectors(t, "tokens.json"), "assertions") {
252 ctx := v["ctx"].(map[string]any)
253 keys, code := ctParseNodeKeys(ctx["node_keys"])
254 if code != "" {
255 t.Fatalf("%v: %s", v["id"], code)
256 }
257 seen := map[string]bool{}
258 for _, id := range ctStrings(ctx["seen"]) {
259 seen[id] = true
260 }
261 got := ctVerifyNodeAssertion(v["assertion"].(string), ctx["now"].(float64), keys, ctx["method"].(string), ctx["url"].(string), []byte(ctx["body"].(string)),
262 ctx["skew"].(float64), seen)
263 ctCheckVerdict(t, v, ctVerdict(got))
264 }
265}
266
267func TestContractsStatementVectors(t *testing.T) {
268 for _, v := range ctList(t, ctVectors(t, "tokens.json"), "statements") {
269 ctx := v["ctx"].(map[string]any)
270 keys, _ := ctParseJWKS(ctx["jwks"])
271 got := ctVerifyStatement(v["statement"].(string), ctx["now"].(float64), keys, ctx["device_id"].(string), ctx["last_seq"].(float64))
272 ctCheckVerdict(t, v, ctVerdict(got))
273 }
274}
275
276func TestContractsSurfaceVectors(t *testing.T) {
277 for _, v := range ctList(t, ctVectors(t, "tokens.json"), "surface") {
278 switch v["kind"] {
279 case "challenge":
280 ctCheckVerdict(t, v, ctVerdict(ctVerifySurfaceChallenge(v["secret"].(string), v["nonce"].(string), v["mac"].(string))))
281 case "proof":
282 ctCheckVerdict(t, v, ctVerdict(ctVerifySurfaceProof(v["secret"].(string), v["nonce"].(string), int(v["pid"].(float64)), int(v["port"].(float64)), v["proof"].(string))))
283 default:
284 if got := ctIdentityRequest(v["host"].(string), int(v["port"].(float64)), v["nonce"].(string), v["mac"].(string)); got != v["text"] {
285 t.Errorf("%v: request %q, want %q", v["id"], got, v["text"])
286 }
287 }
288 }
289}
290
291func TestContractsKeySetVectors(t *testing.T) {
292 for _, v := range ctList(t, ctVectors(t, "tokens.json"), "key_sets") {
293 var code string
294 if v["kind"] == "jwks" {
295 _, code = ctParseJWKS(v["doc"])
296 } else {
297 _, code = ctParseNodeKeys(v["doc"])
298 }
299 got := map[string]any{"ok": true}
300 if code != "" {
301 got = map[string]any{"ok": false, "error": code}
302 }
303 ctCheckVerdict(t, v, got)
304 }
305}
306
307func TestContractsTranscriptVectors(t *testing.T) {
308 for _, v := range ctList(t, ctVectors(t, "transcripts.json"), "transcripts") {
309 violation := ctCheckTranscript(v)
310 expect := v["expect"].(map[string]any)
311 if (violation == "") != (expect["ok"] == true) || (violation != "" && violation != expect["violation"]) {
312 t.Errorf("%v: violation %q, want %v", v["id"], violation, expect)
313 }
314 }
315}
316
317// TestContractsGoNodeDoesWhatTheGoldenTranscriptsSay runs the scripted inputs of every golden transcript through an independent Go node
318// (the twin of contracts/fake_node.py) and demands the very outputs Python produced: two implementations, one behaviour.
319func TestContractsGoNodeDoesWhatTheGoldenTranscriptsSay(t *testing.T) {
320 replayed := 0
321 for _, v := range ctList(t, ctVectors(t, "transcripts.json"), "transcripts") {
322 if v["replay"] != true {
323 continue
324 }
325 replayed++
326 inputs, golden := ctSplitTranscript(v["events"].([]any))
327 node := ctNewNodeSim(v["ctx"].(map[string]any))
328 var produced []map[string]any
329 for _, input := range inputs {
330 produced = append(produced, node.handle(input)...)
331 }
332 if len(produced) != len(golden) {
333 t.Errorf("%v: %d outputs, want %d", v["id"], len(produced), len(golden))
334 continue
335 }
336 for i := range golden {
337 if !ctEqual(toAny(produced[i]), toAny(golden[i])) {
338 t.Errorf("%v: output %d is %v, want %v", v["id"], i, produced[i], golden[i])
339 break
340 }
341 }
342 }
343 if replayed < 8 {
344 t.Errorf("only %d replayable transcripts", replayed)
345 }
346}
347
348func toAny(m map[string]any) any { return m }
349
350func TestContractsEveryTruncationOfAGoodTokenIsRefused(t *testing.T) {
351 tokens := ctVectors(t, "tokens.json")
352 var vector map[string]any
353 for _, v := range ctList(t, tokens, "proofs") {
354 if v["id"] == "proof.ok.baseline" {
355 vector = v
356 }
357 }
358 ctx := vector["ctx"].(map[string]any)
359 keys, _ := ctParseJWKS(ctx["jwks"])
360 session := ctx["session"].(string)
361 claims := ctVerifySession(session, ctx["now"].(float64), "all-tunnel", keys, 5).claims
362 for _, token := range []string{session, vector["proof"].(string)} {
363 for cut := 0; cut < len(token)-1; cut += 7 {
364 if ctVerifySession(token[:cut], ctx["now"].(float64), "all-tunnel", keys, 5).ok {
365 t.Fatalf("truncated session accepted at %d", cut)
366 }
367 if ctVerifyProof(token[:cut], ctx["now"].(float64), session, claims, ctx["nonce"].(string), ctx["htu"].(string), 5, map[string]bool{}).ok {
368 t.Fatalf("truncated proof accepted at %d", cut)
369 }
370 }
371 for i := 0; i < len(token); i += 11 {
372 flipped := token[:i] + map[bool]string{true: "B", false: "A"}[token[i] == 'A'] + token[i+1:]
373 if strings.Count(flipped, ".") == 2 && ctVerifySession(flipped, ctx["now"].(float64), "all-tunnel", keys, 5).ok && flipped != token {
374 t.Fatalf("corrupted session accepted at %d", i)
375 }
376 }
377 }
378}