- 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
136 lines
4.1 KiB
Go
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:]
|
|
}
|