tinyrelayfile
tinyrelay/internal/daemon/private_grasp_test.go/

internal/daemon/private_grasp_test.go raw
1package daemon
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/base64"
7 "encoding/hex"
8 "encoding/json"
9 "net/http"
10 "net/http/httptest"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/FelineStateMachine/tinyrelay/internal/event"
16 "github.com/FelineStateMachine/tinyrelay/internal/policy"
17)
18
19func TestPrivateProfileRedactsTenantMetadata(t *testing.T) {
20 tenant := &Tenant{policy: policy.Defaults(strings.Repeat("a", 64))}
21 tenant.policy.Features.Grasp = true
22 tenant.policy.Features.Grasp08 = true
23 tenant.policy.Reads = "members"
24 tenant.policy.Name = "secret-project"
25 tenant.policy.Description = "do not expose"
26 profile := tenant.PrivateProfile()
27 if !profile.Enabled || profile.Protocol != "GRASP-08" || profile.Reads != "members" {
28 t.Fatalf("unexpected profile: %#v", profile)
29 }
30 if len(profile.Authentication) != 2 || profile.Authentication[0] != "NIP-42" || profile.Authentication[1] != "NIP-98" {
31 t.Fatalf("unexpected auth methods: %#v", profile.Authentication)
32 }
33 info := tenant.privateNIP11()
34 if info["name"] == tenant.policy.Name || info["description"] == tenant.policy.Description {
35 t.Fatalf("private NIP-11 leaked tenant metadata: %#v", info)
36 }
37 if info["private_service"] != true {
38 t.Fatalf("private service marker missing: %#v", info)
39 }
40}
41
42func TestPrivateAccessDisabledIsNoop(t *testing.T) {
43 tenant := &Tenant{policy: policy.Defaults(strings.Repeat("a", 64))}
44 access, err := tenant.PrivateAccess(context.Background(), "outsider")
45 if err != nil || access.Allowed() {
46 t.Fatalf("disabled private access = %#v, %v", access, err)
47 }
48}
49
50func TestPrivatePolicyShape(t *testing.T) {
51 p := policy.Defaults(strings.Repeat("a", 64))
52 p.Features.Grasp08 = true
53 p.Features.Grasp = true
54 if privatePolicy(p) {
55 t.Fatal("open private policy recognized")
56 }
57 p.Reads = "members"
58 if !privatePolicy(p) {
59 t.Fatal("members-only GRASP-08 policy not recognized")
60 }
61}
62
63func TestPrivateAccessFollowsMembershipRevocation(t *testing.T) {
64 _, tenant := testTenant(t)
65 ctx := context.Background()
66 p := tenant.Policy()
67 p.Features.Grasp = true
68 p.Features.Grasp08 = true
69 p.Reads = "members"
70 if err := tenant.applyPolicy(ctx, p); err != nil {
71 t.Fatal(err)
72 }
73 memberSecret := strings.Repeat("2", 64)
74 member, err := event.PublicKey(memberSecret)
75 if err != nil {
76 t.Fatal(err)
77 }
78 if _, err := tenant.community.Execute(ctx, p.Owner, "setmember", []json.RawMessage{json.RawMessage(`"` + member + `"`), json.RawMessage(`{"name":"reader"}`)}); err != nil {
79 t.Fatal(err)
80 }
81 access, err := tenant.PrivateAccess(ctx, member)
82 if err != nil || !access.Allowed() || !access.Member || access.Owner {
83 t.Fatalf("member access = %#v, %v", access, err)
84 }
85 if _, err := tenant.community.Execute(ctx, p.Owner, "removemember", []json.RawMessage{json.RawMessage(`"` + member + `"`)}); err != nil {
86 t.Fatal(err)
87 }
88 if _, err := tenant.PrivateAccess(ctx, member); err == nil {
89 t.Fatal("revoked member retained private access")
90 }
91}
92
93func TestPrivateNIP11OmitsTenantPresentation(t *testing.T) {
94 _, tenant := testTenant(t)
95 p := tenant.Policy()
96 p.Features.Grasp = true
97 p.Features.Grasp08 = true
98 p.Reads = "members"
99 p.Name = "secret-name"
100 p.Description = "secret-description"
101 if err := tenant.applyPolicy(context.Background(), p); err != nil {
102 t.Fatal(err)
103 }
104 req := httptest.NewRequest(http.MethodGet, "http://relay.test/", nil)
105 req.Header.Set("Accept", "application/nostr+json")
106 res := httptest.NewRecorder()
107 tenant.ServeHTTP(res, req)
108 if res.Code != http.StatusOK {
109 t.Fatalf("NIP-11 status=%d body=%s", res.Code, res.Body.String())
110 }
111 body := res.Body.String()
112 if strings.Contains(body, "secret-name") || strings.Contains(body, "secret-description") {
113 t.Fatalf("private NIP-11 leaked presentation metadata: %s", body)
114 }
115 if !strings.Contains(body, "GRASP-08") || !strings.Contains(body, "private_service") {
116 t.Fatalf("private NIP-11 omitted service profile: %s", body)
117 }
118 nip05 := httptest.NewRequest(http.MethodGet, "http://relay.test/.well-known/nostr.json?name=reader", nil)
119 nip05Result := httptest.NewRecorder()
120 tenant.ServeHTTP(nip05Result, nip05)
121 if nip05Result.Code != http.StatusUnauthorized {
122 t.Fatalf("private NIP-05 exposed unsigned metadata: %d %s", nip05Result.Code, nip05Result.Body.String())
123 }
124}
125
126func TestPrivateGitRejectsSignedOutsiderBeforeRepositoryLookup(t *testing.T) {
127 _, tenant := testTenant(t)
128 p := tenant.Policy()
129 p.Features.Grasp = true
130 p.Features.Grasp08 = true
131 p.Reads = "members"
132 if err := tenant.applyPolicy(context.Background(), p); err != nil {
133 t.Fatal(err)
134 }
135 path := "/npub1unknown/private.git/info/refs?service=git-upload-pack"
136 outsider := httptest.NewRequest(http.MethodGet, "http://relay.test"+path, nil)
137 signRequestWithSecret(t, outsider, "", strings.Repeat("3", 64))
138 denied := httptest.NewRecorder()
139 tenant.ServeHTTP(denied, outsider)
140 if denied.Code != http.StatusUnauthorized || denied.Body.Len() != 0 || denied.Header().Get("WWW-Authenticate") != `Nostr method="GET"` {
141 t.Fatalf("signed outsider reached private Git lookup: %d %s", denied.Code, denied.Body.String())
142 }
143 owner := httptest.NewRequest(http.MethodGet, "http://relay.test"+path, nil)
144 signRequest(t, owner, "")
145 allowed := httptest.NewRecorder()
146 tenant.ServeHTTP(allowed, owner)
147 if allowed.Code == http.StatusForbidden || allowed.Code == http.StatusUnauthorized {
148 t.Fatalf("owner was denied before repository lookup: %d %s", allowed.Code, allowed.Body.String())
149 }
150}
151
152func signRequestWithSecret(t *testing.T, r *http.Request, body, secret string) {
153 t.Helper()
154 hash := sha256.Sum256([]byte(body))
155 tags := [][]string{{"u", r.URL.String()}, {"method", r.Method}, {"payload", hex.EncodeToString(hash[:])}}
156 if i := strings.Index(r.URL.Path, ".git/"); i >= 0 {
157 tags = [][]string{{"u", r.URL.Scheme + "://" + r.URL.Host + r.URL.Path[:i+4]}, {"method", http.MethodGet}}
158 }
159 e := event.Event{Kind: 27235, CreatedAt: time.Now().Unix(), Content: "", Tags: tags}
160 if err := event.Sign(&e, secret); err != nil {
161 t.Fatal(err)
162 }
163 raw, err := json.Marshal(e)
164 if err != nil {
165 t.Fatal(err)
166 }
167 r.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(raw))
168}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs