From 9a31607648ba711f2ebe384841859c5ecf039ead Mon Sep 17 00:00:00 2001 From: randogoth Date: Sat, 26 Sep 2026 10:37:10 +0300 Subject: [PATCH] docs: specify response framing and superseded-key retention --- SPEC.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/SPEC.md b/SPEC.md index 22cc63c..51f86c2 100644 --- a/SPEC.md +++ b/SPEC.md @@ -54,7 +54,7 @@ A client MUST NOT `REGISTER`, `FETCH` or `DELETE` against a server whose static **Framing.** 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; nothing on the wire requires text parsing or delimiter scanning. -**Session authentication.** `FETCH` and `DELETE` require one `AUTH` frame after the handshake: +**Session authentication.** `FETCH` and `DELETE` require one `AUTH` frame — an application frame of type `0x00` — after the handshake: ``` username_len u8 || username || identity 32 || signature 64 @@ -157,6 +157,30 @@ Because `esk` is wiped, a sender cannot decrypt what they sent. To keep a Sent f `FETCH` has no cursors, flags or server-side read state; a client loops until it returns nothing. `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 + +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 no payload. + +``` +RESOLVE → username_len u8 || username + ← identity 32 || chain_len u8 || cert 136 × chain_len + +SEND → envelope + ← id 16 + +FETCH → (empty) + ← count u16 || ( id 16 || received_at i64 || env_len u32 || envelope ) × count + +DELETE → count u16 || id 16 × count + ← removed u16 + +REGISTER → username_len u8 || username || identity 32 + || token_len u8 || token || cert_len u8 || cert + ← (empty) +``` + +A zero length means the field is absent. `FETCH` returns records oldest first, up to a server byte budget which MUST be smaller than the maximum application frame; 512 KiB against the default 1 MiB frame is a reasonable choice. + ## 7. Key rotation ``` @@ -166,6 +190,8 @@ signature = Ed25519 by old_pub over "smolmail/1 rotate" || old_pub || new_pub || 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 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. 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`. Without this, mail from a contact who has not yet seen a rotation is addressed to a superseded key and becomes unreachable. + **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; out-of-band re-verification is the only defence against a compromised identity key. There is no revocation mechanism in version 1. ## 8. Trust model @@ -213,7 +239,7 @@ A conforming server is a single binary over an embedded key-value store. 2 bad version 5 auth failed 8 rate limited ``` -A response MAY carry a UTF-8 reason string. Clients MUST NOT parse it. +A status is the first byte of every response body (§6.1), which MAY be followed by a UTF-8 reason string. Clients MUST NOT parse the reason. ## 13. Out of scope for version 1