first
This commit is contained in:
325
README.md
Normal file
325
README.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# n_signer
|
||||
|
||||
`n_signer` is a single statically-linked program that holds signing key material in locked memory and signs on request.
|
||||
|
||||
It runs in the foreground, attached to your terminal. The terminal is the trust anchor and control surface. Nothing touches disk at runtime. If the process crashes or exits, all in-memory state is gone.
|
||||
|
||||
This is a **program, not a daemon**:
|
||||
|
||||
- no hidden background process
|
||||
- no detached service lifecycle
|
||||
- no runtime config or state files
|
||||
- no persistence to recover after compromise
|
||||
|
||||
## 1. What it is
|
||||
|
||||
`n_signer` is one musl-static binary that combines:
|
||||
|
||||
- mnemonic handling
|
||||
- role selection and derivation
|
||||
- purpose/curve enforcement
|
||||
- request dispatch
|
||||
- interactive terminal UI
|
||||
- transport adapter(s)
|
||||
|
||||
You run it when you need signing. You stop it when you are done. Closing the terminal or quitting the program ends the trust session and destroys state.
|
||||
|
||||
## 2. Security model
|
||||
|
||||
### 2.1 Zero filesystem footprint
|
||||
|
||||
At runtime, `n_signer` writes nothing to disk:
|
||||
|
||||
- no config files
|
||||
- no logs
|
||||
- no PID files
|
||||
- no lock files
|
||||
- no socket pathname artifacts
|
||||
|
||||
On Linux desktop, local IPC uses abstract namespace Unix sockets (`@name` semantics) that exist only in kernel memory and disappear with process/kernel namespace lifetime.
|
||||
|
||||
### 2.2 Crash = total wipe
|
||||
|
||||
All sensitive and operational state exists only in-process RAM (mlock'd where applicable):
|
||||
|
||||
- mnemonic-derived key material
|
||||
- role table
|
||||
- policy/approval decisions for the live session
|
||||
- activity display buffer
|
||||
|
||||
If the process dies (fault, kill, exploit, power loss), state is unrecoverable by design. There is no persistence layer to scrape post-crash.
|
||||
|
||||
### 2.3 Single binary, no external dependencies
|
||||
|
||||
Runtime target is one statically-linked musl executable:
|
||||
|
||||
- no shared libraries required at runtime
|
||||
- no interpreter/runtime VM dependency
|
||||
- no sidecar services
|
||||
- no helper daemon binaries
|
||||
|
||||
This reduces deployment variability and shrinks the runtime trust surface.
|
||||
|
||||
### 2.4 Always-attended operation
|
||||
|
||||
`n_signer` is intentionally human-attended. It stays attached to a terminal and can require explicit keystroke approval for unknown callers or sensitive actions. Human presence is part of the security model.
|
||||
|
||||
## 3. How it works
|
||||
|
||||
### 3.1 Startup phase (TUI input mode)
|
||||
|
||||
When started, `n_signer` immediately enters terminal input mode:
|
||||
|
||||
1. Prompt for mnemonic (terminal echo disabled).
|
||||
2. Parse and validate mnemonic.
|
||||
3. Build in-memory role/selector state.
|
||||
4. Initialize transport endpoints.
|
||||
5. Switch to running status display.
|
||||
|
||||
No startup files are read or written.
|
||||
|
||||
### 3.2 Running phase (status display + signer)
|
||||
|
||||
After unlock, terminal becomes a live status and control console. Example layout:
|
||||
|
||||
```text
|
||||
n_signer v0.x | foreground session active
|
||||
transport: unix-abstract:@nsigner
|
||||
session: unlocked (RAM-only)
|
||||
|
||||
Roles
|
||||
-----
|
||||
main purpose=nostr curve=secp256k1 selector=role:main
|
||||
ops purpose=nostr curve=secp256k1 selector=nostr_index:7
|
||||
backup purpose=bitcoin curve=secp256k1 selector=role_path:m/84'/0'/0'/0/5
|
||||
|
||||
Pending approvals
|
||||
-----------------
|
||||
(none)
|
||||
|
||||
Activity (latest first)
|
||||
-----------------------
|
||||
16:03:11 allow caller=uid:1000 method=get_public_key role=main
|
||||
16:02:44 prompt caller=uid:1000 method=sign_event role=ops
|
||||
16:02:46 allow caller=uid:1000 method=sign_event role=ops
|
||||
15:59:10 deny caller=uid:1001 method=sign_event error=unauthorized
|
||||
|
||||
Hotkeys
|
||||
-------
|
||||
a add role
|
||||
r refresh
|
||||
l lock session
|
||||
q quit
|
||||
```
|
||||
|
||||
### 3.3 Approval prompts
|
||||
|
||||
When a request needs confirmation, `n_signer` interrupts the status view with a prompt and waits for a local keystroke.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
Approval required
|
||||
caller: uid:1000
|
||||
method: sign_event
|
||||
selector: role=ops
|
||||
purpose/curve: nostr/secp256k1
|
||||
|
||||
[y] allow once [n] deny [a] always allow for this session
|
||||
```
|
||||
|
||||
No response is emitted to caller until the local user decides.
|
||||
|
||||
### 3.4 Shutdown / lock
|
||||
|
||||
- `q` quits the process: all session state is destroyed.
|
||||
- `l` locks the session in-place: signing stops until mnemonic is re-entered.
|
||||
- terminal close or process termination has the same effect as quit: total state wipe.
|
||||
|
||||
## 4. Mnemonic-rooted role model
|
||||
|
||||
`n_signer` derives many role-scoped keys from one mnemonic root. Requests select a role using explicit selectors, then enforcement checks operation compatibility.
|
||||
|
||||
### 4.1 Nostr shorthand: nostr_index
|
||||
|
||||
Nostr shorthand keeps the explicit index selector:
|
||||
|
||||
`m/44'/1237'/<nostr_index>'/0/0`
|
||||
|
||||
Use `nostr_index` only for Nostr-indexed roles.
|
||||
|
||||
### 4.2 Full derivation path: role_path
|
||||
|
||||
For non-Nostr or advanced layouts, caller may use full `role_path` selector.
|
||||
|
||||
Security rule: `role_path` must match a pre-registered role entry. Unregistered ad-hoc derivation requests are rejected.
|
||||
|
||||
### 4.3 What memorizing your seed phrase gets you
|
||||
|
||||
A single memorized mnemonic can deterministically recover multiple key domains through role definitions, not just one identity.
|
||||
|
||||
Examples include Nostr roles, Bitcoin branches, and future application-specific paths. See [`plans/seed_phrase_uses.md`](plans/seed_phrase_uses.md) for the maintained use-case catalog and caveats.
|
||||
|
||||
## 5. Wire contract (JSON-RPC)
|
||||
|
||||
Request shape is JSON-RPC with NIP-46-style methods and optional trailing selector options.
|
||||
|
||||
```jsonc
|
||||
{ "id": "1", "method": "get_public_key", "params": [] }
|
||||
{ "id": "2", "method": "sign_event", "params": ["<event_json>", { "role": "main" }] }
|
||||
{ "id": "3", "method": "sign_event", "params": ["<event_json>", { "nostr_index": 7 }] }
|
||||
{ "id": "4", "method": "sign_event", "params": ["<event_json>", { "role_path": "m/84'/0'/0'/0/5", "purpose": "bitcoin", "curve": "secp256k1" }] }
|
||||
```
|
||||
|
||||
Selector resolution order:
|
||||
|
||||
1. `role`
|
||||
2. `nostr_index`
|
||||
3. `role_path`
|
||||
4. default role `main`
|
||||
|
||||
Conflicting selectors are rejected (`ambiguous_role_selector`).
|
||||
|
||||
Representative error codes:
|
||||
|
||||
- `invalid_request`
|
||||
- `method_not_found`
|
||||
- `ambiguous_role_selector`
|
||||
- `unknown_role`
|
||||
- `purpose_mismatch`
|
||||
- `curve_mismatch`
|
||||
- `unauthorized`
|
||||
- `approval_denied`
|
||||
- `internal_error`
|
||||
|
||||
## 6. Purpose and curve enforcement
|
||||
|
||||
Selector resolution chooses *which* role. Enforcement decides *whether the requested method is valid* for that role.
|
||||
|
||||
Example:
|
||||
|
||||
- `sign_event` requires `purpose="nostr"` and `curve="secp256k1"`.
|
||||
- If caller selects a Bitcoin-role key for `sign_event`, request fails with `purpose_mismatch`.
|
||||
|
||||
This prevents cross-protocol misuse inside one mnemonic-rooted signer process.
|
||||
|
||||
## 7. Transport
|
||||
|
||||
### 7.1 Linux desktop: abstract namespace Unix socket
|
||||
|
||||
Primary local transport is AF_UNIX abstract namespace (for example `@nsigner`).
|
||||
|
||||
Properties:
|
||||
|
||||
- no pathname in filesystem
|
||||
- endpoint lifetime bound to process/kernel namespace
|
||||
- no stale socket files
|
||||
- caller identity via peer credentials (`SO_PEERCRED`)
|
||||
|
||||
### 7.2 ESP32 MCU: USB-CDC serial
|
||||
|
||||
On MCU targets, the same core modules run with a different transport adapter: USB-CDC serial framing instead of AF_UNIX.
|
||||
|
||||
Core logic stays the same; only transport/UI bindings change.
|
||||
|
||||
### 7.3 NIP-46 relay flow (both platforms)
|
||||
|
||||
NIP-46 request/response flow can be bridged on both desktop and MCU transports. The JSON-RPC signer contract remains consistent while the outer carrier differs.
|
||||
|
||||
### 7.4 Caller verification
|
||||
|
||||
Every transport must provide concrete caller identity before policy evaluation.
|
||||
|
||||
- Linux AF_UNIX: map peer credentials to caller identity.
|
||||
- ESP32 USB bridge: map authenticated host bridge identity to caller identity.
|
||||
- Relay session: bind remote peer/session identity before allowing signer verbs.
|
||||
|
||||
Identity verification and interactive approval are separate layers. Passing identity checks does not bypass prompt requirements.
|
||||
|
||||
## 8. Platform targets
|
||||
|
||||
### 8.1 Linux desktop (primary)
|
||||
|
||||
Primary deployment is a local, foreground terminal program with abstract namespace socket transport.
|
||||
|
||||
### 8.2 ESP32 / MCU
|
||||
|
||||
MCU target reuses mnemonic/role/selector/enforcement/dispatcher core and swaps transport/UI for constrained hardware (for example UART/OLED/buttons or USB-CDC host interaction).
|
||||
|
||||
### 8.3 Qubes OS qube
|
||||
|
||||
Qubes deployment runs `n_signer` in a dedicated signer qube as a foreground process under explicit user session control. Transport binding is platform-specific, but lifecycle and memory-only state model are unchanged.
|
||||
|
||||
## 9. Usage
|
||||
|
||||
### 9.1 Run the program
|
||||
|
||||
```bash
|
||||
nsigner
|
||||
```
|
||||
|
||||
Program starts in attached foreground mode and prompts for mnemonic.
|
||||
|
||||
### 9.2 Send a request (client mode)
|
||||
|
||||
From another terminal:
|
||||
|
||||
```bash
|
||||
nsigner client '{"id":"1","method":"get_public_key","params":[]}'
|
||||
```
|
||||
|
||||
Example signing request:
|
||||
|
||||
```bash
|
||||
nsigner client '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}'
|
||||
```
|
||||
|
||||
### 9.3 Example session
|
||||
|
||||
Terminal A:
|
||||
|
||||
```text
|
||||
$ nsigner
|
||||
[unlock] enter mnemonic:
|
||||
[ok] session unlocked
|
||||
[listen] unix-abstract:@nsigner
|
||||
[prompt] caller=uid:1000 method=sign_event role=main -> allow? (y/n)
|
||||
```
|
||||
|
||||
Terminal B:
|
||||
|
||||
```text
|
||||
$ nsigner client '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}'
|
||||
{"id":"2","result":"<signed_event_json>"}
|
||||
```
|
||||
|
||||
## 10. Build and versioning
|
||||
|
||||
Build outputs a single executable artifact.
|
||||
|
||||
- [`build_static.sh`](build_static.sh): musl-static build path
|
||||
- [`Makefile`](Makefile): local build targets
|
||||
- [`Dockerfile.alpine-musl`](Dockerfile.alpine-musl): reproducible Alpine musl toolchain
|
||||
- [`increment_and_push.sh`](increment_and_push.sh): version/tag/release workflow
|
||||
- [`src/main.h`](src/main.h): version macros
|
||||
|
||||
Local dev build:
|
||||
|
||||
```bash
|
||||
make dev
|
||||
./build/nsigner_dev --version
|
||||
```
|
||||
|
||||
Static build:
|
||||
|
||||
```bash
|
||||
./build_static.sh
|
||||
./build/nsigner_static_x86_64 --version
|
||||
```
|
||||
|
||||
## 11. Document map
|
||||
|
||||
- [`README.md`](README.md): authoritative behavior specification for the foreground single-program model
|
||||
- [`plans/nsigner.md`](plans/nsigner.md): implementation plan and sequencing
|
||||
- [`plans/seed_phrase_uses.md`](plans/seed_phrase_uses.md): seed phrase domain/use catalog and caveats
|
||||
- [`firmware/README.md`](firmware/README.md): firmware-side notes for MCU transport/UI integration
|
||||
Reference in New Issue
Block a user