diff --git a/internal/testkit/releasevectors.go b/internal/testkit/releasevectors.go index a667c6d..5c49796 100644 --- a/internal/testkit/releasevectors.go +++ b/internal/testkit/releasevectors.go @@ -252,6 +252,35 @@ func ReleaseVectors() (ReleaseVectorFile, error) { {"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} diff --git a/provider/drand/client.go b/provider/drand/client.go index 2496611..b48c899 100644 --- a/provider/drand/client.go +++ b/provider/drand/client.go @@ -11,9 +11,6 @@ package drand import ( "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" "fmt" "io" "net/http" @@ -146,29 +143,14 @@ func (c *Client) fetch(ctx context.Context, relay string, p *profile.Profile, co if len(b) > maxResponseSize { return provider.Release{}, fmt.Errorf("%s: response larger than %d bytes: %w", relay, maxResponseSize, 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 { - return provider.Release{}, fmt.Errorf("%s: malformed response: %w", relay, datekeys.ErrReleaseInvalid) - } - sig, err := hex.DecodeString(wire.Signature) + // The answer is read as a release the caller gives, with the strict + // rules of spec v0.16, §47.1, randomness included. + release, err := provider.ParseDrandJSON(b) if err != nil { - return provider.Release{}, fmt.Errorf("%s: signature is not hex: %w", relay, datekeys.ErrReleaseInvalid) + return provider.Release{}, fmt.Errorf("%s: %w", relay, err) } - release := provider.Release{Round: wire.Round, Signature: sig} if err := provider.Verify(p, cond, release); err != nil { return provider.Release{}, fmt.Errorf("%s: %w", relay, err) } - // The v2 API omits randomness; if a relay supplies it, it must be - // SHA-256 of the verified signature. - if wire.Randomness != "" { - sum := sha256.Sum256(sig) - if !strings.EqualFold(wire.Randomness, hex.EncodeToString(sum[:])) { - return provider.Release{}, fmt.Errorf("%s: randomness does not match the signature: %w", relay, datekeys.ErrReleaseInvalid) - } - } return release, nil } diff --git a/provider/drandjson.go b/provider/drandjson.go new file mode 100644 index 0000000..8c4b0aa --- /dev/null +++ b/provider/drandjson.go @@ -0,0 +1,298 @@ +package provider + +import ( + "strings" + "unicode/utf8" +) + +// The strict reading of drand's JSON (spec v0.16, §47.1): JSON of RFC 8259, +// in UTF-8, whose value is an object; no object of the JSON repeats a name, +// and names are compared exactly, code point by code point, once their +// escapes are decoded, so that "round" is round and Round is another +// name; and an escape of a surrogate that does not pair with the next one +// makes the JSON malformed. A common JSON reader keeps the last of two +// repeated names, or does not tell upper from lower case in them, and two +// readers would see two rounds in the same input. + +// jsonMember is a member of the outer object: its name, decoded, the kind of +// its value (a byte of `"`, `0`, `{`, `[`, `t`, `f` or `n`), the text of the +// value as written, and for a string, the string decoded. +type jsonMember struct { + name string + kind byte + raw []byte + str string +} + +// strictJSON reads b as a JSON object with the rules above, and returns the +// members of the outer object in their order, or false when b breaks one. +func strictJSON(b []byte) ([]jsonMember, bool) { + if !utf8.Valid(b) { + return nil, false + } + r := &jsonReader{b: b} + r.space() + if !r.at('{') { + return nil, false + } + members, ok := r.object(true) + r.space() + return members, ok && r.i == len(b) +} + +// jsonReader reads JSON from b at i. +type jsonReader struct { + b []byte + i int +} + +func (r *jsonReader) at(c byte) bool { return r.i < len(r.b) && r.b[r.i] == c } + +// space skips the four spaces of JSON. +func (r *jsonReader) space() { + for r.i < len(r.b) && strings.IndexByte(" \t\n\r", r.b[r.i]) >= 0 { + r.i++ + } +} + +// value reads one value of any kind. +func (r *jsonReader) value() (jsonMember, bool) { + if r.i >= len(r.b) { + return jsonMember{}, false + } + start := r.i + m := jsonMember{kind: r.b[r.i]} + ok := false + switch c := r.b[r.i]; { + case c == '{': + _, ok = r.object(false) + case c == '[': + ok = r.array() + case c == '"': + m.str, ok = r.string() + case c == 't': + ok = r.literal("true") + case c == 'f': + ok = r.literal("false") + case c == 'n': + ok = r.literal("null") + case c == '-' || c >= '0' && c <= '9': + m.kind, ok = '0', r.number() + } + m.raw = r.b[start:r.i] + return m, ok +} + +// object reads an object, with no name twice, and returns its members when +// keep is set. +func (r *jsonReader) object(keep bool) ([]jsonMember, bool) { + r.i++ // { + r.space() + if r.at('}') { + r.i++ + return nil, true + } + var out []jsonMember + seen := map[string]bool{} + for { + r.space() + if !r.at('"') { + return nil, false + } + name, ok := r.string() + if !ok || seen[name] { + return nil, false + } + seen[name] = true + r.space() + if !r.at(':') { + return nil, false + } + r.i++ + r.space() + m, ok := r.value() + if !ok { + return nil, false + } + if keep { + m.name = name + out = append(out, m) + } + r.space() + switch { + case r.at(','): + r.i++ + case r.at('}'): + r.i++ + return out, true + default: + return nil, false + } + } +} + +func (r *jsonReader) array() bool { + r.i++ // [ + r.space() + if r.at(']') { + r.i++ + return true + } + for { + r.space() + if _, ok := r.value(); !ok { + return false + } + r.space() + switch { + case r.at(','): + r.i++ + case r.at(']'): + r.i++ + return true + default: + return false + } + } +} + +// string reads a string and decodes its escapes; a surrogate escaped alone, +// without its pair, breaks it. +func (r *jsonReader) string() (string, bool) { + r.i++ // " + var sb strings.Builder + for r.i < len(r.b) { + c := r.b[r.i] + switch { + case c == '"': + r.i++ + return sb.String(), true + case c < 0x20: + return "", false + case c != '\\': + _, n := utf8.DecodeRune(r.b[r.i:]) + sb.Write(r.b[r.i : r.i+n]) + r.i += n + continue + } + if r.i+1 >= len(r.b) { + return "", false + } + e := r.b[r.i+1] + r.i += 2 + if k := strings.IndexByte(`"\/bfnrt`, e); k >= 0 { + sb.WriteByte("\"\\/\b\f\n\r\t"[k]) + continue + } + if e != 'u' { + return "", false + } + u, ok := r.hex4() + switch { + case !ok || u >= 0xdc00 && u <= 0xdfff: + return "", false + case u >= 0xd800 && u <= 0xdbff: + if r.i+1 >= len(r.b) || r.b[r.i] != '\\' || r.b[r.i+1] != 'u' { + return "", false + } + r.i += 2 + low, ok := r.hex4() + if !ok || low < 0xdc00 || low > 0xdfff { + return "", false + } + u = 0x10000 + (u-0xd800)<<10 + (low - 0xdc00) + } + sb.WriteRune(rune(u)) + } + return "", false +} + +// hex4 reads the four hexadecimal digits of an escape \u. +func (r *jsonReader) hex4() (int, bool) { + if r.i+4 > len(r.b) { + return 0, false + } + u := 0 + for _, c := range r.b[r.i : r.i+4] { + var d int + switch { + case c >= '0' && c <= '9': + d = int(c - '0') + case c >= 'a' && c <= 'f': + d = int(c-'a') + 10 + case c >= 'A' && c <= 'F': + d = int(c-'A') + 10 + default: + return 0, false + } + u = u<<4 | d + } + r.i += 4 + return u, true +} + +func (r *jsonReader) literal(word string) bool { + if !strings.HasPrefix(string(r.b[r.i:]), word) { + return false + } + r.i += len(word) + return true +} + +// number reads a number of the grammar of RFC 8259: a minus, an integer +// part without leading zeros, and an optional fraction and exponent. +func (r *jsonReader) number() bool { + digits := func() int { + n := 0 + for r.i < len(r.b) && r.b[r.i] >= '0' && r.b[r.i] <= '9' { + r.i++ + n++ + } + return n + } + if r.at('-') { + r.i++ + } + switch { + case r.at('0'): + r.i++ + case digits() == 0: + return false + } + if r.at('.') { + r.i++ + if digits() == 0 { + return false + } + } + if r.at('e') || r.at('E') { + r.i++ + if r.at('+') || r.at('-') { + r.i++ + } + if digits() == 0 { + return false + } + } + return true +} + +// maxJSONRound is the largest round of drand's JSON, 2^53 - 1, as in the +// release object. +const maxJSONRound = 1<<53 - 1 + +// jsonRound reads the round of drand's JSON: a number without sign, fraction +// or exponent, from 1 to 2^53 - 1. +func jsonRound(m jsonMember) (uint64, bool) { + if m.kind != '0' || len(m.raw) == 0 || len(m.raw) > 16 || m.raw[0] == '0' { + return 0, false + } + var n uint64 + for _, c := range m.raw { + if c < '0' || c > '9' { + return 0, false + } + n = n*10 + uint64(c-'0') + } + return n, n <= maxJSONRound +} diff --git a/provider/drandjson_test.go b/provider/drandjson_test.go new file mode 100644 index 0000000..b03e81b --- /dev/null +++ b/provider/drandjson_test.go @@ -0,0 +1,86 @@ +package provider + +import ( + "strings" + "testing" +) + +// Spec v0.16, §47.1: the strict reading of drand's JSON, at the edges of the +// grammar of RFC 8259 that release.json does not reach. +func TestStrictJSON(t *testing.T) { + for _, tc := range []struct { + in string + ok bool + }{ + {`{}`, true}, + {` {"a":1} `, true}, + {"\t{\"a\":1}\r\n", true}, + {`{"a":[]}`, true}, + {`{"a":[1,2,[3,{}]]}`, true}, + {`{"a":true,"b":false,"c":null}`, true}, + {`{"a":-0,"b":0.5,"c":1E+2,"d":1e-2,"e":-12.25e3}`, true}, + {`{"a":"\"\\\/\b\f\n\r\té"}`, true}, + {`{"é":1,"é":2}`, false}, // one name, escaped and not + {`{"a":1,"a":2}`, false}, + {`{"a":{"b":1},"c":{"b":2}}`, true}, // the same name in two objects + {`{"a":[{"b":1,"b":1}]}`, false}, + {`{"a":01}`, false}, + {`{"a":1.}`, false}, + {`{"a":.5}`, false}, + {`{"a":1e}`, false}, + {`{"a":+1}`, false}, + {`{"a":-}`, false}, + {`{"a":tru}`, false}, + {`{"a":nul}`, false}, + {`{"a":"\x"}`, false}, + {`{"a":"\u12"}`, false}, + {`{"a":"\u12g4"}`, false}, + {`{"a":"\ud800"}`, false}, + {`{"a":"\ud800A"}`, false}, + {`{"a":"\ud800x"}`, false}, + {`{"a":"\udfff\ud800"}`, false}, + {`{"a":"😀"}`, true}, + {`{"a":"` + "\x01" + `"}`, false}, + {`{"a":"` + "\xff" + `"}`, false}, + {"{\"a\":\"\xed\xa0\x80\"}", false}, // a surrogate in UTF-8 + {`{"a":1,}`, false}, + {`{,"a":1}`, false}, + {`{"a" 1}`, false}, + {`{"a":1 "b":2}`, false}, + {`{a:1}`, false}, + {`{"a":[1,]}`, false}, + {`{"a":[1 2]}`, false}, + {`{"a":"b"`, false}, + {`{"a":1}x`, false}, + {`[]`, false}, + {`"a"`, false}, + {``, false}, + {"{\"a\":1}\v", false}, // not a space of JSON + } { + if _, ok := strictJSON([]byte(tc.in)); ok != tc.ok { + t.Errorf("%q: %v, want %v", tc.in, ok, tc.ok) + } + } + m, ok := strictJSON([]byte(`{"round":1000,"note":"a😀","n":-1.5}`)) + if !ok || len(m) != 3 || m[0].name != "round" || string(m[0].raw) != "1000" || m[1].str != "a\U0001F600" || m[2].kind != '0' || string(m[2].raw) != "-1.5" { + t.Errorf("members %+v", m) + } +} + +func TestJSONRound(t *testing.T) { + for raw, want := range map[string]bool{ + "1": true, "1000": true, "9007199254740991": true, + "0": false, "9007199254740992": false, "99999999999999999": false, + "-1": false, "1.0": false, "1e3": false, "01": false, + } { + if _, ok := jsonRound(jsonMember{kind: '0', raw: []byte(raw)}); ok != want { + t.Errorf("%s: %v, want %v", raw, ok, want) + } + } + if _, ok := jsonRound(jsonMember{kind: '"', raw: []byte(`"1"`)}); ok { + t.Error("a string is not a round") + } + if _, err := ParseDrandJSON([]byte(`{"round":1000,"signature":"` + strings.Repeat("ab", 48) + `","note":"` + "\xff" + `"}`)); err == nil { + t.Error("invalid UTF-8 is malformed") + } +} diff --git a/provider/release.go b/provider/release.go index aa673e3..e9f57c5 100644 --- a/provider/release.go +++ b/provider/release.go @@ -3,7 +3,6 @@ package provider import ( "bytes" "encoding/hex" - "encoding/json" "fmt" datekeys "g.activething.com/go/DateKeys" @@ -146,32 +145,59 @@ func DecodeRelease(b []byte) (Release, error) { // 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 ParseDrandJSON(b) } return DecodeRelease(b) } -// parseDrandJSON reads the JSON of a drand relay. -func parseDrandJSON(b []byte) (Release, error) { +// ParseDrandJSON reads the JSON of a drand relay with the strict rules of +// spec v0.16, §47.1: at most MaxReleaseJSONSize bytes of JSON whose value is +// an object, with no repeated name and names compared exactly once their +// escapes are decoded; "round" a number without sign, fraction or exponent, +// from 1 to 2^53 - 1; "signature" a string of hexadecimal, in lower or upper +// case; and "randomness", when present, a string with SHA-256 of the +// signature in hexadecimal. Other members are ignored. Any failure is +// ErrReleaseInvalid. provider/drand reads the answers of the relays with it. +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"` + malformed := fmt.Errorf("provider: drand JSON: malformed, or without round or signature: %w", datekeys.ErrReleaseInvalid) + members, ok := strictJSON(b) + if !ok { + return Release{}, malformed } - 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) + var round uint64 + var signature, randomness *string + for _, m := range members { + switch m.name { + case "round": + if round, ok = jsonRound(m); !ok { + return Release{}, malformed + } + case "signature", "randomness": + if m.kind != '"' { + return Release{}, malformed + } + v := m.str + if m.name == "signature" { + signature = &v + } else { + randomness = &v + } + } + } + if round == 0 || signature == nil { + return Release{}, malformed } - sig, err := hex.DecodeString(*wire.Signature) + sig, err := hex.DecodeString(*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) { + if randomness != nil && !randomnessMatches(*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 + return Release{Round: round, Signature: sig}, nil } // Supplier hands over a release that the caller has in hand (spec v0.15, diff --git a/testdata/README.md b/testdata/README.md index e77c67a..8838067 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -358,7 +358,13 @@ one of CBOR (RFC 8949). 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. + 8192 bytes; then the round and the signature, as for an object. Since + v0.16 it is read strictly: no object of the JSON repeats a name, names are + compared exactly once their escapes are decoded (`"round"` is + `round`, and `ROUND` another name, which is ignored), an escape of a lone + surrogate is malformed, `round` is a number without sign, fraction or + exponent from 1 to 2^53 − 1, and `signature` and `randomness` are strings. + The cases of v0.16 follow those of v0.15 in the list. - `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, diff --git a/testdata/vectors/release.json b/testdata/vectors/release.json index f41bb1b..d60af10 100644 --- a/testdata/vectors/release.json +++ b/testdata/vectors/release.json @@ -432,6 +432,204 @@ }, "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": "round twice", + "input": "{\"round\":1000,\"round\":1001,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "round twice, once escaped as round", + "input": "{\"round\":1000,\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "round twice, the second null", + "input": "{\"round\":1000,\"round\":null,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "round escaped as round", + "input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "result": "ok" + }, + { + "name": "Round instead of round", + "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": "ROUND beside round: another name, ignored", + "input": "{\"round\":1000,\"ROUND\":1001,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "result": "ok" + }, + { + "name": "round null", + "input": "{\"round\":null,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "round 1000.0", + "input": "{\"round\":1000.0,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "round 1e3", + "input": "{\"round\":1e3,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "round -1000", + "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": "round 0", + "input": "{\"round\":0,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "round 2^53", + "input": "{\"round\":9007199254740992,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "round 2^53 - 1 for a DateKey of round 1000", + "input": "{\"round\":9007199254740991,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "release": { + "round": 9007199254740991, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "result": "ERR_ROUND_MISMATCH", + "text": "provider: release for round 9007199254740991, expected 1000: ERR_ROUND_MISMATCH" + }, + { + "name": "round with a leading zero", + "input": "{\"round\":01000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "signature twice", + "input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "signature null", + "input": "{\"round\":1000,\"signature\":null}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "randomness empty", + "input": "{\"round\":1000,\"randomness\":\"\",\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: randomness does not match the signature: ERR_RELEASE_INVALID" + }, + { + "name": "randomness null", + "input": "{\"round\":1000,\"randomness\":null,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "another name twice", + "input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"note\":1,\"note\":2}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "a name twice in a nested object", + "input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"meta\":{\"a\":1,\"a\":2}}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "nested objects and arrays, ignored", + "input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"meta\":{\"a\":[1,{\"a\":2}],\"b\":{},\"c\":[]}}", + "round": 1000, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "result": "ok" + }, + { + "name": "a lone surrogate in another name", + "input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"\\ud800\":1}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "a lone low surrogate in a value", + "input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"note\":\"\\udc00\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "a surrogate pair in a value", + "input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"note\":\"😀\"}", + "round": 1000, + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "result": "ok" + }, + { + "name": "a tab inside a string", + "input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\",\"note\":\"a\tb\"}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" + }, + { + "name": "something after the object", + "input": "{\"round\":1000,\"signature\":\"b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39\"}{}", + "round": 1000, + "result": "ERR_RELEASE_INVALID", + "text": "provider: drand JSON: malformed, or without round or signature: ERR_RELEASE_INVALID" } ], "archive": {