diff --git a/capsule/inhand_test.go b/capsule/inhand_test.go new file mode 100644 index 0000000..aa9756c --- /dev/null +++ b/capsule/inhand_test.go @@ -0,0 +1,119 @@ +package capsule_test + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + datekeys "g.activething.com/go/DateKeys" + "g.activething.com/go/DateKeys/capsule" + "g.activething.com/go/DateKeys/internal/testkit" + "g.activething.com/go/DateKeys/profile" + "g.activething.com/go/DateKeys/provider" +) + +// supplierFunc adapts a function to provider.Supplier. +type supplierFunc func(*profile.Profile, provider.Condition) ([]byte, error) + +func (f supplierFunc) Supply(p *profile.Profile, c provider.Condition) ([]byte, error) { + return f(p, c) +} + +func readRelease(t *testing.T, round uint64) []byte { + t.Helper() + b, err := os.ReadFile(filepath.Join("../testdata/releases", testkit.ReleaseFileName(round))) + if err != nil { + t.Fatal(err) + } + return b +} + +// Spec v0.15, §63 step 9.c: a release in hand is not compared with the +// clock. The capsule opens with a clock before the round time, Opened says +// the clock is behind, and Opened.Release encodes to the official .dkr. +func TestReleaseInHand(t *testing.T) { + f := loadFixture(t, "format3_time_and_key_portable") + dkr := readRelease(t, 1000) + for _, tc := range []struct { + name string + now time.Time + behind bool + }{ + {"after the round time", f.unlock(t), false}, + {"a clock one nanosecond behind", f.unlock(t).Add(-1), true}, + {"a clock years behind", testkit.Genesis(), true}, + } { + o := f.openOptions(t) + o.Source, o.Release, o.Now = nil, provider.Encoded(dkr), testkit.Fixed(tc.now) + out, err := capsule.Open(context.Background(), nil, bytes.NewReader(f.dkc), o) + if err != nil { + t.Fatalf("%s: %v", tc.name, err) + } + if out.ClockBehind != tc.behind { + t.Fatalf("%s: ClockBehind %v", tc.name, out.ClockBehind) + } + saved, err := provider.EncodeRelease(out.Release) + if err != nil || !bytes.Equal(saved, dkr) { + t.Fatalf("%s: the release saved again is %x, %v", tc.name, saved, err) + } + } +} + +// A release in hand keeps the order of step 9: the credentials first (9.a, +// 9.b), with no request; then the release, whose failure to be supplied is +// ErrReleaseUnavailable alone at step 9; then step 10 with its codes, the +// layers of the object first. +func TestReleaseInHandErrors(t *testing.T) { + f := loadFixture(t, "format3_time_and_key_portable") + other := profile.Quicknet().ChainHash + other[5] ^= 1 + otherChain, err := provider.EncodeRelease(provider.Release{Round: 1001, Signature: testkit.Release(1001).Signature, ChainHash: other[:]}) + if err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + name string + supply func() ([]byte, error) + noAccess bool + want *datekeys.Error + step int + calls int + }{ + {"no credential, before any supply", func() ([]byte, error) { return readRelease(t, 1000), nil }, true, datekeys.ErrAccessRequired, 9, 0}, + {"nothing supplied", func() ([]byte, error) { return nil, fmt.Errorf("none: %w", datekeys.ErrReleaseUnavailable) }, false, datekeys.ErrReleaseUnavailable, 9, 1}, + {"a supplier failing with another code", func() ([]byte, error) { return nil, fmt.Errorf("odd: %w", datekeys.ErrIntegrity) }, false, datekeys.ErrReleaseUnavailable, 9, 1}, + {"an empty object", func() ([]byte, error) { return nil, nil }, false, datekeys.ErrNonCanonicalCBOR, 10, 1}, + {"another chain and another round", func() ([]byte, error) { return otherChain, nil }, false, datekeys.ErrProfileMismatch, 10, 1}, + {"another round", func() ([]byte, error) { return readRelease(t, 1001), nil }, false, datekeys.ErrRoundMismatch, 10, 1}, + {"drand's JSON of another round", func() ([]byte, error) { + return []byte(`{"round":1001,"signature":"00"}`), nil + }, false, datekeys.ErrRoundMismatch, 10, 1}, + } { + calls := 0 + o := f.openOptions(t) + o.Source, o.Now = nil, testkit.Fixed(testkit.Genesis()) + o.Release = supplierFunc(func(*profile.Profile, provider.Condition) ([]byte, error) { calls++; return tc.supply() }) + if tc.noAccess { + o.AccessKey = nil + } + step, err := openAt(t, f.dkc, o) + if !onlyCode(err, tc.want) || step != tc.step || calls != tc.calls { + t.Errorf("%s: got %v at step %d after %d supplies, want %v at step %d after %d", tc.name, err, step, calls, tc.want, tc.step, tc.calls) + } + } + + o := f.openOptions(t) + o.Release = provider.Encoded(readRelease(t, 1000)) + if _, err := capsule.Open(context.Background(), nil, bytes.NewReader(f.dkc), o); err == nil || datekeys.Code(err) != "" { + t.Fatalf("Source and Release together: %v", err) + } + o.Source, o.Release = nil, nil + if _, err := capsule.Open(context.Background(), nil, bytes.NewReader(f.dkc), o); err == nil || errors.Is(err, datekeys.ErrReleaseUnavailable) { + t.Fatalf("no source: %v", err) + } +} diff --git a/capsule/mutation_test.go b/capsule/mutation_test.go index 4246124..ff79917 100644 --- a/capsule/mutation_test.go +++ b/capsule/mutation_test.go @@ -96,7 +96,7 @@ func TestExportedMutationCorpus(t *testing.T) { if err != nil { t.Fatal(err) } - if !bytes.Equal(got.DKC, want.DKC) || !bytes.Equal(got.DKK, want.DKK) { + if !bytes.Equal(got.DKC, want.DKC) || !bytes.Equal(got.DKK, want.DKK) || got.NetworkSource != want.NetworkSource { t.Fatal("the exported capsule or .dkk differs from the one the mutation derives") } }) diff --git a/capsule/open.go b/capsule/open.go index cf3a503..52acb68 100644 --- a/capsule/open.go +++ b/capsule/open.go @@ -27,7 +27,7 @@ type OpenOptions struct { // Extensions lists the extensions the application implements; see // InspectOptions.Extensions. Extensions extension.Registry - // Source fetches the release. Required. Its answer is always verified + // Source fetches the release, unless Release is set. Its answer is always verified // locally, at step 10. A source that fetches releases over a network, // like provider/drand.Client, verifies each response itself and discards // the invalid ones, so that Open reports ErrReleaseUnavailable at step 9 @@ -37,6 +37,17 @@ type OpenOptions struct { // code: the text of an error that carries another code, or none, is // kept, not its code. Source provider.ReleaseSource + // Release is a release the caller has in hand, the alternative to + // Source (spec v0.15, §63 step 9.c): a .dkr file or drand's JSON read + // with provider.Encoded, or a local archive with provider.Archive. It + // makes no network request, so Open asks it for the release without + // comparing Now with the round time, and reports in + // Opened.ClockBehind a clock that is behind it. An error of Release is + // reported at step 9 with ErrReleaseUnavailable as its only code, as one + // of Source; the release it supplies is decoded and verified at step 10, + // with the codes of that step. Exactly one of Source and Release must be + // set. + Release provider.Supplier // Identities are the caller's own X25519 identities, for time_and_key // capsules encrypted to known recipients. Nil entries are ignored. Identities []age.Identity @@ -53,7 +64,7 @@ type OpenOptions struct { AccessKeyFile io.Reader // Now is the clock. Required: no package of this module reads the wall // clock on its own. Open does not ask Source for a round whose time has - // not been reached. + // not been reached; it does not compare it with a Release in hand. Now func() time.Time // Sink receives the files of a format 3 capsule; dst receives the // content of formats 1 and 2. Open fails right after step 2 with @@ -74,7 +85,16 @@ type OpenOptions struct { // Opened describes a capsule that Open decrypted completely. type Opened struct { Inspection *Inspection - Release provider.Release + // Release is the release that opened the capsule, verified at step 10, + // with the chain hash of the pinned profile: provider.EncodeRelease + // gives the .dkr that a reader saves next to the capsule (spec v0.15, + // §62.1). + Release provider.Release + // ClockBehind reports that the release was in the caller's hand and that + // Now was before the round time of the DateKey: the release proves the + // round was published, so the clock is probably behind, which a reader + // may say (spec v0.15, §63 step 9.c). + ClockBehind bool // Format is the format of the capsule (spec §22). A caller should show // it: format 1 does not hide the number of credentials or the exact // length of the content (spec §55.2, §70). @@ -127,8 +147,8 @@ type Opened struct { // discarded, and that must not be presented as valid: write to a temporary // file and publish it only on success (spec §56), as the datekeys CLI does. func Open(ctx context.Context, dst io.Writer, r io.Reader, opts OpenOptions) (*Opened, error) { - if opts.Source == nil { - return nil, errors.New("capsule: OpenOptions.Source is required") + if (opts.Source == nil) == (opts.Release == nil) { + return nil, errors.New("capsule: set exactly one of OpenOptions.Source and OpenOptions.Release") } if opts.Now == nil { return nil, errors.New("capsule: OpenOptions.Now is required") @@ -201,26 +221,50 @@ func Open(ctx context.Context, dst io.Writer, r io.Reader, opts OpenOptions) (*O in.pass(9, "access credential", fmt.Sprintf("%d identities to try%s", len(ids), unusable(out.UnusableAccessKeyExtensions))) } - // Step 9: obtain the release, never before its round time. A network - // source has verified each response with the rules of step 10 and - // discarded the invalid ones. Whatever the failure of the source, none - // valid included, it is ErrReleaseUnavailable here. + // Step 9: obtain the release. A network source is never asked before the + // round time (9.c); a release in hand is not compared with the clock, + // whose being behind it is only reported (spec v0.15). A network source + // has verified each response with the rules of step 10 and discarded the + // invalid ones. Whatever the failure of the source, none valid included, + // it is ErrReleaseUnavailable here. cond := provider.Condition{Round: h.DateKey.Round} - if now := opts.Now(); now.Before(in.UnlockAt) { - err := fmt.Errorf("capsule: round %d is published at %s, it is %s: %w", cond.Round, - in.UnlockAt.Format(time.RFC3339), now.UTC().Format(time.RFC3339), datekeys.ErrReleaseUnavailable) - return out, in.fail(9, "release", err) - } - release, err := opts.Source.Fetch(ctx, p, cond) - if err != nil { - return out, in.fail(9, "release", sourceFailure(err)) + now := opts.Now() + var release provider.Release + if opts.Release != nil { + encoded, err := opts.Release.Supply(p, cond) + if err != nil { + return out, in.fail(9, "release", sourceFailure(err)) + } + out.ClockBehind = now.Before(in.UnlockAt) + detail := "release supplied by the caller" + if out.ClockBehind { + detail = fmt.Sprintf("release supplied by the caller; round %d is published at %s and the clock says %s: it may be behind", + cond.Round, in.UnlockAt.Format(time.RFC3339), now.UTC().Format(time.RFC3339)) + } + in.pass(9, "release", detail) + // Step 10 starts with the layers of the release object. + if release, err = provider.ParseRelease(encoded); err != nil { + return out, in.fail(10, "release verification", err) + } + } else { + if now.Before(in.UnlockAt) { + err := fmt.Errorf("capsule: round %d is published at %s, it is %s: %w", cond.Round, + in.UnlockAt.Format(time.RFC3339), now.UTC().Format(time.RFC3339), datekeys.ErrReleaseUnavailable) + return out, in.fail(9, "release", err) + } + var err error + if release, err = opts.Source.Fetch(ctx, p, cond); err != nil { + return out, in.fail(9, "release", sourceFailure(err)) + } + in.pass(9, "release", fmt.Sprintf("round %d obtained", release.Round)) } - in.pass(9, "release", fmt.Sprintf("round %d obtained", release.Round)) - // Step 10: verify the release locally. + // Step 10: verify the release locally: the chain it names, its round and + // its signature. if err := provider.Verify(p, cond, release); err != nil { return out, in.fail(10, "release verification", err) } + release.ChainHash = bytes.Clone(p.ChainHash[:]) out.Release = release in.pass(10, "release verification", "BLS signature valid under the pinned key") diff --git a/cmd/datekeys/main.go b/cmd/datekeys/main.go index 2fae10f..8bca962 100644 --- a/cmd/datekeys/main.go +++ b/cmd/datekeys/main.go @@ -7,12 +7,15 @@ // datekeys author keygen -out autor.key -pass-file clave.txt // datekeys encrypt -at 2030-01-01T00:00:00Z -in carta.txt -sign autor.key -sign-pass-file clave.txt -out carta.dkc // datekeys decrypt -in regalo.dkc -out regalo [-dkk key.dkk] [-identity key.txt] [-words-file palabras.txt] +// datekeys decrypt -in regalo.dkc -out regalo -release regalo.dkr +// datekeys release -in regalo.dkc -out regalo.dkr // datekeys datekey resolve -at 2030-01-01T00:00:00Z // datekeys profile hash // datekeys version // // Encryption never touches the network. Decryption fetches the release from -// public drand relays and verifies it locally. Outputs are written to a +// public drand relays, or reads the one the person has in hand with -release, +// and verifies it locally. Outputs are written to a // temporary file in the destination directory and published only when // complete; existing files are never overwritten. The files of a format 3 // capsule go to a new folder, staged inside it and moved into place only @@ -43,13 +46,15 @@ import ( "g.activething.com/go/DateKeys/datekey" "g.activething.com/go/DateKeys/internal/inspectview" "g.activething.com/go/DateKeys/profile" + "g.activething.com/go/DateKeys/provider" "g.activething.com/go/DateKeys/provider/drand" "g.activething.com/go/DateKeys/wordkey" ) const usage = `usage: datekeys encrypt -at TIME -in FILE|FOLDER... -out FILE.dkc [-comment TEXT] [-author TEXT] [-no-mtime] [-policy time_only|time_and_key] [-recipient age1...]... [-dkk FILE.dkk] [-words TEXT|-words-file FILE] [-padding reforzado|bloque256] [-note TEXT] [-sign KEY [-sign-pass-file FILE]] [-large-area] - datekeys decrypt -in FILE.dkc -out PATH [-dkk FILE.dkk] [-identity FILE]... [-words TEXT|-words-file FILE] [-expect-author dkauthor1...] [-relay URL]... + datekeys decrypt -in FILE.dkc -out PATH [-dkk FILE.dkk] [-identity FILE]... [-words TEXT|-words-file FILE] [-expect-author dkauthor1...] [-relay URL]... [-release FILE] [-save-release FILE.dkr] + datekeys release -in FILE.dkc -out FILE.dkr [-relay URL]... datekeys author keygen -out FILE (-pass-file FILE|-plain) datekeys author public -key FILE [-pass-file FILE] datekeys inspect -in FILE.dkc [-json] @@ -65,6 +70,16 @@ capsule holds 16 slots, from 1 to 16 credentials and a dummy in each slot left (spec §29, §39). decrypt writes the files of a format 3 capsule to the new folder PATH, and the content of formats 1 and 2 to the new file PATH. +decrypt fetches the release of the round from drand relays, never before the +round time. -release FILE gives it instead, without any network request: a +.dkr file, drand's JSON or a local release archive. It is verified like one +from a relay, and the clock does not stop it: a valid release proves that the +round was published (spec v0.15, §63 step 9.c). -save-release keeps the +release that opened the capsule in a new .dkr file, to keep next to the .dkc: +in decades the relays may no longer serve the round. release fetches, +verifies and saves that .dkr without opening the capsule; the request tells +the relay the round, as opening does. + -words and -words-file give a key of words to a time_and_key capsule: at least 6 different words of 3 or more letters that open it with decrypt, instead of a .dkk @@ -123,6 +138,8 @@ func run(args []string, stdout, stderr io.Writer, now func() time.Time) error { return encrypt(args[1:], stderr, now) case "decrypt": return decrypt(args[1:], stdout, stderr, now) + case "release": + return saveRelease(args[1:], stderr, now) case "author": return author(args[1:], stdout, stderr, stdin) case "inspect": @@ -311,12 +328,22 @@ func decrypt(args []string, stdout, stderr io.Writer, now func() time.Time) erro words := fs.String("words", "", "the words of a key of words; they stay in the shell history") wordsFile := fs.String("words-file", "", "file with the words of a key of words") expect := fs.String("expect-author", "", "fail unless the capsule is signed with this public key, dkauthor1...") + release := fs.String("release", "", "the release in hand: a .dkr, drand's JSON or a local release archive; no network request") + keep := fs.String("save-release", "", "new .dkr file for the release that opened the capsule; never overwritten") if err := parse(fs, args); err != nil { return err } if *in == "" || *out == "" { return errors.New("decrypt: -in and -out are required") } + if *release != "" && len(relays) > 0 { + return errors.New("decrypt: -release and -relay are exclusive") + } + if *keep != "" { + if err := checkNew(*keep); err != nil { + return err + } + } var expected []byte if *expect != "" { k, err := authorkey.ParsePublic(*expect) @@ -329,7 +356,17 @@ func decrypt(args []string, stdout, stderr io.Writer, now func() time.Time) erro if err != nil { return err } - opts := capsule.OpenOptions{Registry: reg, Source: drand.New(relays...), Now: now} + opts := capsule.OpenOptions{Registry: reg, Now: now} + if *release != "" { + supplier, closer, err := releaseInHand(*release) + if err != nil { + return err + } + defer closer.Close() + opts.Release = supplier + } else { + opts.Source = drand.New(relays...) + } if expected != nil { // The expected key is not a key that the person saved, with a label // she gave it: a signature with it is F4, which shows the whole key. A @@ -423,6 +460,16 @@ func decrypt(args []string, stdout, stderr io.Writer, now func() time.Time) erro } fmt.Fprintf(stderr, "Decrypted capsule %s (round %d, unlocked at %s); release verified locally\n", opened.Inspection.Header.CapsuleIDHex(), opened.Release.Round, opened.Inspection.UnlockAt.Format(time.RFC3339)) + if opened.ClockBehind { + fmt.Fprintf(stderr, "warning: the release proves that round %d was published at %s, and this clock says %s: it may be behind\n", + opened.Release.Round, opened.Inspection.UnlockAt.Format(time.RFC3339), now().UTC().Format(time.RFC3339)) + } + if *keep != "" { + if err := writeRelease(*keep, opened.Release); err != nil { + return err + } + fmt.Fprintf(stderr, " release saved to %s: keep it next to the capsule\n", *keep) + } if opened.Format == capsule.Format3 { fmt.Fprintf(stderr, " format 3, %d files\n", len(opened.Head.Files)) if len(opened.Head.Files) == 0 { @@ -443,6 +490,107 @@ func decrypt(args []string, stdout, stderr io.Writer, now func() time.Time) erro return nil } +// releaseInHand reads the release that the person has in hand, from path: a +// local release archive, which is read when Open asks for the round, or a +// .dkr file or drand's JSON, which Open decodes at step 10. A file larger +// than any of them is cut where it can no longer be valid, so that step 10 +// rejects it with the code of its size. +func releaseInHand(path string) (provider.Supplier, io.Closer, error) { + f, err := os.Open(path) + if err != nil { + return nil, nil, err + } + info, err := f.Stat() + if err != nil { + f.Close() + return nil, nil, err + } + head := make([]byte, min(info.Size(), 64)) + if _, err := io.ReadFull(f, head); err != nil { + f.Close() + return nil, nil, err + } + if provider.IsArchive(head) { + return provider.NewArchive(f, info.Size()), f, nil + } + defer f.Close() + rest, err := io.ReadAll(io.LimitReader(f, provider.MaxReleaseJSONSize+1-int64(len(head)))) + if err != nil { + return nil, nil, err + } + return provider.Encoded(append(head, rest...)), io.NopCloser(nil), nil +} + +// writeRelease writes the release object of r, a verified release, to the +// new file path (spec v0.15, §47.1). +func writeRelease(path string, r provider.Release) error { + b, err := provider.EncodeRelease(r) + if err != nil { + return err + } + return writeAtomic(path, func(w io.Writer) error { + _, err := w.Write(b) + return err + }) +} + +// saveRelease fetches the release of the round of a capsule from drand +// relays, verifies it and saves it as a new .dkr file, without opening the +// capsule (spec v0.15, §62.1). Steps 1 to 8 come first, and no request is +// made before the round time. +func saveRelease(args []string, stderr io.Writer, now func() time.Time) error { + fs := newFlags("release") + in := fs.String("in", "", ".dkc file") + out := fs.String("out", "", "new .dkr file; never overwritten") + timeout := fs.Duration("timeout", 30*time.Second, "release request timeout") + var relays multi + fs.Var(&relays, "relay", "drand relay base URL (repeatable); default: public relays") + if err := parse(fs, args); err != nil { + return err + } + if *in == "" || *out == "" { + return errors.New("release: -in and -out are required") + } + if err := checkNew(*out); err != nil { + return err + } + reg, err := profile.Default() + if err != nil { + return err + } + src, err := os.Open(*in) + if err != nil { + return err + } + defer src.Close() + insp, err := capsule.Inspect(src, capsule.InspectOptions{Registry: reg}) + if err != nil { + return err + } + cond := provider.Condition{Round: insp.Header.DateKey.Round} + if t := now(); t.Before(insp.UnlockAt) { + return fmt.Errorf("release: round %d is published at %s, it is %s: %w", cond.Round, + insp.UnlockAt.Format(time.RFC3339), t.UTC().Format(time.RFC3339), datekeys.ErrReleaseUnavailable) + } + ctx, cancel := context.WithTimeout(context.Background(), *timeout) + defer cancel() + // The relays verify each answer and discard the invalid ones; it is + // verified again here, as Open does at step 10. + r, err := drand.New(relays...).Fetch(ctx, insp.Profile, cond) + if err != nil { + return err + } + if err := provider.Verify(insp.Profile, cond, r); err != nil { + return err + } + r.ChainHash = insp.Profile.ChainHash[:] + if err := writeRelease(*out, r); err != nil { + return err + } + fmt.Fprintf(stderr, "Saved the release of round %d, verified locally, to %s: keep it next to %s\n", cond.Round, *out, *in) + return nil +} + // wordsText is the text of the words of -words or of -words-file, at most // 4 KiB, or "" when neither is given. func wordsText(cmd, words, file string) (string, error) { diff --git a/cmd/datekeys/release_test.go b/cmd/datekeys/release_test.go new file mode 100644 index 0000000..31d9212 --- /dev/null +++ b/cmd/datekeys/release_test.go @@ -0,0 +1,145 @@ +package main + +import ( + "bytes" + "encoding/hex" + "errors" + "os" + "path/filepath" + "strings" + "testing" + "time" + + datekeys "g.activething.com/go/DateKeys" + "g.activething.com/go/DateKeys/internal/testkit" +) + +const releases = "../../testdata/releases" + +// unlock1000 is the round time of round 1000, that of time_only.dkc. +var unlock1000 = time.Date(2023, 8, 23, 15, 59, 24, 0, time.UTC) + +// Spec v0.15, §63 step 9.c: a release in hand opens the capsule without any +// network request, even with a clock behind the round time, which decrypt +// only warns about; -save-release keeps it as a .dkr, the same bytes as the +// official one. +func TestDecryptWithReleaseInHand(t *testing.T) { + dir := t.TempDir() + want, err := os.ReadFile(filepath.Join(fixtures, "time_only.plaintext")) + if err != nil { + t.Fatal(err) + } + jsonFile := filepath.Join(dir, "1000.json") + if err := os.WriteFile(jsonFile, []byte(`{"round":1000,"signature":"`+hex.EncodeToString(testkit.Release(1000).Signature)+`"}`), 0o644); err != nil { + t.Fatal(err) + } + for i, c := range []struct { + name, release string + now time.Time + }{ + {".dkr", filepath.Join(releases, "1000.dkr"), later}, + {".dkr and a clock behind", filepath.Join(releases, "1000.dkr"), unlock1000.Add(-time.Hour)}, + {"drand's JSON", jsonFile, later}, + {"a local archive", filepath.Join(releases, testkit.ArchiveFile), unlock1000.Add(-time.Nanosecond)}, + } { + t.Run(c.name, func(t *testing.T) { + out := filepath.Join(dir, c.name) + saved := filepath.Join(dir, "saved"+string(rune('a'+i))+".dkr") + _, stderr, err := cli(t, c.now, "decrypt", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", out, "-release", c.release, "-save-release", saved) + if err != nil { + t.Fatal(err) + } + if got, _ := os.ReadFile(out); !bytes.Equal(got, want) { + t.Fatal("wrong content") + } + if behind := strings.Contains(stderr, "may be behind"); behind != c.now.Before(unlock1000) { + t.Fatalf("warning of a clock behind: %v, in %q", behind, stderr) + } + official, _ := os.ReadFile(filepath.Join(releases, "1000.dkr")) + if got, _ := os.ReadFile(saved); !bytes.Equal(got, official) { + t.Fatalf("saved .dkr %x, want %x", got, official) + } + }) + } +} + +// A release in hand that step 10 rejects gives the code of step 10, and one +// the archive lacks is ErrReleaseUnavailable; nothing is written. +func TestDecryptWithBadReleaseInHand(t *testing.T) { + dir := t.TempDir() + write := func(name string, b []byte) string { + p := filepath.Join(dir, name) + if err := os.WriteFile(p, b, 0o644); err != nil { + t.Fatal(err) + } + return p + } + r1001, _ := os.ReadFile(filepath.Join(releases, "1001.dkr")) + r1000, _ := os.ReadFile(filepath.Join(releases, "1000.dkr")) + v2 := bytes.Clone(r1000) + v2[bytes.Index(v2, []byte{0x01, 0x01})+1] = 2 + for _, c := range []struct { + name string + release string + capsule string + want *datekeys.Error + }{ + {"another round", write("1001.dkr", r1001), "time_only", datekeys.ErrRoundMismatch}, + {"version 2", write("v2.dkr", v2), "time_only", datekeys.ErrUnsupportedVersion}, + {"a byte after it", write("long.dkr", append(bytes.Clone(r1000), 0)), "time_only", datekeys.ErrNonCanonicalCBOR}, + {"a large file", write("large.dkr", append(bytes.Clone(r1000), make([]byte, 20000)...)), "time_only", datekeys.ErrNonCanonicalCBOR}, + {"bad JSON", write("bad.json", []byte(`{"round":1000}`)), "time_only", datekeys.ErrReleaseInvalid}, + {"a round the archive lacks", filepath.Join(releases, testkit.ArchiveFile), "format2_time_and_key_sixteen", datekeys.ErrReleaseUnavailable}, + } { + t.Run(c.name, func(t *testing.T) { + out := filepath.Join(dir, c.name) + args := []string{"decrypt", "-in", filepath.Join(fixtures, c.capsule+".dkc"), "-out", out, "-release", c.release} + if c.capsule == "format2_time_and_key_sixteen" { + args = append(args, "-identity", filepath.Join(dir, "none")) + // The capsule needs a credential before the release: give it + // one that is never reached. + write("none", []byte(testkit.Stranger().String()+"\n")) + } + _, _, err := cli(t, later, args...) + if !errors.Is(err, c.want) { + t.Fatalf("got %v, want %v", err, c.want) + } + if _, err := os.Stat(out); !os.IsNotExist(err) { + t.Fatal("output written") + } + }) + } + if _, _, err := cli(t, later, "decrypt", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", filepath.Join(dir, "x"), "-release", "a.dkr", "-relay", "http://127.0.0.1:1"); err == nil { + t.Fatal("-release and -relay together") + } +} + +// A network source is still never asked before the round time. +func TestDecryptFromRelayBeforeTheRoundTime(t *testing.T) { + _, _, err := cli(t, unlock1000.Add(-time.Second), "decrypt", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", filepath.Join(t.TempDir(), "x"), "-relay", "http://127.0.0.1:1") + if !errors.Is(err, datekeys.ErrReleaseUnavailable) { + t.Fatalf("got %v", err) + } +} + +// datekeys release fetches, verifies and saves the .dkr of a capsule +// without opening it, and never before the round time. +func TestReleaseCommand(t *testing.T) { + dir := t.TempDir() + url := relay(t) + out := filepath.Join(dir, "carta.dkr") + if _, _, err := cli(t, later, "release", "-in", filepath.Join(fixtures, "format3_time_and_key_portable.dkc"), "-out", out, "-relay", url); err != nil { + t.Fatal(err) + } + official, _ := os.ReadFile(filepath.Join(releases, "1000.dkr")) + if got, _ := os.ReadFile(out); !bytes.Equal(got, official) { + t.Fatalf("saved .dkr %x, want %x", got, official) + } + if _, _, err := cli(t, later, "release", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", out, "-relay", url); err == nil { + t.Fatal("an existing .dkr was overwritten") + } + _, _, err := cli(t, unlock1000.Add(-time.Second), "release", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", filepath.Join(dir, "early.dkr"), "-relay", "http://127.0.0.1:1") + if !errors.Is(err, datekeys.ErrReleaseUnavailable) { + t.Fatalf("before the round time: %v", err) + } +} diff --git a/internal/testkit/fixture.go b/internal/testkit/fixture.go index 69db98d..0dbced0 100644 --- a/internal/testkit/fixture.go +++ b/internal/testkit/fixture.go @@ -17,6 +17,10 @@ type FixtureStanza struct { type FixtureRelease struct { Round uint64 `json:"round"` Signature string `json:"signature"` + // ChainHash is the chain a release object names (spec v0.15, §47.1), + // when it is not the chain of the pinned Quicknet profile; absent + // otherwise. + ChainHash string `json:"chain_hash,omitempty"` } // FixtureStage is the expected result of one step of spec §63. diff --git a/internal/testkit/genfixtures/main.go b/internal/testkit/genfixtures/main.go index 6394571..6faec38 100644 --- a/internal/testkit/genfixtures/main.go +++ b/internal/testkit/genfixtures/main.go @@ -157,9 +157,39 @@ func vectors(dir string) error { if err := testkit.WriteJSON(filepath.Join(dir, "tlock_ibe.json"), iv); err != nil { return err } + if err := releaseVectors(dir); err != nil { + return err + } return format3Vectors(dir) } +// releaseVectors writes the release objects (spec v0.15, §47.1): +// vectors/release.json, and in releases/ the .dkr of each published round of +// the tests and a local archive, the informative format of §50. +func releaseVectors(dir string) error { + rv, err := testkit.ReleaseVectors() + if err != nil { + return err + } + if err := testkit.WriteJSON(filepath.Join(dir, "release.json"), rv); err != nil { + return err + } + files, err := testkit.ReleaseFiles() + if err != nil { + return err + } + out := filepath.Join(filepath.Dir(dir), "releases") + if err := os.MkdirAll(out, 0o755); err != nil { + return err + } + for name, b := range files { + if err := os.WriteFile(filepath.Join(out, name), b, 0o644); err != nil { + return err + } + } + return nil +} + // format3Vectors writes the vectors of format 3 (spec §67): the paths, the // keys of R7, the heads and security. func format3Vectors(dir string) error { diff --git a/internal/testkit/mutations.go b/internal/testkit/mutations.go index 2ac0c5c..5e76004 100644 --- a/internal/testkit/mutations.go +++ b/internal/testkit/mutations.go @@ -58,7 +58,11 @@ type Mutation struct { // opens without a code: the mutations of security, which never decides // the opening (spec §29.3, §64). Want is then nil and Step 0. Verdicts *capsule.Verdicts - Make func(e *MutationEnv) (*MutationInput, error) + // Opens reports a case of format 1 or 2 that opens, such as a release in + // hand with a clock behind it (spec v0.15, §63 step 9.c). Want is then + // nil and Step 0. + Opens bool + Make func(e *MutationEnv) (*MutationInput, error) } // Code is the result the mutation is written for: the code of Want, or @@ -84,7 +88,14 @@ type MutationInput struct { // Release is the only release the source knows: it answers every request // with it. Nil means that no release is available. Release *provider.Release - Now time.Time + // NetworkSource makes the source a network source, which is not asked + // before the round time and discards a release that breaks the rules of + // step 10 (spec §63 step 9). Otherwise the release is in the caller's + // hand, a release object that the reader decodes and verifies at step + // 10, and the clock is not compared with the round time (spec v0.15, + // step 9.c). + NetworkSource bool + Now time.Time // EmptyRegistry pins no profile; otherwise profile.Default is used. EmptyRegistry bool // Extensions are the extensions the application implements; nil knows @@ -128,20 +139,39 @@ func (k KnownExtensions) ValidateData(e extension.Extension) error { } // singleSource answers every request with one release, or with -// ErrReleaseUnavailable, and counts the requests. +// ErrReleaseUnavailable, and counts the requests. As a network source, +// Fetch verifies the release and discards it when it breaks a rule of step +// 10 (spec §63 step 9); as a release in hand, Supply hands over its release +// object, with the chain of the pinned profile unless the release names +// another. type singleSource struct { release *provider.Release calls int } -func (s *singleSource) Fetch(context.Context, *profile.Profile, provider.Condition) (provider.Release, error) { +func (s *singleSource) Fetch(_ context.Context, p *profile.Profile, c provider.Condition) (provider.Release, error) { s.calls++ if s.release == nil { return provider.Release{}, fmt.Errorf("testkit: no release: %w", datekeys.ErrReleaseUnavailable) } + if err := provider.Verify(p, c, *s.release); err != nil { + return provider.Release{}, fmt.Errorf("testkit: the release is discarded: %v: %w", err, datekeys.ErrReleaseUnavailable) + } return *s.release, nil } +func (s *singleSource) Supply(p *profile.Profile, _ provider.Condition) ([]byte, error) { + s.calls++ + if s.release == nil { + return nil, fmt.Errorf("testkit: no release: %w", datekeys.ErrReleaseUnavailable) + } + r := *s.release + if r.ChainHash == nil { + r.ChainHash = p.ChainHash[:] + } + return provider.EncodeRelease(r) +} + // Verdict is how a reader rejected a capsule. type Verdict struct { Err error @@ -163,7 +193,12 @@ func (in *MutationInput) Open() (Verdict, error) { } } src := &singleSource{release: in.Release} - o := capsule.OpenOptions{Registry: reg, Source: src, Now: Fixed(in.Now)} + o := capsule.OpenOptions{Registry: reg, Now: Fixed(in.Now)} + if in.NetworkSource { + o.Source = src + } else { + o.Release = src + } if in.Extensions != nil { o.Extensions = in.Extensions } @@ -527,7 +562,52 @@ func Mutations() []Mutation { out = append(out, specMutations(capsule.Format2)...) out = append(out, format2Mutations()...) out = append(out, specMutations(capsule.Format3)...) - return append(out, format3Mutations()...) + out = append(out, format3Mutations()...) + return append(out, sourceMutations()...) +} + +// sourceMutations returns the cases of spec v0.15 on the source of the +// release, on the format 1 fixture time_only: a release in the caller's +// hand is not compared with the clock and gets the codes of step 10, and a +// network source is not asked before the round time and discards what step +// 10 rejects, ErrReleaseUnavailable at step 9 (spec §63 steps 9 and 10, +// §69.1). The case "round not reached yet", among the further cases, is the +// same capsule and clock with a release in hand: it opens. +func sourceMutations() []Mutation { + otherChain := func(i int, bit byte) []byte { + h := profile.Quicknet().ChainHash + h[i] ^= bit + return h[:] + } + return []Mutation{ + {Name: "round not reached yet, from a network source", Want: datekeys.ErrReleaseUnavailable, Step: 9, + Make: func(e *MutationEnv) (*MutationInput, error) { + in := e.TimeOnly.input(e.TimeOnly.DKC) + in.Now, in.NetworkSource = e.TimeOnly.Unlock.Add(-1), true + return in, nil + }}, + {Name: "release of another round, from a network source", Want: datekeys.ErrReleaseUnavailable, Step: 9, Network: true, + Make: func(e *MutationEnv) (*MutationInput, error) { + in := e.TimeOnly.input(e.TimeOnly.DKC) + in.Release, in.NetworkSource = &provider.Release{Round: 1001, Signature: Release(1001).Signature}, true + return in, nil + }}, + {Name: "release object of another chain", Want: datekeys.ErrProfileMismatch, Step: 10, Network: true, + Make: func(e *MutationEnv) (*MutationInput, error) { + in := e.TimeOnly.input(e.TimeOnly.DKC) + r := *in.Release + r.ChainHash = otherChain(31, 1) + in.Release = &r + return in, nil + }}, + {Name: "release object of another chain and another round, with a clock behind", Want: datekeys.ErrProfileMismatch, Step: 10, Network: true, + Make: func(e *MutationEnv) (*MutationInput, error) { + in := e.TimeOnly.input(e.TimeOnly.DKC) + in.Release = &provider.Release{Round: 1001, Signature: Release(1001).Signature, ChainHash: otherChain(0, 0x80)} + in.Now = e.TimeOnly.Unlock.Add(-1) + return in, nil + }}, + } } // specMutations returns the thirty-three mutations of the first two lists of @@ -901,7 +981,10 @@ func furtherMutations() []Mutation { in.DKK, in.Identities = nil, []string{Stranger().String()} return in, nil }}, - {Name: "round not reached yet", Want: datekeys.ErrReleaseUnavailable, Step: 9, + // Spec v0.15, §63 step 9.c: a release in hand is not compared with the + // clock. The same case with a network source is among the cases of + // sourceMutations. + {Name: "round not reached yet", Opens: true, Network: true, Make: func(e *MutationEnv) (*MutationInput, error) { in := e.TimeOnly.input(e.TimeOnly.DKC) in.Now = e.TimeOnly.Unlock.Add(-1) @@ -1154,6 +1237,12 @@ func (m Mutation) Check(in *MutationInput) error { } func (m Mutation) checkVerdict(v Verdict) error { + if m.Opens { + if v.Err != nil { + return fmt.Errorf("got %v, want the capsule to open", v.Err) + } + return nil + } if m.Verdicts != nil { switch { case v.Err != nil: @@ -1179,6 +1268,12 @@ func (m Mutation) checkVerdict(v Verdict) error { // --------------------------------------------------------------------------- // Exported corpus: testdata/vectors/mutations.json +// The sources of a case of the corpus (spec v0.15, §63 step 9). +const ( + SourceSupplied = "supplied" + SourceNetwork = "network" +) + // MutationFile is testdata/vectors/mutations.json. type MutationFile struct { Spec string `json:"spec"` @@ -1198,6 +1293,10 @@ type MutationCase struct { // Release is the only release the source serves, for any requested // round; null when no release is available. Release *FixtureRelease `json:"release"` + // Source is "supplied", a release in the caller's hand, given to the + // reader as a release object, or "network", a network source (spec + // v0.15, §63 step 9). + Source string `json:"source"` // Now is the reader's clock, RFC 3339. Now string `json:"now"` // Registry is "default" (the Quicknet profile pinned) or "empty". @@ -1263,6 +1362,18 @@ func (c *MutationCase) Input(dir string) (*MutationInput, error) { return nil, err } in.Release = &provider.Release{Round: c.Release.Round, Signature: sig} + if c.Release.ChainHash != "" { + if in.Release.ChainHash, err = hex.DecodeString(c.Release.ChainHash); err != nil { + return nil, err + } + } + } + switch c.Source { + case SourceSupplied: + case SourceNetwork: + in.NetworkSource = true + default: + return nil, fmt.Errorf("testkit: unknown source %q", c.Source) } if in.Now, err = time.Parse(time.RFC3339Nano, c.Now); err != nil { return nil, err @@ -1304,6 +1415,10 @@ func (m Mutation) record(in *MutationInput, dir string, v Verdict) (MutationCase c := MutationCase{ Name: m.Name, Spec: m.Spec, Identities: in.Identities, Now: in.Now.UTC().Format(time.RFC3339Nano), Registry: "default", Network: m.Network, Frozen: m.Random, Error: Result(v.Err), Step: v.Step, + Source: SourceSupplied, + } + if in.NetworkSource { + c.Source = SourceNetwork } if in.EmptyRegistry { c.Registry = "empty" @@ -1327,6 +1442,9 @@ func (m Mutation) record(in *MutationInput, dir string, v Verdict) (MutationCase } if in.Release != nil { c.Release = &FixtureRelease{Round: in.Release.Round, Signature: hex.EncodeToString(in.Release.Signature)} + if in.Release.ChainHash != nil { + c.Release.ChainHash = hex.EncodeToString(in.Release.ChainHash) + } } for _, x := range in.Extensions { c.Extensions = append(c.Extensions, KnownExtensionRecord{ID: x.ID, Version: x.Version, ValidData: hex.EncodeToString(x.ValidData)}) diff --git a/internal/testkit/releasevectors.go b/internal/testkit/releasevectors.go new file mode 100644 index 0000000..565f4d6 --- /dev/null +++ b/internal/testkit/releasevectors.go @@ -0,0 +1,331 @@ +package testkit + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + + datekeys "g.activething.com/go/DateKeys" + "g.activething.com/go/DateKeys/codec" + "g.activething.com/go/DateKeys/profile" + "g.activething.com/go/DateKeys/provider" +) + +// ReleaseVectorFile is testdata/vectors/release.json: release objects, the +// content of a .dkr file (spec v0.15, §47.1), and drand's JSON, each checked +// as step 10 of spec §63 checks a release that the caller supplies, against +// the pinned Quicknet profile and the round of a DateKey; and the lookups of +// a local release archive, an informative format (spec v0.15, §50). +type ReleaseVectorFile struct { + Spec string `json:"spec"` + Description string `json:"description"` + Profile string `json:"profile"` + Objects []ReleaseVector `json:"objects"` + JSON []ReleaseVector `json:"json"` + Archive ArchiveVectors `json:"archive"` +} + +// ReleaseVector is an input, in hexadecimal for a release object and as text +// for drand's JSON, the round of the DateKey it is checked against, and the +// result: ResultOK or the code, and the text of the error of the reference. +// When the input decodes, Release is what it says. +type ReleaseVector struct { + Name string `json:"name"` + Encoding string `json:"encoding,omitempty"` + Input string `json:"input,omitempty"` + Round uint64 `json:"round"` + Release *FixtureRelease `json:"release,omitempty"` + Result string `json:"result"` + Text string `json:"text,omitempty"` +} + +// ArchiveVectors is the local archive of testdata/releases and what it +// supplies for each round looked up: the release object of the round, or +// ErrReleaseUnavailable. +type ArchiveVectors struct { + File string `json:"file"` + Header string `json:"header"` + Lookups []ArchiveLookup `json:"lookups"` +} + +// ArchiveLookup is a round looked up in the archive. +type ArchiveLookup struct { + Round uint64 `json:"round"` + Encoding string `json:"encoding,omitempty"` + Result string `json:"result"` +} + +// The local archive of testdata/releases: rounds 1000 to 1004, with 1002 +// and 1003, whose signatures the tests do not hold, written as zeros. +const ( + ArchiveFile = "archive_1000_1004.bin" + archiveFirst = 1000 + archiveCount = 5 +) + +// ReleaseFileName is the name of the .dkr of round in testdata/releases. +func ReleaseFileName(round uint64) string { return strconv.FormatUint(round, 10) + ".dkr" } + +// releaseObject returns the release object of r with the Quicknet chain +// hash, or with chain when it is not nil. +func releaseObject(r provider.Release, chain []byte) []byte { + if chain == nil { + chain = profile.Quicknet().ChainHash[:] + } + r.ChainHash = chain + b, err := provider.EncodeRelease(r) + if err != nil { + panic(err) + } + return b +} + +// ReleaseFiles computes the files of testdata/releases: the .dkr of each +// published round of the tests, and the local archive ArchiveFile. +func ReleaseFiles() (map[string][]byte, error) { + files := map[string][]byte{} + for _, round := range Rounds { + files[ReleaseFileName(round)] = releaseObject(Release(round), nil) + } + p := profile.Quicknet() + b, err := provider.EncodeArchiveHeader(p.ChainHash[:], archiveFirst, archiveCount) + if err != nil { + return nil, err + } + for round := uint64(archiveFirst); round < archiveFirst+archiveCount; round++ { + if _, ok := signatures[round]; ok { + b = append(b, Release(round).Signature...) + } else { + b = append(b, make([]byte, 48)...) + } + } + files[ArchiveFile] = b + return files, nil +} + +// checkRelease is what step 10 of spec §63 does with a release that the +// caller supplies, for a DateKey of round: decode it, then verify it against +// the pinned Quicknet profile. +func checkRelease(b []byte, round uint64) (*provider.Release, error) { + r, err := provider.ParseRelease(b) + if err != nil { + return nil, err + } + return &r, provider.Verify(profile.Quicknet(), provider.Condition{Round: round}, r) +} + +// ReleaseVectors computes testdata/vectors/release.json, and fails if a +// vector does not get the result it is written for. +func ReleaseVectors() (ReleaseVectorFile, error) { + p := profile.Quicknet() + f := ReleaseVectorFile{ + Spec: SpecVersion, + Description: "The release object, the content of a .dkr file (spec v0.15, §47.1), and drand's JSON as the input of the caller, " + + "each checked against the pinned Quicknet profile and the round of a DateKey as step 10 of spec §63 checks a release that the caller supplies; " + + "and the lookups of a local release archive (spec v0.15, §50). See testdata/README.md.", + Profile: p.ID, + } + var errs []error + ok := ResultOK + nonCanonical, unsupported := datekeys.ErrNonCanonicalCBOR.Code(), datekeys.ErrUnsupportedVersion.Code() + mismatch, roundMismatch, invalid := datekeys.ErrProfileMismatch.Code(), datekeys.ErrRoundMismatch.Code(), datekeys.ErrReleaseInvalid.Code() + + r1000, r1001 := Release(1000), Release(1001) + other := p.ChainHash + other[31] ^= 1 + xPlusP, err := AddModulus(Release(XPlusPRound).Signature, 0) + if err != nil { + return f, err + } + // raw writes a release object field by field, with the encoder of the + // codec, so that it may break the schema. + raw := func(fields ...func(e *codec.Encoder)) []byte { + var e codec.Encoder + e.Map(len(fields)) + for _, w := range fields { + w(&e) + } + b, err := e.Out() + if err != nil { + panic(err) + } + return b + } + key := func(k uint64, w func(e *codec.Encoder)) func(e *codec.Encoder) { + return func(e *codec.Encoder) { e.Uint(k); w(e) } + } + text := func(s string) func(e *codec.Encoder) { return func(e *codec.Encoder) { e.Text(s) } } + uint_ := func(v uint64) func(e *codec.Encoder) { return func(e *codec.Encoder) { e.Uint(v) } } + bstr := func(b []byte) func(e *codec.Encoder) { return func(e *codec.Encoder) { e.Bstr(b) } } + fields := func(tag string, version uint64, chain []byte, round uint64, sig []byte) []func(e *codec.Encoder) { + return []func(e *codec.Encoder){key(0, text(tag)), key(1, uint_(version)), key(2, bstr(chain)), key(3, uint_(round)), key(4, bstr(sig))} + } + good := func(round uint64) []func(e *codec.Encoder) { + return fields(provider.ReleaseTypeTag, 1, p.ChainHash[:], round, Release(round).Signature) + } + obj1000 := releaseObject(r1000, nil) + pad := func(b []byte, n int) []byte { return append(bytes.Clone(b), make([]byte, n-len(b))...) } + withSig := func(sig []byte) []byte { return raw(fields(provider.ReleaseTypeTag, 1, p.ChainHash[:], 1000, sig)...) } + + objects := []struct { + name string + enc []byte + round uint64 + want string + }{ + {"round 1000", obj1000, 1000, ok}, + {"round 1001", releaseObject(r1001, nil), 1001, ok}, + {"round 1004", releaseObject(Release(1004), nil), 1004, ok}, + {"round 2000", releaseObject(Release(2000), nil), 2000, ok}, + // Size: the object has no frame, and is checked before it is decoded. + {"no byte", []byte{}, 1000, nonCanonical}, + {"1025 bytes: the object of round 1000 followed by zeros", pad(obj1000, 1025), 1000, nonCanonical}, + {"1025 bytes of an object of version 2: the size first", pad(raw(key(0, text(provider.ReleaseTypeTag)), key(1, uint_(2))), 1025), 1000, nonCanonical}, + // Type and version (spec §69.1, layer 2). + {"type tag of the Provider Profile, version 2", raw(fields(profile.TypeTag, 2, p.ChainHash[:], 1000, r1000.Signature)...), 1000, nonCanonical}, + {"type tag in upper case", raw(fields(strings.ToUpper(provider.ReleaseTypeTag), 1, p.ChainHash[:], 1000, r1000.Signature)...), 1000, nonCanonical}, + {"version 2", raw(fields(provider.ReleaseTypeTag, 2, p.ChainHash[:], 1000, r1000.Signature)...), 1000, unsupported}, + {"version 2, an unknown key and another chain", raw(append(fields(provider.ReleaseTypeTag, 2, other[:], 1000, r1000.Signature), key(5, uint_(0)))...), 1000, unsupported}, + {"version 0", raw(fields(provider.ReleaseTypeTag, 0, p.ChainHash[:], 1000, r1000.Signature)...), 1000, unsupported}, + {"keys 0 and 1 swapped", raw(key(1, uint_(1)), key(0, text(provider.ReleaseTypeTag)), key(2, bstr(p.ChainHash[:])), key(3, uint_(1000)), key(4, bstr(r1000.Signature))), 1000, nonCanonical}, + // Encoding and schema (layer 3). + {"a byte after the object", append(bytes.Clone(obj1000), 0), 1000, nonCanonical}, + {"without the signature", raw(good(1000)[:4]...), 1000, nonCanonical}, + {"a key 5", raw(append(good(1000), key(5, uint_(0)))...), 1000, nonCanonical}, + {"chain hash of 31 bytes", raw(fields(provider.ReleaseTypeTag, 1, p.ChainHash[:31], 1000, r1000.Signature)...), 1000, nonCanonical}, + {"chain hash as text", raw(key(0, text(provider.ReleaseTypeTag)), key(1, uint_(1)), key(2, text(p.ChainHashHex())), key(3, uint_(1000)), key(4, bstr(r1000.Signature))), 1000, nonCanonical}, + {"round 0", raw(fields(provider.ReleaseTypeTag, 1, p.ChainHash[:], 0, r1000.Signature)...), 1000, nonCanonical}, + {"round 2^53", raw(fields(provider.ReleaseTypeTag, 1, p.ChainHash[:], 1<<53, r1000.Signature)...), 1000, nonCanonical}, + {"round 1000 in four bytes", longRound(obj1000, len(raw(good(1000)[:3]...))), 1000, nonCanonical}, + {"signature of no byte", withSig([]byte{}), 1000, nonCanonical}, + {"signature of 97 bytes", withSig(append(bytes.Repeat(r1000.Signature, 2), 0)), 1000, nonCanonical}, + {"map of indefinite length", append(append([]byte{0xbf}, obj1000[1:]...), 0xff), 1000, nonCanonical}, + // The step: chain hash, round, signature, in this order. + {"another chain", releaseObject(r1000, other[:]), 1000, mismatch}, + {"another chain, another round and a signature of no point", raw(fields(provider.ReleaseTypeTag, 1, other[:], 1001, make([]byte, 48))...), 1000, mismatch}, + {"round 1001 for a DateKey of round 1000", releaseObject(r1001, nil), 1000, roundMismatch}, + {"another round and a signature of 47 bytes", withSigRound(raw, fields, p.ChainHash[:], 1001, r1001.Signature[:47]), 1000, roundMismatch}, + {"the signature of round 1001 as round 1000", withSig(r1001.Signature), 1000, invalid}, + {"signature of 47 bytes", withSig(r1000.Signature[:47]), 1000, invalid}, + {"signature of 96 bytes, the size of G2", withSig(bytes.Repeat(r1000.Signature, 2)), 1000, invalid}, + {"signature of zeros", withSig(make([]byte, 48)), 1000, invalid}, + {"signature the point at infinity", withSig(Infinity(48)), 1000, invalid}, + {"signature with the infinity flag and a payload", withSig(InfinityWithPayload(r1000.Signature)), 1000, invalid}, + {"signature negated", withSig(Negated(r1000.Signature)), 1000, invalid}, + {"signature of round 1004 with x + p", withSigRound(raw, fields, p.ChainHash[:], XPlusPRound, xPlusP), XPlusPRound, invalid}, + } + for _, c := range objects { + v := ReleaseVector{Name: c.name, Encoding: hex.EncodeToString(c.enc), Round: c.round} + r, err := checkRelease(c.enc, c.round) + v.Result, v.Release = Result(err), fixtureRelease(r) + if err != nil { + v.Text = err.Error() + } + if v.Result != c.want { + errs = append(errs, fmt.Errorf("release object %q: %s, want %s (%v)", c.name, v.Result, c.want, err)) + } + f.Objects = append(f.Objects, v) + } + + s1000, s1001 := hex.EncodeToString(r1000.Signature), hex.EncodeToString(r1001.Signature) + jsons := []struct { + name string + input string + round uint64 + want string + }{ + {"the answer of a relay, API v2", `{"round":1000,"signature":"` + s1000 + `"}`, 1000, ok}, + {"with randomness, API v1", `{"round":1000,"randomness":"` + randomness(r1000.Signature) + `","signature":"` + s1000 + `"}`, 1000, ok}, + {"spaces and a line feed before and after", " \n" + `{"round": 1000, "signature": "` + strings.ToUpper(s1000) + `"}` + "\n", 1000, ok}, + {"randomness of another signature", `{"round":1000,"randomness":"` + randomness(r1001.Signature) + `","signature":"` + s1000 + `"}`, 1000, invalid}, + {"without signature", `{"round":1000}`, 1000, invalid}, + {"without round", `{"signature":"` + s1000 + `"}`, 1000, invalid}, + {"signature not in hexadecimal", `{"round":1000,"signature":"` + s1000[:94] + `zz"}`, 1000, invalid}, + {"round as a string", `{"round":"1000","signature":"` + s1000 + `"}`, 1000, invalid}, + {"not JSON", `{"round":1000,`, 1000, invalid}, + {"8193 bytes", `{"round":1000,"signature":"` + s1000 + `"}` + strings.Repeat(" ", 8193-len(`{"round":1000,"signature":"`+s1000+`"}`)), 1000, invalid}, + {"round 1001 for a DateKey of round 1000", `{"round":1001,"signature":"` + s1001 + `"}`, 1000, roundMismatch}, + {"the signature of round 1001 as round 1000", `{"round":1000,"signature":"` + s1001 + `"}`, 1000, invalid}, + } + for _, c := range jsons { + v := ReleaseVector{Name: c.name, Input: c.input, Round: c.round} + r, err := checkRelease([]byte(c.input), c.round) + v.Result, v.Release = Result(err), fixtureRelease(r) + if err != nil { + v.Text = err.Error() + } + if v.Result != c.want { + errs = append(errs, fmt.Errorf("drand JSON %q: %s, want %s (%v)", c.name, v.Result, c.want, err)) + } + f.JSON = append(f.JSON, v) + } + + files, err := ReleaseFiles() + if err != nil { + return f, err + } + archive := files[ArchiveFile] + header, err := provider.EncodeArchiveHeader(p.ChainHash[:], archiveFirst, archiveCount) + if err != nil { + return f, err + } + f.Archive = ArchiveVectors{File: "releases/" + ArchiveFile, Header: hex.EncodeToString(header)} + a := provider.NewArchive(bytes.NewReader(archive), int64(len(archive))) + for _, round := range []uint64{999, 1000, 1001, 1002, 1003, 1004, 1005} { + l := ArchiveLookup{Round: round} + b, err := a.Supply(p, provider.Condition{Round: round}) + if err == nil { + l.Encoding = hex.EncodeToString(b) + _, err = checkRelease(b, round) + } + l.Result = Result(err) + _, known := signatures[round] + if want := map[bool]string{true: ok, false: datekeys.ErrReleaseUnavailable.Code()}[known]; l.Result != want { + errs = append(errs, fmt.Errorf("archive round %d: %s, want %s (%v)", round, l.Result, want, err)) + } + if l.Result == ok && !bytes.Equal(b, files[ReleaseFileName(round)]) { + errs = append(errs, fmt.Errorf("archive round %d: another object than its .dkr", round)) + } + f.Archive.Lookups = append(f.Archive.Lookups, l) + } + return f, errors.Join(errs...) +} + +// longRound returns obj, a release object of round 1000 whose key 3 is at +// offset at, with the round written in four bytes instead of two. +func longRound(obj []byte, at int) []byte { + if !bytes.Equal(obj[at:at+4], []byte{0x03, 0x19, 0x03, 0xe8}) { + panic("testkit: key 3 is not round 1000 in two bytes") + } + out := append(bytes.Clone(obj[:at]), 0x03, 0x1a, 0x00, 0x00, 0x03, 0xe8) + return append(out, obj[at+4:]...) +} + +// withSigRound writes a release object of the Quicknet type with the given +// chain, round and signature. +func withSigRound(raw func(...func(*codec.Encoder)) []byte, fields func(string, uint64, []byte, uint64, []byte) []func(*codec.Encoder), chain []byte, round uint64, sig []byte) []byte { + return raw(fields(provider.ReleaseTypeTag, 1, chain, round, sig)...) +} + +// fixtureRelease is r as JSON, or nil. +func fixtureRelease(r *provider.Release) *FixtureRelease { + if r == nil { + return nil + } + f := &FixtureRelease{Round: r.Round, Signature: hex.EncodeToString(r.Signature)} + if r.ChainHash != nil { + f.ChainHash = hex.EncodeToString(r.ChainHash) + } + return f +} + +// randomness is the randomness drand derives from a signature: its SHA-256, +// in hexadecimal. +func randomness(sig []byte) string { + sum := sha256.Sum256(sig) + return hex.EncodeToString(sum[:]) +} diff --git a/internal/testkit/testkit_test.go b/internal/testkit/testkit_test.go index 69ad1d8..c7bf41c 100644 --- a/internal/testkit/testkit_test.go +++ b/internal/testkit/testkit_test.go @@ -279,6 +279,20 @@ func TestVectorFilesAreCurrent(t *testing.T) { 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) + } + } for _, v := range []struct { file string want any @@ -295,6 +309,7 @@ func TestVectorFilesAreCurrent(t *testing.T) { {"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) diff --git a/provider/archive.go b/provider/archive.go new file mode 100644 index 0000000..1686502 --- /dev/null +++ b/provider/archive.go @@ -0,0 +1,175 @@ +package provider + +import ( + "bytes" + "fmt" + "io" + + datekeys "g.activething.com/go/DateKeys" + "g.activething.com/go/DateKeys/codec" + "g.activething.com/go/DateKeys/profile" +) + +// Schema constants of the header of a release archive, an informative format +// (spec v0.15, §50). +const ( + ArchiveTypeTag = "datekeys-release-archive" + ArchiveSchemaVersion = 1 +) + +// maxArchiveHeader bounds the header of an archive: its five keys take at +// most 1 + 26 + 2 + 35 + 9 + 9 bytes. +const maxArchiveHeader = 128 + +// archiveKeys is the number of keys of the header, all required. +const archiveKeys = 5 + +// archiveHeader is the CBOR map at the start of an archive. +type archiveHeader struct { + ChainHash []byte // key 2, 32 bytes + First uint64 // key 3, the first round, 1..2^53-1 + Count uint64 // key 4, the number of rounds, 1..2^53-1 +} + +func (h *archiveHeader) encode(e *codec.Encoder) { + e.Map(archiveKeys) + e.Uint(0) + e.Text(ArchiveTypeTag) + e.Uint(1) + e.Uint(ArchiveSchemaVersion) + e.Uint(2) + e.Bstr(h.ChainHash) + e.Uint(3) + e.Uint(h.First) + e.Uint(4) + e.Uint(h.Count) +} + +func (h *archiveHeader) decode(d *codec.Decoder) error { + pairs, err := d.Map(archiveKeys) + if err != nil { + return err + } + if pairs != archiveKeys { + return fmt.Errorf("%d keys, want all %d", pairs, archiveKeys) + } + for want := range uint64(archiveKeys) { + k, err := d.Key() + if err != nil { + return err + } + if k != want { + return fmt.Errorf("key %d where key %d was expected", k, want) + } + switch k { + case 0: + _, err = d.Text(len(ArchiveTypeTag)) + case 1: + _, err = d.Uint(ArchiveSchemaVersion) + case 2: + h.ChainHash, err = d.Bstr(32, 32) + case 3: + h.First, err = d.Uint(codec.MaxSafeUint) + case 4: + h.Count, err = d.Uint(codec.MaxSafeUint) + } + if err != nil { + return fmt.Errorf("key %d: %w", k, err) + } + } + return d.EndMap() +} + +// EncodeArchiveHeader returns the header of an archive of count rounds of +// the chain chainHash from the round first (spec v0.15, §50). The signatures +// follow it, one after another, each with the length of a signature of the +// chain, and a round the archive lacks is written as zeros. +func EncodeArchiveHeader(chainHash []byte, first, count uint64) ([]byte, error) { + if len(chainHash) != 32 || first == 0 || count == 0 || first > codec.MaxSafeUint-count+1 { + return nil, fmt.Errorf("provider: archive header: chain hash of %d bytes, rounds %d to %d + %d - 1", len(chainHash), first, first, count) + } + h := archiveHeader{ChainHash: chainHash, First: first, Count: count} + var e codec.Encoder + h.encode(&e) + return e.Out() +} + +// IsArchive reports whether b, the start of a file, is the start of a +// release archive: a map whose type tag is ArchiveTypeTag. +func IsArchive(b []byte) bool { + tag, _, err := codec.Peek(b) + return err == nil && tag == ArchiveTypeTag +} + +// Archive is a local release archive, the informative format of spec v0.15, +// §50: a header in deterministic CBOR, {0: "datekeys-release-archive", 1: 1, +// 2: chain_hash, 3: first round, 4: number of rounds}, followed by the +// signatures, so that the one of round r starts at the end of the header plus +// (r - first)·n, with n the length of a signature of the chain, 48 bytes in +// Quicknet. A round written as zeros is missing. +// +// A local archive is a release in hand: it implements Supplier, and its +// entry is decoded and verified at step 10 of spec §63 like a .dkr. A round +// it lacks, a header it cannot read, an archive of another chain or of +// another length are failures to supply a release, ErrReleaseUnavailable at +// step 9: the format is informative and has no codes of its own. +type Archive struct { + r io.ReaderAt + size int64 +} + +var _ Supplier = (*Archive)(nil) + +// NewArchive returns the archive of size bytes read from r. It reads nothing +// until Supply. +func NewArchive(r io.ReaderAt, size int64) *Archive { return &Archive{r: r, size: size} } + +// Supply implements Supplier: it returns the release object of the round of +// c, with the chain hash of the header of the archive. +func (a *Archive) Supply(p *profile.Profile, c Condition) ([]byte, error) { + unavailable := func(format string, args ...any) error { + return fmt.Errorf("provider: release archive: "+format+": %w", append(args, datekeys.ErrReleaseUnavailable)...) + } + head := make([]byte, min(a.size, maxArchiveHeader)) + if _, err := a.r.ReadAt(head, 0); err != nil && err != io.EOF { + return nil, unavailable("%v", err) + } + if err := codec.CheckSchema(head, ArchiveTypeTag, ArchiveSchemaVersion); err != nil { + return nil, unavailable("not an archive of version %d", ArchiveSchemaVersion) + } + var h archiveHeader + d := codec.NewDecoder(head) + if err := h.decode(d); err != nil { + return nil, unavailable("its header does not decode: %v", err) + } + // The header is the deterministic encoding of what it says: its length + // is that of the encoding, and the signatures follow it. + var e codec.Encoder + h.encode(&e) + enc, err := e.Out() + if err != nil || !bytes.Equal(enc, head[:min(len(enc), len(head))]) { + return nil, unavailable("its header is not the deterministic encoding of its value") + } + if !bytes.Equal(h.ChainHash, p.ChainHash[:]) { + return nil, unavailable("archive of chain %s, the pinned profile %s is chain %s", chainHashHex(h.ChainHash), p.ID, p.ChainHashHex()) + } + if h.First == 0 || h.Count == 0 || c.Round < h.First || c.Round-h.First >= h.Count { + return nil, unavailable("round %d is not in the archive, which holds %d rounds from %d", c.Round, h.Count, h.First) + } + scheme, err := p.DrandScheme() + if err != nil { + return nil, unavailable("%v", err) + } + n := uint64(scheme.SigGroup.PointLen()) + if want := uint64(len(enc)) + h.Count*n; h.Count > (1<<62)/n || uint64(a.size) != want { + return nil, unavailable("%d bytes, its header announces %d rounds of %d bytes", a.size, h.Count, n) + } + sig := make([]byte, n) + if _, err := a.r.ReadAt(sig, int64(uint64(len(enc))+(c.Round-h.First)*n)); err != nil { + return nil, unavailable("%v", err) + } + if bytes.Equal(sig, make([]byte, n)) { + return nil, unavailable("round %d is missing: its entry is zeros", c.Round) + } + return EncodeRelease(Release{ChainHash: h.ChainHash, Round: c.Round, Signature: sig}) +} diff --git a/provider/provider.go b/provider/provider.go index 81e3a0e..9093eac 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -7,8 +7,12 @@ package provider import ( + "bytes" "context" + "crypto/sha256" + "encoding/hex" "fmt" + "strings" "github.com/drand/drand/v2/common" @@ -26,6 +30,10 @@ type Condition struct { type Release struct { Round uint64 Signature []byte + // ChainHash is the chain the release names, key 2 of a release object + // (spec v0.15, §47.1), or nil when it names none, as the answer of a + // relay and drand's JSON. Verify compares it with the pinned profile. + ChainHash []byte } // ReleaseSource fetches the release of a condition. Callers always verify the @@ -57,12 +65,15 @@ func (f ReleaseSourceFunc) Fetch(ctx context.Context, p *profile.Profile, c Cond } // Verify checks a release locally against the pinned profile (spec §17, §51), -// in the order of spec §63 step 10: the expected round (ErrRoundMismatch), +// in the order of spec §63 step 10: the chain hash it names, if any, which +// must be that of the pinned profile (ErrProfileMismatch, spec v0.15); the +// expected round (ErrRoundMismatch), // then the signature (ErrReleaseInvalid), which must be the canonical // encoding of a point of the signature group of the scheme other than the // point at infinity (spec §12.2), with the length of that group, and a valid // BLS signature of the round under the pinned public key. The chain hash is -// covered because the pinned public key is bound to it by profile.Validate. +// covered too because the pinned public key is bound to it by +// profile.Validate. // // The BLS verification of drand decodes the signature with the canonical // decoder of kilic/bls12-381, which rejects every other encoding, and the @@ -72,6 +83,9 @@ func Verify(p *profile.Profile, c Condition, r Release) error { if c.Round == 0 || c.Round > p.MaxRound() { return fmt.Errorf("provider: round %d outside the range of %s: %w", c.Round, p.ID, datekeys.ErrDateKeyInvalid) } + if r.ChainHash != nil && !bytes.Equal(r.ChainHash, p.ChainHash[:]) { + return fmt.Errorf("provider: release of chain %s, the pinned profile %s is chain %s: %w", chainHashHex(r.ChainHash), p.ID, p.ChainHashHex(), datekeys.ErrProfileMismatch) + } if r.Round != c.Round { return fmt.Errorf("provider: release for round %d, expected %d: %w", r.Round, c.Round, datekeys.ErrRoundMismatch) } @@ -92,3 +106,10 @@ func Verify(p *profile.Profile, c Condition, r Release) error { } return nil } + +// randomnessMatches reports whether the randomness of a drand answer, in +// hexadecimal, is SHA-256 of its signature, as drand defines it. +func randomnessMatches(randomness string, signature []byte) bool { + sum := sha256.Sum256(signature) + return strings.EqualFold(randomness, hex.EncodeToString(sum[:])) +} diff --git a/provider/release.go b/provider/release.go new file mode 100644 index 0000000..3a6655f --- /dev/null +++ b/provider/release.go @@ -0,0 +1,208 @@ +package provider + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + + datekeys "g.activething.com/go/DateKeys" + "g.activething.com/go/DateKeys/codec" + "g.activething.com/go/DateKeys/profile" +) + +// Schema constants of the release object, the content of a .dkr file (spec +// v0.15, §47.1). +const ( + ReleaseTypeTag = "datekeys-release" + ReleaseSchemaVersion = 1 +) + +// Limits of the release object (spec v0.15, §47.1). The object has no frame: +// a file or an input of more than MaxReleaseObjectSize bytes is rejected +// before it is decoded, with ErrNonCanonicalCBOR, and no valid encoding comes +// close to it. MaxSignatureLen is the longest compressed point of +// BLS12-381, one of G2; Quicknet signs with 48 bytes, a point of G1. +const ( + MaxReleaseObjectSize = 1024 + MaxSignatureLen = 96 +) + +// MaxReleaseJSONSize bounds drand's JSON, which a reader accepts too as the +// input of the caller (spec v0.15, §47.1). It is the bound of a relay +// response in provider/drand. +const MaxReleaseJSONSize = 8 << 10 + +// releaseKeys is the number of keys of the release object, all required. +const releaseKeys = 5 + +// releaseWire is the CBOR map of the release object, keys 2 to 4; keys 0 and +// 1 are the constants ReleaseTypeTag and ReleaseSchemaVersion. +type releaseWire struct { + ChainHash []byte // key 2, 32 bytes + Round uint64 // key 3, 1..2^53-1 + Signature []byte // key 4, 1..MaxSignatureLen bytes +} + +func (w *releaseWire) encode(e *codec.Encoder) { + e.Map(releaseKeys) + e.Uint(0) + e.Text(ReleaseTypeTag) + e.Uint(1) + e.Uint(ReleaseSchemaVersion) + e.Uint(2) + e.Bstr(w.ChainHash) + e.Uint(3) + e.Uint(w.Round) + e.Uint(4) + e.Bstr(w.Signature) +} + +// decode reads the map with every CDDL rule of the release object, all of +// them ErrNonCanonicalCBOR: what each field means against the pinned profile +// and the DateKey is checked by Verify, at step 10. +func (w *releaseWire) decode(d *codec.Decoder) error { + pairs, err := d.Map(releaseKeys) + if err != nil { + return err + } + if pairs != releaseKeys { + return fmt.Errorf("%d keys, want all %d: %w", pairs, releaseKeys, datekeys.ErrNonCanonicalCBOR) + } + for want := range uint64(releaseKeys) { + k, err := d.Key() + if err != nil { + return err + } + if k != want { + return fmt.Errorf("key %d where key %d was expected: %w", k, want, datekeys.ErrNonCanonicalCBOR) + } + switch k { + case 0: + _, err = d.Text(len(ReleaseTypeTag)) + case 1: + _, err = d.Uint(ReleaseSchemaVersion) + case 2: + w.ChainHash, err = d.Bstr(32, 32) + case 3: + if w.Round, err = d.Uint(codec.MaxSafeUint); err == nil && w.Round == 0 { + err = fmt.Errorf("round 0: %w", datekeys.ErrNonCanonicalCBOR) + } + case 4: + w.Signature, err = d.Bstr(1, MaxSignatureLen) + } + if err != nil { + return fmt.Errorf("key %d: %w", k, err) + } + } + return d.EndMap() +} + +// EncodeRelease returns the release object of r, the content of a .dkr file +// (spec v0.15, §47.1): its chain hash, its round and its signature. It does +// not verify the release: Verify does, against the pinned profile. +func EncodeRelease(r Release) ([]byte, error) { + switch { + case len(r.ChainHash) != 32: + return nil, fmt.Errorf("provider: release object: chain hash of %d bytes, want 32: %w", len(r.ChainHash), datekeys.ErrNonCanonicalCBOR) + case r.Round == 0 || r.Round > codec.MaxSafeUint: + return nil, fmt.Errorf("provider: release object: round %d outside 1..%d: %w", r.Round, uint64(codec.MaxSafeUint), datekeys.ErrNonCanonicalCBOR) + case len(r.Signature) == 0 || len(r.Signature) > MaxSignatureLen: + return nil, fmt.Errorf("provider: release object: signature of %d bytes outside 1..%d: %w", len(r.Signature), MaxSignatureLen, datekeys.ErrNonCanonicalCBOR) + } + w := releaseWire{ChainHash: r.ChainHash, Round: r.Round, Signature: r.Signature} + var e codec.Encoder + w.encode(&e) + return e.Out() +} + +// DecodeRelease decodes a release object (spec v0.15, §47.1) with the layers +// of spec §69.1 that it has: its size, at most MaxReleaseObjectSize bytes; +// its type and schema version (ErrNonCanonicalCBOR, then +// ErrUnsupportedVersion); its encoding and schema (ErrNonCanonicalCBOR). The +// release it returns names its chain, and Verify checks it against the pinned +// profile at step 10 of spec §63: the chain hash, the round, the signature. +func DecodeRelease(b []byte) (Release, error) { + if len(b) == 0 || len(b) > MaxReleaseObjectSize { + return Release{}, fmt.Errorf("provider: release object of %d bytes, outside 1..%d: %w", len(b), MaxReleaseObjectSize, datekeys.ErrNonCanonicalCBOR) + } + if err := codec.CheckSchema(b, ReleaseTypeTag, ReleaseSchemaVersion); err != nil { + return Release{}, fmt.Errorf("provider: release object: %w", err) + } + var w releaseWire + if err := codec.Unmarshal(b, w.decode, w.encode); err != nil { + return Release{}, fmt.Errorf("provider: release object: %w", err) + } + return Release{Round: w.Round, Signature: w.Signature, ChainHash: w.ChainHash}, nil +} + +// ParseRelease reads a release that the caller supplies: drand's JSON, when +// its first byte other than a JSON space is "{", or else a release object +// (spec v0.15, §47.1), with DecodeRelease. drand's JSON is the answer of a +// relay, {"round": …, "signature": "…"}, with an optional "randomness" that +// must be SHA-256 of the signature; it does not name its chain, so the +// release has no chain hash, and any failure to read it is +// ErrReleaseInvalid. It is accepted as input, never written. +func ParseRelease(b []byte) (Release, error) { + if t := bytes.TrimLeft(b, " \t\r\n"); len(t) > 0 && t[0] == '{' { + return parseDrandJSON(b) + } + return DecodeRelease(b) +} + +// parseDrandJSON reads the JSON of a drand relay. +func parseDrandJSON(b []byte) (Release, error) { + if len(b) > MaxReleaseJSONSize { + return Release{}, fmt.Errorf("provider: drand JSON of %d bytes, larger than %d: %w", len(b), MaxReleaseJSONSize, datekeys.ErrReleaseInvalid) + } + var wire struct { + Round *uint64 `json:"round"` + Signature *string `json:"signature"` + Randomness string `json:"randomness"` + } + if err := json.Unmarshal(b, &wire); err != nil || wire.Round == nil || wire.Signature == nil { + return Release{}, fmt.Errorf("provider: drand JSON: malformed, or without round or signature: %w", datekeys.ErrReleaseInvalid) + } + sig, err := hex.DecodeString(*wire.Signature) + if err != nil { + return Release{}, fmt.Errorf("provider: drand JSON: signature is not hex: %w", datekeys.ErrReleaseInvalid) + } + if wire.Randomness != "" && !randomnessMatches(wire.Randomness, sig) { + return Release{}, fmt.Errorf("provider: drand JSON: randomness does not match the signature: %w", datekeys.ErrReleaseInvalid) + } + return Release{Round: *wire.Round, Signature: sig}, nil +} + +// Supplier hands over a release that the caller has in hand (spec v0.15, +// §49, §63 step 9.c): a release object read from a .dkr file, drand's JSON +// that the person saved, or an entry of a local archive. It makes no network +// request, so capsule.Open asks it for the release without comparing its +// clock with the round time: a valid signature proves that the round was +// published. +// +// Supply returns the encoding of the release of c, as it is: a release +// object or drand's JSON, which capsule.Open decodes and verifies at step 10 +// with the codes of that step. Without a release for c it returns an error +// that wraps datekeys.ErrReleaseUnavailable, the code of step 9. +type Supplier interface { + Supply(p *profile.Profile, c Condition) ([]byte, error) +} + +// Encoded is a release in hand, already read: the bytes of a .dkr file or of +// drand's JSON. It supplies itself whatever the condition; step 10 compares +// its round with the DateKey. +type Encoded []byte + +// Supply implements Supplier. +func (e Encoded) Supply(*profile.Profile, Condition) ([]byte, error) { return e, nil } + +// NewReleaseObject returns the release object of a release of the profile p, +// with the chain hash of p: what a reader saves as a .dkr after verifying the +// release (spec v0.15, §62.1). +func NewReleaseObject(p *profile.Profile, r Release) ([]byte, error) { + r.ChainHash = p.ChainHash[:] + return EncodeRelease(r) +} + +// chainHashHex is the chain hash of a release in the text of an error. +func chainHashHex(b []byte) string { return hex.EncodeToString(b) } diff --git a/provider/release_test.go b/provider/release_test.go new file mode 100644 index 0000000..8daf6e6 --- /dev/null +++ b/provider/release_test.go @@ -0,0 +1,187 @@ +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 .dkr 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.dkr")) { + 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.dkr") + for name, c := range map[string]struct { + a *provider.Archive + p *profile.Profile + }{ + "another chain": {a, other}, + "a byte missing": {short, p}, + "a .dkr": {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.dkr")) + 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 +} diff --git a/scripts/fuzz.sh b/scripts/fuzz.sh index 716cf74..a618a63 100644 --- a/scripts/fuzz.sh +++ b/scripts/fuzz.sh @@ -19,6 +19,7 @@ targets=( "./codec FuzzEncodeImpliesWalk" "./extension FuzzDecodeArray" "./profile FuzzDecode" + "./provider FuzzDecodeRelease" "./datekey FuzzParse" "./agewrap FuzzStanzas" "./accesskey FuzzDecode" diff --git a/testdata/README.md b/testdata/README.md index 3de1177..579909e 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -22,6 +22,10 @@ committed file changes: every file below is exactly what the implementation computes today. The `spec` field of every file is `"0.14"`, the version this module declares. +The branch `v0.15` adds what the draft v0.15 defines, the release object and +a release in the caller's hand (spec v0.15, §47.1, §63 step 9.c), and keeps +that field until the draft is approved: `vectors/release.json`, the files of +`releases/`, and the field `source` of `mutations.json`. What v0.12 changes from v0.11, the texts of the verdicts of a certificate and of a seal, the profile of a certificate and the rules of the addresses and of the padding of a locator, is in the files: the verdicts and the lines of @@ -48,6 +52,9 @@ Conventions for every file: | `vectors/dk1.json` | canonical `dk1_` strings, and rejected encodings with their code | §18, §19, §66 | | `vectors/cbor.json` | the CBOR profile, and one block of vectors per schema, CONTROL_CBOR in the three formats | §58, CDDL | | `vectors/tlock_ibe.json` | H2 of the tlock IBE: the serialization of an element of GT | §63 step 11 | +| `vectors/release.json` | the release object, the content of a `.dkr` file, and drand's JSON, each with the result of step 10; the lookups of a local release archive | §47.1, §50, §63 step 10 (v0.15) | +| `releases/.dkr` | the release object of each published round of the tests: 1000, 1001, 1004 and 2000 | §47.1 (v0.15) | +| `releases/archive_1000_1004.bin` | a local release archive, the informative format of §50, of rounds 1000 to 1004, two of them missing | §50 (v0.15) | | `vectors/tlock_steps.json` | steps 10 and 11 for Quicknet value by value: the message of a round, its hash to G1, and the decryption of a tlock stanza with H2, H4, H3 and the file key | §63 steps 10 and 11 (v0.14) | | `vectors/padding.json` | the padding of formats 2 and 3: P for each content length L, and the length of PAYLOAD_AGE | §29.1 | | `vectors/paths.json` | the paths of a format 3 head: the rules of one entry, and those of the paths of a head | §29.5 | @@ -60,7 +67,7 @@ Conventions for every file: | `vectors/resolved_ip.json` | the IP address a name of a locator resolves to, NAT64 included, and whether a reader may connect | §44.1 (v0.13) | | `vectors/wordkey.json` | the key of words: the words of a text, what a writer refuses, and the identity the words derive | §38.1, §64 | | `vectors/locator.json` | the extension `datekeys.capsule` of a `.dkk`, its envelope and its locator, and what a reader rejects and uses of them | §44.1, §64 | -| `vectors/mutations.json` | the mutation corpus: the 178 mutations of §64 and further cases | §63, §64 | +| `vectors/mutations.json` | the mutation corpus: the 178 mutations of §64 and further cases, each with the kind of its release source | §63, §64 | | `vectors/inspect_differential.json` | 5110 mutations of fourteen fixtures with the verdict of steps 1 to 8 | §63 | | `fixtures/.dkc`, `.json` | official capsules and every intermediate value | §67 | | `fixtures/.dkk`, `.dkk.json` | official access keys | §68 | @@ -312,6 +319,58 @@ serialization at once. The same 576 bytes with the twelve coordinates of Fp in reverse order, c0 first at every level as `Fp12.toBytes` of `@noble/curves` writes them, give `0118eea9d5971745f71e3c94926f1717` and another FK_TIME. +## `vectors/release.json` and `releases/` + +The release object of spec v0.15, §47.1: the release of a round as a file, +`.dkr`, that a person keeps next to the capsule. It is deterministic CBOR with +the profile of §58, a map of five keys, all required: + +```text +0 → "datekeys-release" +1 → 1 +2 → chain_hash (32 bytes) +3 → round (1 to 2^53 − 1) +4 → signature (1 to 96 bytes; 48 in Quicknet) +``` + +The object of a round above 255 measures 111 bytes. `releases/.dkr` is +the object of each published round the fixtures use, 1000, 1001, 1004 and +2000, with the Quicknet chain hash: the release that opens each fixture, as a +file. + +`release.json` has three lists: + +- `objects`: an `encoding` in hexadecimal, the `round` of the DateKey it is + checked against, and the `result`, `ok` or a code, with the `text` of the + error of the reference. When the encoding decodes, `release` is what it + says: `round`, `signature` and `chain_hash`. The checks are those of step + 10 for a release in the caller's hand (spec v0.15, §63): the size of the + object, from 1 to 1024 bytes, before anything else; its type and version; + its encoding and schema (all three `ERR_NON_CANONICAL_CBOR`, but a version + other than 1, `ERR_UNSUPPORTED_VERSION`); then, against the pinned profile + and the DateKey, the chain hash (`ERR_PROFILE_MISMATCH`), the round + (`ERR_ROUND_MISMATCH`) and the signature (`ERR_RELEASE_INVALID`), in that + order. A case with several faults gets the code of the first. +- `json`: drand's JSON, which a reader accepts too as the input of the + caller, never as the release object: an `input` whose first byte other than + a JSON space is `{`. It has `round` and `signature` in hexadecimal, of either + case, and may have `randomness`, which must then be SHA-256 of the + signature; it names no chain, so its `release` has no `chain_hash`. Any + failure to read it is `ERR_RELEASE_INVALID`, and so is one of more than + 8192 bytes; then the round and the signature, as for an object. +- `archive`: the lookups of the local archive `releases/archive_1000_1004.bin`, + an informative format (spec v0.15, §50). It is the `header`, the + deterministic CBOR map `{0: "datekeys-release-archive", 1: 1, 2: chain_hash, + 3: first round, 4: number of rounds}`, followed by the 48-byte signature of + each round, one after another; a round the archive lacks is 48 zero bytes. + This one holds rounds 1000 to 1004, and 1002 and 1003 are zeros. Each lookup + gives a `round` and its `result`: `ok` with the `encoding` of the release + object the archive supplies, the `.dkr` of that round, or + `ERR_RELEASE_UNAVAILABLE` for a round the archive lacks or does not cover. + +The texts are those of the reference, for an implementation that wants to +match them; the codes are normative. + ## `vectors/tlock_steps.json` Steps 10 and 11 of spec §63 for Quicknet, every intermediate value written @@ -753,6 +812,7 @@ reading flow (`capsule.Open`, §63) must fail. "spec": true, "dkc": { "base": "time_only.dkc", "edits": [[4, 1, "04"]] }, "release": { "round": 1000, "signature": "b446…" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -770,8 +830,9 @@ reading flow (`capsule.Open`, §63) must fail. named "format 2: …" (66 to 98), the 23 of the list of format 2 (99 to 121), 5 further cases (122 to 126), the same 33 on the format 3 fixtures, named "format 3: …" (127 to 159), the 48 of the list of format 3 (160 to 207), the - 8 of the list of v0.11 that a capsule can hold (208 to 215), and 3 further - cases (216 to 218). A line of the lists of §64 with several values, such as + 8 of the list of v0.11 that a capsule can hold (208 to 215), 3 further + cases (216 to 218), and the 4 cases of the source of the release of v0.15 + (219 to 222). A line of the lists of §64 with several values, such as "AREA_LEN 0, 511, 513 o 66048", is one case for each. - `dkc`: the capsule, as edits of a fixture (see above). The reader gets it as a seekable file, so that the `capsule_digest` of an offered `.dkk` is checked @@ -784,13 +845,23 @@ reading flow (`capsule.Open`, §63) must fail. is asked for. The reader must verify it (§51): a case may serve a release of another round, a round with the signature of another, or a signature that is not the canonical encoding of a point (§12.2). `null` means that no release - is available (`ERR_RELEASE_UNAVAILABLE`). The release is supplied directly, - as the caller's own, so one that breaks the rules of step 10 gets the code - of step 10; a source that fetched it over a network would have discarded - it, and the code would be `ERR_RELEASE_UNAVAILABLE` at step 9 (spec §63 - steps 9 and 10). -- `now`: the reader's clock, RFC 3339. No release is requested before the round - time of the DateKey. + is available (`ERR_RELEASE_UNAVAILABLE`). `chain_hash`, present in two + cases only, is the chain the release object names when it is not the + Quicknet chain. +- `source` (v0.15): what kind of source answers, spec v0.15 §63 step 9: + - `supplied`: the release is in the caller's hand, as a `.dkr` would be. + The reader is given the release object of `release`, with the Quicknet + chain hash unless `chain_hash` says another, and decodes and verifies it + at step 10: one that breaks a rule of step 10 gets the code of step 10. + The clock is not compared with the round time (step 9.c): with a `now` + before it, the capsule opens all the same. Every case but three is + `supplied`. + - `network`: a network source, such as a drand relay. It is never asked + before the round time (step 9.c), and it verifies its answer with the + rules of step 10 and discards it when it fails, so the reader gets no + release: `ERR_RELEASE_UNAVAILABLE` at step 9. +- `now`: the reader's clock, RFC 3339. A network source is not asked before + the round time of the DateKey; a release in hand is not compared with it. - `registry`: `default` pins exactly the Quicknet profile of `profile_quicknet.json`; `empty` pins none. - `extensions`: the extensions the application implements. Each entry is known @@ -803,9 +874,10 @@ reading flow (`capsule.Open`, §63) must fail. data" and "known critical .dkk extension with invalid data", at steps 4, 14 and 9, depend on it. Absent: the application knows no extension, the state of the base protocol V1. -- `network`: whether the failure may come after a release request. When false, - the reader must fail without requesting any release (§27, §63): every failure - of steps 1 to 8 and of step 9 before the request (9.a to 9.c). +- `network`: whether the failure may come after a release request, to a + network source or to the caller's release in hand. When false, the reader + must fail without requesting any release (§27, §63): every failure of steps + 1 to 8 and of step 9 before the request (9.a to 9.c). - `frozen`: the capsule was built once with age randomness; its bytes are kept and never regenerated. These cases have no `base`. - `error`, `step`: the expected code and the step of §63 that fails. For a @@ -815,6 +887,19 @@ reading flow (`capsule.Open`, §63) must fail. format 3, and seven of the list of v0.11. Every case reproduces offline: the recorded release stands in for the network. + +What v0.15 changed in this file: every case gained `source`, `supplied` but +for three; the further case "round not reached yet", a valid release of round +1000 and a clock one nanosecond before its round time, was +`ERR_RELEASE_UNAVAILABLE` at step 9 and now opens (`ok`, step 0, with +`network` true), because the release is in the caller's hand; and four cases +were added at the end: the same capsule and clock with a network source, +still `ERR_RELEASE_UNAVAILABLE` at step 9 without any request; a release of +another round from a network source, discarded, `ERR_RELEASE_UNAVAILABLE` at +step 9, where the same release in hand is `ERR_ROUND_MISMATCH` at step 10; +and two release objects of another chain, `ERR_PROFILE_MISMATCH` at step 10, +one of them of another round too and with a clock behind. No other case +changed its result. A reader that implements only steps 1 to 8 can replay every case whose `step` is at most 8: 57 cases, 39 of them from §64. Steps 1 to 8 are summarised in "The checks of steps 1 to 8" below. @@ -825,7 +910,8 @@ What happens between step 8 and the release request is spec §63 step 9: for `time_and_key` only, an offered `.dkk` is checked as an object and then bound to the capsule (9.a), at least one credential must be offered (9.b), and for either policy a `now` before the round time of the DateKey fails without a -request (9.c); only then is the release requested. For `time_only` the +request to a network source (9.c), while a release in hand is not compared +with it; only then is the release requested. For `time_only` the credentials play no part: the §64 case "access_policy=time_only with time_and_key structure" offers a `.dkk` whose `capsule_digest` is that of the unmutated capsule, and fails at step 12, not at step 9. The codes after the @@ -834,8 +920,9 @@ file (steps 11, 13 and 17), are those of spec §63 as well. In this corpus: - identities are not examined before the release (spec §63 step 13); - a `release` of `null` is `ERR_RELEASE_UNAVAILABLE` at step 9; -- every `release` is supplied directly, so an invalid one fails at step 10: - the source is not a network source, which would discard it at step 9; +- a `release` of a `supplied` case is in the caller's hand, so an invalid one + fails at step 10, and step 9.c does not apply to it; a `network` source + discards it at step 9, and is not asked before the round time; - every `.dkk` offered decodes: the corpus checks step 9.a, not the decoding of a `.dkk`, whose errors spec §63 also places at step 9.a. diff --git a/testdata/releases/1000.dkr b/testdata/releases/1000.dkr new file mode 100644 index 0000000..ca67205 Binary files /dev/null and b/testdata/releases/1000.dkr differ diff --git a/testdata/releases/1001.dkr b/testdata/releases/1001.dkr new file mode 100644 index 0000000..4bb8bd7 Binary files /dev/null and b/testdata/releases/1001.dkr differ diff --git a/testdata/releases/1004.dkr b/testdata/releases/1004.dkr new file mode 100644 index 0000000..58772ed Binary files /dev/null and b/testdata/releases/1004.dkr differ diff --git a/testdata/releases/2000.dkr b/testdata/releases/2000.dkr new file mode 100644 index 0000000..55a1eac Binary files /dev/null and b/testdata/releases/2000.dkr differ diff --git a/testdata/releases/archive_1000_1004.bin b/testdata/releases/archive_1000_1004.bin new file mode 100644 index 0000000..06ca9d7 Binary files /dev/null and b/testdata/releases/archive_1000_1004.bin differ diff --git a/testdata/vectors/mutations.json b/testdata/vectors/mutations.json index fe2ff9f..185038b 100644 --- a/testdata/vectors/mutations.json +++ b/testdata/vectors/mutations.json @@ -14,6 +14,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -33,6 +34,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -51,6 +53,7 @@ "round": 1001, "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -71,6 +74,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -91,6 +95,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -111,6 +116,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -131,6 +137,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -151,6 +158,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -171,6 +179,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -191,6 +200,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -214,6 +224,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -237,6 +248,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -255,6 +267,7 @@ "round": 1000, "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -276,6 +289,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -299,6 +313,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -318,6 +333,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -337,6 +353,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -359,6 +376,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": true, @@ -378,6 +396,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -397,6 +416,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -418,6 +438,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -439,6 +460,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -460,6 +482,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -481,6 +504,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -502,6 +526,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -523,6 +548,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -545,6 +571,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -567,6 +594,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -586,6 +614,7 @@ "round": 1004, "signature": "be076aa25a40df5b4d22f9f7bcedaff30dcac20d94556107b8e652c7b54b2a440ce796f9fa53ad28023c84b40a580f55" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": true, @@ -604,6 +633,7 @@ "round": 1000, "signature": "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -622,6 +652,7 @@ "round": 1000, "signature": "d44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -640,6 +671,7 @@ "round": 1000, "signature": "944679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -661,6 +693,7 @@ "round": 1000, "signature": "944679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -681,6 +714,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -701,6 +735,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -721,6 +756,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -741,6 +777,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -761,6 +798,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -781,6 +819,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -801,6 +840,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -821,6 +861,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -842,6 +883,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -862,6 +904,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -881,6 +924,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -900,6 +944,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -919,6 +964,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -938,6 +984,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": true, @@ -957,6 +1004,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "extensions": [ @@ -983,6 +1031,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "extensions": [ @@ -1009,6 +1058,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "extensions": [ @@ -1037,6 +1087,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1058,6 +1109,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1076,6 +1128,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1095,6 +1148,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1116,6 +1170,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1137,6 +1192,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1155,12 +1211,13 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:23.999999999Z", "registry": "default", - "network": false, + "network": true, "frozen": false, - "error": "ERR_RELEASE_UNAVAILABLE", - "step": 9 + "error": "ok", + "step": 0 }, { "name": "release source unavailable", @@ -1170,6 +1227,7 @@ "edits": [] }, "release": null, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1190,6 +1248,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1210,6 +1269,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1230,6 +1290,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1248,6 +1309,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "empty", "network": false, @@ -1267,6 +1329,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": true, @@ -1289,6 +1352,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": true, @@ -1311,6 +1375,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": true, @@ -1330,6 +1395,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1349,6 +1415,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1367,6 +1434,7 @@ "round": 1001, "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1387,6 +1455,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1407,6 +1476,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1427,6 +1497,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1447,6 +1518,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1467,6 +1539,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1487,6 +1560,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1507,6 +1581,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1530,6 +1605,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1553,6 +1629,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1571,6 +1648,7 @@ "round": 1000, "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1592,6 +1670,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1615,6 +1694,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1634,6 +1714,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -1653,6 +1734,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -1675,6 +1757,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": true, @@ -1694,6 +1777,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -1713,6 +1797,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -1734,6 +1819,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1755,6 +1841,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1776,6 +1863,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -1797,6 +1885,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1818,6 +1907,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1839,6 +1929,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1861,6 +1952,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1883,6 +1975,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1902,6 +1995,7 @@ "round": 1004, "signature": "be076aa25a40df5b4d22f9f7bcedaff30dcac20d94556107b8e652c7b54b2a440ce796f9fa53ad28023c84b40a580f55" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": true, @@ -1920,6 +2014,7 @@ "round": 1000, "signature": "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1938,6 +2033,7 @@ "round": 1000, "signature": "d44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1956,6 +2052,7 @@ "round": 1000, "signature": "944679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1977,6 +2074,7 @@ "round": 1000, "signature": "944679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -1997,6 +2095,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2017,6 +2116,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2040,6 +2140,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2060,6 +2161,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2083,6 +2185,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2108,6 +2211,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2133,6 +2237,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2158,6 +2263,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2179,6 +2285,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2203,6 +2310,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2227,6 +2335,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2247,6 +2356,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2267,6 +2377,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2287,6 +2398,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2310,6 +2422,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2333,6 +2446,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2356,6 +2470,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2376,6 +2491,7 @@ "round": 2000, "signature": "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e" }, + "source": "supplied", "now": "2023-08-23T16:49:24Z", "registry": "default", "network": true, @@ -2397,6 +2513,7 @@ "round": 2000, "signature": "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e" }, + "source": "supplied", "now": "2023-08-23T16:49:24Z", "registry": "default", "network": true, @@ -2418,6 +2535,7 @@ "round": 2000, "signature": "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e" }, + "source": "supplied", "now": "2023-08-23T16:49:24Z", "registry": "default", "network": true, @@ -2439,6 +2557,7 @@ "round": 2000, "signature": "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e" }, + "source": "supplied", "now": "2023-08-23T16:49:24Z", "registry": "default", "network": true, @@ -2459,6 +2578,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2479,6 +2599,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2500,6 +2621,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -2521,6 +2643,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -2544,6 +2667,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -2567,6 +2691,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -2590,6 +2715,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -2609,6 +2735,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2628,6 +2755,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2646,6 +2774,7 @@ "round": 1001, "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2666,6 +2795,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -2686,6 +2816,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -2706,6 +2837,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -2726,6 +2858,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -2746,6 +2879,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2766,6 +2900,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2786,6 +2921,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2809,6 +2945,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -2832,6 +2969,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -2850,6 +2988,7 @@ "round": 1000, "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2871,6 +3010,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2894,6 +3034,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -2913,6 +3054,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -2932,6 +3074,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -2954,6 +3097,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": true, @@ -2973,6 +3117,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -2992,6 +3137,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": false, @@ -3013,6 +3159,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -3034,6 +3181,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -3055,6 +3203,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, @@ -3076,6 +3225,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3097,6 +3247,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3118,6 +3269,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3140,6 +3292,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3162,6 +3315,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3181,6 +3335,7 @@ "round": 1004, "signature": "be076aa25a40df5b4d22f9f7bcedaff30dcac20d94556107b8e652c7b54b2a440ce796f9fa53ad28023c84b40a580f55" }, + "source": "supplied", "now": "2024-08-23T15:09:27Z", "registry": "default", "network": true, @@ -3199,6 +3354,7 @@ "round": 1000, "signature": "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3217,6 +3373,7 @@ "round": 1000, "signature": "d44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3235,6 +3392,7 @@ "round": 1000, "signature": "944679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3256,6 +3414,7 @@ "round": 1000, "signature": "944679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3276,6 +3435,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3297,6 +3457,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3318,6 +3479,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3339,6 +3501,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3360,6 +3523,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3381,6 +3545,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3402,6 +3567,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3423,6 +3589,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3444,6 +3611,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3465,6 +3633,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3488,6 +3657,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3509,6 +3679,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3530,6 +3701,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3551,6 +3723,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3574,6 +3747,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3597,6 +3771,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3620,6 +3795,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3643,6 +3819,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3666,6 +3843,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3689,6 +3867,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3712,6 +3891,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3735,6 +3915,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3758,6 +3939,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3781,6 +3963,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3802,6 +3985,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3825,6 +4009,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3848,6 +4033,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3872,6 +4058,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3895,6 +4082,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3918,6 +4106,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3940,6 +4129,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3964,6 +4154,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -3985,6 +4176,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4006,6 +4198,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4029,6 +4222,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4050,6 +4244,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4074,6 +4269,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4098,6 +4294,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4121,6 +4318,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4142,6 +4340,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4163,6 +4362,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4186,6 +4386,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4209,6 +4410,7 @@ "round": 1001, "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41" }, + "source": "supplied", "now": "2023-08-23T15:59:27Z", "registry": "default", "network": true, @@ -4232,6 +4434,7 @@ "round": 1001, "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41" }, + "source": "supplied", "now": "2023-08-23T15:59:27Z", "registry": "default", "network": true, @@ -4253,6 +4456,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4282,6 +4486,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4311,6 +4516,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4340,6 +4546,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4369,6 +4576,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4398,6 +4606,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4426,6 +4635,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4455,6 +4665,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4485,6 +4696,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4515,6 +4727,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4544,6 +4757,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4571,6 +4785,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4591,6 +4806,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4614,6 +4830,7 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": true, @@ -4635,12 +4852,91 @@ "round": 1000, "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" }, + "source": "supplied", "now": "2023-08-23T15:59:24Z", "registry": "default", "network": false, "frozen": false, "error": "ERR_ACCESS_INVALID", "step": 9 + }, + { + "name": "round not reached yet, from a network source", + "spec": false, + "dkc": { + "base": "time_only.dkc", + "edits": [] + }, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "source": "network", + "now": "2023-08-23T15:59:23.999999999Z", + "registry": "default", + "network": false, + "frozen": false, + "error": "ERR_RELEASE_UNAVAILABLE", + "step": 9 + }, + { + "name": "release of another round, from a network source", + "spec": false, + "dkc": { + "base": "time_only.dkc", + "edits": [] + }, + "release": { + "round": 1001, + "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41" + }, + "source": "network", + "now": "2023-08-23T15:59:24Z", + "registry": "default", + "network": true, + "frozen": false, + "error": "ERR_RELEASE_UNAVAILABLE", + "step": 9 + }, + { + "name": "release object of another chain", + "spec": false, + "dkc": { + "base": "time_only.dkc", + "edits": [] + }, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e970" + }, + "source": "supplied", + "now": "2023-08-23T15:59:24Z", + "registry": "default", + "network": true, + "frozen": false, + "error": "ERR_PROFILE_MISMATCH", + "step": 10 + }, + { + "name": "release object of another chain and another round, with a clock behind", + "spec": false, + "dkc": { + "base": "time_only.dkc", + "edits": [] + }, + "release": { + "round": 1001, + "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41", + "chain_hash": "d2db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "source": "supplied", + "now": "2023-08-23T15:59:23.999999999Z", + "registry": "default", + "network": true, + "frozen": false, + "error": "ERR_PROFILE_MISMATCH", + "step": 10 } ] } diff --git a/testdata/vectors/release.json b/testdata/vectors/release.json new file mode 100644 index 0000000..51766bf --- /dev/null +++ b/testdata/vectors/release.json @@ -0,0 +1,474 @@ +{ + "spec": "0.14", + "description": "The release object, the content of a .dkr file (spec v0.15, §47.1), and drand's JSON as the input of the caller, each checked against the pinned Quicknet profile and the round of a DateKey as step 10 of spec §63 checks a release that the caller supplies; and the lookups of a local release archive (spec v0.15, §50). See testdata/README.md.", + "profile": "datekeys:quicknet:v1", + "objects": [ + { + "name": "round 1000", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "result": "ok" + }, + { + "name": "round 1001", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e9045830b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41", + "round": 1001, + "release": { + "round": 1001, + "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "result": "ok" + }, + { + "name": "round 1004", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903ec045830a40658b820c0f8c10207524179a2031ba9537688a0d04e4851b58026be9a341fee3b96fb48ffad28483d84b40a5864aa", + "round": 1004, + "release": { + "round": 1004, + "signature": "a40658b820c0f8c10207524179a2031ba9537688a0d04e4851b58026be9a341fee3b96fb48ffad28483d84b40a5864aa", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "result": "ok" + }, + { + "name": "round 2000", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031907d0045830b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e", + "round": 2000, + "release": { + "round": 2000, + "signature": "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "result": "ok" + }, + { + "name": "no byte", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object of 0 bytes, outside 1..1024: ERR_NON_CANONICAL_CBOR" + }, + { + "name": "1025 bytes: the object of round 1000 followed by zeros", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e390000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object of 1025 bytes, outside 1..1024: ERR_NON_CANONICAL_CBOR" + }, + { + "name": "1025 bytes of an object of version 2: the size first", + "encoding": "a20070646174656b6579732d72656c6561736501020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object of 1025 bytes, outside 1..1024: ERR_NON_CANONICAL_CBOR" + }, + { + "name": "type tag of the Provider Profile, version 2", + "encoding": "a5007819646174656b6579732d70726f76696465722d70726f66696c65010202582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object: codec: type \"datekeys-provider-profile\", want \"datekeys-release\": ERR_NON_CANONICAL_CBOR" + }, + { + "name": "type tag in upper case", + "encoding": "a50070444154454b4559532d52454c45415345010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object: codec: type \"DATEKEYS-RELEASE\", want \"datekeys-release\": ERR_NON_CANONICAL_CBOR" + }, + { + "name": "version 2", + "encoding": "a50070646174656b6579732d72656c65617365010202582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "result": "ERR_UNSUPPORTED_VERSION", + "text": "provider: release object: codec: datekeys-release schema version 2, want 1: ERR_UNSUPPORTED_VERSION" + }, + { + "name": "version 2, an unknown key and another chain", + "encoding": "a60070646174656b6579732d72656c65617365010202582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e970031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e390500", + "round": 1000, + "result": "ERR_UNSUPPORTED_VERSION", + "text": "provider: release object: codec: datekeys-release schema version 2, want 1: ERR_UNSUPPORTED_VERSION" + }, + { + "name": "version 0", + "encoding": "a50070646174656b6579732d72656c65617365010002582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "result": "ERR_UNSUPPORTED_VERSION", + "text": "provider: release object: codec: datekeys-release schema version 0, want 1: ERR_UNSUPPORTED_VERSION" + }, + { + "name": "keys 0 and 1 swapped", + "encoding": "a501010070646174656b6579732d72656c6561736502582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object: codec: offset 2: map key 1 where key 0 was expected: ERR_NON_CANONICAL_CBOR" + }, + { + "name": "a byte after the object", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e3900", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object: codec: offset 111: 1 trailing bytes: ERR_NON_CANONICAL_CBOR" + }, + { + "name": "without the signature", + "encoding": "a40070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object: 4 keys, want all 5: ERR_NON_CANONICAL_CBOR" + }, + { + "name": "a key 5", + "encoding": "a60070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e390500", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object: codec: offset 1: map of 6 entries, at most 5: ERR_NON_CANONICAL_CBOR" + }, + { + "name": "chain hash of 31 bytes", + "encoding": "a50070646174656b6579732d72656c65617365010102581f52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e9031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object: key 2: codec: offset 24: a byte string of 31 bytes outside 32..32: ERR_NON_CANONICAL_CBOR" + }, + { + "name": "chain hash as text", + "encoding": "a50070646174656b6579732d72656c65617365010102784035326462396261373065306363306636656166373830336464303734343761316635343737373335666433663636313739326261393436303063383465393731031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object: key 2: codec: offset 22: a text string where a byte string was expected: ERR_NON_CANONICAL_CBOR" + }, + { + "name": "round 0", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e9710300045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object: key 3: round 0: ERR_NON_CANONICAL_CBOR" + }, + { + "name": "round 2^53", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031b0020000000000000045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object: key 3: codec: offset 66: unsigned integer 9007199254740992 above 9007199254740991: ERR_NON_CANONICAL_CBOR" + }, + { + "name": "round 1000 in four bytes", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031a000003e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object: key 3: codec: offset 57: 1000 is not in its shortest form (initial byte 0x1a): ERR_NON_CANONICAL_CBOR" + }, + { + "name": "signature of no byte", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e80440", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object: key 4: codec: offset 62: a byte string of 0 bytes outside 1..96: ERR_NON_CANONICAL_CBOR" + }, + { + "name": "signature of 97 bytes", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045861b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e3900", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object: key 4: codec: offset 63: a byte string of 97 bytes outside 1..96: ERR_NON_CANONICAL_CBOR" + }, + { + "name": "map of indefinite length", + "encoding": "bf0070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39ff", + "round": 1000, + "result": "ERR_NON_CANONICAL_CBOR", + "text": "provider: release object: codec: offset 0: indefinite length (initial byte 0xbf): ERR_NON_CANONICAL_CBOR" + }, + { + "name": "another chain", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e970031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e970" + }, + "result": "ERR_PROFILE_MISMATCH", + "text": "provider: release of chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e970, the pinned profile datekeys:quicknet:v1 is chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971: ERR_PROFILE_MISMATCH" + }, + { + "name": "another chain, another round and a signature of no point", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e970031903e9045830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "round": 1000, + "release": { + "round": 1001, + "signature": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e970" + }, + "result": "ERR_PROFILE_MISMATCH", + "text": "provider: release of chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e970, the pinned profile datekeys:quicknet:v1 is chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971: ERR_PROFILE_MISMATCH" + }, + { + "name": "round 1001 for a DateKey of round 1000", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e9045830b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41", + "round": 1000, + "release": { + "round": 1001, + "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "result": "ERR_ROUND_MISMATCH", + "text": "provider: release for round 1001, expected 1000: ERR_ROUND_MISMATCH" + }, + { + "name": "another round and a signature of 47 bytes", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e904582fb33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa", + "round": 1000, + "release": { + "round": 1001, + "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "result": "ERR_ROUND_MISMATCH", + "text": "provider: release for round 1001, expected 1000: ERR_ROUND_MISMATCH" + }, + { + "name": "the signature of round 1001 as round 1000", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41", + "round": 1000, + "release": { + "round": 1000, + "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "result": "ERR_RELEASE_INVALID", + "text": "provider: the signature is not a canonical point encoding, or does not verify as the BLS signature of round 1000 under datekeys:quicknet:v1: ERR_RELEASE_INVALID" + }, + { + "name": "signature of 47 bytes", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e804582fb44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e", + "round": 1000, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "result": "ERR_RELEASE_INVALID", + "text": "provider: signature is 47 bytes, bls-unchained-g1-rfc9380 uses 48: ERR_RELEASE_INVALID" + }, + { + "name": "signature of 96 bytes, the size of G2", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045860b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "result": "ERR_RELEASE_INVALID", + "text": "provider: signature is 96 bytes, bls-unchained-g1-rfc9380 uses 48: ERR_RELEASE_INVALID" + }, + { + "name": "signature of zeros", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "round": 1000, + "release": { + "round": 1000, + "signature": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "result": "ERR_RELEASE_INVALID", + "text": "provider: the signature is not a canonical point encoding, or does not verify as the BLS signature of round 1000 under datekeys:quicknet:v1: ERR_RELEASE_INVALID" + }, + { + "name": "signature the point at infinity", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "round": 1000, + "release": { + "round": 1000, + "signature": "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "result": "ERR_RELEASE_INVALID", + "text": "provider: the signature is not a canonical point encoding, or does not verify as the BLS signature of round 1000 under datekeys:quicknet:v1: ERR_RELEASE_INVALID" + }, + { + "name": "signature with the infinity flag and a payload", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830d44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "release": { + "round": 1000, + "signature": "d44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "result": "ERR_RELEASE_INVALID", + "text": "provider: the signature is not a canonical point encoding, or does not verify as the BLS signature of round 1000 under datekeys:quicknet:v1: ERR_RELEASE_INVALID" + }, + { + "name": "signature negated", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830944679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "round": 1000, + "release": { + "round": 1000, + "signature": "944679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "result": "ERR_RELEASE_INVALID", + "text": "provider: the signature is not a canonical point encoding, or does not verify as the BLS signature of round 1000 under datekeys:quicknet:v1: ERR_RELEASE_INVALID" + }, + { + "name": "signature of round 1004 with x + p", + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903ec045830be076aa25a40df5b4d22f9f7bcedaff30dcac20d94556107b8e652c7b54b2a440ce796f9fa53ad28023c84b40a580f55", + "round": 1004, + "release": { + "round": 1004, + "signature": "be076aa25a40df5b4d22f9f7bcedaff30dcac20d94556107b8e652c7b54b2a440ce796f9fa53ad28023c84b40a580f55", + "chain_hash": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + }, + "result": "ERR_RELEASE_INVALID", + "text": "provider: the signature is not a canonical point encoding, or does not verify as the BLS signature of round 1004 under datekeys:quicknet:v1: ERR_RELEASE_INVALID" + } + ], + "json": [ + { + "name": "the answer of a relay, API v2", + "input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "result": "ok" + }, + { + "name": "with randomness, API v1", + "input": "{\"round\":1000,\"randomness\":\"fe290beca10872ef2fb164d2aa4442de4566183ec51c56ff3cd603d930e54fdd\",\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "result": "ok" + }, + { + "name": "spaces and a line feed before and after", + "input": " \n{\"round\": 1000, \"signature\": \"B44679B9A59AF2EC876B1A6B1AD52EA9B1615FC3982B19576350F93447CB1125E342B73A8DD2BACBE47E4B6B63ED5E39\"}\n", + "round": 1000, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "result": "ok" + }, + { + "name": "randomness of another signature", + "input": "{\"round\":1000,\"randomness\":\"b0a591ad1b9002cfc372ac6e30abd3da8d3088d4caae026961743474fb94235f\",\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: randomness does not match the signature: ERR_RELEASE_INVALID" + }, + { + "name": "without signature", + "input": "{\"round\":1000}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "without round", + "input": "{\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "signature not in hexadecimal", + "input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5ezz\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: signature is not hex: ERR_RELEASE_INVALID" + }, + { + "name": "round as a string", + "input": "{\"round\":\"1000\",\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "not JSON", + "input": "{\"round\":1000,", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "8193 bytes", + "input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"} ", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON of 8193 bytes, larger than 8192: ERR_RELEASE_INVALID" + }, + { + "name": "round 1001 for a DateKey of round 1000", + "input": "{\"round\":1001,\"signature\":\"b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41\"}", + "round": 1000, + "release": { + "round": 1001, + "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41" + }, + "result": "ERR_ROUND_MISMATCH", + "text": "provider: release for round 1001, expected 1000: ERR_ROUND_MISMATCH" + }, + { + "name": "the signature of round 1001 as round 1000", + "input": "{\"round\":1000,\"signature\":\"b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41\"}", + "round": 1000, + "release": { + "round": 1000, + "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41" + }, + "result": "ERR_RELEASE_INVALID", + "text": "provider: the signature is not a canonical point encoding, or does not verify as the BLS signature of round 1000 under datekeys:quicknet:v1: ERR_RELEASE_INVALID" + } + ], + "archive": { + "file": "releases/archive_1000_1004.bin", + "header": "a5007818646174656b6579732d72656c656173652d61726368697665010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e80405", + "lookups": [ + { + "round": 999, + "result": "ERR_RELEASE_UNAVAILABLE" + }, + { + "round": 1000, + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e8045830b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "result": "ok" + }, + { + "round": 1001, + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903e9045830b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41", + "result": "ok" + }, + { + "round": 1002, + "result": "ERR_RELEASE_UNAVAILABLE" + }, + { + "round": 1003, + "result": "ERR_RELEASE_UNAVAILABLE" + }, + { + "round": 1004, + "encoding": "a50070646174656b6579732d72656c65617365010102582052db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971031903ec045830a40658b820c0f8c10207524179a2031ba9537688a0d04e4851b58026be9a341fee3b96fb48ffad28483d84b40a5864aa", + "result": "ok" + }, + { + "round": 1005, + "result": "ERR_RELEASE_UNAVAILABLE" + } + ] + } +}