Compare commits
2 commits
06249ed244
...
56a6ed1186
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56a6ed1186 | ||
|
|
5bfb415830 |
3 changed files with 17 additions and 1 deletions
|
|
@ -28,6 +28,8 @@ The short form is typeable. The long form carries the key itself, so an address
|
|||
|
||||
Keys learned from a server are pinned on first use. Later changes need a rotation certificate signed by the previous 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).
|
||||
|
||||
## 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.
|
||||
|
|
@ -68,6 +70,8 @@ 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.
|
||||
|
||||
`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.
|
||||
|
||||
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.
|
||||
|
|
|
|||
9
SPEC.md
9
SPEC.md
|
|
@ -135,7 +135,7 @@ Body text starts here.
|
|||
|
||||
One `Key: value` per line. Key is 1–64 bytes of `[A-Za-z0-9-]`; value is the remainder of the line after `:`, trimmed of surrounding spaces. No nesting, arrays, multi-line values, quoting, comments or type coercion. Where a key repeats, the first occurrence wins. Maximum 4 KiB and 64 keys. A malformed line invalidates the whole block, which is then displayed as ordinary body text — frontmatter fails closed toward display, never toward silent discard.
|
||||
|
||||
`Subject` and `In-Reply-To` (a message id as 32 lowercase hex characters) are reserved. Unknown keys MUST be preserved, MAY be displayed, and MUST NOT alter client behaviour. A body whose text genuinely begins with `---` is escaped by emitting an empty block first.
|
||||
`Subject`, `In-Reply-To` (a message id as 32 lowercase hex characters) and `Reply-To` (§5.7) are reserved. Unknown keys MUST be preserved, MAY be displayed, and MUST NOT alter client behaviour. A body whose text genuinely begins with `---` is escaped by emitting an empty block first.
|
||||
|
||||
**This grammar is not YAML; do not use a YAML parser.** It is flat so that a correct parser is twenty dependency-free lines, in a code path that handles attacker-controlled input.
|
||||
|
||||
|
|
@ -143,6 +143,12 @@ One `Key: value` per line. Key is 1–64 bytes of `[A-Za-z0-9-]`; value is the r
|
|||
|
||||
Because `esk` is wiped, a sender cannot decrypt what they sent. To keep a Sent folder, a client seals a second copy of the payload to the sender's own identity key with a fresh ephemeral and stores it locally. This is a client convention and involves no server.
|
||||
|
||||
### 5.7 Sender addresses
|
||||
|
||||
Nothing on the wire names a sender. A receiver learns only the signing key of §5.3, so a first contact is identified by nothing but its fingerprint, and replying needs an address the sender supplied somewhere: out of band, or in the message itself.
|
||||
|
||||
Like the sent copies of §5.6 this is a client convention and involves no server: a sender MAY include a `Reply-To` field holding its own full `smol://` address. The value sits inside the sealed, signed payload, so the signature binds the claim. A receiver that acts on it MUST check that the key carried in the URI equals the message's `sender`, MUST treat anything else as ordinary text, and MUST NOT let it replace a key already bound to that address (§8) — it is a first-contact aid, not a trust upgrade.
|
||||
|
||||
## 6. Operations
|
||||
|
||||
| Type | Op | Auth | Request → response |
|
||||
|
|
@ -202,6 +208,7 @@ A client MUST likewise retain every private key it has rotated away from. Sealin
|
|||
| --- | --- |
|
||||
| Home server key from a trusted channel | Authenticated, pinned, mismatch detected |
|
||||
| Contact key from `smol://` or a QR code | Strong; requires no server trust |
|
||||
| Sender's `Reply-To` field, key equal to the signer (§5.7) | Strong; the sender's own signed claim, but never overrides a pinned key |
|
||||
| `RESOLVE` over a pinned session | Trust on first use, as good as that server |
|
||||
| `RESOLVE` over an unpinned session | Trust on first use, interceptable at first contact; MUST be shown as unverified |
|
||||
| Key change with a valid chain | Accepted, surfaced in the interface |
|
||||
|
|
|
|||
|
|
@ -680,6 +680,9 @@ def cmd_send(args: argparse.Namespace, store: Store) -> int:
|
|||
if not sep or not FM_KEY.match(key.strip()):
|
||||
raise SmolError(f"{raw!r} is not a valid `Key: value` header")
|
||||
fields.append((key.strip(), value.strip()))
|
||||
# §5.7: a signed reply address lets a first-time recipient answer us.
|
||||
if (account := store.account()) is not None and not args.anonymous:
|
||||
fields.append(("Reply-To", account.uri(me.pk)))
|
||||
|
||||
body = build_frontmatter(fields, text).encode()
|
||||
envelope = seal(me, recipient, body, pad=not args.no_pad)
|
||||
|
|
@ -828,6 +831,8 @@ COMMANDS = [
|
|||
(("--body",), {"help": "message text; read from stdin when omitted"}),
|
||||
(("--header",), {"action": "append", "metavar": "KEY:VALUE",
|
||||
"help": "extra frontmatter field; repeatable"}),
|
||||
(("--anonymous",), {"action": "store_true",
|
||||
"help": "omit the Reply-To field carrying this address (SPEC.md §5.7)"}),
|
||||
(("--no-pad",), {"action": "store_true", "help": "do not pad to 1 KiB"})]),
|
||||
("fetch", cmd_fetch, "retrieve, verify and acknowledge mail",
|
||||
[(("--keep",), {"action": "store_true", "help": "do not delete from the server"})]),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue