You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
363 lines
18 KiB
363 lines
18 KiB
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 \u0072ound`, `{"round":1000,"\u0072ound":1000,"signature":"` + s1000 + `"}`, 1000, invalid},
|
|
{"round twice, the second null", `{"round":1000,"round":null,"signature":"` + s1000 + `"}`, 1000, invalid},
|
|
{`round escaped as \u0072ound`, `{"\u0072ound":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":"\ud83d\ude00"}`, 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[:])
|
|
}
|