ALL

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

SHA-256: 85de26d2e6e520850a4a1bc4343dbae43e89bdbac289c6a20b09997e2feaff35
1643 líneas · 48650 bytes · Ver en GitHub

1package 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
8import (
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
29const 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.
35func 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
51func 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).
110func 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
132func 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.
167func 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
198func 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
238func 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
248var 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.
257func 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
265func ctPolInt(dotted string) int { return int(ctPol(dotted).(float64)) }
266func ctPolStr(dotted string) string { return ctPol(dotted).(string) }
267
268func 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).
277func 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
284var (
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
292func ctLabelOK(value string) bool { return ctLabelRE.MatchString(value) }
293
294func 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.
311func 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
316func 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
326type ctSchemas struct {
327 docs map[string]any
328 patterns map[string]*regexp.Regexp
329}
330
331func 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
347func (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
353func (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
369func (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
378func 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
404func (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
522var ctSchemaSet = ctLoadSchemas()
523
524// ---------------------------------------------------------------------------------------------------------------------------- the wire grammar
525
526type ctProtoErr struct{ code string }
527
528func (e *ctProtoErr) Error() string { return e.code }
529
530var 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
538func 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
561func 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.
573func 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).
661func 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
695type ctResult struct {
696 ok bool
697 err string
698 claims map[string]any
699 jti string
700}
701
702func ctFail(code string) ctResult { return ctResult{err: code} }
703
704var ctB64Charset = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
705
706func 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
717func ctB64Encode(raw []byte) string { return base64.RawURLEncoding.EncodeToString(raw) }
718
719func ctSHA256B64(data []byte) string {
720 sum := sha256.Sum256(data)
721 return ctB64Encode(sum[:])
722}
723
724type 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).
732func 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
781func 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
792func ctStr(m map[string]any, key string) (string, bool) {
793 s, ok := m[key].(string)
794 return s, ok
795}
796
797func 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).
803func 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
816func 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".
833func 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
849type ctNodeKey struct{ node, x string }
850
851// ctParseNodeKeys: kid -> (node, x), or "bad_node_keys".
852func 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).
869func 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.
895func 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.
931func 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.
988func 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.
1043func 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.
1088func 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
1119func ctSurfaceKey(secret string) ([]byte, bool) {
1120 raw, err := ctB64Decode(secret)
1121 return raw, err == nil && len(raw) == 32
1122}
1123
1124func 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
1134func 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
1144func ctEqualText(a, b string) bool { return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 }
1145
1146func 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
1154func 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
1162func 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
1169type 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.
1178type ctHello struct{ ip, class string }
1179
1180type ctStream struct {
1181 secret string
1182 t float64
1183 consumed bool
1184}
1185
1186func 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.
1192func 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
1399type ctSimConn struct {
1400 phase, session, kind, nonce, host, status, sub string
1401 claims map[string]any
1402 waiting bool
1403 validUntil float64
1404}
1405
1406type 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
1417func 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
1434func ctSend(t float64, conn, line string) map[string]any {
1435 return map[string]any{"t": t, "conn": conn, "ev": "send", "line": line}
1436}
1437
1438func (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
1446func 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
1454func 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
1466func (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
1490func (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
1546func (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
1586func (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
1608func (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.
1623func 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}