docs: checkpoint signer modes + fail-closed key export (post-triage)
Re-point the checkpoint at HEAD 6eff510 and document the three-session
commit split (audit log -> signer modes -> fail-closed export), the real
file list, per-commit verification results, and the remaining
uncommitted packaging icon + hygiene leftovers.
This commit is contained in:
parent
6eff510609
commit
d92371fbc2
1 changed files with 107 additions and 87 deletions
|
|
@ -1,100 +1,120 @@
|
||||||
# Checkpoint — Embedded Signer & NIP-46 Client Modes (2026-09-01)
|
# Checkpoint — Signer Modes + Fail-Closed Key Export (2026-09-03)
|
||||||
|
|
||||||
## Where things are
|
## Where things are
|
||||||
- Project: `/home/avi/Projects/Keynctr`
|
- Project: `/home/avi/Projects/Keynctr`
|
||||||
- Git repo: `master` @ `b484bde` ("feat: add embedded signer and NIP-46 client signer modes").
|
- Branch: `master` @ **`6eff510`** ("feat: fail-closed ExportSecretKey with fresh auth and audit").
|
||||||
- Working tree: clean except the usual untracked items (`.directory`, `.opencode/`,
|
- Working tree: **not clean** — see "Still uncommitted" below. The three feature
|
||||||
`.impeccable/`, `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`).
|
commits of this session are committed; only the packaging icon, the old
|
||||||
|
checkpoint, and pre-existing hygiene leftovers remain uncommitted.
|
||||||
|
|
||||||
## What was completed
|
## What was completed (this session)
|
||||||
1. **Added unified Signer architecture** with a common `Signer` trait in `src/signer/mod.rs`
|
The previously-uncommitted working tree (27 modified + 10 untracked files, ~1852/692)
|
||||||
that both embedded and NIP-46 client implementations share. The trait provides:
|
was triaged into **three logical, independently-verifiable commits**. Order is
|
||||||
- `get_public_key()`, `sign_event()`, `get_signer_type()`, `is_available()`
|
`a → c → b`: `ExportSecretKey` (b) reads the per-profile `signer_mode` field and the
|
||||||
- `request_approval()`, `disconnect()`, `revoke()`, `status_string()`, `detailed_status()`
|
`external_signer_not_connected` error that the signer-mode commit (c) introduces, so
|
||||||
|
(c) had to land first. The only uncommitted *tests* were the export tests and the
|
||||||
|
signer-mode/permission tests, so "(d) tests" is not a separate commit — each feature's
|
||||||
|
tests travel with it.
|
||||||
|
|
||||||
2. **Embedded Signer mode** (`src/signer/embedded.rs`):
|
1. **Per-profile signer modes + persisted NIP-46 connections (c)** — three coexisting
|
||||||
- Keys stored locally in the encrypted vault (Argon2id + AES-256-GCM)
|
signing modes (Embedded / Nip46Client / Nip46Bunker), a `signer_mode` field on every
|
||||||
- Zeroize memory protection for secret keys
|
stored profile, a vault `nip46_connections` store (owner-scoped, with parsed
|
||||||
- Per-request user approval with 5-minute timeout and 20-request queue cap
|
permissions, expiry, revocation), the expanded `Signer` trait (identity validation,
|
||||||
- Sensitive operations (kind 0, 3, 5, 6, 10000-10002, 30000-30015) flagged for extra confirmation
|
`Signing` enum, permission surface), the NIP-46 permission model, and the redesigned
|
||||||
- Active profile binding with automatic updates on profile create/import/select
|
Signer Mode screen with a nostr-tools-based client.
|
||||||
|
2. **Fail-closed key export (b)** — `export_secret_key` always re-authenticates, requires
|
||||||
|
a reason, refuses external-signer profiles, and writes the audit entry *before*
|
||||||
|
returning the key (fail-closed on audit failure). Frontend `ExportSecretKeyModal`
|
||||||
|
replaces the old reveal modal.
|
||||||
|
3. **Hash-chained audit log (a)** — SHA-256 hash-chained append-only log with atomic
|
||||||
|
append and end-to-end `verify_chain()`; the `sha2` dependency.
|
||||||
|
|
||||||
3. **NIP-46 Client Signer mode** (`src/signer/nip46_client.rs`):
|
### Security properties confirmed
|
||||||
- Connects to external signer (bunker) via `nostrconnect://` URI
|
- No secret material is logged or returned except the single, authenticated, audited
|
||||||
- Supports both local (separate process) and remote signers over relays
|
export. The export `reason` is logged by design; passwords and nsecs are not.
|
||||||
- NIP-44 v2 encryption for all communication
|
- Export is **fail-closed**: a failed audit write prevents the key from being returned
|
||||||
- Connection state tracking (connecting/connected/error) with relay connection monitoring
|
(`log.record(...)?` — the earlier `let _ =` that let a key out on audit failure is gone).
|
||||||
- Automatic approval timeout (5 min) and queue cap (20 pending)
|
- External (Nip46Client) profiles cannot export a secret key — the key is not local.
|
||||||
- Connect secret echo verification per NIP-46 spec
|
- Profile identity is resolved server-side (`profiles::find_stored_profile`), not trusted
|
||||||
- Graceful disconnect/revoke with connection cleanup
|
from the client.
|
||||||
|
- NIP-46 permissions are deny-by-default and cannot be broadened on reconnect.
|
||||||
|
- Audit writes are serialized (single mutex across the read-compute-write-update cycle)
|
||||||
|
and the chain is tamper-evident from a genesis hash.
|
||||||
|
- Renderer never receives an nsec outside the intentional one-time export.
|
||||||
|
|
||||||
3. **Signer mode management**:
|
## Commits added this session (newest first)
|
||||||
- Users can choose "Embedded signer" or "NIP-46 signer" during account setup
|
| Hash | Message |
|
||||||
- Switch modes anytime without changing public key (preserves `npub`)
|
|------|---------|
|
||||||
- Clear UI showing active mode, connection status, and pending approvals
|
| `6eff510` | feat: fail-closed ExportSecretKey with fresh auth and audit |
|
||||||
- Security notes explaining trade-offs between modes
|
| `2c61830` | feat: add per-profile signer modes with persisted NIP-46 connections |
|
||||||
|
| `caed722` | feat: add hash-chained, append-only audit log |
|
||||||
|
|
||||||
4. **Frontend integration**:
|
Parent of this session: `4038e2d` ("checkpoint: document embedded signer and NIP-46
|
||||||
- New `SignerModeScreen.tsx` for mode selection and status monitoring
|
client signer modes").
|
||||||
- Updated `AppProvider` with `signerModeGet`, `signerModeSet`, `embeddedSignerStatus`,
|
|
||||||
`nip46Connect`, `nip46Disconnect`, `nip46Status`, and approval handlers
|
|
||||||
- New types: `SignerMode`, `ApprovalDetails`, `EmbeddedSignerStatus`, `Nip46SignerStatus`
|
|
||||||
- Sidebar navigation updated with "Signer Mode" entry
|
|
||||||
|
|
||||||
5. **IPC protocol extensions** (`src/ipc.rs`):
|
### Files touched by the three commits (30 files, +3921 / -611)
|
||||||
- `SignerModeGet`, `SignerModeSet` for mode management
|
```
|
||||||
- `EmbeddedSignerStatus`, `EmbeddedSignerApprove`
|
Cargo.lock Cargo.toml frontend/electron/main.ts frontend/package.json
|
||||||
- `Nip46Connect`, `Nip46Disconnect`, `Nip46Status`, `Nip46Approve`
|
frontend/package-lock.json frontend/src/components/ExportSecretKeyModal.tsx (new)
|
||||||
- Legacy bunker signer commands delegated to new NIP-46 client when in that mode
|
frontend/src/components/ShowSecretKeyModal.tsx (deleted)
|
||||||
|
frontend/src/lib/api.ts frontend/src/lib/signer/SignerManager.ts (new)
|
||||||
|
frontend/src/lib/types.ts frontend/src/screens/ProfilesScreen.tsx
|
||||||
|
frontend/src/screens/SignerModeScreen.tsx frontend/src/state/AppProvider.tsx
|
||||||
|
frontend/src/styles.css frontend/src/test/apiMock.ts
|
||||||
|
frontend/src/test/ExportSecretKey.test.tsx (new) frontend/src/test/fakeBackend.ts
|
||||||
|
frontend/src/test/ShowSecretKey.test.tsx (deleted)
|
||||||
|
src/app.rs src/audit.rs (new) src/errors.rs src/ipc.rs src/lib.rs src/main.rs
|
||||||
|
src/profiles.rs src/signer/mod.rs src/signer/nip46_client.rs
|
||||||
|
src/signer/permissions.rs (new) src/signer/types.rs src/vault.rs
|
||||||
|
```
|
||||||
|
|
||||||
6. **Security hardening**:
|
## Verification (run this session, per commit)
|
||||||
- All secret keys encrypted at rest with Argon2id + AES-256-GCM
|
Each commit was verified **in isolation** (checked out on top of its parent), not just
|
||||||
- Zeroize for in-memory key cleanup
|
as the final tree:
|
||||||
- Approval timeouts and queue caps prevent DoS
|
- `caed722` (audit): `cargo test --release` → **124 passed** (119 prior + 5 audit).
|
||||||
- Connection secrets verified on handshake
|
- `2c61830` (signer modes): `cargo test --release` → **186 passed**; `cargo clippy
|
||||||
- No private keys in logs, crash reports, or network requests
|
--all-targets` clean; `cargo fmt --check` clean; frontend `tsc --noEmit` clean;
|
||||||
- Keys never sent to server/relay/AI services
|
`npx vitest run` → **110 passed**.
|
||||||
|
- `6eff510` (export): `cargo test --release` → **186 passed**; frontend `tsc --noEmit`
|
||||||
|
clean; `npx vitest run` → **116 passed** (110 + 6 export tests).
|
||||||
|
|
||||||
## Commits added in this session (newest first)
|
## How to reproduce / exercise
|
||||||
- `b484bde` feat: add embedded signer and NIP-46 client signer modes
|
- Backend: `cargo run --release -- serve` (JSON-lines IPC on stdio) or the CLI in
|
||||||
|
`src/main.rs`.
|
||||||
|
- GUI: from `frontend/`, `npm run electron:build && electron .` (prod) or `npm run
|
||||||
|
start:dev` with `NOSTR_GUI_DEV_URL`.
|
||||||
|
- Exercise export: Profiles → a profile → Export secret key → enter the vault password
|
||||||
|
and a reason. An external (NIP-46 client) profile shows it cannot export.
|
||||||
|
- Exercise modes: Signer Mode screen → pick Embedded / NIP-46 client; connect a
|
||||||
|
`nostrconnect://` URI with a `perms=` parameter to grant scoped permissions.
|
||||||
|
|
||||||
## Verification commands run
|
## Still uncommitted (do NOT lose; do NOT commit the hygiene junk)
|
||||||
All green in this session, run after the changes:
|
- **`frontend/build/icon.png` is deleted** (working tree) — the electron-builder Linux
|
||||||
|
icon. This is the Step-2 packaging fix: regenerate it from
|
||||||
|
`KeynectrAppIconPossibility02.jpeg` (or point electron-builder at the new asset),
|
||||||
|
verify `npm run dist`, then commit. **Not done in this session.**
|
||||||
|
- **`CHECKPOINT-encryption.md`** — this file, committed to reference the post-triage
|
||||||
|
HEAD (`6eff510`). It must be re-updated to reference the new HEAD once Step 2
|
||||||
|
(packaging) lands its own commit.
|
||||||
|
- Pre-existing untracked hygiene leftovers (out of scope, address in the hygiene pass):
|
||||||
|
`COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`, `.opencode/`, `.impeccable/`,
|
||||||
|
`.directory`. **`profiles_vault.json*` and `target/` remain correctly untracked and
|
||||||
|
uncommitted** (vault is gitignored).
|
||||||
|
|
||||||
- Rust: `cargo fmt --check` clean; `cargo test` — 119 passed;
|
## Deferred / next steps (unchanged, plus new)
|
||||||
`cargo clippy --all-targets` — clean, zero warnings; `cargo build --release` — success.
|
- **Step 2 (packaging)**: restore `frontend/build/icon.png`, verify `npm run dist`.
|
||||||
- Frontend: `npm test` — 16 files / 110 passed;
|
- Signer abstraction (Step 3): promote `src/signer` to the single `Signer` source of
|
||||||
`npm run typecheck` clean; `npm run lint` clean (only harmless ES-module warning);
|
truth; introduce `SigningBackend { Internal{..}, Remote{..} }` per profile and route
|
||||||
`npm run format:check` clean; `npm run build` — success (Vite bundle built);
|
every IPC handler through the trait (no inline mode branching).
|
||||||
`npm run electron:build` — success.
|
- External-signer permissions (Step 4): wire `src/signer/permissions.rs` into the
|
||||||
|
approval modal so grants (kinds, relays, expiry, rate) are persisted AND enforced in
|
||||||
## How to resume / reproduce
|
the UI, not just parsed.
|
||||||
- Build + run the GUI: `cargo build --release && cd frontend && npm run build &&
|
- Deferred security (Step 5): KDF upgrade to m=64 MiB / t=3 with vault-header versioning
|
||||||
npm run electron:build && npm start` (dev: `npm run dev` in one terminal +
|
+ backward-compatible migration; `--allow-env-secret` flag; remove or gate deprecated
|
||||||
`NOSTR_GUI_DEV_URL=http://localhost:5173 npm start` in a second, or
|
`RevealSecretKey` IPC behind the same fail-closed path.
|
||||||
`npm run start:dev`).
|
- Undo history (Step 6): resolve the `ProfileSummary`-loses-the-secret question.
|
||||||
- Choose signer mode: Open **Signer Mode** from sidebar → select "Embedded Signer" or "NIP-46 Remote Signer".
|
- Hygiene (Step 7): Keynctr rename pass, delete legacy Python, migrate root
|
||||||
- For NIP-46 mode: In your Nostr app (Amber, Nostr Connect, etc.), choose "use a remote signer",
|
`profiles_vault.json*` into `~/.local/share/keynectr`.
|
||||||
copy the `nostrconnect://` link, paste it in Keynctr's Signer Mode screen, and click Connect.
|
- Open question carried forward: `migrate_vault_signer_modes` currently reports a change
|
||||||
- Switch modes anytime without changing your public key (same `npub`).
|
on every load (always `changed = true`), so `App::load` re-saves the vault each start.
|
||||||
- Signing workflow: When a sensitive operation needs approval, a prompt appears with event kind,
|
Harmless (idempotent) but wasteful; tighten to only report real changes.
|
||||||
content preview, and destination relays. Click Approve or Reject.
|
|
||||||
|
|
||||||
## Threat model summary
|
|
||||||
| Aspect | Embedded Signer | NIP-46 Client |
|
|
||||||
|--------|-----------------|---------------|
|
|
||||||
| **Key Location** | Local encrypted vault | Remote signer (never on this device) |
|
|
||||||
| **Compromise Impact** | Full key extraction if vault unlocked + malware | Attacker can *request* signatures, cannot extract key |
|
|
||||||
| **Phishing Resistance** | None (local UI spoofable) | None (NIP-46 doesn't prevent malicious requests) |
|
|
||||||
| **Device Theft** | Vault encrypted at rest; unlock needed | No key on device; connection revocable |
|
|
||||||
| **Malicious Relay** | N/A (local signing) | Relay sees only encrypted NIP-44 payloads |
|
|
||||||
| **Connection Leak** | N/A | Attacker can request signatures until revoked |
|
|
||||||
| **Replay Protection** | N/A | NIP-46 uses unique request IDs + timestamps |
|
|
||||||
|
|
||||||
## Outstanding / next-step items
|
|
||||||
- OS keyring integration (GNOME Keyring, KWallet, macOS Keychain, Windows Credential Manager)
|
|
||||||
for vault encryption key storage as optional enhancement
|
|
||||||
- Hardware wallet NIP-46 signer integration testing
|
|
||||||
- Connection URI rotation / periodic re-pairing for NIP-46
|
|
||||||
- Session binding to specific device/account where platform allows
|
|
||||||
- Comprehensive NIP-46 client test suite (mock signer, timeout/rejection scenarios)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue