docs(roadmap): §8a done (mandatory pairing); split §8b into host+web / peer layers
ci / rust (push) Has been cancelled

§8a (require native pairing by default, serve --open) shipped + deployed. §8b
(delegated approval) refined into §8b-1 (host pending-requests + mgmt endpoints +
web Approve/Deny — achievable now) and §8b-2 (peer push to a paired Device A —
needs the native/Apple client UI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 10:28:39 +00:00
parent 9a6058cd20
commit 57f7e32c24
+22 -17
View File
@@ -139,23 +139,28 @@ it's unbuildable on the Linux dev box; the trait boundaries are already in the r
The unified host + web-console pairing (arm a window → display the host PIN → user enters it on the
client) is built and live. Two changes harden it from "works" to "secure by default":
- **Mandatory PIN pairing by default.** Today the punktfunk/1 host can run open (trust-on-first-use)
— *not* acceptable on a shared LAN, where any reachable device could connect. The unified host
should `require_pairing` out of the box: a client must complete the SPAKE2 PIN ceremony (one online
guess, no offline attack) before any session. The operator arms a window and reads the PIN from the
web console (already built); an explicit `--open` escape hatch covers trusted single-user setups.
The wire is already in place (`M3Options.require_pairing` + the `serve_session` gate); this flips
the default and threads it through `serve --native` and the mgmt arm endpoint.
- **Delegated pairing approval** — the ergonomic enabler for "mandatory" (pair a new device without
fetching the host PIN out of band):
- **Mandatory PIN pairing by default — done & live** (`§8a`, `serve --native` now requires
pairing; `serve --open` disables it). An unpaired client is rejected at the session gate; pairing
is via the SPAKE2 PIN ceremony (one online guess, no offline attack) armed from the web console.
Validated live: unpaired → "this host requires pairing", then web-armed PIN → "client trusted".
Deployed to the dev box + Bazzite.
- **Delegated pairing approval** *(next — the ergonomic enabler for "mandatory": pair a device
without fetching the host PIN out of band).* Target flow:
1. Device A is already paired (authenticated) to Host X.
2. The user tries to connect Device B to Host X.
3. Host X pushes a request to the authenticated Device A: *"Allow Device B to pair with Host X?"*
4. The user approves/denies on Device A; on approve, Host X admits Device B — binding B's
certificate fingerprint — with no PIN typed.
3. Host X surfaces a request: *"Allow Device B to pair with Host X?"*
4. The user approves/denies; on approve, Host X admits Device B — binding B's certificate
fingerprint — with no PIN typed.
Needs: a host→client *pairing-approval-request* (B's fingerprint + a human label) delivered to A's
live connection (a QUIC side-plane message) or polled via the mgmt API; an approve/deny round-trip
carrying an approval token; the host gating B's admission on it. The web console **and** the Apple
client render the approval prompt. PIN pairing stays the bootstrap (the first device, or when no
paired device is online to approve).
Two buildable layers:
- **§8b-1 (host + web — achievable now):** an unpaired B that connects to an approval-enabled host
is held as a **pending request** `{id, name, fingerprint, requested_at}` in `NativePairing`
instead of a flat reject; mgmt gains `GET /native/pending` + `POST /native/pending/{id}/{approve,
deny}`; the web console lists pending requests with Approve/Deny. The **operator approves from
the console** — delegated approval via the management surface.
- **§8b-2 (peer push — needs the client):** the host also pushes the pending request over a paired
**Device A**'s live QUIC connection (a new control-plane message); A's app renders the prompt and
replies approve/deny — the user's exact "Device A gets a notification" flow. The native/Apple UI
is a client-agent task.
PIN pairing (§8a) stays the bootstrap — the first device, or when no approver is online.