diff --git a/pkg/covenant/name.go b/pkg/covenant/name.go new file mode 100644 index 0000000..f4f0464 --- /dev/null +++ b/pkg/covenant/name.go @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package covenant + +import ( + "crypto/sha3" + + core "dappco.re/go/core" + "dappco.re/go/lns/pkg/primitives" +) + +const maxNameSize = 63 + +var blacklist = map[string]struct{}{ + "example": {}, + "invalid": {}, + "local": {}, + "localhost": {}, + "test": {}, +} + +// VerifyString reports whether a domain name meets the covenant rules. +func VerifyString(name string) bool { + if len(name) == 0 || len(name) > maxNameSize { + return false + } + + for i := 0; i < len(name); i++ { + ch := name[i] + + if ch&0x80 != 0 { + return false + } + + switch { + case ch >= '0' && ch <= '9': + case ch >= 'A' && ch <= 'Z': + return false + case ch >= 'a' && ch <= 'z': + case ch == '-' || ch == '_': + if i == 0 || i == len(name)-1 { + return false + } + default: + return false + } + } + + _, blocked := blacklist[name] + return !blocked +} + +// VerifyBinary reports whether a binary domain name meets the covenant rules. +func VerifyBinary(name []byte) bool { + if len(name) == 0 || len(name) > maxNameSize { + return false + } + + for i := 0; i < len(name); i++ { + ch := name[i] + + if ch&0x80 != 0 { + return false + } + + switch { + case ch >= '0' && ch <= '9': + case ch >= 'A' && ch <= 'Z': + return false + case ch >= 'a' && ch <= 'z': + case ch == '-' || ch == '_': + if i == 0 || i == len(name)-1 { + return false + } + default: + return false + } + } + + _, blocked := blacklist[string(name)] + return !blocked +} + +// VerifyName reports whether a name meets the covenant rules. +func VerifyName(name []byte) bool { + return VerifyBinary(name) +} + +// HashString hashes a validated domain name. +func HashString(name string) (primitives.Hash, error) { + if !VerifyString(name) { + return primitives.Hash{}, core.E("covenant.HashString", "invalid name", nil) + } + + sum := sha3.Sum256([]byte(name)) + return primitives.Hash(sum), nil +} + +// HashBinary hashes a validated domain name. +func HashBinary(name []byte) (primitives.Hash, error) { + if !VerifyBinary(name) { + return primitives.Hash{}, core.E("covenant.HashBinary", "invalid name", nil) + } + + sum := sha3.Sum256(name) + return primitives.Hash(sum), nil +} + +// HashName hashes a validated domain name. +func HashName(name []byte) (primitives.Hash, error) { + return HashBinary(name) +} diff --git a/pkg/covenant/name_test.go b/pkg/covenant/name_test.go new file mode 100644 index 0000000..ce94495 --- /dev/null +++ b/pkg/covenant/name_test.go @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package covenant + +import ( + "crypto/sha3" + "testing" +) + +func TestVerifyString(t *testing.T) { + cases := []struct { + name string + ok bool + }{ + {name: "example", ok: false}, + {name: "invalid", ok: false}, + {name: "localhost", ok: false}, + {name: "test", ok: false}, + {name: "", ok: false}, + {name: "foo", ok: true}, + {name: "foo-bar", ok: true}, + {name: "foo_bar", ok: true}, + {name: "-foo", ok: false}, + {name: "foo-", ok: false}, + {name: "_foo", ok: false}, + {name: "foo_", ok: false}, + {name: "Foo", ok: false}, + {name: "f\x7fo", ok: false}, + } + + for _, tc := range cases { + if got := VerifyString(tc.name); got != tc.ok { + t.Fatalf("VerifyString(%q) = %v, want %v", tc.name, got, tc.ok) + } + } +} + +func TestVerifyBinary(t *testing.T) { + cases := []struct { + name []byte + ok bool + }{ + {name: nil, ok: false}, + {name: []byte("example"), ok: false}, + {name: []byte("foo"), ok: true}, + {name: []byte("foo-bar"), ok: true}, + {name: []byte("foo_bar"), ok: true}, + {name: []byte("-foo"), ok: false}, + {name: []byte("foo-"), ok: false}, + {name: []byte("Foo"), ok: false}, + {name: []byte{0xff}, ok: false}, + } + + for _, tc := range cases { + if got := VerifyBinary(tc.name); got != tc.ok { + t.Fatalf("VerifyBinary(%q) = %v, want %v", tc.name, got, tc.ok) + } + } +} + +func TestHashString(t *testing.T) { + got, err := HashString("foo-bar") + if err != nil { + t.Fatalf("HashString returned error: %v", err) + } + + want := sha3.Sum256([]byte("foo-bar")) + if got != want { + t.Fatalf("HashString returned %x, want %x", got, want) + } +} + +func TestHashBinary(t *testing.T) { + got, err := HashBinary([]byte("foo_bar")) + if err != nil { + t.Fatalf("HashBinary returned error: %v", err) + } + + want := sha3.Sum256([]byte("foo_bar")) + if got != want { + t.Fatalf("HashBinary returned %x, want %x", got, want) + } +} + +func TestHashRejectsInvalidName(t *testing.T) { + if _, err := HashString("Foo"); err == nil { + t.Fatal("HashString should reject invalid names") + } + + if _, err := HashBinary([]byte("foo-")); err == nil { + t.Fatal("HashBinary should reject invalid names") + } +}