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.
598 lines
26 KiB
598 lines
26 KiB
package testkit
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/accesskey"
|
|
"g.activething.com/go/DateKeys/capsule"
|
|
"g.activething.com/go/DateKeys/codec"
|
|
"g.activething.com/go/DateKeys/datekey"
|
|
"g.activething.com/go/DateKeys/internal/cbortest"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
)
|
|
|
|
// Limits with which the generic vectors of cbor.json are walked: at most three
|
|
// nested containers, the most any object of the protocol has (object,
|
|
// extension array, extension), and at most 64 bytes in a string and 64 items
|
|
// or entries in a container, the size of the largest extension array.
|
|
const (
|
|
WalkMaxDepth = 3
|
|
WalkMaxLen = 64
|
|
)
|
|
|
|
// Schema names of the schema vectors: the object the bytes encode and the
|
|
// decoder that reads them.
|
|
const (
|
|
SchemaProfile = "provider_profile"
|
|
SchemaHeader = "public_header"
|
|
SchemaControl = "control_cbor"
|
|
SchemaDKKBody = "dkk_body"
|
|
)
|
|
|
|
// ResultOK is the result of a vector, mutation or inspection that is accepted.
|
|
const ResultOK = "ok"
|
|
|
|
// CBORVectorFile is testdata/vectors/cbor.json.
|
|
type CBORVectorFile struct {
|
|
Spec string `json:"spec"`
|
|
Description string `json:"description"`
|
|
Walk WalkLimits `json:"walk"`
|
|
Accept []CBORVector `json:"accept"`
|
|
Reject []CBORVector `json:"reject"`
|
|
Schemas []SchemaVector `json:"schemas"`
|
|
}
|
|
|
|
// WalkLimits are the maxDepth and maxLen arguments of codec.Walk.
|
|
type WalkLimits struct {
|
|
MaxDepth int `json:"max_depth"`
|
|
MaxLen int `json:"max_len"`
|
|
}
|
|
|
|
// CBORVector is one generic vector: bytes that are, or are not, exactly one
|
|
// data item of the CBOR profile of spec §58 within the walk limits.
|
|
type CBORVector struct {
|
|
Name string `json:"name"`
|
|
Hex string `json:"hex"`
|
|
// Value is the value of an accepted unsigned integer: a JSON number up
|
|
// to 2^53-1, a decimal string above.
|
|
Value any `json:"value,omitempty"`
|
|
// Error is the code of a rejected vector.
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// SchemaVector is one encoded object and the result of its decoder.
|
|
type SchemaVector struct {
|
|
// Block names the schema the vector exercises: one of the Schema*
|
|
// names, "verification_metadata" (inside a .dkk body) or "extension"
|
|
// (inside the noncritical_extensions of a PUBLIC_HEADER).
|
|
Block string `json:"block"`
|
|
// Schema names the object the bytes encode and the decoder to run.
|
|
Schema string `json:"schema"`
|
|
Name string `json:"name"`
|
|
Hex string `json:"hex"`
|
|
// Result is ResultOK or the normative code of the rejection.
|
|
Result string `json:"result"`
|
|
}
|
|
|
|
// DecodeSchema runs the decoder of the named schema on b, as a reader does
|
|
// before any registry is consulted: profile.Decode, capsule.DecodeHeader,
|
|
// capsule.DecodeControl or accesskey.DecodeBody. Secrets it decodes are
|
|
// wiped.
|
|
func DecodeSchema(schema string, b []byte) error {
|
|
switch schema {
|
|
case SchemaProfile:
|
|
_, err := profile.Decode(b)
|
|
return err
|
|
case SchemaHeader:
|
|
_, err := capsule.DecodeHeader(b)
|
|
return err
|
|
case SchemaControl:
|
|
c, err := capsule.DecodeControl(b)
|
|
if c != nil {
|
|
clear(c.PayloadIdentity[:])
|
|
}
|
|
return err
|
|
case SchemaDKKBody:
|
|
k, err := accesskey.DecodeBody(b)
|
|
if k != nil {
|
|
k.Wipe()
|
|
}
|
|
return err
|
|
}
|
|
return fmt.Errorf("testkit: unknown schema %q", schema)
|
|
}
|
|
|
|
// Result returns ResultOK for a nil error and its normative code otherwise.
|
|
func Result(err error) string {
|
|
if err == nil {
|
|
return ResultOK
|
|
}
|
|
if c := datekeys.Code(err); c != "" {
|
|
return c
|
|
}
|
|
return "error without a normative code: " + err.Error()
|
|
}
|
|
|
|
// CBORVectors computes testdata/vectors/cbor.json with the implementation,
|
|
// and fails if any vector does not get the result it is written for.
|
|
func CBORVectors() (CBORVectorFile, error) {
|
|
f := CBORVectorFile{
|
|
Spec: SpecVersion,
|
|
Description: "CBOR profile of spec §58 and the schemas of spec/datekeys.cddl, generated by the reference implementation. " +
|
|
"accept and reject are walked as one data item of the profile with the limits of walk; " +
|
|
"schemas are decoded with the decoder of their schema. See testdata/README.md.",
|
|
Walk: WalkLimits{MaxDepth: WalkMaxDepth, MaxLen: WalkMaxLen},
|
|
}
|
|
var errs []error
|
|
for _, g := range genericVectors() {
|
|
b, err := hex.DecodeString(g.hex)
|
|
if err != nil {
|
|
return f, fmt.Errorf("vector %q: %w", g.name, err)
|
|
}
|
|
got := Result(codec.Walk(b, WalkMaxDepth, WalkMaxLen))
|
|
v := CBORVector{Name: g.name, Hex: g.hex}
|
|
if g.accept {
|
|
if got != ResultOK {
|
|
errs = append(errs, fmt.Errorf("vector %q: want accepted, got %s", g.name, got))
|
|
}
|
|
v.Value = uintValue(b)
|
|
f.Accept = append(f.Accept, v)
|
|
continue
|
|
}
|
|
if got != datekeys.ErrNonCanonicalCBOR.Code() {
|
|
errs = append(errs, fmt.Errorf("vector %q: want %s, got %s", g.name, datekeys.ErrNonCanonicalCBOR.Code(), got))
|
|
}
|
|
v.Error = got
|
|
f.Reject = append(f.Reject, v)
|
|
}
|
|
sv, err := schemaVectors()
|
|
if err != nil {
|
|
return f, err
|
|
}
|
|
for _, s := range sv {
|
|
b, err := cbortest.Marshal(s.value)
|
|
if err != nil {
|
|
return f, fmt.Errorf("schema vector %s %q: %w", s.block, s.name, err)
|
|
}
|
|
got := Result(DecodeSchema(s.schema, b))
|
|
if got != s.want {
|
|
errs = append(errs, fmt.Errorf("schema vector %s %q: want %s, got %s", s.block, s.name, s.want, got))
|
|
}
|
|
f.Schemas = append(f.Schemas, SchemaVector{Block: s.block, Schema: s.schema, Name: s.name, Hex: hex.EncodeToString(b), Result: got})
|
|
}
|
|
return f, errors.Join(errs...)
|
|
}
|
|
|
|
// uintValue returns the value of b when b is exactly one unsigned integer:
|
|
// a number up to 2^53-1, a decimal string above. It returns nil otherwise.
|
|
func uintValue(b []byte) any {
|
|
v, err := cbortest.Unmarshal(b)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
n, ok := v.(uint64)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if n > codec.MaxSafeUint {
|
|
return strconv.FormatUint(n, 10)
|
|
}
|
|
return n
|
|
}
|
|
|
|
type genericVector struct {
|
|
name string
|
|
hex string
|
|
accept bool
|
|
}
|
|
|
|
func genericVectors() []genericVector {
|
|
rep := func(s string, n int) string { return strings.Repeat(s, n) }
|
|
// 64 map entries {0: 0, ..., 63: 0}, and one more.
|
|
mapEntries := func(n int) string {
|
|
var b strings.Builder
|
|
for k := range n {
|
|
if k < 24 {
|
|
fmt.Fprintf(&b, "%02x00", k)
|
|
} else {
|
|
fmt.Fprintf(&b, "18%02x00", k)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
accept := []genericVector{
|
|
{name: "uint 0", hex: "00"},
|
|
{name: "uint 23 inline", hex: "17"},
|
|
{name: "uint 24 one byte", hex: "1818"},
|
|
{name: "uint 255 one byte", hex: "18ff"},
|
|
{name: "uint 256 two bytes", hex: "190100"},
|
|
{name: "uint 65535 two bytes", hex: "19ffff"},
|
|
{name: "uint 65536 four bytes", hex: "1a00010000"},
|
|
{name: "uint 2^32-1 four bytes", hex: "1affffffff"},
|
|
{name: "uint 2^32 eight bytes", hex: "1b0000000100000000"},
|
|
{name: "uint 2^53-1 eight bytes", hex: "1b001fffffffffffff"},
|
|
{name: "uint 2^53 eight bytes", hex: "1b0020000000000000"},
|
|
{name: "uint 2^64-1 eight bytes", hex: "1bffffffffffffffff"},
|
|
{name: "empty bstr", hex: "40"},
|
|
{name: "bstr of one byte", hex: "4100"},
|
|
{name: "bstr of 23 bytes, inline length", hex: "57" + rep("ab", 23)},
|
|
{name: "bstr of 24 bytes, one-byte length", hex: "5818" + rep("ab", 24)},
|
|
{name: "bstr of 64 bytes, max_len", hex: "5840" + rep("ab", 64)},
|
|
{name: "empty text", hex: "60"},
|
|
{name: "text a", hex: "6161"},
|
|
{name: "text with NUL", hex: "6100"},
|
|
{name: "text with leading BOM", hex: "64efbbbf61"},
|
|
{name: "text U+FF61", hex: "63efbda1"},
|
|
{name: "text U+10000", hex: "64f0908080"},
|
|
{name: "text U+10FFFF", hex: "64f48fbfbf"},
|
|
{name: "text of 64 bytes, max_len", hex: "7840" + rep("61", 64)},
|
|
{name: "empty array", hex: "80"},
|
|
{name: "empty map", hex: "a0"},
|
|
{name: "map two sorted keys", hex: "a200010101"},
|
|
{name: "map keys 23 and 24", hex: "a21700181800"},
|
|
{name: "map keys 255 and 256", hex: "a218ff0019010000"},
|
|
{name: "map with text and bstr values", hex: "a20061610141" + "00"},
|
|
{name: "array of mixed items", hex: "8500406080a0"},
|
|
{name: "array of 64 items, max_len", hex: "9840" + rep("00", 64)},
|
|
{name: "map of 64 entries, max_len", hex: "b840" + mapEntries(64)},
|
|
{name: "containers nested 3 deep, max_depth", hex: "81818100"},
|
|
{name: "map in array in map", hex: "a10081a10000"},
|
|
}
|
|
reject := []genericVector{
|
|
{name: "empty input", hex: ""},
|
|
{name: "uint 23 with one extra byte", hex: "1817"},
|
|
{name: "uint 255 in two bytes", hex: "1900ff"},
|
|
{name: "uint 65535 in four bytes", hex: "1a0000ffff"},
|
|
{name: "uint 2^32-1 in eight bytes", hex: "1b00000000ffffffff"},
|
|
{name: "bstr length not shortest", hex: "5800"},
|
|
{name: "text length not shortest", hex: "7800"},
|
|
{name: "array length not shortest", hex: "9800"},
|
|
{name: "map length not shortest", hex: "b800"},
|
|
{name: "map key not shortest", hex: "a1180000"},
|
|
{name: "keys out of order", hex: "a201000001"},
|
|
{name: "duplicate key", hex: "a200000001"},
|
|
{name: "keys out of order in a nested map", hex: "a100a2010000" + "00"},
|
|
{name: "text key", hex: "a1616100"},
|
|
{name: "bstr key", hex: "a1416100"},
|
|
{name: "negative integer key", hex: "a12000"},
|
|
{name: "array key", hex: "a18000"},
|
|
{name: "float key", hex: "a1f93c0000"},
|
|
{name: "indefinite array", hex: "9f01ff"},
|
|
{name: "indefinite map", hex: "bf0000ff"},
|
|
{name: "indefinite bstr", hex: "5f4100ff"},
|
|
{name: "indefinite text", hex: "7f6161ff"},
|
|
{name: "break", hex: "ff"},
|
|
{name: "tag", hex: "c101"},
|
|
{name: "tag 24, encoded CBOR data item", hex: "d8184100"},
|
|
{name: "tag 2, bignum", hex: "c24101"},
|
|
{name: "float", hex: "f97e00"},
|
|
{name: "half-precision float 1.0", hex: "f93c00"},
|
|
{name: "single-precision float 1.0", hex: "fa3f800000"},
|
|
{name: "double-precision float 1.0", hex: "fb3ff0000000000000"},
|
|
{name: "false", hex: "f4"},
|
|
{name: "true", hex: "f5"},
|
|
{name: "null", hex: "f6"},
|
|
{name: "undefined", hex: "f7"},
|
|
{name: "simple value 16", hex: "f0"},
|
|
{name: "simple value 32", hex: "f820"},
|
|
{name: "negative int", hex: "20"},
|
|
{name: "negative int -25", hex: "3818"},
|
|
{name: "reserved additional information 28", hex: "1c"},
|
|
{name: "reserved additional information 29 in a bstr head", hex: "5d"},
|
|
{name: "reserved additional information 30 in an array head", hex: "9e"},
|
|
{name: "truncated uint", hex: "1901"},
|
|
{name: "truncated eight-byte uint", hex: "1b00000000"},
|
|
{name: "truncated bstr", hex: "4200"},
|
|
{name: "truncated text", hex: "6261"},
|
|
{name: "truncated array", hex: "8200"},
|
|
{name: "map without the value of its last key", hex: "a100"},
|
|
{name: "truncated map head", hex: "b900"},
|
|
{name: "length beyond input", hex: "5affffffff"},
|
|
{name: "array count beyond input", hex: "9affffffff"},
|
|
{name: "map count beyond input", hex: "baffffffff"},
|
|
{name: "trailing byte", hex: "0100"},
|
|
{name: "trailing byte after a map", hex: "a000"},
|
|
{name: "invalid UTF-8", hex: "61ff"},
|
|
{name: "overlong UTF-8", hex: "62c080"},
|
|
{name: "overlong three-byte UTF-8", hex: "63e08080"},
|
|
{name: "UTF-8 surrogate", hex: "63eda080"},
|
|
{name: "truncated UTF-8 sequence", hex: "62e282"},
|
|
{name: "UTF-8 above U+10FFFF", hex: "64f4908080"},
|
|
{name: "invalid UTF-8 in a map value", hex: "a10061ff"},
|
|
{name: "tag inside an array", hex: "81c101"},
|
|
{name: "float inside a map", hex: "a100f93c00"},
|
|
{name: "containers nested 4 deep, above max_depth", hex: "8181818100"},
|
|
{name: "bstr of 65 bytes, above max_len", hex: "5841" + rep("ab", 65)},
|
|
{name: "text of 65 bytes, above max_len", hex: "7841" + rep("61", 65)},
|
|
{name: "array of 65 items, above max_len", hex: "9841" + rep("00", 65)},
|
|
{name: "map of 65 entries, above max_len", hex: "b841" + mapEntries(65)},
|
|
}
|
|
for i := range accept {
|
|
accept[i].accept = true
|
|
}
|
|
return append(accept, reject...)
|
|
}
|
|
|
|
type schemaVector struct {
|
|
block, schema, name string
|
|
value any // encoded with cbortest.Marshal
|
|
want string
|
|
}
|
|
|
|
// with returns a copy of m with the given keys set; a nil value deletes the key.
|
|
func with(m map[uint64]any, kv ...any) map[uint64]any {
|
|
c := make(map[uint64]any, len(m)+len(kv)/2)
|
|
for k, v := range m {
|
|
c[k] = v
|
|
}
|
|
for i := 0; i < len(kv); i += 2 {
|
|
k := uint64(kv[i].(int))
|
|
if kv[i+1] == nil {
|
|
delete(c, k)
|
|
} else {
|
|
c[k] = kv[i+1]
|
|
}
|
|
}
|
|
return c
|
|
}
|
|
|
|
// null is CBOR null, which with cannot set because nil deletes a key.
|
|
var null = cbortest.Raw{0xf6}
|
|
|
|
// appendRaw returns the encoding of v followed by extra bytes.
|
|
func appendRaw(v any, extra ...byte) cbortest.Raw {
|
|
b, err := cbortest.Marshal(v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return cbortest.Raw(append(b, extra...))
|
|
}
|
|
|
|
func fill(b byte, n int) []byte { return bytes.Repeat([]byte{b}, n) }
|
|
|
|
func ext(id string, version uint64, data ...any) map[uint64]any {
|
|
e := map[uint64]any{0: id, 1: version}
|
|
if len(data) > 0 {
|
|
e[2] = data[0]
|
|
}
|
|
return e
|
|
}
|
|
|
|
func exts(n int) []any {
|
|
out := make([]any, n)
|
|
for i := range out {
|
|
out[i] = ext(fmt.Sprintf("org.example.%03d", i), 1)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// chainHash returns the chain hash that profile.Validate requires of a
|
|
// Provider Profile map, the drand chain-info hash: SHA-256 of period (key 7)
|
|
// as a big-endian uint32, genesis_time (key 8) as a big-endian int64,
|
|
// public_key (key 6), genesis_seed (key 10) and, unless it is "default",
|
|
// network (key 4). The vectors below that change one of these keys and keep
|
|
// the chain hash consistent test that key's own rule, not the self-check.
|
|
func chainHash(m map[uint64]any) []byte {
|
|
var n [12]byte
|
|
binary.BigEndian.PutUint32(n[:4], uint32(m[7].(uint64)))
|
|
binary.BigEndian.PutUint64(n[4:], m[8].(uint64))
|
|
h := sha256.New()
|
|
h.Write(n[:])
|
|
h.Write(m[6].([]byte))
|
|
h.Write(m[10].([]byte))
|
|
if network := m[4].(string); network != "default" {
|
|
h.Write([]byte(network))
|
|
}
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
func schemaVectors() ([]schemaVector, error) {
|
|
const (
|
|
nc = "ERR_NON_CANONICAL_CBOR"
|
|
unsup = "ERR_UNSUPPORTED_VERSION"
|
|
unknown = "ERR_UNKNOWN_PROFILE"
|
|
)
|
|
qb, err := profile.Quicknet().CanonicalCBOR()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
q, err := cbortest.UnmarshalMap(qb)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pairs := func(m map[uint64]any, order ...uint64) cbortest.Pairs {
|
|
var p cbortest.Pairs
|
|
for _, k := range order {
|
|
p = append(p, k, m[k])
|
|
}
|
|
return p
|
|
}
|
|
if !bytes.Equal(chainHash(q), q[5].([]byte)) {
|
|
return nil, errors.New("testkit: chainHash does not reproduce the Quicknet chain hash")
|
|
}
|
|
var out []schemaVector
|
|
add := func(block, schema, name string, v any, want string) {
|
|
out = append(out, schemaVector{block: block, schema: schema, name: name, value: v, want: want})
|
|
}
|
|
|
|
// Provider Profile (spec §11): keys 0 to 10, all required.
|
|
pp := func(name string, v any, want string) { add(SchemaProfile, SchemaProfile, name, v, want) }
|
|
pp("Quicknet profile", q, ResultOK)
|
|
pp("unknown key 11", with(q, 11, uint64(0)), nc)
|
|
pp("missing key 10, genesis_seed", with(q, 10, nil), nc)
|
|
pp("missing key 6, public_key", with(q, 6, nil), nc)
|
|
pp("keys 2 and 3 out of order", pairs(q, 0, 1, 3, 2, 4, 5, 6, 7, 8, 9, 10), nc)
|
|
pp("period as a text string", with(q, 7, "3"), nc)
|
|
pp("chain_hash as a text string", with(q, 5, strings.Repeat("ab", 32)), nc)
|
|
pp("null genesis_seed", with(q, 10, null), nc)
|
|
pp("chain_hash of 31 bytes", with(q, 5, q[5].([]byte)[:31]), nc)
|
|
pp("genesis_seed of 33 bytes", with(q, 10, append(bytes.Clone(q[10].([]byte)), 0)), nc)
|
|
pp("period 0", with(q, 7, uint64(0)), nc)
|
|
// rehashed keeps chain_hash consistent with the changed keys.
|
|
rehashed := func(kv ...any) map[uint64]any {
|
|
m := with(q, kv...)
|
|
return with(m, 5, chainHash(m))
|
|
}
|
|
pp("network default, left out of the chain hash", rehashed(4, "default"), ResultOK)
|
|
pp("period of one day, the implementation limit", rehashed(7, uint64(86400)), ResultOK)
|
|
pp("period of one day and one second, above the implementation limit", rehashed(7, uint64(86401)), nc)
|
|
pp("period 2^53", with(q, 7, uint64(1)<<53), nc)
|
|
pp("genesis_time 2^53", with(q, 8, uint64(1)<<53), nc)
|
|
pp("negative genesis_time", with(q, 8, -1), nc)
|
|
pp("period not in shortest form", with(q, 7, cbortest.Raw{0x1a, 0, 0, 0, 3}), nc)
|
|
pp("schema version 2", with(q, 1, uint64(2)), unsup)
|
|
pp("schema version 2 and an unknown key 11: the version is read first", with(q, 1, uint64(2), 11, uint64(0)), unsup)
|
|
pp("schema version 2^53", with(q, 1, uint64(1)<<53), nc)
|
|
pp("type tag of PUBLIC_HEADER", with(q, 0, capsule.HeaderTypeTag), nc)
|
|
pp("type tag of PUBLIC_HEADER and schema version 2: the type tag is checked first", with(q, 0, capsule.HeaderTypeTag, 1, uint64(2)), nc)
|
|
pp("invalid profile_id", with(q, 2, "Datekeys:quicknet:v1"), unknown)
|
|
pp("network changed: the chain hash no longer matches", with(q, 4, "quicknet2"), "ERR_PROFILE_MISMATCH")
|
|
pp("network changed with its chain hash", rehashed(4, "quicknet2"), ResultOK)
|
|
pp("empty public_key", with(q, 6, []byte{}), unknown)
|
|
pp("public_key of 1025 bytes", rehashed(6, fill(0xaa, 1025)), unknown)
|
|
pp("public_key that is not a group element", rehashed(6, fill(0x00, 96)), unknown)
|
|
pp("public_key the identity element", rehashed(6, append([]byte{0xc0}, fill(0x00, 95)...)), unknown)
|
|
pp("provider other than drand", rehashed(3, "drand2"), unknown)
|
|
pp("unknown scheme", rehashed(9, "bls-unknown"), unknown)
|
|
pp("scheme pedersen-bls-chained, not supported by tlock", rehashed(9, "pedersen-bls-chained"), unknown)
|
|
pp("genesis_time 0", rehashed(8, uint64(0)), unknown)
|
|
pp("genesis_time 253402300798, 9999-12-31T23:59:58Z", rehashed(8, uint64(profile.MaxUnixTime-1)), ResultOK)
|
|
pp("genesis_time 253402300799, 9999-12-31T23:59:59Z", rehashed(8, uint64(profile.MaxUnixTime)), unknown)
|
|
|
|
// PUBLIC_HEADER (spec §24): keys 0 to 4, optional 5 and 6.
|
|
id := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}
|
|
dk := datekey.DateKey{ProfileID: profile.QuicknetID, Round: 1000}
|
|
h := map[uint64]any{0: capsule.HeaderTypeTag, 1: uint64(capsule.HeaderVersion), 2: id, 3: dk.Compact(), 4: uint64(capsule.TimeOnly)}
|
|
dkJSON := func(s string) string { return datekey.Prefix + base64.RawURLEncoding.EncodeToString([]byte(s)) }
|
|
ph := func(name string, v any, want string) { add(SchemaHeader, SchemaHeader, name, v, want) }
|
|
ph("minimal header", h, ResultOK)
|
|
ph("time_and_key policy", with(h, 4, uint64(capsule.TimeAndKey)), ResultOK)
|
|
ph("both extension arrays", with(h, 5, []any{ext("org.example.a", 1)}, 6, []any{ext("org.example.b", 1, []byte{0})}), ResultOK)
|
|
ph("DateKey of a profile that is not pinned: the registry decides", with(h, 3, datekey.DateKey{ProfileID: "datekeys:evmnet:v1", Round: 1000}.Compact()), ResultOK)
|
|
ph("unknown key 7", with(h, 7, uint64(0)), nc)
|
|
ph("missing key 2, capsule_id", with(h, 2, nil), nc)
|
|
ph("missing key 4, access_policy", with(h, 4, nil), nc)
|
|
ph("keys 3 and 4 out of order", pairs(h, 0, 1, 2, 4, 3), nc)
|
|
ph("capsule_id as a text string", with(h, 2, "0123456789abcdef"), nc)
|
|
ph("DateKey as a byte string", with(h, 3, []byte(dk.Compact())), nc)
|
|
ph("capsule_id of 15 bytes", with(h, 2, id[:15]), nc)
|
|
ph("capsule_id of 17 bytes", with(h, 2, append(bytes.Clone(id), 0x10)), nc)
|
|
ph("access_policy 2", with(h, 4, uint64(2)), nc)
|
|
ph("access_policy 256", with(h, 4, uint64(256)), nc)
|
|
ph("access_policy not in shortest form", with(h, 4, cbortest.Raw{0x18, 0x00}), nc)
|
|
ph("null access_policy", with(h, 4, null), nc)
|
|
ph("schema version 2", with(h, 1, uint64(2)), unsup)
|
|
ph("schema version 2 and a trailing byte: the version is read first", appendRaw(with(h, 1, uint64(2)), 0x00), unsup)
|
|
ph("schema version 2^53", with(h, 1, uint64(1)<<53), nc)
|
|
ph("schema version 2^64-1", with(h, 1, uint64(math.MaxUint64)), nc)
|
|
ph("type tag of CONTROL_CBOR", with(h, 0, capsule.ControlTypeTag), nc)
|
|
ph("type tag of CONTROL_CBOR and schema version 2: the type tag is checked first", with(h, 0, capsule.ControlTypeTag, 1, uint64(2)), nc)
|
|
ph("empty critical_extensions", with(h, 5, []any{}), nc)
|
|
ph("same extension_id in both arrays", with(h, 5, []any{ext("org.example.a", 1)}, 6, []any{ext("org.example.a", 1)}), nc)
|
|
ph("trailing byte after the map", appendRaw(h, 0x00), nc)
|
|
ph("non-canonical dk1_ JSON", with(h, 3, dkJSON(`{"version":1, "network":"datekeys:quicknet:v1", "round":1000}`)), "ERR_DATEKEY_NON_CANONICAL")
|
|
ph("DateKey that is not dk1_", with(h, 3, "hello"), "ERR_DATEKEY_INVALID")
|
|
ph("non-canonical DateKey and undefined access_policy: the CDDL is checked first",
|
|
with(h, 3, dkJSON(`{"version":1, "network":"datekeys:quicknet:v1", "round":1000}`), 4, uint64(2)), nc)
|
|
|
|
// CONTROL_CBOR (spec §31): keys 0 to 3, optional 4 and 5.
|
|
c := map[uint64]any{0: capsule.ControlTypeTag, 1: uint64(capsule.ControlVersion), 2: fill(0x11, 32), 3: fill(0x22, 32)}
|
|
pc := func(name string, v any, want string) { add(SchemaControl, SchemaControl, name, v, want) }
|
|
pc("minimal control", c, ResultOK)
|
|
pc("both extension arrays", with(c, 4, []any{ext("org.example.a", 1)}, 5, []any{ext("org.example.b", 1, []byte("x"))}), ResultOK)
|
|
pc("unknown key 6", with(c, 6, uint64(0)), nc)
|
|
pc("missing key 3, payload_identity", with(c, 3, nil), nc)
|
|
pc("missing key 2, header_binding", with(c, 2, nil), nc)
|
|
pc("header_binding as a text string", with(c, 2, strings.Repeat("a", 32)), nc)
|
|
pc("header_binding of 31 bytes", with(c, 2, fill(0x11, 31)), nc)
|
|
pc("payload_identity of 33 bytes", with(c, 3, fill(0x22, 33)), nc)
|
|
pc("null payload_identity", with(c, 3, null), nc)
|
|
pc("schema version 2", with(c, 1, uint64(2)), unsup)
|
|
pc("type tag of PUBLIC_HEADER", with(c, 0, capsule.HeaderTypeTag), nc)
|
|
pc("empty noncritical_extensions", with(c, 5, []any{}), nc)
|
|
|
|
// .dkk body (spec §41): keys 0 to 5, optional 6, 7 and 8.
|
|
k := map[uint64]any{0: accesskey.TypeTag, 1: uint64(accesskey.SchemaVersion), 2: fill(0x33, 16), 3: fill(0x44, 16), 4: accesskey.TypeX25519, 5: fill(0x55, 32)}
|
|
pk := func(name string, v any, want string) { add(SchemaDKKBody, SchemaDKKBody, name, v, want) }
|
|
pk("minimal body", k, ResultOK)
|
|
pk("verification_metadata and both extension arrays",
|
|
with(k, 6, map[uint64]any{0: fill(0x66, 32)}, 7, []any{ext("org.example.a", 1)}, 8, []any{ext("org.example.b", 1, []byte("x"))}), ResultOK)
|
|
pk("unknown key 9", with(k, 9, uint64(0)), nc)
|
|
pk("missing key 5, access_material", with(k, 5, nil), nc)
|
|
pk("missing key 3, capsule_id", with(k, 3, nil), nc)
|
|
pk("access_type as a byte string", with(k, 4, []byte(accesskey.TypeX25519)), nc)
|
|
pk("credential_id of 15 bytes", with(k, 2, fill(0x33, 15)), nc)
|
|
pk("capsule_id of 17 bytes", with(k, 3, fill(0x44, 17)), nc)
|
|
pk("null access_material", with(k, 5, null), nc)
|
|
pk("access_type x448", with(k, 4, "x448"), "ERR_ACCESS_INVALID")
|
|
pk("access_material of 31 bytes", with(k, 5, fill(0x55, 31)), "ERR_ACCESS_INVALID")
|
|
pk("access_material of 33 bytes", with(k, 5, fill(0x55, 33)), "ERR_ACCESS_INVALID")
|
|
pk("schema version 2", with(k, 1, uint64(2)), unsup)
|
|
pk("type tag of CONTROL_CBOR", with(k, 0, capsule.ControlTypeTag), nc)
|
|
pk("empty critical_extensions", with(k, 7, []any{}), nc)
|
|
|
|
// verification_metadata (spec §43), key 6 of a .dkk body.
|
|
const vm = "verification_metadata"
|
|
pv := func(name string, v any, want string) { add(vm, SchemaDKKBody, name, with(k, 6, v), want) }
|
|
pv("capsule_digest", map[uint64]any{0: fill(0x66, 32)}, ResultOK)
|
|
pv("empty map", map[uint64]any{}, nc)
|
|
pv("unknown key 1 instead of key 0", map[uint64]any{1: fill(0x66, 32)}, nc)
|
|
pv("unknown key 1 after capsule_digest", map[uint64]any{0: fill(0x66, 32), 1: uint64(0)}, nc)
|
|
pv("capsule_digest of 31 bytes", map[uint64]any{0: fill(0x66, 31)}, nc)
|
|
pv("capsule_digest of 33 bytes", map[uint64]any{0: fill(0x66, 33)}, nc)
|
|
pv("capsule_digest as a text string", map[uint64]any{0: strings.Repeat("f", 32)}, nc)
|
|
pv("null capsule_digest", map[uint64]any{0: null}, nc)
|
|
pv("verification_metadata as an array", []any{fill(0x66, 32)}, nc)
|
|
pv("null verification_metadata", null, nc)
|
|
|
|
// Extensions (spec §31, §54), in the noncritical_extensions of a
|
|
// PUBLIC_HEADER.
|
|
const ex = "extension"
|
|
pe := func(name string, v []any, want string) { add(ex, SchemaHeader, name, with(h, 6, v), want) }
|
|
pe("one extension without data", []any{ext("org.example.a", 1)}, ResultOK)
|
|
pe("data of one byte", []any{ext("org.example.a", 1, []byte{0})}, ResultOK)
|
|
pe("data that is not CBOR", []any{ext("org.example.a", 1, []byte{0xff, 0xfe})}, ResultOK)
|
|
pe("data 40, an empty byte string", []any{ext("org.example.a", 1, []byte{})}, nc)
|
|
pe("data 5801xx, a length not in its shortest form", []any{ext("org.example.a", 1, cbortest.Raw{0x58, 0x01, 0x2a})}, nc)
|
|
pe("data as a text string", []any{ext("org.example.a", 1, "public label")}, nc)
|
|
pe("null data", []any{ext("org.example.a", 1, null)}, nc)
|
|
pe("data as an unsigned integer", []any{ext("org.example.a", 1, uint64(7))}, nc)
|
|
pe("data as a map", []any{ext("org.example.a", 1, map[uint64]any{0: uint64(7)})}, nc)
|
|
pe("data as an array", []any{ext("org.example.a", 1, []any{[]byte{0}})}, nc)
|
|
pe("data as a tagged byte string", []any{ext("org.example.a", 1, cbortest.Raw{0xc1, 0x41, 0x00})}, nc)
|
|
pe("data as an indefinite-length byte string", []any{ext("org.example.a", 1, cbortest.Raw{0x5f, 0x41, 0x00, 0xff})}, nc)
|
|
pe("64 extensions", exts(64), ResultOK)
|
|
pe("65 extensions", exts(65), nc)
|
|
pe("empty array", []any{}, nc)
|
|
pe("extension_id starting with a BOM", []any{ext("\ufefforg.example.a", 1)}, ResultOK)
|
|
pe("U+FF61 before U+10000: UTF-8 byte order", []any{ext("\uff61", 1), ext("\U00010000", 1)}, ResultOK)
|
|
pe("U+10000 before U+FF61: UTF-16 order, not UTF-8 byte order", []any{ext("\U00010000", 1), ext("\uff61", 1)}, nc)
|
|
pe("extensions out of order", []any{ext("org.example.b", 1), ext("org.example.a", 1)}, nc)
|
|
pe("extension_id repeated", []any{ext("org.example.a", 1), ext("org.example.a", 2)}, nc)
|
|
pe("extension_version 2^32-1", []any{ext("org.example.a", 1<<32-1)}, ResultOK)
|
|
pe("extension_version 2^32", []any{ext("org.example.a", 1<<32)}, nc)
|
|
pe("extension_version 2^53", []any{ext("org.example.a", 1<<53)}, nc)
|
|
pe("extension_version 0", []any{ext("org.example.a", 0)}, ResultOK)
|
|
pe("unknown key 3", []any{map[uint64]any{0: "org.example.a", 1: uint64(1), 3: []byte{0}}}, nc)
|
|
pe("missing key 1, extension_version", []any{map[uint64]any{0: "org.example.a"}}, nc)
|
|
pe("missing key 0, extension_id", []any{map[uint64]any{1: uint64(1)}}, nc)
|
|
pe("keys 0 and 1 out of order", []any{cbortest.Pairs{uint64(1), uint64(1), uint64(0), "org.example.a"}}, nc)
|
|
pe("extension_id as a byte string", []any{map[uint64]any{0: []byte("org.example.a"), 1: uint64(1)}}, nc)
|
|
pe("empty extension_id", []any{ext("", 1)}, nc)
|
|
pe("extension_id of 256 bytes, the implementation limit", []any{ext(strings.Repeat("a", 256), 1)}, ResultOK)
|
|
pe("extension_id of 257 bytes, above the implementation limit", []any{ext(strings.Repeat("a", 257), 1)}, nc)
|
|
pe("extension_id that is not valid UTF-8", []any{map[uint64]any{0: "org.example.\xff", 1: uint64(1)}}, nc)
|
|
pe("extension that is not a map", []any{uint64(1)}, nc)
|
|
return out, nil
|
|
}
|