clone

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

feat/ui-updates
tinyrelay / protocol / auth / replay_test.go
replay_test.go 3.3K
1package auth23import (4 "encoding/base64"5 "strings"6 "sync"7 "sync/atomic"8 "testing"9 "time"1011 "github.com/FelineStateMachine/tinyrelay/protocol/nostr"12)1314func TestNIP98ReplayWindowBoundary(t *testing.T) {15 now := time.Unix(1700000000, 0)16 v := NewValidator(func() time.Time { return now })17 // A future-dated proof is still fresh after its first replay window ends.18 e := signedEvent(t, 27235, now.Unix()+60, "", [][]string{{"u", "https://relay.example/api"}, {"method", "GET"}})19 header := token(t, e)20 if _, err := v.VerifyNIP98(header, "https://relay.example/api", "GET", ""); err != nil {21 t.Fatal(err)22 }23 now = now.Add(time.Minute)24 if _, err := v.VerifyNIP98(header, "https://relay.example/api", "GET", ""); err == nil || !strings.Contains(err.Error(), "replay") {25 t.Fatalf("proof must remain seen at exactly one minute: %v", err)26 }27 now = now.Add(time.Second)28 if _, err := v.VerifyNIP98(header, "https://relay.example/api", "GET", ""); err != nil {29 t.Fatalf("expired replay entry should be pruned: %v", err)30 }31}3233func TestNIP98AcceptsAllExistingBase64Forms(t *testing.T) {34 now := time.Unix(1700000000, 0)35 e := signedEvent(t, 27235, now.Unix(), "\u083e\u083f", [][]string{{"u", "https://relay.example/api"}, {"method", "GET"}})36 raw, err := nostr.Canonical(e)37 if err != nil {38 t.Fatal(err)39 }40 for _, encoding := range []*base64.Encoding{base64.StdEncoding, base64.RawStdEncoding, base64.URLEncoding, base64.RawURLEncoding} {41 v := NewValidator(func() time.Time { return now })42 header := "nOsTr " + encoding.EncodeToString(raw)43 if _, err := v.VerifyNIP98(header, "https://relay.example/api", "GET", ""); err != nil {44 t.Fatalf("base64 form rejected: %v", err)45 }46 }47}4849func TestNIP42ChallengeExpiresAtOneMinute(t *testing.T) {50 now := time.Unix(1700000000, 0)51 m := NewChallengeManager("wss://relay.example", func() time.Time { return now })52 challenge, err := m.Issue()53 if err != nil {54 t.Fatal(err)55 }56 e := signedEvent(t, 22242, now.Unix(), "", [][]string{{"relay", "wss://relay.example"}, {"challenge", challenge}})57 now = now.Add(time.Minute)58 if err := m.Verify(e); err == nil || !strings.Contains(err.Error(), "challenge") {59 t.Fatalf("challenge must expire even while timestamp remains fresh: %v", err)60 }61}6263func TestConcurrentProofConsumptionIsSingleUse(t *testing.T) {64 now := time.Unix(1700000000, 0)65 v := NewValidator(func() time.Time { return now })66 e := signedEvent(t, 27235, now.Unix(), "", [][]string{{"u", "https://relay.example/api"}, {"method", "GET"}})67 header := token(t, e)68 m := NewChallengeManager("wss://relay.example", func() time.Time { return now })69 challenge, err := m.Issue()70 if err != nil {71 t.Fatal(err)72 }73 authEvent := signedEvent(t, 22242, now.Unix(), "", [][]string{{"relay", "wss://relay.example"}, {"challenge", challenge}})74 for name, verify := range map[string]func() error{75 "NIP-98": func() error { _, err := v.VerifyNIP98(header, "https://relay.example/api", "GET", ""); return err },76 "NIP-42": func() error { return m.Verify(authEvent) },77 } {78 t.Run(name, func(t *testing.T) {79 var successes atomic.Int3280 var wg sync.WaitGroup81 for range 16 {82 wg.Add(1)83 go func() {84 defer wg.Done()85 if verify() == nil {86 successes.Add(1)87 }88 }()89 }90 wg.Wait()91 if got := successes.Load(); got != 1 {92 t.Fatalf("successful consumers = %d, want 1", got)93 }94 })95 }96}