- 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
186 lines
4.8 KiB
Go
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
|
|
}
|