You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/extension/extension_test.go

505 lines
20 KiB

package extension_test
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"slices"
"strings"
"testing"
"time"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/codec"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/cbortest"
)
func ext(t *testing.T, id string, v uint64, data []byte) extension.Extension {
t.Helper()
if data == nil {
return extension.Extension{ID: id, Version: v}
}
e, err := extension.New(id, v, data)
if err != nil {
t.Fatal(err)
}
return e
}
func TestNew(t *testing.T) {
data := []byte("public label")
e, err := extension.New("org.example.label", 1, data)
if err != nil || e.ID != "org.example.label" || e.Version != 1 || !bytes.Equal(e.Data, data) {
t.Fatalf("%+v %v", e, err)
}
data[0] = 'P'
if e.Data[0] != 'p' {
t.Fatal("New does not copy data")
}
for name, tc := range map[string]struct {
id string
version uint64
data []byte
}{
// Spec §54, §58.1: an extension without data omits key 2; it is built
// as a literal, never through New.
"nil data": {"org.a", 1, nil},
"empty data": {"org.a", 1, []byte{}},
"empty id": {"", 1, []byte{1}},
"invalid UTF-8 id": {"org.\xff", 1, []byte{1}},
"id too long": {strings.Repeat("a", extension.MaxIDLen+1), 1, []byte{1}},
"version above 2^32": {"org.a", extension.MaxVersion + 1, []byte{1}},
} {
if _, err := extension.New(tc.id, tc.version, tc.data); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
t.Errorf("%s: %v", name, err)
}
}
if _, err := extension.New("org.a", extension.MaxVersion, []byte{0}); err != nil {
t.Fatalf("version 2^32-1 rejected: %v", err)
}
}
// decodeArray decodes b, one extension array, as the containing objects do:
// DecodeArray and then the re-encoding check.
func decodeArray(b []byte) ([]extension.Extension, error) {
var exts []extension.Extension
decode := func(d *codec.Decoder) (err error) { exts, err = extension.DecodeArray(d); return err }
encode := func(e *codec.Encoder) { extension.EncodeArray(e, exts) }
if err := codec.Unmarshal(b, decode, encode); err != nil {
return nil, err
}
return exts, nil
}
func encodeArray(t *testing.T, exts []extension.Extension) []byte {
t.Helper()
var e codec.Encoder
extension.EncodeArray(&e, exts)
b, err := e.Out()
if err != nil {
t.Fatal(err)
}
return b
}
func TestCanonicalSorts(t *testing.T) {
in := []extension.Extension{ext(t, "org.b", 1, nil), ext(t, "org.a", 2, []byte("x")), ext(t, "Z", 9, nil), ext(t, "org.aa", 1, []byte{7})}
w, err := extension.Canonical(in)
if err != nil {
t.Fatal(err)
}
var order []string
for _, e := range w {
order = append(order, e.ID)
}
// Bytewise UTF-8 order: uppercase before lowercase, prefixes first.
if got := []string{"Z", "org.a", "org.aa", "org.b"}; !slices.Equal(order, got) {
t.Fatalf("order %v, want %v", order, got)
}
if in[0].ID != "org.b" {
t.Fatal("Canonical reordered its input")
}
if w, _ := extension.Canonical(nil); w != nil {
t.Fatal("an empty array must be nil so that the key is omitted")
}
back, err := decodeArray(encodeArray(t, w))
if err != nil || len(back) != 4 || back[1].ID != "org.a" || string(back[1].Data) != "x" || back[0].Data != nil {
t.Fatalf("decode: %+v %v", back, err)
}
// The wire form: data is a byte string, and key 2 is omitted without data.
if got, want := hex.EncodeToString(encodeArray(t, w[:2])), "82"+"a200615a0109"+"a300656f72672e6101020241"+"78"; got != want {
t.Fatalf("wire %s, want %s", got, want)
}
}
func many(n int) []extension.Extension {
out := make([]extension.Extension, n)
for i := range out {
out[i] = extension.Extension{ID: fmt.Sprintf("org.example.%03d", i), Version: 1}
}
return out
}
func TestCanonicalRejects(t *testing.T) {
for name, in := range map[string][]extension.Extension{
"same id twice": {ext(t, "org.a", 1, nil), ext(t, "org.a", 2, nil)},
"empty id": {{ID: "", Version: 1}},
"invalid UTF-8 id": {{ID: "org.\xff", Version: 1}},
"present but empty": {{ID: "org.a", Version: 1, Data: []byte{}}},
"version above 2^32": {{ID: "org.a", Version: extension.MaxVersion + 1}},
"65 extensions (§64)": many(extension.MaxExtensions + 1),
"duplicate among many": append(many(3), extension.Extension{ID: "org.example.001", Version: 2}),
} {
if _, err := extension.Canonical(in); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
t.Errorf("%s: %v", name, err)
}
}
if w, err := extension.Canonical(many(extension.MaxExtensions)); err != nil || len(w) != extension.MaxExtensions {
t.Fatalf("64 extensions rejected: %v", err)
}
}
// EncodeArray never writes an array that DecodeArray rejects: the Encoder
// records the error and Out returns it.
func TestEncodeArrayRejects(t *testing.T) {
for name, in := range map[string][]extension.Extension{
"nil": nil,
"empty": {},
"65 extensions": many(extension.MaxExtensions + 1),
"present but empty": {{ID: "org.a", Version: 1, Data: []byte{}}},
"out of order": {{ID: "org.b", Version: 1}, {ID: "org.a", Version: 1}},
"same id twice": {{ID: "org.a", Version: 1}, {ID: "org.a", Version: 2}},
"version above 2^32": {{ID: "org.a", Version: extension.MaxVersion + 1}},
"empty id": {{ID: "", Version: 1}},
"id too long": {{ID: strings.Repeat("a", extension.MaxIDLen+1), Version: 1}},
"invalid UTF-8 id": {{ID: "org.\xff", Version: 1}},
} {
var e codec.Encoder
extension.EncodeArray(&e, in)
if b, err := e.Out(); b != nil || !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
t.Errorf("%s: %x %v", name, b, err)
}
}
if b := encodeArray(t, many(extension.MaxExtensions)); b[0] != 0x98 || b[1] != extension.MaxExtensions {
t.Fatalf("64 extensions: %x", b[:2])
}
}
// entries returns the wire maps of exts, without data.
func entries(exts []extension.Extension) []any {
out := make([]any, len(exts))
for i, e := range exts {
out[i] = map[uint64]any{0: e.ID, 1: e.Version}
}
return out
}
func TestDecodeArrayRejects(t *testing.T) {
entry := func(id any, v uint64) map[uint64]any { return map[uint64]any{0: id, 1: v} }
for name, in := range map[string]any{
"out of order": []any{entry("org.b", 1), entry("org.a", 1)},
"repeated id": []any{entry("org.a", 1), entry("org.a", 2)},
"present but empty": []any{map[uint64]any{0: "org.a", 1: uint64(1), 2: []byte{}}},
"version above 2^32": []any{entry("org.a", extension.MaxVersion+1)},
"empty id": []any{entry("", 1)},
"id too long": []any{entry(strings.Repeat("a", extension.MaxIDLen+1), 1)},
"invalid UTF-8 id": []any{entry("org.\xff", 1)},
"id not a string": []any{entry(uint64(1), 1)},
"65 extensions": entries(many(extension.MaxExtensions + 1)),
"empty array": []any{},
"not an array": entry("org.a", 1),
"entry not a map": []any{"org.a"},
"four keys": []any{map[uint64]any{0: "org.a", 1: uint64(1), 2: []byte{1}, 3: uint64(0)}},
"unknown key": []any{map[uint64]any{0: "org.a", 1: uint64(1), 3: uint64(0)}},
"without version": []any{map[uint64]any{0: "org.a"}},
"without id": []any{map[uint64]any{1: uint64(1)}},
"text key": cbortest.Raw{0x81, 0xa2, 0x61, 0x61, 0x00, 0x01, 0x01},
"truncated": cbortest.Raw{0x82, 0xa2, 0x00, 0x61, 0x61, 0x01, 0x01},
} {
b, err := cbortest.Marshal(in)
if err != nil {
t.Fatal(err)
}
if _, err := decodeArray(b); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
t.Errorf("%s: %v", name, err)
}
}
b, _ := cbortest.Marshal(entries(many(extension.MaxExtensions)))
if got, err := decodeArray(b); err != nil || len(got) != extension.MaxExtensions {
t.Fatalf("64 extensions rejected: %v", err)
}
}
// Spec §54: key 2 is absent or a non-empty byte string, whatever it contains.
// Every other form is rejected by the extension map itself.
func TestData(t *testing.T) {
const head = "81" + "a3006161" + "0101" + "02" // [{0: "a", 1: 1, 2: ...}]
valid := []struct{ name, item, data string }{
{"one byte", "4100", "00"},
{"bytes that are not CBOR", "44ff1c00f7", "ff1c00f7"},
{"CBOR that the base protocol never decodes", "49a2f97e0000f97e0001", "a2f97e0000f97e0001"},
{"14 nested arrays", "4f" + strings.Repeat("81", 14) + "00", strings.Repeat("81", 14) + "00"},
{"24 bytes, one-byte length", "5818" + strings.Repeat("ab", 24), strings.Repeat("ab", 24)},
}
for _, tc := range valid {
b, _ := hex.DecodeString(head + tc.item)
if w, err := decodeArray(b); err != nil || hex.EncodeToString(w[0].Data) != tc.data {
t.Errorf("%s: %v", tc.name, err)
}
}
if none, err := decodeArray([]byte{0x81, 0xa2, 0x00, 0x61, 0x61, 0x01, 0x01}); err != nil || none[0].Data != nil || none[0].ID != "a" {
t.Fatalf("extension without data: %+v %v", none, err)
}
for _, tc := range []struct{ name, item string }{
{"empty byte string h''", "40"},
{"text string", "6161"},
{"unsigned integer", "01"},
{"negative integer", "20"},
{"array", "820102"},
{"empty array", "80"},
{"map", "a10001"},
{"true", "f5"},
{"null", "f6"},
{"undefined", "f7"},
{"float", "f93c00"},
{"tag", "c24101"},
{"length not in shortest form", "5801" + "00"},
{"indefinite-length byte string", "5f4100ff"},
{"truncated byte string", "42" + "00"},
} {
b, _ := hex.DecodeString(head + tc.item)
if w, err := decodeArray(b); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
t.Errorf("%s: %+v %v", tc.name, w, err)
}
}
}
func TestCheckDisjoint(t *testing.T) {
crit := []extension.Extension{ext(t, "org.a", 1, nil)}
non := []extension.Extension{ext(t, "org.a", 2, nil)}
if err := extension.CheckDisjoint(crit, non); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
t.Fatalf("id in both arrays: %v", err)
}
a := []extension.Extension{{ID: "a"}, {ID: "c"}, {ID: "e"}}
b := []extension.Extension{{ID: "b"}, {ID: "d"}, {ID: "f"}}
if err := extension.CheckDisjoint(a, b); err != nil {
t.Fatal(err)
}
// Input that is not in canonical order is sorted before the merge.
unsorted := []extension.Extension{{ID: "z"}, {ID: "e"}}
if err := extension.CheckDisjoint(a, unsorted); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
t.Fatalf("unsorted input: %v", err)
}
if !slices.IsSortedFunc(unsorted, func(x, y extension.Extension) int { return strings.Compare(y.ID, x.ID) }) {
t.Fatal("CheckDisjoint reordered its input")
}
if err := extension.CheckDisjoint(nil, b); err != nil {
t.Fatal(err)
}
}
// The merge is linear: 200 000 + 200 000 identifiers take milliseconds, where
// the former pairwise comparison took hours (spec §76, case 6).
func TestCheckDisjointIsLinear(t *testing.T) {
const n = 200_000
crit, non := make([]extension.Extension, n), make([]extension.Extension, n)
for i := range n {
crit[i].ID = fmt.Sprintf("a.%07d", i)
non[i].ID = fmt.Sprintf("b.%07d", i)
}
start := time.Now()
if err := extension.CheckDisjoint(crit, non); err != nil {
t.Fatal(err)
}
if d := time.Since(start); d > 5*time.Second {
t.Fatalf("CheckDisjoint took %s", d)
}
}
// validator knows org.a v1 and org.b v1, and accepts only the data "ok".
type validator struct{}
func (validator) Known(id string, v uint64) bool { return (id == "org.a" || id == "org.b") && v == 1 }
func (validator) ValidateData(e extension.Extension) error {
if string(e.Data) != "ok" {
// A validator may report a normative code of its own; the result
// carries ErrExtensionDataInvalid only.
return fmt.Errorf("want \"ok\": %w", datekeys.ErrNonCanonicalCBOR)
}
return nil
}
func TestCheckCritical(t *testing.T) {
crit := []extension.Extension{ext(t, "org.a", 1, nil)}
if err := extension.CheckCritical(crit, nil); !errors.Is(err, datekeys.ErrExtensionCriticalUnknown) {
t.Fatalf("unknown critical accepted by the base protocol: %v", err)
}
if err := extension.CheckCritical(crit, extension.Set{"org.a": {2}}); !errors.Is(err, datekeys.ErrExtensionCriticalUnknown) {
t.Fatalf("other version accepted: %v", err)
}
if err := extension.CheckCritical(crit, extension.Set{"org.a": {1}}); err != nil {
t.Fatalf("known critical rejected: %v", err)
}
if err := extension.CheckCritical(nil, nil); err != nil {
t.Fatal(err)
}
// Spec §54: a known critical extension with invalid data.
good, bad := ext(t, "org.a", 1, []byte("ok")), ext(t, "org.b", 1, []byte("ko"))
if err := extension.CheckCritical([]extension.Extension{good}, validator{}); err != nil {
t.Fatal(err)
}
err := extension.CheckCritical([]extension.Extension{good, bad}, validator{})
if datekeys.Code(err) != "ERR_EXTENSION_DATA_INVALID" || errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
t.Fatalf("invalid data: %v", err)
}
// An unknown critical extension takes precedence over invalid data.
unknown := ext(t, "org.c", 1, nil)
if err := extension.CheckCritical([]extension.Extension{bad, unknown}, validator{}); !errors.Is(err, datekeys.ErrExtensionCriticalUnknown) {
t.Fatalf("unknown and invalid: %v", err)
}
}
func TestCheckNoncritical(t *testing.T) {
good, bad, unknown := ext(t, "org.a", 1, []byte("ok")), ext(t, "org.b", 1, nil), ext(t, "org.c", 1, []byte("ko"))
all := []extension.Extension{good, bad, unknown}
if u := extension.CheckNoncritical(all, nil); u != nil {
t.Fatalf("base protocol: %v", u)
}
if u := extension.CheckNoncritical(all, extension.Set{"org.b": {1}}); u != nil {
t.Fatalf("a Set does not validate data: %v", u)
}
u := extension.CheckNoncritical(all, validator{})
if len(u) != 1 || u[0].ID != "org.b" || u[0].Version != 1 || !errors.Is(u[0].Err, datekeys.ErrExtensionDataInvalid) {
t.Fatalf("unusable: %+v", u)
}
}
Spec v0.8.2: corrections from the formal review A formal review of the whole v0.8.2 text found it approvable after these corrections, recorded in §76 ("Correcciones de la revisión formal"): - §27 no longer calls header_binding the authenticity of PUBLIC_HEADER: it binds the header to the opened control, never authorship or date (§55.1); the age MAC only protects against whoever lacks the file key. - §63 steps 9 and 10: a network source (relay, Release API, cache) MUST verify every response and gives ERR_RELEASE_UNAVAILABLE at step 9 when none verifies; the step-10 codes are for a directly supplied release. The reference already behaved so; TestReleaseFromANetworkSource pins both paths. - §54 and §72: registrations declare the objects and arrays where an extension may appear, and a known extension out of place counts as unknown there. The reference gains the optional extension.Placement interface, used at steps 4, 9.a and 14. - §63 step 11 fixes the GT serialization hashed by H2 (kilic/kyber order) with the frozen vector H2(e(G1, G2))[:16] = cb87319f..., shared as testdata/vectors/tlock_ibe.json; H2-H4 are cited to drand/kyber. - Step 5 makes the SEALED_CONTROL read mandatory, step 15 names ERR_HEADER_BINDING, §21 makes capsule_id 16 CSPRNG bytes a MUST, §76 is made accurate (four dk1.json vectors, the §36 time_only rule, two cases rewritten against the texts that really existed), and editorial fixes in §5, §36, §55.1, §69.1 and §77. §73 lists the three new decisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
// placed is validator with the placement of its registrations (spec §72):
// org.a only in the critical_extensions of CONTROL_CBOR, org.b only in the
// noncritical_extensions of PUBLIC_HEADER and of the .dkk.
type placed struct{ validator }
func (placed) RegisteredIn(id string, v uint64, obj extension.Object, arr extension.Array) bool {
switch id {
case "org.a":
return obj == extension.Control && arr == extension.Critical
case "org.b":
return arr == extension.Noncritical && obj != extension.Control
}
return false
}
// Spec §54, §72: a known extension that appears in an object or array it is
// not registered for is treated there as unknown. A Registry that is not a
Review fixes: author keys, the writer, the CLI, extensions and the locator Fixes of the review of the session of 1 and 2 October that the text of spec v0.11 already asks for: - authorkey: String and GoString hide the secret key, which only Secret returns; ParsePublic refuses a key that is not a point of the curve (ed25519strict.OnCurve, checked against the square root of testkit). - capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never a capsule without the signature or the seal that was asked for. A panic while evaluating the signature or the seal fails only that part, F1 or S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can refuse to publish the files. - extension.CheckWrite, the rule of encoders of spec 72: the writers of capsules and .dkk files refuse datekeys.note and datekeys.capsule outside the arrays where they are registered, or with invalid data. - CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE before it signs (rule 20); decrypt -expect-author compares the key of an F4 and writes nothing unless it matches; decrypt notifies a public note that it does not show; the lines of the verdicts break at the last space that fits, each row after the first behind a mark, so that the terminal never breaks them; L is the payload, not the content. - locator: a reader rejects an address that breaks 44.1 and keeps the others; addresses refuse the special-purpose blocks of IANA, IPv6 outside 2000::/3, localhost and local names, characters outside RFC 3986, dot segments, and a CID that does not decode to version 1 and a multihash; ParseInfo checks that the locator is an age file with one tlock stanza for the round of its DateKey; Info.Extension reads what it writes; its errors carry no normative code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
// CheckWrite, the rule of encoders of spec §72: what the Registry knows goes
// only where it is registered, with valid data; the rest is not checked.
func TestCheckWrite(t *testing.T) {
ok := []extension.Extension{ext(t, "org.a", 1, []byte("ok"))}
if err := extension.CheckWrite(placed{}, extension.Control, extension.Critical, ok); err != nil {
t.Errorf("where it is registered: %v", err)
}
if err := extension.CheckWrite(placed{}, extension.PublicHeader, extension.Critical, ok); err == nil || !strings.Contains(err.Error(), "not registered") {
t.Errorf("where it is not registered: %v", err)
}
if err := extension.CheckWrite(placed{}, extension.Control, extension.Critical, []extension.Extension{ext(t, "org.a", 1, []byte("ko"))}); err == nil {
t.Error("invalid data was written")
}
other := []extension.Extension{ext(t, "org.z", 1, []byte("anything"))}
if extension.CheckWrite(placed{}, extension.PublicHeader, extension.Critical, other) != nil || extension.CheckWrite(nil, extension.PublicHeader, extension.Critical, ok) != nil {
t.Error("an extension that the Registry does not know was checked")
}
var std extension.Standard
note := []extension.Extension{{ID: extension.NoteID, Version: 1, Data: []byte("Cartas")}}
if extension.CheckWrite(std, extension.PublicHeader, extension.Noncritical, note) != nil ||
extension.CheckWrite(std, extension.AccessKey, extension.Noncritical, note) == nil ||
extension.CheckWrite(std, extension.PublicHeader, extension.Noncritical, []extension.Extension{{ID: extension.NoteID, Version: 1, Data: []byte(" a")}}) == nil {
t.Error("datekeys.note")
}
}
Spec v0.8.2: corrections from the formal review A formal review of the whole v0.8.2 text found it approvable after these corrections, recorded in §76 ("Correcciones de la revisión formal"): - §27 no longer calls header_binding the authenticity of PUBLIC_HEADER: it binds the header to the opened control, never authorship or date (§55.1); the age MAC only protects against whoever lacks the file key. - §63 steps 9 and 10: a network source (relay, Release API, cache) MUST verify every response and gives ERR_RELEASE_UNAVAILABLE at step 9 when none verifies; the step-10 codes are for a directly supplied release. The reference already behaved so; TestReleaseFromANetworkSource pins both paths. - §54 and §72: registrations declare the objects and arrays where an extension may appear, and a known extension out of place counts as unknown there. The reference gains the optional extension.Placement interface, used at steps 4, 9.a and 14. - §63 step 11 fixes the GT serialization hashed by H2 (kilic/kyber order) with the frozen vector H2(e(G1, G2))[:16] = cb87319f..., shared as testdata/vectors/tlock_ibe.json; H2-H4 are cited to drand/kyber. - Step 5 makes the SEALED_CONTROL read mandatory, step 15 names ERR_HEADER_BINDING, §21 makes capsule_id 16 CSPRNG bytes a MUST, §76 is made accurate (four dk1.json vectors, the §36 time_only rule, two cases rewritten against the texts that really existed), and editorial fixes in §5, §36, §55.1, §69.1 and §77. §73 lists the three new decisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
// Placement knows its extensions everywhere, and CheckCritical and
// CheckNoncritical, which do not know the object, consult no Placement.
func TestPlacement(t *testing.T) {
objects := []extension.Object{extension.PublicHeader, extension.Control, extension.AccessKey}
arrays := []extension.Array{extension.Critical, extension.Noncritical}
for _, obj := range objects {
for _, arr := range arrays {
if !extension.KnownIn(validator{}, "org.a", 1, obj, arr) || extension.KnownIn(validator{}, "org.c", 1, obj, arr) || extension.KnownIn(nil, "org.a", 1, obj, arr) {
t.Fatalf("without a Placement, in the %s of %s", arr, obj)
}
if got, want := extension.KnownIn(placed{}, "org.a", 1, obj, arr), obj == extension.Control && arr == extension.Critical; got != want {
t.Errorf("org.a in the %s of %s: known %v", arr, obj, got)
}
if extension.KnownIn(placed{}, "org.a", 2, obj, arr) {
t.Errorf("org.a v2, which the registry does not know, in the %s of %s", arr, obj)
}
}
}
// A critical extension outside its registration is unknown there, with
// the object in the message.
crit := []extension.Extension{ext(t, "org.a", 1, []byte("ok"))}
if err := extension.CheckCriticalIn(extension.Control, crit, placed{}); err != nil {
t.Fatalf("where it is registered: %v", err)
}
for _, obj := range []extension.Object{extension.PublicHeader, extension.AccessKey} {
err := extension.CheckCriticalIn(obj, crit, placed{})
if !errors.Is(err, datekeys.ErrExtensionCriticalUnknown) || !strings.Contains(err.Error(), obj.String()) {
t.Errorf("CONTROL_CBOR extension in the critical_extensions of %s: %v", obj, err)
}
if err := extension.CheckCriticalIn(obj, crit, validator{}); err != nil {
t.Errorf("a Registry that is not a Placement, in %s: %v", obj, err)
}
}
if err := extension.CheckCritical(crit, placed{}); err != nil {
t.Errorf("CheckCritical consulted the Placement: %v", err)
}
// Unknown there comes before the invalid data of another extension.
mixed := []extension.Extension{ext(t, "org.a", 1, []byte("ko")), ext(t, "org.b", 1, []byte("ok"))}
if err := extension.CheckCriticalIn(extension.Control, mixed, placed{}); !errors.Is(err, datekeys.ErrExtensionCriticalUnknown) {
t.Errorf("noncritical-only extension in a critical array, after invalid data: %v", err)
}
// A noncritical extension outside its registration is ignored, its data
// unchecked; where it is registered, invalid data makes it unusable.
non := []extension.Extension{ext(t, "org.b", 1, []byte("ko"))}
for _, obj := range objects {
u := extension.CheckNoncriticalIn(obj, non, placed{})
if obj == extension.Control && u != nil || obj != extension.Control && (len(u) != 1 || u[0].ID != "org.b") {
t.Errorf("org.b with invalid data in the noncritical_extensions of %s: %+v", obj, u)
}
if u := extension.CheckNoncriticalIn(obj, non, validator{}); len(u) != 1 {
t.Errorf("a Registry that is not a Placement, in %s: %+v", obj, u)
}
}
if u := extension.CheckNoncritical(non, placed{}); len(u) != 1 {
t.Errorf("CheckNoncritical consulted the Placement: %+v", u)
}
for v, want := range map[fmt.Stringer]string{
extension.PublicHeader: "PUBLIC_HEADER", extension.Control: "CONTROL_CBOR", extension.AccessKey: ".dkk", extension.Object(0): "Object(0)",
extension.Critical: "critical_extensions", extension.Noncritical: "noncritical_extensions", extension.Array(3): "Array(3)",
} {
if v.String() != want {
t.Errorf("%s, want %s", v, want)
}
}
}
// FuzzDecodeArray: whatever DecodeArray accepts is in canonical order and
// re-encodes to its input with EncodeArray, and every error carries
// ErrNonCanonicalCBOR.
func FuzzDecodeArray(f *testing.F) {
for _, h := range []string{
"81a2006161" + "0101",
"82a2006161" + "0101" + "a3006162" + "0102" + "02" + "4100",
"81a3006161010102" + "40",
"80",
} {
b, _ := hex.DecodeString(h)
f.Add(b)
}
same := func(a, b extension.Extension) bool {
return a.ID == b.ID && a.Version == b.Version && bytes.Equal(a.Data, b.Data) && (a.Data == nil) == (b.Data == nil)
}
f.Fuzz(func(t *testing.T, b []byte) {
exts, err := decodeArray(b)
if err != nil {
if !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
t.Fatalf("error without ErrNonCanonicalCBOR: %v", err)
}
return
}
if c, err := extension.Canonical(exts); err != nil || !slices.EqualFunc(c, exts, same) {
t.Fatalf("decoded array is not canonical: %v", err)
}
if re := encodeArray(t, exts); !bytes.Equal(re, b) {
t.Fatalf("%x re-encodes to %x", b, re)
}
})
}

Powered by TurnKey Linux.