niko_trust/pkg/identity/alias.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

136 lines
4.1 KiB
Go

package identity
import (
"errors"
"strings"
"unicode"
"unicode/utf8"
)
// Aliases (INV-7).
//
// An alias is a human-readable label that an identity asserts about itself. It
// is decoration for user interfaces and nothing else:
//
// - It is not unique. Two identities may claim the same alias, and the
// protocol does nothing to prevent that.
// - It is not verified. "Self-asserted" means exactly that; no one checks it.
// - It never participates in signature verification or in any authorization
// decision.
//
// Alias spoofing is therefore not a protocol vulnerability but a user
// interface concern. The mitigation is presentational: never show an alias
// without the address it belongs to. Display code should render
//
// NikoCraft (trust1q...w4np)
//
// and never "NikoCraft" alone.
//
// The validation here is not a security control. It exists to bound size and
// to strip characters that let one alias impersonate another visually or
// corrupt terminal output.
const (
// MaxAliasLen is the maximum length of an alias in bytes.
MaxAliasLen = 64
// MaxAliasRunes is the maximum length of an alias in runes, bounding the
// visual width independently of UTF-8 encoding length.
MaxAliasRunes = 32
)
var (
// ErrAliasTooLong is returned for an alias exceeding the size limits.
ErrAliasTooLong = errors.New("identity: alias too long")
// ErrAliasInvalidUTF8 is returned for an alias that is not valid UTF-8.
ErrAliasInvalidUTF8 = errors.New("identity: alias is not valid UTF-8")
// ErrAliasControlChar is returned for an alias containing control,
// bidirectional-override or other non-printing characters.
ErrAliasControlChar = errors.New("identity: alias contains a disallowed character")
// ErrAliasWhitespace is returned for an alias with leading or trailing
// whitespace, which would otherwise create look-alike aliases.
ErrAliasWhitespace = errors.New("identity: alias has leading or trailing whitespace")
)
// Alias is a validated display label. Its zero value is the empty alias, which
// is always acceptable: an identity is under no obligation to name itself.
type Alias struct {
s string
}
// ParseAlias validates a self-asserted display label.
//
// An empty alias is valid and yields the zero Alias.
func ParseAlias(s string) (Alias, error) {
if s == "" {
return Alias{}, nil
}
if len(s) > MaxAliasLen {
return Alias{}, ErrAliasTooLong
}
if !utf8.ValidString(s) {
return Alias{}, ErrAliasInvalidUTF8
}
if utf8.RuneCountInString(s) > MaxAliasRunes {
return Alias{}, ErrAliasTooLong
}
if strings.TrimSpace(s) != s {
return Alias{}, ErrAliasWhitespace
}
for _, r := range s {
if !allowedAliasRune(r) {
return Alias{}, ErrAliasControlChar
}
}
return Alias{s: s}, nil
}
// allowedAliasRune reports whether r may appear in an alias.
//
// Rejected: control characters, format characters (which include the
// bidirectional overrides U+202A..U+202E and U+2066..U+2069 used to make text
// render in a misleading order), unassigned code points, surrogates, private
// use characters, and every space character other than a plain ASCII space.
func allowedAliasRune(r rune) bool {
if r == ' ' {
return true
}
if unicode.IsControl(r) || unicode.IsSpace(r) {
return false
}
if unicode.In(r, unicode.Cf, unicode.Cs, unicode.Co, unicode.Cn) {
return false
}
return unicode.IsPrint(r)
}
// String returns the alias text.
func (a Alias) String() string { return a.s }
// IsEmpty reports whether the alias is unset.
func (a Alias) IsEmpty() bool { return a.s == "" }
// Display renders an identity for a user interface.
//
// The address is always included, because an alias on its own is not evidence
// of anything. Callers must not build their own alias-only display strings.
func Display(alias Alias, id Identity) string {
addr := id.String()
if alias.IsEmpty() {
return addr
}
return alias.String() + " (" + shortAddress(addr) + ")"
}
// shortAddress abbreviates an address for display while keeping enough of both
// ends to make substitution visible.
func shortAddress(addr string) string {
const head, tail = 10, 6
if len(addr) <= head+tail+1 {
return addr
}
return addr[:head] + "\u2026" + addr[len(addr)-tail:]
}