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.
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.
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.
- 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 `RESOLVE` results from that session as unverified and mark them as such.
- 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 is acceptable for `SEND` when 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.
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.
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.
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 `time` is more than 86400 seconds ahead of its own clock, and MAY mark a larger backwards skew.
- 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.
- Senders MAY pad the plaintext to a multiple of 1024 bytes to blur message length.
- Receivers MUST ignore trailing bytes. `body_len` makes padding unambiguous, and the AEAD tag protects it from alteration.
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.
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.
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.
- 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).
-`t` is 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 reserved `Accept` frontmatter field, base32, inside a sealed and signed payload.
- A receiver MUST ignore an `Accept` value 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-To` the 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).
-`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 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.
`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.
`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 `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`.
- 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.
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.
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. 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.
### 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@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.
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`/`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.
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 `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.
What each half buys:
-`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.
### 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 `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.
### 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 `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.
## 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.