niko_trust/internal/address/address_test.go
Niko Marmeladkov 9d66003689
Initial commit: signed-object trust relay, verifier, and docs
- server: relay storing signed objects (PUT/GET), per-IP rate limiting,
  per-subject quota (1000), one-response-per-request, pagination,
  /v1/healthz /v1/readyz /v1/metrics
- verify: signature-verifying trust evaluator; every object is checked via
  env.Verify(), approvals via VerifyApprovalResponse, revocations via
  VerifyRevocationOf; k-of-n approval quorum
- docs: TRUST-MODEL.md and API.md describing issuer-anchored signatures and
  the endpoint/status-code contract
- tests: server, verify, and ratelimit packages
2026-08-12 22:36:49 +03:00

388 lines
11 KiB
Go

package address_test
import (
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"math/rand"
"strings"
"testing"
"github.com/btcsuite/btcd/btcutil/bech32"
"git.n1ko.dev/Niko/niko_trust/internal/address"
)
// deterministicKey returns a reproducible valid Ed25519 public key.
func deterministicKey(t *testing.T, seedByte byte) ed25519.PublicKey {
t.Helper()
seed := make([]byte, ed25519.SeedSize)
for i := range seed {
seed[i] = seedByte
}
return ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
}
func TestRoundTrip(t *testing.T) {
for i := 0; i < 256; i++ {
pub := deterministicKey(t, byte(i))
a, err := address.FromPubKey(pub)
if err != nil {
t.Fatalf("FromPubKey: %v", err)
}
back, err := address.Parse(a.String())
if err != nil {
t.Fatalf("Parse(%q): %v", a.String(), err)
}
if !back.Equal(a) {
t.Fatalf("round trip changed address")
}
if !ed25519.PublicKey(back.PubKey()).Equal(pub) {
t.Fatalf("public key not recovered exactly")
}
}
}
func TestAddressShape(t *testing.T) {
pub := deterministicKey(t, 7)
a, err := address.FromPubKey(pub)
if err != nil {
t.Fatal(err)
}
s := a.String()
if !strings.HasPrefix(s, "trust1q") {
t.Errorf("address must start with trust1q, got %q", s)
}
if len(s) != address.EncodedLen {
t.Errorf("length = %d, want %d (%q)", len(s), address.EncodedLen, s)
}
if strings.ToLower(s) != s {
t.Errorf("address must be lowercase")
}
if a.Version() != address.Version0 {
t.Errorf("version = %d, want 0", a.Version())
}
t.Logf("sample address: %s", s)
}
// TestKnownVector freezes the encoding. If this test fails, the wire format
// changed and every previously issued address became invalid.
func TestKnownVector(t *testing.T) {
seed := make([]byte, 32) // all zero seed
pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
const wantPub = "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29"
if got := hex.EncodeToString(pub); got != wantPub {
t.Fatalf("test vector public key drifted: %s", got)
}
a, err := address.FromPubKey(pub)
if err != nil {
t.Fatal(err)
}
// Verified against an independent BIP-350 reference implementation, not
// against this package's own output.
const want = "trust1qqak5faue6m2gttz5w5dq2n0p4ek2vs4wuw7ysax8tqy3gvtt8dzj0yfahr"
if a.String() != want {
t.Fatalf("encoding drifted:\n got %s\nwant %s", a.String(), want)
}
}
func TestParseRejectsInvalid(t *testing.T) {
valid, err := address.FromPubKey(deterministicKey(t, 3))
if err != nil {
t.Fatal(err)
}
v := valid.String()
tests := []struct {
name string
in string
want error
}{
{"empty", "", address.ErrEmpty},
{"uppercase", strings.ToUpper(v), address.ErrNotLowercase},
{"mixed case", strings.ToUpper(v[:6]) + v[6:], address.ErrNotLowercase},
{"too long", v + "qqqqqqqqqq", address.ErrTooLong},
{"no separator", strings.ReplaceAll(v, "1", "q"), address.ErrChecksum},
{"truncated", v[:len(v)-1], address.ErrChecksum},
{"extended", v + "q", address.ErrTooLong},
{"bare hrp", "trust1", address.ErrChecksum},
{"garbage", "not-an-address", address.ErrChecksum},
{"invalid char b", strings.Replace(v, "q", "b", 1), address.ErrChecksum},
{"space", "trust1 " + v[7:], address.ErrChecksum},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err := address.Parse(tc.in)
if err == nil {
t.Fatalf("accepted invalid input %q", tc.in)
}
if tc.want != nil && !errorIs(err, tc.want) {
t.Fatalf("error = %v, want %v", err, tc.want)
}
})
}
}
func errorIs(err, target error) bool {
return err != nil && (err == target || strings.Contains(err.Error(), target.Error()))
}
// TestRejectsBech32NotBech32m is the checksum downgrade test. The same payload
// encoded with the original bech32 constant must be refused, otherwise two
// different strings would denote one identity and a caller could be tricked
// into accepting a foreign address family.
func TestRejectsBech32NotBech32m(t *testing.T) {
pub := deterministicKey(t, 11)
payload := append([]byte{address.Version0}, pub...)
conv, err := bech32.ConvertBits(payload, 8, 5, true)
if err != nil {
t.Fatal(err)
}
legacy, err := bech32.Encode(address.HRP, conv) // bech32, not bech32m
if err != nil {
t.Fatal(err)
}
modern, err := bech32.EncodeM(address.HRP, conv)
if err != nil {
t.Fatal(err)
}
if legacy == modern {
t.Fatal("test is meaningless: both constants produced the same string")
}
if _, err := address.Parse(legacy); !errorIs(err, address.ErrNotBech32m) {
t.Fatalf("bech32 (non-m) accepted or wrong error: %v", err)
}
if _, err := address.Parse(modern); err != nil {
t.Fatalf("bech32m rejected: %v", err)
}
}
func TestRejectsWrongHRP(t *testing.T) {
pub := deterministicKey(t, 13)
payload := append([]byte{address.Version0}, pub...)
conv, _ := bech32.ConvertBits(payload, 8, 5, true)
for _, hrp := range []string{"bc", "trus", "trustx", "tb", "trust2"} {
s, err := bech32.EncodeM(hrp, conv)
if err != nil {
t.Fatalf("encode %q: %v", hrp, err)
}
if _, err := address.Parse(s); err == nil {
t.Fatalf("accepted foreign hrp %q", hrp)
}
}
}
func TestRejectsUnknownVersion(t *testing.T) {
pub := deterministicKey(t, 17)
for _, ver := range []byte{1, 2, 0x7f, 0xff} {
payload := append([]byte{ver}, pub...)
conv, _ := bech32.ConvertBits(payload, 8, 5, true)
s, _ := bech32.EncodeM(address.HRP, conv)
if _, err := address.Parse(s); !errorIs(err, address.ErrVersion) {
t.Fatalf("version %d: err = %v, want ErrVersion", ver, err)
}
}
}
func TestRejectsWrongPayloadSize(t *testing.T) {
for _, n := range []int{0, 1, 16, 31, 33, 40} {
payload := make([]byte, n+1)
conv, _ := bech32.ConvertBits(payload, 8, 5, true)
s, err := bech32.EncodeM(address.HRP, conv)
if err != nil {
continue
}
if _, err := address.Parse(s); err == nil {
t.Fatalf("accepted payload of %d key bytes", n)
}
}
}
// TestChecksumDetectsSingleCharCorruption is the core integrity property of
// bech32: any single character substitution must invalidate the string. A
// corrupted address must never silently decode to a different valid identity,
// because that would send a claim or an approval to the wrong party.
func TestChecksumDetectsSingleCharCorruption(t *testing.T) {
const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
valid, err := address.FromPubKey(deterministicKey(t, 23))
if err != nil {
t.Fatal(err)
}
v := valid.String()
sep := strings.LastIndexByte(v, '1')
mutations, accepted := 0, 0
for i := sep + 1; i < len(v); i++ {
for _, c := range charset {
if byte(c) == v[i] {
continue
}
mutated := v[:i] + string(c) + v[i+1:]
mutations++
got, err := address.Parse(mutated)
if err == nil {
accepted++
if got.Equal(valid) {
t.Fatalf("corruption produced the same identity")
}
t.Errorf("single-char corruption at %d accepted: %s", i, mutated)
}
}
}
if mutations == 0 {
t.Fatal("no mutations generated")
}
t.Logf("tested %d single-character corruptions, %d accepted", mutations, accepted)
}
// TestTwoCharSwapDetected covers transposition, the most common human
// transcription error.
func TestTwoCharSwapDetected(t *testing.T) {
valid, _ := address.FromPubKey(deterministicKey(t, 29))
v := valid.String()
sep := strings.LastIndexByte(v, '1')
swaps, accepted := 0, 0
for i := sep + 1; i < len(v)-1; i++ {
if v[i] == v[i+1] {
continue
}
m := v[:i] + string(v[i+1]) + string(v[i]) + v[i+2:]
swaps++
if _, err := address.Parse(m); err == nil {
accepted++
t.Errorf("transposition at %d accepted: %s", i, m)
}
}
t.Logf("tested %d transpositions, %d accepted", swaps, accepted)
}
func TestEqualAndZero(t *testing.T) {
a, _ := address.FromPubKey(deterministicKey(t, 31))
b, _ := address.FromPubKey(deterministicKey(t, 31))
c, _ := address.FromPubKey(deterministicKey(t, 32))
if !a.Equal(b) {
t.Error("same key must be equal")
}
if a.Equal(c) {
t.Error("different keys must not be equal")
}
var zero address.Address
if !zero.IsZero() {
t.Error("zero value must report IsZero")
}
if zero.Equal(zero) {
t.Error("zero address must not compare equal to itself; it denotes no identity")
}
if a.Equal(zero) || zero.Equal(a) {
t.Error("zero must not equal a real address")
}
if zero.String() != "" {
t.Error("zero address must stringify to empty")
}
}
// TestPubKeyIsCopied guards against a caller mutating the key inside a
// validated Address, which would let validated and actual key diverge.
func TestPubKeyIsCopied(t *testing.T) {
a, _ := address.FromPubKey(deterministicKey(t, 37))
k1 := a.PubKey()
for i := range k1 {
k1[i] = 0xff
}
k2 := a.PubKey()
for i := range k2 {
if k2[i] == 0xff {
t.Fatal("mutating a returned key changed the Address")
}
}
if _, err := address.Parse(a.String()); err != nil {
t.Fatalf("address became invalid after caller mutation: %v", err)
}
}
func TestTextMarshaling(t *testing.T) {
a, _ := address.FromPubKey(deterministicKey(t, 41))
b, err := json.Marshal(a)
if err != nil {
t.Fatal(err)
}
if string(b) != `"`+a.String()+`"` {
t.Fatalf("json = %s", b)
}
var back address.Address
if err := json.Unmarshal(b, &back); err != nil {
t.Fatal(err)
}
if !back.Equal(a) {
t.Fatal("json round trip failed")
}
// Unmarshaling must run full validation, not just store the string.
var bad address.Address
if err := json.Unmarshal([]byte(`"trust1qinvalid"`), &bad); err == nil {
t.Fatal("unmarshal accepted an invalid address")
}
var zero address.Address
if _, err := json.Marshal(zero); err == nil {
t.Fatal("marshaling the zero address must fail")
}
}
func TestMustParsePanics(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("MustParse did not panic on invalid input")
}
}()
address.MustParse("trust1qnope")
}
// TestNoTwoStringsForOneKey asserts canonicality (INV-8) over random keys.
func TestNoTwoStringsForOneKey(t *testing.T) {
rng := rand.New(rand.NewSource(1))
seen := make(map[[32]byte]string)
for i := 0; i < 500; i++ {
seed := make([]byte, ed25519.SeedSize)
rng.Read(seed)
pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
a, err := address.FromPubKey(pub)
if err != nil {
t.Fatal(err)
}
key := a.KeyBytes()
if prev, ok := seen[key]; ok && prev != a.String() {
t.Fatalf("one key produced two addresses: %s and %s", prev, a.String())
}
seen[key] = a.String()
// Encoding the same key twice must be byte-identical.
again, _ := address.FromPubKey(pub)
if again.String() != a.String() {
t.Fatal("encoding is not deterministic")
}
}
}
func BenchmarkParse(b *testing.B) {
seed := make([]byte, ed25519.SeedSize)
pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
a, _ := address.FromPubKey(pub)
s := a.String()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := address.Parse(s); err != nil {
b.Fatal(err)
}
}
}