Merge pull request 'add-ai-declaration' (#2) from add-ai-declaration into main

Reviewed-on: #2
This commit is contained in:
randogoth 2026-09-29 20:45:37 +00:00
commit 8f576d6bcc
5 changed files with 141 additions and 233 deletions

View file

@ -1,6 +1,6 @@
--- ---
version: "0.1.2" version: "0.1.2"
level: auto level: pair
processes: processes:
design: assist design: assist
implementation: auto implementation: auto
@ -21,3 +21,8 @@ This format is based on [AI-DECLARATION.md](https://ai-declaration.md/en/0.1.2).
## Notes ## Notes
- The primary agent is Mistral Vibe. Claude Code was consulted for reviews and expertise.
- The design thinking is human. SPEC.md's ideas and rulings are the author's.
- The agent is involved as technical consultant and interactive encyclopedia.
- The agent acts on the document only on instruction, for structural integrity, prose recasts, technical clarifications, executed under explicit human decisions.
- The code reference is developed by the agent autonomously as asked by the author: to test the integrity and completeness the agent writes and refactors against the protocol, and tests the implementations until they match the author's expectations.

60
FAQ.md
View file

@ -1,67 +1,31 @@
# FAQ # FAQ
Answers to questions that come up but aren't spelled out elsewhere. Answers to questions that come up but are not spelled out elsewhere.
## Why can't I just click a link to add a mail server, the way I can add a contact? ## Why can't I just click a link to add a mail server, the way I can add a contact?
Adding a contact via a `smol://` link is safe to get wrong: worst case, you Adding a contact through a `smol://` link is safe to get wrong. The worst case is that you add the wrong person, and nobody can intercept your messages because of it.
add the wrong person, but nobody can intercept your messages because of it.
Trusting a mail server is a bigger deal. Once you trust a server, it's Trusting a mail server carries more. Once you trust a server, it is allowed to act on your behalf: register your address, hand you your mail, delete it. A server therefore has to reach you through a channel you actually control, such as someone telling you in person, a message from someone you already know, or a company handing you the address directly. A clickable link is not such a channel. If server trust worked via links, anyone could send you one that quietly switches which server you trust, and you would have no way to tell.
allowed to act on your behalf — register your address, hand you your mail,
delete it. Because of that, a server has to be trusted through some channel
you actually control — someone telling you in person, a message from
someone you already know, a company handing you the address directly — not
a link that could show up in an email or a random webpage. If server trust
worked via clickable links, anyone could send you a link that quietly
switches which server you trust, and you'd have no way to tell.
So adding a server is a manual, deliberate step, on purpose — never a link Adding a server is a manual, deliberate step. It is never a link you can be tricked into clicking.
you can be tricked into clicking.
## Why does Smol Mail not have native WebSocket support? ## Why does Smol Mail not have native WebSocket support?
It does — but kept separate, as an optional add-on ([WS.md](WS.md)), not It does, kept separate as an optional annex ([WS.md](WS.md)) rather than built into the core protocol.
built into the core mail protocol. That's deliberate.
The core protocol talks over a plain, minimal connection with nothing extra The core protocol talks over a plain, minimal connection with nothing attached: no certificates, no expiry dates, nothing to configure. A WebSocket is needed only because browsers cannot open that kind of connection directly, and because some networks pass only the traffic websites use. Baking WebSocket support into the core would mean baking in everything that comes with it, web certificates and browser security rules, none of which the mail protocol needs, and none of which takes part in how it decides what to trust. Trust is still the server's key, exactly as without WebSocket.
attached: no certificates, no expiry dates, nothing to configure. A
WebSocket is only needed because web browsers can't open that kind of plain
connection directly, and some networks only allow the kind of traffic
websites use. Baking WebSocket support into the core would mean baking in
everything that comes with it — web certificates, browser security rules —
none of which the mail protocol actually needs, and none of which is part
of how it decides what to trust (that's still just the server's key, exactly
as without WebSocket).
So instead, the WebSocket support is a small side process that sits next to The WebSocket support is instead a small side process that sits beside a mail server and passes the same data through unchanged. It holds no keys and does not need to understand mail. It is a pipe, not a participant, and a server that does not run it remains usable by every other client.
a mail server and simply passes the same data through unchanged. It doesn't
hold any keys and doesn't need to understand mail at all — it's a pipe, not
a participant. A server that doesn't run it is still perfectly usable by
every other client.
This mirrors how the separate delivery method over the Reticulum network This mirrors the delivery method over the Reticulum network (SPEC.md §13): an alternate way to reach a mailbox, kept outside the core so the core stays simple regardless of how people connect to it.
works too: an alternate way to reach a mailbox, kept outside the core
protocol so the core stays simple regardless of how people connect to it.
## Why do new smolmails always end up in my Requests folder? ## Why do new smolmails always end up in my Requests folder?
Because you haven't approved that person yet — that's what the Requests Because you have not approved that person yet. That is what the Requests folder is for.
folder is for.
A mail server can't see who's sending you mail, only that something A mail server cannot see who is sending you mail, only that something arrived. Without a way to tell someone you know apart from a stranger or spam, anyone who knew your address could flood your mailbox. Mail from anyone you have not approved therefore goes to a separate, smaller Requests folder with limited retention, instead of your main mailbox.
arrived. Without some way to tell "someone I know" apart from "a stranger
or spam," anyone who knew your address could flood your mailbox. So by
default, mail from anyone you haven't approved goes into a separate,
smaller Requests folder with limited retention, instead of your main
mailbox.
To fix this for someone, approve them — the reference client's `accept` To fix this for someone, approve them. The reference client's `accept` command does this in one step, and replying to them has the same effect. From then on their mail goes to your main mailbox, and withdrawing your approval with `block` sends them back to Requests.
command does this in one step, or simply replying to them has the same
effect. From then on, their mail goes straight to your main mailbox;
withdrawing your approval (`block`) sends them back to Requests.
This is on purpose and there's no way to skip it for a first message: your There is no way to skip this for a first message, and that is the point. Your server can tell approved senders apart from everyone else without ever learning who any of them are, and that is what keeps your mailbox both usable and private.
server can tell approved senders apart from everyone else without ever
learning who any of them actually are, which is what keeps your mailbox
both usable and private.

View file

@ -1,19 +1,16 @@
# Smol Mail # smol mail
[![AI-DECLARATION: auto](https://img.shields.io/badge/%E4%B7%BC%20AI--DECLARATION-auto-ede9fe?labelColor=ede9fe)](https://ai-declaration.md) [![AI-DECLARATION: auto](https://img.shields.io/badge/%E4%B7%BC%20AI--DECLARATION-auto-ede9fe?labelColor=ede9fe)](https://ai-declaration.md)
[![License: CC BY-SA 4.0](https://img.shields.io/badge/License-CC%20BY--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-sa/4.0/) [![License: CC BY-SA 4.0](https://img.shields.io/badge/License-CC%20BY--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-sa/4.0/)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ![Powered by Mistral](https://img.shields.io/badge/Powered_by-Mistral_AI-FA520F?logo=mistral-ai&logoColor=white)
A minimalist, decentralized, end-to-end encrypted mail protocol. A minimalist, decentralised, end-to-end encrypted mail protocol.
Email should be simple again. Inspired by [Spartan](spartan://spartan.mozz.us/), [Misfin](gemini://misfin.org/), and [LXMF](https://github.com/markqvist/lxmf). Smol Mail reduces electronic correspondence to its essentials: an address, a key and a message. One keypair per user, five operations, one binary message format. No sprawling infrastructure, no proprietary accounts, no advertising, no tracking. Anyone can run a server, anyone can write a client, and only the intended recipient can read a message. Online correspondence reduced to an address, a key and a message.
A server learns which mailbox a message is for, how big it is and when it arrived. Not the sender, not the subject, not the content. 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.
- **Minimal** — five operations, four primitives, no extensibility mechanisms. 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.
- **Private** — messages are sealed and signed; senders are absent from the wire format.
- **Decentralized** — independent servers, no federation, no directories.
- **Portable** — identity is a keypair, not an account.
## Identity ## Identity
@ -21,7 +18,7 @@ The only secret is a 32-byte master. Back that up and nothing else: the Ed25519
Signatures prove authorship. A throwaway key per message means a stolen identity key cannot decrypt anything already sent. Signatures prove authorship. A throwaway key per message means a stolen identity key cannot decrypt anything already sent.
Rotating a key advances an index rather than inventing an unrelated key, so the master alone can re-derive every key you have ever used — which is what makes mail addressed to a superseded key readable years later, with nothing archived. `restore` rebuilds a client from the master and a username. Rotating a key advances an index rather than inventing an unrelated key, so the master alone can re-derive every key you have ever used. That is what makes mail addressed to a superseded key readable years later, with nothing archived. `restore` rebuilds a client from the master and a username.
## Addressing ## Addressing
@ -32,29 +29,9 @@ smol://alice@example.org/mfrggzdfmztwq2lknnwg23tpo… self-certifying, carries
The short form is typeable. The long form carries the key itself, so an address shared by QR code, contact file or link needs no trust in any server at all. Either way, changing servers never changes an identity. The short form is typeable. The long form carries the key itself, so an address shared by QR code, contact file or link needs no trust in any server at all. Either way, changing servers never changes an identity.
Keys learned from a server are pinned on first use. Later changes need a rotation certificate signed by both the old and the new key, or explicit confirmation.
Recipients learn only the sender's key, never a name: a first contact arrives as a bare fingerprint until its address is known. A client may carry its full `smol://` address in a signed `Reply-To` field so first contacts can be answered; receivers bind it only when its key matches the message's signer (SPEC.md §5.7).
## A mailbox nobody else can fill
A server cannot see senders, so it cannot tell wanted mail from a flood, and without help anyone who knows an address could fill a mailbox to its quota.
An **accept token** fixes that without telling the server anything. It is a 32-byte secret you derive per correspondent, hand to them inside a sealed reply, and upload to your own server. Their messages carry a MAC over it; your server matches the MAC and puts those messages in your mailbox proper. Everything else — a first contact, a stranger, a flood — goes to a small **requests** tier with a short retention, which your client shows separately. Accepting someone is one command, or just replying to them; withdrawing the token puts them back in requests.
Your server learns from this how many correspondents you have accepted and which token a message matched, so a token is a stable pseudonym. It still never learns who is behind one (SPEC.md §5.8, §9).
## Cryptography ## Cryptography
Ed25519 · X25519 · ChaCha20-Poly1305 · SHA-256. Four established primitives, no novel cryptography, nothing else anywhere in the protocol. Transport is TCP with a Noise handshake — no certificates, no CA, no expiry. Ed25519 · X25519 · ChaCha20-Poly1305 · SHA-256. Four established primitives, no novel cryptography, nothing else anywhere in the protocol. Transport is TCP with a Noise handshake. No certificates, no CA, no expiry.
## What is not protected
Traffic analysis, delivery timing, mailbox size, and whether a user has an account on a given server. Run a server as a Tor onion service for metadata resistance; the transport needs no changes for it.
There is no forward secrecy on the recipient side: a seized recipient key decrypts ciphertext recorded while it was valid. Messages are signed, so they carry non-repudiation rather than deniability.
Version 1.1 excludes attachments, group messaging, federation, anonymous routing, multi-device synchronisation and key revocation.
## Reference server ## Reference server
@ -65,7 +42,7 @@ uv run smolmaild.py keygen
uv run smolmaild.py serve uv run smolmaild.py serve
``` ```
`keygen` writes the server's static key to `server.key` and prints its public key in base32. Publish that public key through a trusted channel — clients pin it, and a mismatch aborts the handshake. `keygen` writes the server's static key to `server.key` and prints its public key in base32. Publish that public key through a trusted channel. Clients pin it, and a mismatch aborts the handshake.
`serve` listens on `127.0.0.1:1961` by default and stores mail in `mail.db`. Use `--host 0.0.0.0` to accept remote connections, and `--invite-token` to close registration. `--help` lists the size, quota, retention and rate limits, including the separate `--requests-quota` for mail that arrives without an accept token. `serve` listens on `127.0.0.1:1961` by default and stores mail in `mail.db`. Use `--host 0.0.0.0` to accept remote connections, and `--invite-token` to close registration. `--help` lists the size, quota, retention and rate limits, including the separate `--requests-quota` for mail that arrives without an accept token.
@ -84,9 +61,9 @@ uv run smolmail.py list
uv run smolmail.py read <id> uv run smolmail.py read <id>
``` ```
Sent mail carries the sender's full `smol://` address in a signed `Reply-To` field (SPEC.md §5.7), so a first-time recipient can answer without an out-of-band channel; pass `--anonymous` to omit it. Sent mail carries the sender's full `smol://` address in a signed `Reply-To` field (SPEC.md §5.7), so a first-time recipient can answer without an out-of-band channel. Pass `--anonymous` to omit it.
`accept` admits a contact to your mailbox proper and pushes the token to your server at once; `block` withdraws it; `list --requests` shows what arrived without one. `import` adds a contact from a `smol://` address without trusting any server, `contacts` lists known keys and how each was learned, and `rotate` advances the identity with a certificate signed by both keys, which your contacts accept automatically. `accept` admits a contact to your mailbox proper and pushes the token to your server at once. `block` withdraws it. `list --requests` shows what arrived without one. `import` adds a contact from a `smol://` address without trusting any server, `contacts` lists known keys and how each was learned, and `rotate` advances the identity with a certificate signed by both keys, which your contacts accept automatically.
`fetch` deletes what it has verified and stored. `fetch --keep` leaves mail on the server and remembers where it stopped, and `--reset` pages through it again. `fetch` deletes what it has verified and stored. `fetch --keep` leaves mail on the server and remembers where it stopped, and `--reset` pages through it again.
@ -94,10 +71,10 @@ Mail is stored sealed and opened on demand, so the local database holds no plain
## Specification ## Specification
[SPEC.md](SPEC.md) — wire format, operations and trust model. [SPEC.md](SPEC.md) defines the wire format, the operations and the trust model.
## License ## License
The protocol definition — [SPEC.md](SPEC.md) — is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license ([CC BY-SA 4.0](LICENSES/CC-BY-SA-4.0.txt)). The protocol definition, [SPEC.md](SPEC.md), is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license ([CC BY-SA-4.0](LICENSES/CC-BY-SA-4.0.txt)).
The reference code — `smolmail.py`, `smolmaild.py`, `smolmail_rns.py` and `smolmaild_rns.py` — is licensed under the Apache License 2.0 ([Apache-2.0](LICENSES/Apache-2.0.txt)). The reference code, `smolmail.py`, `smolmaild.py`, `smolmail_rns.py` and `smolmaild_rns.py`, is licensed under the Apache License 2.0 ([Apache-2.0](LICENSES/Apache-2.0.txt)).

220
SPEC.md
View file

@ -2,44 +2,35 @@
# Smol Mail Protocol, version 1.2 # Smol Mail Protocol, version 1.2
A minimalist, decentral, end-to-end encrypted mail protocol. A minimalist, decentralised, end-to-end encrypted mail protocol.
Online correspondence reduced to an address, a key and a message. Online correspondence reduced to an address, a key and a message.
- Five operations, four primitives, zero extensibility mechanisms. 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.
- 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. 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 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 ## 1. Primitives
- Ed25519 (RFC 8032) - Ed25519 (RFC 8032)
- X25519 (RFC 7748) - X25519 (RFC 7748)
- ChaCha20-Poly1305 (RFC 8439) - ChaCha20-Poly1305 (RFC 8439)
- SHA-256 / HMAC-SHA256 (RFC 2104) anHKDF-SHA256 (RFC 5869) - SHA-256 / HMAC-SHA256 (RFC 2104) and HKDF-SHA256 (RFC 5869)
- Plus a Noise pattern (§4) as a construction over these four. 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.
- Integers are big-endian. Text is UTF-8.
- Every hash, signature and MAC is domain-separated by a label from §11.
## 2. Identity ## 2. Identity
- The only secret a user holds is a **32-byte master**. 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:
- 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 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) 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. `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 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. 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.
X25519 keys for key agreement come from the same keypair: X25519 keys for key agreement come from the same keypair:
@ -48,14 +39,17 @@ 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 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. 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.
- Implementations MUST reject an all-zero X25519 output and MUST reject a received ephemeral public key that is a low-order point. The map admits two degenerate inputs:
- Implementations MUST reject an all-zero X25519 output.
- Implementations MUST reject a received ephemeral public key that is a low-order point.
## 3. Addressing ## 3. Addressing
`alice@example.org[:1961]` -- short form, resolved via the server `alice@example.org[:1961]`: short form, resolved via the server
`smol://alice@example.org/mfrggzdfzt...` -- self-certifying, carries its own key `smol://alice@example.org/mfrggzdfzt...`: self-certifying, carries its own key
- Usernames are 1-63 bytes of `[a-z0-9._-]`. - Usernames are 1-63 bytes of `[a-z0-9._-]`.
- A username MUST begin and end with `[a-z0-9]`. - A username MUST begin and end with `[a-z0-9]`.
@ -64,13 +58,13 @@ These are libsodium's `crypto_sign_ed25519_sk_to_curve25519` and `..._pk_to_curv
- Clients normalise to lowercase before sending. - Clients normalise to lowercase before sending.
- The default TCP port is **1961**. - 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. 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, 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. 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.
## 4. Transport ## 4. Transport
TCP with a **Noise_NX_25519_ChaChaPoly_SHA256** handshake, prologue `smolmail/1`. One round trip. No certificates, no CA, no expiry. 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 NX pattern has the server transmit its static public key during the handshake, so pinning is a single code path: The NX pattern has the server transmit its static public key during the handshake, so pinning is a single code path:
@ -78,18 +72,15 @@ The NX pattern has the server transmit its static public key during the handshak
- On mismatch the client MUST abort and surface the discrepancy. - 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. - 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. 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.
- 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.
### Framing ### Framing
- Each Noise message carries a 2-byte length prefix and is at most 65535 bytes. 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.
- 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 ### Session authentication
- `FETCH` and `DELETE` require one `AUTH` frame: an application frame of type `0x00` -- after the handshake: `FETCH` and `DELETE` require one `AUTH` frame, an application frame of type `0x00`, after the handshake:
``` ```
username_len u8 || username || identity 32 || signature 64 username_len u8 || username || identity 32 || signature 64
@ -97,12 +88,14 @@ username_len u8 || username || identity 32 || signature 64
signature = Ed25519 over "smolmail/1 auth" || h # h = Noise handshake hash 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 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 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. Message format
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.
### 5.1 Envelope: what the server stores cleartext ### 5.1 Envelope: what the server stores cleartext
``` ```
@ -114,16 +107,9 @@ ciphertext n
tag 16 Poly1305 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). 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.
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, 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.
- 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 ### 5.2 Sealing
@ -137,16 +123,9 @@ key = HKDF-SHA256(shared, salt = epk || recipient_identity,
wipe(esk) wipe(esk)
``` ```
The recipient recovers the same key with `X25519(own_x_priv, epk)`. 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's long-term key signs only and never participates in key agreement. 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.)
- 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 ### 5.3 Sealed payload
@ -160,21 +139,21 @@ signature 64 Ed25519 by sender
padding 0..n ignored 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. 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.
- 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. - 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. - 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. - Receivers MUST ignore trailing bytes. `body_len` makes padding unambiguous, and the AEAD tag protects it from alteration.
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.
### 5.4 Message identifier ### 5.4 Message identifier
``` ```
id = SHA-256("smolmail/1 id" || envelope) 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. 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.
- It is used whole, nothing truncates it.
### 5.5 Body frontmatter ### 5.5 Body frontmatter
@ -193,7 +172,7 @@ Body text starts here.
- Key is 1-64 bytes of `[A-Za-z0-9-]`. - Key is 1-64 bytes of `[A-Za-z0-9-]`.
- Value is the remainder of the line after `:`, trimmed of surrounding spaces. - Value is the remainder of the line after `:`, trimmed of surrounding spaces.
- No nesting, arrays, multi-line values, quoting, comments or type coercion. - No nesting, arrays, multi-line values, quoting, comments or type coercion.
- Keys are compared case-insensitively, so `Subject` and `subject` are the same key - Keys are compared case-insensitively, so `Subject` and `subject` are the same key.
- Where a key repeats, the first occurrence wins. - Where a key repeats, the first occurrence wins.
- Maximum 4 KiB and 64 keys. - Maximum 4 KiB and 64 keys.
- A malformed line invalidates the whole block, which is then displayed as ordinary body text. - A malformed line invalidates the whole block, which is then displayed as ordinary body text.
@ -201,56 +180,45 @@ Body text starts here.
- `Subject`, `In-Reply-To` (a message id as 64 lowercase hex characters), `Reply-To` (§5.7) and `Accept` (§5.8) are reserved. - `Subject`, `In-Reply-To` (a message id as 64 lowercase hex characters), `Reply-To` (§5.7) and `Accept` (§5.8) are reserved.
- Unknown keys MUST be preserved, MAY be displayed, and MUST NOT alter client behaviour. - 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. - 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. 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 ### 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. 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.
### 5.7 Sender addresses ### 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. 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: 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 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).
- It is a first-contact aid, not a trust upgrade.
### 5.8 Accept tokens ### 5.8 Accept tokens
- A server cannot see senders. 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.
- 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 t = HMAC-SHA256(accept, correspondent_identity) # accept: §2
mac = HMAC-SHA256(t, "smolmail/1 mac" || id) # id: §5.4 mac = HMAC-SHA256(t, "smolmail/1 mac" || id) # id: §5.4
``` ```
- The accept key never leaves the owner's device. 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.
- `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. Two rules bind the receiver, one about provenance and one about filing:
- 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. - 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. - 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: 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 token matches: the envelope enters the mailbox's main quota and retention. 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.
- 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. 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.
- 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.
## 6. Operations ## 6. Operations
The protocol's entire surface is five operations, each a single request and a single response.
| Type | Op | Auth | Request → response | | Type | Op | Auth | Request → response |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 0x01 | `RESOLVE` | no | username → identity key + rotation chain, possibly empty | | 0x01 | `RESOLVE` | no | username → identity key + rotation chain, possibly empty |
@ -261,19 +229,15 @@ A sender holding a token attaches `mac` to `SEND` (§6.1). The server recomputes
`AUTH` (§4) is session setup, not an operation. `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. `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, and deleting is a separate decision. `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. `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 ### 6.1 Request and response bodies
- A request body is the operation's payload. 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.
- 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.
``` ```
AUTH → username_len u8 || username || identity 32 || signature 64 AUTH → username_len u8 || username || identity 32 || signature 64
@ -307,9 +271,9 @@ signature = Ed25519 by identity over
"smolmail/1 register" || server_static || username || identity "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. `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. `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.
## 7. Key rotation ## 7. Key rotation
@ -318,30 +282,31 @@ 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 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. `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 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 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. - 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. - 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 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. - 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. - 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. 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.
## 8. Trust model ## 8. Trust model
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.
| Key source | Guarantee | | Key source | Guarantee |
| --- | --- | | --- | --- |
| Home server key from a trusted channel | Authenticated, pinned, mismatch detected | | Home server key from a trusted channel | Authenticated, pinned, mismatch detected |
| Contact key from `smol://` or a QR code | Strong; requires no server trust | | 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 | | 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 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 | | `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 with a valid chain | Accepted, surfaced in the interface |
| Key change without a chain | Rejected until the user confirms | | Key change without a chain | Rejected until the user confirms |
@ -349,19 +314,17 @@ both signatures over "smolmail/1 rotate" || username || old_pub || new_pub || ti
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. 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. 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. 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. 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. 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 ## 10. Servers
- Independent store-and-forward mailboxes that never communicate with one another. 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.
- 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: Every mailbox has two tiers (§5.8). Defaults, all configurable:
@ -373,7 +336,9 @@ Every mailbox has two tiers (§5.8). Defaults, all configurable:
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. 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. 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 ## 11. Domain separation labels
@ -400,11 +365,9 @@ A status is the first byte of every response body (§6.1), which MAY be followed
## 13. Reticulum transport ## 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. 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. 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.
- 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. 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.
@ -418,11 +381,12 @@ A server holds one **Reticulum identity**, persisted, separate from every Smol M
RNS.Destination(identity, IN, SINGLE, "smolmail", "server") 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. 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 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 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.
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 ### 13.2 Addressing
@ -439,13 +403,13 @@ The authority is the username of §3 and the server's destination hash as **32 l
- 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. - 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. - 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. 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 ### 13.3 Discovery
A client resolves a destination through Reticulum's own path 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. - 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. - 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. - 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.
@ -453,9 +417,9 @@ A client resolves a destination through Reticulum's own path discovery:
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. 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. §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 -- 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. 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.
### 13.5 Requests ### 13.5 Requests
@ -485,13 +449,11 @@ h = SHA-256("smolmail/1 bind" || destination || link_id) # replace
server_static = SHA-256("smolmail/1 bind" || destination) # replaces the Noise static key 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. `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` 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.
- `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. 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.
- `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 ### 13.7 Sizes and budgets
@ -501,9 +463,9 @@ This is where a mesh differs most. The 768 KiB envelope of §10 is over half an
32 KiB per envelope 32 KiB fetch budget 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. 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. 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 ### 13.8 Abuse control without addresses
@ -524,13 +486,11 @@ A server no longer learns a connecting IP address. It learns an ephemeral link i
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. 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. 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 ### 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. 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.
- 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 ## 14. Across transports

28
WS.md
View file

@ -1,18 +1,18 @@
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 --> <!-- SPDX-License-Identifier: CC-BY-SA-4.0 -->
# Smol Mail over WebSocket -- annex # Smol Mail over WebSocket annex
This annex is not part of the protocol. It adds no version, no section, no address form and no label, and a server that ignores it is fully conformant. It records one convention for carrying the transport of §4 through a WebSocket, so that browsers, which cannot open TCP sockets, and clients on networks that pass only port 443 can reach a mailbox, and so that independent implementations of that carriage agree. The keywords below bind only an implementation that claims to follow this annex. Section references are to [SPEC.md](SPEC.md). This annex is not part of the protocol. It adds no version, no section, no address form and no label, and a server that ignores it is fully conformant. It records one convention for carrying the transport of §4 through a WebSocket, so that browsers, which cannot open TCP sockets, and clients on networks that pass only port 443 can reach a mailbox, and so that independent implementations of that carriage agree. The keywords below bind only an implementation that claims to follow this annex. Section references are to [SPEC.md](SPEC.md).
## Why an annex and not a transport ## Why an annex and not a transport
Reticulum (§13) replaced the transport and gave the protocol a mailbox that works without the internet. A WebSocket replaces nothing: it is a pipe around §4, and everything it needs -- HTTP, TLS, WebPKI certificates, Origin policy -- is what §4 was written to do without. Keeping that in a separate process keeps it out of the mailbox, whose pre-authentication surface stays `length u32 || type u8`. Reticulum (§13) replaced the transport and gave the protocol a mailbox that works without the internet. A WebSocket replaces nothing: it is a pipe around §4, and everything it demands, from HTTP and TLS to WebPKI certificates and Origin policy, is what §4 was written to do without. Keeping that in a separate process keeps it out of the mailbox, whose pre-authentication surface stays `length u32 || type u8`.
## Carriage ## Carriage
- The WebSocket carries the exact byte stream of §4: the Noise_NX handshake, the 2-byte Noise length prefixes and the application frames, unchanged in both directions. The WebSocket carries the exact byte stream of §4, the Noise_NX handshake, the 2-byte Noise length prefixes and the application frames, unchanged in both directions. Bytes travel in binary messages. Message boundaries carry no meaning and are not aligned to Noise messages, and a receiver MUST treat the messages as one continuous stream.
- Bytes travel in binary messages. Message boundaries carry no meaning and are not aligned to Noise messages; a receiver MUST treat the messages as one continuous stream.
- A sender MUST NOT emit a WebSocket message larger than 64 KiB, and a receiver MAY close on one that is; a full Noise message with its prefix is 65537 bytes, one over, so a full one does not fit. - A sender MUST NOT emit a WebSocket message larger than 64 KiB, and a receiver MAY close on one that is. A full Noise message with its prefix is 65537 bytes, one over, so a full one does not fit.
- A text message is an error, and the receiver MUST close the connection. - A text message is an error, and the receiver MUST close the connection.
- WebSocket close codes carry no meaning. Every status is a §12 byte inside the Noise channel. A failed upgrade, a refused origin and a dropped socket are local errors, and a client MUST NOT report them as a status. - WebSocket close codes carry no meaning. Every status is a §12 byte inside the Noise channel. A failed upgrade, a refused origin and a dropped socket are local errors, and a client MUST NOT report them as a status.
@ -23,12 +23,12 @@ alice@example.org → wss://example.org/smol
alice@example.org:1962 → wss://example.org/smol/1962 alice@example.org:1962 → wss://example.org/smol/1962
``` ```
- Addresses are those of §3, unchanged. This is a second way to dial a `user@host` address, not a second kind of address. The key in a `smol://` path plays no part in the dial. Addresses are those of §3, unchanged. This is a second way to dial a `user@host` address, not a second kind of address, and the key in a `smol://` path plays no part in the dial. The port in an address is the server's TCP port and selects the path, and the WebSocket itself is on 443. Nothing advertises that a host offers this endpoint. A client learns by trying.
- The port in an address is the server's TCP port and selects the path. The WebSocket itself is on 443.
- A client MUST dial `/smol` for an address that names port 1961 explicitly, so that each upstream has one path. - A client MUST dial `/smol` for an address that names port 1961 explicitly, so that each upstream has one path.
- The client MUST offer the subprotocol `smolmail.1` and the endpoint MUST select it. A client MUST abort when the response selects anything else or nothing: RFC 6455 lets a handshake succeed with no subprotocol, and the peer is then not known to speak this one. - The client MUST offer the subprotocol `smolmail.1` and the endpoint MUST select it. A client MUST abort when the response selects anything else or nothing: RFC 6455 lets a handshake succeed with no subprotocol, and the peer is then not known to speak this one.
- Nothing advertises that a host offers this endpoint. A client learns by trying.
- The endpoint is `wss://` because a browser refuses `ws://` from an `https://` page and because TLS on 443 is what restrictive networks pass, not because TLS protects anything: §4 is plain TCP and the Noise channel supplies the confidentiality on either. `ws://` MAY be used where neither reason applies, such as loopback and onion services. The endpoint is `wss://` because a browser refuses `ws://` from an `https://` page and because TLS on 443 is what restrictive networks pass, not because TLS protects anything. §4 is plain TCP and the Noise channel supplies the confidentiality on either. `ws://` MAY be used where neither reason applies, such as loopback and onion services.
## Trust ## Trust
@ -40,14 +40,16 @@ Because the endpoint is a pipe to the same server, the static key, the handshake
The endpoint is a process beside the server, behind a TLS-terminating reverse proxy. The endpoint is a process beside the server, behind a TLS-terminating reverse proxy.
- It MUST NOT relay to an upstream the operator did not configure. The path selects among configured upstreams and does nothing else; a path that names none is refused, and a host is never taken from the request. - It MUST NOT relay to an upstream the operator did not configure. The path selects among configured upstreams and does nothing else. A path that names none is refused, and a host is never taken from the request.
- It holds no keys and parses nothing past the WebSocket framing. - It holds no keys and parses nothing past the WebSocket framing.
- It admits any `Origin` by default, because a browser client hosted elsewhere has to be able to deliver here. An operator MAY restrict origins at the price of refusing those senders, but an `Origin` restriction is not access control: only browsers send the header truthfully, it stops a page the operator did not approve and nothing more, and a connection with no `Origin` header is admitted. - It admits any `Origin` by default, because a browser client hosted elsewhere has to be able to deliver here. An operator MAY restrict origins at the price of refusing those senders, but an `Origin` restriction is not access control: only browsers send the header truthfully, it stops a page the operator did not approve and nothing more, and a connection with no `Origin` header is admitted.
- It SHOULD NOT listen where the TCP port would not. A sidecar on loopback or a private network is reachable from every page the local browser opens, which the TCP port never was. - It SHOULD NOT listen where the TCP port would not. A sidecar on loopback or a private network is reachable from every page the local browser opens, which the TCP port never was.
## Limits and privacy ## Limits and privacy
The server sees every connection arrive from the sidecar's address, so the per-IP limits of §10 see one peer; the sidecar or the proxy SHOULD enforce them in its place, reading the client address from a proxy header only when the proxy is one the operator configured, and the server's own limits have to be raised to carry the sidecar's whole traffic. The per-accept-token `SEND` limit is unaffected. §9 holds with one change: the connecting IP address is learned by the proxy and the sidecar rather than by the server. All three belong to the same operator, so the operator learns what it did before. A network observer sees TLS to port 443 and the host name instead of a connection to port 1961. The server sees every connection arrive from the sidecar's address, so the per-IP limits of §10 see one peer. The sidecar or the proxy SHOULD enforce them in its place, reading the client address from a proxy header only when the proxy is one the operator configured, and the server's own limits have to be raised to carry the sidecar's whole traffic. The per-accept-token `SEND` limit is unaffected.
§9 holds with one change: the connecting IP address is learned by the proxy and the sidecar rather than by the server. All three belong to the same operator, so the operator learns what it did before. A network observer sees TLS to port 443 and the host name instead of a connection to port 1961.
## Clients ## Clients
@ -58,9 +60,9 @@ The server sees every connection arrive from the sidecar's address, so the per-I
## Relays ## Relays
Most servers are TCP only, so a browser client reaches most hosts through a relay: a process run for the client rather than for a mailbox, which takes the host and port from the request and pipes the same byte stream to them over TCP. It is the opposite role to a sidecar, and the rule against unconfigured upstreams does not apply to it. The carriage is the one above; the request form is not specified here, because a relay serves the pages of its own origin and the two need agree only with each other. Most servers are TCP only, so a browser client reaches most hosts through a relay: a process run for the client rather than for a mailbox, which takes the host and port from the request and pipes the same byte stream to them over TCP. It is the opposite role to a sidecar, and the rule against unconfigured upstreams does not apply to it. The carriage is the one above. The request form is not specified here, because a relay serves the pages of its own origin and the two need agree only with each other.
A relay sees ciphertext only when the host is pinned, and a relay that tampers then produces a failed handshake or a pin mismatch. Against an unpinned host a relay can complete the handshake itself and present its own key, with no failure to notice: the first-contact interception §4 already describes, whose rules are the defence -- the session is unverified, MUST be shown as such, and MUST NOT carry `REGISTER`, `FETCH` or `DELETE`. A relay does learn the client's IP address and every host the client contacts, and the server learns the relay's address in place of the client's, so a client SHOULD use only a relay run by a party it would trust with that list; for a browser client that is the origin serving its code, which it already trusts with the master. Bounding a relay as a proxy is its operator's hardening and outside this annex. A relay sees ciphertext only when the host is pinned, and a relay that tampers then produces a failed handshake or a pin mismatch. Against an unpinned host a relay can complete the handshake itself and present its own key, with no failure to notice. This is the first-contact interception §4 already describes, and its rules are the defence: the session is unverified, MUST be shown as such, and MUST NOT carry `REGISTER`, `FETCH` or `DELETE`. A relay does learn the client's IP address and every host the client contacts, and the server learns the relay's address in place of the client's. A client SHOULD use only a relay run by a party it would trust with that list. For a browser client that is the origin serving its code, which it already trusts with the master. Bounding a relay as a proxy is its operator's hardening and outside this annex.
## Status ## Status