//go:build ignore // Writes the vectors of the head, the public note, the inspection and the // opening of datekeys-dart, stage 4c of docs/PLAN_dart.md: the results, the // normative codes, the texts and the checks of the Go reference on // // - open_heads.json: HEAD_CBOR (capsule.DecodeHead), heads built with a // fixed seed, valid and broken in each layer of spec §69.1, with no // registry and with one; and capsule.EncodeHead of values; // - open_notes.json: the public note (extension.CheckNote on texts and on // bytes, extension.Note and Header.UnusableNote on extension arrays, // extension.Standard through CheckNoncriticalIn and CheckWrite); // - open_inspect.json: the text of the error of capsule.Inspect for each // mutation of testdata/vectors/inspect_differential.json, and the exact // output of `datekeys inspect -json` (internal/inspectview) for capsules // with a public note, valid or not, and unusable extensions; // - open_cases.json: capsule.Open on every fixture with each of its // credentials, and on fixtures edited or opened with other options at // each step of spec §63 that the mutation corpus does not reach: the // framing of a .dkk at step 9.a, its fields, extensions and bindings, the // clock and the failures of the release source at step 9, the headers // of age at steps 11 and 17, a malformed X25519 stanza in // INNER_ACCESS_AGE, CONTROL_CBOR sealed again, BODY of format 3 sealed // again, the sinks and the output that fail, the refusal of Accept and // the unusable extensions of each object. // // Every case of open_cases.json records the text of the error, its code and // step, and the checks of capsule.Open with their details; a capsule of // format 3 that opens, its verdicts too (stage 5): the signature and the // seal, the lines that show them, the key and the label of a signature of // alg 1 and the earliest time of a valid seal. The fixtures signed with alg 1 // open also with their author key saved, and with another. The edited // capsules are sealed again as the testkit of the reference does, with the // file keys of the fixtures and their nonces, so the output is the same on // every run. It also writes test/vectors/open_vectors.g.dart: the fixtures // it names and a part of each file, as Dart constants for the tests that // also run compiled to JavaScript. // // It imports internal packages of the reference (internal/testkit, // internal/cbortest and internal/inspectview), so it runs in an export of // datekeys-go made with git archive, without changing the repository, on // the branch v0.12 at c531e93: // // commit=$(git -C ../datekeys-go rev-parse v0.12) // root=$PWD // tmp=$(mktemp -d) // git -C ../datekeys-go archive "$commit" | tar -x -C "$tmp" // cp tool/open_go_vectors.go "$tmp" // (cd "$tmp" && go run ./open_go_vectors.go -source "$commit" \ // -testdata "$root/testdata" -out "$root/test/vectors") // rm -rf "$tmp" package main import ( "bytes" "context" "crypto/sha256" "encoding/base64" "encoding/binary" "encoding/hex" "encoding/json" "errors" "flag" "fmt" "hash" "io" "math/rand/v2" "os" "path/filepath" "slices" "strings" "time" "unicode/utf8" "filippo.io/age" datekeys "g.activething.com/go/DateKeys" "g.activething.com/go/DateKeys/accesskey" "g.activething.com/go/DateKeys/agewrap" "g.activething.com/go/DateKeys/authorkey" "g.activething.com/go/DateKeys/capsule" "g.activething.com/go/DateKeys/extension" "g.activething.com/go/DateKeys/internal/cbortest" "g.activething.com/go/DateKeys/internal/inspectview" "g.activething.com/go/DateKeys/internal/testkit" "g.activething.com/go/DateKeys/profile" "g.activething.com/go/DateKeys/provider" ) const specVersion = "0.11" // The fixed seed of every random choice. var rng = rand.New(rand.NewPCG(0x4c0a052026, 0x0de4c)) func must[T any](v T, err error) T { if err != nil { panic(err) } return v } func unhex(s string) []byte { return must(hex.DecodeString(s)) } func hx(b []byte) string { return hex.EncodeToString(b) } func pick[T any](xs ...T) T { return xs[rng.IntN(len(xs))] } func randBytes(n int) []byte { b := make([]byte, n) for i := range b { b[i] = byte(rng.IntN(256)) } return b } // Case is one vector: a JSON object. type Case map[string]any // outcome records the result of err in c: "ok", the normative code, or // "error" for an error without one, with its text. func outcome(c Case, err error) Case { if err == nil { c["result"] = "ok" return c } code := datekeys.Code(err) if code == "" { code = "error" } c["result"] = code text := err.Error() if !utf8.ValidString(text) { panic("an error text that is not valid UTF-8: " + text) } c["text"] = text return c } // --------------------------------------------------------------------------- // Fixtures var fixtureDir string var loaded = map[string]*testkit.LoadedFixture{} func fx(name string) *testkit.LoadedFixture { if f, ok := loaded[name]; ok { return f } f := must(testkit.LoadFixture(fixtureDir, name)) loaded[name] = f return f } // fixtureNames are the 26 capsules of the synced testdata, sorted. func fixtureNames() []string { var names []string for _, e := range must(os.ReadDir(fixtureDir)) { if n, ok := strings.CutSuffix(e.Name(), ".dkc"); ok { names = append(names, n) } } slices.Sort(names) return names } // edits are the edits that turn the fixture base into out, as // testdata/README.md writes them. func edits(base string, out []byte) Case { b := fx(strings.TrimSuffix(base, ".dkc")).DKC es := [][]any{} for _, e := range testkit.Splice(b, out) { es = append(es, []any{e.At, e.Delete, hx(e.Insert)}) } return Case{"base": base, "edits": es} } // --------------------------------------------------------------------------- // Openings // opening is what capsule.Open is given in one case. type opening struct { name string base string // the fixture the capsule derives from dkc []byte release *provider.Release sourceErr error // when set, the source fails with it now time.Time empty bool // no pinned profile known testkit.KnownExtensions rejectAll string // a registry that knows everything and rejects all data identities []string dkk []byte // a .dkk decoded before material []byte // the material of that .dkk, replaced dkkFile []byte // a .dkk still encoded sinkFail string // begin, create i, write i, close i or commit outputFail bool refuse bool authorKeys map[string]string // the author keys saved, with their labels } // rejecting knows every extension and rejects the data of all. type rejecting string func (rejecting) Known(string, uint64) bool { return true } func (r rejecting) ValidateData(extension.Extension) error { return errors.New(string(r)) } func (r rejecting) RegisteredIn(string, uint64, extension.Object, extension.Array) bool { return true } type source struct { release *provider.Release err error calls int } func (s *source) Fetch(context.Context, *profile.Profile, provider.Condition) (provider.Release, error) { s.calls++ if s.err != nil { return provider.Release{}, s.err } if s.release == nil { return provider.Release{}, fmt.Errorf("testkit: no release: %w", datekeys.ErrReleaseUnavailable) } return *s.release, nil } // recordingSink keeps the files and fails at the call named. type recordingSink struct { fail string files [][]byte committed bool aborted bool begun bool } func (s *recordingSink) Begin(h *capsule.Head) error { if s.fail == "begin" { return errors.New("no begin") } s.begun = true s.files = make([][]byte, len(h.Files)) return nil } func (s *recordingSink) Create(i int) (io.WriteCloser, error) { if s.fail == fmt.Sprintf("create %d", i) { return nil, fmt.Errorf("no create %d", i) } return &recordingFile{s, i}, nil } func (s *recordingSink) Commit() error { if s.fail == "commit" { return errors.New("no commit") } s.committed = true return nil } func (s *recordingSink) Abort() { s.aborted = true } type recordingFile struct { s *recordingSink i int } func (f *recordingFile) Write(b []byte) (int, error) { if f.s.fail == fmt.Sprintf("write %d", f.i) { return 0, fmt.Errorf("no write %d", f.i) } f.s.files[f.i] = append(f.s.files[f.i], b...) return len(b), nil } func (f *recordingFile) Close() error { if f.s.fail == fmt.Sprintf("close %d", f.i) { return fmt.Errorf("no close %d", f.i) } return nil } // output records the length and the SHA-256 of what it receives. type output struct { fail bool h hash.Hash n int } func (w *output) Write(b []byte) (int, error) { if w.fail { return 0, errors.New("disk full") } w.h.Write(b) w.n += len(b) return len(b), nil } func unusableJSON(us []extension.Unusable) [][]any { out := [][]any{} for _, u := range us { out = append(out, []any{u.ID, u.Version, u.Err.Error()}) } return out } func checksJSON(cs []capsule.CheckResult) [][]any { out := [][]any{} for _, c := range cs { out = append(out, []any{c.Step, c.Name, c.OK, c.Detail, c.Error}) } return out } func (o opening) run() Case { reg := testkit.Registry() if o.empty { reg = must(profile.NewRegistry()) } src := &source{release: o.release, err: o.sourceErr} sink := &recordingSink{fail: o.sinkFail} now := o.now opts := capsule.OpenOptions{Registry: reg, Source: src, Now: func() time.Time { return now }, Sink: sink} c := Case{"name": o.name, "dkc": edits(o.base, o.dkc), "now": now.Format(time.RFC3339Nano)} if o.release != nil { c["release"] = Case{"round": o.release.Round, "signature": hx(o.release.Signature)} } else { c["release"] = nil } if o.sourceErr != nil { se := Case{"text": o.sourceErr.Error()} if code := datekeys.Code(o.sourceErr); code != "" { se["code"] = code se["text"] = strings.TrimSuffix(o.sourceErr.Error(), ": "+code) } c["source_error"] = se } if o.empty { c["registry"] = "empty" } switch { case o.known != nil: opts.Extensions = o.known var ks []Case for _, k := range o.known { ks = append(ks, Case{"id": k.ID, "version": k.Version, "valid_data": hx(k.ValidData)}) } c["extensions"] = ks case o.rejectAll != "": opts.Extensions = rejecting(o.rejectAll) c["reject_all"] = o.rejectAll } if o.dkk != nil { k := must(accesskey.Decode(bytes.NewReader(o.dkk))) c["dkk"] = hx(o.dkk) if o.material != nil { k.Material = o.material c["dkk_material"] = hx(o.material) } opts.AccessKey = k } if o.dkkFile != nil { opts.AccessKeyFile = bytes.NewReader(o.dkkFile) c["dkk_file"] = hx(o.dkkFile) } if o.identities != nil { c["identities"] = o.identities for _, s := range o.identities { opts.Identities = append(opts.Identities, must(age.ParseX25519Identity(s))) } } if o.sinkFail != "" { c["sink_fail"] = o.sinkFail } if o.outputFail { c["output_fail"] = true } if o.refuse { c["accept"] = "refuse" opts.Accept = func(capsule.Verdicts) error { return errors.New("not trusted") } } if o.authorKeys != nil { c["author_keys"] = o.authorKeys opts.AuthorKeys = o.authorKeys } out := &output{fail: o.outputFail, h: sha256.New()} opened, err := capsule.Open(context.Background(), out, bytes.NewReader(o.dkc), opts) switch { case err == nil: c["result"], c["text"] = "ok", "ok" case o.refuse && datekeys.Code(err) == "": c["result"], c["text"] = "refused", err.Error() default: outcome(c, err) } checks := opened.Inspection.Checks c["checks"] = checksJSON(checks) if err != nil && checks[len(checks)-1].OK && c["result"] != "refused" { panic(o.name + ": a failure without a failed check") } c["release_requests"] = src.calls if err == nil { if opened.Format == capsule.Format3 { files := [][]any{} for i, f := range opened.Head.Files { sum := sha256.Sum256(sink.files[i]) files = append(files, []any{f.Path, f.Size, hx(sum[:])}) } c["files"] = files c["verdicts"] = verdictsJSON(opened.Verdicts) } else { c["content"] = Case{"length": out.n, "sha256": hx(out.h.Sum(nil))} } } switch { case sink.committed: c["sink"] = "committed" case sink.aborted: c["sink"] = "aborted" case sink.begun: c["sink"] = "begun" default: c["sink"] = "untouched" } unusable := Case{} if opened.Inspection != nil && len(opened.Inspection.UnusableExtensions) > 0 { unusable["header"] = unusableJSON(opened.Inspection.UnusableExtensions) } if len(opened.UnusableControlExtensions) > 0 { unusable["control"] = unusableJSON(opened.UnusableControlExtensions) } if len(opened.UnusableAccessKeyExtensions) > 0 { unusable["dkk"] = unusableJSON(opened.UnusableAccessKeyExtensions) } if len(opened.UnusableHeadExtensions) > 0 { unusable["head"] = unusableJSON(opened.UnusableHeadExtensions) } if len(unusable) > 0 { c["unusable"] = unusable } return c } // verdictsJSON records the verdicts of a capsule of format 3 that opens: // the signature and the seal, the lines that show them, the key of a valid // signature of alg 1 as dkauthor1..., the label of a saved one, and the // earliest instant of a valid seal. func verdictsJSON(v capsule.Verdicts) Case { c := Case{"signature": string(v.Signature), "seal": string(v.Seal), "lines": v.Lines()} if v.Signature == capsule.VerdictSignedSaved || v.Signature == capsule.VerdictSignedOther { c["author_key"] = must(authorkey.PublicString(v.AuthorKey[:])) } if v.Signature == capsule.VerdictSignedSaved { c["author_label"] = v.AuthorLabel } if t, ok := v.SealedAt(); ok { c["sealed_at"] = t.UTC().Format(time.RFC3339Nano) } return c } // otherAuthorKey is a key of an author that signed no fixture, from a fixed // seed. func otherAuthorKey() string { seed := sha256.Sum256([]byte("datekeys-dart: another author")) return must(authorkey.PublicString(must(authorkey.NewFromSeed(seed[:])).Public())) } // open is the opening of the fixture f as it is, with its release and its // clock. func open(name string, f *testkit.LoadedFixture) opening { r := f.Published return opening{name: name, base: f.File, dkc: f.DKC, release: &r, now: f.Unlock} } // --------------------------------------------------------------------------- // The fixtures, with each credential func fixtureCases() []Case { var out []Case for _, name := range fixtureNames() { f := fx(name) if f.AccessPolicy == capsule.TimeOnly.String() { out = append(out, open(name, f).run()) // A signature of alg 1 with its key saved, F3, and with another // key saved, F4 (spec §29.7, §29.12). if f.Signature != nil && f.Signature.AuthorKey != "" { o := open(name+": its author key saved", f) o.authorKeys = map[string]string{f.Signature.AuthorKey: "Ana", otherAuthorKey(): "Luis"} out = append(out, o.run()) o = open(name+": another author key saved", f) o.authorKeys = map[string]string{otherAuthorKey(): "Luis"} out = append(out, o.run()) } continue } var all []opening if f.DKK != nil { o := open(name+": the .dkk, still encoded", f) o.dkkFile = f.DKK all = append(all, o) o = open(name+": the .dkk, decoded", f) o.dkk = f.DKK all = append(all, o) } for i, id := range f.Identities { o := open(fmt.Sprintf("%s: identity %d", name, i+1), f) o.identities = []string{id} all = append(all, o) } if len(f.Identities) > 0 { o := open(name+": every credential", f) o.identities = f.Identities o.dkkFile = f.DKK all = append(all, o) } for _, o := range all { out = append(out, o.run()) } } return out } // --------------------------------------------------------------------------- // The cases at each step // dkkWith returns the .dkk of f with its body decoded as a map, changed by // edit and encoded again, in a frame of its length. func dkkWith(f *testkit.LoadedFixture, edit func(m map[uint64]any)) []byte { m := must(cbortest.UnmarshalMap(f.DKK[12:])) edit(m) body := must(cbortest.Marshal(m)) pre := []byte{'D', 'K', 'K', '1', 1, 0, 0, 0, 0, 0, 0, 0} binary.BigEndian.PutUint32(pre[8:], uint32(len(body))) return append(pre, body...) } func set(b []byte, i int, v byte) []byte { c := bytes.Clone(b) c[i] = v return c } // macEdit changes one character in the middle of the MAC of the age header // at the start of file: the header stays canonical Base64, and its MAC no // longer verifies. func macEdit(file []byte) []byte { i := bytes.Index(file, []byte("\n--- ")) + 10 return set(file, i, map[bool]byte{true: 'B', false: 'A'}[file[i] == 'A']) } func reframe(f *testkit.LoadedFixture, header, sealed, payload []byte) []byte { return testkit.Reframe(f.Parts.Prelude, header, sealed, payload) } // withBody returns the opening of f, a time_only fixture of format 3, with // BODY replaced and followed by the zeros of its padding up to P = rule(L); // plain, when given, edits that plaintext. func withBody(name string, f *testkit.LoadedFixture, body []byte, plain func(p []byte) []byte) opening { in := must(f.WithBody(body, plain)) o := open(name, f) o.dkc = in.DKC return o } // withPlaintext returns the opening of f with the plaintext of its // PAYLOAD_AGE replaced, L and P unchanged. func withPlaintext(name string, f *testkit.LoadedFixture, plaintext []byte) opening { in := must(f.WithPayloadPlaintext(plaintext)) o := open(name, f) o.dkc = in.DKC return o } // body3 is the BODY of a format 3 fixture, split. type body3 struct { frame capsule.BodyFrame area []byte head []byte content []byte } func readBody3(f *testkit.LoadedFixture) body3 { b := must(os.ReadFile(filepath.Join(fixtureDir, f.PlaintextFile))) frame := must(capsule.ParseBodyFrame(b[:capsule.BodyFrameSize], uint64(len(b)))) a := capsule.BodyFrameSize + int(frame.AreaLen) h := a + int(frame.HeadLen) return body3{frame: frame, area: b[capsule.BodyFrameSize:a], head: b[a:h], content: b[h:]} } func (b body3) bytes() []byte { fb := b.frame.Bytes() return testkit.Join(fb[:], b.area, b.head, b.content) } // withHead returns BODY with its head replaced by the map of the head // changed by edit. func (b body3) withHead(edit func(h map[uint64]any)) []byte { m := must(cbortest.UnmarshalMap(b.head)) edit(m) b.head = must(cbortest.Marshal(m)) b.frame.HeadLen = uint32(len(b.head)) return b.bytes() } func plaintextOf(f *testkit.LoadedFixture) []byte { return must(os.ReadFile(filepath.Join(fixtureDir, f.PlaintextFile))) } func zeros(n int) []byte { return make([]byte, n) } func stepCases() []Case { var os_ []opening add := func(o opening) { os_ = append(os_, o) } to, tox := fx("time_only"), fx("time_only_extensions") tk, tkr := fx("time_and_key_portable"), fx("time_and_key_recipients") tox2 := fx("format2_time_only_extensions") tk2r := fx("format2_time_and_key_recipients") s3, tree3, tk3 := fx("format3_single"), fx("format3_tree"), fx("format3_time_and_key_portable") // Steps 3 and 7 through the opening, which stops there without a // request. { o := open("PUBLIC_HEADER cut short", tox) o.dkc = tox.DKC[:capsule.PreludeSize+10] add(o) h := must(testkit.RawHeader([16]byte(must(capsule.DecodeHeader(tox.Parts.Header)).CapsuleID), dkRound(83903165812), 0)) o = open("a DateKey round after 9999-12-31T23:59:59Z", tox) o.dkc = reframe(tox, h, tox.Parts.Sealed, tox.Parts.Payload) add(o) h = must(testkit.RawHeader([16]byte(must(capsule.DecodeHeader(tox.Parts.Header)).CapsuleID), dkRound(83903165811), 0)) o = open("a DateKey of the last round, with the tlock stanza of round 2000", tox) o.dkc = reframe(tox, h, tox.Parts.Sealed, tox.Parts.Payload) o.now = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC) add(o) } // Step 9.a: the .dkk as an object, decoded at step 9, in the order of // spec §40, §41 and §69.1. dkkCases := []struct { name string file []byte }{ {"three bytes DKK", []byte("DKK")}, {"another magic", set(tk.DKK, 3, '2')}, {"a prelude of 11 bytes", tk.DKK[:11]}, {"framing version 2", set(tk.DKK, 4, 2)}, {"FLAGS 1", set(tk.DKK, 5, 1)}, {"RESERVED 1", set(tk.DKK, 7, 1)}, {"BODY_LEN 0", append(append(bytes.Clone(tk.DKK[:8]), 0, 0, 0, 0), tk.DKK[12:]...)}, {"BODY_LEN above 16 MiB", append(append(bytes.Clone(tk.DKK[:8]), 1, 0, 0, 1), tk.DKK[12:]...)}, {"a body cut short", tk.DKK[:len(tk.DKK)-1]}, {"a byte after the body", append(bytes.Clone(tk.DKK), 0)}, {"a body that is not canonical CBOR", set(tk.DKK, 12, 0xbf)}, {"another type tag", dkkWith(tk, func(m map[uint64]any) { m[0] = "datekeys-access-kex" })}, {"schema version 2", dkkWith(tk, func(m map[uint64]any) { m[1] = uint64(2) })}, {"an unknown key", dkkWith(tk, func(m map[uint64]any) { m[9] = uint64(1) })}, {"access_type ed25519", dkkWith(tk, func(m map[uint64]any) { m[4] = "ed25519" })}, {"access_material of 31 bytes", dkkWith(tk, func(m map[uint64]any) { m[5] = m[5].([]byte)[:31] })}, {"an unknown critical extension", dkkWith(tk, func(m map[uint64]any) { m[7] = []any{map[uint64]any{0: "org.example.lock", 1: uint64(1)}} })}, {"another capsule_id", dkkWith(tk, func(m map[uint64]any) { m[3] = bytes.Repeat([]byte{7}, 16) })}, {"another capsule_id and an unknown critical extension", dkkWith(tk, func(m map[uint64]any) { m[3] = bytes.Repeat([]byte{7}, 16) m[7] = []any{map[uint64]any{0: "org.example.lock", 1: uint64(1)}} })}, {"the capsule_digest of another file", dkkWith(tk, func(m map[uint64]any) { m[6] = map[uint64]any{0: bytes.Repeat([]byte{9}, 32)} })}, {"no capsule_digest", dkkWith(tk, func(m map[uint64]any) { delete(m, 6) })}, {"an unknown noncritical extension", dkkWith(tk, func(m map[uint64]any) { m[8] = []any{map[uint64]any{0: "org.example.note", 1: uint64(1), 2: []byte("hola")}} })}, } for _, d := range dkkCases { o := open("the .dkk: "+d.name, tk) o.dkkFile = d.file add(o) } { o := open("the .dkk: a known critical extension with invalid data", tk) o.dkkFile = dkkWith(tk, func(m map[uint64]any) { m[7] = []any{map[uint64]any{0: "org.example.lock", 1: uint64(1), 2: []byte("no")}} }) o.known = testkit.KnownExtensions{{ID: "org.example.lock", Version: 1, ValidData: []byte("ok")}} add(o) o = open("the .dkk: a known noncritical extension with invalid data", tk) o.dkkFile = dkkWith(tk, func(m map[uint64]any) { m[8] = []any{map[uint64]any{0: "org.example.note", 1: uint64(1), 2: []byte("hola")}} }) o.known = testkit.KnownExtensions{{ID: "org.example.note", Version: 1, ValidData: []byte("ok")}} add(o) o = open("the .dkk with an extension, every extension rejected", tk) o.dkkFile = must(os.ReadFile(filepath.Join(fixtureDir, "time_and_key_portable_extension.dkk"))) o.rejectAll = "not today" add(o) o = open("the .dkk, decoded, with access_material of 31 bytes", tk) o.dkk, o.material = tk.DKK, unhex(must(accessMaterial(tk.DKK)))[1:] add(o) o = open("time_only and a .dkk that is not one, which plays no part", to) o.dkkFile = []byte("DKK") add(o) o = open("time_only and a decoded .dkk of another capsule, which plays no part", tox) o.dkk = tk.DKK add(o) o = open("time_and_key without credentials, before the round time", tk) o.now = tk.Unlock.Add(-time.Hour) add(o) o = open("time_and_key with FLAGS 1 and a .dkk without magic", tk) o.dkc = set(tk.DKC, 5, 1) o.dkkFile = []byte("XKK1") add(o) o = open("format 3: the .dkk of another capsule", tk3) o.dkkFile = tk.DKK add(o) o = open("format 2: the .dkk and three identities", tk2r) o.dkkFile = tk2r.DKK o.identities = tk2r.Identities add(o) o = open("format 2: the .dkk and an identity that is not a recipient", tk2r) o.dkkFile = tk2r.DKK o.identities = []string{testkit.Stranger().String()} add(o) } // Step 9.c: the clock, and the failures of the source. { o := open("one second before the round time", to) o.now = to.Unlock.Add(-time.Second) add(o) o = open("one nanosecond before the round time", to) o.now = to.Unlock.Add(-time.Nanosecond) add(o) o = open("one nanosecond after the round time", tox) o.now = tox.Unlock.Add(time.Nanosecond) add(o) o = open("a year after the round time", tox) o.now = tox.Unlock.AddDate(1, 0, 0) add(o) for _, e := range []error{ fmt.Errorf("relay: not yet: %w", datekeys.ErrReleaseUnavailable), fmt.Errorf("relay: bad: %w", datekeys.ErrReleaseInvalid), fmt.Errorf("relay: round: %w", datekeys.ErrRoundMismatch), errors.New("connection refused"), } { o = open("the source fails: "+e.Error(), tox) o.release, o.sourceErr = nil, e add(o) } o = open("time_and_key: the source fails after the credentials", tk) o.dkkFile = tk.DKK o.release, o.sourceErr = nil, errors.New("connection refused") add(o) } // Step 10: a release of another round, supplied directly. { r := testkit.Release(2000) o := open("the valid release of round 2000 for round 1000", to) o.release = &r add(o) } // Step 11: the age header of OUTER_TIME_AGE and its STREAM. { sealed := tox.Parts.Sealed o := open("OUTER_TIME_AGE with its MAC edited", tox) o.dkc = reframe(tox, tox.Parts.Header, macEdit(sealed), tox.Parts.Payload) add(o) o = open("OUTER_TIME_AGE one byte short", tox) o.dkc = reframe(tox, tox.Parts.Header, sealed[:len(sealed)-1], tox.Parts.Payload) add(o) o = open("OUTER_TIME_AGE with a byte more", tox) o.dkc = reframe(tox, tox.Parts.Header, append(bytes.Clone(sealed), 0), tox.Parts.Payload) add(o) o = open("OUTER_TIME_AGE cut after its header", tox) hdr := must(testkit.HeaderLen(sealed)) o.dkc = reframe(tox, tox.Parts.Header, sealed[:hdr+8], tox.Parts.Payload) add(o) o = open("OUTER_TIME_AGE without its STREAM", tox) o.dkc = reframe(tox, tox.Parts.Header, sealed[:hdr+16], tox.Parts.Payload) add(o) } // Steps 12 and 13: INNER_ACCESS_AGE. { zero := strings.Repeat("A", 43) in := must(tkr.WithInnerStanzas(func(fk []byte, s []*age.Stanza) ([]*age.Stanza, error) { s[0] = &age.Stanza{Type: agewrap.StanzaX25519, Args: []string{zero}, Body: make([]byte, 32)} return s, nil })) o := open("INNER_ACCESS_AGE with a share of low order", tkr) o.dkc, o.identities = in.DKC, in.Identities add(o) in = must(tkr.WithInnerStanzas(func(fk []byte, s []*age.Stanza) ([]*age.Stanza, error) { s[0] = &age.Stanza{Type: agewrap.StanzaX25519, Args: []string{"AAAA"}, Body: make([]byte, 32)} return s, nil })) o = open("INNER_ACCESS_AGE with a share of 3 bytes", tkr) o.dkc, o.identities = in.DKC, in.Identities add(o) in = must(tkr.WithInnerStanzas(func(fk []byte, s []*age.Stanza) ([]*age.Stanza, error) { return append(s, &age.Stanza{Type: agewrap.StanzaX25519, Args: s[0].Args, Body: s[0].Body}), nil })) o = open("INNER_ACCESS_AGE with a share repeated", tkr) o.dkc, o.identities = in.DKC, in.Identities add(o) // The identity of the .dkk opens two stanzas, another identity one. in = must(tkr.WithInnerStanzas(func(fk []byte, s []*age.Stanza) ([]*age.Stanza, error) { id := must(tkr.AccessIdentity()) extra := must(testkit.FixedX25519Stanza(fk, id.Recipient(), "datekeys-dart: a second stanza")) return append(s, extra), nil })) o = open("two identities, one of them opens two stanzas", tkr) o.dkc = in.DKC o.identities = append([]string{tkr.Identities[0]}, in.Identities...) add(o) o = open("one identity opens two stanzas, given twice", tkr) o.dkc = in.DKC o.identities = []string{in.Identities[0], in.Identities[0]} add(o) } // Step 14: CONTROL_CBOR sealed again. { ctl := func(name string, f *testkit.LoadedFixture, edit func(m map[uint64]any)) opening { in := must(f.WithControl(edit)) o := open(name, f) o.dkc = in.DKC return o } add(ctl("CONTROL_CBOR an empty map", tox, func(m map[uint64]any) { clear(m) })) add(ctl("CONTROL_CBOR without key 3", tox, func(m map[uint64]any) { delete(m, 3) })) add(ctl("CONTROL_CBOR of version 7", tox, func(m map[uint64]any) { m[1] = uint64(7) })) add(ctl("CONTROL_CBOR with an unknown key", tox, func(m map[uint64]any) { m[9] = uint64(0) })) add(ctl("CONTROL_CBOR with a payload_identity of 31 bytes", tox, func(m map[uint64]any) { m[3] = m[3].([]byte)[:31] })) add(ctl("format 2: CONTROL_CBOR with padding 0", tox2, func(m map[uint64]any) { m[7] = uint64(0) })) o := ctl("CONTROL_CBOR with an unknown critical extension", tox, func(m map[uint64]any) { m[4] = []any{map[uint64]any{0: "org.example.lock", 1: uint64(1)}} }) add(o) o = ctl("CONTROL_CBOR with a known critical extension", tox, func(m map[uint64]any) { m[4] = []any{map[uint64]any{0: "org.example.lock", 1: uint64(1), 2: []byte("ok")}} }) o.known = testkit.KnownExtensions{{ID: "org.example.lock", Version: 1, ValidData: []byte("ok")}} add(o) o = open("every extension rejected: PUBLIC_HEADER and CONTROL_CBOR", tox) o.rejectAll = "not today" add(o) o = open("format 2: every extension rejected", tox2) o.rejectAll = "not today" add(o) } // Step 15. { h := must(testkit.RawHeader([16]byte(must(capsule.DecodeHeader(tox.Parts.Header)).CapsuleID), must(capsule.DecodeHeader(tox.Parts.Header)).DateKey.Compact(), 0)) o := open("PUBLIC_HEADER without its extension, sealed again", tox) o.dkc = reframe(tox, h, tox.Parts.Sealed, tox.Parts.Payload) add(o) } // Step 17, formats 1 and 2: the age header of PAYLOAD_AGE, its STREAM, // the padding and the output. { payload := tox.Parts.Payload o := open("PAYLOAD_AGE with its MAC edited", tox) o.dkc = testkit.Join(tox.Parts.Prelude, tox.Parts.Header, tox.Parts.Sealed, macEdit(payload)) add(o) o = open("PAYLOAD_AGE without its nonce", tox) hdr := must(testkit.HeaderLen(payload)) o.dkc = testkit.Join(tox.Parts.Prelude, tox.Parts.Header, tox.Parts.Sealed, payload[:hdr+5]) add(o) o = open("PAYLOAD_AGE without its STREAM", tox) o.dkc = testkit.Join(tox.Parts.Prelude, tox.Parts.Header, tox.Parts.Sealed, payload[:hdr+16]) add(o) o = open("time_only cut in its second STREAM chunk", to) o.dkc = to.DKC[:len(to.DKC)-1] add(o) o = open("time_only with its first STREAM chunk edited", to) o.dkc = set(to.DKC, len(to.DKC)-20000, to.DKC[len(to.DKC)-20000]^1) add(o) for _, name := range []string{"time_only_extensions", "format2_time_only_extensions", "format2_time_only"} { o = open(name+": the output fails", fx(name)) o.outputFail = true add(o) } l := int(tox2.PayloadLength) p := int(tox2.PaddedLength) content := plaintextOf(tox2) for _, v := range []struct { name string plain []byte }{ {"the content only", content}, {"the content and P - L - 1 zeros", append(bytes.Clone(content), zeros(p-l-1)...)}, {"the content and P - L + 1 zeros", append(bytes.Clone(content), zeros(p-l+1)...)}, {"the content, zeros and a last byte 1", append(append(bytes.Clone(content), zeros(p-l-1)...), 1)}, {"the content, a byte 2 and zeros", append(append(append(bytes.Clone(content), 2), zeros(p-l-1)...))}, {"no plaintext", nil}, {"half the content", content[:l/2]}, {"P zeros", zeros(p)}, } { add(withPlaintext("format 2: PAYLOAD_AGE holds "+v.name, tox2, v.plain)) } o = withPlaintext("format 1: PAYLOAD_AGE holds the content and a zero", tox, append(plaintextOf(tox), 0)) add(o) } // Step 17, format 3: BODY sealed again. { b := readBody3(s3) l, p := int(s3.PayloadLength), int(s3.PaddedLength) body := b.bytes() // nota.txt as CON.txta, of the same length, which breaks R6. bad := b.withHead(func(h map[uint64]any) { h[5].([]any)[0].(map[uint64]any)[0] = "CON.txta" }) for _, v := range []struct { name string plain []byte }{ {"a plaintext of 5 bytes", body[:5]}, {"a plaintext of 100 bytes", body[:100]}, {"a plaintext cut inside the file", body[:l-2]}, {"a plaintext of P + 1 bytes", testkit.Join(body, zeros(p-l+1))}, {"the last padding byte 1", testkit.Join(body, zeros(p-l-1), []byte{1})}, {"a plaintext of P - 1 bytes", testkit.Join(body, zeros(p-l-1))}, {"an invalid head and a plaintext of P + 1 bytes", testkit.Join(bad, zeros(p-l+1))}, {"an invalid head and a plaintext of P - 1 bytes", testkit.Join(bad, zeros(p-l-1))}, {"an invalid head and a plaintext of P bytes", testkit.Join(bad, zeros(p-l))}, {"BODY and its padding", testkit.Join(body, zeros(p-l))}, } { add(withPlaintext("format 3: "+v.name, s3, v.plain)) } o := withBody("format 3: a head with an unknown critical extension", s3, b.withHead(func(h map[uint64]any) { h[6] = []any{map[uint64]any{0: "org.example.lock", 1: uint64(1)}} }), nil) add(o) o = withBody("format 3: a head with an unknown critical extension and a padding byte 1", s3, b.withHead(func(h map[uint64]any) { h[6] = []any{map[uint64]any{0: "org.example.lock", 1: uint64(1)}} }), func(p []byte) []byte { p[len(p)-1] = 1; return p }) add(o) o = withBody("format 3: a head with a known critical extension", s3, b.withHead(func(h map[uint64]any) { h[6] = []any{map[uint64]any{0: "org.example.lock", 1: uint64(1), 2: []byte("ok")}} }), nil) o.known = testkit.KnownExtensions{{ID: "org.example.lock", Version: 1, ValidData: []byte("ok")}} add(o) o = withBody("format 3: a head with a known critical extension with invalid data", s3, b.withHead(func(h map[uint64]any) { h[6] = []any{map[uint64]any{0: "org.example.lock", 1: uint64(1), 2: []byte("no")}} }), nil) o.known = testkit.KnownExtensions{{ID: "org.example.lock", Version: 1, ValidData: []byte("ok")}} add(o) o = withBody("format 3: a head with a noncritical extension, rejected", s3, b.withHead(func(h map[uint64]any) { h[7] = []any{map[uint64]any{0: "org.example.tag", 1: uint64(3), 2: []byte("x")}} }), nil) o.rejectAll = "not today" add(o) nb := b nb.content = nil nh := must(cbortest.UnmarshalMap(b.head)) delete(nh, 5) nh[3] = "Sin ficheros." nb.head = must(cbortest.Marshal(nh)) nb.frame.HeadLen = uint32(len(nb.head)) add(withBody("format 3: a head without files and an empty CONTENT", s3, nb.bytes(), nil)) nb.content = []byte{0} add(withBody("format 3: a head without files and a CONTENT of 1 byte", s3, nb.bytes(), nil)) two := b.withHead(func(h map[uint64]any) { f0 := h[5].([]any)[0].(map[uint64]any) sum := sha256.Sum256([]byte("Hola")) h[5] = []any{ map[uint64]any{0: "a.txt", 1: uint64(len(b.content)), 2: uint64(0), 3: uint64(len(b.content)), 4: f0[4]}, map[uint64]any{0: "b.txt", 1: uint64(4), 2: uint64(len(b.content)), 3: uint64(len(b.content) + 4), 4: sum[:]}, } }) add(withBody("format 3: two files, the second of another content", s3, append(bytes.Clone(two), []byte("Hole")...), nil)) add(withBody("format 3: two files", s3, append(bytes.Clone(two), []byte("Hola")...), nil)) large := b large.area = append(bytes.Clone(b.area), zeros(1024)...) large.frame.AreaLen = uint32(len(large.area)) add(withBody("format 3: an area of 1536 bytes", s3, large.bytes(), nil)) one := b one.area = append([]byte{0xa0}, zeros(len(b.area)-1)...) one.frame.SecurityLen = 1 add(withBody("format 3: SECURITY_CBOR of one byte", s3, one.bytes(), nil)) // The sinks and the caller. for _, at := range []string{"begin", "create 0", "create 1", "write 0", "write 1", "close 0", "close 4", "commit"} { o = open("format 3: the sink fails at "+at, tree3) o.sinkFail = at add(o) } o = open("format 3: the caller refuses the verdicts", tree3) o.refuse = true add(o) o = open("format 3: the caller refuses the verdicts of a capsule without files", fx("format3_comment_only")) o.refuse = true add(o) o = open("format 3: the sink fails at begin, with an invalid padding", s3) o.dkc = must(s3.WithBody(body, func(p []byte) []byte { p[len(p)-1] = 1; return p })).DKC o.sinkFail = "begin" add(o) o = open("format 3, time_and_key: the sink fails at commit", tk3) o.dkkFile = tk3.DKK o.sinkFail = "commit" add(o) } var out []Case for _, o := range os_ { out = append(out, o.run()) } return out } // dkRound is the canonical dk1_ string of round of Quicknet. func dkRound(round uint64) string { j := fmt.Sprintf(`{"version":1,"network":"datekeys:quicknet:v1","round":%d}`, round) return "dk1_" + base64.RawURLEncoding.EncodeToString([]byte(j)) } // accessMaterial is the hex of the material of the .dkk dkk. func accessMaterial(dkk []byte) (string, error) { k, err := accesskey.Decode(bytes.NewReader(dkk)) if err != nil { return "", err } return hx(k.Material), nil } // --------------------------------------------------------------------------- // The inspection // inspectCases are the texts of capsule.Inspect for the mutations of // inspect_differential.json, unique texts in a table, with the result and // the step of Go checked against the file; and the views of the CLI for // headers with a public note and with unusable extensions. func inspectCases() (texts []string, results []int, views []Case) { var diff struct { Bases []struct { File string `json:"file"` } `json:"bases"` Mutations []struct { Base int `json:"base"` Edits [][]json.RawMessage `json:"edits"` Result string `json:"result"` Step int `json:"step"` } `json:"mutations"` } if err := json.Unmarshal(must(os.ReadFile(filepath.Join(fixtureDir, "..", "vectors", "inspect_differential.json"))), &diff); err != nil { panic(err) } index := map[string]int{} disagree := 0 for i, m := range diff.Mutations { base := fx(strings.TrimSuffix(diff.Bases[m.Base].File, ".dkc")).DKC dkc := applyEdits(base, m.Edits) in, err := capsule.Inspect(bytes.NewReader(dkc), capsule.InspectOptions{Registry: testkit.Registry()}) code, step := "ok", 0 if err != nil { code = datekeys.Code(err) step = in.Checks[len(in.Checks)-1].Step } if code != m.Result || step != m.Step { disagree++ fmt.Fprintf(os.Stderr, "inspect_differential.json mutation %d: Go gives %s at step %d, the file says %s at step %d\n", i, code, step, m.Result, m.Step) } if err == nil { results = append(results, -1) continue } t := err.Error() k, ok := index[t] if !ok { k = len(texts) index[t] = k texts = append(texts, t) } results = append(results, k) } fmt.Fprintf(os.Stderr, "inspect_differential.json: %d mutations, %d where Go and the file disagree, %d texts\n", len(diff.Mutations), disagree, len(texts)) // The views of the CLI: headers of time_only_extensions with a note. tox := fx("time_only_extensions") h := must(capsule.DecodeHeader(tox.Parts.Header)) note := func(data []byte) extension.Extension { return extension.Extension{ID: extension.NoteID, Version: 1, Data: data} } for _, v := range []struct { name string non []extension.Extension crit []extension.Extension reg extension.Registry }{ {"a public note", []extension.Extension{note([]byte("Cartas del viaje a Lisboa"))}, nil, extension.Standard{}}, {"a public note, no registry", []extension.Extension{note([]byte("Cartas del viaje a Lisboa"))}, nil, nil}, {"a public note with <, > and &", []extension.Extension{note([]byte(" & "))}, nil, extension.Standard{}}, {"a public note with U+2028, which breaks the rules", []extension.Extension{note([]byte("a\u2028b"))}, nil, extension.Standard{}}, {"a public note with a tab", []extension.Extension{note([]byte("a\tb"))}, nil, extension.Standard{}}, {"a public note without data", []extension.Extension{{ID: extension.NoteID, Version: 1}}, nil, extension.Standard{}}, {"a public note of version 2", []extension.Extension{{ID: extension.NoteID, Version: 2, Data: []byte("a\tb")}}, nil, extension.Standard{}}, {"a public note and another extension", []extension.Extension{note([]byte("\u00d1and\u00fa, a\u00f1o 2026 \U0001f30d")), {ID: "org.example", Version: 1, Data: []byte{1}}}, nil, extension.Standard{}}, {"a public note in the critical array", nil, []extension.Extension{note([]byte("Cartas"))}, extension.Standard{}}, {"every extension rejected", []extension.Extension{note([]byte("Cartas")), {ID: "org.example", Version: 1, Data: []byte{1}}}, nil, rejecting("not today")}, } { hh := *h hh.Noncritical, hh.Critical = v.non, v.crit hb := must(encodeHeaderRaw(&hh)) dkc := reframe(tox, hb, tox.Parts.Sealed, tox.Parts.Payload) in, err := capsule.Inspect(bytes.NewReader(dkc), capsule.InspectOptions{Registry: testkit.Registry(), Extensions: v.reg}) var b bytes.Buffer if err := inspectview.New("capsule.dkc", in, err).WriteJSON(&b); err != nil { panic(err) } c := Case{"name": v.name, "dkc": edits(tox.File, dkc), "view": b.String()} switch v.reg.(type) { case extension.Standard: c["registry"] = "standard" case rejecting: c["registry"] = "reject_all" default: c["registry"] = "none" } views = append(views, c) } return texts, results, views } // encodeHeaderRaw is capsule.EncodeHeader without the rule of the encoders // of spec §72, which this generator breaks on purpose: the header as the // reader reads it. func encodeHeaderRaw(h *capsule.Header) ([]byte, error) { m := map[uint64]any{0: capsule.HeaderTypeTag, 1: uint64(capsule.HeaderVersion), 2: h.CapsuleID[:], 3: h.DateKey.Compact(), 4: uint64(h.Policy)} ext := func(xs []extension.Extension) []any { var out []any for _, x := range xs { e := map[uint64]any{0: x.ID, 1: x.Version} if x.Data != nil { e[2] = x.Data } out = append(out, e) } return out } if len(h.Critical) > 0 { m[5] = ext(h.Critical) } if len(h.Noncritical) > 0 { m[6] = ext(h.Noncritical) } return cbortest.Marshal(m) } func applyEdits(base []byte, edits [][]json.RawMessage) []byte { var out []byte pos := 0 for _, e := range edits { var at, del int var insert string if len(e) != 3 || json.Unmarshal(e[0], &at) != nil || json.Unmarshal(e[1], &del) != nil || json.Unmarshal(e[2], &insert) != nil { panic("an edit is [at, delete, insert]") } out = append(append(out, base[pos:at]...), unhex(insert)...) pos = at + del } return append(out, base[pos:]...) } // --------------------------------------------------------------------------- // The public note // noteTexts are texts and bytes for extension.CheckNote: the cases of the // note vectors of the reference, and texts of a fixed seed built from // pieces that meet or break the rules of spec §24.1 and §29.6. func noteCases() (checks, notes, standard []Case) { pieces := []string{ "a", "Cartas", " ", "\t", "\n", "\r", "\u00e9", "\u00f1", "\u65e5\u672c", "\U0001f30d", "\u2764\ufe0f", "\ufe0f", "\u200d", "\u200c", "\u202e", "\u2066", "\u2028", "\ufeff", "\u200b", "\u00ad", "\U000e0041", "\ufffe", "\x7f", "\x1b", "\u0085", "\u061c", "\u200e", ".", "-", "~", "\U0001f3f3\ufe0f\u200d\U0001f308", "x", } add := func(name string, data []byte) { c := Case{"name": name, "data": hx(data)} checks = append(checks, outcome(c, extension.CheckNote(string(data)))) } for _, c := range []struct { name string data []byte }{ {"a note", []byte("Cartas del viaje a Lisboa")}, {"letters that are not ASCII and an emoji", []byte("\u00d1and\u00fa, a\u00f1o 2026 \U0001F30D")}, {"1024 bytes", []byte(strings.Repeat("a", 1024))}, {"1023 bytes and a letter of 2 bytes", []byte(strings.Repeat("a", 1023) + "\u00e9")}, {"1022 bytes and a letter of 2 bytes", []byte(strings.Repeat("a", 1022) + "\u00e9")}, {"an emoji with VS16", []byte("\u2764\ufe0f")}, {"no byte", []byte{}}, {"1025 bytes", []byte(strings.Repeat("a", 1025))}, {"1025 bytes of 0xff", bytes.Repeat([]byte{0xff}, 1025)}, {"a tab and a byte that is not UTF-8", []byte{9, 0xff}}, {"a tab", []byte("a\tb")}, {"a line feed", []byte("a\nb")}, {"a carriage return", []byte("a\rb")}, {"a space at the start", []byte(" a")}, {"a space at the end", []byte("a ")}, {"a bidi override", []byte("a\u202eb")}, {"a zero width space", []byte("a\u200bb")}, {"a byte order mark at the start", []byte("\ufeffHola")}, {"a byte that is not UTF-8", []byte{'a', 0xff}}, {"the UTF-8 of a lone surrogate", []byte{0xed, 0xa0, 0x80}}, {"a noncharacter", []byte("a\ufffeb")}, {"a tag", []byte("Hola\U000e0041")}, {"the flag of Scotland", []byte("\U0001f3f4\U000e0067\U000e0062\U000e0073\U000e0063\U000e0074\U000e007f")}, {"the rainbow flag", []byte("\U0001f3f3\ufe0f\u200d\U0001f308")}, {"VS16 after a letter", []byte("a\ufe0f")}, {"ZWJ at the start", []byte("\u200da")}, {"an overlong slash", []byte{0xc0, 0xaf}}, } { add(c.name, c.data) } for i := range 300 { var sb strings.Builder for range 1 + rng.IntN(8) { sb.WriteString(pick(pieces...)) } data := []byte(sb.String()) if rng.IntN(12) == 0 { data = append(data, byte(0x80+rng.IntN(0x80))) } if rng.IntN(20) == 0 { data = bytes.Repeat(data, 1+1024/max(1, len(data))) } add(fmt.Sprintf("random %d", i), data) } // extension.Note and Header.UnusableNote on noncritical arrays. n := func(v uint64, data []byte) extension.Extension { return extension.Extension{ID: extension.NoteID, Version: v, Data: data} } other := extension.Extension{ID: "org.example", Version: 1, Data: []byte{1}} for _, v := range []struct { name string exts []extension.Extension }{ {"no extension", nil}, {"a note", []extension.Extension{n(1, []byte("Cartas"))}}, {"another extension and a note", []extension.Extension{other, n(1, []byte("Hola, \u00d1and\u00fa \U0001f30d"))}}, {"a note with a tab", []extension.Extension{n(1, []byte("a\tb"))}}, {"a note without data", []extension.Extension{n(1, nil)}}, {"a note of version 2", []extension.Extension{n(2, []byte("v2"))}}, {"a note of version 2 that breaks the rules", []extension.Extension{n(2, []byte("a\tb"))}}, {"a note that is not UTF-8", []extension.Extension{n(1, []byte{0xff, 0xfe})}}, {"a note with a byte order mark", []extension.Extension{n(1, []byte("\ufeffabc"))}}, {"another extension only", []extension.Extension{other}}, } { h := capsule.Header{Noncritical: v.exts} text, ok := h.PublicNote() c := Case{"name": v.name, "extensions": extsJSON(v.exts), "usable": ok, "unusable": h.UnusableNote()} if ok { c["text"] = text } notes = append(notes, c) } // extension.Standard: CheckNoncriticalIn and CheckWrite of a note. for _, c := range checks { data := unhex(c["data"].(string)) e := extension.Extension{ID: extension.NoteID, Version: 1, Data: data} if len(data) == 0 { e.Data = nil } s := Case{"name": c["name"]} u := extension.CheckNoncriticalIn(extension.PublicHeader, []extension.Extension{e}, extension.Standard{}) s["unusable"] = unusableJSON(u) outcome(s, extension.CheckWrite(extension.Standard{}, extension.PublicHeader, extension.Noncritical, []extension.Extension{e})) standard = append(standard, s) } return checks, notes, standard } func extsJSON(xs []extension.Extension) [][]any { out := [][]any{} for _, x := range xs { var d any if x.Data != nil { d = hx(x.Data) } out = append(out, []any{x.ID, x.Version, d}) } return out } // --------------------------------------------------------------------------- // Heads // pathPieces build the segments of the paths of the heads. var pathPieces = []string{ "a", "b", "carta", "nota", ".txt", ".jpg", "fotos", "2025", "\u00e9", "e\u0301", "\u00c9", "\u00f1", "\u65e5\u672c", "\U0001f30d", "\uff5e", "\U0001f600", "~1", "ABCDEF", "CON", "con", "LPT1", "aux", ".", "..", " ", "\u00a0", "\u3000", "\u200c", "\u200d", "\ufe0f", "\u202e", "\t", ":", "?", "\u2215", "\uff0f", ".datekeys-x", "x", "K", "\u212a", "\u00df", "SS", "\u0390", } func randSegment() string { var sb strings.Builder for range 1 + rng.IntN(3) { sb.WriteString(pick(pathPieces...)) } return sb.String() } func randPath() string { var segs []string for range 1 + rng.IntN(3) { segs = append(segs, randSegment()) } return strings.Join(segs, "/") } var textPieces = []string{ "Para abrir en familia.", "Con cari\u00f1o", " ", "\t", "\n", "\r", "\u00e9", "\U0001f30d", "\u2764\ufe0f", "a\ufe0f", "\u202e", "\u2028", "\ufeff", "\u200b", "\U000e0041", "\u0085", "\x1b", "\u061c", "\u200e", "\ufffe", "Ana L\u00f3pez", "\u00a0", } func randText() string { var sb strings.Builder for range 1 + rng.IntN(4) { sb.WriteString(pick(textPieces...)) } return sb.String() } // headMap is a head of a fixed seed as a map of cbortest, and the paths of // its files. func headMap() map[uint64]any { h := map[uint64]any{0: capsule.HeadTypeTag, 1: uint64(1), 2: randBytes(32)} if rng.IntN(3) == 0 { h[3] = pick(randText(), "Para abrir en familia.\nCon cari\u00f1o, desde 2026.", "Hola") } if rng.IntN(3) == 0 { h[4] = pick(randText(), "Ana L\u00f3pez", "Ana") } if rng.IntN(5) > 0 { n := 1 + rng.IntN(5) var paths []string for range n { p := randPath() if rng.IntN(3) == 0 { p = pick("a.txt", "b.txt", "carta.txt", "fotos/2025/atardecer.jpg", "fotos/2025/playa.jpg", "notas/a.md") } paths = append(paths, p) } slices.SortFunc(paths, func(a, b string) int { return strings.Compare(a, b) }) paths = slices.Compact(paths) var files []any var end uint64 for _, p := range paths { size := uint64(pick(0, 1, 37, 3000, rng.IntN(100000))) f := map[uint64]any{0: p, 1: size, 2: end, 3: end + size, 4: randBytes(32)} if rng.IntN(2) == 0 { f[5] = pick(uint64(0), uint64(1790769600), uint64(capsule.MaxMTime), uint64(rng.IntN(1<<31))) } end += size files = append(files, f) } h[5] = files } ext := func() []any { var out []any ids := []string{"a", "org.example", "org.example.tag", "z"} for _, id := range ids { if rng.IntN(2) == 0 { e := map[uint64]any{0: id, 1: uint64(pick(1, 2))} if rng.IntN(2) == 0 { e[2] = randBytes(1 + rng.IntN(4)) } out = append(out, e) } } return out } if rng.IntN(5) == 0 { if x := ext(); len(x) > 0 { h[6] = x } } if rng.IntN(4) == 0 { if x := ext(); len(x) > 0 { h[7] = x } } return h } // headMutation changes the head map h, or returns raw bytes for a change // that cbortest maps cannot hold. func headMutation(h map[uint64]any) (string, []byte) { files, _ := h[5].([]any) file := func() map[uint64]any { return files[rng.IntN(len(files))].(map[uint64]any) } switch k := rng.IntN(34); { case k == 0: return "none", nil case k == 1: h[0] = pick("datekeys-heaD", "datekeys-head2", "datekeys-control") return "type tag", nil case k == 2: h[1] = uint64(pick(0, 2, 3)) return "version", nil case k == 3: delete(h, uint64(pick(0, 1, 2))) return "a required key removed", nil case k == 4: h[uint64(pick(8, 9, 24, 1000))] = uint64(1) return "an unknown key", nil case k == 5: h[2] = randBytes(pick(0, 31, 33)) return "a salt of another length", nil case k == 6: h[3] = pick("", "a\u202eb", "Hola\nmundo", "a\x00b") return "a comment", nil case k == 7: h[4] = pick("", "Ana\nL\u00f3pez", " Ana", "Ana\t") return "a declared author", nil case k == 8: h[3] = uint64(7) return "a comment that is not text", nil case k == 9 && len(files) > 1: i := rng.IntN(len(files) - 1) files[i], files[i+1] = files[i+1], files[i] return "two files swapped", nil case k == 10 && len(files) > 0: h[5] = append(files, files[len(files)-1]) return "the last file repeated", nil case k == 11 && len(files) > 0: file()[0] = pick("", "\x00", "a//b", "/a") return "a path at or past R1", nil case k == 12 && len(files) > 0: f := file() f[1] = f[1].(uint64) + 1 return "a size one too large", nil case k == 13 && len(files) > 0: f := file() f[2] = f[2].(uint64) + 1 return "a start one too large", nil case k == 14 && len(files) > 0: f := file() f[3] = uint64(pick(0, 8936830510563329, 9007199254740992)) return "an end out of range", nil case k == 15 && len(files) > 0: file()[5] = uint64(pick(253402300800, 1<<53-1)) return "an mtime past 9999", nil case k == 16 && len(files) > 0: file()[4] = randBytes(pick(0, 31, 33)) return "a SHA-256 of another length", nil case k == 17 && len(files) > 0: delete(file(), uint64(pick(0, 1, 2, 3, 4))) return "a key of a file removed", nil case k == 18 && len(files) > 0: file()[uint64(pick(6, 7, 100))] = uint64(0) return "an unknown key in a file", nil case k == 19: h[5] = []any{} return "an empty array of files", nil case k == 20: h[uint64(pick(6, 7))] = []any{} return "an empty extension array", nil case k == 21: x := map[uint64]any{0: "org.example", 1: uint64(1)} h[6], h[7] = []any{x}, []any{x} return "an extension in both arrays", nil case k == 22: h[6] = []any{map[uint64]any{0: "b", 1: uint64(1)}, map[uint64]any{0: "a", 1: uint64(1)}} return "extensions out of order", nil case k == 23: h[7] = []any{map[uint64]any{0: "org.example", 1: uint64(1), 2: []byte{}}} return "extension data present but empty", nil case k == 24 && len(files) > 0: file()[0] = "a/" + randSegment() return "a path in a", nil case k == 25: h[5] = []any{ map[uint64]any{0: "\uff5e", 1: uint64(0), 2: uint64(0), 3: uint64(0), 4: randBytes(32)}, map[uint64]any{0: "\U0001f600", 1: uint64(0), 2: uint64(0), 3: uint64(0), 4: randBytes(32)}, } if rng.IntN(2) == 0 { h[5] = []any{h[5].([]any)[1], h[5].([]any)[0]} return "U+1F600 before U+FF5E, UTF-16 order", nil } return "U+FF5E before U+1F600, UTF-8 order", nil case k == 26: b := must(cbortest.Marshal(h)) return "a byte more", append(b, 0) case k == 27: b := must(cbortest.Marshal(h)) return "cut short", b[:len(b)-1-rng.IntN(min(len(b)-1, 8))] case k == 28: b := must(cbortest.Marshal(h)) i := rng.IntN(len(b)) b[i] ^= byte(1 << rng.IntN(8)) return "a bit flipped", b case k == 29: // The keys out of order: keys 0 and 1 swapped. var pairs cbortest.Pairs pairs = append(pairs, uint64(1), h[1], uint64(0), h[0]) for k := uint64(2); k < 8; k++ { if v, ok := h[k]; ok { pairs = append(pairs, k, v) } } return "keys 0 and 1 swapped", must(cbortest.Marshal(pairs)) case k == 30: // The version as a byte string... layer 2. h[1] = []byte{1} return "a version that is not an integer", nil case k == 31: var tags []any for i := range 3 { tags = append(tags, map[uint64]any{0: fmt.Sprintf("f%d", i), 1: uint64(0), 2: uint64(0), 3: uint64(0), 4: randBytes(32)}) } h[5] = tags return "three files of 0 bytes", nil case k == 32: h[5] = []any{map[uint64]any{0: "A", 1: uint64(0), 2: uint64(0), 3: uint64(0), 4: randBytes(32)}, map[uint64]any{0: "a/b", 1: uint64(0), 2: uint64(0), 3: uint64(0), 4: randBytes(32)}} return "a file A and a folder a", nil default: return "none", nil } } func headCases() (decode, encode []Case) { reg := testkit.KnownExtensions{{ID: "a", Version: 1, ValidData: nil}, {ID: "org.example", Version: 2, ValidData: []byte{1}}} for range 350 { h := headMap() kind, raw := headMutation(h) b := raw if b == nil { var err error if b, err = cbortest.Marshal(h); err != nil { continue } } c := Case{"kind": kind, "hex": hx(b)} _, err := capsule.DecodeHead(b, nil) outcome(c, err) r := Case{} _, err = capsule.DecodeHead(b, reg) outcome(r, err) if r["result"] != c["result"] || r["text"] != c["text"] { c["with_registry"] = r } decode = append(decode, c) } // The limits of the texts and of R1, once each. for _, v := range []struct { kind string key uint64 val string }{ {"a comment of 16384 bytes", 3, strings.Repeat("a", 16384)}, {"a comment of 16385 bytes", 3, strings.Repeat("a", 16385)}, {"a declared author of 256 bytes", 4, strings.Repeat("a", 256)}, {"a declared author of 257 bytes", 4, strings.Repeat("a", 257)}, {"a path of 1024 bytes in 5 segments", 5, strings.Repeat(strings.Repeat("b", 204)+"/", 4) + strings.Repeat("b", 204)}, {"a path of 1025 bytes", 5, strings.Repeat("a/", 512) + "a"}, {"a path of 1024 bytes in 512 segments", 5, strings.Repeat("a/", 511) + "aa"}, } { h := map[uint64]any{0: capsule.HeadTypeTag, 1: uint64(1), 2: make([]byte, 32)} if v.key == 5 { h[5] = []any{map[uint64]any{0: v.val, 1: uint64(0), 2: uint64(0), 3: uint64(0), 4: make([]byte, 32)}} } else { h[v.key] = v.val } b := must(cbortest.Marshal(h)) c := Case{"kind": v.kind, "hex": hx(b)} _, err := capsule.DecodeHead(b, nil) decode = append(decode, outcome(c, err)) } // EncodeHead of values: the heads of the decode cases that decode, // with their extensions in another order and with errors of their own. for _, c := range decode { if c["result"] != "ok" || len(encode) >= 160 { continue } h := must(capsule.DecodeHead(unhex(c["hex"].(string)), nil)) switch rng.IntN(4) { case 0: slices.Reverse(h.Critical) slices.Reverse(h.Noncritical) case 1: h.Noncritical = append(h.Noncritical, extension.Extension{ID: "dup", Version: 1}, extension.Extension{ID: "dup", Version: 2}) case 2: h.Critical = append(h.Critical, extension.Extension{ID: "both", Version: 1}) h.Noncritical = append(h.Noncritical, extension.Extension{ID: "both", Version: 1}) } e := Case{"head": headJSON(h)} b, err := capsule.EncodeHead(h) outcome(e, err) if err == nil { e["hex"] = hx(b) } encode = append(encode, e) } return decode, encode } func headJSON(h *capsule.Head) Case { files := [][]any{} for _, f := range h.Files { var mtime any if f.HasMTime { mtime = f.MTime } files = append(files, []any{f.Path, f.Size, f.Start, f.End, hx(f.SHA256[:]), mtime}) } return Case{"salt": hx(h.Salt[:]), "comment": h.Comment, "author": h.Author, "files": files, "critical": extsJSON(h.Critical), "noncritical": extsJSON(h.Noncritical)} } // --------------------------------------------------------------------------- // Output func enc(v any) string { var b bytes.Buffer e := json.NewEncoder(&b) e.SetEscapeHTML(false) if err := e.Encode(v); err != nil { panic(err) } // No apostrophe, so that the text is a raw string of Dart. return strings.ReplaceAll(strings.TrimSuffix(b.String(), "\n"), "'", `'`) } // render is the text of one file: the object with its fields and sections, // one case per line. func render(source, description string, fields []string, values map[string]any, sections []string, cases map[string][]Case) string { var sb strings.Builder sb.WriteString("{\n") sb.WriteString(` "spec": ` + enc(specVersion) + ",\n") sb.WriteString(` "generator": "tool/open_go_vectors.go",` + "\n") sb.WriteString(` "source": ` + enc(source) + ",\n") sb.WriteString(` "description": ` + enc(description)) for _, f := range fields { sb.WriteString(",\n " + enc(f) + ": " + enc(values[f])) } for _, s := range sections { sb.WriteString(",\n " + enc(s) + ": [") for i, c := range cases[s] { if i > 0 { sb.WriteString(",") } sb.WriteString("\n " + enc(c)) } sb.WriteString("\n ]") } sb.WriteString("\n}\n") return sb.String() } func main() { testdata := flag.String("testdata", "", "the synced testdata/ of datekeys-dart") outDir := flag.String("out", "", "where the vectors go") src := flag.String("source", "", "the commit of datekeys-go") flag.Parse() fixtureDir = filepath.Join(*testdata, "fixtures") type file struct { name, description string fields []string values map[string]any sections []string cases map[string][]Case } var files []file decode, encode := headCases() files = append(files, file{name: "heads", description: "HEAD_CBOR of a fixed seed, valid and broken in each layer of spec §69.1, through capsule.DecodeHead with no registry: the result, the code and the text; with_registry when the registry of the extensions a version 1 and org.example version 2 with data 01 (testkit.KnownExtensions) gives another. encode: capsule.EncodeHead of the decoded heads, their extensions reordered, repeated or in both arrays.", sections: []string{"decode", "encode"}, cases: map[string][]Case{"decode": decode, "encode": encode}}) checks, notes, standard := noteCases() files = append(files, file{name: "notes", description: "The public note of spec §24.1: extension.CheckNote on the string of the bytes data; extension.Note and Header.UnusableNote of noncritical arrays (usable, text, unusable); and extension.Standard, in the order of check, on a note of version 1 with its bytes, absent data when empty: CheckNoncriticalIn of PUBLIC_HEADER (unusable, as [id, version, text]) and CheckWrite in its noncritical array (result and text).", sections: []string{"check", "note", "standard"}, cases: map[string][]Case{"check": checks, "note": notes, "standard": standard}}) texts, results, views := inspectCases() files = append(files, file{name: "inspect", description: "texts: the texts of the errors of capsule.Inspect, and results: for each mutation of testdata/vectors/inspect_differential.json, in its order, the index of its text in texts, -1 when steps 1 to 8 pass. views: the exact output of datekeys inspect -json (internal/inspectview) for time_only_extensions with another PUBLIC_HEADER, with the registry standard (extension.Standard), none, or reject_all, which knows every extension and rejects all data with not today.", fields: []string{"texts", "results"}, values: map[string]any{"texts": texts, "results": results}, sections: []string{"views"}, cases: map[string][]Case{"views": views}}) fixtureOpenings := fixtureCases() steps := stepCases() files = append(files, file{name: "cases", description: "capsule.Open of a .dkc, as edits of a fixture, given the release (null: none), or a source that fails with source_error (its text and its code, none for a plain error); the clock now; the registry of profiles, empty when registry is empty; the extensions known at (id, version) with valid data valid_data (testkit.KnownExtensions), or reject_all, a registry that knows every extension and rejects all data with that text; the age identities, the .dkk decoded before (dkk, with its material replaced by dkk_material) or the .dkk still encoded (dkk_file); a sink that fails at sink_fail (begin, create i, write i, close i or commit, with the text no and the call); an output that fails with disk full when output_fail; accept, refuse to refuse every verdict with not trusted; and author_keys, the author keys saved with their labels. The outcome: result (ok, refused, or the code), text, the checks as [step, name, ok, detail, code], the release requests, the state of the sink, the content (length and SHA-256) or the files ([path, size, SHA-256 of what the sink received]) of a capsule that opens, the verdicts of a capsule of format 3 that opens (signature, seal, lines, author_key and author_label of alg 1, sealed_at), and the unusable extensions of each object as [id, version, text].", sections: []string{"fixtures", "steps"}, cases: map[string][]Case{"fixtures": fixtureOpenings, "steps": steps}}) writeDart(*outDir, *src, files[0].cases, files[1].cases, views, texts, results, fixtureOpenings, steps) for _, f := range files { text := render(*src, f.description, f.fields, f.values, f.sections, f.cases) if err := os.WriteFile(filepath.Join(*outDir, "open_"+f.name+".json"), []byte(text), 0o644); err != nil { panic(err) } var counts []string for _, s := range f.sections { counts = append(counts, fmt.Sprintf("%s %d", s, len(f.cases[s]))) } fmt.Fprintf(os.Stderr, "open_%s.json: %d bytes, %s\n", f.name, len(text), strings.Join(counts, ", ")) } } // embedded are the fixtures whose bytes test/vectors/open_vectors.g.dart // holds, for the tests compiled to JavaScript: the small ones of each format // and policy. var embedded = []string{"time_only_extensions", "time_and_key_portable", "time_and_key_recipients", "format2_time_only_extensions", "format2_time_and_key_recipients", "format3_single", "format3_time_and_key_portable"} // writeDart writes test/vectors/open_vectors.g.dart: the embedded fixtures, // the cases of open_cases.json on them, every second one of the steps, every eighth head, every second // note, the views, and every sixth mutation of inspect_differential.json // of the embedded fixtures with its text. func writeDart(dir, source string, heads, notes map[string][]Case, views []Case, texts []string, results []int, fixtures, steps []Case) { bases := map[string]bool{} fx := map[string]string{} records := map[string]Case{} for _, name := range embedded { f := testkitFixture(name) bases[f.File] = true fx[f.File] = hx(f.DKC) if f.DKK != nil { fx[f.AccessKeyFile] = hx(f.DKK) } rec := Case{"format": f.Format, "release": Case{"round": f.Published.Round, "signature": hx(f.Published.Signature)}, "unlock_at": f.UnlockAt, "payload_identity": f.PayloadIdentity, "payload_length": f.PayloadLength} if f.AccessKeyFile != "" { rec["access_key_file"] = f.AccessKeyFile } if len(f.Identities) > 0 { rec["identities"] = f.Identities } records[f.File] = rec } sub := func(cs []Case, keep func(i int, c Case) bool) []Case { var out []Case for i, c := range cs { if keep(i, c) { out = append(out, c) } } return out } onBase := func(_ int, c Case) bool { return bases[c["dkc"].(Case)["base"].(string)] } stepsOnBase := sub(steps, onBase) cases := map[string][]Case{"fixtures": sub(fixtures, onBase), "steps": sub(stepsOnBase, func(i int, _ Case) bool { return i%2 == 0 })} h := map[string][]Case{ "decode": sub(heads["decode"], func(i int, _ Case) bool { return i%8 == 0 }), "encode": sub(heads["encode"], func(i int, _ Case) bool { return i%4 == 0 }), } n := map[string][]Case{ "check": sub(notes["check"], func(i int, _ Case) bool { return i%2 == 0 }), "note": notes["note"], "standard": sub(notes["standard"], func(i int, _ Case) bool { return i%2 == 0 }), } // The mutations of inspect_differential.json of the embedded fixtures. var diff struct { Bases []struct { File string `json:"file"` } `json:"bases"` Mutations []struct { Base int `json:"base"` Edits json.RawMessage `json:"edits"` Result string `json:"result"` Step int `json:"step"` } `json:"mutations"` } if err := json.Unmarshal(must(os.ReadFile(filepath.Join(fixtureDir, "..", "vectors", "inspect_differential.json"))), &diff); err != nil { panic(err) } var mutations []Case for i, m := range diff.Mutations { file := diff.Bases[m.Base].File if !bases[file] || i%6 != 0 { continue } c := Case{"index": i, "base": file, "edits": m.Edits, "result": m.Result} if results[i] >= 0 { c["step"], c["text"] = m.Step, texts[results[i]] } mutations = append(mutations, c) } var b strings.Builder b.WriteString("// Generated by tool/open_go_vectors.go: the fixtures that the cases below\n") b.WriteString("// edit, and a part of test/vectors/open_*.json, for the tests that also run\n") b.WriteString("// compiled to JavaScript, where no file can be read. Do not edit.\n\n") constant := func(name, doc, text string) { if strings.Contains(text, "'''") { panic("a raw string of Dart cannot hold '''") } fmt.Fprintf(&b, "/// %s\nconst %s = r'''\n%s''';\n\n", doc, name, text) } constant("openFixturesJson", "The bytes of the fixtures that the cases edit, in hexadecimal.", enc(fx)+"\n") constant("openRecordsJson", "What the records of those fixtures say of them: the format, the release, the round time, I_PAYLOAD, L, and their credentials.", enc(records)+"\n") constant("openCasesJson", "Part of test/vectors/open_cases.json: its cases on those fixtures.", render(source, "Part of open_cases.json.", nil, nil, []string{"fixtures", "steps"}, cases)) constant("openHeadsJson", "Part of test/vectors/open_heads.json.", render(source, "Part of open_heads.json.", nil, nil, []string{"decode", "encode"}, h)) constant("openNotesJson", "Part of test/vectors/open_notes.json.", render(source, "Part of open_notes.json.", nil, nil, []string{"check", "note", "standard"}, n)) constant("openInspectJson", "The views of test/vectors/open_inspect.json, and every sixth mutation of testdata/vectors/inspect_differential.json of those fixtures, with the text of Go.", render(source, "Part of open_inspect.json.", nil, nil, []string{"views", "mutations"}, map[string][]Case{"views": views, "mutations": mutations})) out := strings.TrimSuffix(b.String(), "\n") if err := os.WriteFile(filepath.Join(dir, "open_vectors.g.dart"), []byte(out), 0o644); err != nil { panic(err) } fmt.Fprintf(os.Stderr, "open_vectors.g.dart: %d bytes, %d cases, %d mutations\n", len(out), len(cases["fixtures"])+len(cases["steps"]), len(mutations)) } func testkitFixture(name string) *testkit.LoadedFixture { return fx(name) }