package provider_test import ( "bytes" "encoding/hex" "errors" "os" "testing" datekeys "g.activething.com/go/DateKeys" "g.activething.com/go/DateKeys/internal/testkit" "g.activething.com/go/DateKeys/profile" "g.activething.com/go/DateKeys/provider" ) // Spec v0.15, ยง47.1: every vector of release.json, replayed from the file // alone, gets its result and its text: decoding, then step 10 against the // pinned profile and the round of the vector. func TestReleaseVectors(t *testing.T) { var f testkit.ReleaseVectorFile if err := testkit.ReadJSON("../testdata/vectors/release.json", &f); err != nil { t.Fatal(err) } p := profile.Quicknet() check := func(t *testing.T, in []byte, v testkit.ReleaseVector) { r, err := provider.ParseRelease(in) if err == nil { err = provider.Verify(p, provider.Condition{Round: v.Round}, r) } if got := testkit.Result(err); got != v.Result { t.Fatalf("%s, want %s (%v)", got, v.Result, err) } if err != nil && err.Error() != v.Text { t.Fatalf("text %q, want %q", err.Error(), v.Text) } } for _, v := range f.Objects { t.Run(v.Name, func(t *testing.T) { b, err := hex.DecodeString(v.Encoding) if err != nil { t.Fatal(err) } check(t, b, v) if v.Result == testkit.ResultOK { // A valid object is the deterministic encoding of its value. r, _ := provider.DecodeRelease(b) if again, err := provider.EncodeRelease(r); err != nil || !bytes.Equal(again, b) { t.Fatalf("encoding again: %x, %v", again, err) } } }) } for _, v := range f.JSON { t.Run("JSON "+v.Name, func(t *testing.T) { check(t, []byte(v.Input), v) }) } if len(f.Objects) < 30 || len(f.JSON) < 10 { t.Fatalf("%d objects and %d JSON inputs", len(f.Objects), len(f.JSON)) } } // The local archive of testdata supplies the release object of each round it // holds, and nothing for a round it lacks or outside it, for another chain, // or when its length is not the one its header announces. func TestArchive(t *testing.T) { b, err := os.ReadFile("../testdata/releases/" + testkit.ArchiveFile) if err != nil { t.Fatal(err) } if !provider.IsArchive(b) || provider.IsArchive(mustRead(t, "../testdata/releases/1000.cbor")) { t.Fatal("IsArchive") } p := profile.Quicknet() a := provider.NewArchive(bytes.NewReader(b), int64(len(b))) for _, round := range []uint64{1000, 1001, 1004} { got, err := a.Supply(p, provider.Condition{Round: round}) if err != nil || !bytes.Equal(got, mustRead(t, "../testdata/releases/"+testkit.ReleaseFileName(round))) { t.Fatalf("round %d: %x, %v", round, got, err) } } for _, round := range []uint64{999, 1002, 1003, 1005, 2000} { if _, err := a.Supply(p, provider.Condition{Round: round}); !onlyUnavailable(err) { t.Fatalf("round %d: %v", round, err) } } other := p.Clone() other.ChainHash[0] ^= 1 short := provider.NewArchive(bytes.NewReader(b[:len(b)-1]), int64(len(b)-1)) notArchive := mustRead(t, "../testdata/releases/1000.cbor") for name, c := range map[string]struct { a *provider.Archive p *profile.Profile }{ "another chain": {a, other}, "a byte missing": {short, p}, "a release object": {provider.NewArchive(bytes.NewReader(notArchive), int64(len(notArchive))), p}, "no byte": {provider.NewArchive(bytes.NewReader(nil), 0), p}, } { if _, err := c.a.Supply(c.p, provider.Condition{Round: 1000}); !onlyUnavailable(err) { t.Fatalf("%s: %v", name, err) } } } // onlyUnavailable reports whether err wraps ErrReleaseUnavailable and no // other normative code. func onlyUnavailable(err error) bool { for _, e := range datekeys.All() { if errors.Is(err, e) != (e == datekeys.ErrReleaseUnavailable) { return false } } return true } func mustRead(t *testing.T, path string) []byte { t.Helper() b, err := os.ReadFile(path) if err != nil { t.Fatal(err) } return b } // EncodeRelease never writes an object that DecodeRelease rejects, and a // release of the profile carries its chain hash. func TestEncodeRelease(t *testing.T) { p := profile.Quicknet() r := testkit.Release(1000) for _, bad := range []provider.Release{ r, {Round: 0, Signature: r.Signature, ChainHash: p.ChainHash[:]}, {Round: 1 << 53, Signature: r.Signature, ChainHash: p.ChainHash[:]}, {Round: 1000, ChainHash: p.ChainHash[:]}, {Round: 1000, Signature: make([]byte, 97), ChainHash: p.ChainHash[:]}, } { if _, err := provider.EncodeRelease(bad); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) { t.Fatalf("%+v: %v", bad, err) } } b, err := provider.NewReleaseObject(p, r) if err != nil || len(b) != 111 { t.Fatalf("%d bytes, %v", len(b), err) } got, err := provider.DecodeRelease(b) if err != nil || got.Round != 1000 || !bytes.Equal(got.Signature, r.Signature) || !bytes.Equal(got.ChainHash, p.ChainHash[:]) { t.Fatalf("%+v, %v", got, err) } if _, err := provider.EncodeArchiveHeader(p.ChainHash[:], 0, 1); err == nil { t.Fatal("archive from round 0") } } // FuzzDecodeRelease: whatever the input, DecodeRelease returns exactly one // normative code, and what it accepts encodes again to the same bytes. func FuzzDecodeRelease(f *testing.F) { f.Add(mustReadF(f, "../testdata/releases/1000.cbor")) f.Add([]byte(`{"round":1000,"signature":"00"}`)) f.Fuzz(func(t *testing.T, b []byte) { r, err := provider.ParseRelease(b) if err != nil { n := 0 for _, e := range datekeys.All() { if errors.Is(err, e) { n++ } } if n != 1 { t.Fatalf("%d codes: %v", n, err) } return } if r.ChainHash == nil { return } if again, err := provider.EncodeRelease(r); err != nil || !bytes.Equal(again, b) { t.Fatalf("encoding again: %x, %v", again, err) } }) } func mustReadF(f *testing.F, path string) []byte { b, err := os.ReadFile(path) if err != nil { f.Fatal(err) } return b }