validation.go raw
1 package directory
2
3 import (
4 "github.com/minio/sha256-simd"
5 "encoding/hex"
6 "net/url"
7 "regexp"
8 "strings"
9 "time"
10
11 "next.orly.dev/pkg/lol/chk"
12 "next.orly.dev/pkg/lol/errorf"
13 "next.orly.dev/pkg/nostr/crypto/ec/schnorr"
14 "next.orly.dev/pkg/nostr/crypto/ec/secp256k1"
15 "next.orly.dev/pkg/nostr/encoders/bech32encoding"
16 "next.orly.dev/pkg/nostr/encoders/event"
17 )
18
19 // Validation constants
20 const (
21 MaxKeyDelegations = 512
22 KeyExpirationDays = 30
23 MinNonceSize = 16 // bytes
24 MaxContentLength = 65536 // bytes
25 )
26
27 // Regular expressions for validation
28 var (
29 hexKeyRegex = regexp.MustCompile(`^[0-9a-fA-F]{64}$`)
30 npubRegex = regexp.MustCompile(`^npub1[0-9a-z]+$`)
31 wsURLRegex = regexp.MustCompile(`^wss?://[a-zA-Z0-9.-]+(?::[0-9]+)?(?:/.*)?$`)
32 groupTagNameRegex = regexp.MustCompile(`^[a-zA-Z0-9._~-]+$`) // RFC 3986 URL-safe characters
33 )
34
35 // ValidateGroupTagName validates that a group tag name is URL-safe (RFC 3986).
36 func ValidateGroupTagName(name string) (err error) {
37 if len(name) < 1 {
38 return errorf.E("group tag name cannot be empty")
39 }
40 if len(name) > 255 {
41 return errorf.E("group tag name cannot exceed 255 characters")
42 }
43
44 // Check for reserved prefixes
45 if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
46 return errorf.E("group tag names starting with '.' or '_' are reserved for system use")
47 }
48
49 // Validate URL-safe character set
50 if !groupTagNameRegex.MatchString(name) {
51 return errorf.E("group tag name must contain only URL-safe characters (a-z, A-Z, 0-9, -, ., _, ~)")
52 }
53
54 return nil
55 }
56
57 // ValidateHexKey validates that a string is a valid 64-character hex key.
58 func ValidateHexKey(key string) (err error) {
59 if !hexKeyRegex.MatchString(key) {
60 return errorf.E("invalid hex key format: must be 64 hex characters")
61 }
62 return nil
63 }
64
65 // ValidateNPub validates that a string is a valid npub-encoded public key.
66 func ValidateNPub(npub string) (err error) {
67 if !npubRegex.MatchString(npub) {
68 return errorf.E("invalid npub format")
69 }
70
71 // Try to decode to verify it's valid
72 if _, err = bech32encoding.NpubToBytes(npub); chk.E(err) {
73 return errorf.E("invalid npub encoding: %w", err)
74 }
75
76 return nil
77 }
78
79 // ValidateWebSocketURL validates that a string is a valid WebSocket URL.
80 func ValidateWebSocketURL(wsURL string) (err error) {
81 if !wsURLRegex.MatchString(wsURL) {
82 return errorf.E("invalid WebSocket URL format")
83 }
84
85 // Parse URL for additional validation
86 var u *url.URL
87 if u, err = url.Parse(wsURL); chk.E(err) {
88 return errorf.E("invalid URL: %w", err)
89 }
90
91 if u.Scheme != "ws" && u.Scheme != "wss" {
92 return errorf.E("URL must use ws:// or wss:// scheme")
93 }
94
95 if u.Host == "" {
96 return errorf.E("URL must have a host")
97 }
98
99 return nil
100 }
101
102 // ValidateNonce validates that a nonce meets minimum security requirements.
103 func ValidateNonce(nonce string) (err error) {
104 if len(nonce) < MinNonceSize*2 { // hex-encoded, so double the byte length
105 return errorf.E("nonce must be at least %d bytes (%d hex characters)",
106 MinNonceSize, MinNonceSize*2)
107 }
108
109 // Verify it's valid hex
110 if _, err = hex.DecodeString(nonce); chk.E(err) {
111 return errorf.E("nonce must be valid hex: %w", err)
112 }
113
114 return nil
115 }
116
117 // ValidateSignature validates that a signature is properly formatted.
118 func ValidateSignature(sig string) (err error) {
119 if len(sig) != 128 { // 64 bytes hex-encoded
120 return errorf.E("signature must be 64 bytes (128 hex characters)")
121 }
122
123 // Verify it's valid hex
124 if _, err = hex.DecodeString(sig); chk.E(err) {
125 return errorf.E("signature must be valid hex: %w", err)
126 }
127
128 return nil
129 }
130
131 // ValidateDerivationPath validates a BIP32 derivation path for this protocol.
132 func ValidateDerivationPath(path string) (err error) {
133 // Expected format: m/39103'/1237'/identity'/usage/index
134 if !strings.HasPrefix(path, "m/39103'/1237'/") {
135 return errorf.E("derivation path must start with m/39103'/1237'/")
136 }
137
138 parts := strings.Split(path, "/")
139 if len(parts) != 6 {
140 return errorf.E("derivation path must have 6 components")
141 }
142
143 // Validate hardened components
144 if parts[1] != "39103'" || parts[2] != "1237'" {
145 return errorf.E("invalid hardened components in derivation path")
146 }
147
148 // Identity component should be hardened (end with ')
149 if !strings.HasSuffix(parts[3], "'") {
150 return errorf.E("identity component must be hardened")
151 }
152
153 return nil
154 }
155
156 // ValidateEventContent validates that event content is within size limits.
157 func ValidateEventContent(content []byte) (err error) {
158 if len(content) > MaxContentLength {
159 return errorf.E("content exceeds maximum length of %d bytes", MaxContentLength)
160 }
161 return nil
162 }
163
164 // ValidateTimestamp validates that a timestamp is reasonable (not too far in past/future).
165 func ValidateTimestamp(ts int64) (err error) {
166 now := time.Now().Unix()
167
168 // Allow up to 1 hour in the future
169 if ts > now+3600 {
170 return errorf.E("timestamp too far in the future")
171 }
172
173 // Allow up to 1 year in the past
174 if ts < now-31536000 {
175 return errorf.E("timestamp too far in the past")
176 }
177
178 return nil
179 }
180
181 // VerifyIdentityTagSignature verifies the signature in an identity tag.
182 func VerifyIdentityTagSignature(
183 identityTag *IdentityTag,
184 delegatePubkey []byte,
185 ) (valid bool, err error) {
186 if identityTag == nil {
187 return false, errorf.E("identity tag cannot be nil")
188 }
189
190 // Decode npub to get identity public key
191 var identityPubkey []byte
192 if identityPubkey, err = bech32encoding.NpubToBytes(identityTag.NPubIdentity); chk.E(err) {
193 return false, errorf.E("failed to decode npub: %w", err)
194 }
195
196 // Decode nonce and signature
197 var nonce, signature []byte
198 if nonce, err = hex.DecodeString(identityTag.Nonce); chk.E(err) {
199 return false, errorf.E("invalid nonce hex: %w", err)
200 }
201 if signature, err = hex.DecodeString(identityTag.Signature); chk.E(err) {
202 return false, errorf.E("invalid signature hex: %w", err)
203 }
204
205 // Create message to verify: nonce + delegate_pubkey_hex + identity_pubkey_hex
206 message := make([]byte, 0, len(nonce)+64+64)
207 message = append(message, nonce...)
208 message = append(message, []byte(hex.EncodeToString(delegatePubkey))...)
209 message = append(message, []byte(hex.EncodeToString(identityPubkey))...)
210
211 // Hash the message
212 hash := sha256.Sum256(message)
213
214 // Parse signature and verify
215 var sig *schnorr.Signature
216 if sig, err = schnorr.ParseSignature(signature); chk.E(err) {
217 return false, errorf.E("failed to parse signature: %w", err)
218 }
219
220 // Parse public key
221 var pubKey *secp256k1.PublicKey
222 if pubKey, err = schnorr.ParsePubKey(identityPubkey); chk.E(err) {
223 return false, errorf.E("failed to parse public key: %w", err)
224 }
225
226 return sig.Verify(hash[:], pubKey), nil
227 }
228
229 // ValidateEventKindForReplication validates that an event kind is appropriate
230 // for replication in the directory consensus protocol.
231 func ValidateEventKindForReplication(kind uint16) (err error) {
232 // Directory events are always valid
233 if IsDirectoryEventKind(kind) {
234 return nil
235 }
236
237 // Protocol events (39100-39105) should not be replicated as regular events
238 if kind >= 39100 && kind <= 39105 {
239 return errorf.E("protocol events should not be replicated as directory events")
240 }
241
242 // Ephemeral events (20000-29999) should not be stored
243 if kind >= 20000 && kind <= 29999 {
244 return errorf.E("ephemeral events should not be replicated")
245 }
246
247 return nil
248 }
249
250 // ValidateRelayIdentityBinding verifies that a relay identity announcement
251 // is properly bound to its network address through NIP-11 signature verification.
252 func ValidateRelayIdentityBinding(
253 announcement *RelayIdentityAnnouncement,
254 nip11Pubkey, nip11Nonce, nip11Sig, relayAddress string,
255 ) (valid bool, err error) {
256 if announcement == nil {
257 return false, errorf.E("announcement cannot be nil")
258 }
259
260 // Verify the announcement event pubkey matches the NIP-11 pubkey
261 announcementPubkeyHex := hex.EncodeToString(announcement.Event.Pubkey)
262 if announcementPubkeyHex != nip11Pubkey {
263 return false, errorf.E("announcement pubkey does not match NIP-11 pubkey")
264 }
265
266 // Verify NIP-11 signature format
267 if err = ValidateHexKey(nip11Pubkey); chk.E(err) {
268 return false, errorf.E("invalid NIP-11 pubkey: %w", err)
269 }
270 if err = ValidateNonce(nip11Nonce); chk.E(err) {
271 return false, errorf.E("invalid NIP-11 nonce: %w", err)
272 }
273 if err = ValidateSignature(nip11Sig); chk.E(err) {
274 return false, errorf.E("invalid NIP-11 signature: %w", err)
275 }
276
277 // Decode components
278 var pubkey, signature []byte
279 if pubkey, err = hex.DecodeString(nip11Pubkey); chk.E(err) {
280 return false, errorf.E("failed to decode NIP-11 pubkey: %w", err)
281 }
282 if signature, err = hex.DecodeString(nip11Sig); chk.E(err) {
283 return false, errorf.E("failed to decode NIP-11 signature: %w", err)
284 }
285
286 // Create message: pubkey + nonce + relay_address
287 message := nip11Pubkey + nip11Nonce + relayAddress
288 hash := sha256.Sum256([]byte(message))
289
290 // Parse signature and verify
291 var sig *schnorr.Signature
292 if sig, err = schnorr.ParseSignature(signature); chk.E(err) {
293 return false, errorf.E("failed to parse signature: %w", err)
294 }
295
296 // Parse public key
297 var pubKey *secp256k1.PublicKey
298 if pubKey, err = schnorr.ParsePubKey(pubkey); chk.E(err) {
299 return false, errorf.E("failed to parse public key: %w", err)
300 }
301
302 return sig.Verify(hash[:], pubKey), nil
303 }
304
305 // ValidateConsortiumEvent performs comprehensive validation of any consortium
306 // protocol event, including signature verification and protocol-specific checks.
307 func ValidateConsortiumEvent(ev *event.E) (err error) {
308 if ev == nil {
309 return errorf.E("event cannot be nil")
310 }
311
312 // Verify basic event signature
313 if _, err = ev.Verify(); chk.E(err) {
314 return errorf.E("invalid event signature: %w", err)
315 }
316
317 // Validate timestamp
318 if err = ValidateTimestamp(ev.CreatedAt); chk.E(err) {
319 return errorf.E("invalid timestamp: %w", err)
320 }
321
322 // Validate content size
323 if err = ValidateEventContent(ev.Content); chk.E(err) {
324 return errorf.E("invalid content: %w", err)
325 }
326
327 // Protocol-specific validation based on event kind
328 switch ev.Kind {
329 case RelayIdentityAnnouncementKind.K:
330 var ria *RelayIdentityAnnouncement
331 if ria, err = ParseRelayIdentityAnnouncement(ev); chk.E(err) {
332 return errorf.E("failed to parse relay identity announcement: %w", err)
333 }
334 return ria.Validate()
335
336 case TrustActKind.K:
337 var ta *TrustAct
338 if ta, err = ParseTrustAct(ev); chk.E(err) {
339 return errorf.E("failed to parse trust act: %w", err)
340 }
341 return ta.Validate()
342
343 case GroupTagActKind.K:
344 var gta *GroupTagAct
345 if gta, err = ParseGroupTagAct(ev); chk.E(err) {
346 return errorf.E("failed to parse group tag act: %w", err)
347 }
348 return gta.Validate()
349
350 case PublicKeyAdvertisementKind.K:
351 var pka *PublicKeyAdvertisement
352 if pka, err = ParsePublicKeyAdvertisement(ev); chk.E(err) {
353 return errorf.E("failed to parse public key advertisement: %w", err)
354 }
355 return pka.Validate()
356
357 case DirectoryEventReplicationRequestKind.K:
358 var derr *DirectoryEventReplicationRequest
359 if derr, err = ParseDirectoryEventReplicationRequest(ev); chk.E(err) {
360 return errorf.E("failed to parse replication request: %w", err)
361 }
362 return derr.Validate()
363
364 case DirectoryEventReplicationResponseKind.K:
365 var derr *DirectoryEventReplicationResponse
366 if derr, err = ParseDirectoryEventReplicationResponse(ev); chk.E(err) {
367 return errorf.E("failed to parse replication response: %w", err)
368 }
369 return derr.Validate()
370
371 default:
372 return errorf.E("unknown consortium event kind: %d", ev.Kind)
373 }
374 }
375
376 // IsConsortiumEvent returns true if the event is a consortium protocol event.
377 func IsConsortiumEvent(ev *event.E) bool {
378 if ev == nil {
379 return false
380 }
381 return ev.Kind >= 39100 && ev.Kind <= 39105
382 }
383