diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md index 3c05400..fabed80 100644 --- a/CHECKPOINT-encryption.md +++ b/CHECKPOINT-encryption.md @@ -1,4 +1,4 @@ -# Checkpoint — NIP-46 remote signer (2026-08-05) +# Checkpoint — NIP-46 remote signer + approval gate (2026-08-06) A stopping point you can return to if this session is closed. Everything below was verified green at the moment this file was written. @@ -6,58 +6,74 @@ verified green at the moment this file was written. ## Where things are - Project: `/home/avi/Projects/skills/nost-feed-manager` -- Git repo: `master` @ `73cb08d` ("Add NIP-46 remote signer (external signing)"), on top of - `13efee6` ("Refresh checkpoint with Forgejo README"), `3eb9fa9` ("Rewrite README for Forgejo - hosting"), and the history described in the previous checkpoint. +- Git repo: `master` @ `ca3203c` ("Require user approval for NIP-46 sign/encrypt requests"), + on top of `593fc6a` ("Refresh checkpoint with NIP-46 remote signer"), `73cb08d` ("Add NIP-46 + remote signer (external signing)"), `13efee6` ("Refresh checkpoint with Forgejo README"), + `3eb9fa9` ("Rewrite README for Forgejo hosting"), and the history described in earlier + checkpoints. - The working tree is **clean** — everything from this session is committed. -- The signer feature is split across one commit (`73cb08d`) that contains both the Rust backend - and the Electron/React frontend. +- The signer feature spans `73cb08d` (the signer itself) plus `ca3203c` (the approval gate). ## What was completed: NIP-46 remote signer (bunker role) The app can now act as a **remote signer** (NIP-46, "bunker") for another Nostr app. Instead of pasting your nsec into other apps, they send signing requests to this app, which approves them -with the active profile's keys. Decisions made this session: the connected peer is -**auto-approved** once the handshake succeeds, the signer runs **inside the `serve` process as a -concurrent tokio task** (sharing `Arc>`), and the frontend gets a **dedicated Signer -screen** as its own sidebar nav item. +with the active profile's keys. The signer runs **inside the `serve` process as a concurrent +tokio task** (sharing `Arc>`), and the frontend gets a **dedicated Signer screen** as +its own sidebar nav item. -- **Backend** (`src/signer.rs`): a hand-rolled NIP-46 implementation (the SDK has no ready-made +**Security change this session (commit `ca3203c`): every key-using request now requires explicit +user approval.** The peer is still "trusted" after the handshake, but `sign_event`, +`nip44_encrypt`, and `nip44_decrypt` are no longer auto-approved. Each such request is parked in a +`pending` queue, exposed in `SignerStatus.pending`, and only runs after the user clicks +**Approve** on the Signer screen (or is answered with **Reject** / a 300-second timeout). All +non-key methods (`connect`, `get_public_key`, `ping`, `get_relays`, `switch_relays`, `logout`) +still run immediately. + + - **Backend** (`src/signer.rs`): a hand-rolled NIP-46 implementation (the SDK has no ready-made NIP-46 server). `Signer` exposes `status()`, `connect()`, `disconnect()`, `fail()`, - `set_connected()`. `parse_connect_uri` accepts `nostrconnect://` links (and clearly rejects the - opposite-role `bunker://` scheme). The async `run_sign_task` connects over the chosen relays, - listens for kind 24133 requests decrypted with a NIP-44 v2 conversation key, and answers - `get_public_key`, `sign_event`, and `nip44_encrypt`/`nip44_decrypt`. Responses are NIP-44-encrypted - and published back to the peer. Six unit tests cover URI parsing, bunker rejection, NIP-44 - round-trip, get_public_key, sign_event, and unknown-method error responses. -- **IPC** (`src/ipc.rs`): `serve`/`handle`/`run` now share `Arc>`; new variants - `SignerConnect { uri }`, `SignerDisconnect`, `SignerStatus`. The connect/disconnect/status - requests are serviced by `run()` before the (long-lived) vault lock so a slow signer task never - blocks ordinary commands. -- **CLI** (`src/main.rs`): `signer status` and `signer connect `. -- **Electron/React frontend**: new **Signer screen** (sidebar nav item with the key icon) where you - paste a `nostrconnect://` link, Connect/Disconnect, and see the phase (Not connected / - Connecting… / Connected), the connected client's hex pubkey (shortened), the relays in use, and - any error. Wired through `api.ts` (`signerConnect`/`signerDisconnect`/`signerStatus`) and - `AppProvider`. When the vault is locked, a warning explains the signer needs an unlocked vault. - Tests: `SignerScreen.test.tsx` (new) + `apiMock.ts`/`fakeBackend.ts` signer methods. + `set_connected()`, `approve()`, and `await_approval()`. `parse_connect_uri` accepts + `nostrconnect://` links (and clearly rejects the opposite-role `bunker://` scheme). The async + `run_sign_task` connects over the chosen relays, listens for kind 24133 requests decrypted with + a NIP-44 v2 conversation key, and answers `get_public_key`, `sign_event`, and + `nip44_encrypt`/`nip44_decrypt`. Responses are NIP-44-encrypted and published back to the peer. + Key-using methods route through `gated_response` → `await_approval` (a `HashMap` of pending + entries, each with a `oneshot` decision channel) → `approved_response`, while everything else + flows through the non-blocking `handle_request` dispatcher. The pending list clears on + disconnect and on failure, so a slow/abandoned approval never leaks into a new connection. + Unit tests cover URI parsing, bunker rejection, NIP-44 round-trip, get_public_key, sign_event, + unknown-method, and the approval gate (wait-then-sign, reject-never-uses-key, unknown-id error). +- **IPC** (`src/ipc.rs`): `serve`/`handle`/`run` share `Arc>`; variants + `SignerConnect { uri }`, `SignerDisconnect`, `SignerStatus`, and the new `SignerApprove { id, + approved }`. The signer-status/approve requests are serviced by `run()` before the (long-lived) + vault lock so a slow signer task never blocks ordinary commands. +- **CLI** (`src/main.rs`): `signer status` and `signer connect `; `signer status` notes that + approvals happen in the GUI (the CLI's blocking `connect` loop cannot answer approvals itself). +- **Electron/React frontend**: the **Signer screen** (sidebar nav item with the key icon) where you + paste a `nostrconnect://` link, **Connect**/**Disconnect**, see the phase, peer, relays, and any + error, and now review pending requests with **Approve**/**Reject** buttons. The screen polls + `signer_status` every second while mounted so approval requests appear without a manual + refresh. Wired through `api.ts` (`signerConnect`/`signerDisconnect`/`signerStatus`/ + `signerApprove`) and `AppProvider`. ## Commits this session (newest first) -- `73cb08d` "Add NIP-46 remote signer (external signing)" — everything above in one commit. - Full verification suite re-run green (below). +- `ca3203c` "Require user approval for NIP-46 sign/encrypt requests" — the approval gate + (backend pending queue + `SignerApprove` IPC + Signer screen Approve/Reject + tests). +- `593fc6a` "Refresh checkpoint with NIP-46 remote signer" — checkpoint refresh (previous session). +- `73cb08d` "Add NIP-46 remote signer (external signing)" — the signer feature itself. ## How it was verified (all green) ``` -cargo test # 70 passed +cargo test # 75 passed cargo clippy --all-targets # clean cargo fmt --check # clean cargo build --release # builds (rebuilt so the GUI runs the new backend) npm run typecheck # clean (frontend/) npm run lint # clean (pre-existing module warning only) npm run format:check # clean -npm test # 69 passed (13 files) +npm test # 71 passed (13 files) npm run electron:build # compiles the Electron main process npm run build # rebuilds the React bundle (dist/) ``` @@ -69,19 +85,23 @@ a `bunker://` link and a non-`nostrconnect` URI were both rejected with clear er 1. Open the repo: `cd /home/avi/Projects/skills/nost-feed-manager` 2. State is committed: `git status` should be clean; `git log --oneline -5` shows - `73cb08d` at the top. + `ca3203c` at the top. 3. To try the signer: - GUI: `cd frontend && npm start`, then the **Signer** screen in the sidebar. Paste a - `nostrconnect://` link from a Nostr app to sign for it. (Note: `npm start` alone does NOT - rebuild the React bundle — run `npm run build` first, or use `npm run dev` + - `NOSTR_GUI_DEV_URL=http://localhost:5173 npm start` for live reload.) + `nostrconnect://` link from a Nostr app to sign for it. When the app sends a sign/decrypt + request it appears under "Requests waiting for approval" — click **Approve** or **Reject**. + (Note: `npm start` alone does NOT rebuild the React bundle — run `npm run build` first, or + use `npm run dev` + `NOSTR_GUI_DEV_URL=http://localhost:5173 npm start` for live reload.) - CLI: `cargo build --release`, then `./target/release/nostr-manager-backend signer status`. + Note: the CLI's blocking `signer connect` loop cannot answer approval prompts — run the GUI + for that. 4. Re-run verification with the commands above. ## Outstanding / next steps (if you continue) - Nothing uncommitted. Possible follow-ups: a real feed view (currently Home only shows the last publication); a NIP-46 **client** role (initiating `bunker://` from this app is intentionally - not implemented — only the signer role is). Live cross-client verification against an actual - Nostr app (e.g. a NIP-46-compatible client that produces a `nostrconnect://` link) has not yet - been done end-to-end, only the unit and CLI smoke tests. \ No newline at end of file + not implemented — only the signer role is); a CLI-side approval escape hatch for `signer + connect` (prompt on stdin) so approvals also work headlessly. Live cross-client verification + against an actual Nostr app (e.g. a NIP-46-compatible client that produces a `nostrconnect://` + link) has not yet been done end-to-end, only the unit and CLI smoke tests. \ No newline at end of file