on master help sign in
Remove message ID from federation protocol
e870868iko
server.knot+18−47
@@ -77,9 +77,6 @@ maxForwardsPerServer = 2000
77-- Max accepted length of a server hostname.
77-- Max accepted length of a server hostname.
78maxServerNameLen : Int
78maxServerNameLen : Int
79maxServerNameLen = 128
79maxServerNameLen = 128
80−-- Max accepted length of a hex-encoded message ID.
81−maxMessageIdLen : Int
82−maxMessageIdLen = 128
83-- Ceiling on outstanding auth challenges held in memory.
80-- Ceiling on outstanding auth challenges held in memory.
84maxChallenges : Int
81maxChallenges : Int
85maxChallenges = 10000
82maxChallenges = 10000
@@ -204,8 +201,7 @@ requireClientIp = True {}
204-- applied at federation egress, not in the type predicate.
201-- applied at federation egress, not in the type predicate.
205
202
206-- One event in a /poll response: the opaque cipher blob addressed to the
203-- One event in a /poll response: the opaque cipher blob addressed to the
207−-- polling pubkey. The server cannot read the blob; clients derive any
204+-- polling pubkey. The server cannot read the blob.
208−-- per-message id locally as `messageIdOf encryptedBlob`.
209type PollEvent = {encryptedBlob: BlobHex}
205type PollEvent = {encryptedBlob: BlobHex}
210
206
211-- Kind of presence event flowing through federation gossip. `GossipUnknown` is
207-- Kind of presence event flowing through federation gossip. `GossipUnknown` is
@@ -244,10 +240,6 @@ type PubkeyHex = Text where isValidExactHex pubkeyLen
244type BlobHex = Text where isValidBoundedHex maxBlobLen
240type BlobHex = Text where isValidBoundedHex maxBlobLen
245-- Hex-encoded Ed25519 signature (64 bytes → 128 chars exact).
241-- Hex-encoded Ed25519 signature (64 bytes → 128 chars exact).
246type SignatureHex = Text where isValidExactHex signatureLen
242type SignatureHex = Text where isValidExactHex signatureLen
247−isValidMessageId : Text -> Bool
248−isValidMessageId = isValidBoundedHex maxMessageIdLen
249−-- Hex-encoded message id, length ≤ `maxMessageIdLen`.
250−type MessageIdHex = Text where isValidMessageId
251-- Normalized hostname (lowercased, no trailing dot, length-bounded). The SSRF
243-- Normalized hostname (lowercased, no trailing dot, length-bounded). The SSRF
252-- deny-list (`isBadServerName`) is enforced separately at federation egress
244-- deny-list (`isBadServerName`) is enforced separately at federation egress
253-- (`withFederationAuth`, `handlePingPeer`); keeping it out of the type predicate
245-- (`withFederationAuth`, `handlePingPeer`); keeping it out of the type predicate
@@ -265,11 +257,9 @@ type Challenge = {pubkey: PubkeyHex, challenge: Text, expiresAt: Timestamp, ip:
265-- An issued bearer token bound to (pubkey, ip, expiry).
257-- An issued bearer token bound to (pubkey, ip, expiry).
266type Session = {token: Text, pubkey: PubkeyHex, expiresAt: Timestamp, ip: Text}
258type Session = {token: Text, pubkey: PubkeyHex, expiresAt: Timestamp, ip: Text}
267-- Mailbox row: a stored message awaiting recipient pickup or federation forward.
259-- Mailbox row: a stored message awaiting recipient pickup or federation forward.
268−-- Identity is `messageIdOf encryptedBlob` (BLAKE3 of the ciphertext); we don't
269−-- persist it because it's deterministically derivable. Storage TTL is fixed
270−-- at `messageMaxAge`, so expiry is derived from `receivedAt`. Any send time
260+-- Storage TTL is fixed at `messageMaxAge`, so expiry is derived from
261+-- `receivedAt`. Any send time the sender wants to convey lives inside
271−-- the sender wants to convey lives inside `encryptedBlob` (the server cannot
262+-- `encryptedBlob` (the server cannot read it).
272−-- read it).
273type Message = {toKey: PubkeyHex, encryptedBlob: BlobHex, receivedAt: Timestamp}
263type Message = {toKey: PubkeyHex, encryptedBlob: BlobHex, receivedAt: Timestamp}
274-- Last-known location of a pubkey, either local (`server == serverHostN`) or
264-- Last-known location of a pubkey, either local (`server == serverHostN`) or
275-- learned from a peer via gossip. Multiple entries per pubkey are allowed.
265-- learned from a peer via gossip. Multiple entries per pubkey are allowed.
@@ -391,14 +381,14 @@ route AuthApi where
391
381
392route ClientApi where
382route ClientApi where
393 POST {cursor: Timestamp, generation: Int} /poll headers {authorization: Text, clientIp: Text} -> {events: [PollEvent], cursor: Timestamp, authorized: Bool, rateLimited: Bool, generation: Int, heartbeat: Bool, retryAfterMs: Int, cursorReset: Bool, hasMore: Bool, cursorStale: Bool, loadPct: Int} = Poll
383 POST {cursor: Timestamp, generation: Int} /poll headers {authorization: Text, clientIp: Text} -> {events: [PollEvent], cursor: Timestamp, authorized: Bool, rateLimited: Bool, generation: Int, heartbeat: Bool, retryAfterMs: Int, cursorReset: Bool, hasMore: Bool, cursorStale: Bool, loadPct: Int} = Poll
394− POST {messages: [{to: PubkeyHex, encryptedBlob: BlobHex}]} /messages headers {authorization: Text, clientIp: Text} -> {results: [{messageId: MessageIdHex, error: Text}], error: Text, retryAfterMs: Int} = SendMessages
384+ POST {messages: [{to: PubkeyHex, encryptedBlob: BlobHex}]} /messages headers {authorization: Text, clientIp: Text} -> {results: [{error: Text}], error: Text, retryAfterMs: Int} = SendMessages
395 GET /lookup/{pubkey: PubkeyHex} headers {authorization: Text, clientIp: Text} -> {server: ServerName, authorized: Bool, error: Text} = LookupPresence
385 GET /lookup/{pubkey: PubkeyHex} headers {authorization: Text, clientIp: Text} -> {server: ServerName, authorized: Bool, error: Text} = LookupPresence
396 POST {pubkeys: [PubkeyHex]} /lookup/batch headers {authorization: Text, clientIp: Text} -> {results: [{pubkey: PubkeyHex, server: ServerName}], authorized: Bool, error: Text} = BatchLookupPresence
386 POST {pubkeys: [PubkeyHex]} /lookup/batch headers {authorization: Text, clientIp: Text} -> {results: [{pubkey: PubkeyHex, server: ServerName}], authorized: Bool, error: Text} = BatchLookupPresence
397
387
398route FederationApi where
388route FederationApi where
399 POST {events: [GossipEvent], fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/gossip headers {authorization: Text} -> {ok: Bool, error: Text, retryAfterMs: Int} = RecvGossip
389 POST {events: [GossipEvent], fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/gossip headers {authorization: Text} -> {ok: Bool, error: Text, retryAfterMs: Int} = RecvGossip
400− POST {toKey: PubkeyHex, encryptedBlob: BlobHex, messageId: MessageIdHex, fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/forward headers {authorization: Text} -> {ok: Bool, error: Text, retryAfterMs: Int} = ForwardMessage
390+ POST {toKey: PubkeyHex, encryptedBlob: BlobHex, fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/forward headers {authorization: Text} -> {ok: Bool, error: Text, retryAfterMs: Int} = ForwardMessage
401− POST {forwards: [{toKey: PubkeyHex, encryptedBlob: BlobHex, messageId: MessageIdHex}], fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/forward-batch headers {authorization: Text} -> {ok: Bool, error: Text, results: [{messageId: MessageIdHex, ok: Bool}], retryAfterMs: Int} = FederationBatchForward
391+ POST {forwards: [{toKey: PubkeyHex, encryptedBlob: BlobHex}], fromServer: ServerName, requestTimestamp: Timestamp, correlationId: Text} /federation/forward-batch headers {authorization: Text} -> {ok: Bool, error: Text, results: [{ok: Bool}], retryAfterMs: Int} = FederationBatchForward
402
392
403route FederationPubkeyApi where
393route FederationPubkeyApi where
404 GET /federation/pubkey -> {pubkey: Text, ok: Bool} = FederationPubkey
394 GET /federation/pubkey -> {pubkey: Text, ok: Bool} = FederationPubkey
@@ -585,13 +575,6 @@ else do
585genToken : IO {random} Text
575genToken : IO {random} Text
586genToken = genTokenN 48
576genToken = genTokenN 48
587
577
588−-- Canonical message identity: BLAKE3 of the encrypted blob, hex-encoded.
589−-- Binding the id to the payload means a hostile peer cannot multiply-store
590−-- the same ciphertext under different ids on /federation/forward — same
591−-- blob always collides with the existing dedup key.
592−messageIdOf : BlobHex -> MessageIdHex
593−messageIdOf = \blob -> bytesToHex (hash blob)
594−
595-- True when `s` exceeds `maxLen` characters.
578-- True when `s` exceeds `maxLen` characters.
596tooLong : Text -> Int -> Bool
579tooLong : Text -> Int -> Bool
597tooLong = \s maxLen -> count (chars s) > maxLen
580tooLong = \s maxLen -> count (chars s) > maxLen
@@ -1020,12 +1003,10 @@ retryPendingForwards = do
1020 blocked <- *blockedPeers
1003 blocked <- *blockedPeers
1021 forEach (filter (collectPeers fwds (\f -> f.toServer)) (\s -> shouldRetryServer s t failures && not elem s blocked)) (\server -> forEach (takeRelation maxBatchSize (filter fwds (\f -> f.toServer == server))) (\f -> do
1004 forEach (filter (collectPeers fwds (\f -> f.toServer)) (\s -> shouldRetryServer s t failures && not elem s blocked)) (\server -> forEach (takeRelation maxBatchSize (filter fwds (\f -> f.toServer == server))) (\f -> do
1022 t2 <- now
1005 t2 <- now
1023− let messageId = messageIdOf f.encryptedBlob
1024− auth <- generateFederationAuth ("forward:" ++ messageId) t2
1006+ auth <- generateFederationAuth "forward" t2
1025 result <- fetch (federationUrl server) (ForwardMessage {
1007 result <- fetch (federationUrl server) (ForwardMessage {
1026 f.toKey,
1008 f.toKey,
1027 f.encryptedBlob,
1009 f.encryptedBlob,
1028− messageId,
1029 fromServer: serverHostN,
1010 fromServer: serverHostN,
1030 requestTimestamp: t2,
1011 requestTimestamp: t2,
1031 authorization: auth,
1012 authorization: auth,
@@ -1314,16 +1295,15 @@ ackPolledMessages = \pubkey cursor -> do
1314-- the rest; federated forwards are forked per entry. Top-level `error`
1295-- the rest; federated forwards are forked per entry. Top-level `error`
1315-- carries global gate failures ("rate_limited" | "unauthorized") and
1296-- carries global gate failures ("rate_limited" | "unauthorized") and
1316-- short-circuits with empty `results`.
1297-- short-circuits with empty `results`.
1317−handleSendMessages : [{encryptedBlob: BlobHex, to: PubkeyHex | a}] -> Text -> Text -> IO {rw *globalRateCount, rw *globalRateWindowStart, r *sessions, clock} {error: Text, results: [{error: Text, messageId: MessageIdHex}], retryAfterMs: Int}
1298+handleSendMessages : [{encryptedBlob: BlobHex, to: PubkeyHex | a}] -> Text -> Text -> IO {rw *globalRateCount, rw *globalRateWindowStart, r *sessions, clock} {error: Text, results: [{error: Text}], retryAfterMs: Int}
1318handleSendMessages = \messages authorization clientIp -> do
1299handleSendMessages = \messages authorization clientIp -> do
1319 t <- now
1300 t <- now
1320 let mkErr = \e -> {results: [], error: e, retryAfterMs: 0}
1301 let mkErr = \e -> {results: [], error: e, retryAfterMs: 0}
1321 gateAuth authorization clientIp t mkErr (\senderKey -> do
1302 gateAuth authorization clientIp t mkErr (\senderKey -> do
1322 results <- mapEach (takeRelation maxBatchSize messages) (\m -> do
1303 results <- mapEach (takeRelation maxBatchSize messages) (\m -> do
1323− let effMsgId = messageIdOf m.encryptedBlob
1324 r <- atomic (do sendMessageCore senderKey m.to m.encryptedBlob t)
1304 r <- atomic (do sendMessageCore senderKey m.to m.encryptedBlob t)
1325 when federationEnabled (fork (forkForwardsTo m.to m.encryptedBlob r.remoteServers))
1305 when federationEnabled (fork (forkForwardsTo m.to m.encryptedBlob r.remoteServers))
1326− yield {messageId: effMsgId, r.error})
1306+ yield {r.error})
1327 yield {results, error: "", retryAfterMs: 0})
1307 yield {results, error: "", retryAfterMs: 0})
1328
1308
1329-- Atomic core of /messages. Charges send/pair/recv rate
1309-- Atomic core of /messages. Charges send/pair/recv rate
@@ -1384,18 +1364,16 @@ sendMessageCore = \senderKey to encryptedBlob t -> do
1384-- stays so `retryPendingForwards` can re-attempt with backoff.
1364-- stays so `retryPendingForwards` can re-attempt with backoff.
1385forkForwardsTo : PubkeyHex -> BlobHex -> [Text] -> IO {} {}
1365forkForwardsTo : PubkeyHex -> BlobHex -> [Text] -> IO {} {}
1386forkForwardsTo = \to encryptedBlob servers -> forEach servers (\server -> do
1366forkForwardsTo = \to encryptedBlob servers -> forEach servers (\server -> do
1387− let effMsgId = messageIdOf encryptedBlob
1388 fwds <- *forwards
1367 fwds <- *forwards
1389 let same = \x -> x.encryptedBlob == encryptedBlob && x.toServer == server
1368 let same = \x -> x.encryptedBlob == encryptedBlob && x.toServer == server
1390 case findFirst fwds same of
1369 case findFirst fwds same of
1391 Nothing {} -> yield {}
1370 Nothing {} -> yield {}
1392 Just {value: f} -> do
1371 Just {value: f} -> do
1393 t <- now
1372 t <- now
1394− auth <- generateFederationAuth ("forward:" ++ effMsgId) t
1373+ auth <- generateFederationAuth "forward" t
1395 result <- fetch (federationUrl server) (ForwardMessage {
1374 result <- fetch (federationUrl server) (ForwardMessage {
1396 toKey: to,
1375 toKey: to,
1397 encryptedBlob,
1376 encryptedBlob,
1398− messageId: effMsgId,
1399 fromServer: serverHostN,
1377 fromServer: serverHostN,
1400 requestTimestamp: t,
1378 requestTimestamp: t,
1401 authorization: auth,
1379 authorization: auth,
@@ -1527,31 +1505,24 @@ storeFederated = \toKey encryptedBlob checkRecvRate t -> do
1527
1505
1528-- POST /federation/forward: receive a single federated message destined for
1506-- POST /federation/forward: receive a single federated message destined for
1529-- a local key. Delegates the actual store decision to `storeFederated`,
1507-- a local key. Delegates the actual store decision to `storeFederated`,
1530−-- which returns "" on success. The peer-supplied messageId in the request
1508+-- which returns "" on success.
1531−-- body is ignored: we derive the id from the blob ourselves so a hostile
1532−-- peer cannot influence the auth scope by varying the id. Honest senders
1533−-- derive the same value, so the "forward:" ++ messageId auth scope still
1534−-- verifies.
1535−handleForwardMessage : PubkeyHex -> BlobHex -> Text -> Text -> Int -> Text -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, rw *messages, rw *pollWakeup, r *presence, rw *recvRates, rw *seqCounter, clock, network} {error: Text, ok: Bool, retryAfterMs: Int}
1509+handleForwardMessage : PubkeyHex -> BlobHex -> Text -> Int -> Text -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, rw *messages, rw *pollWakeup, r *presence, rw *recvRates, rw *seqCounter, clock, network} {error: Text, ok: Bool, retryAfterMs: Int}
1536−handleForwardMessage = \toKey encryptedBlob _ fromServer requestTimestamp authorization -> do
1510+handleForwardMessage = \toKey encryptedBlob fromServer requestTimestamp authorization -> do
1537− let messageId = messageIdOf encryptedBlob
1538 t <- now
1511 t <- now
1539− withFederationAuth authorization t (\e r -> {ok: False {}, error: e, retryAfterMs: r}) fromServer ("forward:" ++ messageId) requestTimestamp (isRemote fromServer) (do
1512+ withFederationAuth authorization t (\e r -> {ok: False {}, error: e, retryAfterMs: r}) fromServer "forward" requestTimestamp (isRemote fromServer) (do
1540 result <- atomic storeFederated toKey encryptedBlob (True {}) t
1513 result <- atomic storeFederated toKey encryptedBlob (True {}) t
1541 yield {ok: result == "", error: result, retryAfterMs: 0})
1514 yield {ok: result == "", error: result, retryAfterMs: 0})
1542
1515
1543-- POST /federation/forward-batch: bulk variant of `handleForwardMessage`.
1516-- POST /federation/forward-batch: bulk variant of `handleForwardMessage`.
1544-- Skips per-entry recv-rate charging (the per-server federation rate limit
1517-- Skips per-entry recv-rate charging (the per-server federation rate limit
1545-- already gates the call as a whole).
1518-- already gates the call as a whole).
1546−handleFederationBatchForward : [{encryptedBlob: BlobHex, messageId: MessageIdHex, toKey: PubkeyHex | a}] -> Text -> Int -> Text -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, clock, network} {error: Text, ok: Bool, results: [{messageId: MessageIdHex, ok: Bool}], retryAfterMs: Int}
1519+handleFederationBatchForward : [{encryptedBlob: BlobHex, toKey: PubkeyHex | a}] -> Text -> Int -> Text -> IO {r *allowedPeers, r *blockedPeers, rw *federationRates, clock, network} {error: Text, ok: Bool, results: [{ok: Bool}], retryAfterMs: Int}
1547handleFederationBatchForward = \forwards fromServer requestTimestamp authorization -> do
1520handleFederationBatchForward = \forwards fromServer requestTimestamp authorization -> do
1548 t <- now
1521 t <- now
1549 withFederationAuth authorization t (\e r -> {ok: False {}, error: e, results: [], retryAfterMs: r}) fromServer "forward-batch" requestTimestamp (isRemote fromServer && count forwards <= maxBatchSize) (do
1522 withFederationAuth authorization t (\e r -> {ok: False {}, error: e, results: [], retryAfterMs: r}) fromServer "forward-batch" requestTimestamp (isRemote fromServer && count forwards <= maxBatchSize) (do
1550 results <- mapEach (takeRelation maxBatchSize forwards) (\e -> do
1523 results <- mapEach (takeRelation maxBatchSize forwards) (\e -> do
1551− -- Recompute id from blob; ignore e.messageId (untrusted, peer-chosen).
1552− let messageId = messageIdOf e.encryptedBlob
1553 result <- atomic storeFederated e.toKey e.encryptedBlob (False {}) t
1524 result <- atomic storeFederated e.toKey e.encryptedBlob (False {}) t
1554− yield {messageId, ok: result == ""})
1525+ yield {ok: result == ""})
1555 yield {ok: True {}, error: "", results, retryAfterMs: 0})
1526 yield {ok: True {}, error: "", results, retryAfterMs: 0})
1556
1527
1557-- ============================================
1528-- ============================================
@@ -2163,7 +2134,7 @@ api = serve Api where
2163 LookupPresence = \{pubkey, authorization, clientIp} -> handleLookupPresence pubkey authorization clientIp
2134 LookupPresence = \{pubkey, authorization, clientIp} -> handleLookupPresence pubkey authorization clientIp
2164 BatchLookupPresence = \{pubkeys, authorization, clientIp} -> handleBatchLookupPresence pubkeys authorization clientIp
2135 BatchLookupPresence = \{pubkeys, authorization, clientIp} -> handleBatchLookupPresence pubkeys authorization clientIp
2165 RecvGossip = \{events, fromServer, requestTimestamp, authorization} -> handleRecvGossip events fromServer requestTimestamp authorization
2136 RecvGossip = \{events, fromServer, requestTimestamp, authorization} -> handleRecvGossip events fromServer requestTimestamp authorization
2166− ForwardMessage = \{toKey, encryptedBlob, messageId, fromServer, requestTimestamp, authorization} -> handleForwardMessage toKey encryptedBlob messageId fromServer requestTimestamp authorization
2137+ ForwardMessage = \{toKey, encryptedBlob, fromServer, requestTimestamp, authorization} -> handleForwardMessage toKey encryptedBlob fromServer requestTimestamp authorization
2167 FederationBatchForward = \{forwards, fromServer, requestTimestamp, authorization} -> handleFederationBatchForward forwards fromServer requestTimestamp authorization
2138 FederationBatchForward = \{forwards, fromServer, requestTimestamp, authorization} -> handleFederationBatchForward forwards fromServer requestTimestamp authorization
2168 FederationPubkey = \{} -> handleFederationPubkey
2139 FederationPubkey = \{} -> handleFederationPubkey
2169 Prune = \{authorization} -> handlePrune authorization
2140 Prune = \{authorization} -> handlePrune authorization