feat: adopt smolmail protocol 1.1 with accept tokens and leave-mail-on-server
This commit is contained in:
parent
eef7bf9b1d
commit
d6fa097626
11 changed files with 911 additions and 232 deletions
23
README.md
23
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# gsmol
|
||||
|
||||
An HTML5 browser client for [Smol Mail](../smolmail), the minimalist end-to-end encrypted mail protocol: one Ed25519 identity, five operations, Noise_NX transport, sealed and signed messages. Everything the reference CLI client does, in a browser tab.
|
||||
An HTML5 browser client for [Smol Mail](../smolmail), the minimalist end-to-end encrypted mail protocol: one 32-byte master secret, five operations, Noise_NX transport, sealed and signed messages, and accept tokens that sort correspondence from cold mail. Everything the reference CLI client does, in a browser tab.
|
||||
|
||||
## Why there is a bridge
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ browser (all crypto, all keys) ⇄ bridge.py (dumb byte pipe) ⇄ smolmaild
|
|||
devbox run serve
|
||||
```
|
||||
|
||||
Then open http://127.0.0.1:8096. First use: create an identity, back up the seed, pin your server's public key (from the operator, through a trusted channel), register an address, and fetch.
|
||||
Then open http://127.0.0.1:8096. First use: create an identity, back up the master, pin your server's public key (from the operator, through a trusted channel), register an address, and fetch.
|
||||
|
||||
For a server on a non-default port, start the bridge with `uv run bridge.py --allow-port <port>`.
|
||||
|
||||
|
|
@ -64,11 +64,13 @@ The service runs under `DynamicUser`, restarts on failure, and — like the stan
|
|||
|
||||
## What it implements
|
||||
|
||||
- **Identity** (§2): one 32-byte seed; create or restore; the X25519 agreement keys are derived from the Ed25519 keypair. The seed lives in localStorage, mail stays sealed in IndexedDB — the browser profile is the trust boundary.
|
||||
- **Identity** (§2): one 32-byte master secret; create or restore. Every rotation index's signing seed and the accept key are derived from it with HKDF, so the X25519 agreement keys and every superseded identity are re-derivable rather than archived. The master lives in localStorage, mail stays sealed in IndexedDB — the browser profile is the trust boundary.
|
||||
- **Addressing** (§3): short `user@host` and self-certifying `smol://` addresses, base32 fingerprints.
|
||||
- **Trust** (§4, §8): server keys pinned on first trust, mismatch aborts the handshake; `RESOLVE` over an unpinned session is marked unverified; registration and fetching require a pinned server.
|
||||
- **Mail** (§5): sealed and signed envelopes, 1 KiB padding, frontmatter bodies, sent copies sealed to self, fetch verifies id and signature before acknowledging — anything unreadable stays on the server. Outgoing mail carries a signed `Reply-To` frontmatter field with the sender's full `smol://` address (an "anonymous" checkbox omits it), so a first-time recipient can name the sender and answer; on receipt such a claim binds only when its key matches the message's signer, and never over an address already pinned to a different key.
|
||||
- **Rotation** (§7): rotate with a signed certificate; contacts accept the change from the chain; superseded seeds are kept, since mail sealed to them is readable with nothing else; key changes without a chain are surfaced, never applied silently.
|
||||
- **Trust** (§4, §8): server keys pinned on first trust, mismatch aborts the handshake; `RESOLVE` over an unpinned session is marked unverified; registration and fetching require a pinned server; `REGISTER` carries a proof-of-possession signature bound to the server's own key, so a binding cannot be replayed elsewhere.
|
||||
- **Mail** (§5): sealed and signed envelopes, 1 KiB padding, frontmatter bodies, sent copies sealed to self, fetch pages forward by cursor and verifies id and signature before acknowledging — anything unreadable stays on the server. Outgoing mail carries a signed `Reply-To` frontmatter field with the sender's full `smol://` address (an "anonymous" checkbox omits it), so a first-time recipient can name the sender and answer; on receipt such a claim binds only when its key matches the message's signer, and never over an address already pinned to a different key.
|
||||
- **Accept tokens** (§5.8): mail from an accepted correspondent lands in the inbox's main tier; everything else lands in a separate "requests" tier. Accepting or blocking a contact pushes the changed token set to the server immediately; a learned token is attached automatically to outgoing mail once a correspondent's own token has been received.
|
||||
- **Leave mail on server**: a settings toggle that has fetch page forward by cursor without acknowledging, so mail stays server-side until deleted from the reader — off by default, which acknowledges (and so deletes) fetched mail immediately, as before.
|
||||
- **Rotation** (§7): rotate to the next index's derived key with a certificate both the old and new keys sign, binding the change to this username; contacts accept it from the chain; every superseded key stays derivable from the master, since mail sealed to it is readable with nothing else; key changes without a chain are surfaced, never applied silently.
|
||||
|
||||
## Layout
|
||||
|
||||
|
|
@ -91,14 +93,15 @@ The crypto is pure JavaScript rather than WebCrypto so the exact same modules ru
|
|||
devbox run test
|
||||
```
|
||||
|
||||
1. `test/gen_vectors.py` regenerates `test/vectors.json` from the reference stack (hashlib/hmac, PyNaCl, noiseprotocol): hashes, AEAD, key agreement, signatures, the §2 conversions, a fixed-ephemeral envelope, and a deterministic Noise_NX transcript with internal states.
|
||||
2. `test/vectors.test.mjs` checks the JS implementation byte for byte against those vectors, plus protocol cases: frontmatter, addresses, rotation chains (accepted, broken, missing link, oversize).
|
||||
3. `test/e2e.py` runs a live `smolmaild.py` and drives the reference client (Alice) against this client's JS stack (Bob, headless via `test/client.mjs`): registration, both send directions, fetch with acknowledgment, and a rotation whose chain the reference client walks. Bob's reply travels through the bridge's WebSocket relay — the exact byte path the browser takes.
|
||||
1. `test/gen_vectors.py` regenerates `test/vectors.json` from the reference stack (hashlib/hmac, PyNaCl, noiseprotocol): hashes, AEAD, key agreement, signatures, the §2 conversions, master-derived rotation seeds and accept tokens, a fixed-ephemeral envelope, and a deterministic Noise_NX transcript with internal states.
|
||||
2. `test/vectors.test.mjs` checks the JS implementation byte for byte against those vectors, plus protocol cases: frontmatter (case-insensitive keys), addresses, and rotation chains bound to a username (accepted, broken, missing link, oversize, wrong username).
|
||||
3. `test/e2e.py` runs a live `smolmaild.py` and drives the reference client (Alice) against this client's JS stack (Bob, headless via `test/client.mjs`): registration, unsolicited mail landing in the requests tier, accepting a correspondent and the accept-token round trip that follows, fetch with acknowledgment, and a rotation whose chain the reference client walks. Bob's reply travels through the bridge's WebSocket relay — the exact byte path the browser takes.
|
||||
|
||||
What is not verified here: clicking through the actual browser UI. `devbox run serve` and a manual pass remain the last step; the logic under every button is what the tests exercise.
|
||||
|
||||
## Notes and limits
|
||||
|
||||
- The seed sits in localStorage: an XSS in the page or a compromised browser profile is game over, same as a stolen `identity.key` for the CLI client. The bridge serves a strict Content-Security-Policy (`default-src 'self'`, no inline script or style, no foreign origin) to raise that bar, but it is a mitigation and not a fix. No server push or notifications in v1 (SPEC.md §13); click fetch.
|
||||
- The master sits in localStorage: an XSS in the page or a compromised browser profile is game over, same as a stolen `identity.key` for the CLI client. The bridge serves a strict Content-Security-Policy (`default-src 'self'`, no inline script or style, no foreign origin) to raise that bar, but it is a mitigation and not a fix. No server push or notifications; click fetch.
|
||||
- Rotation is not revocation (§7): a stolen key can rotate onward and the chain validates. The UI surfaces rotations instead of applying them invisibly; out-of-band re-verification is the only defence.
|
||||
- The requests tier is a quota boundary the server enforces, not a filter: it does not refuse anyone's mail outright, which stays the reader's decision after decryption (§5.8).
|
||||
- The bridge binds to 127.0.0.1 by default and has no TLS of its own. Serving it beyond localhost directly is possible but unadvised: the relay is plaintext-agnostic (the traffic is already end-to-end encrypted, but the bridge would relay anyone's bytes to allowlisted ports). The NixOS module's `nginx.enable` puts a TLS-terminating reverse proxy in front instead, which is the deployment path "Deploy" above documents.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue