package capsule_test import ( "errors" "maps" "slices" "testing" datekeys "g.activething.com/go/DateKeys" "g.activething.com/go/DateKeys/capsule" "g.activething.com/go/DateKeys/datekey" "g.activething.com/go/DateKeys/internal/cbortest" "g.activething.com/go/DateKeys/profile" ) // swapped encodes m with the entries of keys a and b in each other's place. func swapped(t *testing.T, m map[uint64]any, a, b uint64) []byte { t.Helper() var p cbortest.Pairs for _, k := range slices.Sorted(maps.Keys(m)) { switch k { case a: k = b case b: k = a } p = append(p, k, m[k]) } out, err := cbortest.Marshal(p) if err != nil { t.Fatal(err) } return out } func with(m map[uint64]any, edit func(m map[uint64]any)) map[uint64]any { c := maps.Clone(m) edit(c) return c } // Spec §58: the maps of PUBLIC_HEADER and CONTROL_CBOR are closed, their keys // strictly ascending unsigned integers, and every required key is present. func TestDecodeMapStructure(t *testing.T) { dk := datekey.DateKey{ProfileID: profile.QuicknetID, Round: 1000}.Compact() h := map[uint64]any{0: capsule.HeaderTypeTag, 1: uint64(1), 2: make([]byte, 16), 3: dk, 4: uint64(0)} c := map[uint64]any{0: capsule.ControlTypeTag, 1: uint64(1), 2: make([]byte, 32), 3: make([]byte, 32)} exts := []any{ext("a", 1)} for name, in := range map[string][]byte{ "missing capsule_id": marshal(t, with(h, func(m map[uint64]any) { delete(m, 2) })), "missing access_policy": marshal(t, with(h, func(m map[uint64]any) { delete(m, 4) })), "capsule_id as text": marshal(t, with(h, func(m map[uint64]any) { m[2] = "x" })), "access_policy 2^53": marshal(t, with(h, func(m map[uint64]any) { m[4] = uint64(1) << 53 })), "unknown key 7": marshal(t, with(h, func(m map[uint64]any) { m[7] = uint64(0) })), "eight entries": marshal(t, with(h, func(m map[uint64]any) { m[5], m[6], m[7] = exts, []any{ext("b", 1)}, uint64(0) })), "keys 3 and 4 swapped": swapped(t, h, 3, 4), "text key": mustMarshal(t, cbortest.Pairs{uint64(0), capsule.HeaderTypeTag, uint64(1), uint64(1), "2", make([]byte, 16)}), } { if _, err := capsule.DecodeHeader(in); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) { t.Errorf("PUBLIC_HEADER %s: %v", name, err) } } // access_policy is 0 or 1 (spec §25): a value whose low byte is 0 or 1 // is not a V1 policy. for _, p := range []uint64{2, 255, 256, 257, 512, 65536, 65537, 1 << 32, 1<<32 + 1, 1<<53 - 256, 1<<53 - 255, 1<<53 - 1} { if _, err := capsule.DecodeHeader(marshal(t, with(h, func(m map[uint64]any) { m[4] = p }))); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) { t.Errorf("PUBLIC_HEADER access_policy %d: %v", p, err) } } for name, in := range map[string][]byte{ "missing payload_identity": marshal(t, with(c, func(m map[uint64]any) { delete(m, 3) })), "seven entries": marshal(t, with(c, func(m map[uint64]any) { m[4], m[5], m[6] = exts, []any{ext("b", 1)}, uint64(0) })), "keys 2 and 3 swapped": swapped(t, c, 2, 3), "text key": mustMarshal(t, cbortest.Pairs{uint64(0), capsule.ControlTypeTag, uint64(1), uint64(1), "2", make([]byte, 32)}), } { if _, err := capsule.DecodeControl(in, capsule.Format1); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) { t.Errorf("CONTROL_CBOR %s: %v", name, err) } } } func mustMarshal(t *testing.T, v any) []byte { t.Helper() b, err := cbortest.Marshal(v) if err != nil { t.Fatal(err) } return b } // Spec §70: the schema version is reported as such whatever follows it, and // a version that is missing or outside the profile is not a version. func TestDecodeSchemaVersion(t *testing.T) { for name, tc := range map[string]struct { in cbortest.Pairs want error }{ "version 2, rest unreadable": {cbortest.Pairs{uint64(0), capsule.HeaderTypeTag, uint64(1), uint64(2), uint64(2), cbortest.Raw{0xff}}, datekeys.ErrUnsupportedVersion}, "version missing": {cbortest.Pairs{uint64(0), capsule.HeaderTypeTag, uint64(2), make([]byte, 16)}, datekeys.ErrNonCanonicalCBOR}, "version null": {cbortest.Pairs{uint64(0), capsule.HeaderTypeTag, uint64(1), nil}, datekeys.ErrNonCanonicalCBOR}, "version before type": {cbortest.Pairs{uint64(1), uint64(1), uint64(0), capsule.HeaderTypeTag}, datekeys.ErrNonCanonicalCBOR}, } { if _, err := capsule.DecodeHeader(mustMarshal(t, tc.in)); !errors.Is(err, tc.want) { t.Errorf("%s: got %v, want %v", name, err, tc.want) } } } // A PUBLIC_HEADER that breaks the CDDL and holds an invalid DateKey reports // the CDDL violation: the DateKey is parsed after the map (spec §57, §63 // step 4). func TestDecodeHeaderReportsTheCDDLFirst(t *testing.T) { h := map[uint64]any{0: capsule.HeaderTypeTag, 1: uint64(1), 2: make([]byte, 16), 3: "dk1_x", 4: uint64(0)} if _, err := capsule.DecodeHeader(marshal(t, h)); !errors.Is(err, datekeys.ErrDateKeyInvalid) { t.Fatalf("invalid DateKey alone: %v", err) } for name, edit := range map[string]func(m map[uint64]any){ "undefined access_policy": func(m map[uint64]any) { m[4] = uint64(2) }, "access_policy 256": func(m map[uint64]any) { m[4] = uint64(256) }, "extensions out of order": func(m map[uint64]any) { m[6] = []any{ext("b", 1), ext("a", 1)} }, "id in both arrays": func(m map[uint64]any) { m[5], m[6] = []any{ext("a", 1)}, []any{ext("a", 1)} }, } { if _, err := capsule.DecodeHeader(marshal(t, with(h, edit))); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) { t.Errorf("%s and an invalid DateKey: %v", name, err) } } }