Skip to content

Latest commit

 

History

History
310 lines (242 loc) · 19.8 KB

File metadata and controls

310 lines (242 loc) · 19.8 KB

fmsg — Concise Implementation Specification

NOTE This is a distilled version of the full specification attempting to capture all neccssary information such that implementations following should be correct. More context may be needed at times to explain the logic, please refer to the full fmsg SPECIFICATION.

All integers are little-endian. "case-insensitive" means Unicode default case folding on UTF-8 strings. "TERMINATE" means tear down all connections with the remote host immediately.


1. Addresses

Format: @recipient@domain — UTF-8, prefixed by uint8 byte length.

Recipient part: Unicode letters/numbers (\p{L}, \p{N}), plus - _ . non-consecutively and not at start/end. Whole address < 256 bytes.

2. Message Binary Format

All fields are read sequentially. [ ] = conditionally present.

# Field Type Condition / Notes
1 version uint8 1–127 = message version. 129–255 = CHALLENGE (version = 256 − value). 0 and 128 unused.
2 flags uint8 Bit field, see §3.
3 [pid] 32 bytes SHA-256 of parent message. Present iff flag bit 0 set.
4 from address Sender address.
5 to uint8 count + addresses ≥ 1 distinct (case-insensitive) addresses.
6 [add to from] address Present iff flag bit 1 set. Must be in from or to.
7 [add to] uint8 count + addresses Present iff flag bit 1 set. ≥ 1 addresses, distinct within this list (case-insensitive); MAY overlap to.
8 time float64 POSIX epoch, stamped by sending host.
9 [topic] uint8 length + UTF-8 Present iff pid is NOT present. Length may be 0.
10 type uint8 + [US-ASCII string] If flag bit 2 (common type) set: uint8 is a Common Media Type ID (see §4). Otherwise: uint8 is length of subsequent ASCII Media Type string.
11 size uint32 Byte length of data on the wire (after compression, if zlib-deflate set).
12 [expanded size] uint32 Decompressed byte length. Present iff zlib-deflate set.
13 attachment headers uint8 count + [headers] Count may be 0. Each header: see §5.
14 data bytes Exactly size bytes.
15 [attachments data] bytes Concatenated attachment payloads, sizes defined by headers.

Message header = fields 1–13. Message header hash = SHA-256(message header). Message hash = SHA-256(entire message, fields 1–15).

The hash MUST be computed over the full message bytes: message header fields exactly as transmitted, followed by message data and any attachments data. When the zlib-deflate flag is set for message data or an attachment's data, that data MUST be decompressed prior to inclusion in the hash computation and MUST exactly match the corresponding expanded size; mismatch means invalid → TERMINATE.

Sender = from when has add to not set; add to from when set.

Participants = all addresses in from, to, add to from (if any), add to (if any).

3. Flags (uint8 bit field)

Bit Name Description
0 has pid pid field present; message is a reply.
1 has add to add to from + add to fields present; message adds recipients to an existing message.
2 common type type field is a 1-byte Common Media Type ID instead of length-prefixed string.
3 important Sender flags message as important.
4 no reply Sender will discard any reply.
5 zlib-deflate Message data compressed with zlib/deflate (RFC 1950/1951); expanded size field present.
6–7 reserved Must be 0.

4. Common Media Types

When flag bit 2 is set, the type field is a single uint8 mapping to:

ID Media Type ID Media Type
1 application/epub+zip 33 image/avif
2 application/gzip 34 image/bmp
3 application/json 35 image/gif
4 application/msword 36 image/heic
5 application/octet-stream 37 image/jpeg
6 application/pdf 38 image/png
7 application/rtf 39 image/svg+xml
8 application/vnd.amazon.ebook 40 image/tiff
9 application/vnd.ms-excel 41 image/webp
10 application/vnd.ms-powerpoint 42 model/3mf
11 application/vnd.oasis.opendocument.presentation 43 model/gltf-binary
12 application/vnd.oasis.opendocument.spreadsheet 44 model/obj
13 application/vnd.oasis.opendocument.text 45 model/step
14 application/vnd.openxmlformats-officedocument.presentationml.presentation 46 model/stl
15 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 47 model/vnd.usdz+zip
16 application/vnd.openxmlformats-officedocument.wordprocessingml.document 48 text/calendar
17 application/x-tar 49 text/css
18 application/xhtml+xml 50 text/csv
19 application/xml 51 text/html
20 application/zip 52 text/javascript
21 audio/aac 53 text/markdown
22 audio/midi 54 text/plain;charset=US-ASCII
23 audio/mpeg 55 text/plain;charset=UTF-16
24 audio/ogg 56 text/plain;charset=UTF-8
25 audio/opus 57 text/vcard
26 audio/vnd.wave 58 video/H264
27 audio/webm 59 video/H265
28 font/otf 60 video/H266
29 font/ttf 61 video/ogg
30 font/woff 62 video/VP8
31 font/woff2 63 video/VP9
32 image/apng 64 video/webm

An unmapped ID MUST be rejected (code 1 invalid).

5. Attachment Header

Each attachment header, in order:

Field Type Notes
flags uint8 Bit 0 = common type (same lookup as §4). Bit 1 = zlib-deflate. Bits 2–7 reserved.
type uint8 + [ASCII string] Same encoding rule as message type, using this attachment's own common type flag.
filename uint8 length + UTF-8 < 256 bytes. Unicode letters/numbers, plus - _ . non-consecutively, not at start/end. Unique per message (case-insensitive).
size uint32 Byte length of this attachment's data on the wire (after compression, if zlib-deflate set).
[expanded size] uint32 Decompressed byte length. Present iff this attachment's zlib-deflate flag set.

Attachment data payloads follow all headers, concatenated in order.

6. Challenge (sent on Connection 2)

Field Type
version uint8
header hash 32 bytes

7. Challenge Response

Field Type
msg hash 32 bytes

8. Response Codes

Single-value codes (sent as first/only byte):

Code Name Meaning
1 invalid Message header fails validation.
3 undisclosed No reason given.
4 too big Exceeds MAX_SIZE or MAX_EXPANDED_SIZE.
5 insufficient resources e.g. disk full.
6 parent not found pid references unknown message.
7 too old Timestamp too far in past.
8 future time Timestamp too far in future.
9 time travel Timestamp before parent's timestamp.
10 duplicate Message already received by this host.
11 accept add to Additional recipients received, discontinue.
64 continue Header accepted; send data.
65 skip data Add-to accepted; parent already stored; add to recipients on this host. Skip data, per-recipient codes follow.

Per-recipient codes (one byte per recipient on this host, in message order):

Code Name Meaning
100 user unknown Address not known.
101 user full Recipient quota exceeded.
102 user not accepting Recipient not accepting messages.
103 user duplicate Already received for this recipient.
105 user undisclosed No reason disclosed. MAY replace any 100–103.
200 accept Message stored for this recipient.

9. Domain Resolution

Resolve fmsg.<domain> for A/AAAA records. The sender's domain is:

  • The domain of add to from when has add to is set.
  • The domain of from otherwise.

The Receiving Host MUST verify the incoming connection IP is in the resolved set for the sender's domain. DNSSEC SHOULD be validated; on failure TERMINATE.

10. Protocol Flow

One message per connection. Two TCP connections used: Connection 1 (message transfer) and Connection 2 (optional challenge). Host A = Sending Host, Host B = Receiving Host.

10.1 Host Configuration

Variable Description
MAX_SIZE Max total bytes of data + attachment data on the wire.
MAX_EXPANDED_SIZE Max total bytes after decompression; SHOULD normally equal MAX_SIZE.
MAX_MESSAGE_AGE Max seconds a message time may be in the past.
MAX_TIME_SKEW Max seconds a message time may be in the future.

10.2 Sending (Host A perspective)

Host A delivers iff from or add to from belongs to Host A's domain.

When has add to is NOT set: perform the steps below for each unique recipient domain.

When has add to IS set: perform the steps below for each unique participant domain — the domains of from and of every address in to and add to. from's domain is omitted when from is the add to from (the adder is the original sender, whose host is Host A).

  1. Resolve recipient domain IPs via fmsg.<domain>. Connect to first responsive IP (Connection 1). Retry with backoff if unreachable.
  2. Register the message header hash and Host B's IP in an outgoing record (for matching challenges).
  3. Transmit the message header on Connection 1.
  4. Wait for response. During this wait, be ready to handle a CHALLENGE on Connection 2 (see §10.5).
  5. Read one byte from Connection 1:
    • 1–10: Rejected for all recipients. Record and close.
    • 11: Accept add-to (only valid when has add to set). Record and close.
    • 64: Continue — transmit data + attachment data (exact declared sizes).
    • 65: Skip data (only valid when has add to set). Do NOT transmit data.
    • Other: TERMINATE.
  6. After transmitting data (code 64) or immediately (code 65), read one byte per recipient on this host (in message field order: to then add to). Each byte is a per-recipient response code.
  7. Record results. Remove Host B's IP from outgoing record; remove entry if no IPs remain. Close Connection 1.

10.3 Receiving — Header Exchange (Host B perspective)

  1. Read first byte on Connection 1:
    • 1–127 and supported → message version, continue.
    • 129–255 and (256 − value) supported → incoming CHALLENGE, handle per §10.5.
    • Otherwise → TERMINATE (unsupported version — we don't know how to respond).
  2. Parse remaining header. If unparseable → TERMINATE.
  3. Validate (all must pass, else respond code 1 invalid and close):
    • to has ≥ 1 distinct address.
    • If has add to: add to from exists and is in from or to; add to has ≥ 1 address, distinct within add to (case-insensitive). add to MAY overlap to — re-serving an original recipient who lost the message.
    • If has add to not set: ≥ 1 recipient in to belongs to Host B's domain. If has add to set: ≥ 1 participant (from, to, add to from or add to) belongs to Host B's domain.
    • Common type IDs (message and attachment) are mapped.
    • expanded size fields are present iff the corresponding zlib-deflate flag is set.
  4. DNS-verify sender IP: resolve fmsg.<sender domain>, check Connection 1 source IP is in result set. Fail → TERMINATE.
  5. If size + attachment sizes > MAX_SIZE, or total expanded size > MAX_EXPANDED_SIZE → respond code 4, close. Total expanded size uses expanded size for compressed parts and size for uncompressed parts.
  6. Compute DELTA = now − time:
    • DELTA > MAX_MESSAGE_AGE → respond code 7, close.
    • DELTA < −MAX_TIME_SKEW → respond code 8, close.
  7. Evaluate pid / add-to:
    • No pid, no add-to (new thread): proceed.
    • pid set, no add-to (reply):
      • Verify parent stored (§11). Not found → respond code 6, close.
      • Parent time − MAX_TIME_SKEW must be before incoming time. Fail → respond code 9, close.
      • from must be a participant of the parent. Fail → respond code 1, close.
    • add-to set (adding recipients):
      • pid MUST also be set. Fail → respond code 1, close.
      • Check if parent stored (§11):
        • Stored: check time travel (code 9 if fail).
        • Not stored: if ≥ 1 recipient in to or add to belongs to Host B's domain, treat as full message delivery. Otherwise (Host B hosts only non-recipient participants) respond code 6 (parent not found), close.
  8. Optionally issue a CHALLENGE on Connection 2 (see §10.5).

10.4 Receiving — ACCEPT Response, Data Download and Per-Recipient Response

Steps 1–3 determine exactly one response code for the message header: the first rule that matches decides the code sent and the remaining rules are not evaluated.

  1. If add to set and parent verified stored in step 7:
    • If Host B has already recorded this exact add-to batch (§11) → respond code 10 (duplicate), close.
    • If any add to recipient belongs to Host B's domain → respond 65 (skip data).
    • Otherwise → record the add-to batch (add to from, add to, time) per §11, respond 11 (accept add to), close. This is the path taken by a participant domain hosting none of the add to recipients, including a notification-only domain — one with no address in to or add to at all, being told only that recipients were added.
  2. If challenge was completed, use the message hash from the challenge response to check for duplicates across all recipients on Host B. If duplicate for all → respond code 10, close.
  3. Otherwise → respond 64 (continue).
  4. If code 65 was sent, skip to step 6 (data already stored). Otherwise download data + attachments (exactly declared on-wire sizes). For each zlib-deflate part, decompress and verify output byte length exactly equals expanded size; failure or mismatch means invalid → TERMINATE.
  5. If challenge was completed, verify computed message hash matches the challenge response hash. For code 65, compute from received header + stored data. Mismatch → TERMINATE.
  6. For each recipient on Host B's domain, send one response byte, in to order then add to order. An address in both lists is a recipient of each and gets one byte for its to entry and one for its add to entry.
    • Already received → 103 (or 105).
    • Unknown address → 100 (or 105).
    • Quota exceeded → 101 (or 105).
    • Not accepting → 102 (or 105).
    • Otherwise → 200 (accept).
  7. Close Connection 1.

10.5 Challenge Flow

The challenge is optional (Receiving Host's discretion). It runs on a separate Connection 2 while Connection 1 is paused after the header exchange.

Receiving Host (Host B) initiates:

  1. Open Connection 2 to Host A's IP (the source IP of Connection 1). DNS verification of sender IP must already have passed.
  2. Send CHALLENGE: version byte + 32-byte message header hash.

Sending Host (Host A) handles:

  1. Read first byte on incoming connection:
    • 1–127 and supported → incoming message, handle normally.
    • 129–255 and (256 − value) supported → CHALLENGE, continue.
    • Otherwise → TERMINATE (unsupported version).
  2. Read 32-byte header hash. Match against outgoing record by header hash AND challenger's IP. No match → TERMINATE.
  3. Send CHALLENGE RESPONSE: 32-byte SHA-256 of entire message.

Host B receives the 32-byte message hash from Host A. Both close Connection 2. Exchange continues on Connection 1.

Host A MUST maintain a record of outgoing messages keyed by message header hash, including each Receiving Host's IP. Used to match challenges and verify the challenger's IP. Create before transmission; remove a host's IP on completion/abort; remove the entry when no IPs remain.

11. Verifying Message Stored

A message is verified as stored iff:

  • A SHA-256 digest matches a message either previously accepted by the host (code 200 to ≥ 1 recipient, or code 11), or sent by the host (its from / add to from belongs to the host's domain and the host transmitted it or holds it for sending — including each add-to batch it sent, since replies may reference a batch by its hash).
  • That message currently exists and is retrievable.

A host MUST retain each stored message in full and exactly as transmitted — including the complete to and add to recipient lists, not only recipients on its own domain — so the message hash can always be faithfully recomputed and participant checks (§10.3 step 7) evaluate against the true participant set.

An add-to message's data never crosses the wire when the host already holds the original (codes 11 and 65), and its sender likewise already holds it. Its hash is therefore computed by RECONSTRUCTION: the add-to header exactly as transmitted combined with the original message's data and attachment data. Every host holding a batch — accepted via 11 or 65, or sent by it — MUST be able to reconstruct it so replies referencing the batch hash can be verified; hosts on the 65 path MUST record the batch fields (add to from, add to, time) exactly as transmitted, as the 11 path already requires.

Each add-to batch produces a distinct hash — batch identity IS the batch message hash, which covers time: the same add to addresses re-issued at a new time are a distinct batch. Only the exact batch that had an accepted response (200 or 11) matches.

12. Adding Recipients

An add-to message is a duplicate of the original message with these differences:

  • Flag bits 0 (has pid) and 1 (has add to) set.
  • pid = hash of the message being added to (replacing any pid the original had).
  • add to from = participant initiating the add (must be in original from or to).
  • add to = the added recipient addresses; these MAY include an address already in to, re-serving an original recipient.
  • time = new timestamp.
  • topic is NOT present (pid is set).

An add-to message MUST be sent to every participant domain per §10.2, so all participants of the message being added to — including the original sender, when not themselves the add to from — learn of the added recipients, not only the domains hosting the new recipients. This is required because a subsequent reply may reference this add-to message via pid, and a host can only accept a reply whose parent it holds.

Add-to batches do not chain: recipients are always added to the original message; an add-to message's pid MUST NOT reference another add-to message. A message therefore has 0 or more add-to batches, each a sibling branch under the original — the thread evolves as a tree.

A recipient added by a batch and not already in to is a participant of that batch message only, not of the original: their replies MUST reference the batch message via pid (referencing the original would fail the participant check, §10.3 step 7) and extend the batch's branch. An address in both to and add to was already a participant of the original and may reply on either branch.

A batch is identified by its message hash, which covers time (§11): re-issuing the same add to addresses at a new time is a new, distinct batch — a new sibling branch — not a duplicate.

13. Security Requirements

  • Enforce MAX_SIZE and MAX_EXPANDED_SIZE before downloading data.
  • For zlib-deflate parts, bound decompression and require output length to exactly match expanded size; mismatch means invalid → TERMINATE.
  • Enforce per-connection and per-IP rate limits.
  • Apply idle/slow-connection timeouts.
  • Verify sender IP via DNS BEFORE issuing any challenge.
  • Rate-limit outgoing challenge connections.
  • Use DNSSEC where supported. Fail → TERMINATE.
  • Track accepted message hashes to reject duplicates.
  • Support per-user storage quotas.
  • Use code 105 (user undisclosed) to prevent sender enumeration.
  • Log: timestamp, source IP, sender domain, response codes, challenge outcomes, termination reasons.