Solo para leer y verificar. No puedes copiar, usar, modificar ni redistribuir este código. Aviso completo en la página principal.
tunnel/contracts_verify_test.go
| 1 | package main |
| 2 | |
| 3 | // The Go reference verifier of the ALL 2.0 contracts (the contract): the same checks as contracts/e2e_contracts.py and |
| 4 | // contracts/ts/verify.ts, written independently, so that the three give the same verdict AND the same error word on every golden vector |
| 5 | // (contracts_test.go runs them). It lives in a test file on purpose: WP01 is spec and fixtures only. The node (WP05, WP06) is written in its |
| 6 | // own files and is tested against these functions and against the vectors; every constant is read from contracts/policy.json, never typed. |
| 7 | |
| 8 | import ( |
| 9 | "crypto/ed25519" |
| 10 | "crypto/hmac" |
| 11 | "crypto/sha256" |
| 12 | "crypto/subtle" |
| 13 | "encoding/base64" |
| 14 | "encoding/json" |
| 15 | "errors" |
| 16 | "fmt" |
| 17 | "io" |
| 18 | "math" |
| 19 | "net/netip" |
| 20 | "os" |
| 21 | "path/filepath" |
| 22 | "regexp" |
| 23 | "sort" |
| 24 | "strconv" |
| 25 | "strings" |
| 26 | "unicode/utf8" |
| 27 | ) |
| 28 | |
| 29 | const ctContracts = "../contracts" |
| 30 | |
| 31 | // ---------------------------------------------------------------------------------------------------------------------------- JSON |
| 32 | |
| 33 | // ctParseJSON parses one JSON value: duplicate object keys are an error (a body with a repeated key means different things to different |
| 34 | // implementations), numbers are float64 and a non-integral one is the caller's business, anything after the value is an error. |
| 35 | func ctParseJSON(text string) (any, error) { |
| 36 | if !utf8.ValidString(text) { |
| 37 | return nil, errors.New("utf-8") |
| 38 | } |
| 39 | dec := json.NewDecoder(strings.NewReader(text)) |
| 40 | dec.UseNumber() |
| 41 | value, err := ctDecodeValue(dec) |
| 42 | if err != nil { |
| 43 | return nil, err |
| 44 | } |
| 45 | if _, err := dec.Token(); err != io.EOF { |
| 46 | return nil, errors.New("trailing data") |
| 47 | } |
| 48 | return value, nil |
| 49 | } |
| 50 | |
| 51 | func ctDecodeValue(dec *json.Decoder) (any, error) { |
| 52 | token, err := dec.Token() |
| 53 | if err != nil { |
| 54 | return nil, err |
| 55 | } |
| 56 | switch t := token.(type) { |
| 57 | case json.Delim: |
| 58 | if t == '{' { |
| 59 | out := map[string]any{} |
| 60 | for dec.More() { |
| 61 | keyToken, err := dec.Token() |
| 62 | if err != nil { |
| 63 | return nil, err |
| 64 | } |
| 65 | key, ok := keyToken.(string) |
| 66 | if !ok { |
| 67 | return nil, errors.New("key") |
| 68 | } |
| 69 | if _, dup := out[key]; dup { |
| 70 | return nil, errors.New("duplicate key") |
| 71 | } |
| 72 | value, err := ctDecodeValue(dec) |
| 73 | if err != nil { |
| 74 | return nil, err |
| 75 | } |
| 76 | out[key] = value |
| 77 | } |
| 78 | if _, err := dec.Token(); err != nil { |
| 79 | return nil, err |
| 80 | } |
| 81 | return out, nil |
| 82 | } |
| 83 | if t == '[' { |
| 84 | out := []any{} |
| 85 | for dec.More() { |
| 86 | value, err := ctDecodeValue(dec) |
| 87 | if err != nil { |
| 88 | return nil, err |
| 89 | } |
| 90 | out = append(out, value) |
| 91 | } |
| 92 | if _, err := dec.Token(); err != nil { |
| 93 | return nil, err |
| 94 | } |
| 95 | return out, nil |
| 96 | } |
| 97 | return nil, errors.New("delimiter") |
| 98 | case json.Number: |
| 99 | f, err := strconv.ParseFloat(string(t), 64) |
| 100 | if err != nil || math.IsInf(f, 0) { |
| 101 | return nil, errors.New("number") |
| 102 | } |
| 103 | return f, nil |
| 104 | default: |
| 105 | return token, nil // string, bool, nil |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | // ctNoFloats refuses a non-integral number anywhere in a value (bodies hold integers, never fractions). |
| 110 | func ctNoFloats(value any) error { |
| 111 | switch v := value.(type) { |
| 112 | case float64: |
| 113 | if v != math.Trunc(v) { |
| 114 | return errors.New("float") |
| 115 | } |
| 116 | case map[string]any: |
| 117 | for _, item := range v { |
| 118 | if err := ctNoFloats(item); err != nil { |
| 119 | return err |
| 120 | } |
| 121 | } |
| 122 | case []any: |
| 123 | for _, item := range v { |
| 124 | if err := ctNoFloats(item); err != nil { |
| 125 | return err |
| 126 | } |
| 127 | } |
| 128 | } |
| 129 | return nil |
| 130 | } |
| 131 | |
| 132 | func ctEscape(s string) string { |
| 133 | var b strings.Builder |
| 134 | b.WriteByte('"') |
| 135 | for _, r := range s { |
| 136 | switch { |
| 137 | case r == '"': |
| 138 | b.WriteString(`\"`) |
| 139 | case r == '\\': |
| 140 | b.WriteString(`\\`) |
| 141 | case r == '\n': |
| 142 | b.WriteString(`\n`) |
| 143 | case r == '\r': |
| 144 | b.WriteString(`\r`) |
| 145 | case r == '\t': |
| 146 | b.WriteString(`\t`) |
| 147 | case r == '\b': |
| 148 | b.WriteString(`\b`) |
| 149 | case r == '\f': |
| 150 | b.WriteString(`\f`) |
| 151 | case r < 0x20 || r > 0x7e: |
| 152 | if r > 0xffff { |
| 153 | r -= 0x10000 |
| 154 | fmt.Fprintf(&b, `\u%04x\u%04x`, 0xd800+(r>>10), 0xdc00+(r&0x3ff)) |
| 155 | } else { |
| 156 | fmt.Fprintf(&b, `\u%04x`, r) |
| 157 | } |
| 158 | default: |
| 159 | b.WriteRune(r) |
| 160 | } |
| 161 | } |
| 162 | b.WriteByte('"') |
| 163 | return b.String() |
| 164 | } |
| 165 | |
| 166 | // ctCanonicalJSON is the form of a JSON body on a wire line: keys sorted, no whitespace, ASCII only. |
| 167 | func ctCanonicalJSON(value any) string { |
| 168 | switch v := value.(type) { |
| 169 | case nil: |
| 170 | return "null" |
| 171 | case bool: |
| 172 | return strconv.FormatBool(v) |
| 173 | case float64: |
| 174 | return strconv.FormatInt(int64(v), 10) |
| 175 | case string: |
| 176 | return ctEscape(v) |
| 177 | case []any: |
| 178 | parts := make([]string, len(v)) |
| 179 | for i, item := range v { |
| 180 | parts[i] = ctCanonicalJSON(item) |
| 181 | } |
| 182 | return "[" + strings.Join(parts, ",") + "]" |
| 183 | case map[string]any: |
| 184 | keys := make([]string, 0, len(v)) |
| 185 | for key := range v { |
| 186 | keys = append(keys, key) |
| 187 | } |
| 188 | sort.Strings(keys) |
| 189 | parts := make([]string, len(keys)) |
| 190 | for i, key := range keys { |
| 191 | parts[i] = ctEscape(key) + ":" + ctCanonicalJSON(v[key]) |
| 192 | } |
| 193 | return "{" + strings.Join(parts, ",") + "}" |
| 194 | } |
| 195 | panic(fmt.Sprintf("canonical json: %T", value)) |
| 196 | } |
| 197 | |
| 198 | func ctEqual(a, b any) bool { |
| 199 | switch x := a.(type) { |
| 200 | case nil: |
| 201 | return b == nil |
| 202 | case bool: |
| 203 | y, ok := b.(bool) |
| 204 | return ok && x == y |
| 205 | case float64: |
| 206 | y, ok := b.(float64) |
| 207 | return ok && x == y |
| 208 | case string: |
| 209 | y, ok := b.(string) |
| 210 | return ok && x == y |
| 211 | case []any: |
| 212 | y, ok := b.([]any) |
| 213 | if !ok || len(x) != len(y) { |
| 214 | return false |
| 215 | } |
| 216 | for i := range x { |
| 217 | if !ctEqual(x[i], y[i]) { |
| 218 | return false |
| 219 | } |
| 220 | } |
| 221 | return true |
| 222 | case map[string]any: |
| 223 | y, ok := b.(map[string]any) |
| 224 | if !ok || len(x) != len(y) { |
| 225 | return false |
| 226 | } |
| 227 | for key, item := range x { |
| 228 | other, present := y[key] |
| 229 | if !present || !ctEqual(item, other) { |
| 230 | return false |
| 231 | } |
| 232 | } |
| 233 | return true |
| 234 | } |
| 235 | return false |
| 236 | } |
| 237 | |
| 238 | func ctLoadFile(path string) (any, error) { |
| 239 | raw, err := os.ReadFile(path) |
| 240 | if err != nil { |
| 241 | return nil, err |
| 242 | } |
| 243 | return ctParseJSON(string(raw)) |
| 244 | } |
| 245 | |
| 246 | // ---------------------------------------------------------------------------------------------------------------------------- policy |
| 247 | |
| 248 | var ctPolicy = func() map[string]any { |
| 249 | value, err := ctLoadFile(filepath.Join(ctContracts, "policy.json")) |
| 250 | if err != nil { |
| 251 | panic(err) |
| 252 | } |
| 253 | return value.(map[string]any) |
| 254 | }() |
| 255 | |
| 256 | // ctPol reads one policy value by its dotted key. |
| 257 | func ctPol(dotted string) any { |
| 258 | var value any = ctPolicy |
| 259 | for _, part := range strings.Split(dotted, ".") { |
| 260 | value = value.(map[string]any)[part] |
| 261 | } |
| 262 | return value |
| 263 | } |
| 264 | |
| 265 | func ctPolInt(dotted string) int { return int(ctPol(dotted).(float64)) } |
| 266 | func ctPolStr(dotted string) string { return ctPol(dotted).(string) } |
| 267 | |
| 268 | func ctPolStrings(dotted string) []string { |
| 269 | var out []string |
| 270 | for _, item := range ctPol(dotted).([]any) { |
| 271 | out = append(out, item.(string)) |
| 272 | } |
| 273 | return out |
| 274 | } |
| 275 | |
| 276 | // ctAnchored compiles one of the contracts' ^...$ patterns (RE2: `$` is the end of the text, as in JavaScript). |
| 277 | func ctAnchored(pattern string) *regexp.Regexp { |
| 278 | if !strings.HasPrefix(pattern, "^") || !strings.HasSuffix(pattern, "$") { |
| 279 | panic("contract patterns are anchored: " + pattern) |
| 280 | } |
| 281 | return regexp.MustCompile(pattern) |
| 282 | } |
| 283 | |
| 284 | var ( |
| 285 | ctLabelRE = ctAnchored(ctPolStr("identity.label_regex")) |
| 286 | ctFQDNRE = ctAnchored(ctPolStr("identity.fqdn_regex")) |
| 287 | ctNonceRE = ctAnchored(ctPolStr("identity.nonce_regex")) |
| 288 | ctKidRE = ctAnchored(ctPolStr("identity.kid_regex")) |
| 289 | ctDeviceIDRE = ctAnchored(ctPolStr("identity.device_id_regex")) |
| 290 | ) |
| 291 | |
| 292 | func ctLabelOK(value string) bool { return ctLabelRE.MatchString(value) } |
| 293 | |
| 294 | func ctFQDNOK(value string, zones []string) bool { |
| 295 | if len(value) > ctPolInt("identity.fqdn_max_length") || !ctFQDNRE.MatchString(value) { |
| 296 | return false |
| 297 | } |
| 298 | if zones == nil { |
| 299 | return true |
| 300 | } |
| 301 | zone := value[strings.Index(value, ".")+1:] |
| 302 | for _, z := range zones { |
| 303 | if z == zone { |
| 304 | return true |
| 305 | } |
| 306 | } |
| 307 | return false |
| 308 | } |
| 309 | |
| 310 | // ctHostPattern is the detector a node or a device builds for its own address in a zone. |
| 311 | func ctHostPattern(zone string) *regexp.Regexp { |
| 312 | label := ctPolStr("identity.label_regex") |
| 313 | return regexp.MustCompile("^" + label[1:len(label)-1] + `\.` + regexp.QuoteMeta(zone) + "$") |
| 314 | } |
| 315 | |
| 316 | func ctValidIP(value string) bool { |
| 317 | if value == "" || value != strings.ToLower(value) || strings.ContainsAny(value, "%[] \t\r\n/") { |
| 318 | return false |
| 319 | } |
| 320 | _, err := netip.ParseAddr(value) |
| 321 | return err == nil |
| 322 | } |
| 323 | |
| 324 | // ---------------------------------------------------------------------------------------------------------------------------- JSON Schema subset |
| 325 | |
| 326 | type ctSchemas struct { |
| 327 | docs map[string]any |
| 328 | patterns map[string]*regexp.Regexp |
| 329 | } |
| 330 | |
| 331 | func ctLoadSchemas() *ctSchemas { |
| 332 | matches, err := filepath.Glob(filepath.Join(ctContracts, "schemas", "*.schema.json")) |
| 333 | if err != nil || len(matches) == 0 { |
| 334 | panic("no schemas") |
| 335 | } |
| 336 | s := &ctSchemas{docs: map[string]any{}, patterns: map[string]*regexp.Regexp{}} |
| 337 | for _, path := range matches { |
| 338 | doc, err := ctLoadFile(path) |
| 339 | if err != nil { |
| 340 | panic(err) |
| 341 | } |
| 342 | s.docs[filepath.Base(path)] = doc |
| 343 | } |
| 344 | return s |
| 345 | } |
| 346 | |
| 347 | func (s *ctSchemas) validate(name string, instance any) []string { |
| 348 | var errs []string |
| 349 | s.check(name, s.docs[name], instance, "", &errs) |
| 350 | return errs |
| 351 | } |
| 352 | |
| 353 | func (s *ctSchemas) resolve(doc, ref string) (string, any) { |
| 354 | file, pointer, _ := strings.Cut(ref, "#") |
| 355 | target := file |
| 356 | if target == "" { |
| 357 | target = doc |
| 358 | } |
| 359 | node := s.docs[target] |
| 360 | if pointer != "" { |
| 361 | for _, part := range strings.Split(strings.TrimPrefix(pointer, "/"), "/") { |
| 362 | part = strings.ReplaceAll(strings.ReplaceAll(part, "~1", "/"), "~0", "~") |
| 363 | node = node.(map[string]any)[part] |
| 364 | } |
| 365 | } |
| 366 | return target, node |
| 367 | } |
| 368 | |
| 369 | func (s *ctSchemas) pattern(text string) *regexp.Regexp { |
| 370 | if p, ok := s.patterns[text]; ok { |
| 371 | return p |
| 372 | } |
| 373 | p := ctAnchored(text) |
| 374 | s.patterns[text] = p |
| 375 | return p |
| 376 | } |
| 377 | |
| 378 | func ctTypeOK(kind string, value any) bool { |
| 379 | switch kind { |
| 380 | case "string": |
| 381 | _, ok := value.(string) |
| 382 | return ok |
| 383 | case "boolean": |
| 384 | _, ok := value.(bool) |
| 385 | return ok |
| 386 | case "null": |
| 387 | return value == nil |
| 388 | case "object": |
| 389 | _, ok := value.(map[string]any) |
| 390 | return ok |
| 391 | case "array": |
| 392 | _, ok := value.([]any) |
| 393 | return ok |
| 394 | case "integer": |
| 395 | f, ok := value.(float64) |
| 396 | return ok && f == math.Trunc(f) |
| 397 | case "number": |
| 398 | _, ok := value.(float64) |
| 399 | return ok |
| 400 | } |
| 401 | return false |
| 402 | } |
| 403 | |
| 404 | func (s *ctSchemas) check(doc string, schemaAny any, value any, path string, errs *[]string) { |
| 405 | schema := schemaAny.(map[string]any) |
| 406 | at := path |
| 407 | if at == "" { |
| 408 | at = "/" |
| 409 | } |
| 410 | if ref, ok := schema["$ref"].(string); ok { |
| 411 | target, node := s.resolve(doc, ref) |
| 412 | s.check(target, node, value, path, errs) |
| 413 | } |
| 414 | if want, ok := schema["const"]; ok && !ctEqual(value, want) { |
| 415 | *errs = append(*errs, at+": const") |
| 416 | } |
| 417 | if list, ok := schema["enum"].([]any); ok { |
| 418 | found := false |
| 419 | for _, item := range list { |
| 420 | found = found || ctEqual(value, item) |
| 421 | } |
| 422 | if !found { |
| 423 | *errs = append(*errs, at+": enum") |
| 424 | } |
| 425 | } |
| 426 | if kinds, ok := schema["type"]; ok { |
| 427 | var names []any |
| 428 | if list, isList := kinds.([]any); isList { |
| 429 | names = list |
| 430 | } else { |
| 431 | names = []any{kinds} |
| 432 | } |
| 433 | match := false |
| 434 | for _, name := range names { |
| 435 | match = match || ctTypeOK(name.(string), value) |
| 436 | } |
| 437 | if !match { |
| 438 | *errs = append(*errs, at+": type") |
| 439 | return |
| 440 | } |
| 441 | } |
| 442 | if options, ok := schema["oneOf"].([]any); ok { |
| 443 | hits := 0 |
| 444 | for _, option := range options { |
| 445 | var trial []string |
| 446 | s.check(doc, option, value, path, &trial) |
| 447 | if len(trial) == 0 { |
| 448 | hits++ |
| 449 | } |
| 450 | } |
| 451 | if hits != 1 { |
| 452 | *errs = append(*errs, at+": oneOf") |
| 453 | } |
| 454 | } |
| 455 | switch v := value.(type) { |
| 456 | case string: |
| 457 | if n, ok := schema["minLength"].(float64); ok && float64(utf8.RuneCountInString(v)) < n { |
| 458 | *errs = append(*errs, at+": minLength") |
| 459 | } |
| 460 | if n, ok := schema["maxLength"].(float64); ok && float64(utf8.RuneCountInString(v)) > n { |
| 461 | *errs = append(*errs, at+": maxLength") |
| 462 | } |
| 463 | if p, ok := schema["pattern"].(string); ok && !s.pattern(p).MatchString(v) { |
| 464 | *errs = append(*errs, at+": pattern") |
| 465 | } |
| 466 | if schema["format"] == "ip" && !ctValidIP(v) { |
| 467 | *errs = append(*errs, at+": format") |
| 468 | } |
| 469 | case float64: |
| 470 | if n, ok := schema["minimum"].(float64); ok && v < n { |
| 471 | *errs = append(*errs, at+": minimum") |
| 472 | } |
| 473 | if n, ok := schema["maximum"].(float64); ok && v > n { |
| 474 | *errs = append(*errs, at+": maximum") |
| 475 | } |
| 476 | case map[string]any: |
| 477 | if required, ok := schema["required"].([]any); ok { |
| 478 | for _, key := range required { |
| 479 | if _, present := v[key.(string)]; !present { |
| 480 | *errs = append(*errs, path+"/"+key.(string)+": required") |
| 481 | } |
| 482 | } |
| 483 | } |
| 484 | props, _ := schema["properties"].(map[string]any) |
| 485 | keys := make([]string, 0, len(v)) |
| 486 | for key := range v { |
| 487 | keys = append(keys, key) |
| 488 | } |
| 489 | sort.Strings(keys) |
| 490 | for _, key := range keys { |
| 491 | if sub, ok := props[key]; ok { |
| 492 | s.check(doc, sub, v[key], path+"/"+key, errs) |
| 493 | } else if schema["additionalProperties"] == false { |
| 494 | *errs = append(*errs, path+"/"+key+": additionalProperties") |
| 495 | } |
| 496 | } |
| 497 | case []any: |
| 498 | if n, ok := schema["minItems"].(float64); ok && float64(len(v)) < n { |
| 499 | *errs = append(*errs, at+": minItems") |
| 500 | } |
| 501 | if n, ok := schema["maxItems"].(float64); ok && float64(len(v)) > n { |
| 502 | *errs = append(*errs, at+": maxItems") |
| 503 | } |
| 504 | if schema["uniqueItems"] == true { |
| 505 | for i := range v { |
| 506 | for j := i + 1; j < len(v); j++ { |
| 507 | if ctEqual(v[i], v[j]) { |
| 508 | *errs = append(*errs, at+": uniqueItems") |
| 509 | i, j = len(v), len(v) |
| 510 | } |
| 511 | } |
| 512 | } |
| 513 | } |
| 514 | if items, ok := schema["items"]; ok { |
| 515 | for i, item := range v { |
| 516 | s.check(doc, items, item, path+"/"+strconv.Itoa(i), errs) |
| 517 | } |
| 518 | } |
| 519 | } |
| 520 | } |
| 521 | |
| 522 | var ctSchemaSet = ctLoadSchemas() |
| 523 | |
| 524 | // ---------------------------------------------------------------------------------------------------------------------------- the wire grammar |
| 525 | |
| 526 | type ctProtoErr struct{ code string } |
| 527 | |
| 528 | func (e *ctProtoErr) Error() string { return e.code } |
| 529 | |
| 530 | var ctGrammar = func() map[string]any { |
| 531 | value, err := ctLoadFile(filepath.Join(ctContracts, "schemas", "wire-v2.grammar.json")) |
| 532 | if err != nil { |
| 533 | panic(err) |
| 534 | } |
| 535 | return value.(map[string]any) |
| 536 | }() |
| 537 | |
| 538 | func ctFieldOK(kind, token string) bool { |
| 539 | spec := ctGrammar["types"].(map[string]any)[kind].(map[string]any) |
| 540 | if key, ok := spec["max_bytes_from"].(string); ok && len(token) > int(ctPol(key).(float64)) { |
| 541 | return false |
| 542 | } |
| 543 | if p, ok := spec["pattern"].(string); ok && !ctAnchored(p).MatchString(token) { |
| 544 | return false |
| 545 | } |
| 546 | if spec["format"] == "ip" && !ctValidIP(token) { |
| 547 | return false |
| 548 | } |
| 549 | if key, ok := spec["enum_from"].(string); ok { |
| 550 | found := false |
| 551 | for _, item := range ctPolStrings(key) { |
| 552 | found = found || item == token |
| 553 | } |
| 554 | if !found { |
| 555 | return false |
| 556 | } |
| 557 | } |
| 558 | return true |
| 559 | } |
| 560 | |
| 561 | func ctRules(spec map[string]any, key string) []map[string]any { |
| 562 | var out []map[string]any |
| 563 | if list, ok := spec[key].([]any); ok { |
| 564 | for _, item := range list { |
| 565 | out = append(out, item.(map[string]any)) |
| 566 | } |
| 567 | } |
| 568 | return out |
| 569 | } |
| 570 | |
| 571 | // ctParseLine parses one protocol line (the text before the LF) travelling in `direction` ("d2n" or "n2d"): {"verb": ..., <field>: ...}; HINT |
| 572 | // gives "addresses" and OK gives "body". The error is a *ctProtoErr whose code is the word the node answers with. |
| 573 | func ctParseLine(line, direction string) (map[string]any, error) { |
| 574 | for _, r := range line { |
| 575 | if r < 0x20 || r > 0x7e { |
| 576 | return nil, &ctProtoErr{"bad_command"} |
| 577 | } |
| 578 | } |
| 579 | verb, rest, hasSpace := strings.Cut(line, " ") |
| 580 | var spec map[string]any |
| 581 | if s, ok := ctGrammar["verbs"].(map[string]any)[verb]; ok { |
| 582 | spec = s.(map[string]any) |
| 583 | } |
| 584 | limit := ctPolInt("protocol.line_max_bytes") |
| 585 | if spec != nil { |
| 586 | if key, ok := spec["max"].(string); ok { |
| 587 | limit = int(ctPol(key).(float64)) |
| 588 | } |
| 589 | } |
| 590 | if len(line)+1 > limit { |
| 591 | return nil, &ctProtoErr{"line_too_long"} |
| 592 | } |
| 593 | if spec == nil || (spec["dir"] != direction && spec["dir"] != "both") || line == "" || (hasSpace && rest == "") { |
| 594 | return nil, &ctProtoErr{"bad_command"} |
| 595 | } |
| 596 | out := map[string]any{"verb": verb} |
| 597 | if schema, ok := spec["json"].(string); ok { |
| 598 | if !hasSpace { |
| 599 | return nil, &ctProtoErr{"bad_command"} |
| 600 | } |
| 601 | body, err := ctParseJSON(rest) |
| 602 | if err == nil { |
| 603 | err = ctNoFloats(body) |
| 604 | } |
| 605 | if err != nil || len(ctSchemaSet.validate(schema, body)) > 0 { |
| 606 | return nil, &ctProtoErr{"bad_command"} |
| 607 | } |
| 608 | out["body"] = body |
| 609 | return out, nil |
| 610 | } |
| 611 | var tokens []string |
| 612 | if hasSpace { |
| 613 | tokens = strings.Split(rest, " ") |
| 614 | } |
| 615 | for _, token := range tokens { |
| 616 | if token == "" { |
| 617 | return nil, &ctProtoErr{"bad_command"} |
| 618 | } |
| 619 | } |
| 620 | if repeat, ok := spec["repeat"].(map[string]any); ok { |
| 621 | min, max := int(repeat["min"].(float64)), int(ctPol(repeat["max_from"].(string)).(float64)) |
| 622 | if len(tokens) < min || len(tokens) > max { |
| 623 | return nil, &ctProtoErr{"bad_command"} |
| 624 | } |
| 625 | addresses := make([]any, len(tokens)) |
| 626 | for i, token := range tokens { |
| 627 | if !ctFieldOK(repeat["type"].(string), token) { |
| 628 | return nil, &ctProtoErr{"bad_command"} |
| 629 | } |
| 630 | addresses[i] = token |
| 631 | } |
| 632 | out["addresses"] = addresses |
| 633 | return out, nil |
| 634 | } |
| 635 | fields, optional := ctRules(spec, "fields"), ctRules(spec, "optional") |
| 636 | if len(tokens) < len(fields) || len(tokens) > len(fields)+len(optional) { |
| 637 | return nil, &ctProtoErr{"bad_command"} |
| 638 | } |
| 639 | for i, token := range tokens { |
| 640 | rule := append(append([]map[string]any{}, fields...), optional...)[i] |
| 641 | kind := rule["type"].(string) |
| 642 | if want, ok := rule["equals"].(string); ok && token != want { |
| 643 | if ctFieldOK(kind, token) { |
| 644 | return nil, &ctProtoErr{rule["error_if_number"].(string)} |
| 645 | } |
| 646 | return nil, &ctProtoErr{"bad_command"} |
| 647 | } |
| 648 | if !ctFieldOK(kind, token) { |
| 649 | code := "bad_command" |
| 650 | if custom, ok := rule["error"].(string); ok { |
| 651 | code = custom |
| 652 | } |
| 653 | return nil, &ctProtoErr{code} |
| 654 | } |
| 655 | out[rule["name"].(string)] = token |
| 656 | } |
| 657 | return out, nil |
| 658 | } |
| 659 | |
| 660 | // ctFormatLine is the canonical line of a parsed message (the inverse of ctParseLine, which it is checked against). |
| 661 | func ctFormatLine(message map[string]any) (string, error) { |
| 662 | verb := message["verb"].(string) |
| 663 | spec := ctGrammar["verbs"].(map[string]any)[verb].(map[string]any) |
| 664 | var line string |
| 665 | switch { |
| 666 | case spec["json"] != nil: |
| 667 | line = verb + " " + ctCanonicalJSON(message["body"]) |
| 668 | case spec["repeat"] != nil: |
| 669 | parts := []string{verb} |
| 670 | for _, address := range message["addresses"].([]any) { |
| 671 | parts = append(parts, address.(string)) |
| 672 | } |
| 673 | line = strings.Join(parts, " ") |
| 674 | default: |
| 675 | parts := []string{verb} |
| 676 | for _, rule := range append(ctRules(spec, "fields"), ctRules(spec, "optional")...) { |
| 677 | if value, ok := message[rule["name"].(string)]; ok { |
| 678 | parts = append(parts, value.(string)) |
| 679 | } |
| 680 | } |
| 681 | line = strings.Join(parts, " ") |
| 682 | } |
| 683 | direction := spec["dir"].(string) |
| 684 | if direction == "both" { |
| 685 | direction = "d2n" |
| 686 | } |
| 687 | if _, err := ctParseLine(line, direction); err != nil { |
| 688 | return "", err |
| 689 | } |
| 690 | return line, nil |
| 691 | } |
| 692 | |
| 693 | // ---------------------------------------------------------------------------------------------------------------------------- tokens |
| 694 | |
| 695 | type ctResult struct { |
| 696 | ok bool |
| 697 | err string |
| 698 | claims map[string]any |
| 699 | jti string |
| 700 | } |
| 701 | |
| 702 | func ctFail(code string) ctResult { return ctResult{err: code} } |
| 703 | |
| 704 | var ctB64Charset = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) |
| 705 | |
| 706 | func ctB64Decode(text string) ([]byte, error) { |
| 707 | if !ctB64Charset.MatchString(text) { |
| 708 | return nil, errors.New("base64url") |
| 709 | } |
| 710 | raw, err := base64.RawURLEncoding.Strict().DecodeString(text) |
| 711 | if err != nil || base64.RawURLEncoding.EncodeToString(raw) != text { |
| 712 | return nil, errors.New("base64url") |
| 713 | } |
| 714 | return raw, nil |
| 715 | } |
| 716 | |
| 717 | func ctB64Encode(raw []byte) string { return base64.RawURLEncoding.EncodeToString(raw) } |
| 718 | |
| 719 | func ctSHA256B64(data []byte) string { |
| 720 | sum := sha256.Sum256(data) |
| 721 | return ctB64Encode(sum[:]) |
| 722 | } |
| 723 | |
| 724 | type ctJWS struct { |
| 725 | header, claims map[string]any |
| 726 | signature []byte |
| 727 | signingInput []byte |
| 728 | } |
| 729 | |
| 730 | // ctJWSParts splits and decodes a compact JWS; any structural fault is "malformed_token". An empty signature part is allowed here (alg none |
| 731 | // then fails as bad_alg, which is the more useful word). |
| 732 | func ctJWSParts(token string, maxBytes int) (*ctJWS, string) { |
| 733 | for _, r := range token { |
| 734 | if r > 0x7f { |
| 735 | return nil, "malformed_token" |
| 736 | } |
| 737 | } |
| 738 | if len(token) > maxBytes { |
| 739 | return nil, "malformed_token" |
| 740 | } |
| 741 | parts := strings.Split(token, ".") |
| 742 | if len(parts) != 3 { |
| 743 | return nil, "malformed_token" |
| 744 | } |
| 745 | decode := func(text string) (map[string]any, bool) { |
| 746 | raw, err := ctB64Decode(text) |
| 747 | if err != nil { |
| 748 | return nil, false |
| 749 | } |
| 750 | value, err := ctParseJSON(string(raw)) |
| 751 | if err != nil { |
| 752 | return nil, false |
| 753 | } |
| 754 | object, ok := value.(map[string]any) |
| 755 | return object, ok |
| 756 | } |
| 757 | header, ok1 := decode(parts[0]) |
| 758 | claims, ok2 := decode(parts[1]) |
| 759 | if !ok1 || !ok2 { |
| 760 | return nil, "malformed_token" |
| 761 | } |
| 762 | var signature []byte |
| 763 | if parts[2] != "" { |
| 764 | raw, err := ctB64Decode(parts[2]) |
| 765 | if err != nil { |
| 766 | return nil, "malformed_token" |
| 767 | } |
| 768 | signature = raw |
| 769 | } |
| 770 | for _, name := range []string{"crit", "b64", "jku", "x5u", "x5c"} { |
| 771 | if _, present := header[name]; present { |
| 772 | return nil, "malformed_token" |
| 773 | } |
| 774 | } |
| 775 | if ctNoFloats(claims) != nil { |
| 776 | return nil, "malformed_token" |
| 777 | } |
| 778 | return &ctJWS{header, claims, signature, []byte(parts[0] + "." + parts[1])}, "" |
| 779 | } |
| 780 | |
| 781 | func ctEd25519OK(x string, signature, input []byte) bool { |
| 782 | if len(signature) != ed25519.SignatureSize { |
| 783 | return false |
| 784 | } |
| 785 | raw, err := ctB64Decode(x) |
| 786 | if err != nil || len(raw) != ed25519.PublicKeySize { |
| 787 | return false |
| 788 | } |
| 789 | return ed25519.Verify(ed25519.PublicKey(raw), input, signature) |
| 790 | } |
| 791 | |
| 792 | func ctStr(m map[string]any, key string) (string, bool) { |
| 793 | s, ok := m[key].(string) |
| 794 | return s, ok |
| 795 | } |
| 796 | |
| 797 | func ctNum(m map[string]any, key string) (float64, bool) { |
| 798 | f, ok := m[key].(float64) |
| 799 | return f, ok |
| 800 | } |
| 801 | |
| 802 | // ctClaimsOK validates claims by a schema, ignoring the errors of the named keys (checked earlier under their own error word). |
| 803 | func ctClaimsOK(claims map[string]any, schema string, ignore ...string) bool { |
| 804 | for _, e := range ctSchemaSet.validate(schema, claims) { |
| 805 | skipped := false |
| 806 | for _, name := range ignore { |
| 807 | skipped = skipped || strings.HasPrefix(e, "/"+name+":") |
| 808 | } |
| 809 | if !skipped { |
| 810 | return false |
| 811 | } |
| 812 | } |
| 813 | return true |
| 814 | } |
| 815 | |
| 816 | func ctJWKX(value any) (string, bool) { |
| 817 | jwk, ok := value.(map[string]any) |
| 818 | if !ok { |
| 819 | return "", false |
| 820 | } |
| 821 | x, hasX := jwk["x"].(string) |
| 822 | if _, private := jwk["d"]; private || jwk["kty"] != "OKP" || jwk["crv"] != "Ed25519" || !hasX || len(x) != 43 { |
| 823 | return "", false |
| 824 | } |
| 825 | raw, err := ctB64Decode(x) |
| 826 | if err != nil || len(raw) != 32 { |
| 827 | return "", false |
| 828 | } |
| 829 | return x, true |
| 830 | } |
| 831 | |
| 832 | // ctParseJWKS: kid -> x, or "bad_jwks". |
| 833 | func ctParseJWKS(doc any) (map[string]string, string) { |
| 834 | if len(ctSchemaSet.validate("jwks.schema.json", doc)) > 0 { |
| 835 | return nil, "bad_jwks" |
| 836 | } |
| 837 | keys := map[string]string{} |
| 838 | for _, item := range doc.(map[string]any)["keys"].([]any) { |
| 839 | key := item.(map[string]any) |
| 840 | kid := key["kid"].(string) |
| 841 | if _, dup := keys[kid]; dup { |
| 842 | return nil, "bad_jwks" |
| 843 | } |
| 844 | keys[kid] = key["x"].(string) |
| 845 | } |
| 846 | return keys, "" |
| 847 | } |
| 848 | |
| 849 | type ctNodeKey struct{ node, x string } |
| 850 | |
| 851 | // ctParseNodeKeys: kid -> (node, x), or "bad_node_keys". |
| 852 | func ctParseNodeKeys(doc any) (map[string]ctNodeKey, string) { |
| 853 | if len(ctSchemaSet.validate("node-keys.schema.json", doc)) > 0 { |
| 854 | return nil, "bad_node_keys" |
| 855 | } |
| 856 | keys := map[string]ctNodeKey{} |
| 857 | for _, item := range doc.(map[string]any)["keys"].([]any) { |
| 858 | key := item.(map[string]any) |
| 859 | kid := key["kid"].(string) |
| 860 | if _, dup := keys[kid]; dup { |
| 861 | return nil, "bad_node_keys" |
| 862 | } |
| 863 | keys[kid] = ctNodeKey{key["node"].(string), key["x"].(string)} |
| 864 | } |
| 865 | return keys, "" |
| 866 | } |
| 867 | |
| 868 | // ctSignedByJWKS: the common first half of the session and statement checks (parse, alg, kid, signature, typ). |
| 869 | func ctSignedByJWKS(token string, keys map[string]string, typ string) (*ctJWS, string) { |
| 870 | jws, code := ctJWSParts(token, ctPolInt("protocol.statement_line_max_bytes")) |
| 871 | if code != "" { |
| 872 | return nil, code |
| 873 | } |
| 874 | if jws.header["alg"] != "EdDSA" { |
| 875 | return nil, "bad_alg" |
| 876 | } |
| 877 | kid, isString := ctStr(jws.header, "kid") |
| 878 | if !isString || !ctKidRE.MatchString(kid) { |
| 879 | return nil, "unknown_kid" |
| 880 | } |
| 881 | x, known := keys[kid] |
| 882 | if !known { |
| 883 | return nil, "unknown_kid" |
| 884 | } |
| 885 | if !ctEd25519OK(x, jws.signature, jws.signingInput) { |
| 886 | return nil, "bad_signature" |
| 887 | } |
| 888 | if jws.header["typ"] != typ || jws.claims["typ"] != typ { |
| 889 | return nil, "wrong_type" |
| 890 | } |
| 891 | return jws, "" |
| 892 | } |
| 893 | |
| 894 | // ctVerifySession is verify_session of e2e_contracts.py. |
| 895 | func ctVerifySession(token string, now float64, audience string, keys map[string]string, skew float64) ctResult { |
| 896 | if len(token) > ctPolInt("protocol.jwt_max_bytes") { |
| 897 | return ctFail("malformed_token") |
| 898 | } |
| 899 | jws, code := ctSignedByJWKS(token, keys, "device-session") |
| 900 | if code != "" { |
| 901 | return ctFail(code) |
| 902 | } |
| 903 | claims := jws.claims |
| 904 | if v, ok := ctNum(claims, "ver"); !ok || v != 2 { |
| 905 | return ctFail("wrong_type") |
| 906 | } |
| 907 | if claims["iss"] != ctPolStr("identity.issuer") { |
| 908 | return ctFail("wrong_issuer") |
| 909 | } |
| 910 | if claims["aud"] != audience { |
| 911 | return ctFail("wrong_audience") |
| 912 | } |
| 913 | if !ctClaimsOK(claims, "session-claims.schema.json", "aud") { |
| 914 | return ctFail("bad_claims") |
| 915 | } |
| 916 | iat, _ := ctNum(claims, "iat") |
| 917 | exp, _ := ctNum(claims, "exp") |
| 918 | if exp-iat != float64(ctPolInt("authorization.session_ttl_s")) { |
| 919 | return ctFail("bad_lifetime") |
| 920 | } |
| 921 | if iat > now+skew { |
| 922 | return ctFail("not_yet_valid") |
| 923 | } |
| 924 | if exp <= now { |
| 925 | return ctFail("expired") |
| 926 | } |
| 927 | return ctResult{ok: true, claims: claims, jti: claims["jti"].(string)} |
| 928 | } |
| 929 | |
| 930 | // ctVerifyProof is verify_proof. |
| 931 | func ctVerifyProof(proof string, now float64, sessionToken string, sessionClaims map[string]any, nonce, htu string, skew float64, seen map[string]bool) ctResult { |
| 932 | if len(proof) > ctPolInt("protocol.jwt_max_bytes") { |
| 933 | return ctFail("malformed_token") |
| 934 | } |
| 935 | jws, code := ctJWSParts(proof, ctPolInt("protocol.jwt_max_bytes")) |
| 936 | if code != "" { |
| 937 | return ctFail(code) |
| 938 | } |
| 939 | if jws.header["alg"] != "EdDSA" { |
| 940 | return ctFail("bad_alg") |
| 941 | } |
| 942 | if jws.header["typ"] != "dpop+jwt" { |
| 943 | return ctFail("wrong_type") |
| 944 | } |
| 945 | x, ok := ctJWKX(jws.header["jwk"]) |
| 946 | if !ok { |
| 947 | return ctFail("malformed_token") |
| 948 | } |
| 949 | bound := sessionClaims["cnf"].(map[string]any)["jwk"].(map[string]any)["x"].(string) |
| 950 | if x != bound { |
| 951 | return ctFail("wrong_key") |
| 952 | } |
| 953 | if !ctEd25519OK(x, jws.signature, jws.signingInput) { |
| 954 | return ctFail("bad_signature") |
| 955 | } |
| 956 | claims := jws.claims |
| 957 | switch { |
| 958 | case claims["sub"] != sessionClaims["sub"]: |
| 959 | return ctFail("wrong_subject") |
| 960 | case claims["aud"] != ctPolStr("identity.aud_tunnel"): |
| 961 | return ctFail("wrong_audience") |
| 962 | case claims["htm"] != ctPolStr("protocol.proof_htm"): |
| 963 | return ctFail("wrong_htm") |
| 964 | case claims["htu"] != htu: |
| 965 | return ctFail("wrong_htu") |
| 966 | case claims["ath"] != ctSHA256B64([]byte(sessionToken)): |
| 967 | return ctFail("wrong_ath") |
| 968 | case claims["nonce"] != nonce: |
| 969 | return ctFail("wrong_nonce") |
| 970 | case !ctClaimsOK(claims, "proof-claims.schema.json", "htu"): |
| 971 | return ctFail("bad_claims") |
| 972 | } |
| 973 | iat, _ := ctNum(claims, "iat") |
| 974 | if iat > now+skew { |
| 975 | return ctFail("not_yet_valid") |
| 976 | } |
| 977 | if iat+float64(ctPolInt("authorization.proof_ttl_s")) <= now { |
| 978 | return ctFail("stale_proof") |
| 979 | } |
| 980 | id := claims["jti"].(string) |
| 981 | if seen[id] { |
| 982 | return ctFail("replayed_jti") |
| 983 | } |
| 984 | return ctResult{ok: true, claims: claims, jti: id} |
| 985 | } |
| 986 | |
| 987 | // ctVerifyNodeAssertion is verify_node_assertion. |
| 988 | func ctVerifyNodeAssertion(token string, now float64, keys map[string]ctNodeKey, method, url string, body []byte, skew float64, seen map[string]bool) ctResult { |
| 989 | jws, code := ctJWSParts(token, ctPolInt("protocol.jwt_max_bytes")) |
| 990 | if code != "" { |
| 991 | return ctFail(code) |
| 992 | } |
| 993 | if jws.header["alg"] != "EdDSA" { |
| 994 | return ctFail("bad_alg") |
| 995 | } |
| 996 | kid, isString := ctStr(jws.header, "kid") |
| 997 | if !isString || !ctKidRE.MatchString(kid) { |
| 998 | return ctFail("unknown_kid") |
| 999 | } |
| 1000 | key, known := keys[kid] |
| 1001 | if !known { |
| 1002 | return ctFail("unknown_kid") |
| 1003 | } |
| 1004 | if !ctEd25519OK(key.x, jws.signature, jws.signingInput) { |
| 1005 | return ctFail("bad_signature") |
| 1006 | } |
| 1007 | claims := jws.claims |
| 1008 | if v, ok := ctNum(claims, "ver"); jws.header["typ"] != "node-assertion" || claims["typ"] != "node-assertion" || !ok || v != 1 { |
| 1009 | return ctFail("wrong_type") |
| 1010 | } |
| 1011 | switch { |
| 1012 | case claims["aud"] != ctPolStr("identity.aud_node_assertion"): |
| 1013 | return ctFail("wrong_audience") |
| 1014 | case claims["sub"] != key.node: |
| 1015 | return ctFail("wrong_subject") |
| 1016 | case claims["htm"] != method: |
| 1017 | return ctFail("wrong_htm") |
| 1018 | case claims["htu"] != url: |
| 1019 | return ctFail("wrong_htu") |
| 1020 | case !ctClaimsOK(claims, "node-assertion-claims.schema.json", "htm", "htu", "aud", "sub"): |
| 1021 | return ctFail("bad_claims") |
| 1022 | } |
| 1023 | iat, _ := ctNum(claims, "iat") |
| 1024 | exp, _ := ctNum(claims, "exp") |
| 1025 | switch { |
| 1026 | case exp-iat != float64(ctPolInt("authorization.node_assertion_ttl_s")): |
| 1027 | return ctFail("bad_lifetime") |
| 1028 | case iat > now+skew: |
| 1029 | return ctFail("not_yet_valid") |
| 1030 | case exp <= now: |
| 1031 | return ctFail("expired") |
| 1032 | case claims["bh"] != ctSHA256B64(body): |
| 1033 | return ctFail("wrong_body_hash") |
| 1034 | } |
| 1035 | id := claims["jti"].(string) |
| 1036 | if seen[id] { |
| 1037 | return ctFail("replayed_jti") |
| 1038 | } |
| 1039 | return ctResult{ok: true, claims: claims, jti: id} |
| 1040 | } |
| 1041 | |
| 1042 | // ctVerifyStatement is verify_statement. |
| 1043 | func ctVerifyStatement(token string, now float64, keys map[string]string, deviceID string, lastSeq float64) ctResult { |
| 1044 | jws, code := ctSignedByJWKS(token, keys, "device-access") |
| 1045 | if code != "" { |
| 1046 | return ctFail(code) |
| 1047 | } |
| 1048 | claims := jws.claims |
| 1049 | if v, ok := ctNum(claims, "ver"); !ok || v != 1 { |
| 1050 | return ctFail("wrong_type") |
| 1051 | } |
| 1052 | switch { |
| 1053 | case claims["iss"] != ctPolStr("identity.issuer"): |
| 1054 | return ctFail("wrong_issuer") |
| 1055 | case claims["aud"] != ctPolStr("identity.aud_access"): |
| 1056 | return ctFail("wrong_audience") |
| 1057 | case claims["sub"] != deviceID: |
| 1058 | return ctFail("wrong_subject") |
| 1059 | } |
| 1060 | _, hasPaywall := claims["paywall"] |
| 1061 | _, hasPaused := claims["paused"] |
| 1062 | _, hasResumed := claims["resumed"] |
| 1063 | if !ctClaimsOK(claims, "statement-claims.schema.json", "aud", "sub") || hasPaywall != (claims["mode"] == "paywall") || (hasPaused && hasResumed) { |
| 1064 | return ctFail("bad_claims") |
| 1065 | } |
| 1066 | iat, _ := ctNum(claims, "iat") |
| 1067 | exp, _ := ctNum(claims, "exp") |
| 1068 | ttl := exp - iat |
| 1069 | capName := "authorization.access_ttl_paid_s" |
| 1070 | if claims["mode"] == "paywall" { |
| 1071 | capName = "authorization.access_ttl_paywall_s" |
| 1072 | } |
| 1073 | if ttl <= 0 || ttl > float64(ctPolInt(capName)) { |
| 1074 | return ctFail("bad_lifetime") |
| 1075 | } |
| 1076 | if iat > now+float64(ctPolInt("authorization.access_future_slack_s")) { |
| 1077 | return ctFail("not_yet_valid") |
| 1078 | } |
| 1079 | if seq, _ := ctNum(claims, "seq"); seq < lastSeq { |
| 1080 | return ctFail("stale_seq") |
| 1081 | } |
| 1082 | return ctResult{ok: true, claims: claims, jti: claims["jti"].(string)} |
| 1083 | } |
| 1084 | |
| 1085 | // ---------------------------------------------------------------------------------------------------------------------------- the feed |
| 1086 | |
| 1087 | // ctCheckFeed is check_feed: the schema and the rules a schema cannot say. |
| 1088 | func ctCheckFeed(body any, since float64) []string { |
| 1089 | if errs := ctSchemaSet.validate("feed-response.schema.json", body); len(errs) > 0 { |
| 1090 | return errs |
| 1091 | } |
| 1092 | var errs []string |
| 1093 | previous := since |
| 1094 | doc := body.(map[string]any) |
| 1095 | for i, item := range doc["events"].([]any) { |
| 1096 | event := item.(map[string]any) |
| 1097 | epoch := event["epoch"].(float64) |
| 1098 | if epoch <= previous { |
| 1099 | errs = append(errs, fmt.Sprintf("/events/%d: order", i)) |
| 1100 | } |
| 1101 | previous = math.Max(previous, epoch) |
| 1102 | kind := event["type"].(string) |
| 1103 | global := kind == "kill" || kind == "kill_clear" |
| 1104 | if global != (event["device_id"] == nil && event["label"] == nil) { |
| 1105 | errs = append(errs, fmt.Sprintf("/events/%d: scope", i)) |
| 1106 | } |
| 1107 | if (event["device_id"] == nil) != (event["label"] == nil) { |
| 1108 | errs = append(errs, fmt.Sprintf("/events/%d: scope", i)) |
| 1109 | } |
| 1110 | } |
| 1111 | if doc["epoch"].(float64) < previous { |
| 1112 | errs = append(errs, "/epoch: behind events") |
| 1113 | } |
| 1114 | return errs |
| 1115 | } |
| 1116 | |
| 1117 | // ---------------------------------------------------------------------------------------------------------------------------- surface identity proof |
| 1118 | |
| 1119 | func ctSurfaceKey(secret string) ([]byte, bool) { |
| 1120 | raw, err := ctB64Decode(secret) |
| 1121 | return raw, err == nil && len(raw) == 32 |
| 1122 | } |
| 1123 | |
| 1124 | func ctSurfaceMAC(secret, nonce string) (string, bool) { |
| 1125 | key, ok := ctSurfaceKey(secret) |
| 1126 | if !ok { |
| 1127 | return "", false |
| 1128 | } |
| 1129 | mac := hmac.New(sha256.New, key) |
| 1130 | mac.Write([]byte("all-surface-challenge-v1\n" + nonce)) |
| 1131 | return ctB64Encode(mac.Sum(nil)), true |
| 1132 | } |
| 1133 | |
| 1134 | func ctSurfaceProof(secret, nonce string, pid, port int) (string, bool) { |
| 1135 | key, ok := ctSurfaceKey(secret) |
| 1136 | if !ok { |
| 1137 | return "", false |
| 1138 | } |
| 1139 | mac := hmac.New(sha256.New, key) |
| 1140 | mac.Write([]byte("all-surface-proof-v1\n" + nonce + "\n" + strconv.Itoa(pid) + "\n" + strconv.Itoa(port))) |
| 1141 | return ctB64Encode(mac.Sum(nil)), true |
| 1142 | } |
| 1143 | |
| 1144 | func ctEqualText(a, b string) bool { return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 } |
| 1145 | |
| 1146 | func ctVerifySurfaceChallenge(secret, nonce, mac string) ctResult { |
| 1147 | want, ok := ctSurfaceMAC(secret, nonce) |
| 1148 | if !ok || !ctNonceRE.MatchString(nonce) || !ctEqualText(want, mac) { |
| 1149 | return ctFail("bad_challenge") |
| 1150 | } |
| 1151 | return ctResult{ok: true} |
| 1152 | } |
| 1153 | |
| 1154 | func ctVerifySurfaceProof(secret, nonce string, pid, port int, proof string) ctResult { |
| 1155 | want, ok := ctSurfaceProof(secret, nonce, pid, port) |
| 1156 | if !ok || !ctNonceRE.MatchString(nonce) || !ctEqualText(want, proof) { |
| 1157 | return ctFail("bad_proof") |
| 1158 | } |
| 1159 | return ctResult{ok: true} |
| 1160 | } |
| 1161 | |
| 1162 | func ctIdentityRequest(host string, port int, nonce, mac string) string { |
| 1163 | return fmt.Sprintf("GET %s HTTP/1.1\r\nHost: %s:%d\r\n%s: %s\r\n%s: %s\r\nConnection: keep-alive\r\n\r\n", ctPolStr("surface.identity_path"), host, port, |
| 1164 | ctPolStr("surface.identity_challenge_header"), nonce, ctPolStr("surface.identity_mac_header"), mac) |
| 1165 | } |
| 1166 | |
| 1167 | // ---------------------------------------------------------------------------------------------------------------------------- node behaviour |
| 1168 | |
| 1169 | type ctConn struct { |
| 1170 | phase, session, kind, nonce, proof, host, status, sub string |
| 1171 | claims map[string]any |
| 1172 | sessionOK, authorized, superseded bool |
| 1173 | reply map[string]any |
| 1174 | validUntil float64 |
| 1175 | } |
| 1176 | |
| 1177 | // ctHello is a complete ClientHello waiting for its OPEN. |
| 1178 | type ctHello struct{ ip, class string } |
| 1179 | |
| 1180 | type ctStream struct { |
| 1181 | secret string |
| 1182 | t float64 |
| 1183 | consumed bool |
| 1184 | } |
| 1185 | |
| 1186 | func ctText(m map[string]any, key string) string { |
| 1187 | s, _ := m[key].(string) |
| 1188 | return s |
| 1189 | } |
| 1190 | |
| 1191 | // ctCheckTranscript is check_transcript: does the recorded behaviour of one node obey the contract? "" is ok, otherwise the violation word. |
| 1192 | func ctCheckTranscript(doc map[string]any) string { |
| 1193 | ctx := doc["ctx"].(map[string]any) |
| 1194 | keys, code := ctParseJWKS(ctx["jwks"]) |
| 1195 | if code != "" { |
| 1196 | return code |
| 1197 | } |
| 1198 | skew := ctx["skew"].(float64) |
| 1199 | conns := map[string]*ctConn{} |
| 1200 | var order []string |
| 1201 | seen := map[string]bool{} |
| 1202 | streams := map[string]*ctStream{} |
| 1203 | secretsUsed := map[string]bool{} |
| 1204 | hellos := map[string][]ctHello{} // per host: the peer_ip and class of each complete ClientHello still waiting for its OPEN |
| 1205 | ended := false |
| 1206 | var now float64 |
| 1207 | |
| 1208 | for _, item := range doc["events"].([]any) { |
| 1209 | event := item.(map[string]any) |
| 1210 | now = event["t"].(float64) |
| 1211 | kind := ctText(event, "ev") |
| 1212 | var conn *ctConn |
| 1213 | if name, ok := event["conn"].(string); ok && (kind == "recv" || kind == "send" || kind == "cp" || kind == "cp_reply") { |
| 1214 | if conns[name] == nil { |
| 1215 | conns[name] = &ctConn{phase: "new", kind: "hello", proof: "none"} |
| 1216 | order = append(order, name) |
| 1217 | } |
| 1218 | conn = conns[name] |
| 1219 | } |
| 1220 | switch kind { |
| 1221 | case "client_hello": |
| 1222 | if event["complete"] == true { |
| 1223 | hellos[ctText(event, "sni")] = append(hellos[ctText(event, "sni")], ctHello{ctText(event, "peer_ip"), ctText(event, "class")}) |
| 1224 | } |
| 1225 | case "recv": |
| 1226 | message, err := ctParseLine(ctText(event, "line"), "d2n") |
| 1227 | if err != nil { |
| 1228 | continue |
| 1229 | } |
| 1230 | verb := message["verb"].(string) |
| 1231 | if (verb == "HELLO" && conn.phase == "new") || (verb == "RENEW" && conn.phase == "ready") { |
| 1232 | token := message["session"].(string) |
| 1233 | result := ctVerifySession(token, now, ctPolStr("identity.aud_tunnel"), keys, skew) |
| 1234 | conn.sessionOK = result.ok && (verb == "HELLO" || result.claims["sub"] == conn.sub) |
| 1235 | conn.session = token |
| 1236 | conn.claims = nil |
| 1237 | if result.ok { |
| 1238 | conn.claims = result.claims |
| 1239 | } |
| 1240 | conn.kind = map[bool]string{true: "hello", false: "renew"}[verb == "HELLO"] |
| 1241 | conn.nonce = "" |
| 1242 | conn.proof, conn.authorized, conn.reply = "none", false, nil |
| 1243 | if verb == "HELLO" { |
| 1244 | conn.phase = "hello" |
| 1245 | } |
| 1246 | } else if verb == "PROOF" && conn.sessionOK && conn.nonce != "" { |
| 1247 | result := ctVerifyProof(message["proof"].(string), now, conn.session, conn.claims, conn.nonce, ctx["node_url"].(string), skew, seen) |
| 1248 | switch { |
| 1249 | case result.ok: |
| 1250 | conn.proof = "ok" |
| 1251 | case result.err == "replayed_jti": |
| 1252 | conn.proof = "replayed" |
| 1253 | default: |
| 1254 | conn.proof = "invalid" |
| 1255 | } |
| 1256 | conn.authorized, conn.reply = false, nil |
| 1257 | if result.ok { |
| 1258 | seen[result.jti] = true |
| 1259 | } |
| 1260 | } |
| 1261 | case "cp": |
| 1262 | body := event["body"].(map[string]any) |
| 1263 | if len(ctSchemaSet.validate("authorize-request.schema.json", body)) > 0 || conn.claims == nil || body["device_id"] != conn.claims["sub"] || |
| 1264 | body["session_jti"] != conn.claims["jti"] || body["reason"] != conn.kind || body["node"] != ctx["node_id"] { |
| 1265 | return "bad_authorize_request" |
| 1266 | } |
| 1267 | if conn.proof == "ok" { |
| 1268 | conn.authorized = true |
| 1269 | } |
| 1270 | case "cp_reply": |
| 1271 | conn.reply = nil |
| 1272 | if conn.authorized { |
| 1273 | conn.reply = event |
| 1274 | } |
| 1275 | case "send": |
| 1276 | message, err := ctParseLine(ctText(event, "line"), "n2d") |
| 1277 | if err != nil { |
| 1278 | return "bad_line" |
| 1279 | } |
| 1280 | switch message["verb"].(string) { |
| 1281 | case "CHALLENGE": |
| 1282 | if !conn.sessionOK || (conn.phase != "hello" && conn.phase != "ready") { |
| 1283 | return "challenge_for_invalid_session" |
| 1284 | } |
| 1285 | conn.nonce = message["nonce"].(string) |
| 1286 | case "OK": |
| 1287 | if ctText(event, "line") != "OK "+ctCanonicalJSON(message["body"]) { |
| 1288 | return "bad_line" |
| 1289 | } |
| 1290 | switch conn.proof { |
| 1291 | case "none": |
| 1292 | return "ok_without_proof" |
| 1293 | case "replayed": |
| 1294 | return "replayed_proof_accepted" |
| 1295 | case "invalid": |
| 1296 | return "ok_with_invalid_proof" |
| 1297 | } |
| 1298 | if !conn.authorized || conn.reply == nil || conn.reply["status"].(float64) != 200 { |
| 1299 | if conn.kind == "renew" { |
| 1300 | return "renew_without_authorize" |
| 1301 | } |
| 1302 | return "ok_without_authorize" |
| 1303 | } |
| 1304 | body, answer := message["body"].(map[string]any), conn.reply["body"].(map[string]any) |
| 1305 | if body["host"] != answer["host"] { |
| 1306 | return "host_not_from_control_plane" |
| 1307 | } |
| 1308 | if body["status"] != answer["status"] || body["valid_until"] != answer["valid_until"] || !ctEqual(body["policy"], answer["policy"]) || body["node"] != ctx["node_id"] { |
| 1309 | return "ok_mismatch_control_plane" |
| 1310 | } |
| 1311 | conn.phase, conn.host, conn.status = "ready", body["host"].(string), body["status"].(string) |
| 1312 | conn.validUntil, conn.sub = body["valid_until"].(float64), conn.claims["sub"].(string) |
| 1313 | conn.proof, conn.authorized, conn.reply = "none", false, nil |
| 1314 | for _, name := range order { |
| 1315 | if other := conns[name]; other != conn && other.phase == "ready" && other.sub == conn.sub { |
| 1316 | other.superseded = true |
| 1317 | } |
| 1318 | } |
| 1319 | case "OPEN": |
| 1320 | switch { |
| 1321 | case conn.phase != "ready" || conn.status == "": |
| 1322 | return "open_before_ok" |
| 1323 | case conn.status == "paused": |
| 1324 | return "open_when_paused" |
| 1325 | case now >= conn.validUntil: |
| 1326 | return "open_after_valid_until" |
| 1327 | case len(hellos[conn.host]) == 0: |
| 1328 | for _, list := range hellos { |
| 1329 | if len(list) > 0 { |
| 1330 | return "open_wrong_host" |
| 1331 | } |
| 1332 | } |
| 1333 | return "open_without_client_hello" |
| 1334 | } |
| 1335 | waiting := -1 |
| 1336 | for k, hello := range hellos[conn.host] { |
| 1337 | if hello.ip == message["peer_ip"] && hello.class == message["class"] { |
| 1338 | waiting = k |
| 1339 | break |
| 1340 | } |
| 1341 | } |
| 1342 | if waiting < 0 { |
| 1343 | return "open_wrong_client" |
| 1344 | } |
| 1345 | id, secret := message["stream_id"].(string), message["secret"].(string) |
| 1346 | if streams[id] != nil || secretsUsed[secret] { |
| 1347 | return "duplicate_stream" |
| 1348 | } |
| 1349 | hellos[conn.host] = append(hellos[conn.host][:waiting], hellos[conn.host][waiting+1:]...) |
| 1350 | streams[id] = &ctStream{secret: secret, t: now} |
| 1351 | secretsUsed[secret] = true |
| 1352 | case "CLOSE": |
| 1353 | conn.phase = "closed" |
| 1354 | } |
| 1355 | case "attach": |
| 1356 | message, err := ctParseLine(ctText(event, "line"), "d2n") |
| 1357 | if err != nil { |
| 1358 | continue |
| 1359 | } |
| 1360 | known := streams[message["stream_id"].(string)] |
| 1361 | good := known != nil && ctEqualText(known.secret, message["secret"].(string)) |
| 1362 | fresh := good && !known.consumed && now-known.t <= float64(ctPolInt("protocol.attach_timeout_s")) |
| 1363 | if event["result"] == "ok" { |
| 1364 | switch { |
| 1365 | case known == nil: |
| 1366 | return "attach_unknown_accepted" |
| 1367 | case !good: |
| 1368 | return "attach_wrong_secret_accepted" |
| 1369 | case known.consumed: |
| 1370 | return "attach_secret_reused" |
| 1371 | case !fresh: |
| 1372 | return "attach_after_timeout" |
| 1373 | } |
| 1374 | known.consumed = true |
| 1375 | } else if fresh { |
| 1376 | return "attach_refused_good_secret" |
| 1377 | } |
| 1378 | case "end": |
| 1379 | ended = true |
| 1380 | for _, name := range order { |
| 1381 | other := conns[name] |
| 1382 | if other.phase == "ready" && other.superseded { |
| 1383 | return "replaced_not_closed" |
| 1384 | } |
| 1385 | if other.phase == "ready" && now > other.validUntil { |
| 1386 | return "no_close_at_valid_until" |
| 1387 | } |
| 1388 | } |
| 1389 | } |
| 1390 | } |
| 1391 | if !ended { |
| 1392 | return "transcript_without_end" |
| 1393 | } |
| 1394 | return "" |
| 1395 | } |
| 1396 | |
| 1397 | // ---------------------------------------------------------------------------------------------------------------------------- an active node (the Go twin of contracts/fake_node.py) |
| 1398 | |
| 1399 | type ctSimConn struct { |
| 1400 | phase, session, kind, nonce, host, status, sub string |
| 1401 | claims map[string]any |
| 1402 | waiting bool |
| 1403 | validUntil float64 |
| 1404 | } |
| 1405 | |
| 1406 | type ctNodeSim struct { |
| 1407 | nodeID, nodeURL string |
| 1408 | keys map[string]string |
| 1409 | skew float64 |
| 1410 | nonces, streamIDs, secrets []string |
| 1411 | conns map[string]*ctSimConn |
| 1412 | order []string |
| 1413 | seen map[string]bool |
| 1414 | streams map[string]*ctStream |
| 1415 | } |
| 1416 | |
| 1417 | func ctNewNodeSim(ctx map[string]any) *ctNodeSim { |
| 1418 | keys, code := ctParseJWKS(ctx["jwks"]) |
| 1419 | if code != "" { |
| 1420 | panic(code) |
| 1421 | } |
| 1422 | randoms := ctx["randoms"].(map[string]any) |
| 1423 | list := func(name string) []string { |
| 1424 | var out []string |
| 1425 | for _, item := range randoms[name].([]any) { |
| 1426 | out = append(out, item.(string)) |
| 1427 | } |
| 1428 | return out |
| 1429 | } |
| 1430 | return &ctNodeSim{nodeID: ctx["node_id"].(string), nodeURL: ctx["node_url"].(string), keys: keys, skew: ctx["skew"].(float64), nonces: list("nonces"), |
| 1431 | streamIDs: list("stream_ids"), secrets: list("secrets"), conns: map[string]*ctSimConn{}, seen: map[string]bool{}, streams: map[string]*ctStream{}} |
| 1432 | } |
| 1433 | |
| 1434 | func ctSend(t float64, conn, line string) map[string]any { |
| 1435 | return map[string]any{"t": t, "conn": conn, "ev": "send", "line": line} |
| 1436 | } |
| 1437 | |
| 1438 | func (n *ctNodeSim) conn(name string) *ctSimConn { |
| 1439 | if n.conns[name] == nil { |
| 1440 | n.conns[name] = &ctSimConn{phase: "new", kind: "hello"} |
| 1441 | n.order = append(n.order, name) |
| 1442 | } |
| 1443 | return n.conns[name] |
| 1444 | } |
| 1445 | |
| 1446 | func ctErrLine(code string) string { |
| 1447 | line, err := ctFormatLine(map[string]any{"verb": "ERR", "code": code}) |
| 1448 | if err != nil { |
| 1449 | panic(err) |
| 1450 | } |
| 1451 | return line |
| 1452 | } |
| 1453 | |
| 1454 | func ctErrFor(code, fallback string) string { |
| 1455 | switch code { |
| 1456 | case "malformed_token": |
| 1457 | return "bad_hello" |
| 1458 | case "not_yet_valid", "stale_proof": |
| 1459 | return "clock_skew" |
| 1460 | case "wrong_audience", "expired": |
| 1461 | return "auth" |
| 1462 | } |
| 1463 | return fallback |
| 1464 | } |
| 1465 | |
| 1466 | func (n *ctNodeSim) handle(event map[string]any) []map[string]any { |
| 1467 | t := event["t"].(float64) |
| 1468 | switch event["ev"] { |
| 1469 | case "recv": |
| 1470 | return n.recv(t, event["conn"].(string), event["line"].(string)) |
| 1471 | case "cp_reply": |
| 1472 | return n.cpReply(t, event["conn"].(string), event["status"].(float64), event["body"]) |
| 1473 | case "client_hello": |
| 1474 | return n.clientHello(t, event) |
| 1475 | case "attach": |
| 1476 | return n.attach(t, event) |
| 1477 | case "tick": |
| 1478 | var out []map[string]any |
| 1479 | for _, name := range n.order { |
| 1480 | if c := n.conns[name]; c.phase == "ready" && t >= c.validUntil { |
| 1481 | c.phase = "closed" |
| 1482 | out = append(out, ctSend(t, name, "CLOSE expired")) |
| 1483 | } |
| 1484 | } |
| 1485 | return out |
| 1486 | } |
| 1487 | return nil |
| 1488 | } |
| 1489 | |
| 1490 | func (n *ctNodeSim) recv(t float64, name, line string) []map[string]any { |
| 1491 | c := n.conn(name) |
| 1492 | message, err := ctParseLine(line, "d2n") |
| 1493 | if err != nil { |
| 1494 | return []map[string]any{ctSend(t, name, ctErrLine(err.(*ctProtoErr).code))} |
| 1495 | } |
| 1496 | verb := message["verb"].(string) |
| 1497 | switch verb { |
| 1498 | case "HELLO", "RENEW": |
| 1499 | if (verb == "HELLO" && c.phase != "new") || (verb == "RENEW" && c.phase != "ready") { // RENEW only on a live connection |
| 1500 | return []map[string]any{ctSend(t, name, ctErrLine("bad_command"))} |
| 1501 | } |
| 1502 | token := message["session"].(string) |
| 1503 | result := ctVerifySession(token, t, ctPolStr("identity.aud_tunnel"), n.keys, n.skew) |
| 1504 | if !result.ok { |
| 1505 | return []map[string]any{ctSend(t, name, ctErrLine(ctErrFor(result.err, "auth")))} |
| 1506 | } |
| 1507 | if verb == "RENEW" && result.claims["sub"] != c.sub { |
| 1508 | return []map[string]any{ctSend(t, name, ctErrLine("auth"))} |
| 1509 | } |
| 1510 | c.session, c.claims, c.kind, c.waiting = token, result.claims, "hello", false |
| 1511 | if verb == "RENEW" { |
| 1512 | c.kind = "renew" |
| 1513 | } else { |
| 1514 | c.phase = "hello" |
| 1515 | } |
| 1516 | c.nonce, n.nonces = n.nonces[0], n.nonces[1:] |
| 1517 | return []map[string]any{ctSend(t, name, "CHALLENGE "+c.nonce)} |
| 1518 | case "PROOF": |
| 1519 | if c.nonce == "" { |
| 1520 | return []map[string]any{ctSend(t, name, ctErrLine("bad_command"))} |
| 1521 | } |
| 1522 | result := ctVerifyProof(message["proof"].(string), t, c.session, c.claims, c.nonce, n.nodeURL, n.skew, n.seen) |
| 1523 | c.nonce = "" |
| 1524 | if !result.ok { |
| 1525 | word := "bad_proof" |
| 1526 | if result.err != "replayed_jti" { |
| 1527 | word = ctErrFor(result.err, "bad_proof") |
| 1528 | } |
| 1529 | return []map[string]any{ctSend(t, name, ctErrLine(word))} |
| 1530 | } |
| 1531 | n.seen[result.jti] = true |
| 1532 | c.waiting = true |
| 1533 | body := map[string]any{"v": 1.0, "device_id": c.claims["sub"], "session_jti": c.claims["jti"], "reason": c.kind, "node": n.nodeID} |
| 1534 | return []map[string]any{{"t": t, "conn": name, "ev": "cp", "call": "authorize", "body": body}} |
| 1535 | case "PING": |
| 1536 | return []map[string]any{ctSend(t, name, "PONG")} |
| 1537 | case "HINT": |
| 1538 | return nil |
| 1539 | case "CLOSE": |
| 1540 | c.phase = "closed" |
| 1541 | return nil |
| 1542 | } |
| 1543 | return []map[string]any{ctSend(t, name, ctErrLine("bad_command"))} |
| 1544 | } |
| 1545 | |
| 1546 | func (n *ctNodeSim) cpReply(t float64, name string, status float64, body any) []map[string]any { |
| 1547 | c := n.conn(name) |
| 1548 | if !c.waiting { |
| 1549 | return nil |
| 1550 | } |
| 1551 | c.waiting = false |
| 1552 | if status != 200 { |
| 1553 | word := "auth" |
| 1554 | if status >= 500 || status == 429 { |
| 1555 | word = "control_unavailable" |
| 1556 | } |
| 1557 | if m, ok := body.(map[string]any); ok { |
| 1558 | switch m["error"] { |
| 1559 | case "license_inactive", "device_revoked", "paused": |
| 1560 | word = m["error"].(string) |
| 1561 | } |
| 1562 | } |
| 1563 | return []map[string]any{ctSend(t, name, ctErrLine(word))} |
| 1564 | } |
| 1565 | answer, _ := body.(map[string]any) |
| 1566 | if len(ctSchemaSet.validate("authorize-response.schema.json", body)) > 0 || answer["device_id"] != c.claims["sub"] { |
| 1567 | return []map[string]any{ctSend(t, name, ctErrLine("control_unavailable"))} |
| 1568 | } |
| 1569 | okBody := map[string]any{"v": 2.0, "node": n.nodeID, "host": answer["host"], "status": answer["status"], "valid_until": answer["valid_until"], |
| 1570 | "server_time": answer["server_time"], "policy": answer["policy"]} |
| 1571 | line, err := ctFormatLine(map[string]any{"verb": "OK", "body": okBody}) |
| 1572 | if err != nil { |
| 1573 | panic(err) |
| 1574 | } |
| 1575 | out := []map[string]any{ctSend(t, name, line)} |
| 1576 | c.phase, c.host, c.status, c.validUntil, c.sub = "ready", answer["host"].(string), answer["status"].(string), answer["valid_until"].(float64), c.claims["sub"].(string) |
| 1577 | for _, other := range n.order { |
| 1578 | if o := n.conns[other]; other != name && o.phase == "ready" && o.sub == c.sub { |
| 1579 | o.phase = "closed" |
| 1580 | out = append(out, ctSend(t, other, "CLOSE replaced")) |
| 1581 | } |
| 1582 | } |
| 1583 | return out |
| 1584 | } |
| 1585 | |
| 1586 | func (n *ctNodeSim) clientHello(t float64, event map[string]any) []map[string]any { |
| 1587 | sni := event["sni"].(string) |
| 1588 | alert := []map[string]any{{"t": t, "ev": "tls_alert", "sni": sni}} |
| 1589 | if event["complete"] != true { |
| 1590 | return alert |
| 1591 | } |
| 1592 | for _, name := range n.order { |
| 1593 | c := n.conns[name] |
| 1594 | if c.phase == "ready" && c.host == sni && c.status != "paused" && t < c.validUntil { |
| 1595 | id, secret := n.streamIDs[0], n.secrets[0] |
| 1596 | n.streamIDs, n.secrets = n.streamIDs[1:], n.secrets[1:] |
| 1597 | n.streams[id] = &ctStream{secret: secret, t: t} |
| 1598 | line, err := ctFormatLine(map[string]any{"verb": "OPEN", "stream_id": id, "secret": secret, "peer_ip": event["peer_ip"], "class": event["class"]}) |
| 1599 | if err != nil { |
| 1600 | panic(err) |
| 1601 | } |
| 1602 | return []map[string]any{ctSend(t, name, line)} |
| 1603 | } |
| 1604 | } |
| 1605 | return alert |
| 1606 | } |
| 1607 | |
| 1608 | func (n *ctNodeSim) attach(t float64, event map[string]any) []map[string]any { |
| 1609 | out := map[string]any{"t": t, "conn": event["conn"], "ev": "attach", "line": event["line"], "result": "err"} |
| 1610 | message, err := ctParseLine(event["line"].(string), "d2n") |
| 1611 | if err != nil { |
| 1612 | return []map[string]any{out} |
| 1613 | } |
| 1614 | known := n.streams[message["stream_id"].(string)] |
| 1615 | if known != nil && known.secret == message["secret"].(string) && !known.consumed && t-known.t <= float64(ctPolInt("protocol.attach_timeout_s")) { |
| 1616 | known.consumed = true |
| 1617 | out["result"] = "ok" |
| 1618 | } |
| 1619 | return []map[string]any{out} |
| 1620 | } |
| 1621 | |
| 1622 | // ctSplitTranscript is split_transcript: (inputs, outputs) of a golden transcript. |
| 1623 | func ctSplitTranscript(events []any) (inputs, outputs []map[string]any) { |
| 1624 | for _, item := range events { |
| 1625 | event := item.(map[string]any) |
| 1626 | switch event["ev"] { |
| 1627 | case "recv", "cp_reply", "client_hello", "tick", "end": |
| 1628 | inputs = append(inputs, event) |
| 1629 | case "attach": |
| 1630 | stripped := map[string]any{} |
| 1631 | for key, value := range event { |
| 1632 | if key != "result" { |
| 1633 | stripped[key] = value |
| 1634 | } |
| 1635 | } |
| 1636 | inputs = append(inputs, stripped) |
| 1637 | outputs = append(outputs, event) |
| 1638 | case "send", "cp", "tls_alert": |
| 1639 | outputs = append(outputs, event) |
| 1640 | } |
| 1641 | } |
| 1642 | return inputs, outputs |
| 1643 | } |