v0.0.6 - Tier-1 TCP listener + FIPS deployment documentation
This commit is contained in:
116
plans/nsigner.md
116
plans/nsigner.md
@@ -174,3 +174,119 @@ Privacy/UX notes:
|
||||
- Expand integration coverage for NIP-04/NIP-44 edge cases and negative-path errors.
|
||||
- Document and test static artifact size budgets across targets.
|
||||
- Define MCU transport adapter contract to prepare desktop/firmware parity.
|
||||
|
||||
## 7. Transport expansion roadmap
|
||||
|
||||
Goal: keep one signer core, swap transports underneath without touching dispatcher, policy, or role layers. The wire contract in [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md) (4-byte length-prefixed JSON-RPC) stays identical across every transport; only listener and `caller_identity_t` change.
|
||||
|
||||
### 7.0 Prerequisite — transport abstraction (Phase T0)
|
||||
|
||||
Before adding any new transport, factor a small adapter contract out of [`src/server.c`](../src/server.c) and [`src/main.c`](../src/main.c).
|
||||
|
||||
- New header `src/transport.h` declaring an opaque `nsigner_transport_t` with:
|
||||
- `accept(listener) -> connection`
|
||||
- `recv_frame(connection) -> bytes`
|
||||
- `send_frame(connection, bytes)`
|
||||
- `peer_identity(connection) -> caller_identity_t`
|
||||
- `close(connection)` / `shutdown(listener)`
|
||||
- Generalize `caller_identity_t` to a tagged union of:
|
||||
- `unix_peer { uid, pid, comm }` (current behavior)
|
||||
- `qubes { source_qube_name }`
|
||||
- `tcp_local { addr }`
|
||||
- `tcp_remote { addr, authenticated_pubkey }`
|
||||
- `fips { peer_npub }`
|
||||
- `usb_serial { device_path, asserted_caller }`
|
||||
- Move `recv_framed` / `send_framed` from `server.c` and `main.c` into a single shared `transport_frame.c` so client and server share one framing implementation.
|
||||
- Server main loop becomes transport-agnostic (`while accept; recv; dispatch; send`).
|
||||
- Tests: extend [`tests/test_integration.c`](../tests/test_integration.c) with a transport-loopback fake to validate the abstraction without binding any real socket.
|
||||
|
||||
This refactor is purely internal — no observable change.
|
||||
|
||||
### 7.1 Phase T1 — Qubes OS qrexec transport
|
||||
|
||||
Use Qubes' native inter-qube primitive instead of inventing one.
|
||||
|
||||
- Add a qrexec service script (e.g. `qubes.NsignerRpc`) that execs `nsigner` in a "stdio transport" mode where stdin/stdout carry the existing length-prefixed frame protocol.
|
||||
- New CLI: `nsigner --listen stdio` (and `nsigner --listen qrexec`, behaving identically; `qrexec` value is for documentation/intent).
|
||||
- Caller identity comes from qrexec environment (`QREXEC_REMOTE_DOMAIN`) and is mapped to `caller_identity_t.kind=qubes`.
|
||||
- Reference policy file under `packaging/qubes/policy.d/40-nsigner.policy` showing `ask` / `allow` per source qube.
|
||||
- No new attack surface inside nsigner: dom0 enforces who can even invoke the service.
|
||||
- Tests: a unit test that injects fake qrexec env vars and a stdio framing harness; an integration script that documents end-to-end install in a Qubes VM (manual, not in CI).
|
||||
- Docs: add a "Qubes deployment" section to [`README.md`](../README.md) and to [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md).
|
||||
|
||||
### 7.2 Phase T2 — TCP loopback transport
|
||||
|
||||
Smallest IP-based step; on-ramp for non-Linux clients and for FIPS later.
|
||||
|
||||
- New CLI: `nsigner --listen tcp:127.0.0.1:PORT`.
|
||||
- Default-deny non-loopback binds (reject `0.0.0.0` / non-`127.x` / non-`::1` unless `--allow-remote`, see T3).
|
||||
- Caller identity for loopback: `tcp_local { addr }`. Approval prompt still mandatory.
|
||||
- `nsigner list` extended to enumerate active TCP listeners (from internal registry; not from `/proc/net/tcp`).
|
||||
- Same framing as AF_UNIX path; no protocol changes.
|
||||
- Tests: integration coverage that spawns a child signer with `--listen tcp:127.0.0.1:0`, captures the bound port, runs the same NIP-04/NIP-44/sign_event matrix as AF_UNIX.
|
||||
- Docs: extend [`documents/CLIENT_IMPLEMENTATION.md`](../documents/CLIENT_IMPLEMENTATION.md) section 2 with `tcp:` discovery rules and section 3 confirming framing parity.
|
||||
|
||||
Implementation checklist (Tier-1 delivery):
|
||||
|
||||
- [x] Parse `--listen tcp:HOST:PORT` in [`src/main.c`](../src/main.c).
|
||||
- [x] Reject invalid/non-loopback listen targets in [`src/server.c`](../src/server.c).
|
||||
- [x] Bind/listen non-blocking TCP sockets and run server loop without TUI dependence.
|
||||
- [x] Keep existing framed JSON-RPC protocol unchanged via shared [`src/transport_frame.c`](../src/transport_frame.c).
|
||||
- [ ] Add integration test coverage for `tcp:127.0.0.1:PORT` request flow.
|
||||
|
||||
### 7.3 Phase T3 — TCP remote with TLS + caller-pubkey auth
|
||||
|
||||
Only after T2 is solid.
|
||||
|
||||
- New CLI: `nsigner --listen tcp:0.0.0.0:PORT --allow-remote --tls-cert <pem> --tls-key <pem>`.
|
||||
- Mandatory: TLS for any non-loopback bind. Refuse to start otherwise.
|
||||
- Caller authentication: client must sign a per-connection challenge with its declared npub (Schnorr/secp256k1) before any signer verb is dispatched. Identity becomes `tcp_remote { addr, authenticated_pubkey }`.
|
||||
- Failure modes: `transport_tls_required`, `caller_auth_failed`, `caller_auth_timeout` — all surfaced with new error names in dispatcher and documented in [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md).
|
||||
- Approval prompt now displays `caller=npub:abcd…wxyz` instead of `uid:1000`.
|
||||
- Tests: integration test that exercises happy path, wrong-pubkey, replayed-challenge, expired-challenge.
|
||||
- Docs: dedicated "Remote TCP deployment" section in `README.md` with strong "do not expose to the public internet without firewalling" warning.
|
||||
|
||||
### 7.4 Phase T4 — FIPS substrate integration
|
||||
|
||||
FIPS is a *substrate* for an existing TCP listener, not a new transport in nsigner code.
|
||||
|
||||
- Deployment topology: nsigner binds TCP loopback inside the FIPS network namespace (or on a host where `fips0` is up); peers reach it via `fd00::/8` IPv6 derived from the signer's npub.
|
||||
- Optional `caller_kind=fips` enrichment: a small sidecar query (`fipsctl show sessions` style) maps the connecting IPv6 address to a peer npub and feeds it into `caller_identity_t.fips { peer_npub }`. If unavailable, fall back to `tcp_remote` identity.
|
||||
- nsigner does not embed FIPS, does not depend on libfips, and does not require Rust.
|
||||
- New optional flag: `--peer-id-source fips:/var/run/fips/fips.sock` (path/method TBD per FIPS API).
|
||||
- Tests: a Docker-compose fixture borrowed from `resources/fips/testing/` that boots two FIPS nodes, runs nsigner on one, runs a Python client (per snippet in [`documents/CLIENT_IMPLEMENTATION.md`](../documents/CLIENT_IMPLEMENTATION.md)) on the other, and exercises the same verb matrix.
|
||||
- Docs: new [`documents/FIPS_DEPLOYMENT.md`](../documents/FIPS_DEPLOYMENT.md) deep-dive describing identity mapping, npub-as-caller, and operator setup. Cross-link from [`README.md`](../README.md) section 7 (Transport).
|
||||
|
||||
Execution tasks for initial FIPS trial:
|
||||
|
||||
- [x] Deliver T2 TCP loopback listener as FIPS substrate prerequisite.
|
||||
- [x] Document signer/caller qube deployment flow in [`documents/FIPS_DEPLOYMENT.md`](../documents/FIPS_DEPLOYMENT.md).
|
||||
- [ ] Add two-node operator validation script (manual) using `fipsctl` + framed JSON-RPC client.
|
||||
- [ ] Evaluate optional caller identity enrichment from FIPS session metadata.
|
||||
|
||||
### 7.5 Phase T5 — USB / serial transport
|
||||
|
||||
Two distinct sub-tracks; do not conflate.
|
||||
|
||||
- T5a (firmware-side, MCU): ESP32/USB-CDC. Already in the [`firmware/`](../firmware/) track. Same dispatcher; transport adapter is UART read/write loop. `caller_identity_t.kind=usb_serial` with `asserted_caller` because the host claims the identity.
|
||||
- T5b (host-side optional): `nsigner --listen serial:/dev/ttyACM0,baud=115200`. Useful for desktop signer reachable by a USB-tethered client. Same frame protocol over the serial line. Marks identity as asserted (low trust) and forces approval prompt.
|
||||
- USB-as-Ethernet (gadget mode, RNDIS/ECM) is **not** a separate transport — it reduces to T2/T3.
|
||||
- Tests: loopback pty pair (`openpty`) for T5b unit/integration coverage; firmware-side covered in firmware track.
|
||||
|
||||
### 7.6 Cross-cutting concerns
|
||||
|
||||
Apply once per phase as needed:
|
||||
|
||||
- Transport-aware approval prompt: clear visual indication of transport kind and identity (uid vs qube vs npub vs serial-asserted). No silent identity-source confusion.
|
||||
- Per-transport policy gates: deny-by-default for new identity kinds until operator explicitly enables them in policy.
|
||||
- Discovery (`nsigner list`) becomes per-transport pluggable (proc/net/unix today, internal registry for tcp, qrexec service announce for qubes, fips peer table for fips).
|
||||
- Audit logging: include transport kind and identity descriptor in every approval/decision record.
|
||||
- Error name parity: every new transport introduces only well-named errors (extend the table in [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md) section 5).
|
||||
|
||||
### 7.7 Decision points (open)
|
||||
|
||||
- D1: Land T0 (refactor) before any transport, or in parallel with T1?
|
||||
- D2: Bundle T2 and T3 as one phase, or hard split (loopback-only first, then remote-with-TLS later)?
|
||||
- D3: T4 FIPS — embed an explicit `caller_kind=fips` path in nsigner now, or treat FIPS as plain TCP and revisit identity enrichment after a working deployment?
|
||||
- D4: T5b host-side serial — in scope for desktop nsigner, or strictly firmware track?
|
||||
- D5: Qubes packaging — ship `packaging/qubes/` artifacts in this repo, or document only and let operators wire it up?
|
||||
|
||||
Reference in New Issue
Block a user