Known-answer tests for every part of the protocol that produces bytes: key derivation, sealed envelopes with their intermediate values, accept tokens, session and registration frames, rotation certificates, frontmatter parsing, and the inputs a conformant implementation must refuse. vectors.py derives every value by calling smolmail.py, so the vectors cannot drift from the code, and verifies each one from the opposite side before emitting it. --check regenerates and diffs against the committed files. Verified independently: a checker written against SPEC.md and libsodium alone, without importing the reference, reproduces all 81 values. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| LICENSES | ||
| .gitignore | ||
| AI-DECLARATION.md | ||
| FAQ.md | ||
| README.md | ||
| smolmail.py | ||
| smolmail_rns.py | ||
| smolmaild.py | ||
| smolmaild_rns.py | ||
| SPEC.md | ||
| vectors.json | ||
| VECTORS.md | ||
| vectors.py | ||
| WS.md | ||
smolmail
A minimalist, cypherpunk mail protocol. Fun, simple, decentralised, end-to-end encrypted.
Online correspondence reduced to an address, a key and a message.
Five operations, four primitives, zero extensibility mechanisms. One 32-byte master key as the only identity. No certificates, no CAs, no expiry: one round trip and the server's key pin are the entire trust model. A fresh key seals every message: untraceable, unforgeable, and safe by construction.
Anti-spam without censorship: quotas and tokens do the filtering, and servers never see a word. Clients talk directly to the recipient's server. Registration is bound to one server by proof-of-possession. Double-signed certificate chains move a username to a new key, and old keys keep receiving until every contact catches up.
Identity
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. 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
alice@example.org short form, resolved via the server
smol://alice@example.org/mfrggzdfmztwq2lknnwg23tpo… self-certifying, carries its own key
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.
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.
Reference code
The four Python files are a reference, not production software. They were written entirely by an AI agent from SPEC.md alone: if one agent can implement both ends of the protocol, across two transports, from the document and nothing else, the specification is coherent and complete enough to build from. The code favours clarity over hardening and carries simplifications a deployed server would not — see Audit. Treat it as an executable illustration of the spec.
Reference server
smolmaild.py is a complete server in one file. It declares its own dependencies inline, so there is nothing to install:
uv run smolmaild.py keygen
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, including the separate --requests-quota for mail that arrives without an accept token.
Reference client
smolmail.py is a complete client in one file, with the same inline dependencies:
uv run smolmail.py keygen
uv run smolmail.py trust example.org <server-key>
uv run smolmail.py register alice@example.org
uv run smolmail.py resolve bob@example.org
echo "hello" | uv run smolmail.py send bob@example.org --subject Hi
uv run smolmail.py fetch
uv run smolmail.py list
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.
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.
Mail is stored sealed and opened on demand, so the local database holds no plaintext.
Audit
A security review of the protocol and these reference implementations found no issues in the protocol itself: the trust model, sealing, key rotation, accept tokens and the two transports' authentication binding all hold up as specified. Two findings were implementation-level and specific to the reference code:
- The server's rate limiter uses a fixed window, which admits up to twice the configured rate across a window boundary. A production limiter should use a sliding window or token bucket.
- The TCP server runs one unbounded thread per connection, so a distributed connection flood can exhaust threads. A production server should cap concurrency, as the Reticulum server already does (SPEC.md §13.8).
Specification
SPEC.md defines the wire format, the operations and the trust model.
Test vectors
VECTORS.md is a set of known-answer tests for every part of the protocol that produces bytes: key derivation, sealed envelopes with their intermediate values, accept tokens, the session and registration frames, rotation certificates, frontmatter parsing, and the inputs a conformant implementation must refuse. vectors.json carries the same values for a test harness to load.
vectors.py generates both from smolmail.py, so they cannot drift from the code, and verifies every vector from the opposite side before emitting it. uv run vectors.py --check regenerates and compares against the committed files:
uv run vectors.py # write vectors.json and VECTORS.md
uv run vectors.py --check # verify the committed files still reproduce
Start from the unpadded envelope. If its seal_key matches and the envelope does not, the fault is in the payload layout; if x25519_shared already differs, it is in the §2 key map.
Inspiration
The smolmail project takes inspiration from Gemini Protocol, Misfin, and LXMF.
License
The protocol definition, SPEC.md, is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license (CC BY-SA-4.0).
The reference code, smolmail.py, smolmaild.py, smolmail_rns.py, smolmaild_rns.py and vectors.py, is licensed under the Apache License 2.0 (Apache-2.0). The vectors it generates, VECTORS.md and vectors.json, are licensed with the specification.