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

186 lines
4.8 KiB
Go

package identity_test
import (
"strings"
"testing"
"git.n1ko.dev/Niko/niko_trust/pkg/identity"
)
func TestAliasAccepts(t *testing.T) {
for _, s := range []string{
"",
"Niko",
"NikoCraft",
"niko craft",
"Niko-Craft_2",
"[VIP] Niko",
"Ник",
"日本語",
"a",
strings.Repeat("a", 32),
} {
a, err := identity.ParseAlias(s)
if err != nil {
t.Errorf("ParseAlias(%q) = %v", s, err)
continue
}
if a.String() != s {
t.Errorf("alias altered: got %q want %q", a.String(), s)
}
}
}
func TestAliasRejects(t *testing.T) {
tests := []struct {
name string
in string
}{
{"too many runes", strings.Repeat("a", 33)},
{"too many bytes", strings.Repeat("é", 40)},
{"leading space", " Niko"},
{"trailing space", "Niko "},
{"newline", "Niko\nCraft"},
{"carriage return", "Niko\rCraft"},
{"tab", "Niko\tCraft"},
{"nul", "Niko\x00"},
{"escape", "Niko\x1b[31m"},
{"invalid utf8", "Niko\xff\xfe"},
{"zero width space", "Ni\u200bko"},
{"zero width joiner", "Ni\u200dko"},
{"rtl override", "Niko\u202eEVIL"},
{"lrm", "Niko\u200e"},
{"isolate", "Niko\u2066EVIL\u2069"},
{"non breaking space", "Niko\u00a0Craft"},
{"ideographic space", "Niko\u3000Craft"},
{"private use", "Niko\uf8ff"},
{"unassigned", "Niko\U000e0001"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if _, err := identity.ParseAlias(tc.in); err == nil {
t.Fatalf("accepted %q", tc.in)
}
})
}
}
// TestAliasIsNotUnique documents that alias collision is expected and is not
// treated as an error. Two identities may legitimately claim the same label;
// the address is what distinguishes them (INV-7).
func TestAliasIsNotUnique(t *testing.T) {
a, err := identity.ParseAlias("NikoCraft")
if err != nil {
t.Fatal(err)
}
b, err := identity.ParseAlias("NikoCraft")
if err != nil {
t.Fatal(err)
}
if a.String() != b.String() {
t.Fatal("expected the impersonating alias to be accepted verbatim")
}
}
// TestDisplayAlwaysShowsAddress is the mitigation for alias spoofing. An alias
// must never be rendered on its own, or one identity could visually
// impersonate another in an approval prompt.
func TestDisplayAlwaysShowsAddress(t *testing.T) {
victim := mustSigner(t).Identity()
attacker := mustSigner(t).Identity()
alias, err := identity.ParseAlias("NikoCraft")
if err != nil {
t.Fatal(err)
}
victimText := identity.Display(alias, victim)
attackerText := identity.Display(alias, attacker)
if victimText == attackerText {
t.Fatal("two identities with the same alias rendered identically")
}
for _, s := range []string{victimText, attackerText} {
if !strings.Contains(s, "trust1") {
t.Fatalf("display output omits the address: %q", s)
}
}
// An empty alias must still render the full address.
var empty identity.Alias
if got := identity.Display(empty, victim); got != victim.String() {
t.Fatalf("empty alias display = %q, want the full address", got)
}
t.Logf("victim: %s", victimText)
t.Logf("attacker: %s", attackerText)
}
// TestAliasNeverAffectsVerification asserts, at the type level and
// behaviourally, that aliases play no part in signature checking.
func TestAliasNeverAffectsVerification(t *testing.T) {
s := mustSigner(t)
id := s.Identity()
msg := []byte("canonical bytes")
sig := s.Sign(msg)
// Whatever alias anyone asserts, verification is unchanged. There is
// deliberately no API that would even accept an alias here.
for _, name := range []string{"", "Niko", "NikoCraft", "attacker"} {
alias, err := identity.ParseAlias(name)
if err != nil {
t.Fatal(err)
}
_ = alias
if !id.Verify(msg, sig) {
t.Fatal("verification result depends on ambient alias state")
}
}
}
func FuzzParseAlias(f *testing.F) {
for _, s := range []string{"", "Niko", "NikoCraft", " x", "x ", "\u202e", "\xff", strings.Repeat("a", 100)} {
f.Add(s)
}
f.Fuzz(func(t *testing.T, s string) {
a, err := identity.ParseAlias(s)
if err != nil {
return
}
// An accepted alias is returned verbatim: no silent normalisation,
// so what a user reviews is what was stored.
if a.String() != s {
t.Fatalf("alias normalised: %q -> %q", s, a.String())
}
if len(s) > identity.MaxAliasLen {
t.Fatalf("accepted an over-long alias (%d bytes)", len(s))
}
if !utf8Valid(s) {
t.Fatalf("accepted invalid UTF-8")
}
for _, r := range s {
if r == '\n' || r == '\r' || r == 0 || r == 0x1b {
t.Fatalf("accepted a control character %q", r)
}
}
if strings.TrimSpace(s) != s {
t.Fatalf("accepted surrounding whitespace")
}
// Parsing is idempotent.
again, err := identity.ParseAlias(a.String())
if err != nil || again.String() != a.String() {
t.Fatalf("ParseAlias is not idempotent: %v", err)
}
})
}
func utf8Valid(s string) bool {
for _, r := range s {
if r == '\uFFFD' && !strings.Contains(s, "\uFFFD") {
return false
}
}
return true
}