Files
signer/plans/port_nsigner_to_rust.md
T

386 lines
19 KiB
Markdown

# n_signer → Rust Port Implementation Plan
## Overview
Port the C-based `n_signer` (located at `~/lt/n_signer`) to Rust, targeting **x86_64 Linux** only. Microcontroller implementations (ESP32, KB2040, etc.) are excluded. The Rust port will use `~/lt/nostr_core_lib_rust` as the crypto/Nostr protocol library.
## Architecture
```mermaid
graph TB
subgraph "n_signer_rust"
Main[main.rs<br/>CLI + startup + TUI loop]
Main --> SecureMem
Main --> Mnemonic
Main --> RoleWizard
Main --> Server
Main --> Policy
SecureMem[secure_mem.rs<br/>mlock + zeroize]
Mnemonic[mnemonic.rs<br/>BIP-39 via nips::nip006]
RoleTable[role_table.rs<br/>role entries + path templates]
Selector[selector.rs<br/>role resolution]
Enforcement[enforcement.rs<br/>verb/algorithm validation]
Policy[policy.rs<br/>caller access control]
Dispatcher[dispatcher.rs<br/>JSON-RPC 2.0 routing]
Dispatcher --> KeyStore
Dispatcher --> AlgCache
Dispatcher --> OtpPad
Dispatcher --> Miner
KeyStore[key_store.rs<br/>derived keys via nostr_core]
AlgCache[alg_cache.rs<br/>on-demand key derivation]
OtpPad[otp_pad.rs<br/>one-time pad encrypt/decrypt]
Miner[miner.rs<br/>NIP-13 PoW via nips::nip013]
Server[server.rs<br/>multi-transport poll loop]
Server --> Transport
Server --> AuthEnvelope
Server --> Policy
Server --> Dispatcher
Transport[transport.rs<br/>framed JSON + HTTP]
AuthEnvelope[auth_envelope.rs<br/>request authentication]
PQCrypto[pq_crypto.rs<br/>ed25519, x25519, PQ algorithms]
Tui[tui.rs<br/>terminal UI]
end
subgraph "nostr_core_lib_rust"
Core[core::<br/>keys, sha256, hmac, nip44]
Nips[nips::<br/>nip001, nip004, nip006, nip013, nip019, nip044]
SignerLib[signer::<br/>NostrSigner trait]
end
KeyStore --> Core
KeyStore --> Nips
Dispatcher --> Nips
Miner --> Nips
PQCrypto --> Core
```
## Dependency Mapping: C → Rust
| C Module | Rust Module | nostr_core_lib_rust Usage | External Crates |
|----------|-------------|--------------------------|------------------|
| `secure_mem.c` | `secure_mem.rs` | — | `zeroize`, `libc` (mlock/munlock) |
| `mnemonic.c` | `mnemonic.rs` | `nips::nip006` (BIP-39 wordlist, mnemonic_to_seed) | — |
| `role_table.c` | `role_table.rs` | — | — |
| `selector.c` | `selector.rs` | — | — |
| `enforcement.c` | `enforcement.rs` | — | — |
| `policy.c` | `policy.rs` | — | — |
| `key_store.c` | `key_store.rs` | `core::crypto::keys`, `nips::nip001`, `nips::nip004`, `nips::nip044` | `secp256k1` (via nostr_core) |
| `pq_crypto.c` | `pq_crypto.rs` | — | `ed25519-dalek`, `x25519-dalek`, `pqcrypto` (or vendored PQClean) |
| `pq_drbg.c` | `pq_drbg.rs` | — | `sha3` (SHAKE-256) |
| `dispatcher.c` | `dispatcher.rs` | `nips::nip001`, `nips::nip004`, `nips::nip044`, `nips::nip013` | `serde_json` |
| `server.c` | `server.rs` | — | `libc` (sockets, SO_PEERCRED) |
| `transport_frame.c` | `transport.rs` | — | — |
| `http_listener.c` | `http.rs` | — | — |
| `auth_envelope.c` | `auth_envelope.rs` | `core::crypto::keys` (verify) | — |
| `miner.c` | `miner.rs` | `nips::nip013` | `std::thread` |
| `otp_pad.c` | `otp_pad.rs` | — | — |
| `socket_name.c` | `socket_name.rs` | `nips::nip006` (BIP-39 wordlist for random names) | — |
| `main.c` (TUI) | `tui.rs` | — | `crossterm` or `ratatui` |
| `main.c` (CLI) | `main.rs` | — | `clap` |
## Key Design Decisions
### 1. Memory Safety
- **C**: Manual `mlock`/`munlock` + `explicit_bzero` via `secure_buf_t`
- **Rust**: `zeroize` crate with `ZeroizeOnDrop` derive. Wrap sensitive buffers in a `SecureBuf` type that calls `mlock` on alloc and `munlock`+`zeroize` on drop. Use `libc::mlock`/`libc::munlock` for the syscall.
### 2. Concurrency Model
- **C**: Single-threaded poll loop with detached pthread for `nostr_mine_event`
- **Rust**: Same model — single-threaded `poll(2)` loop via `mio` or raw `libc::poll`. Mining runs in `std::thread::spawn`. No async runtime needed (the C version is sync).
### 3. JSON Handling
- **C**: cJSON (manual parse/build)
- **Rust**: `serde_json` with typed structs for request/response
### 4. Error Handling
- **C**: Integer error codes + string messages
- **Rust**: `thiserror`-based `SignerError` enum. The JSON-RPC error codes are preserved exactly for wire compatibility.
### 5. Transport
- **C**: Raw syscalls (`socket`, `bind`, `accept`, `SO_PEERCRED`)
- **Rust**: `std::os::unix::net::UnixListener` + `libc` for `SO_PEERCRED`. TCP via `std::net::TcpListener`. HTTP via a minimal hand-rolled parser (same as C — no framework).
### 6. TUI
- **C**: `tui_continuous` vendored library
- **Rust**: `crossterm` for terminal control + custom rendering (or `ratatui` if it fits the continuous-redraw model). Start with `crossterm` + manual rendering to match the C behavior closely.
### 7. PQ Crypto
- **C**: PQClean vendored sources + custom DRBG
- **Rust**: Use `ed25519-dalek` and `x25519-dalek` for classic curves. For PQ algorithms (ML-DSA-65, SLH-DSA-128s, ML-KEM-768), either:
- **Option A**: FFI to the existing PQClean C code (fastest path to working)
- **Option B**: Use `pqcrypto` crate or vendor the PQClean Rust ports
- **Recommendation**: Start with Option A (FFI) for Phase 13, migrate to pure Rust later
### 8. nostr_core_lib_rust Integration
The Rust library provides:
- `core::crypto::keys` — secp256k1 key generation, Schnorr sign/verify, ECDH
- `core::crypto::hmac` — HMAC-SHA256/512, HKDF
- `core::crypto::nip44` — NIP-44 encryption (ChaCha20-Poly1305)
- `core::types` — Event, PublicKey, SecretKey, Signature, etc.
- `nips::nip001` — `create_and_sign_event`, `validate_event`
- `nips::nip004` — NIP-04 encrypt/decrypt
- `nips::nip006` — BIP-39 mnemonic generation, validation, `mnemonic_to_seed`, `keypair_from_seed`
- `nips::nip013` — NIP-13 proof-of-work mining
- `nips::nip019` — bech32 encoding (npub, nsec)
- `signer::NostrSigner` trait — can be used for the signer abstraction
**Gap**: `nips::nip006::keypair_from_seed` currently uses the master key directly rather than doing full BIP-32 derivation through `m/44'/1237'/0'/0/0`. The port needs to implement proper BIP-32 HD derivation (either add it to `nostr_core_lib_rust` or implement locally in n_signer).
## Phased Implementation
### Phase 1: Project Scaffolding
- [ ] Create `Cargo.toml` workspace with path dependency on `~/lt/nostr_core_lib_rust`
- [ ] Create `src/` module structure matching the C source layout
- [ ] Add dependencies: `serde`, `serde_json`, `libc`, `zeroize`, `clap`, `crossterm`, `hex`, `base64`, `thiserror`, `secp256k1` (transitive via nostr_core)
- [ ] Create `build.rs` if PQClean FFI is needed
- [ ] Verify `cargo build` compiles with empty module stubs
### Phase 2: Secure Memory (`secure_mem.rs`)
- [ ] `SecureBuf` struct: holds `Vec<u8>`, `locked: bool`
- [ ] `SecureBuf::alloc(size)` — malloc + `libc::mlock` + zero init
- [ ] `SecureBuf::free()` — `zeroize` + `libc::munlock` + drop
- [ ] `secure_memzero()` — use `zeroize::zeroize()`
- [ ] `allow_unlocked()` flag for dev mode
- [ ] Implement `Drop` trait for automatic cleanup
- [ ] Unit tests: alloc/free, zeroization verification
### Phase 3: Mnemonic (`mnemonic.rs`)
- [ ] `MnemonicState` struct: holds `SecureBuf`, `loaded: bool`, `word_count: u8`
- [ ] `mnemonic_load(phrase)` — validate via `nips::nip006::mnemonic_validate`, store in `SecureBuf`
- [ ] `mnemonic_generate(word_count)` — use `nips::nip006::mnemonic_from_bytes` with `rand::thread_rng` entropy
- [ ] `mnemonic_unload()` — wipe `SecureBuf`
- [ ] Unit tests: valid/invalid mnemonics, word count validation
### Phase 4: Role Table, Selector, Enforcement
- [ ] `RolePurpose` enum: Nostr, Bitcoin, Ssh, Age, Fips, PqSig, PqKem
- [ ] `RoleCurve` enum: Secp256k1, Ed25519, X25519, MlDsa65, SlhDsa128s, MlKem768
- [ ] `RoleEntry` struct: name, purpose, curve, selector_type, path, range, allowed_indices, requires_approval
- [ ] `RoleTable` struct: Vec of entries, add/find/register methods
- [ ] Path template parser: wildcard (`*`), range (`N-M`), set (`A+B+C`), fixed path
- [ ] `SelectorRequest` struct: has_role, has_role_path, has_index
- [ ] `selector_resolve()` — match role+path against table
- [ ] `enforce_verb_role()` — check purpose==Nostr && curve==Secp256k1 for nostr verbs
- [ ] `enforce_verb_algorithm()` — check verb+algorithm validity
- [ ] Unit tests: path template parsing, selector resolution, enforcement matrix
### Phase 5: Policy (`policy.rs`)
- [ ] `PromptMode` enum: Never, FirstPerBoot, EveryRequest, Deny
- [ ] `PolicyEntry` struct: caller, verbs, roles, purposes, algorithms, index range, prompt mode, source
- [ ] `PolicyTable` struct: Vec of entries
- [ ] `policy_check()` — role-based check
- [ ] `policy_check_algorithm()` — algorithm-based check
- [ ] `policy_check_with_role()` — role-as-password shortcut (requires_approval==0 → allow)
- [ ] `parse_preapprove_spec()` — parse `caller=uid:1000,role=main,verb=sign`
- [ ] Session grants: insert before catch-all
- [ ] Unit tests: policy matching, preapprove parsing, session grants
### Phase 6: Key Store & Crypto (`key_store.rs` + `alg_cache.rs`)
- [ ] `DerivedKey` struct: private_key (SecureBuf), public_key (SecureBuf), pubkey_hex, npub, alg
- [ ] `KeyStore` struct: Vec of derived keys per role
- [ ] BIP-32 HD derivation: implement proper path derivation (`m/44'/1237'/<n>'/0/0`)
- Either add BIP-32 to `nostr_core_lib_rust` or implement locally using `secp256k1` crate
- [ ] SLIP-0010 derivation for ed25519/x25519 (HMAC-SHA512, all-hardened paths)
- [ ] `crypto_derive_all()` — derive keys for all roles
- [ ] `crypto_derive_one()` — derive for a single role (on-demand)
- [ ] `crypto_sign_event()` — via `nips::nip001::create_and_sign_event`
- [ ] `crypto_nip04_encrypt/decrypt()` — via `nips::nip004`
- [ ] `crypto_nip44_encrypt/decrypt()` — via `core::crypto::nip44`
- [ ] `AlgorithmKeyCache` — FIFO cache of on-demand derived keys (alg, index) → keypair
- [ ] Unit tests: derivation determinism, sign/verify roundtrip
### Phase 7: Dispatcher (`dispatcher.rs`)
- [ ] `DispatcherContext` struct: references to role_table, mnemonic, key_store, alg_cache
- [ ] `dispatcher_handle_request(json)` → response JSON string
- [ ] Parse JSON-RPC: id, method, params, options
- [ ] Route algorithm verbs: `get_public_key`, `sign`, `verify`, `encapsulate`, `decapsulate`, `derive_shared_secret`, `derive`
- [ ] Route nostr verbs: `nostr_get_public_key`, `nostr_sign_event`, `nostr_mine_event`, `nostr_nip04_*`, `nostr_nip44_*`
- [ ] Route OTP verbs: `encrypt`, `decrypt`
- [ ] Route metadata: `get_info`
- [ ] Error response builder with exact error codes from C API
- [ ] Unit tests: each verb with valid/invalid params
### Phase 8: Transport (`transport.rs` + `http.rs`)
- [ ] `transport_send_framed(fd, payload)` — 4-byte BE length prefix + data
- [ ] `transport_recv_framed(fd)` — read length, then payload
- [ ] `http_recv_request(fd)` — minimal HTTP/1.1 POST parser
- [ ] `http_send_response(fd, json)` — HTTP 200 + CORS headers
- [ ] `http_send_error(fd, code, message)`
- [ ] `http_send_cors_preflight(fd)` — OPTIONS response
- [ ] Unit tests: framing roundtrip, HTTP parse
### Phase 9: Server (`server.rs`)
- [ ] `ListenMode` enum: Unix, Stdio, Qrexec, Tcp, Http
- [ ] `CallerIdentity` struct: uid, gid, pid, kind, caller_id, source_qube, auth fields
- [ ] `ServerContext` struct: socket_name, listen_fd, listen_mode, dispatcher, policy, auth_mode, whitelists
- [ ] `server_start()` — bind socket (abstract Unix / TCP / HTTP)
- [ ] `server_handle_one()` — accept, read request, auth verify, policy check, dispatch, send response
- [ ] `server_get_caller()` — `SO_PEERCRED` for Unix, `getpeername` for TCP
- [ ] Bridge-source-trusted preamble handling
- [ ] Approval callback mechanism
- [ ] Non-blocking poll loop integration
- [ ] Integration tests: Unix socket roundtrip, HTTP roundtrip
### Phase 10: Miner (`miner.rs`)
- [ ] `MineResult` struct: best_event, achieved_difficulty, target_difficulty, target_reached, elapsed_sec, total_attempts
- [ ] `miner_run(event, private_key, target_difficulty, thread_count, timeout_sec)` → MineResult
- [ ] Multi-threaded: each thread tries nonces with unique stride
- [ ] Stop on target reached or timeout
- [ ] Use `nips::nip013` for PoW computation
- [ ] Unit tests: difficulty achievement, timeout behavior
### Phase 11: Auth Envelope (`auth_envelope.rs`)
- [ ] `AuthNonceCache` — per-pubkey monotonic timestamp + event ID tracking
- [ ] `auth_envelope_verify_request()` — verify auth field in JSON-RPC request
- [ ] Replay protection: check created_at > max_seen, check event_id uniqueness
- [ ] Unit tests: valid/invalid auth, replay detection
### Phase 12: OTP Pad (`otp_pad.rs`)
- [ ] `OtpPadState` struct: bound, pads_dir, chksum, pad_path, pad_file, pad_size, offset, scratch buffer
- [ ] `otp_pad_bind(dir, spec, allow_blkback)` — open pad file, read checksum, verify
- [ ] `otp_pad_encrypt(plaintext, encoding)` — XOR with pad bytes, advance offset
- [ ] `otp_pad_decrypt(ciphertext, encoding)` — reverse XOR
- [ ] ASCII armor encoding + binary encoding
- [ ] Pad offset persistence (`.state` file)
- [ ] Unit tests: encrypt/decrypt roundtrip, offset advancement
### Phase 13: PQ Crypto (`pq_crypto.rs` + `pq_drbg.rs`)
- [ ] `CryptoAlg` enum: Secp256k1, Ed25519, X25519, MlDsa65, SlhDsa128s, MlKem768
- [ ] `CryptoAlgSizes` struct: priv/pub/sig/ciphertext/shared_secret lengths
- [ ] ed25519: `ed25519-dalek` crate
- [ ] x25519: `x25519-dalek` crate
- [ ] SHAKE-256 DRBG: `sha3` crate
- [ ] ML-DSA-65, SLH-DSA-128s, ML-KEM-768: FFI to PQClean (initial), pure Rust later
- [ ] `crypto_alg_from_role()`, `crypto_alg_from_str()`, `crypto_alg_to_str()`
- [ ] Keygen, sign, verify, encaps, decaps for each algorithm
- [ ] Unit tests: keygen determinism, sign/verify roundtrip
### Phase 14: TUI (`tui.rs`)
- [ ] Terminal setup via `crossterm`
- [ ] `render_status()` — roles table, activity log, status line, menu
- [ ] `render_connections()` — on-demand connection display (press `d`)
- [ ] Approval prompt screen
- [ ] Role wizard (preset menu, path editor)
- [ ] Transport selection menu
- [ ] Mnemonic entry screen (echo disabled)
- [ ] Mnemonic generation display
- [ ] Index whitelist prompt
- [ ] OTP pad selection prompt
- [ ] Hotkeys: `l` (lock), `r` (refresh), `d` (connections), `q` (quit)
- [ ] Terminal resize handling
### Phase 15: Main (`main.rs`)
- [ ] CLI parsing with `clap`: `--socket-name`, `--listen`, `--preapprove`, `--register-role`, `--auth`, `--mnemonic-stdin`, `--mnemonic-fd`, `--allow-all`, `--bridge-source-trusted`, `--otp-pad-dir`, `--otp-pad`
- [ ] Subcommands: `client`, `bridge`, `list`
- [ ] Startup flow: mnemonic → roles → transport → socket name → server start → TUI loop
- [ ] Session lock/re-unlock
- [ ] Signal handling: SIGINT, SIGTERM, SIGPIPE (ignore)
- [ ] Shutdown: wipe all secrets, close sockets
- [ ] Non-interactive mode: `--mnemonic-stdin` / `--mnemonic-fd` / `--register-role`
### Phase 16: Integration Tests & Build
- [ ] End-to-end test: start signer, send `get_info`, `get_public_key`, `nostr_sign_event`
- [ ] Algorithm verb tests: `sign`/`verify` for each algorithm
- [ ] NIP-04/NIP-44 encrypt/decrypt roundtrip
- [ ] NIP-13 mining test
- [ ] Policy enforcement tests: allow/deny/prompt
- [ ] Multi-transport test: Unix + HTTP simultaneously
- [ ] OTP encrypt/decrypt test
- [ ] `cargo build --release` verification
- [ ] Static build target (musl) investigation
## File Structure
```
signer/
├── Cargo.toml
├── build.rs # PQClean FFI build script (Phase 13)
├── src/
│ ├── main.rs # CLI + startup + TUI loop
│ ├── lib.rs # Public API re-exports
│ ├── secure_mem.rs # SecureBuf, mlock, zeroize
│ ├── mnemonic.rs # BIP-39 mnemonic state
│ ├── role_table.rs # Role entries, path templates
│ ├── selector.rs # Role selector resolution
│ ├── enforcement.rs # Verb/role/algorithm enforcement
│ ├── policy.rs # Caller access control
│ ├── key_store.rs # Derived key storage
│ ├── alg_cache.rs # On-demand algorithm key cache
│ ├── pq_crypto.rs # ed25519, x25519, PQ algorithms
│ ├── pq_drbg.rs # SHAKE-256 DRBG for PQ keygen
│ ├── dispatcher.rs # JSON-RPC 2.0 request routing
│ ├── server.rs # Multi-transport server
│ ├── transport.rs # Length-prefixed framing
│ ├── http.rs # Minimal HTTP/1.1 parser
│ ├── auth_envelope.rs # Request authentication
│ ├── miner.rs # NIP-13 PoW mining
│ ├── otp_pad.rs # One-time pad encryption
│ ├── socket_name.rs # Abstract socket naming
│ ├── tui.rs # Terminal UI
│ └── error.rs # SignerError enum
├── tests/
│ ├── integration_test.rs
│ ├── algorithm_test.rs
│ ├── policy_test.rs
│ └── transport_test.rs
└── resources/
└── pqclean/ # Vendored PQClean (if FFI path)
```
## Cargo.toml (Draft)
```toml
[package]
name = "signer"
version = "0.1.0"
edition = "2021"
[dependencies]
nostr-core = { path = "../nostr_core_lib_rust/nostr-core" }
nips = { path = "../nostr_core_lib_rust/nips" }
serde = { workspace = true }
serde_json = { workspace = true }
zeroize = { workspace = true }
libc = "0.2"
clap = { version = "4", features = ["derive"] }
crossterm = "0.27"
hex = { workspace = true }
base64 = { workspace = true }
thiserror = { workspace = true }
secp256k1 = { workspace = true }
sha2 = { workspace = true }
hmac = { workspace = true }
rand = { workspace = true }
ed25519-dalek = "2"
x25519-dalek = "2"
sha3 = "0.10"
[dev-dependencies]
tempfile = "3"
```
## Compatibility Requirements
1. **Wire protocol**: JSON-RPC 2.0 request/response format must be byte-identical to C version
2. **Error codes**: All error codes (-32700, -32600, 1001-2009) must match exactly
3. **Derivation paths**: BIP-32/SLIP-0010 paths must produce identical keys from the same mnemonic
4. **Socket protocol**: Length-prefixed framing (4-byte BE) must be compatible
5. **HTTP**: Same minimal HTTP/1.1 POST-only parser behavior
6. **Abstract socket names**: `@signer_<word1>_<word2>` format preserved
## Open Questions
1. **BIP-32 derivation**: `nostr_core_lib_rust` `nips::nip006::keypair_from_seed` does not do full BIP-32 HD derivation through the path. Should we:
- (a) Add proper BIP-32 to `nostr_core_lib_rust`, or
- (b) Implement BIP-32 locally in n_signer using the `secp256k1` crate directly?
2. **PQ crypto**: Should we FFI to PQClean C code initially, or find/use Rust PQ crates?
3. **TUI scope**: Full TUI parity with C version (role wizard, transport menu, approval prompts, connection display), or start with a simpler CLI?