feat: revise to protocol 1.1 with accept tokens, fetch cursors and 32-byte ids

This commit is contained in:
randogoth 2026-09-27 08:27:08 +03:00
parent 56a6ed1186
commit 71f5f628be
4 changed files with 712 additions and 220 deletions

View file

@ -4,7 +4,7 @@ A minimalist, decentralized, 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.
A server learns which mailbox a message is for, how big it is and when it arrived. Nothing else — not the sender, not the subject, not the content.
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.
- **Minimal** — five operations, four primitives, no extensibility mechanisms.
- **Private** — messages are sealed and signed; senders are absent from the wire format.
@ -13,10 +13,12 @@ A server learns which mailbox a message is for, how big it is and when it arrive
## Identity
Identity is one Ed25519 keypair. The 32-byte public key *is* the identity; the 32-byte seed is the only secret to back up. Encryption keys are derived from the same keypair, so there is exactly one thing to hold, move between servers, print on paper or scan from a screen.
The only secret is a 32-byte master. Back that up and nothing else: the Ed25519 signing key, the encryption key derived from it, and the key that issues accept tokens all come out of it. One thing to hold, move between servers, print on paper or scan from a screen.
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.
## Addressing
```
@ -26,10 +28,18 @@ 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.
Keys learned from a server are pinned on first use. Later changes need a rotation certificate signed by the previous key, or explicit confirmation.
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
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.
@ -40,7 +50,7 @@ Traffic analysis, delivery timing, mailbox size, and whether a user has an accou
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 excludes attachments, group messaging, federation, anonymous routing, multi-device synchronisation and key revocation.
Version 1.1 excludes attachments, group messaging, federation, anonymous routing, multi-device synchronisation and key revocation.
## Reference server
@ -53,7 +63,7 @@ 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.
`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.
`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.
## Reference client
@ -72,10 +82,12 @@ 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.
`import` adds a contact from a `smol://` address without trusting any server, `contacts` lists known keys and how each was learned, and `rotate` replaces the identity with a signed certificate 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.
Mail is stored sealed and opened on demand, so the local database holds no plaintext. Superseded keys are retained after a rotation, because mail already addressed to them is readable with nothing else.
`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.
Mail is stored sealed and opened on demand, so the local database holds no plaintext.
## Specification
[SPEC.md](SPEC.md) — wire format, operations, trust model and conformance.
[SPEC.md](SPEC.md) — wire format, operations and trust model.