package delivery import ( "reflect" "regexp" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" ) // secretNamed is facts.secretKey's pattern: a field so named would read as a secret, and the runtime's credential-field // check (ADR 0201 ยง13) refuses such a field in a state every node reads. var secretNamed = regexp.MustCompile(`(?i)(secret|passw|token|api[-_]?key|private[-_]?key|credential|bearer|cookie|salt|signing)`) // Issue 1126 (ADR 0555): a node's record reads back, rides in its have key, refuses a node that is not a name and an empty capability, and // neither it nor the placement file names a field like a secret. func TestNodeRecord(t *testing.T) { hub := NodeRecord{Node: "novox", TunnelKey: "k", Capabilities: []string{"docker"}, SealingKey: "age1x", ServingKey: "pub", Certificate: "cert", OutwardLinks: []string{"eth0"}, Networks: []catalogue.AttachedNetwork{{Link: "eth0", Network: "192.0.2.0/24"}}, NetworksSaid: true, Contract: 3, Trusts: []string{"controller key sha256:aa", "vault key sha256:bb"}} raw, err := EncodeNodeRecord(hub) if err != nil { t.Fatal(err) } back, err := DecodeNodeRecord(raw) if err != nil || !SameNodeRecord(back, hub) || back.V != Version { t.Fatalf("round trip: %+v, %v", back, err) } // It rides in the node's own have key: a have that differs only in its record is another value. with, without := HaveNode{V: Version, Record: &hub}, HaveNode{V: Version} if raw, err := EncodeHaveNode(with); err != nil { t.Fatal(err) } else if got, err := DecodeHaveNode(raw); err != nil || got.Record == nil || !SameHaveNode(got, with) || SameHaveNode(got, without) { t.Fatalf("the have's record: %+v, %v", got, err) } changed := hub changed.Contract = 4 if SameHaveNode(with, HaveNode{V: Version, Record: &changed}) { t.Error("a changed record reads as the same have") } for name, r := range map[string]NodeRecord{ "empty capability": {Node: "ace", Capabilities: []string{""}}, "node that is not a name": {Node: "a.b"}, } { if _, err := EncodeNodeRecord(r); err == nil { t.Errorf("%s: taken", name) } } // Every JSON field name of the record and the types it carries, walked: none reads as a secret. var walk func(reflect.Type, string) walk = func(ty reflect.Type, at string) { for ty.Kind() == reflect.Pointer || ty.Kind() == reflect.Slice { ty = ty.Elem() } if ty.Kind() != reflect.Struct { return } for i := 0; i < ty.NumField(); i++ { f := ty.Field(i) name, _, _ := strings.Cut(f.Tag.Get("json"), ",") if secretNamed.MatchString(name) || secretNamed.MatchString(f.Name) { t.Errorf("%s.%s (%q) is named like a secret", at, f.Name, name) } walk(f.Type, at+"."+f.Name) } } walk(reflect.TypeOf(NodeRecord{}), "NodeRecord") // And the placement file, which holds what a person decides of a node (K2): accounts by name, never a credential. walk(reflect.TypeOf(Placement{}), "Placement") }