niko_trust/internal/tce/tce.go
Niko Marmeladkov 9d66003689
Initial commit: signed-object trust relay, verifier, and docs
- server: relay storing signed objects (PUT/GET), per-IP rate limiting,
  per-subject quota (1000), one-response-per-request, pagination,
  /v1/healthz /v1/readyz /v1/metrics
- verify: signature-verifying trust evaluator; every object is checked via
  env.Verify(), approvals via VerifyApprovalResponse, revocations via
  VerifyRevocationOf; k-of-n approval quorum
- docs: TRUST-MODEL.md and API.md describing issuer-anchored signatures and
  the endpoint/status-code contract
- tests: server, verify, and ratelimit packages
2026-08-12 22:36:49 +03:00

217 lines
7 KiB
Go

// Package tce implements Trust Canonical Encoding version 1.
//
// TCE is the byte-exact representation over which every protocol signature is
// computed. The specification is docs/PROTOCOL.md and the frozen vectors in
// testdata/vectors/tce_vectors.json are normative: this package conforms to
// them, never the other way round.
//
// The central property is that every value has exactly one encoding. The
// encoder emits that encoding, and the decoder accepts only that encoding.
// Anything else, including a longer spelling of the same value, is an error.
// This is what makes a signature over TCE bytes meaningful: two byte strings
// cannot denote the same object, so a signature cannot be transplanted from
// one meaning to another.
//
// The decoder is strict and never repairs input. It does not skip fields it
// does not understand, because a decoder that skipped an unknown field would
// compute a different meaning for the same signed bytes than one that
// understood it, while both saw a valid signature.
//
// This package handles encoding only. It performs no signing, no verification
// and no policy evaluation, and it imports neither encoding/json nor any
// storage or transport package (INV-8, INV-9).
package tce
import (
"errors"
"fmt"
)
// Framing.
const (
// Magic is the domain separation prefix of every TCE object. The trailing
// NUL terminates the ASCII portion so that a longer magic in a future
// version cannot be a prefix of this one.
Magic = "trust.n1ko.dev/tce/1\x00"
// MagicLen is the length of Magic in bytes.
MagicLen = len(Magic)
// Version is the object version encoded by this implementation.
Version = 1
)
// ObjectTag identifies the type of a TCE object. It is part of the signed
// bytes, so a signature over one object type can never be replayed as
// another.
type ObjectTag byte
// Object tags. Tag 0x00 is permanently reserved so that an all-zero buffer is
// never a valid object.
const (
TagReserved ObjectTag = 0x00
TagIdentity ObjectTag = 0x01
TagClaim ObjectTag = 0x02
TagRevocation ObjectTag = 0x03
TagApprovalRequest ObjectTag = 0x04
TagApprovalResponse ObjectTag = 0x05
TagAuthAssertion ObjectTag = 0x06
)
// String renders a tag for diagnostics.
func (t ObjectTag) String() string {
switch t {
case TagIdentity:
return "identity"
case TagClaim:
return "claim"
case TagRevocation:
return "revocation"
case TagApprovalRequest:
return "approval_request"
case TagApprovalResponse:
return "approval_response"
case TagAuthAssertion:
return "auth_assertion"
default:
return fmt.Sprintf("unknown(0x%02x)", byte(t))
}
}
// knownTag reports whether t is a defined object tag in version 1.
func knownTag(t ObjectTag) bool {
switch t {
case TagIdentity, TagClaim, TagRevocation,
TagApprovalRequest, TagApprovalResponse, TagAuthAssertion:
return true
default:
return false
}
}
// ValueTag identifies the type of a claim or payload value.
type ValueTag byte
// Value tags. False and true have distinct tags rather than one boolean tag
// with a payload byte, so there is no invalid third spelling of a boolean.
//
// Tags 0x05 to 0x07 are reserved for future types and must be rejected in
// version 1 rather than skipped.
const (
ValNull ValueTag = 0x00
ValFalse ValueTag = 0x01
ValTrue ValueTag = 0x02
ValString ValueTag = 0x03
ValNumber ValueTag = 0x04
ValResBytes ValueTag = 0x05
ValResArray ValueTag = 0x06
ValResMap ValueTag = 0x07
)
// Sizes fixed by the specification.
const (
AddressVersion = 0
PubKeySize = 32
NonceSize = 16
HashSize = 32
ChallengeSize = 32
SignatureSize = 64
)
// Field and object limits from PROTOCOL.md section 6.3. These are part of the
// format: an object exceeding any of them is invalid everywhere, so a signer
// cannot produce an object that some verifiers accept and others reject.
const (
MaxUvarintBytes = 10
MaxKeyLen = 128
MaxStringValue = 512
MaxNumberToken = 52
MaxMapEntries = 32
MaxActionLen = 128
MaxMessageLen = 256
MaxReasonLen = 256
MaxAliasLen = 64
MaxScopeLen = 32
MaxAudienceLen = 128
MaxNumberIntDigs = 32
MaxNumberFracDig = 18
MaxNumberSource = 64
MaxIdentityTCE = 1024
MaxClaimTCE = 4096
MaxRevocTCE = 1024
MaxRequestTCE = 8192
MaxResponseTCE = 1024
MaxAuthTCE = 1024
// MaxObjectTCE bounds any object and is used to size read limits before
// the object type is known.
MaxObjectTCE = MaxRequestTCE
)
// Timestamp bounds from PROTOCOL.md section 4.6.
const (
MinTimestamp = 1_000_000_000 // 2001-09-09T01:46:40Z
MaxTimestamp = 4_102_444_800 // 2100-01-01T00:00:00Z
// MaxApprovalLifetime is the largest permitted gap between an approval
// request's created_at and expires_at.
MaxApprovalLifetime = 60
)
// Decision values for an ApprovalResponse.
const (
DecisionDeny = 0
DecisionAllow = 1
)
// Errors returned by this package.
//
// The set is deliberately small and carries no attacker-controlled data, so
// that error text cannot be used to exfiltrate input or to fingerprint a
// parser state machine.
var (
ErrMagic = errors.New("tce: bad magic")
ErrObjectTag = errors.New("tce: unknown object tag")
ErrVersion = errors.New("tce: unsupported object version")
ErrTruncated = errors.New("tce: truncated input")
ErrTrailing = errors.New("tce: trailing bytes after object")
ErrUvarint = errors.New("tce: malformed uvarint")
ErrNonMinimal = errors.New("tce: non-minimal uvarint")
ErrOverflow = errors.New("tce: integer overflow")
ErrTooLong = errors.New("tce: field exceeds maximum length")
ErrObjectTooLarge = errors.New("tce: object exceeds maximum size")
ErrUTF8 = errors.New("tce: invalid UTF-8")
ErrControlChar = errors.New("tce: control character in string")
ErrKeyGrammar = errors.New("tce: map key does not match grammar")
ErrDuplicateKey = errors.New("tce: duplicate map key")
ErrKeyOrder = errors.New("tce: map keys not in ascending order")
ErrValueTag = errors.New("tce: unknown or reserved value tag")
ErrNumberFormat = errors.New("tce: not a valid JSON number")
ErrNumberRange = errors.New("tce: number out of representable range")
ErrTimestamp = errors.New("tce: timestamp out of range")
ErrFieldSize = errors.New("tce: fixed-size field has wrong length")
ErrAddressVersion = errors.New("tce: unsupported address version")
ErrEmptyMap = errors.New("tce: map requires at least one entry")
ErrDecision = errors.New("tce: unknown decision value")
ErrLifetime = errors.New("tce: approval lifetime out of bounds")
ErrExpiry = errors.New("tce: expires_at must be after created_at")
)
// fieldError annotates a sentinel error with the field that failed, without
// including any input data.
type fieldError struct {
field string
err error
}
func (e *fieldError) Error() string { return e.field + ": " + e.err.Error() }
func (e *fieldError) Unwrap() error { return e.err }
func fieldErr(field string, err error) error {
if err == nil {
return nil
}
return &fieldError{field: field, err: err}
}