38 KiB
Smol Mail Protocol, version 1.2
A minimalist, decentral, end-to-end encrypted mail protocol.
Online correspondence reduced to an address, a key and a message.
- Five operations, four primitives, zero extensibility mechanisms.
- One 32bit master key as the only identity.
- No certificates, no CAs, no expiry. One round trip and the server's key pin are the entire trust model.
- A fresh key seals every message: untraceable, unforgeable, and safe by construction.
- Anti-spam without censorship: quotas and tokens do the filtering; servers never see a word.
- Clients talk directly to the recipient's server
- Registration is bound to one server by proof-of-possession.
- Couble-signed certificate chains move a username to a new key, and old keys keep receiving until every contact catches up.
1. Primitives
-
Ed25519 (RFC 8032)
-
X25519 (RFC 7748)
-
ChaCha20-Poly1305 (RFC 8439)
-
SHA-256 / HMAC-SHA256 (RFC 2104) anHKDF-SHA256 (RFC 5869)
-
Plus a Noise pattern (§4) as a construction over these four.
-
Integers are big-endian. Text is UTF-8.
-
Every hash, signature and MAC is domain-separated by a label from §11.
2. Identity
- The only secret a user holds is a 32-byte master.
- It is the only thing to back up, and it never changes.
- Everything else is derived from it:
seed_n = HKDF-SHA256(master, salt = "", info = "smolmail/1 identity" || n, len = 32) # n: u32
accept = HKDF-SHA256(master, salt = "", info = "smolmail/1 accept", len = 32)
n is the rotation index, counting from 0. seed_n is an Ed25519 seed, and its 32-byte public key is the identity at that index. Rotation (§7) advances n; the accept key of §5.8 does not depend on it, so it survives every rotation. HKDF is one-way, so a compromised seed_n does not expose the master or any other index.
A client restoring from the master alone recovers the rest: it resolves its own username, derives seed_0, seed_1, ... and stops at the index whose public key the server has bound, within the chain limit of §7. Every superseded signing key is therefore re-derivable and none has to be archived.
X25519 keys for key agreement come from the same keypair:
x_priv = clamp(SHA-512(seed_n)[0..32]) # [0] &= 248; [31] &= 127; [31] |= 64
x_pub = (1 + y) / (1 - y) mod 2^255-19 # y from the Edwards point; sign bit discarded
These are libsodium's crypto_sign_ed25519_sk_to_curve25519 and ..._pk_to_curve25519. Implementations SHOULD call those rather than reimplement the map. Using only one key in both a signature scheme and a key-agreement scheme is a deliberate trade, mitigated by domain-separating every derivation (§11) and explicitly made to keep an identity at 32 bytes: that's one QR code, one spoken fingerprint.
- Implementations MUST reject an all-zero X25519 output and MUST reject a received ephemeral public key that is a low-order point.
3. Addressing
alice@example.org[:1961] -- short form, resolved via the server
smol://alice@example.org/mfrggzdfzt... -- self-certifying, carries its own key
- Usernames are 1-63 bytes of
[a-z0-9._-]. - A username MUST begin and end with
[a-z0-9]. - A username MUST NOT contain two adjacent separators.
- Usernames are compared case-insensitively.
- Clients normalise to lowercase before sending.
- The default TCP port is 1961.
The smol:// path component is the 32-byte identity key in RFC 4648 base32, lowercase and unpadded (52 characters). Because it carries the key, an address shared this way requires no trust in any server. It is the form used for QR codes, contact files and links.
Fingerprints for spoken or visual verification are the first 20 characters of that base32 string in groups of four, a truncation of the identity, not a separate encoding. Twenty characters carry 100 bits, so producing a second key with a given fingerprint costs about 2^100 trials.
4. Transport
TCP with a Noise_NX_25519_ChaChaPoly_SHA256 handshake, prologue smolmail/1. One round trip. No certificates, no CA, no expiry.
The NX pattern has the server transmit its static public key during the handshake, so pinning is a single code path:
-
If a pinned key exists for this host, it MUST match the key received.
-
On mismatch the client MUST abort and surface the discrepancy.
-
If no pinned key exists, the client MAY accept and pin it, but MUST treat
RESOLVEresults from that session as unverified and mark them as such. -
A client MUST NOT
REGISTER,FETCHorDELETEagainst a server whose static key it did not obtain from a trusted channel. -
An unpinned server is acceptable for
SENDwhen the sender already holds the recipient's identity key: the payload is sealed end-to-end, so the channel is providing only confidentiality against a passive observer.
Framing
- Each Noise message carries a 2-byte length prefix and is at most 65535 bytes.
- Application frames are
length (u32) || type (u8) || body, split across Noise messages, with a default maximum of 1 MiB. - Every field is length-prefixed; nothing on the wire requires text parsing or delimiter scanning.
Session authentication
FETCHandDELETErequire oneAUTHframe: an application frame of type0x00-- after the handshake:
username_len u8 || username || identity 32 || signature 64
|| sync u8 || count u16 || token 32 × count
signature = Ed25519 over "smolmail/1 auth" || h # h = Noise handshake hash
The server verifies the signature and that identity is the key bound to username. Because h incorporates the server's fresh ephemeral, this requires no challenge round trip and cannot be replayed across sessions or servers.
The trailing block carries the mailbox's accept tokens (§5.8). sync = 0 leaves the stored set untouched and count MUST then be 0; sync = 1 replaces the stored set with exactly the tokens that follow, which is how a token is both added and removed. A client MUST send sync = 0 unless it knows its set is complete -- in particular a client restored from the master alone, whose empty set would otherwise erase the server's. A set larger than the server's limit is refused with status 7 and leaves the session unauthenticated.
5. Message format
5.1 Envelope: what the server stores cleartext
magic 4 "SMOL"
version 1 0x01
to 32 recipient Ed25519 identity key
epk 32 sender's ephemeral X25519 public key
ciphertext n
tag 16 Poly1305
ChaCha20-Poly1305 with nonce = 0^12 and aad = magic || version || to || epk. The all-zero nonce is correct because the key is used exactly once (§5.2).
There is no nonce field, so nonce reuse is not a failure mode an implementation can have. The envelope is also the on-disk and export format, hence the magic and version.
- An envelope is 69 bytes of header plus ciphertext plus a 16-byte tag.
- It is at most 768 KiB (§10).
- The payload it seals is 109 bytes plus the body.
- A message identifier is 32 bytes.
- A rotation certificate is 200.
- An accept token is 32 and its MAC 32.
5.2 Sealing
The sender generates a throwaway X25519 keypair per message:
esk, epk = X25519_keygen()
shared = X25519(esk, to_x25519(recipient_identity))
key = HKDF-SHA256(shared, salt = epk || recipient_identity,
info = "smolmail/1 seal", len = 32)
wipe(esk)
The recipient recovers the same key with X25519(own_x_priv, epk).
- The sender's long-term key signs only and never participates in key agreement.
- Therefore the sealing key is unique per message.
- Compromising a sender's identity key reveals nothing about messages already sent.
- The sender does not appear in the envelope, so a server cannot observe who corresponds with whom.
- An envelope has exactly one recipient.
- Addressing one message to several people means sealing one envelope per recipient, with no shared state between them.
(A seized recipient key still decrypts ciphertext stored while it was valid.)
5.3 Sealed payload
version 1 0x01
sender 32 Ed25519 identity key
time 8 int64, Unix seconds, UTC
body_len 4 u32
body n UTF-8
signature 64 Ed25519 by sender
padding 0..n ignored
The signature covers "smolmail/1 msg" || to || epk || version || sender || time || body_len || body. Covering to prevents a recorded message being re-addressed to a third party and still verifying; covering epk binds the signature to this exact sealing. A receiver MUST verify the signature against sender and MUST check that to equals its own identity key.
- A receiver MUST reject a payload whose
timeis more than 86400 seconds ahead of its own clock, and MAY mark a larger backwards skew. - This
versionis not the envelope's: the envelope version governs the sealed-box construction of §5.1 and §5.2, this one governs the plaintext layout, and either can change while the other does not. - Senders MAY pad the plaintext to a multiple of 1024 bytes to blur message length.
- Receivers MUST ignore trailing bytes.
body_lenmakes padding unambiguous, and the AEAD tag protects it from alteration.
5.4 Message identifier
id = SHA-256("smolmail/1 id" || envelope)
- The identifier is derived: a sender cannot choose it, and both servers and clients can deduplicate without trusting anyone.
- It is used whole, nothing truncates it.
5.5 Body frontmatter
If the body begins with the exact bytes ---\n, an optional frontmatter block runs to the next line consisting of exactly ---. Everything after that line is the message text.
---
Subject: Re: the thing
In-Reply-To: 4f2a1c9e8b7d6a5f3e2d1c0b9a8f7e6d5c4b3a291807f6e5d4c3b2a1908f7e6d5
X-Mood: cautiously optimistic
---
Body text starts here.
- One
Key: valueper line. - Key is 1-64 bytes of
[A-Za-z0-9-]. - Value is the remainder of the line after
:, trimmed of surrounding spaces. - No nesting, arrays, multi-line values, quoting, comments or type coercion.
- Keys are compared case-insensitively, so
Subjectandsubjectare the same key - Where a key repeats, the first occurrence wins.
- Maximum 4 KiB and 64 keys.
- A malformed line invalidates the whole block, which is then displayed as ordinary body text.
- Frontmatter fails closed toward display, never toward silent discard.
Subject,In-Reply-To(a message id as 64 lowercase hex characters),Reply-To(§5.7) andAccept(§5.8) are reserved.- Unknown keys MUST be preserved, MAY be displayed, and MUST NOT alter client behaviour.
- A body whose text genuinely begins with
---is escaped by emitting an empty block first. - This grammar is not YAML: do not use a YAML parser.
- It is flat so that a correct parser is twenty dependency-free lines, in a code path that handles attacker-controlled input.
5.6 Sent copies
Because esk is wiped, a sender cannot decrypt what they sent. To keep a Sent folder, a client seals a second copy of the payload to the sender's own identity key with a fresh ephemeral and stores it locally. This is a client convention and involves no server.
5.7 Sender addresses
Nothing on the wire names a sender. A receiver learns only the signing key of §5.3, so a first contact is identified by nothing but its fingerprint, and replying needs an address the sender supplied somewhere: out of band, or in the message itself.
Like the sent copies of §5.6 this is a client convention and involves no server:
- a sender MAY include a
Reply-Tofield holding its own fullsmol://address. - The value sits inside the sealed, signed payload, so the signature binds the claim.
- A receiver that acts on it MUST check that the key carried in the URI equals the message's
sender, MUST treat anything else as ordinary text, and MUST NOT let it replace a key already bound to that address (§8). - It is a first-contact aid, not a trust upgrade.
5.8 Accept tokens
- A server cannot see senders.
- It cannot tell wanted mail from a flood.
- An accept token is the recipient's own 32-byte secret, given to one correspondent and to her server.
- It lets the server separate the two without learning who anyone is.
t = HMAC-SHA256(accept, correspondent_identity) # accept: §2
mac = HMAC-SHA256(t, "smolmail/1 mac" || id) # id: §5.4
- The accept key never leaves the owner's device.
tis derived rather than random so that it is a function of the master and the correspondent's identity alone, and a MUST.- One distinct token per correspondent, or they can neither be told apart nor removed individually.
- The owner uploads her tokens with
AUTH(§4) and delivers each one to its correspondent as a reservedAcceptfrontmatter field, base32, inside a sealed and signed payload. - A receiver MUST ignore an
Acceptvalue unless the payload's signature verifies, and MUST attribute the token to the signer. - Because a token is issued by a mailbox rather than by one of its keys, a receiver SHOULD file it under the address it knows that signer by: one already bound to the key (§8), or the
Reply-Tothe same payload signs for itself (§5.7), so that the token keeps working across the issuer's rotations and a rotation it never observed cannot orphan it.
A sender holding a token attaches mac to SEND (§6.1). The server recomputes HMAC(t_i, ...) for each token t_i stored for the recipient's mailbox and compares in constant time:
- A token matches: the envelope enters the mailbox's main quota and retention.
- No token matches, the MAC is malformed, or none is attached: the envelope enters the requests tier, a much smaller quota with a short retention (§10).
FETCHmarks which tier a message arrived in, so a client can present unsolicited mail separately from correspondence.- A failed match is never reported to the sender:
SENDsucceeds either way, and cannot be used to test whether a token is still accepted. - Removing a token from the set, an
AUTHwithsync = 1that omits it, demotes that correspondent to the requests tier from the next message on. - The tier is merely a quota boundary: refusing someone's mail outright remains the client's decision, after decryption.
- Because the accept key is independent of the rotation index (§2), tokens survive the owner's key rotation.
- A token is likewise derived from the correspondent's identity as it stood when they were accepted, so their later rotation does not invalidate the token they hold.
6. Operations
| Type | Op | Auth | Request → response |
|---|---|---|---|
| 0x01 | RESOLVE |
no | username → identity key + rotation chain, possibly empty |
| 0x02 | SEND |
no | one envelope, optionally with an accept MAC → id. Redelivery is a no-op |
| 0x03 | FETCH |
yes | cursor → count, then id || received_at || flags || envelope records, oldest first, up to a server byte budget |
| 0x04 | DELETE |
yes | count + 32-byte ids → number removed. Unknown ids are not an error |
| 0x05 | REGISTER |
no | username + identity key + proof of possession + optional invite token + optional rotation certificate |
AUTH (§4) is session setup, not an operation.
SEND requires no account on the recipient's server. Clients connect directly to the server named in the address; there is no relaying and no federation, so an outbound queue lives in the sending client.
FETCH carries a cursor and has no flags or server-side read state; a client pages forward until a response is empty, and deleting is a separate decision.
REGISTER without a certificate binds a free username, or returns code 9; with one, it rebinds an existing username when the chain validates against the currently bound key.
6.1 Request and response bodies
- A request body is the operation's payload.
- A response body begins with
status (u8)from §12. - On
0the operation's payload follows, on anything else an optional UTF-8 reason string. - A response reuses the request's type byte.
AUTHis type0x00and its response carries a status and, on success, the number of accept tokens the server now holds.
AUTH → username_len u8 || username || identity 32 || signature 64
|| sync u8 || count u16 || token 32 × count
← accepted u16
RESOLVE → username_len u8 || username
← identity 32 || chain_len u8 || cert 200 × chain_len
SEND → mac_len u8 || mac || envelope
← id 32
FETCH → after_received_at i64 || after_id 32
← count u16 || ( id 32 || received_at i64 || flags u8
|| env_len u32 || envelope ) × count
DELETE → count u16 || id 32 × count
← removed u16
REGISTER → username_len u8 || username || identity 32 || signature 64
|| token_len u8 || token || cert_len u8 || cert
← (empty)
A zero length means the field is absent. SEND's mac_len is 0 or 32 (§5.8).
REGISTER's signature is proof of possession, required on every registration and rotation:
signature = Ed25519 by identity over
"smolmail/1 register" || server_static || username || identity
server_static is the key the server sent during the handshake, so the attestation cannot be replayed to another server. Without it any party could bind any public key to any username.
FETCH returns records ordered by (received_at, id), strictly greater than the cursor; an all-zero cursor starts at the beginning, and a client's next cursor is the last record it received. A server fills a response up to a byte budget which MUST be smaller than the maximum application frame -- 512 KiB against the default 1 MiB frame is a reasonable choice -- and MUST return at least one record even when it alone exceeds the budget, so no message can wedge a mailbox shut. In flags, bit 0 is set when the message arrived without a matching accept token (§5.8); the remaining bits are zero.
7. Key rotation
old_pub 32 || new_pub 32 || time 8 || sig_old 64 || sig_new 64
both signatures over "smolmail/1 rotate" || username || old_pub || new_pub || time
sig_old is by old_pub and sig_new by new_pub. Both are required: the old key alone could otherwise hand a username to a key nobody controls, or to someone else's. The username is covered but not carried, which keeps the certificate fixed-size and stops it being a portable object replayable against another username bound to the same key; a verifier always knows which username it is checking.
- A server keeps each username's ordered chain of certificates and returns it with every
RESOLVE. - A client holding a stale pinned key walks the chain forward from that key, verifying both signatures on each link, and accepts the new key silently if the chain terminates at the key
RESOLVEreturned. - A broken, absent or over-long chain, maximum 16 links, requires explicit user confirmation.
- Senders SHOULD also push their certificate to contacts as an ordinary message, so rotation propagates without depending on the old server.
- A server MUST retain every key ever bound to a username, MUST accept
SENDaddressed to any of them, and MUST return messages addressed to any of them onFETCH. - Without this, mail from a contact who has not yet seen a rotation is addressed to a superseded key and becomes unreachable.
- A client MUST likewise be able to produce every private key it has rotated away from, since sealing is to a specific key (§5.2) and mail addressed to a superseded key is readable with nothing else.
- Advancing the rotation index rather than generating an unrelated key makes those keys a derivation from the master (§2) rather than an archive.
- Rotation is not revocation: An attacker holding a stolen identity key can rotate to their own key and the chain will validate.
- Clients MUST surface rotations in the interface rather than applying them invisibly.
- Out-of-band re-verification is the only defence against a compromised identity key.
- There is no revocation mechanism.
8. Trust model
| Key source | Guarantee |
|---|---|
| Home server key from a trusted channel | Authenticated, pinned, mismatch detected |
Contact key from smol:// or a QR code |
Strong; requires no server trust |
Sender's Reply-To field, key equal to the signer (§5.7) |
Strong; the sender's own signed claim, but never overrides a pinned key |
RESOLVE over a pinned session |
Trust on first use, as good as that server |
RESOLVE over an unpinned session |
Trust on first use, interceptable at first contact; MUST be shown as unverified |
| Key change with a valid chain | Accepted, surfaced in the interface |
| Key change without a chain | Rejected until the user confirms |
9. Privacy
A server learns which mailbox each envelope is for, its size, and when it arrived, was fetched and was deleted, plus the connecting IP address. It does not learn the sender, the subject, the content, or any relationship between messages.
Accept tokens (§5.8) add to that: the server learns how many correspondents a user has accepted, whether a given message came from one, and which token it matched. A token is therefore a stable pseudonym, and the server can group a correspondent's messages under it without learning the identity behind it. That is the price of a mailbox that cannot be filled by anyone who knows an address, and an unfillable mailbox is a precondition for the rest of this document mattering.
A network observer learns that a client contacted a host on this port, and the volume and timing of traffic. Everything after the first round trip is encrypted, including usernames, recipient keys and tokens.
Not protected: traffic analysis, delivery timing correlation, mailbox size, and whether a given user has an account on a given server. Operators wanting metadata resistance should run the server as a Tor onion service; the transport is plain TCP and needs no changes.
There is no recipient-side forward secrecy. Messages are signed, which provides non-repudiation rather than deniability: a recipient can prove to a third party who wrote a message.
10. Servers
- Independent store-and-forward mailboxes that never communicate with one another.
- Each holds its own username bindings, rotation chains, accept tokens and envelope queues.
- Envelopes persist until deleted or expired.
Every mailbox has two tiers (§5.8). Defaults, all configurable:
768 KiB per envelope 2 MiB requests-tier quota
64 MiB main-tier quota 7 days requests-tier retention
30 days main-tier retention 1024 accept tokens per mailbox
A server also keeps the identifier of every envelope it has accepted for the length of its retention window, and answers a resend with the same id and no new message, so an envelope captured and replayed after deletion does not reappear. Clients keep their own seen-id set for the same reason.
Abuse control is quotas, size caps and rate limits: per-IP connection and SEND limits, and a SEND limit per accept token, so one accepted correspondent cannot fill the main tier either. The accept-token set is an allow-list the mailbox owner controls and the server cannot interpret; there are no server-side denials, because a server still cannot see senders, and content filtering belongs in the client, after decryption. Operators SHOULD NOT log recipient keys alongside IP addresses and timestamps, which would reconstruct much of the metadata the message format withholds.
11. Domain separation labels
"smolmail/1" Noise prologue "smolmail/1 msg" message signature
"smolmail/1 identity" signing seed "smolmail/1 id" message identifier
"smolmail/1 accept" accept key "smolmail/1 mac" accept token MAC
"smolmail/1 auth" session auth "smolmail/1 rotate" rotation certificate
"smolmail/1 seal" HKDF info "smolmail/1 register" proof of possession
"smolmail/1 bind" link binding (§13.6)
The smolmail/1 prefix is a namespace, not a version list: every label keeps it, including those a later section adds.
12. Status codes
0 ok 3 unknown user 6 quota exceeded 9 not permitted
1 malformed 4 auth required 7 too large 10 internal error
2 bad version 5 auth failed 8 rate limited
A status is the first byte of every response body (§6.1), which MAY be followed by a UTF-8 reason string. Clients MUST NOT parse the reason. Codes not listed are unassigned: a client MUST treat one it does not know as a failure of the operation, MUST NOT retry automatically, and SHOULD surface the number.
13. Reticulum transport
This section was added in version 1.2 and edits nothing above; its numbering continues from §12. A mailbox reachable over the Reticulum Network Stack needs no DNS name, no IP address and no fixed topology. It works over LoRa, packet radio, serial links, I2P or TCP, and it keeps working when the internet does not.
- Reticulum replaces the transport of §4. It does not wrap it.
- An address grows a second form. Identities, envelopes, message identifiers, operations, rotation and accept tokens are untouched.
- A server MAY offer either transport or both over one store.
RNS facts cited below were read from Reticulum 1.5.4. Constants in that stack are version-dependent and some are undocumented, so an implementation MUST read them from the stack at runtime rather than compile them from this text, and SHOULD state the version it was tested against. A reader who finds a figure here contradicted by the stack SHOULD believe the stack.
smolmail_rns.py and smolmaild_rns.py are a reference client and server for this transport, tested against Reticulum 1.5.4.
13.1 Server destination
A server holds one Reticulum identity, persisted, separate from every Smol Mail identity in §2 and never used as one. RNS writes its private key unencrypted, so the file MUST be protected as any long-term server key is: mode 0600, backed up by the operator, never transmitted.
RNS.Destination(identity, IN, SINGLE, "smolmail", "server")
The full name is smolmail.server and the destination hash is 16 bytes, derived by RNS from that name and the server identity's public keys. Nothing random enters that derivation, so the hash is stable across restarts for as long as the identity file survives, and it changes if and only if the identity does.
- A server MUST construct its destination from a loaded identity. Passing none makes RNS generate a throwaway identity and append its hex hash as a third aspect, which yields a different address on every start.
- A server that relies on ordinary path discovery MUST announce its destination periodically. A path request is answered from a node's path table, and a path table is populated by announces; paths expire, soonest on access point and roaming interfaces, so the announce interval MUST stay inside the shortest expiry the stack applies. Interfaces rate-limit a destination to roughly one announce an hour, which sets the ceiling; every few hours is enough.
- A server answers a path request for its own destination whether or not it has ever announced, so an unannounced destination is reachable by a client that can reach it directly, or through nodes whose interfaces search for unknown destinations -- access point, gateway, roaming and boundary modes, or recursive path requests. An unlisted mailbox is therefore dependable on one segment and a matter of the operator's configuration beyond it, not a property this protocol can promise.
13.2 Addressing
smol+rns://alice@8f2c1d0a7b6e5f4c3d2a1b0e9f8c7d6a short form, resolved via the server
smol+rns://alice@8f2c1d0a7b6e5f4c3d2a1b0e9f8c7d6a/mfrggzdfzt... self-certifying, carries its own key
The authority is the username of §3 and the server's destination hash as 32 lowercase hexadecimal characters. Hexadecimal rather than the base32 of §3 because that is how every Reticulum tool prints a destination, and an address is something a user copies from rnstatus or an operator's page.
- The scheme is required in both forms. There is no bare
user@hostshort form on this transport, because a destination hash is not distinguishable from a hostname by shape. - A client MUST reject an authority whose host part is not exactly 32 hexadecimal characters, and MUST compare destination hashes in full.
- There is no port. The
:portsuffix of §3 MUST NOT appear. - Usernames, their normalisation, and the 52-character base32 identity key in the path are exactly as in §3. Fingerprints are of that key and are unaffected.
- A
Reply-To(§5.7) and a contact imported from a URI or QR code MAY carry asmol+rns://address. Every check in §5.7 applies unchanged: the key in the URI MUST equal the payload'ssender, and it MUST NOT replace a key already bound.
Reticulum has no name resolution -- its resolver is a stub -- so the hash is the address. Naming is out of scope here as it is in RNS itself.
13.3 Discovery
A client resolves a destination through Reticulum's own path discovery:
- If no path is known, request one and wait. The default path request timeout is 15 seconds; slow media widen it.
- Recall the identity for the hash, build an
OUT/SINGLEdestination from it, then establish a link. - A client MUST NOT create a link before a path is known. With no path RNS assumes the maximum hop count and the link fails after many minutes instead of promptly.
13.4 Server authentication
There is nothing to pin. A destination hash is a 128-bit truncated hash over the server identity's public keys, and Reticulum establishes the link against exactly that identity: the responder signs the link proof with its long-term key, and RNS verifies it. The address is the pin. Finding a second identity that answers to a given destination costs about 2^128 trials, the same argument §3 makes for fingerprints.
So §4's pinned and unpinned sessions become one question -- where did this address come from -- and every rule §4 states carries over with the address standing in for the pinned key: REGISTER, FETCH and DELETE require a destination obtained from a trusted channel, SEND does not when the sender already holds the recipient's identity key, and RESOLVE results from a destination of unknown provenance MUST be marked unverified.
There is consequently nothing for a client to trust out of band and no key to install: §4's pinning step has no counterpart here. A client SHOULD instead record how it learned each address -- out of band, from a URI or QR code, from a Reply-To (§5.7), or from somewhere unknown -- and read §8's table by that column, since the address and the server key are now one thing.
13.5 Requests
Operations map onto Reticulum's request and response mechanism on a single path, smolmail/1, which carries the version tag that the Noise prologue carries on the other transport. RNS transmits a path only as a 16-byte hash, so the tag is free.
request → type u8 || body # type from §6, body exactly as in §6.1
response → status u8 || payload # status from §12, payload exactly as in §6.1
Every operation body and every response payload in §6.1 is reused byte for byte. Only the length u32 of §4 is gone, because Reticulum delimits messages itself.
- A server MUST answer every registered request, including with an error status. The default handler policy answers nothing at all, which a client cannot distinguish from a dead link.
- A server MUST bound the request size. The largest legitimate request is the greater of an envelope plus 34 bytes and an
AUTHcarrying a full token set, which is about 32 KiB at the 1024 tokens of §10. - Requests and responses above the link's maximum data unit become Reticulum resources automatically, in both directions. Nothing in the protocol has to name them.
- A client MUST set its own request timeout. Reticulum's default is derived from the round trip time and covers a packet, not a bulk transfer over a slow link.
- A response status is not a transport outcome. No path, a failed link, a rejected resource and a timeout are local errors, not status codes, and a client MUST NOT report them as one.
13.6 Session authentication
The link is the session. AUTH is an ordinary request on the same path, and a server keys its session state on the link identifier, discarding it when the link closes. FETCH and DELETE on a link that has not authenticated return status 4, as on the other transport.
The bodies of AUTH (§4) and REGISTER (§6.1) do not change. Only the two values those signatures borrow from the Noise handshake are substituted:
h = SHA-256("smolmail/1 bind" || destination || link_id) # replaces the Noise handshake hash
server_static = SHA-256("smolmail/1 bind" || destination) # replaces the Noise static key
destination is the 16-byte hash the client dialled and link_id the 16-byte link identifier both ends derive from the link request. Both substitutes are 32 bytes, which keeps the concatenations of §4 and §6.1 as unambiguous as they already are -- neither field is length-prefixed, so a substitute of another width would let one transport's signature be reinterpreted under the other.
What each half buys:
destinationbinds the signature to this server, so a malicious server cannot replay a client'sAUTHorREGISTERelsewhere. This is whatserver_staticdoes in §6.1.link_idbinds it to this link. RNS derives the identifier from the initiator's per-link ephemeral keys, so a client MUST establish a fresh link per session and MUST NOT reuse one across authentications.- Unlike the Noise handshake hash, a link identifier is not secret: it addresses every packet on the link and every transit node sees it. The argument therefore rests on the signature travelling inside the encrypted link and on the binding being unique per server and link, not on the binding being unguessable.
13.7 Sizes and budgets
This is where a mesh differs most. The 768 KiB envelope of §10 is over half an hour on a LoRa link at 391 bytes a second, and the 512 KiB FETCH budget suggested in §6.1 is worse. A server reachable over Reticulum SHOULD therefore run much smaller limits. Recommended defaults, all configurable:
32 KiB per envelope 32 KiB fetch budget
Nothing advertises these and nothing negotiates them. Status 7 and status 6 are the authoritative answer, and a client SHOULD remember a cap it learned that way rather than discover it twice. The FETCH budget is the server's alone -- a client never chooses it -- and a server MUST keep §6.1's guarantee of at least one record per response so that no single message can wedge a mailbox shut.
A mailbox reachable over both transports has one queue, and §6.1 requires a record to be returned whole, so an envelope accepted at the 768 KiB cap over TCP is still a half-hour fetch for a mesh client. Nothing breaks -- time is spent -- but an operator whose users are mesh-first SHOULD apply the smaller cap to the whole mailbox rather than per transport.
13.8 Abuse control without addresses
The per-IP connection and SEND limits of §10 have no analogue. Reticulum gives a server no stable handle on an initiator that does not identify itself, and that is the point: a sender is absent from the wire format by construction (§5.2), and a transport that reintroduced a durable sender identifier would undo it.
- A server MUST NOT require Reticulum link identification for any operation. It proves a Reticulum identity rather than a Smol Mail one, and a durable one is exactly the handle this section says a server must not have.
- A server SHOULD limit requests and bytes per link, and cap concurrent links. Reticulum enforces neither.
- A server SHOULD keep a global
SENDceiling in place of the per-peer one. - The per-accept-token
SENDlimit of §10 is unchanged and becomes the main defence. It never needed a peer identity: a token is issued by the mailbox owner (§5.8), which is exactly the handle this transport still has.
Reticulum's own blackholing reaches identified peers only, so it is unavailable here. Its interface-level ingress control and a server's ability to stop accepting links remain as load-shedding measures.
13.9 Privacy over Reticulum
§9 holds, with three amendments.
A server no longer learns a connecting IP address. It learns an ephemeral link identifier and nothing else about who is talking to it, which is strictly better for senders than the transport of §4. It still learns which mailbox each envelope is for, its size, and when it arrived, was fetched and was deleted.
A transit node learns the server's destination hash when a link is requested, and after that only link identifiers, packet sizes and timing. It learns neither the mailbox nor anything about an envelope. Traffic analysis, delivery timing and mailbox size remain unprotected, as in §9.
An announced destination is discoverable network-wide, which is a new exposure for an operator rather than a user; §13.1 leaves announcing optional for that reason. The cost of all this is in §13.8: the operator trades per-IP abuse control for it. Where §9 recommends a Tor onion service for metadata resistance, this transport is a second answer.
13.10 Queueing, retries and liveness
- The outbound queue lives in the sending client, as §6 already requires. A client retries when a path becomes available.
- Retrying a
SENDwhose outcome is unknown is safe without any new machinery: the identifier is derived (§5.4) and the server answers a resend with the same identifier and no new message (§10). This matters more here, because Reticulum has many more ways to fail ambiguously than TCP does. - A client SHOULD tear a link down when it is done rather than hold it open on keepalives, which run for minutes.
14. Across transports
One store, one set of username bindings, rotation chains, accept tokens and queues. Identifiers agree because they are derived from the envelope, so the same message delivered by either route deduplicates against itself. A username registered over one transport is reachable over the other, and AUTH is per-session, so a client may register over one and authenticate over the other.
A 1.1 client and a 1.2 server interoperate over TCP with no negotiation, because there is nothing to negotiate. The implementation is an adapter around the existing operation dispatch: the same request bodies, the same handlers, the same database. Neither LXMF nor any other Reticulum messaging format is involved.
License
© 2026 randogoth. This protocol definition is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license (CC BY-SA 4.0). The reference implementations that accompany it are separate works and are not covered by this license.