- 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
388 lines
11 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|