clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/tinyagent
tinyrelay / protocol / auth / fuzz_test.go
fuzz_test.go 5.0K
1package auth23import (4 "encoding/base64"5 "strings"6 "testing"7 "time"89 "github.com/FelineStateMachine/tinyrelay/protocol/nostr"10)1112// FuzzNIP98Boundary sends arbitrary authorization inputs through the complete13// decoder and verifier. Invalid proofs are expected; panics are not.14func FuzzNIP98Boundary(f *testing.F) {15 f.Add("", "https://relay.example/", "GET", "")16 f.Add("Nostr !!!", "https://relay.example/api", "POST", "body")17 f.Add("nIp98 AA==", "not a URL", "", "\x00")18 secret := strings.Repeat("0", 63) + "1"19 proof := nostr.Event{CreatedAt: 1_700_000_000, Kind: 27235, Tags: [][]string{{"u", "https://relay.example/api"}, {"method", "POST"}}, Content: "seed"}20 if err := nostr.Sign(&proof, secret); err != nil {21 f.Fatal(err)22 }23 raw, err := nostr.Canonical(proof)24 if err != nil {25 f.Fatal(err)26 }27 header := "Nostr " + base64.RawStdEncoding.EncodeToString(raw)28 f.Add(header, "https://relay.example/api", "POST", "")29 f.Fuzz(func(t *testing.T, header, rawURL, method, body string) {30 validator := NewValidator(func() time.Time { return time.Unix(1_700_000_000, 0) })31 _, err := validator.VerifyNIP98(header, rawURL, method, body)32 if header == "Nostr "+base64.RawStdEncoding.EncodeToString(raw) && rawURL == "https://relay.example/api" && method == "POST" && body == "" && err != nil {33 t.Fatalf("valid seeded proof rejected: %v", err)34 }35 })36}3738func FuzzNIP98Binding(f *testing.F) {39 secret := strings.Repeat("0", 63) + "1"40 proof := nostr.Event{CreatedAt: 1_700_000_000, Kind: 27235, Tags: [][]string{{"u", "https://relay.example/api"}, {"method", "POST"}, {"payload", hashBytes([]byte("body"))}}, Content: "binding seed"}41 if err := nostr.Sign(&proof, secret); err != nil {42 f.Fatal(err)43 }44 raw, err := nostr.Canonical(proof)45 if err != nil {46 f.Fatal(err)47 }48 header := "Nostr " + base64.RawStdEncoding.EncodeToString(raw)49 f.Add("https://relay.example/api", "POST", "body")50 f.Add("https://relay.example/other", "POST", "body")51 f.Add("https://relay.example/api", "GET", "body")52 f.Add("https://relay.example/api", "POST", "other")53 f.Fuzz(func(t *testing.T, rawURL, method, body string) {54 validator := NewValidator(func() time.Time { return time.Unix(1_700_000_000, 0) })55 _, err := validator.VerifyNIP98(header, rawURL, method, body)56 if rawURL == "https://relay.example/api" && method == "POST" && body == "body" && err != nil {57 t.Fatalf("valid binding rejected: %v", err)58 }59 if (rawURL != "https://relay.example/api" || method != "POST" || body != "body") && err == nil {60 t.Fatal("accepted mismatched NIP-98 binding")61 }62 })63}6465// FuzzBlossomBoundary covers the alternate authorization scheme and its66// server/blob scope parameters.67func FuzzBlossomBoundary(f *testing.F) {68 f.Add("", "upload", "https://relay.example", "")69 f.Add("Blossom !!!", "delete", "relay", "hash")70 secret := strings.Repeat("0", 63) + "1"71 proof := nostr.Event{CreatedAt: 1_700_000_000, Kind: 24242, Tags: [][]string{{"t", "upload"}, {"expiration", "1700000300"}, {"server", "https://relay.example"}, {"x", strings.Repeat("a", 64)}}, Content: "seed"}72 if err := nostr.Sign(&proof, secret); err != nil {73 f.Fatal(err)74 }75 raw, err := nostr.Canonical(proof)76 if err != nil {77 f.Fatal(err)78 }79 header := "Nostr " + base64.RawStdEncoding.EncodeToString(raw)80 f.Add(header, "upload", "https://relay.example", strings.Repeat("a", 64))81 f.Fuzz(func(t *testing.T, header, action, server, blobHash string) {82 validator := NewValidator(func() time.Time { return time.Unix(1_700_000_000, 0) })83 _, err := validator.VerifyBlossomRequest(header, action, server, blobHash)84 if header == "Nostr "+base64.RawStdEncoding.EncodeToString(raw) && action == "upload" && server == "https://relay.example" && blobHash == strings.Repeat("a", 64) && err != nil {85 t.Fatalf("valid seeded authorization rejected: %v", err)86 }87 })88}8990func FuzzBlossomBinding(f *testing.F) {91 secret := strings.Repeat("0", 63) + "1"92 blobHash := strings.Repeat("a", 64)93 proof := nostr.Event{CreatedAt: 1_700_000_000, Kind: 24242, Tags: [][]string{{"t", "upload"}, {"expiration", "1700000300"}, {"server", "https://relay.example"}, {"x", blobHash}}, Content: "binding seed"}94 if err := nostr.Sign(&proof, secret); err != nil {95 f.Fatal(err)96 }97 raw, err := nostr.Canonical(proof)98 if err != nil {99 f.Fatal(err)100 }101 header := "Nostr " + base64.RawStdEncoding.EncodeToString(raw)102 f.Add("upload", "https://relay.example", blobHash)103 f.Add("delete", "https://relay.example", blobHash)104 f.Add("upload", "https://other.example", blobHash)105 f.Add("upload", "https://relay.example", strings.Repeat("b", 64))106 f.Fuzz(func(t *testing.T, action, server, hash string) {107 validator := NewValidator(func() time.Time { return time.Unix(1_700_000_000, 0) })108 _, err := validator.VerifyBlossomRequest(header, action, server, hash)109 if action == "upload" && server == "https://relay.example" && hash == blobHash && err != nil {110 t.Fatalf("valid binding rejected: %v", err)111 }112 mismatch := action != "upload" || server != "" && server != "https://relay.example" || hash != "" && hash != blobHash113 if mismatch && err == nil {114 t.Fatal("accepted mismatched Blossom binding")115 }116 })117}