19 KiB
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
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_bzeroviasecure_buf_t - Rust:
zeroizecrate withZeroizeOnDropderive. Wrap sensitive buffers in aSecureBuftype that callsmlockon alloc andmunlock+zeroizeon drop. Uselibc::mlock/libc::munlockfor 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 viamioor rawlibc::poll. Mining runs instd::thread::spawn. No async runtime needed (the C version is sync).
3. JSON Handling
- C: cJSON (manual parse/build)
- Rust:
serde_jsonwith typed structs for request/response
4. Error Handling
- C: Integer error codes + string messages
- Rust:
thiserror-basedNsignerErrorenum. 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+libcforSO_PEERCRED. TCP viastd::net::TcpListener. HTTP via a minimal hand-rolled parser (same as C — no framework).
6. TUI
- C:
tui_continuousvendored library - Rust:
crosstermfor terminal control + custom rendering (orratatuiif it fits the continuous-redraw model). Start withcrossterm+ manual rendering to match the C behavior closely.
7. PQ Crypto
- C: PQClean vendored sources + custom DRBG
- Rust: Use
ed25519-dalekandx25519-dalekfor 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
pqcryptocrate 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, ECDHcore::crypto::hmac— HMAC-SHA256/512, HKDFcore::crypto::nip44— NIP-44 encryption (ChaCha20-Poly1305)core::types— Event, PublicKey, SecretKey, Signature, etc.nips::nip001—create_and_sign_event,validate_eventnips::nip004— NIP-04 encrypt/decryptnips::nip006— BIP-39 mnemonic generation, validation,mnemonic_to_seed,keypair_from_seednips::nip013— NIP-13 proof-of-work miningnips::nip019— bech32 encoding (npub, nsec)signer::NostrSignertrait — 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.tomlworkspace 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.rsif PQClean FFI is needed - Verify
cargo buildcompiles with empty module stubs
Phase 2: Secure Memory (secure_mem.rs)
SecureBufstruct: holdsVec<u8>,locked: boolSecureBuf::alloc(size)— malloc +libc::mlock+ zero initSecureBuf::free()—zeroize+libc::munlock+ dropsecure_memzero()— usezeroize::zeroize()allow_unlocked()flag for dev mode- Implement
Droptrait for automatic cleanup - Unit tests: alloc/free, zeroization verification
Phase 3: Mnemonic (mnemonic.rs)
MnemonicStatestruct: holdsSecureBuf,loaded: bool,word_count: u8mnemonic_load(phrase)— validate vianips::nip006::mnemonic_validate, store inSecureBufmnemonic_generate(word_count)— usenips::nip006::mnemonic_from_byteswithrand::thread_rngentropymnemonic_unload()— wipeSecureBuf- Unit tests: valid/invalid mnemonics, word count validation
Phase 4: Role Table, Selector, Enforcement
RolePurposeenum: Nostr, Bitcoin, Ssh, Age, Fips, PqSig, PqKemRoleCurveenum: Secp256k1, Ed25519, X25519, MlDsa65, SlhDsa128s, MlKem768RoleEntrystruct: name, purpose, curve, selector_type, path, range, allowed_indices, requires_approvalRoleTablestruct: Vec of entries, add/find/register methods- Path template parser: wildcard (
*), range (N-M), set (A+B+C), fixed path SelectorRequeststruct: has_role, has_role_path, has_indexselector_resolve()— match role+path against tableenforce_verb_role()— check purpose==Nostr && curve==Secp256k1 for nostr verbsenforce_verb_algorithm()— check verb+algorithm validity- Unit tests: path template parsing, selector resolution, enforcement matrix
Phase 5: Policy (policy.rs)
PromptModeenum: Never, FirstPerBoot, EveryRequest, DenyPolicyEntrystruct: caller, verbs, roles, purposes, algorithms, index range, prompt mode, sourcePolicyTablestruct: Vec of entriespolicy_check()— role-based checkpolicy_check_algorithm()— algorithm-based checkpolicy_check_with_role()— role-as-password shortcut (requires_approval==0 → allow)parse_preapprove_spec()— parsecaller=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)
DerivedKeystruct: private_key (SecureBuf), public_key (SecureBuf), pubkey_hex, npub, algKeyStorestruct: 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_rustor implement locally usingsecp256k1crate
- Either add BIP-32 to
- SLIP-0010 derivation for ed25519/x25519 (HMAC-SHA512, all-hardened paths)
crypto_derive_all()— derive keys for all rolescrypto_derive_one()— derive for a single role (on-demand)crypto_sign_event()— vianips::nip001::create_and_sign_eventcrypto_nip04_encrypt/decrypt()— vianips::nip004crypto_nip44_encrypt/decrypt()— viacore::crypto::nip44AlgorithmKeyCache— FIFO cache of on-demand derived keys (alg, index) → keypair- Unit tests: derivation determinism, sign/verify roundtrip
Phase 7: Dispatcher (dispatcher.rs)
DispatcherContextstruct: references to role_table, mnemonic, key_store, alg_cachedispatcher_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 + datatransport_recv_framed(fd)— read length, then payloadhttp_recv_request(fd)— minimal HTTP/1.1 POST parserhttp_send_response(fd, json)— HTTP 200 + CORS headershttp_send_error(fd, code, message)http_send_cors_preflight(fd)— OPTIONS response- Unit tests: framing roundtrip, HTTP parse
Phase 9: Server (server.rs)
ListenModeenum: Unix, Stdio, Qrexec, Tcp, HttpCallerIdentitystruct: uid, gid, pid, kind, caller_id, source_qube, auth fieldsServerContextstruct: socket_name, listen_fd, listen_mode, dispatcher, policy, auth_mode, whitelistsserver_start()— bind socket (abstract Unix / TCP / HTTP)server_handle_one()— accept, read request, auth verify, policy check, dispatch, send responseserver_get_caller()—SO_PEERCREDfor Unix,getpeernamefor 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)
MineResultstruct: best_event, achieved_difficulty, target_difficulty, target_reached, elapsed_sec, total_attemptsminer_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::nip013for PoW computation - Unit tests: difficulty achievement, timeout behavior
Phase 11: Auth Envelope (auth_envelope.rs)
AuthNonceCache— per-pubkey monotonic timestamp + event ID trackingauth_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)
OtpPadStatestruct: bound, pads_dir, chksum, pad_path, pad_file, pad_size, offset, scratch bufferotp_pad_bind(dir, spec, allow_blkback)— open pad file, read checksum, verifyotp_pad_encrypt(plaintext, encoding)— XOR with pad bytes, advance offsetotp_pad_decrypt(ciphertext, encoding)— reverse XOR- ASCII armor encoding + binary encoding
- Pad offset persistence (
.statefile) - Unit tests: encrypt/decrypt roundtrip, offset advancement
Phase 13: PQ Crypto (pq_crypto.rs + pq_drbg.rs)
CryptoAlgenum: Secp256k1, Ed25519, X25519, MlDsa65, SlhDsa128s, MlKem768CryptoAlgSizesstruct: priv/pub/sig/ciphertext/shared_secret lengths- ed25519:
ed25519-dalekcrate - x25519:
x25519-dalekcrate - SHAKE-256 DRBG:
sha3crate - 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, menurender_connections()— on-demand connection display (pressd)- 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/verifyfor 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 --releaseverification- 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 # NsignerError enum
├── tests/
│ ├── integration_test.rs
│ ├── algorithm_test.rs
│ ├── policy_test.rs
│ └── transport_test.rs
└── resources/
└── pqclean/ # Vendored PQClean (if FFI path)
Cargo.toml (Draft)
[package]
name = "nsigner"
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
- Wire protocol: JSON-RPC 2.0 request/response format must be byte-identical to C version
- Error codes: All error codes (-32700, -32600, 1001-2009) must match exactly
- Derivation paths: BIP-32/SLIP-0010 paths must produce identical keys from the same mnemonic
- Socket protocol: Length-prefixed framing (4-byte BE) must be compatible
- HTTP: Same minimal HTTP/1.1 POST-only parser behavior
- Abstract socket names:
@nsigner_<word1>_<word2>format preserved
Open Questions
-
BIP-32 derivation:
nostr_core_lib_rustnips::nip006::keypair_from_seeddoes 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
secp256k1crate directly?
- (a) Add proper BIP-32 to
-
PQ crypto: Should we FFI to PQClean C code initially, or find/use Rust PQ crates?
-
TUI scope: Full TUI parity with C version (role wizard, transport menu, approval prompts, connection display), or start with a simpler CLI?