tinyrelayfile
tinyrelay/internal/gates/marmot.go/

internal/gates/marmot.go raw
1package gates
2
3import (
4 "context"
5 "encoding/base64"
6 "fmt"
7 "regexp"
8
9 "github.com/FelineStateMachine/tinyrelay/internal/event"
10 "github.com/FelineStateMachine/tinyrelay/internal/relay"
11)
12
13var marmotHex64 = regexp.MustCompile(`^[0-9a-f]{64}$`)
14var marmotID = regexp.MustCompile(`^0x[0-9a-f]{4}$`)
15var marmotVersion = regexp.MustCompile(`^1\.0$`)
16var marmotHex = regexp.MustCompile(`^[0-9a-f]+$`)
17
18// MarmotShape validates relay-visible MLS envelope structure without decoding MLS.
19func MarmotShape(e event.Event) error {
20 if e.Kind == event.KIND_MARMOT_GROUP {
21 return groupShape(e)
22 }
23 if e.Kind == event.KIND_MARMOT_KEY_PACKAGE {
24 return keyPackageShape(e)
25 }
26 return nil
27}
28
29func groupShape(e event.Event) error {
30 if !singletonTag(e, "h", marmotHex64) {
31 return fmt.Errorf("invalid: kind 445 needs one lowercase 32-byte h tag")
32 }
33 b, err := base64.StdEncoding.Strict().DecodeString(e.Content)
34 if err != nil || len(b) < 28 {
35 return fmt.Errorf("invalid: kind 445 content must be padded base64 containing a nonce and authentication tag")
36 }
37 for _, tag := range e.Tags {
38 if len(tag) == 0 || tag[0] != "h" && !(tag[0] == "expiration" && len(tag) == 2 && digits(tag[1])) {
39 return fmt.Errorf("invalid: kind 445 has an unsupported tag")
40 }
41 }
42 if countTag(e, "expiration") > 1 {
43 return fmt.Errorf("invalid: kind 445 has repeated expiration")
44 }
45 return nil
46}
47
48func keyPackageShape(e event.Event) error {
49 if !singletonTag(e, "d", marmotHex64) || !singletonTag(e, "mls_protocol_version", marmotVersion) || !singletonTag(e, "i", marmotHex) {
50 return fmt.Errorf("invalid: kind 30443 has a malformed singleton tag")
51 }
52 for _, name := range []string{"mls_ciphersuite", "mls_extensions", "mls_proposals", "app_components"} {
53 if !idList(e, name) {
54 return fmt.Errorf("invalid: kind 30443 has a malformed id-list tag")
55 }
56 }
57 for _, tag := range e.Tags {
58 if len(tag) > 0 && tag[0] == "app_components" {
59 found := false
60 for _, id := range tag[1:] {
61 if id == "0x8009" {
62 found = true
63 }
64 }
65 if !found {
66 return fmt.Errorf("invalid: kind 30443 must advertise account identity proof")
67 }
68 }
69 }
70 decoded, err := base64.StdEncoding.Strict().DecodeString(e.Content)
71 if err != nil || len(decoded) == 0 {
72 return fmt.Errorf("invalid: kind 30443 content must be padded base64")
73 }
74 allowed := map[string]bool{"d": true, "mls_protocol_version": true, "i": true, "mls_ciphersuite": true, "mls_extensions": true, "mls_proposals": true, "app_components": true}
75 for _, tag := range e.Tags {
76 if len(tag) == 0 || !allowed[tag[0]] {
77 return fmt.Errorf("invalid: kind 30443 has an unsupported tag")
78 }
79 }
80 return nil
81}
82
83// Principal returns the authenticated account whose write policy applies to a group envelope.
84func (g *Gate) Principal(ctx context.Context, e event.Event, s relay.Session) (string, error) {
85 if e.Kind != event.KIND_MARMOT_GROUP {
86 return e.PubKey, nil
87 }
88 p := g.cfg.Policy()
89 if p.Writes == "open" {
90 if len(s.PubKeys) > 0 {
91 return s.PubKeys[0], nil
92 }
93 return e.PubKey, nil
94 }
95 for _, key := range s.PubKeys {
96 a := accessFor(ctx, g.cfg.Community, p, relay.Session{PubKeys: []string{key}})
97 if a.Owner || a.Member {
98 return key, nil
99 }
100 }
101 return "", fmt.Errorf("auth-required: Marmot group envelopes need an authenticated account allowed to write here")
102}
103
104func singletonTag(e event.Event, name string, valid *regexp.Regexp) bool {
105 found := 0
106 for _, tag := range e.Tags {
107 if len(tag) > 0 && tag[0] == name {
108 found++
109 if len(tag) != 2 || !valid.MatchString(tag[1]) {
110 return false
111 }
112 }
113 }
114 return found == 1
115}
116func idList(e event.Event, name string) bool {
117 found := 0
118 for _, tag := range e.Tags {
119 if len(tag) == 0 || tag[0] != name {
120 continue
121 }
122 found++
123 if len(tag) < 2 {
124 return false
125 }
126 seen := map[string]bool{}
127 for _, id := range tag[1:] {
128 if !marmotID.MatchString(id) || seen[id] {
129 return false
130 }
131 seen[id] = true
132 }
133 }
134 return found == 1
135}
136func countTag(e event.Event, name string) int {
137 n := 0
138 for _, tag := range e.Tags {
139 if tag[0] == name {
140 n++
141 }
142 }
143 return n
144}
145func digits(s string) bool {
146 if s == "" {
147 return false
148 }
149 for _, r := range s {
150 if r < '0' || r > '9' {
151 return false
152 }
153 }
154 return true
155}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs