- 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
229 lines
7.4 KiB
Go
229 lines
7.4 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
|
|
TagDelegation ObjectTag = 0x07
|
|
TagKeyRotation ObjectTag = 0x08
|
|
TagKeyRotationConf ObjectTag = 0x09
|
|
)
|
|
|
|
// 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"
|
|
case TagDelegation:
|
|
return "delegation"
|
|
case TagKeyRotation:
|
|
return "key_rotation_request"
|
|
case TagKeyRotationConf:
|
|
return "key_rotation_confirm"
|
|
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, TagDelegation,
|
|
TagKeyRotation, TagKeyRotationConf:
|
|
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
|
|
MaxDelegTCE = 2048
|
|
MaxKeyRotTCE = 1024
|
|
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}
|
|
}
|