From aab98ff59862133e70fc2f1de119711bdb4324fc Mon Sep 17 00:00:00 2001 From: randogoth Date: Sat, 26 Sep 2026 16:36:12 +0300 Subject: [PATCH] feat: carry a signed Reply-To address for first contact --- README.md | 2 +- test/client.mjs | 3 +++ test/e2e.py | 4 ++++ web/index.html | 1 + web/js/app.js | 64 +++++++++++++++++++++++++++++++++++++++++++++---- web/style.css | 1 + 6 files changed, 69 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index de8925b..048ac57 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ For a server on a non-default port, start the bridge with `uv run bridge.py --al - **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. - **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. +- **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. ## Layout diff --git a/test/client.mjs b/test/client.mjs index 890b9b0..c443a91 100644 --- a/test/client.mjs +++ b/test/client.mjs @@ -201,6 +201,9 @@ const commands = { const fields = [["Subject", parseOption("--subject")], ["In-Reply-To", parseOption("--reply-to")]] .filter(([, v]) => v); + if (state.account && !rest.includes("--anonymous")) + fields.push(["Reply-To", proto.parseAddress(`${state.account.user}@${state.account.host}` + + (state.account.port === proto.DEFAULT_PORT ? "" : `:${state.account.port}`)).uri(me.publicKey)]); const body = utf8Bytes(proto.buildFrontmatter(fields, (parseOption("--body") || "") + "\n")); const envelope = proto.seal(me, recipient, body); const opened = await open(addr.host, addr.port, { requirePin: false }); diff --git a/test/e2e.py b/test/e2e.py index ca2e7ff..f6000b7 100644 --- a/test/e2e.py +++ b/test/e2e.py @@ -107,6 +107,10 @@ def main() -> int: alice_read = run(alice + ["read", reply_id[:8]]) check("alice reads bob's reply", "hello alice" in alice_read, alice_read) check("reference client verifies the JS signature", "signature verified" in alice_read) + # bob's reply carries his signed smol:// address; a receiver that does + # not know the field must still preserve and display it (SPEC.md §5.5) + check("reference client preserves the signed reply address", + "reply-to:smol://bob@" in alice_read, alice_read) # --- rotation: the JS client rotates, the reference client walks the chain bob_rotated = run(bob + ["rotate"]) diff --git a/web/index.html b/web/index.html index 43cc861..5367686 100644 --- a/web/index.html +++ b/web/index.html @@ -44,6 +44,7 @@