Fuzz the strict reader of drand's JSON against encoding/json

FuzzStrictJSON, the 28th target of scripts/fuzz.sh: on every input the
strict reader accepts, encoding/json must accept it too and read the same
names in the outer object, the same text of each value, the same strings
and the same round. Its seeds are the 38 inputs of drand's JSON in
release.json. Sixty seconds on four workers found no disagreement. It
comes after the tag spec-v0.16 and changes no rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.16
dev 4 hours ago
parent b6ff17a5fa
commit 0eb5aa0c2b

@ -43,6 +43,10 @@ JSON.
it, and the one with words.
- **The annex.** `annex/recovery.md` is §79 of the draft v0.16, with the
key of words in 79.7.
- **Fuzzing the strict reader.** `provider.FuzzStrictJSON`, the 28th target
of `scripts/fuzz.sh`, after the tag: what the strict reader of drand's
JSON accepts, `encoding/json` reads with the same names, texts, strings
and round.
## Unreleased — specification v0.15

@ -80,7 +80,7 @@ moves the next two subsections of §79 one place. A case of §64 that is not in
| 45 | Release API: its answer is the release object of §47.1 (v0.15), and its user a network source; the HTTP form is informative | the object: `provider.EncodeRelease`, `provider.DecodeRelease`; the API itself is out of scope (server, plan §2) | `provider.TestReleaseVectors` |
| 46 | Release Queue | out of scope (server) | — |
| 47 | Release Cache | every release is verified again: `capsule.Open` step 10 and `agewrap.TimeIdentity`; `release_material` is the release object (v0.15): `provider.EncodeRelease` | mutations *release of another round* |
| 47.1 | Release object (v0.15): deterministic CBOR without a frame, `{0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}`; size from 1 to 1024 bytes, then type and version, then schema (`ERR_NON_CANONICAL_CBOR`, a version other than 1 `ERR_UNSUPPORTED_VERSION`); the chain hash, the round and the signature at step 10; drand's JSON accepted as the input of the caller, `ERR_RELEASE_INVALID` when unreadable, and since v0.16 read strictly: no repeated name, names compared exactly once their escapes are decoded, no lone surrogate, round an integer from 1 to 2^53 − 1; a Release Cache and the Release API keep and serve the object | `provider/release.go`: `EncodeRelease`, `DecodeRelease` (`releaseWire` with `codec.CheckSchema` and `codec.Unmarshal`), `ParseRelease`, `ParseDrandJSON`, `NewReleaseObject`, `MaxReleaseObjectSize`, `MaxReleaseJSONSize`; `provider/drandjson.go` (`strictJSON`, `jsonRound`), which `provider/drand` uses for the answers of the relays too; `cmd/datekeys` `releaseInHand` | `provider.TestReleaseVectors`, `TestEncodeRelease`, `FuzzDecodeRelease`, `TestStrictJSON`, `TestJSONRound`; `testdata/vectors/release.json`, `testdata/releases/<round>.cbor`; `internal/testkit.TestVectorFilesAreCurrent` |
| 47.1 | Release object (v0.15): deterministic CBOR without a frame, `{0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}`; size from 1 to 1024 bytes, then type and version, then schema (`ERR_NON_CANONICAL_CBOR`, a version other than 1 `ERR_UNSUPPORTED_VERSION`); the chain hash, the round and the signature at step 10; drand's JSON accepted as the input of the caller, `ERR_RELEASE_INVALID` when unreadable, and since v0.16 read strictly: no repeated name, names compared exactly once their escapes are decoded, no lone surrogate, round an integer from 1 to 2^53 − 1; a Release Cache and the Release API keep and serve the object | `provider/release.go`: `EncodeRelease`, `DecodeRelease` (`releaseWire` with `codec.CheckSchema` and `codec.Unmarshal`), `ParseRelease`, `ParseDrandJSON`, `NewReleaseObject`, `MaxReleaseObjectSize`, `MaxReleaseJSONSize`; `provider/drandjson.go` (`strictJSON`, `jsonRound`), which `provider/drand` uses for the answers of the relays too; `cmd/datekeys` `releaseInHand` | `provider.TestReleaseVectors`, `TestEncodeRelease`, `FuzzDecodeRelease`, `TestStrictJSON`, `TestJSONRound`, `FuzzStrictJSON` (against `encoding/json`); `testdata/vectors/release.json`, `testdata/releases/<round>.cbor`; `internal/testkit.TestVectorFilesAreCurrent` |
| 48 | Multi-relay; a network source verifies every response with the rules of §63 step 10 and discards the invalid ones: none valid is `ERR_RELEASE_UNAVAILABLE` at step 9, and so is any other failure of a source, with no other code | `provider/drand.Client` (race, first *verified* release wins; the failure of each relay kept as text only, a context that ended detectable with `errors.Is`), the `provider.ReleaseSource` contract, `capsule.Open` (step 9 keeps only the text of a source error with another code or none) | `drand.TestRaceWaitsForAValidSignature`, `TestRejectMalformedRelayResponses`, `TestFetchErrorHasOneCode`, `TestUnavailabilityAndCancellation`; `capsule.TestReleaseFromANetworkSource`, `TestReleaseSourceErrorsAtStep9` |
| 49 | Direct recovery from the provider | `provider/drand`; v0.15: a release in hand, `provider.Supplier` (`provider.Encoded`, `provider.Archive`) in `capsule.OpenOptions.Release`; `datekeys decrypt -release` | `drand.TestLiveRelays`, `capsule.TestLiveLifecycle` (`-tags integration`); `capsule.TestReleaseInHand`, `TestReleaseInHandErrors`; `cmd/datekeys.TestDecryptWithReleaseInHand` |
| 50 | Historical release dependency; v0.15: long-term recovery on archives of all rounds and on cache services that serve them, with no hosting promise, the release archive as an informative format | documented in `README.md`; `provider.Archive`, `provider.EncodeArchiveHeader`; `datekeys decrypt -release` with a local archive | `provider.TestArchive`; `cmd/datekeys.TestDecryptWithReleaseInHand` (*a local archive*); the `archive` block of `testdata/vectors/release.json`, `testdata/releases/archive_1000_1004.bin` |

@ -1,6 +1,9 @@
package provider
import (
"bytes"
"encoding/json"
"os"
"strings"
"testing"
)
@ -84,3 +87,63 @@ func TestJSONRound(t *testing.T) {
t.Error("invalid UTF-8 is malformed")
}
}
// FuzzStrictJSON checks the strict reader of drand's JSON against
// encoding/json (spec v0.16, §47.1): what it accepts is JSON for
// encoding/json too, with the same names in the outer object, each with the
// same text, the same string and, for a round it accepts, the same number.
// The strict reader refuses repeated names, invalid UTF-8 and lone
// surrogates, where encoding/json keeps the last name and writes U+FFFD, so
// on what it accepts the two must agree. Its seeds are the inputs of
// drand's JSON in release.json.
func FuzzStrictJSON(f *testing.F) {
var vectors struct {
JSON []struct {
Input string `json:"input"`
} `json:"json"`
}
b, err := os.ReadFile("../testdata/vectors/release.json")
if err != nil {
f.Fatal(err)
}
if err := json.Unmarshal(b, &vectors); err != nil || len(vectors.JSON) < 30 {
f.Fatalf("release.json: %d inputs of drand's JSON, %v", len(vectors.JSON), err)
}
for _, v := range vectors.JSON {
f.Add([]byte(v.Input))
}
f.Fuzz(func(t *testing.T, b []byte) {
members, ok := strictJSON(b)
if !ok {
return
}
if !json.Valid(b) {
t.Fatalf("strict JSON that encoding/json refuses: %q", b)
}
var outer map[string]json.RawMessage
if err := json.Unmarshal(b, &outer); err != nil {
t.Fatalf("encoding/json: %v", err)
}
if len(outer) != len(members) {
t.Fatalf("%d names for encoding/json, %d for the strict reader: %q", len(outer), len(members), b)
}
for _, m := range members {
raw, ok := outer[m.name]
if !ok || !bytes.Equal(raw, m.raw) {
t.Fatalf("name %q: encoding/json has %q, the strict reader %q", m.name, raw, m.raw)
}
if m.kind == '"' {
var v string
if err := json.Unmarshal(raw, &v); err != nil || v != m.str {
t.Fatalf("string %q: encoding/json gives %q, the strict reader %q", raw, v, m.str)
}
}
if r, ok := jsonRound(m); ok {
var n uint64
if err := json.Unmarshal(raw, &n); err != nil || n != r {
t.Fatalf("round %q: encoding/json gives %d (%v), the strict reader %d", raw, n, err, r)
}
}
}
})
}

@ -20,6 +20,7 @@ targets=(
"./extension FuzzDecodeArray"
"./profile FuzzDecode"
"./provider FuzzDecodeRelease"
"./provider FuzzStrictJSON"
"./datekey FuzzParse"
"./agewrap FuzzStanzas"
"./accesskey FuzzDecode"

Loading…
Cancel
Save

Powered by TurnKey Linux.