package testkit_test import ( "bytes" "encoding/hex" "encoding/json" "os" "path/filepath" "reflect" "slices" "testing" "filippo.io/age" "g.activething.com/go/DateKeys/internal/pathrule" "g.activething.com/go/DateKeys/internal/testkit" "g.activething.com/go/DateKeys/profile" ) func TestEdits(t *testing.T) { base := []byte("0123456789") long := "0123456789abcdefghijklmnopqrstuvwxyz" for _, tc := range []struct { base string out string want string // JSON of Splice(base, out) }{ {"", "0123456789", `[]`}, {"", "01X3456789", `[[2,1,"58"]]`}, {"", "012", `[[3,7,""]]`}, {"", "", `[[0,10,""]]`}, {"", "01234567890", `[[10,0,"30"]]`}, {"", "0123XY456789", `[[4,0,"5859"]]`}, // Runs 16 or more equal bytes apart are edits of their own; closer // ones are merged. {long, "0X23456789abcdefghijklmnopqrstuvwxYz", `[[1,1,"58"],[34,1,"59"]]`}, {long, "0X23456789aXcdefghijklmnopqrstuvwxyz", `[[1,11,"5832333435363738396158"]]`}, // A change and a cut: an edit and a deletion. {long, "0X23456789abcdefghijklmnopqrst", `[[1,1,"58"],[30,6,""]]`}, // A change and more bytes: an edit and an insertion. {long, "0X23456789abcdefghijklmnopqrstuvwxyz!!", `[[1,1,"58"],[36,0,"2121"]]`}, } { base := base if tc.base != "" { base = []byte(tc.base) } edits := testkit.Splice(base, []byte(tc.out)) if edits == nil { edits = []testkit.Edit{} } b, err := json.Marshal(edits) if err != nil || string(b) != tc.want { t.Errorf("Splice(%q) = %s, %v; want %s", tc.out, b, err, tc.want) } var back []testkit.Edit if err := json.Unmarshal(b, &back); err != nil { t.Fatal(err) } out, err := testkit.ApplyEdits(base, back) if err != nil || string(out) != tc.out { t.Errorf("ApplyEdits(%s) = %q, %v; want %q", b, out, err, tc.out) } } // Several edits refer to offsets of the base. out, err := testkit.ApplyEdits(base, []testkit.Edit{{At: 1, Delete: 1, Insert: []byte("ab")}, {At: 5, Delete: 2}, {At: 10, Insert: []byte("!")}}) if err != nil || string(out) != "0ab234789!" { t.Fatalf("got %q, %v", out, err) } for _, bad := range [][]testkit.Edit{ {{At: 5, Delete: 1}, {At: 2, Delete: 1}}, // out of order {{At: 2, Delete: 3}, {At: 4, Delete: 1}}, // overlapping {{At: 9, Delete: 2}}, // beyond the base {{At: -1}}, } { if _, err := testkit.ApplyEdits(base, bad); err == nil { t.Errorf("ApplyEdits accepted %v", bad) } } for _, bad := range []string{`[1,2]`, `[1,2,"zz"]`, `["1",2,""]`, `{}`} { var e testkit.Edit if err := json.Unmarshal([]byte(bad), &e); err == nil { t.Errorf("Edit accepted %s", bad) } } } // The re-encodings of the point mutations (spec §12.2) change only what their // names say, and x + p fits for XPlusPRound only among the known rounds. func TestPointReencodings(t *testing.T) { sig := testkit.Release(testkit.XPlusPRound).Signature xp, err := testkit.AddModulus(sig, 0) if err != nil { t.Fatal(err) } if xp[0]&0xe0 != sig[0]&0xe0 || bytes.Equal(xp, sig) || !bytes.Equal(testkit.ReduceCoordinate(xp, 0), sig) { t.Fatalf("x + p of %x is %x", sig, xp) } for _, r := range testkit.Rounds { if _, err := testkit.AddModulus(testkit.Release(r).Signature, 0); (err == nil) != (r == testkit.XPlusPRound) { t.Errorf("round %d: x + p fits: %v", r, err == nil) } } key := profile.Quicknet().PublicKey for _, at := range []int{0, testkit.CoordinateLen} { k, err := testkit.AddModulus(key, at) if err != nil { t.Fatalf("coordinate at %d of the Quicknet key: %v", at, err) } other := testkit.CoordinateLen - at if !bytes.Equal(k[other:other+testkit.CoordinateLen], key[other:other+testkit.CoordinateLen]) || !bytes.Equal(testkit.ReduceCoordinate(k, at), key) { t.Fatalf("coordinate at %d: %x", at, k) } } for _, at := range []int{-48, 1, testkit.CoordinateLen} { if _, err := testkit.AddModulus(sig, at); err == nil { t.Errorf("offset %d of a G1 point accepted", at) } } if n := testkit.Negated(sig); n[0]^sig[0] != testkit.FlagSort || !bytes.Equal(n[1:], sig[1:]) || !bytes.Equal(testkit.Negated(n), sig) { t.Fatalf("negated %x", n) } if i := testkit.InfinityWithPayload(sig); i[0] != 0xc0|sig[0]&0x1f || !bytes.Equal(i[1:], sig[1:]) { t.Fatalf("infinity with payload %x", i) } if i := testkit.Infinity(96); len(i) != 96 || i[0] != 0xc0 || !bytes.Equal(i[1:], make([]byte, 95)) { t.Fatalf("infinity %x", i) } } // The schema vectors of testdata/vectors/cbor.json replay: the decoder of each // schema gives exactly the recorded result. func TestSchemaVectors(t *testing.T) { var f testkit.CBORVectorFile if err := testkit.ReadJSON("../../testdata/vectors/cbor.json", &f); err != nil { t.Fatal(err) } blocks := map[string]int{} for _, v := range f.Schemas { b, err := hex.DecodeString(v.Hex) if err != nil { t.Fatal(err) } if got := testkit.Result(testkit.DecodeSchema(v.Schema, b, v.Format)); got != v.Result { t.Errorf("%s %q: got %s, want %s", v.Block, v.Name, got, v.Result) } blocks[v.Block]++ } for _, b := range []string{testkit.SchemaProfile, testkit.SchemaHeader, testkit.SchemaControl, testkit.SchemaDKKBody, "verification_metadata", "extension"} { if blocks[b] < 5 { t.Errorf("block %s has %d vectors", b, blocks[b]) } } if err := testkit.DecodeSchema("nope", nil, 0); err == nil { t.Error("unknown schema accepted") } } // The vectors of format 3 replay: each path, tree, key, head and security // area of the committed files gets its recorded result from the // implementation, with the tables the files name (spec §29.3 to §29.7). func TestFormat3VectorFiles(t *testing.T) { const dir = "../../testdata/vectors/" var paths testkit.PathVectorFile if err := testkit.ReadJSON(dir+"paths.json", &paths); err != nil { t.Fatal(err) } var fold testkit.PathFoldFile if err := testkit.ReadJSON(dir+"path_fold.json", &fold); err != nil { t.Fatal(err) } for _, f := range []struct{ version, digest string }{{paths.UnicodeVersion, paths.TablesDigest}, {fold.UnicodeVersion, fold.TablesDigest}} { if f.version != pathrule.UnicodeVersion || f.digest != pathrule.TablesDigest { t.Fatalf("vectors of Unicode %s and tables %s", f.version, f.digest) } } for _, v := range paths.Paths { if got := testkit.PathResult(v.Path); got != v.Result { t.Errorf("path %q: %q, want %q", v.Name, got, v.Result) } } for _, v := range paths.Trees { b, err := testkit.TreeHead(v.Paths) if err != nil { t.Fatal(err) } if got, detail := testkit.HeadResult(b); got != v.Result || detail != v.Detail { t.Errorf("tree %q: %s %q, want %s %q", v.Name, got, detail, v.Result, v.Detail) } } for _, v := range fold.Keys { if pathrule.NFD(v.Segment) != v.NFD || pathrule.Key(v.Segment) != v.Key { t.Errorf("key %q: NFD %+q, key %+q", v.Name, pathrule.NFD(v.Segment), pathrule.Key(v.Segment)) } } var heads testkit.HeadSchemaFile if err := testkit.ReadJSON(dir+"head_schema.json", &heads); err != nil { t.Fatal(err) } for _, v := range heads.Heads { b, err := hex.DecodeString(v.Hex) if err != nil { t.Fatal(err) } if got, detail := testkit.HeadResult(b); got != v.Result || detail != v.Detail { t.Errorf("head %q: %s %q, want %s %q", v.Name, got, detail, v.Result, v.Detail) } } var security testkit.SecurityVectorFile if err := testkit.ReadJSON(dir+"security.json", &security); err != nil { t.Fatal(err) } c, err := testkit.SecurityContextOf(security.Context) if err != nil { t.Fatal(err) } verdicts := map[string]bool{} for _, v := range security.Vectors { b, err := hex.DecodeString(v.Hex) if err != nil { t.Fatal(err) } if sig, seal, lines := testkit.SecurityResultIn(b, c); sig != v.Signature || seal != v.Seal || !slices.Equal(lines, v.Lines) { t.Errorf("security %q: %s %s %q, want %s %s %q", v.Name, sig, seal, lines, v.Signature, v.Seal, v.Lines) } verdicts[v.Signature], verdicts[v.Seal] = true, true } // Every verdict that needs no certificate has a vector here; those of // alg 2 and seal_type 2 are in security_cms.json. for _, v := range []string{"X", "F0", "F1", "F2", "F4", "S0", "S1", "S2"} { if !verdicts[v] { t.Errorf("no vector gives %s", v) } } } // The committed vector files are what the generators compute now. func TestVectorFilesAreCurrent(t *testing.T) { cbor, err := testkit.CBORVectors() if err != nil { t.Fatal(err) } padding, err := testkit.PaddingVectors() if err != nil { t.Fatal(err) } paths, err := testkit.PathVectors() if err != nil { t.Fatal(err) } fold, err := testkit.PathFoldVectors() if err != nil { t.Fatal(err) } heads, err := testkit.HeadSchemaVectors() if err != nil { t.Fatal(err) } security, err := testkit.SecurityVectors() if err != nil { t.Fatal(err) } strict, err := testkit.Ed25519StrictVectors() if err != nil { t.Fatal(err) } note, err := testkit.NoteVectors() if err != nil { t.Fatal(err) } words, err := testkit.WordKeyVectors() if err != nil { t.Fatal(err) } resolved, err := testkit.ResolvedIPVectors() if err != nil { t.Fatal(err) } steps, err := testkit.TlockStepVectors() if err != nil { t.Fatal(err) } releases, err := testkit.ReleaseVectors() if err != nil { t.Fatal(err) } files, err := testkit.ReleaseFiles() if err != nil { t.Fatal(err) } for name, want := range files { got, err := os.ReadFile("../../testdata/releases/" + name) if err != nil || !bytes.Equal(got, want) { t.Errorf("testdata/releases/%s is stale: run go run ./internal/testkit/genfixtures -out testdata", name) } } // The generator never deletes: a file it no longer writes is stale. entries, err := os.ReadDir("../../testdata/releases") if err != nil { t.Fatal(err) } for _, e := range entries { if _, ok := files[e.Name()]; !ok { t.Errorf("testdata/releases/%s is not generated: remove it", e.Name()) } } for _, v := range []struct { file string want any got any }{ {"cbor.json", cbor, &testkit.CBORVectorFile{}}, {"padding.json", padding, &testkit.PaddingVectorFile{}}, {"paths.json", paths, &testkit.PathVectorFile{}}, {"path_fold.json", fold, &testkit.PathFoldFile{}}, {"head_schema.json", heads, &testkit.HeadSchemaFile{}}, {"security.json", security, &testkit.SecurityVectorFile{}}, {"ed25519_strict.json", strict, &testkit.Ed25519StrictFile{}}, {"note.json", note, &testkit.NoteVectorFile{}}, {"wordkey.json", words, &testkit.WordKeyVectorFile{}}, {"resolved_ip.json", resolved, &testkit.ResolvedIPVectorFile{}}, {"tlock_steps.json", steps, &testkit.TlockStepsFile{}}, {"release.json", releases, &testkit.ReleaseVectorFile{}}, } { if err := testkit.ReadJSON("../../testdata/vectors/"+v.file, v.got); err != nil { t.Fatal(err) } a, _ := json.Marshal(v.want) b, _ := json.Marshal(v.got) if !bytes.Equal(a, b) { t.Errorf("testdata/vectors/%s is stale: run go run ./internal/testkit/genfixtures -out testdata", v.file) } } } // The tools that seal age files again with known keys reproduce the official // fixtures byte for byte when nothing is edited: their STREAM, header MAC and // PRELUDE are those of age and of the reference, so that the format 2 // mutations derived with them differ only where they are edited. func TestResealReproducesFixtures(t *testing.T) { const dir = "../../testdata/fixtures" to, err := testkit.LoadFixture(dir, "format2_time_only") if err != nil { t.Fatal(err) } same, err := to.WithControl(func(map[uint64]any) {}) if err != nil || !bytes.Equal(same.DKC, to.DKC) { t.Fatalf("WithControl without an edit changes the capsule: %v", err) } content, err := os.ReadFile(filepath.Join(dir, to.PlaintextFile)) if err != nil { t.Fatal(err) } padded := append(content, make([]byte, to.PaddedLength-to.PayloadLength)...) same, err = to.WithPayloadPlaintext(padded) if err != nil || !bytes.Equal(same.DKC, to.DKC) { t.Fatalf("WithPayloadPlaintext of the same plaintext changes the capsule: %v", err) } tk, err := testkit.LoadFixture(dir, "format2_time_and_key_portable") if err != nil { t.Fatal(err) } same, err = tk.WithInnerStanzas(func(_ []byte, s []*age.Stanza) ([]*age.Stanza, error) { return s, nil }) if err != nil || !bytes.Equal(same.DKC, tk.DKC) || same.DKK != nil || len(same.Identities) != 1 { t.Fatalf("WithInnerStanzas without an edit changes the capsule: %v", err) } if testkit.StreamLen(0) != 16 || testkit.StreamLen(65536) != 65552 || testkit.StreamLen(65537) != 65569 { t.Fatal("StreamLen") } } // FixedX25519Stanza builds the stanza age builds, with a chosen ephemeral: // the identity of the recipient unwraps the file key, and the same seed gives // the same stanza. func TestFixedX25519Stanza(t *testing.T) { id, _ := age.GenerateX25519Identity() fk := bytes.Repeat([]byte{7}, 16) s, err := testkit.FixedX25519Stanza(fk, id.Recipient(), "seed") if err != nil { t.Fatal(err) } got, err := id.Unwrap([]*age.Stanza{s}) if err != nil || !bytes.Equal(got, fk) { t.Fatalf("unwrap: %v", err) } again, _ := testkit.FixedX25519Stanza(fk, id.Recipient(), "seed") other, _ := testkit.FixedX25519Stanza(fk, id.Recipient(), "other seed") if !reflect.DeepEqual(s, again) || reflect.DeepEqual(s, other) { t.Fatal("the stanza does not depend on the seed alone") } }