niko_trust/pkg/tce/encoder.go
Niko Marmeladkov 3bf13fa488 Public SDK packages, proxy-aware rate limits, service login recipe
- internal/{address,identity,protocol,tce,transport,verify} -> pkg/ so
  external Go projects can import the verified core; invariant tests
  updated for the new paths
- Config.TrustProxy: key rate limiting by X-Forwarded-For when the relay
  sits behind a reverse proxy (off by default, header never trusted
  otherwise)
- examples/service + examples/approve: complete passwordless login round
  trip (mint request -> wallet approves -> local verify), run live in CI
- docs/SERVICE-GUIDE.md: the integration recipe
2026-08-26 12:49:54 +03:00

309 lines
7.7 KiB
Go

package tce
import (
"sort"
"unicode/utf8"
)
// Encoder builds canonical TCE bytes.
//
// The encoder is append-only and records the first error it encounters,
// after which every further operation is a no-op. This means a caller writes
// a straight sequence of field writes and checks once at the end, without a
// partially built object ever escaping.
type Encoder struct {
buf []byte
err error
}
// NewEncoder returns an encoder with space reserved for a typical object.
func NewEncoder() *Encoder {
return &Encoder{buf: make([]byte, 0, 256)}
}
// Err returns the first error recorded, if any.
func (e *Encoder) Err() error { return e.err }
// fail records the first error.
func (e *Encoder) fail(field string, err error) {
if e.err == nil {
e.err = fieldErr(field, err)
}
}
// Bytes returns the encoded object, or an error if any write failed.
//
// The returned slice is a copy, so a caller cannot alter the encoder's buffer
// afterwards and no two callers share backing storage.
func (e *Encoder) Bytes() ([]byte, error) {
if e.err != nil {
return nil, e.err
}
out := make([]byte, len(e.buf))
copy(out, e.buf)
return out, nil
}
// Header writes the magic, the object tag and the object version. It must be
// the first call on an encoder.
func (e *Encoder) Header(tag ObjectTag) {
if !knownTag(tag) {
e.fail("object tag", ErrObjectTag)
return
}
e.buf = append(e.buf, Magic...)
e.buf = append(e.buf, byte(tag))
e.Uvarint(Version)
}
// Uvarint appends a canonical LEB128 unsigned varint.
func (e *Encoder) Uvarint(n uint64) {
if e.err != nil {
return
}
e.buf = AppendUvarint(e.buf, n)
}
// AppendUvarint appends the canonical LEB128 encoding of n to dst.
//
// Canonical means shortest: the loop emits a continuation byte only while
// bits remain, so no encoding ever ends in a redundant 0x00 group.
func AppendUvarint(dst []byte, n uint64) []byte {
for n >= 0x80 {
dst = append(dst, byte(n)|0x80)
n >>= 7
}
return append(dst, byte(n))
}
// UvarintLen returns the number of bytes AppendUvarint would write.
func UvarintLen(n uint64) int {
l := 1
for n >= 0x80 {
n >>= 7
l++
}
return l
}
// RawBytes appends a length-prefixed byte string with no length limit of its
// own. Callers use FixedBytes or the field-specific helpers instead where a
// limit applies.
func (e *Encoder) RawBytes(field string, b []byte) {
if e.err != nil {
return
}
e.buf = AppendUvarint(e.buf, uint64(len(b)))
e.buf = append(e.buf, b...)
}
// FixedBytes appends a length-prefixed byte string that must have exactly the
// given length.
//
// The length prefix is written even though the width is fixed, so that every
// field stays self-delimiting and a decoder never depends on out-of-band
// knowledge of a field's size.
func (e *Encoder) FixedBytes(field string, b []byte, want int) {
if e.err != nil {
return
}
if len(b) != want {
e.fail(field, ErrFieldSize)
return
}
e.RawBytes(field, b)
}
// String appends a length-prefixed UTF-8 string after validating it.
func (e *Encoder) String(field, s string, maxLen int) {
if e.err != nil {
return
}
if err := ValidateString(s, maxLen); err != nil {
e.fail(field, err)
return
}
e.RawBytes(field, []byte(s))
}
// Identity appends an identity field: the address version, then the
// length-prefixed raw public key.
//
// The raw key is encoded rather than the bech32m address text. The address is
// a presentation format; the key is the identity. Signing the key means a
// change to address rendering cannot invalidate existing signatures.
func (e *Encoder) Identity(field string, pubkey []byte) {
if e.err != nil {
return
}
if len(pubkey) != PubKeySize {
e.fail(field, ErrFieldSize)
return
}
e.Uvarint(AddressVersion)
e.RawBytes(field, pubkey)
}
// Timestamp appends a timestamp, enforcing the protocol's range.
//
// allowZero permits the single exception where 0 means "does not expire".
func (e *Encoder) Timestamp(field string, ts uint64, allowZero bool) {
if e.err != nil {
return
}
if err := ValidateTimestamp(ts, allowZero); err != nil {
e.fail(field, err)
return
}
e.Uvarint(ts)
}
// ValidateTimestamp checks a timestamp against the specification's bounds.
func ValidateTimestamp(ts uint64, allowZero bool) error {
if allowZero && ts == 0 {
return nil
}
if ts < MinTimestamp || ts > MaxTimestamp {
return ErrTimestamp
}
return nil
}
// ValidateString applies the protocol's string rules.
//
// No normalisation is performed. The bytes are signed exactly as supplied,
// because silently rewriting a user's text before signing it would mean the
// user signs something other than what they reviewed.
func ValidateString(s string, maxLen int) error {
if len(s) > maxLen {
return ErrTooLong
}
if !utf8.ValidString(s) {
return ErrUTF8
}
for _, r := range s {
// utf8.ValidString already rejects surrogates and overlong forms.
if r <= 0x1f || r == 0x7f || (r >= 0x80 && r <= 0x9f) {
return ErrControlChar
}
}
return nil
}
// Value appends a typed claim or payload value.
func (e *Encoder) Value(field string, v Value) {
if e.err != nil {
return
}
switch v.tag {
case ValNull, ValFalse, ValTrue:
e.buf = append(e.buf, byte(v.tag))
case ValString:
if err := ValidateString(v.str, MaxStringValue); err != nil {
e.fail(field, err)
return
}
e.buf = append(e.buf, byte(ValString))
e.RawBytes(field, []byte(v.str))
case ValNumber:
canon, err := CanonicalNumber(v.str)
if err != nil {
e.fail(field, err)
return
}
e.buf = append(e.buf, byte(ValNumber))
e.RawBytes(field, []byte(canon))
default:
e.fail(field, ErrValueTag)
}
}
// Map appends a map of keys to values in canonical order.
//
// Entries are sorted by raw key bytes, unsigned bytewise ascending. Duplicate
// keys are an error rather than a last-one-wins situation, because leaving
// the winner to the implementation would mean two conforming encoders
// disagreed about the meaning of the same input.
func (e *Encoder) Map(field string, m map[string]Value, minEntries int) {
if e.err != nil {
return
}
if len(m) < minEntries {
e.fail(field, ErrEmptyMap)
return
}
if len(m) > MaxMapEntries {
e.fail(field, ErrTooLong)
return
}
keys := make([]string, 0, len(m))
for k := range m {
if err := ValidateKey(k); err != nil {
e.fail(field, err)
return
}
keys = append(keys, k)
}
// Go string comparison is bytewise on the underlying bytes, which is the
// ordering the specification requires.
sort.Strings(keys)
// A Go map cannot hold duplicate keys, but the check is kept so that the
// invariant is enforced here as well as in the decoder.
for i := 1; i < len(keys); i++ {
if keys[i] == keys[i-1] {
e.fail(field, ErrDuplicateKey)
return
}
}
e.Uvarint(uint64(len(keys)))
for _, k := range keys {
e.RawBytes(field, []byte(k))
e.Value(field, m[k])
if e.err != nil {
return
}
}
}
// ValidateKey applies the map key grammar from PROTOCOL.md section 6.2:
//
// [a-z][a-z0-9]*([._-][a-z0-9]+)*
//
// This is a lexical rule with no semantics attached. The protocol never
// interprets a key; the restriction exists so that keys sort predictably and
// cannot carry homoglyphs or bidirectional overrides.
func ValidateKey(k string) error {
if len(k) == 0 || len(k) > MaxKeyLen {
return ErrTooLong
}
if k[0] < 'a' || k[0] > 'z' {
return ErrKeyGrammar
}
prevSep := false
for i := 1; i < len(k); i++ {
c := k[i]
switch {
case c >= 'a' && c <= 'z', c >= '0' && c <= '9':
prevSep = false
case c == '.' || c == '_' || c == '-':
// No repeated separators and none at the end.
if prevSep || i == len(k)-1 {
return ErrKeyGrammar
}
prevSep = true
default:
return ErrKeyGrammar
}
}
return nil
}
// checkSize enforces the per-object maximum size.
func (e *Encoder) checkSize(limit int) {
if e.err == nil && len(e.buf) > limit {
e.err = ErrObjectTooLarge
}
}