on master help sign in
Fix 7 server.knot bugs: offline gossip peer loss, missing hex validation, clock skew intolerance, fromServer validation, senderServers overfitting, poll unauthorized ambiguity, and FederationAck message scope

Fixes: 1. broadcastOfflineGossip now captures peer servers before pruning to avoid missing servers whose entries were pruned 2. Add isValidHex helper and validation before bytesFromHex in VerifyAuth 3. Add clockSkewTolerance config with 5s tolerance for clock skew in gossip timestamp checks 4. Add fromServer length validation in RecvGossip (matching FederationAck pattern) 5. Fix senderServers to use effectiveEvents instead of validEvents to avoid excluding uninvolved servers 6. Poll now returns cursor -1 for unauthorized requests (signals re-authentication needed) 7. FederationAck message deletion now scoped to originServer == "" (locally-originated messages only)

commit0cabbfd1cce0895e74d61dafaa38b659bd838507 authoriko <mail@iko.soy> dateThu 26 Mar 2026, 18:03 (7 months ago) parent242926b
1 file changed+50−46
server.knot+50−46
server.knot+50−46
@@ -16,6 +16,7 @@ maxBlobLen = 65536
1616maxSignatureLen = 256
1717maxMessageIds = 100
1818maxGossipEvents = 100
19+clockSkewTolerance = 5000 -- 5 seconds tolerance for clock skew between federated servers
1920
2021-- ============================================
2122-- Types
@@ -94,6 +95,8 @@ genToken = do
9495
9596tooLong = \s maxLen -> not (drop maxLen s == "")
9697
98+isValidHex = \s -> not (s == "") && bytesToHex (bytesFromHex s) == s
99+
97100lookupSession = \sessions token t ->
98101 single (do
99102 s <- sessions
@@ -148,6 +151,10 @@ pruneExpired = \t -> do
148151 where s.expiresAt > t
149152 yield s
150153 presenceBefore <- *presence
154+ let peerServersBefore = fold (\acc p ->
155+ if p.server == serverHost then acc
156+ else if count (do a <- acc; where a == p.server; yield a) > 0 then acc
157+ else union acc [p.server]) [] presenceBefore
151158 let presenceAfter = do
152159 p <- presenceBefore
153160 where p.expiresAt > t
@@ -158,7 +165,7 @@ pruneExpired = \t -> do
158165 m <- messages
159166 where m.timestamp >= t - messageMaxAge
160167 yield m
161− yield (do
168+ yield {expiredLocal: (do
162169 p <- presenceBefore
163170 where p.expiresAt <= t
164171 where p.server == serverHost
@@ -167,20 +174,14 @@ pruneExpired = \t -> do
167174 where q.pubkey == p.pubkey
168175 where q.server == serverHost
169176 yield q) == 0
170− yield p)
177+ yield p), peerServers: peerServersBefore}
171178
172−broadcastOfflineGossip = \t expiredLocal -> do
179+broadcastOfflineGossip = \t peerServers expiredLocal -> do
173180 let expiredPubkeys = fold (\acc p ->
174181 if count (do a <- acc; where a == p.pubkey; yield a) > 0 then acc
175182 else union acc [p.pubkey]) [] expiredLocal
176− if not (federationSecret == "") && count expiredPubkeys > 0
183+ if not (federationSecret == "") && count expiredPubkeys > 0 && count peerServers > 0
177184 then do
178− peerServers <- atomic do
179− presence <- *presence
180− yield (fold (\acc p ->
181− if p.server == serverHost then acc
182− else if count (do a <- acc; where a == p.server; yield a) > 0 then acc
183− else union acc [p.server]) [] presence)
184185 let gossipEvents = do
185186 pk <- expiredPubkeys
186187 yield {eventType: "offline", pubkey: pk, server: serverHost, timestamp: t, ttl: 3600}
@@ -202,21 +203,21 @@ serve = \req -> case req of
202203 else do
203204 challenge <- genToken
204205 t <- now
205− expiredLocal <- atomic do
206− el <- pruneExpired t
206+ pruneResult <- atomic do
207+ pr <- pruneExpired t
207208 challenges <- *challenges
208209 set *challenges = union challenges
209210 [{pubkey: pubkey, challenge: challenge, expiresAt: t + 60000}]
210− yield el
211+ yield pr
211212 respond {challenge: challenge}
212− broadcastOfflineGossip t expiredLocal
213+ broadcastOfflineGossip t pruneResult.peerServers pruneResult.expiredLocal
213214
214215 VerifyAuth {pubkey, challenge, signature, respond} -> do
215216 t <- now
216217 token <- genToken
217218 let expiry = t + 3600000
218219 result <- atomic do
219− expiredLocal <- pruneExpired t
220+ pr <- pruneExpired t
220221 challenges <- *challenges
221222 sessions <- *sessions
222223 presence <- *presence
@@ -229,6 +230,8 @@ serve = \req -> case req of
229230 let sigValid = case matched of
230231 Nothing {} -> False {}
231232 _ -> if tooLong signature maxSignatureLen
233+ then False {}
234+ else if not (isValidHex pubkey) || not (isValidHex signature)
232235 then False {}
233236 else verify (bytesFromHex pubkey) (textToBytes challenge) (bytesFromHex signature)
234237 set *challenges = if sigValid
@@ -268,7 +271,7 @@ serve = \req -> case req of
268271 else if count (do a <- acc; where a == p.server; yield a) > 0 then acc
269272 else union acc [p.server]) [] updatedPresence
270273 else []
271− yield {sigValid: sigValid, peerServers: peerServers, expiredLocal: expiredLocal}
274+ yield {sigValid: sigValid, peerServers: peerServers, expiredLocal: pr.expiredLocal, offlinePeers: pr.peerServers}
272275 if result.sigValid
273276 then respond {token: token, expiresAt: expiry}
274277 else respond {token: "", expiresAt: 0}
@@ -278,13 +281,13 @@ serve = \req -> case req of
278281 {authorization: "Bearer " ++ federationSecret}
279282 {events: [{eventType: "online", pubkey: pubkey, server: serverHost, timestamp: t, ttl: 3600}], hopsRemaining: 2, fromServer: serverHost})
280283 else yield {}
281− broadcastOfflineGossip t result.expiredLocal
284+ broadcastOfflineGossip t result.offlinePeers result.expiredLocal
282285
283286 Poll {cursor, authorization, respond} -> do
284287 t <- now
285288 let token = extractToken authorization
286289 result <- atomic do
287− expiredLocal <- pruneExpired t
290+ pr <- pruneExpired t
288291 sessions <- *sessions
289292 messages <- *messages
290293 seqCounter <- *seqCounter
@@ -301,11 +304,11 @@ serve = \req -> case req of
301304 encryptedBlob: m.encryptedBlob,
302305 timestamp: m.timestamp}}
303306 let newCursor = case session of
304− Nothing {} -> 0
307+ Nothing {} -> -1
305308 _ -> if seqCounter > cursor then seqCounter else cursor
306− yield {events: evts, cursor: newCursor, expiredLocal: expiredLocal}
309+ yield {events: evts, cursor: newCursor, expiredLocal: pr.expiredLocal, peerServers: pr.peerServers}
307310 respond {events: result.events, cursor: result.cursor}
308− broadcastOfflineGossip t result.expiredLocal
311+ broadcastOfflineGossip t result.peerServers result.expiredLocal
309312
310313 SendMessage {to, encryptedBlob, timestamp, authorization, respond} -> do
311314 t <- now
@@ -313,7 +316,7 @@ serve = \req -> case req of
313316 let clampedTs = if timestamp > t then t else if timestamp < t - messageMaxAge then t else timestamp
314317 let validInput = not (to == "") && not (encryptedBlob == "") && not (tooLong to maxPubkeyLen) && not (tooLong encryptedBlob maxBlobLen)
315318 result <- atomic do
316− expiredLocal <- pruneExpired t
319+ pr <- pruneExpired t
317320 sessions <- *sessions
318321 messages <- *messages
319322 presence <- *presence
@@ -356,7 +359,7 @@ serve = \req -> case req of
356359 else if localOnline then "delivered"
357360 else if count remoteServers > 0 then "federated"
358361 else "queued"
359− yield {status: status, remoteServers: remoteServers, fromKey: fromKey, expiredLocal: expiredLocal}
362+ yield {status: status, remoteServers: remoteServers, fromKey: fromKey, expiredLocal: pr.expiredLocal, peerServers: pr.peerServers}
360363 finalStatus <- if result.status == "federated" && not (federationSecret == "")
361364 then do
362365 anyOk <- forwardToAll result.remoteServers "/federation/forward"
@@ -366,7 +369,7 @@ serve = \req -> case req of
366369 yield (if anyOk then "federated" else "queued")
367370 else yield result.status
368371 respond {status: finalStatus, messageId: (if result.status == "unauthorized" then "" else if result.status == "invalid" then "" else msgId)}
369− broadcastOfflineGossip t result.expiredLocal
372+ broadcastOfflineGossip t result.peerServers result.expiredLocal
370373
371374 AckMessages {messageIds, authorization, respond} -> do
372375 t <- now
@@ -376,7 +379,7 @@ serve = \req -> case req of
376379 else if count (do a <- acc; where a == mid; yield a) > 0 then acc
377380 else union acc [mid]) [] messageIds
378381 result <- atomic do
379− expiredLocal <- pruneExpired t
382+ pr <- pruneExpired t
380383 sessions <- *sessions
381384 messages <- *messages
382385 let session = lookupSession sessions (extractToken authorization) t
@@ -394,7 +397,7 @@ serve = \req -> case req of
394397 let ok = case session of
395398 Nothing {} -> False {}
396399 _ -> True {}
397− yield {ok: ok, ackedMessages: ackedMessages, expiredLocal: expiredLocal}
400+ yield {ok: ok, ackedMessages: ackedMessages, expiredLocal: pr.expiredLocal, peerServers: pr.peerServers}
398401 let fedMessages = do
399402 m <- result.ackedMessages
400403 where not (m.originServer == "")
@@ -411,12 +414,12 @@ serve = \req -> case req of
411414 originServer: server, fromServer: serverHost})
412415 else yield {}
413416 respond {ok: result.ok}
414− broadcastOfflineGossip t result.expiredLocal
417+ broadcastOfflineGossip t result.peerServers result.expiredLocal
415418
416419 LookupPresence {pubkey, authorization, respond} -> do
417420 t <- now
418421 result <- atomic do
419− expiredLocal <- pruneExpired t
422+ pr <- pruneExpired t
420423 sessions <- *sessions
421424 presence <- *presence
422425 let session = lookupSession sessions (extractToken authorization) t
@@ -439,15 +442,15 @@ serve = \req -> case req of
439442 yield p) of
440443 Just {value: p} -> p.server
441444 Nothing {} -> fold (\_ p -> p.server) "" valid
442− yield {foundServer: foundServer, authorized: authorized, expiredLocal: expiredLocal}
445+ yield {foundServer: foundServer, authorized: authorized, expiredLocal: pr.expiredLocal, peerServers: pr.peerServers}
443446 respond {online: not (result.foundServer == ""), server: result.foundServer, authorized: result.authorized}
444− broadcastOfflineGossip t result.expiredLocal
447+ broadcastOfflineGossip t result.peerServers result.expiredLocal
445448
446449 RecvGossip {events, hopsRemaining, fromServer, authorization, respond} -> do
447450 t <- now
448451 let authorized = verifyFederationAuth authorization
449452 let cappedHops = if hopsRemaining > 5 then 5 else hopsRemaining
450− let validRequest = authorized && cappedHops >= 1 && not (fromServer == "") && count events <= maxGossipEvents
453+ let validRequest = authorized && cappedHops >= 1 && not (fromServer == "") && not (tooLong fromServer maxPubkeyLen) && count events <= maxGossipEvents
451454 let onlineEntries = do
452455 e <- events
453456 where validRequest
@@ -457,7 +460,7 @@ serve = \req -> case req of
457460 where not (tooLong e.pubkey maxPubkeyLen)
458461 where not (tooLong e.server maxPubkeyLen)
459462 where not (e.server == serverHost)
460− where e.timestamp <= t
463+ where e.timestamp <= t + clockSkewTolerance
461464 where e.timestamp + (clampTtl e.ttl) * 1000 > t
462465 yield {pubkey: e.pubkey, server: e.server, expiresAt: e.timestamp + (clampTtl e.ttl) * 1000, timestamp: e.timestamp}
463466 let offlineEntries = do
@@ -469,7 +472,7 @@ serve = \req -> case req of
469472 where not (tooLong e.pubkey maxPubkeyLen)
470473 where not (tooLong e.server maxPubkeyLen)
471474 where not (e.server == serverHost)
472− where e.timestamp <= t
475+ where e.timestamp <= t + clockSkewTolerance
473476 where t - e.timestamp < (clampTtl e.ttl) * 1000
474477 yield {pubkey: e.pubkey, server: e.server, timestamp: e.timestamp}
475478 let filteredOnline = do
@@ -503,7 +506,7 @@ serve = \req -> case req of
503506 yield a)
504507 [e]) [] filteredOnline
505508 gossipResult <- atomic do
506− expiredLocal <- pruneExpired t
509+ pr <- pruneExpired t
507510 presence <- *presence
508511 let appliedOnline = do
509512 e <- deduped
@@ -561,7 +564,7 @@ serve = \req -> case req of
561564 set *presence = if validRequest
562565 then afterOffline
563566 else presence
564− yield {appliedKeys: (union appliedOnline appliedOffline), expiredLocal: expiredLocal}
567+ yield {appliedKeys: (union appliedOnline appliedOffline), expiredLocal: pr.expiredLocal, peerServers: pr.peerServers}
565568 let validEvents = do
566569 e <- events
567570 where validRequest
@@ -570,7 +573,7 @@ serve = \req -> case req of
570573 where not (tooLong e.pubkey maxPubkeyLen)
571574 where not (tooLong e.server maxPubkeyLen)
572575 where not (e.server == serverHost)
573− where e.timestamp <= t
576+ where e.timestamp <= t + clockSkewTolerance
574577 where e.timestamp + (clampTtl e.ttl) * 1000 > t
575578 where not (e.eventType == "online" && count (do
576579 o <- offlineEntries
@@ -593,7 +596,7 @@ serve = \req -> case req of
593596 else union acc [p.server]) [] presence)
594597 let senderServers = fold (\acc e ->
595598 if count (do a <- acc; where a == e.server; yield a) > 0 then acc
596− else union acc [e.server]) (if fromServer == "" then [] else [fromServer]) validEvents
599+ else union acc [e.server]) (if fromServer == "" then [] else [fromServer]) effectiveEvents
597600 let targets = do
598601 s <- peerServers
599602 where count (do ss <- senderServers; where ss == s; yield ss) == 0
@@ -604,7 +607,7 @@ serve = \req -> case req of
604607 {events: effectiveEvents, hopsRemaining: cappedHops - 1, fromServer: serverHost})
605608 else yield {}
606609 respond {ok: validRequest}
607− broadcastOfflineGossip t gossipResult.expiredLocal
610+ broadcastOfflineGossip t gossipResult.peerServers gossipResult.expiredLocal
608611
609612 ForwardMessage {toKey, fromKey, encryptedBlob, timestamp, originServer, messageId, authorization, respond} -> do
610613 t <- now
@@ -612,8 +615,8 @@ serve = \req -> case req of
612615 let validOrigin = not (originServer == "") && not (originServer == serverHost)
613616 let validFields = not (toKey == "") && not (fromKey == "") && not (encryptedBlob == "") && not (messageId == "") && not (tooLong toKey maxPubkeyLen) && not (tooLong fromKey maxPubkeyLen) && not (tooLong encryptedBlob maxBlobLen) && not (tooLong messageId maxPubkeyLen)
614617 let clampedTs = if timestamp > t then t else if timestamp < t - messageMaxAge then t else timestamp
615− expiredLocal <- atomic do
616− el <- pruneExpired t
618+ pruneResult <- atomic do
619+ pr <- pruneExpired t
617620 messages <- *messages
618621 seqCounter <- *seqCounter
619622 let seqTs = if t > seqCounter then t else seqCounter + 1
@@ -629,9 +632,9 @@ serve = \req -> case req of
629632 [{messageId: messageId, toKey: toKey, fromKey: fromKey,
630633 encryptedBlob: encryptedBlob, timestamp: clampedTs, originServer: originServer, receivedAt: seqTs}]
631634 else messages
632− yield el
635+ yield pr
633636 respond {ok: authorized && validOrigin && validFields}
634− broadcastOfflineGossip t expiredLocal
637+ broadcastOfflineGossip t pruneResult.peerServers pruneResult.expiredLocal
635638
636639 FederationAck {messageIds, originServer, fromServer, authorization, respond} -> do
637640 t <- now
@@ -642,8 +645,8 @@ serve = \req -> case req of
642645 else if count (do a <- acc; where a == mid; yield a) > 0 then acc
643646 else union acc [mid]) [] messageIds
644647 let validAck = authorized && originServer == serverHost && not (fromServer == "") && not (fromServer == serverHost) && not (tooLong fromServer maxPubkeyLen) && count messageIds <= maxMessageIds
645− expiredLocal <- atomic do
646− el <- pruneExpired t
648+ pruneResult <- atomic do
649+ pr <- pruneExpired t
647650 messages <- *messages
648651 presence <- *presence
649652 set *messages = if validAck
@@ -651,6 +654,7 @@ serve = \req -> case req of
651654 m <- messages
652655 mid <- dedupedIds
653656 where m.messageId == mid
657+ where m.originServer == ""
654658 where count (do
655659 p <- presence
656660 where p.pubkey == m.toKey
@@ -659,9 +663,9 @@ serve = \req -> case req of
659663 yield p) == 0
660664 yield m)
661665 else messages
662− yield el
666+ yield pr
663667 respond {ok: validAck}
664− broadcastOfflineGossip t expiredLocal
668+ broadcastOfflineGossip t pruneResult.peerServers pruneResult.expiredLocal
665669
666670-- ============================================
667671-- Entry Point