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.
1807 lines
67 KiB
1807 lines
67 KiB
//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("<b> & </b>"))}, 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) }
|