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.
463 lines
17 KiB
463 lines
17 KiB
// Package extension implements the single generic extension mechanism shared
|
|
// by PUBLIC_HEADER, CONTROL_CBOR and .dkk (spec §31, §44, §54, §72).
|
|
//
|
|
// Extension data is opaque bytes: the base protocol never decodes or
|
|
// validates its content, and the validity of the containing object never
|
|
// depends on it. The package enforces the structural rules only: valid UTF-8
|
|
// identifiers, extension_version at most 2^32-1, data that is absent or a
|
|
// non-empty byte string, 1 to 64 extensions per array, no identifier repeated
|
|
// within an object, no identifier in both the critical and the noncritical
|
|
// array, canonical order by the UTF-8 bytes of extension_id, rejection of
|
|
// unknown critical extensions, and omission of empty arrays (spec §58.1).
|
|
//
|
|
// Only an application that knows an extension interprets its data. A
|
|
// Registry that also implements DataValidator checks the data of the
|
|
// extensions it knows: invalid data rejects a critical extension with
|
|
// ErrExtensionDataInvalid and makes a noncritical one Unusable (spec §54).
|
|
// A Registry that also implements Placement tells in which objects and
|
|
// arrays each extension is registered: elsewhere a known extension is
|
|
// treated as unknown (spec §54, §72).
|
|
package extension
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/codec"
|
|
)
|
|
|
|
// Limits of one extension array and of one extension (spec §31, §54, §57).
|
|
const (
|
|
// MaxIDLen bounds extension_id. It is an implementation limit (spec §74).
|
|
MaxIDLen = 256
|
|
// MaxExtensions is the largest number of extensions in one array.
|
|
MaxExtensions = 64
|
|
// MaxVersion is the largest extension_version, 2^32-1.
|
|
MaxVersion = 1<<32 - 1
|
|
// MaxDataLen is the largest data: the largest frame of spec §57,
|
|
// SEALED_CONTROL. The frame of the containing object is the effective
|
|
// bound.
|
|
MaxDataLen = 64 << 20
|
|
)
|
|
|
|
// Extension is one entry of an extension array.
|
|
type Extension struct {
|
|
ID string // key 0, extension_id
|
|
Version uint64 // key 1, extension_version
|
|
// Data is the opaque content of key 2, at least one byte, or nil when the
|
|
// extension carries no data and key 2 is omitted. An empty non-nil slice
|
|
// is invalid: an empty byte string never stands for absence (spec §58.1).
|
|
Data []byte
|
|
}
|
|
|
|
// New returns an extension that carries data, of which it keeps a copy. data
|
|
// must hold at least one byte. An extension without data has no constructor:
|
|
// it is the literal Extension{ID: id, Version: version}, which omits key 2.
|
|
func New(id string, version uint64, data []byte) (Extension, error) {
|
|
if data == nil {
|
|
return Extension{}, fmt.Errorf("extension %q: New needs data; an extension without data is Extension{ID, Version}: %w", id, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
e := Extension{ID: id, Version: version, Data: bytes.Clone(data)}
|
|
if err := validate(e); err != nil {
|
|
return Extension{}, err
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
// Registry tells which extensions the application implements. A nil Registry
|
|
// knows none, which is the state of the base protocol V1.
|
|
type Registry interface {
|
|
Known(id string, version uint64) bool
|
|
}
|
|
|
|
// DataValidator is an optional interface of a Registry. ValidateData reports
|
|
// whether the data of e (nil when e carries none) follows the registered
|
|
// schema of (e.ID, e.Version) (spec §72). It is called only for extensions
|
|
// the Registry knows.
|
|
type DataValidator interface {
|
|
ValidateData(e Extension) error
|
|
}
|
|
|
|
// Object names an object that carries extension arrays (spec §54).
|
|
type Object int
|
|
|
|
// The objects that carry extensions.
|
|
const (
|
|
PublicHeader Object = iota + 1 // PUBLIC_HEADER, keys 5 and 6 (spec §24)
|
|
Control // CONTROL_CBOR, keys 4 and 5 (spec §31)
|
|
AccessKey // the body of a .dkk, keys 7 and 8 (spec §41)
|
|
)
|
|
|
|
// String returns the name of the object in the specification.
|
|
func (o Object) String() string {
|
|
switch o {
|
|
case PublicHeader:
|
|
return "PUBLIC_HEADER"
|
|
case Control:
|
|
return "CONTROL_CBOR"
|
|
case AccessKey:
|
|
return ".dkk"
|
|
}
|
|
return fmt.Sprintf("Object(%d)", int(o))
|
|
}
|
|
|
|
// Array names one of the two extension arrays of an object.
|
|
type Array int
|
|
|
|
// The two extension arrays of an object.
|
|
const (
|
|
Critical Array = iota + 1 // critical_extensions
|
|
Noncritical // noncritical_extensions
|
|
)
|
|
|
|
// String returns the name of the array in the specification.
|
|
func (a Array) String() string {
|
|
switch a {
|
|
case Critical:
|
|
return "critical_extensions"
|
|
case Noncritical:
|
|
return "noncritical_extensions"
|
|
}
|
|
return fmt.Sprintf("Array(%d)", int(a))
|
|
}
|
|
|
|
// Placement is an optional interface of a Registry. RegisteredIn reports
|
|
// whether (id, version) is registered for the array arr of the object obj:
|
|
// the registration of each extension declares the objects and arrays where
|
|
// it may appear (spec §72). It is called only for extensions the Registry
|
|
// knows. A known extension that appears in an object or array it is not
|
|
// registered for is treated there as unknown (spec §54): a critical one is
|
|
// rejected with ErrExtensionCriticalUnknown and a noncritical one is ignored,
|
|
// its data neither checked nor interpreted. A Registry that does not
|
|
// implement Placement knows each of its extensions in every object and array.
|
|
//
|
|
// Placement is a rule of readers here. An encoder must not write a
|
|
// registered extension where it is not registered (spec §72), but the
|
|
// writers of this module, capsule.Encrypt and accesskey.Encode, take no
|
|
// Registry and write the extensions they are given: the application, which
|
|
// knows the registration, applies that rule.
|
|
type Placement interface {
|
|
RegisteredIn(id string, version uint64, obj Object, arr Array) bool
|
|
}
|
|
|
|
// KnownIn reports whether reg knows (id, version) in the array arr of obj:
|
|
// reg knows it and, when reg is a Placement, registers it there (spec §54,
|
|
// §72). A nil Registry knows none. It is the rule of CheckCriticalIn and
|
|
// CheckNoncriticalIn, for an application that interprets the extensions of
|
|
// an object it has read: an extension unknown there is not interpreted.
|
|
func KnownIn(reg Registry, id string, version uint64, obj Object, arr Array) bool {
|
|
return reg != nil && reg.Known(id, version) && registered(reg, id, version, obj, arr)
|
|
}
|
|
|
|
// registered reports whether reg, which knows (id, version), registers it for
|
|
// the array arr of obj: always when reg is not a Placement.
|
|
func registered(reg Registry, id string, version uint64, obj Object, arr Array) bool {
|
|
p, ok := reg.(Placement)
|
|
return !ok || p.RegisteredIn(id, version, obj, arr)
|
|
}
|
|
|
|
// Set is a simple Registry. It does not validate data, and it knows each of
|
|
// its extensions in every object and array.
|
|
type Set map[string][]uint64
|
|
|
|
// Known reports whether (id, version) is in the set.
|
|
func (s Set) Known(id string, version uint64) bool { return slices.Contains(s[id], version) }
|
|
|
|
// compare orders extensions by the UTF-8 bytes of extension_id, the order of
|
|
// spec §54: strings.Compare compares the bytes as unsigned values, and a
|
|
// proper prefix sorts first. Within one object an identifier appears at most
|
|
// once.
|
|
func compare(a, b Extension) int { return strings.Compare(a.ID, b.ID) }
|
|
|
|
func validate(e Extension) error {
|
|
if e.ID == "" || len(e.ID) > MaxIDLen || !utf8.ValidString(e.ID) {
|
|
return fmt.Errorf("extension: invalid extension_id %q: %w", e.ID, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
if e.Version > MaxVersion {
|
|
return fmt.Errorf("extension %s: extension_version %d exceeds %d: %w", e.ID, e.Version, uint64(MaxVersion), datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
if e.Data != nil && (len(e.Data) == 0 || len(e.Data) > MaxDataLen) {
|
|
return fmt.Errorf("extension %s: data of %d bytes outside 1..%d: %w", e.ID, len(e.Data), MaxDataLen, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Canonical validates one extension array and returns it in canonical order,
|
|
// sorted by the UTF-8 bytes of extension_id: 1 to 64 valid extensions, no
|
|
// identifier repeated. An empty input yields nil, so that the array key is
|
|
// omitted (spec §58.1).
|
|
func Canonical(exts []Extension) ([]Extension, error) {
|
|
if len(exts) == 0 {
|
|
return nil, nil
|
|
}
|
|
if len(exts) > MaxExtensions {
|
|
return nil, errTooMany(len(exts))
|
|
}
|
|
out := slices.Clone(exts)
|
|
slices.SortFunc(out, compare)
|
|
if err := checkArray(out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func errTooMany(n int) error {
|
|
return fmt.Errorf("extension: %d extensions in one array, at most %d: %w", n, MaxExtensions, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
|
|
// checkArray applies the rules of DecodeArray to an array to be written: 1 to
|
|
// 64 valid extensions in canonical order, no identifier repeated.
|
|
func checkArray(exts []Extension) error {
|
|
switch {
|
|
case len(exts) == 0:
|
|
return fmt.Errorf("extension: empty array; an absent array omits its key: %w", datekeys.ErrNonCanonicalCBOR)
|
|
case len(exts) > MaxExtensions:
|
|
return errTooMany(len(exts))
|
|
}
|
|
for i, e := range exts {
|
|
if err := validate(e); err != nil {
|
|
return err
|
|
}
|
|
if i == 0 {
|
|
continue
|
|
}
|
|
switch c := compare(exts[i-1], e); {
|
|
case c == 0:
|
|
return fmt.Errorf("extension %s: appears more than once: %w", e.ID, datekeys.ErrNonCanonicalCBOR)
|
|
case c > 0:
|
|
return fmt.Errorf("extension %s: array is not in canonical order: %w", e.ID, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeArray writes a non-empty extension array as Canonical returns it:
|
|
// each extension is the map {0: extension_id, 1: extension_version} with
|
|
// key 2, the data as a byte string, only when the extension carries data
|
|
// (spec §54). An array that DecodeArray would reject is not written: its
|
|
// error is recorded in e, whose Out returns it.
|
|
func EncodeArray(e *codec.Encoder, exts []Extension) {
|
|
if err := checkArray(exts); err != nil {
|
|
e.Fail(err)
|
|
return
|
|
}
|
|
e.Array(len(exts))
|
|
for _, x := range exts {
|
|
if x.Data == nil {
|
|
e.Map(2)
|
|
} else {
|
|
e.Map(3)
|
|
}
|
|
e.Uint(0)
|
|
e.Text(x.ID)
|
|
e.Uint(1)
|
|
e.Uint(x.Version)
|
|
if x.Data != nil {
|
|
e.Uint(2)
|
|
e.Bstr(x.Data)
|
|
}
|
|
}
|
|
}
|
|
|
|
// DecodeArray reads one extension array. The array holds 1 to 64 entries,
|
|
// which its head declares before any is read; each entry is a map with
|
|
// key 0, a non-empty UTF-8 extension_id of at most MaxIDLen bytes, key 1, an
|
|
// extension_version of at most MaxVersion, and optionally key 2, a byte
|
|
// string of at least one byte whose content is copied and never decoded
|
|
// (spec §54, §58.1). Entries are in canonical order with no identifier
|
|
// repeated. Every failure wraps ErrNonCanonicalCBOR.
|
|
func DecodeArray(d *codec.Decoder) ([]Extension, error) {
|
|
n, err := d.Array(MaxExtensions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("extension: %w", err)
|
|
}
|
|
if n == 0 {
|
|
return nil, fmt.Errorf("extension: empty array; an absent array omits its key: %w", datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
out := make([]Extension, 0, n)
|
|
for i := range n {
|
|
e, err := decodeOne(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if i > 0 {
|
|
switch c := compare(out[i-1], e); {
|
|
case c == 0:
|
|
return nil, fmt.Errorf("extension %s: appears more than once: %w", e.ID, datekeys.ErrNonCanonicalCBOR)
|
|
case c > 0:
|
|
return nil, fmt.Errorf("extension %s: array is not in canonical order: %w", e.ID, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// decodeOne reads the map of one extension. Key 2, when present, must be a
|
|
// byte string of at least one byte: the empty byte string and every other
|
|
// CBOR type are rejected explicitly (spec §54, §58.1).
|
|
func decodeOne(d *codec.Decoder) (Extension, error) {
|
|
var e Extension
|
|
pairs, err := d.Map(3)
|
|
if err != nil {
|
|
return e, fmt.Errorf("extension: %w", err)
|
|
}
|
|
var seen [3]bool
|
|
for range pairs {
|
|
k, err := d.Key()
|
|
if err != nil {
|
|
return e, fmt.Errorf("extension: %w", err)
|
|
}
|
|
switch k {
|
|
case 0:
|
|
e.ID, err = d.Text(MaxIDLen)
|
|
case 1:
|
|
e.Version, err = d.Uint(MaxVersion)
|
|
case 2:
|
|
if e.Data, err = d.Bstr(0, MaxDataLen); err == nil && len(e.Data) == 0 {
|
|
return e, fmt.Errorf("extension %q: data is present but empty; an extension without data omits key 2: %w", e.ID, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
default:
|
|
return e, fmt.Errorf("extension %q: unknown key %d: %w", e.ID, k, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
if err != nil {
|
|
return e, fmt.Errorf("extension %q: key %d: %w", e.ID, k, err)
|
|
}
|
|
seen[k] = true
|
|
}
|
|
if !seen[0] || !seen[1] {
|
|
return e, fmt.Errorf("extension %q: extension_id and extension_version are required: %w", e.ID, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
if err := validate(e); err != nil {
|
|
return e, err
|
|
}
|
|
return e, d.EndMap()
|
|
}
|
|
|
|
// CheckDisjoint applies the cross-array rule of one object: an extension_id
|
|
// must not appear in both critical_extensions and noncritical_extensions
|
|
// (spec §31, §54). Arrays in canonical order, as Canonical and DecodeArray
|
|
// return them, are merged in one linear pass; other input is sorted first.
|
|
func CheckDisjoint(critical, noncritical []Extension) error {
|
|
critical, noncritical = sorted(critical), sorted(noncritical)
|
|
for i, j := 0, 0; i < len(critical) && j < len(noncritical); {
|
|
switch c := compare(critical[i], noncritical[j]); {
|
|
case c == 0:
|
|
return fmt.Errorf("extension %s: both critical and noncritical: %w", critical[i].ID, datekeys.ErrNonCanonicalCBOR)
|
|
case c < 0:
|
|
i++
|
|
default:
|
|
j++
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sorted returns exts itself when it is in canonical order, and a sorted copy
|
|
// otherwise.
|
|
func sorted(exts []Extension) []Extension {
|
|
if slices.IsSortedFunc(exts, compare) {
|
|
return exts
|
|
}
|
|
sorted := slices.Clone(exts)
|
|
slices.SortFunc(sorted, compare)
|
|
return sorted
|
|
}
|
|
|
|
// CheckCritical rejects every critical extension unknown to reg with
|
|
// ErrExtensionCriticalUnknown and, when reg is a DataValidator, every known
|
|
// one whose data it rejects with ErrExtensionDataInvalid (spec §54, §70).
|
|
// An unknown extension takes precedence over invalid data. It does not know
|
|
// the object of the array and consults no Placement: CheckCriticalIn does.
|
|
func CheckCritical(critical []Extension, reg Registry) error {
|
|
return checkCritical(critical, reg, 0)
|
|
}
|
|
|
|
// CheckCriticalIn is CheckCritical for the critical_extensions of obj: an
|
|
// extension that reg knows but, as a Placement, does not register for that
|
|
// array of obj is unknown there, ErrExtensionCriticalUnknown (spec §54,
|
|
// §72). The reading flow of package capsule checks every critical array
|
|
// with it.
|
|
func CheckCriticalIn(obj Object, critical []Extension, reg Registry) error {
|
|
return checkCritical(critical, reg, obj)
|
|
}
|
|
|
|
// checkCritical checks the critical extensions of obj, or of any object when
|
|
// obj is 0: every unknown one first, then the data of the known ones.
|
|
func checkCritical(critical []Extension, reg Registry, obj Object) error {
|
|
for _, c := range critical {
|
|
if reg == nil || !reg.Known(c.ID, c.Version) {
|
|
return fmt.Errorf("extension %s v%d: %w", c.ID, c.Version, datekeys.ErrExtensionCriticalUnknown)
|
|
}
|
|
if obj != 0 && !registered(reg, c.ID, c.Version, obj, Critical) {
|
|
return fmt.Errorf("extension %s v%d: known, but not registered for the %s of %s: %w", c.ID, c.Version, Critical, obj, datekeys.ErrExtensionCriticalUnknown)
|
|
}
|
|
}
|
|
for _, c := range critical {
|
|
if err := validateData(c, reg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Unusable is a known noncritical extension whose data does not follow its
|
|
// registered schema. The object that carries it stays valid; the application
|
|
// must not use the extension, and the caller is told (spec §54).
|
|
type Unusable struct {
|
|
ID string
|
|
Version uint64
|
|
Err error // wraps datekeys.ErrExtensionDataInvalid
|
|
}
|
|
|
|
// CheckNoncritical returns the noncritical extensions that reg knows and whose
|
|
// data it rejects. It never fails the object: unknown noncritical extensions
|
|
// are ignored, and a Registry that is not a DataValidator rejects no data
|
|
// (spec §54). It does not know the object of the array and consults no
|
|
// Placement: CheckNoncriticalIn does.
|
|
func CheckNoncritical(noncritical []Extension, reg Registry) []Unusable {
|
|
return checkNoncritical(noncritical, reg, 0)
|
|
}
|
|
|
|
// CheckNoncriticalIn is CheckNoncritical for the noncritical_extensions of
|
|
// obj: an extension that reg knows but, as a Placement, does not register
|
|
// for that array of obj is unknown there and ignored, its data unchecked
|
|
// (spec §54, §72). The reading flow of package capsule checks every
|
|
// noncritical array with it.
|
|
func CheckNoncriticalIn(obj Object, noncritical []Extension, reg Registry) []Unusable {
|
|
return checkNoncritical(noncritical, reg, obj)
|
|
}
|
|
|
|
// checkNoncritical checks the noncritical extensions of obj, or of any
|
|
// object when obj is 0.
|
|
func checkNoncritical(noncritical []Extension, reg Registry, obj Object) []Unusable {
|
|
var out []Unusable
|
|
for _, n := range noncritical {
|
|
if reg == nil || !reg.Known(n.ID, n.Version) || obj != 0 && !registered(reg, n.ID, n.Version, obj, Noncritical) {
|
|
continue
|
|
}
|
|
if err := validateData(n, reg); err != nil {
|
|
out = append(out, Unusable{ID: n.ID, Version: n.Version, Err: err})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// validateData applies the optional DataValidator of reg to a known
|
|
// extension. The validator's own error is kept as text only, so that the
|
|
// result carries exactly one normative code.
|
|
func validateData(e Extension, reg Registry) error {
|
|
v, ok := reg.(DataValidator)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if err := v.ValidateData(e); err != nil {
|
|
return fmt.Errorf("extension %s v%d: data: %v: %w", e.ID, e.Version, err, datekeys.ErrExtensionDataInvalid)
|
|
}
|
|
return nil
|
|
}
|