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

391 lines
13 KiB

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")
}
}

Powered by TurnKey Linux.