feat: bake a deploy-time server pin into the client via presetServer

This commit is contained in:
randogoth 2026-09-26 20:00:12 +03:00
parent db83ec1289
commit 4075afbb11
5 changed files with 93 additions and 6 deletions

View file

@ -12,8 +12,6 @@ browser (all crypto, all keys) ⇄ bridge.py (dumb byte pipe) ⇄ smolmaild
`bridge.py` serves the web app and relays WebSocket bytes to the server untouched. It holds no keys, never parses a byte of traffic, and relays only to allowlisted ports (`--allow-port`, default 1961), so it is not an open proxy. It also relays only for upgrades whose `Origin` is its own page — WebSockets are exempt from CORS, so without that check any page the user happened to visit could drive the relay. `--allow-origin` admits others, and an upgrade carrying no `Origin` at all, which a browser never sends, is left alone for non-browser clients. The Noise session, sealing, signing, pinning and trust-on-first-use all run in the browser's JavaScript — the trust boundary is the machine running the browser, as with the CLI client's `identity.key` file.
The bridge exists only because the protocol is TCP, and it is the smaller dependency it looks like: `devbox run test` already needs Python and `uv` for the reference vectors and the end-to-end run. What would retire it altogether is a WebSocket transport binding in `smolmaild` itself — Noise messages already carry a `u16` length prefix, so a binary WebSocket stream is a drop-in byte stream — but that is an addition to SPEC.md §4 and to the server, not something this client can decide.
## Run
```
@ -62,6 +60,8 @@ For a NixOS host, `nixosModules.default` provides `services.gsmol-bridge`:
The service runs under `DynamicUser`, restarts on failure, and — like the standalone script — listens on `127.0.0.1` by default. `nginx.enable` puts a TLS-terminating reverse proxy in front rather than exposing the bridge directly: it has no TLS of its own, and a browser refuses a `ws://` connection from an `https://` page, so a real deployment needs the proxy regardless of `--allow-origin`. `openFirewall = true` is for exposing the bridge's port directly instead, which is not the recommended path (see "Notes and limits").
`services.gsmol-bridge.presetServer = { host, publicKey }` bakes a server's key into the pages this deployment serves, so a first-time visitor is pre-pinned instead of asked to copy a base32 key into settings by hand (§4). The value reaches the browser over this deployment's own TLS, so it only stands in for "from the operator, through a trusted channel" when the deployment operator and the smolmaild operator are the same trusted party — set it only for a server you operate or otherwise vouch for. It seeds the first run only: the browser stores it as an ordinary pin from then on, replaceable or removable in settings like any other.
## 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.
@ -79,6 +79,7 @@ web/js/noise.js Noise_NX_25519_ChaChaPoly_SHA256 initiator
web/js/proto.js addresses, seal/unseal, frontmatter, rotation, op framing
web/js/store.js localStorage + IndexedDB, sealed at rest
web/js/app.js the UI
web/js/config.js deploy-time server preset, empty by default (see "Deploy")
test/ vectors and end-to-end tests (below)
```