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 (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 release object of round in // testdata/releases: the round and .cbor, the generic extension of CBOR (RFC // 8949), since the protocol gives the object no file extension of its own. func ReleaseFileName(round uint64) string { return strconv.FormatUint(round, 10) + ".cbor" } // 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 release object // 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 (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}, // The strict reading of spec v0.16, §47.1: no repeated name, names // compared exactly once their escapes are decoded, and the round an // integer from 1 to 2^53 - 1 without fraction or exponent. {"round twice", `{"round":1000,"round":1001,"signature":"` + s1000 + `"}`, 1000, invalid}, {`round twice, once escaped as round`, `{"round":1000,"round":1000,"signature":"` + s1000 + `"}`, 1000, invalid}, {"round twice, the second null", `{"round":1000,"round":null,"signature":"` + s1000 + `"}`, 1000, invalid}, {`round escaped as round`, `{"round":1000,"signature":"` + s1000 + `"}`, 1000, ok}, {"Round instead of round", `{"Round":1000,"signature":"` + s1000 + `"}`, 1000, invalid}, {"ROUND beside round: another name, ignored", `{"round":1000,"ROUND":1001,"signature":"` + s1000 + `"}`, 1000, ok}, {"round null", `{"round":null,"signature":"` + s1000 + `"}`, 1000, invalid}, {"round 1000.0", `{"round":1000.0,"signature":"` + s1000 + `"}`, 1000, invalid}, {"round 1e3", `{"round":1e3,"signature":"` + s1000 + `"}`, 1000, invalid}, {"round -1000", `{"round":-1000,"signature":"` + s1000 + `"}`, 1000, invalid}, {"round 0", `{"round":0,"signature":"` + s1000 + `"}`, 1000, invalid}, {"round 2^53", `{"round":9007199254740992,"signature":"` + s1000 + `"}`, 1000, invalid}, {"round 2^53 - 1 for a DateKey of round 1000", `{"round":9007199254740991,"signature":"` + s1000 + `"}`, 1000, roundMismatch}, {"round with a leading zero", `{"round":01000,"signature":"` + s1000 + `"}`, 1000, invalid}, {"signature twice", `{"round":1000,"signature":"` + s1000 + `","signature":"` + s1000 + `"}`, 1000, invalid}, {"signature null", `{"round":1000,"signature":null}`, 1000, invalid}, {"randomness empty", `{"round":1000,"randomness":"","signature":"` + s1000 + `"}`, 1000, invalid}, {"randomness null", `{"round":1000,"randomness":null,"signature":"` + s1000 + `"}`, 1000, invalid}, {"another name twice", `{"round":1000,"signature":"` + s1000 + `","note":1,"note":2}`, 1000, invalid}, {"a name twice in a nested object", `{"round":1000,"signature":"` + s1000 + `","meta":{"a":1,"a":2}}`, 1000, invalid}, {"nested objects and arrays, ignored", `{"round":1000,"signature":"` + s1000 + `","meta":{"a":[1,{"a":2}],"b":{},"c":[]}}`, 1000, ok}, {"a lone surrogate in another name", `{"round":1000,"signature":"` + s1000 + `","\ud800":1}`, 1000, invalid}, {"a lone low surrogate in a value", `{"round":1000,"signature":"` + s1000 + `","note":"\udc00"}`, 1000, invalid}, {"a surrogate pair in a value", `{"round":1000,"signature":"` + s1000 + `","note":"😀"}`, 1000, ok}, {"a tab inside a string", `{"round":1000,"signature":"` + s1000 + `","note":"a` + "\t" + `b"}`, 1000, invalid}, {"something after the object", `{"round":1000,"signature":"` + s1000 + `"}{}`, 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 file in testdata/releases", 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[:]) }