checkpoint: external NIP-46 signing end-to-end (Step 3 sub-step 2 done)
This commit is contained in:
parent
1af79d81cd
commit
6e5d80ba0b
1 changed files with 71 additions and 0 deletions
|
|
@ -1,3 +1,74 @@
|
||||||
|
# Checkpoint — External NIP-46 signing works end-to-end (2026-09-10)
|
||||||
|
|
||||||
|
## Where things are
|
||||||
|
- Project: `/home/avi/Projects/Keynctr`
|
||||||
|
- Branch: `master` @ **`1af79d8`** ("feat(signer): end-to-end external NIP-46 signing in
|
||||||
|
publish and upload auth").
|
||||||
|
- Working tree: clean for tracked files (untracked leftovers unchanged — see older
|
||||||
|
"Still untracked" sections).
|
||||||
|
- Verification: `cargo test` **200 passed / 0 failed**, `cargo clippy --all-targets`
|
||||||
|
clean, `cargo fmt --check` clean, `cargo build --release` green.
|
||||||
|
|
||||||
|
## What was completed (this session)
|
||||||
|
|
||||||
|
**Step 3 sub-step 2 (IPC reroute) — DONE, landed as `1af79d8`.** Publishing from an
|
||||||
|
external (NIP-46) profile now signs remotely instead of returning
|
||||||
|
"External signer not yet supported":
|
||||||
|
|
||||||
|
- **`src/signer/nip46_client.rs`** — the outbound/client half of NIP-46:
|
||||||
|
`send_remote_request` encrypts a request (NIP-44) and publishes it to the signer's
|
||||||
|
relay; incoming payloads shaped like responses (`result`/`error`) are demultiplexed
|
||||||
|
to the waiting caller (a response with no registered id is ignored); 30 s
|
||||||
|
`REQUEST_TIMEOUT`; pending waiters are woken with errors on disconnect/failure so
|
||||||
|
callers never hang the full timeout. `Signer::sign_event` is real now: permission
|
||||||
|
check → remote `sign_event` → verify the returned event (a) is signed by the
|
||||||
|
connected remote identity, (b) matches the exact unsigned event requested, (c) has a
|
||||||
|
valid signature — then return it. **No local-key fallback anywhere.**
|
||||||
|
`audit_permission_denied` uses `try_lock` (audit is best-effort; blocking here would
|
||||||
|
deadlock the very request being denied while the IPC dispatcher holds the App lock).
|
||||||
|
- **`src/app.rs`** — `App::signing_for(npub)` / `App::signing_active()`: the single
|
||||||
|
place that maps a profile's `SignerMode` to a `Signing` source.
|
||||||
|
`Embedded` → `Signing::Local` with the vault-resolved key; `Nip46Client` →
|
||||||
|
`Signing::External` wrapping the live signer **only when present and connected**,
|
||||||
|
else `ExternalSignerNotConnected` (fail closed, never a silent local fallback);
|
||||||
|
`Nip46Bunker` (not wired) also fails closed.
|
||||||
|
- **`src/publish.rs`** — `publish_with_keys` builds the unsigned event from the
|
||||||
|
`Signing`'s own pubkey (external identities validate there before any relay work)
|
||||||
|
and signs via `Signing::sign`; new `publish_signed` entry point for IPC. The CLI's
|
||||||
|
`publish_active`/`publish_as` keep the local vault path.
|
||||||
|
- **`src/ipc.rs`** — `PublishNote` and `UploadAuth` route through
|
||||||
|
`app.signing_active()`; no handler branches on signer mode anymore.
|
||||||
|
`Nip46Connect`/`Nip46Disconnect` now clone the signer handle and **drop the App
|
||||||
|
guard before awaiting** `connect()`/`disconnect()` (they re-lock the App
|
||||||
|
internally — a latent deadlock, fixed).
|
||||||
|
- **`src/relays.rs`** — keyless relay pool: `open_pool_inner(Option<Keys>, …)` so
|
||||||
|
external signing can publish/relay without local keys (no relay AUTH).
|
||||||
|
- **`src/uploads.rs`** — `nip98_authorization(url, method, &Signing)` — NIP-98 upload
|
||||||
|
auth events sign through the same `Signing` source, so uploads authenticate with
|
||||||
|
the remote signer for external profiles.
|
||||||
|
- **`src/profiles.rs`** — `store_remote_profile(vault, npub, label)`: connecting a
|
||||||
|
NIP-46 signer creates/refreshes a **secretless** `Nip46Client` profile row (empty
|
||||||
|
`secret_key`, made active) so publish has a selection; refuses to silently convert
|
||||||
|
an existing local profile into a remote one. `connect()` calls it and adopts the
|
||||||
|
remote npub as the signer's active profile.
|
||||||
|
|
||||||
|
New tests (`src/app.rs`): embedded → `Signing::Local`; external profile with no live
|
||||||
|
signer → `ExternalSignerNotConnected` (fail closed); no active profile →
|
||||||
|
`NoActiveProfile`; `store_remote_profile` creates a secretless external profile and
|
||||||
|
refuses to clobber a local one.
|
||||||
|
|
||||||
|
Security properties: remote-signed events are triple-verified (identity, content
|
||||||
|
match, signature) before publish; external profiles never touch a local key; the
|
||||||
|
connection secret stays vault-encrypted (Step 3 sub-step 1 unchanged).
|
||||||
|
|
||||||
|
## Out of scope this session (deliberate)
|
||||||
|
- `publish_profile_metadata` (kind 0) still signs locally from the vault — a
|
||||||
|
synchronous key-based path; rerouting it is a separate follow-up.
|
||||||
|
- Dead `src/signer/nip46_external.rs` stub cleanup (untracked leftover).
|
||||||
|
- Step 4 (permissions UI), Step 5 (KDF upgrade), Step 7 (rename/hygiene).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Checkpoint — Undo-delete restores working profiles (2026-09-10)
|
# Checkpoint — Undo-delete restores working profiles (2026-09-10)
|
||||||
|
|
||||||
## Where things are
|
## Where things are
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue