Five operations, four primitives, zero extensibility mechanisms. One 32-byte 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, and servers never see a word. Clients talk directly to the recipient's server. Registration is bound to one server by proof-of-possession. Double-signed certificate chains move a username to a new key, and old keys keep receiving until every contact catches up.
A Noise pattern (§4) adds a construction over these four rather than a fifth primitive. Integers are big-endian. Text is UTF-8. Every hash, signature and MAC is domain-separated by a label from §11.
`n` is the **rotation index**, counted from 0. `seed_n` is an Ed25519 seed, and its 32-byte public key *is* the identity at that index. Rotation (§7) advances `n`, and the accept key of §5.8 does not depend on the index and therefore survives every rotation. HKDF is one-way, so a compromised `seed_n` exposes neither the master nor any other index.
A client restoring from the master alone recovers everything else: 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 needs to be archived.
These are libsodium's `crypto_sign_ed25519_sk_to_curve25519` and `..._pk_to_curve25519`. Implementations SHOULD call those rather than reimplement the map. Using one key in both a signature scheme and a key-agreement scheme is a deliberate trade, mitigated by domain-separating every derivation (§11), and made explicitly to keep an identity at 32 bytes: one QR code, one spoken fingerprint.
The `smol://` path component is the 32-byte identity key in RFC 4648 base32, lowercase and unpadded, 52 characters. Because the address carries the key, it requires no trust in any server, and this 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. They are 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.
The transport carries every operation, and its only trust decision is which server sits at the other end. TCP with a **Noise_NX_25519_ChaChaPoly_SHA256** handshake, prologue `smolmail/1`. One round trip. No certificates, no CA, no expiry.
The trust a channel must earn depends on what the operation risks. A client MUST NOT `REGISTER`, `FETCH` or `DELETE` against a server whose static key it did not obtain from a trusted channel. An unpinned server remains acceptable for `SEND` when the sender already holds the recipient's identity key: the payload is sealed end-to-end, so the channel provides only confidentiality against a passive observer.
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, and nothing on the wire requires text parsing or delimiter scanning.
The server verifies the signature and that `identity` is the key bound to `username`. Because `h` incorporates the server's fresh ephemeral, the frame needs 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.
The server stores an envelope it cannot open, and the recipient opens it to find a signed payload that names its sender. This section defines the envelope, the sealing and the payload.
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 also serves as 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, and 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 200, an accept token 32 and its MAC 32.
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, so the sealing key is unique per message and 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.)
The signature covers `"smolmail/1 msg" || to || epk || version || sender || time || body_len || body`. Covering `to` prevents a recorded message from 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.
This `version` is 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.
The identifier is derived: a sender cannot choose it, and servers and clients alike can deduplicate without trusting anyone. It is used whole, and nothing truncates it.
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.
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.
Because `esk` is wiped, a sender cannot decrypt what it 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.
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-To` field holding its own full `smol://` address, and because the value sits inside the sealed, signed payload, 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.
A server cannot see senders, so 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.
The accept key never leaves the owner's device. Deriving `t` rather than choosing it at random is a MUST: the token is then a function of the master and the correspondent's identity alone. Each correspondent receives one distinct token, or tokens 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 reserved `Accept` frontmatter field, base32, inside a sealed and signed payload.
Two rules bind the receiver, one about provenance and one about filing:
- 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-To` the same payload signs for itself (§5.7). The token then keeps working across the issuer's rotations, and a rotation the receiver 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. When a token matches, the envelope enters the mailbox's main quota and retention. When no token matches, the MAC is malformed, or none is attached, the envelope enters the **requests** tier instead, a much smaller quota with a short retention (§10). `FETCH` marks 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: `SEND` succeeds either way, and cannot be used to test whether a token is still accepted. Removing a token from the set, an `AUTH` with `sync = 1` that 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.
`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 the 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. 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.
A request body is the operation's payload. A response body begins with `status (u8)` from §12: on `0` the operation's payload follows, on anything else an optional UTF-8 reason string. A response reuses the request's type byte. `AUTH` is type `0x00`, and its response carries a status and, on success, the number of accept tokens the server now holds.
`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. A server MUST return at least one record even when that record 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.
`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 prevents it from becoming 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 `RESOLVE` returned.
- 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 `SEND` addressed to any of them, and MUST return messages addressed to any of them on `FETCH`.
- 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.
- Clients MUST surface rotations in the interface rather than applying them invisibly.
Without retained keys, mail from a contact who has not yet seen a rotation is addressed to a superseded key and becomes unreachable. Advancing the rotation index rather than generating an unrelated key is what 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. Out-of-band re-verification is the only defence against a compromised identity key. There is no revocation mechanism.
Trust here is a property of routes, not of authorities. A key is as good as the channel it arrived by, and no channel can replace a binding a stronger channel made.
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 exposure: 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, under which the server can group a correspondent's messages 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.
Servers are independent store-and-forward mailboxes that never communicate with one another. Each holds its own username bindings, rotation chains, accept tokens and envelope queues, and envelopes persist until deleted or expired.
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.
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.
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, while 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](smolmail_rns.py) and [smolmaild_rns.py](smolmaild_rns.py) are a reference client and server for this transport, tested against Reticulum 1.5.4.
A server holds one **Reticulum identity**, persisted, separate from every smolmail 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.
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 the 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.
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@host` short 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 `:port` suffix 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 a `smol+rns://` address. Every check in §5.7 applies unchanged: the key in the URI MUST equal the payload's `sender`, and it MUST NOT replace a key already bound.
- Recall the identity for the hash, build an `OUT`/`SINGLE` destination 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.
§4's pinned and unpinned sessions therefore collapse into one question, where the address came from. 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, whether 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.
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 `AUTH` carrying 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
`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.
`destination` and `link_id` each buy something distinct. `destination` binds the signature to this server, so a malicious server cannot replay a client's `AUTH` or `REGISTER` elsewhere. This is what `server_static` does in §6.1. `link_id` binds 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.
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:
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, and a client never chooses it. 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.
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 smolmail 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 `SEND` ceiling in place of the per-peer one.
- The per-accept-token `SEND` limit 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.
The outbound queue lives in the sending client, as §6 already requires, and a client retries when a path becomes available. Retrying a `SEND` whose 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.
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.