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 }