Solo para leer y verificar. No puedes copiar, usar, modificar ni redistribuir este código. Aviso completo en la página principal.
tunnel/contracts_test.go
| 1 | package 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 | |
| 7 | import ( |
| 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). |
| 16 | func ctVectorsDir() string { |
| 17 | if dir := os.Getenv("ALL_CONTRACT_VECTORS_DIR"); dir != "" { |
| 18 | return dir |
| 19 | } |
| 20 | return filepath.Join(ctContracts, "vectors") |
| 21 | } |
| 22 | |
| 23 | func 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 | |
| 32 | func 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 | |
| 45 | func 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 | |
| 52 | func 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 | |
| 59 | func 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 | |
| 67 | func 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 | |
| 87 | func 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 | |
| 107 | func 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 | |
| 131 | func 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 | |
| 149 | func 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 | |
| 157 | func 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 | |
| 191 | func truncate(s string) string { |
| 192 | if len(s) > 80 { |
| 193 | return s[:80] + "..." |
| 194 | } |
| 195 | return s |
| 196 | } |
| 197 | |
| 198 | func 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 | |
| 214 | func 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 | |
| 232 | func 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 | |
| 250 | func 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 | |
| 267 | func 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 | |
| 276 | func 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 | |
| 291 | func 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 | |
| 307 | func 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. |
| 319 | func 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 | |
| 348 | func toAny(m map[string]any) any { return m } |
| 349 | |
| 350 | func 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 | } |